From 094f34b6718f12a11041373be4b23eb7660b7f00 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:28:13 -0600 Subject: [PATCH] v1.11.1 (#566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Temporary merge for 1.11.1 syncing (#543) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge confli… * feat: add configurable base path for reverse proxy subpath deployment (#542) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) … * fix: sort file manager entries alphabetically with directories first (#541) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (… * fix: add timeout for SFTP readlink to prevent request hang (#540) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * f… * feat: add copy deploy command for SSH keys without password auth (#538) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503… * fix: preserve all ssh_data fields in database export/import (#537) * fix: remote translations * feat: support OIDC configuration via environment variables (#531) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: support OIDC configuration via environment variables Add support for configuring OIDC authentication through environment variables, enabling containerized deployments without database access: - OIDC_CLIENT_ID - OIDC_CLIENT_SECRET - OIDC_ISSUER_URL - OIDC_AUTHORIZATION_URL - OIDC_TOKEN_URL - OIDC_USERINFO_URL (optional) - OIDC_IDENTIFIER_PATH (optional, default: "sub") - OIDC_NAME_PATH (optional, default: "name") - OIDC_SCOPES (optional, default: "openid email profile") Environment variables take priority over database configuration. Closes Termix-SSH/Support#16 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: add modern DH group KEX algorithms for better compatibility (#530) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * fix: resolve merge conflict artifacts in dev-1.10.1 - Fix missing closing tags in AppView.tsx NetworkGraphView - Fix incomplete catch blocks in server-stats.ts and db/index.ts - Fix missing closing brace in en.json ports section - Fix HostManagerApp.tsx import path - Fix stats-widgets.ts type definition - Fix schema.ts networkTopology table definition - Add type annotations in user-data-import.ts * feat: support URL routes to open terminal directly (#156) - Add /terminal/{hostNameOrId} route for new format - Keep /hosts/{id}/terminal for backward compatibility - Smart detection: numeric IDs for ID lookup, otherwise name lookup - Clean URL after opening to prevent duplicate on refresh - Show toast error when host not found --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501) Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal. This adds Ctrl+Alt+ as an alternative that sends Ctrl+. - Ctrl+Alt+W → Ctrl+W (nano search, delete word) - Ctrl+Alt+T → Ctrl+T (transpose chars) - Ctrl+Alt+N → Ctrl+N (next line) - Ctrl+Alt+Q → Ctrl+Q (XON flow control) Fixes Termix-SSH/Support#407 * feat: remove locales * New Crowdin updates (#504) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * feat: add option to disable update checker (#502) * feat: add option to disable update checker Add a new setting in User Profile > Settings to disable automatic update checking on startup and dashboard. - Adds 'Disable Update Check' toggle in profile settings - Skips GitHub API calls when disabled (reduces network requests) - Works for both web app and Electron client Fixes Termix-SSH/Support#410 * feat: remove locales --------- Co-authored-by: LukeGus * New Crowdin updates (#505) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * feat: add crowdin i18n * feat: remove locales * New Crowdin updates (#506) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * New translations en.json (Norwegian) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Arabic) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Swedish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Finnish) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Bulgarian) * New translations en.json (Indonesian) * New translations en.json (Hindi) * New translations en.json (Bengali) * New translations en.json (Thai) * feat: update readme * feat: update readme * feat: update credential editor to use submitting system and add health monitor * feat: added toggle for command pallete * feat: added close button on tab dropdown * feat: added sidebar management and improved some host manager UI/UX * feat: re-added missing users.ts route from merge * feat: add toggle for password reset feature in admin settings (#508) * feat: add sudo support for file manager operations (#509) * fix: add sudo support for listFiles and improve permission error handling (#512) * feat: add sudo support for file manager operations * fix: add sudo support for listFiles and improve permission error handling --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated * feat: add copy password button and fixed new line carriage issues and backend crash for auth key * feat: added quick connection system (ad-hoc) * Enter Key for Quick Login (#513) * feat: added -r and -l support for tunnels * feat: begin dashboard overhaul by splitting into cards and adding customization * feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc. * feat: add auth.tsx suppot for fullscreen * feat: greatly improve network graph ui/ux and migrated to use translations and theme system * feat: update to use blacksmith * feat: improve ui for customized tabs and hide add/edit host/credential when submiting * feat: add warpgate support with a dialog (terminal only) * feat: expand warpgate to docker/file manager * fix: docker not working wtih warpgate and none auth failing for terminal * fix: prevent owner permission loss when sharing host to own role (#514) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * fix: prevent owner permission loss when sharing host to own role Fixes #391 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: SSH key passphrase not passed for Docker and Tunnel (#521) * perf: optimize Host Manager for large host lists - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. * fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel Database field is `key_password` but code used `keyPassword`. Added fallback to check both field names. Affected: - docker.ts: Docker SSH connections with encrypted keys - tunnel.ts: Tunnel connections with encrypted keys --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * perf: optimize Host Manager for large host lists (#520) - Add ServerStatusContext for shared status polling (reduces API calls from N to 1) - Move TooltipProvider to component root (eliminates N context instances) - Add pagination with "Show More" button (limits initial DOM nodes per folder) Fixes performance issues when managing ~1000 hosts with status monitoring enabled. Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing mimeTypes definition for image preview (#518) Fixes Termix-SSH/Support#408 * fix: prevent session reset when updating host properties (#517) Move WebSocket cleanup logic to a separate unmount-only effect to prevent SSH sessions from being closed when host properties are updated. Closes Termix-SSH/Support#401 * fix: backend type error * feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385) * feat: improved conneciton log ui/logic * feat: improved conneciton log ui/logic * feat: expanded connection log to work across all components (readying for release) * feat: update readme * feat: update readme * fix: build error * fix: build error * fix: changed ver * chore: clean up * chore: continue clean up * fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies * fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field * fix: update readme and run cleaner * fix: update readme * fix: update readme * fix: update readme * feat: update chinese readme * feat: add modern DH group KEX algorithms for better compatibility Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms which are supported by ssh2 library but were not configured in Termix. These algorithms provide: - Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.) - Stronger security with larger DH groups - RFC 8268 compliance Related to Termix-SSH/Support#205 --------- Co-authored-by: Steven Josefs Co-authored-by: LukeGus Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> * feat: clarify that hostname/FQDN is supported in IP address field (#529) * Feature request network graph * Fixing PR442: - Fixed: - UI design elemets - UI and button colors - JSON export - recent activity is default again - Removed: - Online/Offline UI labels - left-click menu on hosts - Added: - small pulsing dot inside the hosts to indicate online status like in the left bar * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker * Handle enter button (#481) * Update Crowdin configuration file * Update Crowdin configuration file * Update Linux Portable section with AUR link (#474) * fix: file manager incorrectly decoding/encoding when editing files (#476) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: build error on docker (#477) * fix: electron build errors and skip macos job * fix: testflight submit failure * fix: made submit job match build type * fix: resolve Vite build warnings for mixed static/dynamic imports (#473) * Update Crowdin configuration file * Update Crowdin configuration file * fix: resolve Vite build warnings for mixed static/dynamic imports - Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent) * fix: build error on docker --------- Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude * Increase max old space size for npm builds * Increase Node.js memory limit in Dockerfile * Remove NODE_OPTIONS from build commands in Dockerfile * Change runner to blacksmith-4vcpu-ubuntu-2404 * fix: build error on docker * Add handle on enter button; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Gaylord Julien Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: LukeGus * fix: remove top tech * fix: update readme * fix: prevent long container names from overflowing card (#496) Added min-w-0 to CardTitle to allow text truncation in flexbox. Without this, flex items have min-width: auto which prevents the truncate class from working properly. Fixes #411 * fix: use SFTP readdir for file listing to support non-Linux systems (#495) The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 * fix: restore SSH connection timeout to 120s for 2FA authentication (#494) The timeout was reduced from 120s to 30s in v1.10, causing 2FA login failures. Users with keyboard-interactive authentication (TOTP/2FA) need sufficient time to enter their verification codes before the SSH connection times out. Fixes #404 * feat: add Docker container healthcheck (#493) * fix: owner should not be marked as shared when host is shared to their role (#492) * fix: use correct MIME types for image preview (#491) * fix: prevent session reset when updating host properties (#490) * fix: add shell creation timeout and improve error handling (#489) * fix: set default lineHeight to 1.0 for TUI apps compatibility (#488) * fix: delete all related data when removing user (#487) * fix: nginx permission denied on restricted kernels (#486) * fix: skip existing hosts and credentials during JSON import (#485) Added duplicate detection for SSH hosts (by ip+port+username) and credentials (by name) during import. Existing items are now skipped by default, or updated if replaceExisting option is enabled. This matches the existing behavior of importDismissedAlerts. Fixes #389 * feat: add firewall status widget for server stats (#484) * Feature: PWA (#479) * feat: add PWA support with offline capabilities - Add web app manifest with icons and theme configuration - Add service worker with cache-first strategy for static assets - Add useServiceWorker hook for SW registration - Add PWA meta tags and Apple-specific tags to index.html - Update vite.config.ts for optimal asset caching * Update package-lock.json * New Crowdin updates (#472) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Chinese Simplified) * New translations en.json (English) * New translations en.json (Vietnamese) * New translations en.json (German) * feat: add listening ports widget for server stats (#483) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: fix network stats merge and add openapi jsdocs comments * feat: add workflow/config to auto generate openapi json * feat: remove locales * feat: support URL routes to open terminal directly (#156) (#503) * … * fix: change minmax lower bound to 540px from 600px for gridTemplateColumns (#532) * feat: update/standardize the icon * feat: update mobile terminal ui to support all new features * fix: increment ver * feat: begin opkssh implementation by adding auth type * feat: finalize opkssh based auth * fix: update docker/file manager to reflect need for Terminal * fix: opkssh issues * fix: ssh.ts build error * feat: improve logging verbosity (is that a word?) * fix: remove debug logging * feat: add host key verification * feat: consolidate host manager tabs * feat: allow no password in a credential and fix opkssh issues * Remove macOS-specific keyboard handling (#544) * fix: translation with region (#535) * fix: translation with region * fix: update `LanguageSwitcher.tsx` and `i18n.ts` * chore: add temp locales to fix build error * feat: add opkssh docs button adn update default config * feat: improve opkssh reliability and docker support * feat: add remote redirect uri support * feat: continued opkssh docker support * feat: continued opkssh docker support * feat: continued opkssh docker support * feat: continued opkssh docker support * feat: continued opkssh docker support * feat: continued opkssh docker support * fix: disable browser caching for file manager API responses (#549) The file manager API endpoint (/ssh/file_manager/ssh/) did not set any Cache-Control headers. Browsers applied heuristic caching to GET requests (e.g. listFiles), causing stale file listings to be returned after creating, deleting, or renaming files. Users had to clear cookies and re-login to see updated file lists. Add Cache-Control: no-store, no-cache, must-revalidate to both nginx.conf and nginx-https.conf for the file manager API location to ensure browsers always fetch fresh data from the server. Fixes: Termix-SSH/Support#455 * fix: prevent data loss from encrypted database format mismatch (#548) * fix: isEncryptedDatabaseFile() fallback to single-file detection when .meta is invalid Previously, if a stale or corrupted .meta file existed alongside a valid single-file format encrypted database, isEncryptedDatabaseFile() would return false without attempting single-file format detection. This could cause the initialization flow to treat a valid encrypted database as non-existent, leading to creation of an empty database that would then overwrite the original encrypted data on the next periodic save cycle — resulting in permanent data loss. Now the method falls through to single-file format detection when .meta validation fails, matching the behavior of decryptDatabaseToBuffer(). Refs: Termix-SSH/Support#452 * fix: decryptDatabaseFile() support single-file encrypted format decryptDatabaseFile() previously only supported the two-file format (.encrypted + .meta), but encryptDatabaseFromBuffer() (used by the periodic 15-second save) writes single-file format and deletes .meta. This meant /database/restore via restoreFromEncryptedBackup() would fail on any database that had been saved by the periodic save cycle. Refactored to delegate to decryptDatabaseToBuffer() which already handles both formats correctly, eliminating the duplicated decryption logic and the format mismatch. Refs: Termix-SSH/Support#452 * fix: force save after database import to prevent data loss The /database/import endpoint wrote imported data to the in-memory SQLite database but did not trigger an immediate persist to the encrypted file on disk. Data persistence relied solely on the 15-second periodic save interval. If the container crashed or was restarted within that window, all imported data would be lost. This was the likely cause of users reporting that imported data "flashed briefly then disappeared." Now calls DatabaseSaveTrigger.forceSave() immediately after a successful import to ensure data is written to disk before returning the response to the client. Refs: Termix-SSH/Support#452 * fix: getEncryptedFileInfo() support single-file encrypted format getEncryptedFileInfo() only read metadata from the separate .meta file, but the periodic save (encryptDatabaseFromBuffer) writes single-file format and deletes .meta files. This caused the method to always return null for databases saved by the periodic cycle, breaking diagnostic APIs and status checks. Now tries .meta first, then falls back to reading embedded metadata from the single-file format header, consistent with the detection logic in isEncryptedDatabaseFile(). Refs: Termix-SSH/Support#452 * feat: file manager disconnecting and added toggle for snippet confirmation * feat: puid/pgid support * fix: opkssh timesouts and dockerfile error * fix: build error * fix: opkssh auth not loading properly * fix: show opkssh auth failed logs if failed * chore: clean up files * fix: accidental line paste * fix: remove translatiosn for merge * New Crowdin updates (#551) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Traditional) * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Bulgarian) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Bengali) * New translations en.json (Thai) * New translations en.json (Hindi) * Delete src/locales/translated directory * New translations en.json (Chinese Simplified) * New translations en.json (Chinese Traditional) * New translations en.json (Romanian) * New translations en.json (French) * New translations en.json (Spanish) * New translations en.json (Afrikaans) * New translations en.json (Arabic) * New translations en.json (Bulgarian) * New translations en.json (Catalan) * New translations en.json (Czech) * New translations en.json (Danish) * New translations en.json (German) * New translations en.json (Greek) * New translations en.json (Finnish) * New translations en.json (Hebrew) * New translations en.json (Hungarian) * New translations en.json (Italian) * New translations en.json (Japanese) * New translations en.json (Korean) * New translations en.json (Dutch) * New translations en.json (Norwegian) * New translations en.json (Polish) * New translations en.json (Portuguese) * New translations en.json (Russian) * New translations en.json (Serbian (Cyrillic)) * New translations en.json (Swedish) * New translations en.json (Turkish) * New translations en.json (Ukrainian) * New translations en.json (Vietnamese) * New translations en.json (Portuguese, Brazilian) * New translations en.json (Indonesian) * New translations en.json (Bengali) * New translations en.json (Thai) * New translations en.json (Hindi) * fix: updated i18n config to support new file naming scheme for crowdin * fix: host/credential enter key not working and saving overriden username * feat: update readme * fix: credential in server stats error and browser refresh and client terminal size issue * feat: add HTTP proxy support for GitHub API requests (#554) * feat: add HTTP proxy support for GitHub API requests * Remove comments from getProxyAgent function Removed documentation comments for the getProxyAgent function. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: desktop app random logouts * fix: constant logouts * fix: constant logouts * fix: unlinked credentials after ujpdating --------- Co-authored-by: ZacharyZcR Co-authored-by: Steven Josefs Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com> Co-authored-by: Termix CI Co-authored-by: Claude Co-authored-by: Nunzio Marfè Co-authored-by: Gaylord Julien Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com> Co-authored-by: Deepansh Khurana Co-authored-by: Dylan Ysmal Co-authored-by: TomyJan Co-authored-by: ZacharyZcR --- README.md | 4 +- docker/Dockerfile | 9 +- docker/entrypoint.sh | 38 +- docker/nginx-https.conf | 28 + docker/nginx.conf | 29 +- index.html | 2 +- package-lock.json | 6 +- package.json | 5 +- public/favicon.ico | Bin 171947 -> 361102 bytes public/full-icon.png | Bin 0 -> 37523 bytes public/icon.ico | Bin 361102 -> 361102 bytes public/icon.png | Bin 16007 -> 75161 bytes public/icon.svg | 30 +- public/icons/1024x1024.png | Bin 56926 -> 48762 bytes public/icons/128x128.png | Bin 4072 -> 3400 bytes public/icons/16x16.png | Bin 483 -> 447 bytes public/icons/24x24.png | Bin 749 -> 671 bytes public/icons/256x256.png | Bin 8597 -> 7543 bytes public/icons/32x32.png | Bin 986 -> 896 bytes public/icons/48x48.png | Bin 1554 -> 1317 bytes public/icons/512x512.png | Bin 20523 -> 18539 bytes public/icons/64x64.png | Bin 1989 -> 1748 bytes public/icons/icon.icns | Bin 125606 -> 109591 bytes public/icons/icon.ico | Bin 361102 -> 361102 bytes public/manifest.json | 14 +- repo-images/YouTube.png | Bin 0 -> 152817 bytes src/backend/dashboard.ts | 10 +- src/backend/database/database.ts | 105 +- src/backend/database/db/index.ts | 108 +- src/backend/database/db/schema.ts | 33 +- src/backend/database/routes/alerts.ts | 2 + src/backend/database/routes/credentials.ts | 60 +- .../database/routes/network-topology.ts | 11 +- src/backend/database/routes/rbac.ts | 41 +- src/backend/database/routes/snippets.ts | 37 +- src/backend/database/routes/ssh.ts | 926 +++++- src/backend/database/routes/terminal.ts | 7 +- src/backend/database/routes/users.ts | 197 +- src/backend/ssh/auth-manager.ts | 12 +- src/backend/ssh/docker-console.ts | 58 +- src/backend/ssh/docker.ts | 416 ++- src/backend/ssh/file-manager.ts | 335 ++- src/backend/ssh/host-key-verifier.ts | 471 +++ src/backend/ssh/opkssh-auth.ts | 803 +++++ src/backend/ssh/server-stats.ts | 123 +- src/backend/ssh/terminal.ts | 389 ++- src/backend/ssh/tunnel.ts | 91 +- src/backend/starter.ts | 41 + src/backend/swagger.ts | 13 +- src/backend/utils/auth-manager.ts | 13 +- src/backend/utils/database-file-encryption.ts | 113 +- src/backend/utils/field-crypto.ts | 6 + src/backend/utils/opkssh-binary-manager.ts | 226 ++ src/backend/utils/proxy-agent.ts | 32 + src/backend/utils/user-crypto.ts | 24 +- src/hooks/use-service-worker.ts | 5 +- src/i18n/i18n.ts | 98 +- src/lib/base-path.ts | 5 + src/lib/db-health-monitor.ts | 60 +- src/locales/en.json | 47 +- .../translated/{af.json => af_ZA.json} | 0 .../translated/{ar.json => ar_SA.json} | 0 .../translated/{bg.json => bg_BG.json} | 0 .../translated/{bn.json => bn_BD.json} | 0 .../translated/{ca.json => ca_ES.json} | 0 .../translated/{cs.json => cs_CZ.json} | 0 .../translated/{da.json => da_DK.json} | 0 .../translated/{de.json => de_DE.json} | 0 .../translated/{el.json => el_GR.json} | 0 src/locales/translated/en.json | 2665 ----------------- .../translated/{es.json => es_ES.json} | 0 .../translated/{fi.json => fi_FI.json} | 0 .../translated/{fr.json => fr_FR.json} | 0 .../translated/{he.json => he_IL.json} | 0 .../translated/{hi.json => hi_IN.json} | 0 .../translated/{hu.json => hu_HU.json} | 0 .../translated/{id.json => id_ID.json} | 0 .../translated/{it.json => it_IT.json} | 0 .../translated/{ja.json => ja_JP.json} | 0 .../translated/{ko.json => ko_KR.json} | 0 .../translated/{nl.json => nl_NL.json} | 0 .../translated/{no.json => no_NO.json} | 0 .../translated/{pl.json => pl_PL.json} | 0 .../translated/{pt.json => pt_BR.json} | 0 src/locales/translated/pt_PT.json | 2665 +++++++++++++++++ .../translated/{ro.json => ro_RO.json} | 0 .../translated/{ru.json => ru_RU.json} | 0 .../translated/{sr.json => sr_SP.json} | 0 .../translated/{sv.json => sv_SE.json} | 0 .../translated/{th.json => th_TH.json} | 0 .../translated/{tr.json => tr_TR.json} | 0 .../translated/{uk.json => uk_UA.json} | 0 .../translated/{vi.json => vi_VN.json} | 0 src/locales/translated/zh_CN.json | 2665 +++++++++++++++++ .../translated/{zh.json => zh_TW.json} | 604 ++-- src/types/index.ts | 15 +- src/ui/desktop/DesktopApp.tsx | 9 +- .../apps/admin/tabs/DatabaseSecurityTab.tsx | 5 +- src/ui/desktop/apps/dashboard/Dashboard.tsx | 42 +- .../apps/dashboard/cards/NetworkGraphCard.tsx | 1 - .../dashboard/cards/ServerOverviewCard.tsx | 26 +- .../docker/components/ConsoleTerminal.tsx | 3 +- .../features/file-manager/FileManager.tsx | 25 +- .../apps/features/terminal/Terminal.tsx | 338 ++- .../credentials/CredentialEditor.tsx | 29 +- .../credentials/CredentialsManager.tsx | 242 +- .../apps/host-manager/hosts/HostManager.tsx | 211 +- .../host-manager/hosts/HostManagerEditor.tsx | 81 +- .../host-manager/hosts/HostManagerViewer.tsx | 47 +- .../hosts/tabs/HostGeneralTab.tsx | 105 +- src/ui/desktop/apps/tools/SSHToolsSidebar.tsx | 19 +- .../dialogs/HostKeyVerificationDialog.tsx | 217 ++ .../navigation/dialogs/OPKSSHDialog.tsx | 138 + src/ui/desktop/navigation/hosts/Host.tsx | 3 +- src/ui/desktop/user/LanguageSwitcher.tsx | 14 +- src/ui/desktop/user/UserProfile.tsx | 24 + src/ui/hooks/useCommandTracker.ts | 2 +- src/ui/main-axios.ts | 9 +- src/ui/mobile/apps/terminal/Terminal.tsx | 923 +++++- vite.config.ts | 2 +- 120 files changed, 12316 insertions(+), 3896 deletions(-) create mode 100644 public/full-icon.png create mode 100644 repo-images/YouTube.png create mode 100644 src/backend/ssh/host-key-verifier.ts create mode 100644 src/backend/ssh/opkssh-auth.ts create mode 100644 src/backend/utils/opkssh-binary-manager.ts create mode 100644 src/backend/utils/proxy-agent.ts create mode 100644 src/lib/base-path.ts rename src/locales/translated/{af.json => af_ZA.json} (100%) rename src/locales/translated/{ar.json => ar_SA.json} (100%) rename src/locales/translated/{bg.json => bg_BG.json} (100%) rename src/locales/translated/{bn.json => bn_BD.json} (100%) rename src/locales/translated/{ca.json => ca_ES.json} (100%) rename src/locales/translated/{cs.json => cs_CZ.json} (100%) rename src/locales/translated/{da.json => da_DK.json} (100%) rename src/locales/translated/{de.json => de_DE.json} (100%) rename src/locales/translated/{el.json => el_GR.json} (100%) delete mode 100644 src/locales/translated/en.json rename src/locales/translated/{es.json => es_ES.json} (100%) rename src/locales/translated/{fi.json => fi_FI.json} (100%) rename src/locales/translated/{fr.json => fr_FR.json} (100%) rename src/locales/translated/{he.json => he_IL.json} (100%) rename src/locales/translated/{hi.json => hi_IN.json} (100%) rename src/locales/translated/{hu.json => hu_HU.json} (100%) rename src/locales/translated/{id.json => id_ID.json} (100%) rename src/locales/translated/{it.json => it_IT.json} (100%) rename src/locales/translated/{ja.json => ja_JP.json} (100%) rename src/locales/translated/{ko.json => ko_KR.json} (100%) rename src/locales/translated/{nl.json => nl_NL.json} (100%) rename src/locales/translated/{no.json => no_NO.json} (100%) rename src/locales/translated/{pl.json => pl_PL.json} (100%) rename src/locales/translated/{pt.json => pt_BR.json} (100%) create mode 100644 src/locales/translated/pt_PT.json rename src/locales/translated/{ro.json => ro_RO.json} (100%) rename src/locales/translated/{ru.json => ru_RU.json} (100%) rename src/locales/translated/{sr.json => sr_SP.json} (100%) rename src/locales/translated/{sv.json => sv_SE.json} (100%) rename src/locales/translated/{th.json => th_TH.json} (100%) rename src/locales/translated/{tr.json => tr_TR.json} (100%) rename src/locales/translated/{uk.json => uk_UA.json} (100%) rename src/locales/translated/{vi.json => vi_VN.json} (100%) create mode 100644 src/locales/translated/zh_CN.json rename src/locales/translated/{zh.json => zh_TW.json} (89%) create mode 100644 src/ui/desktop/navigation/dialogs/HostKeyVerificationDialog.tsx create mode 100644 src/ui/desktop/navigation/dialogs/OPKSSHDialog.tsx diff --git a/README.md b/README.md index 4fcf603e..58c7acd1 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ free and self-hosted alternative to Termius available for all platforms. - **Command History** - Auto-complete and view previously ran SSH commands - **Quick Connect** - Connect to a server without having to save the connection data - **Command Palette** - Double tap left shift to quickly access SSH connections with your keyboard -- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, password autofill, etc. +- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), etc. - **Network Graph** - Customize your Dashboard to visualize your homelab based off your SSH connections with status support # Planned Features @@ -136,7 +136,7 @@ channel, however, response times may be longer. # Screenshots -[![](https://utfs.io/f/nGnSqDveMsqxqiKJF5EnObSopHatulx93N0E1KVsP7hvjMfF)](https://www.youtube.com/watch?v=j1_I1mkhUkE) +[![YouTube](./repo-images/YouTube.png)](https://youtu.be/8OYCPwS7ciA)

Termix Demo 1 diff --git a/docker/Dockerfile b/docker/Dockerfile index 38869770..a77f62dc 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -51,11 +51,12 @@ ENV DATA_DIR=/app/data \ PORT=8080 \ NODE_ENV=production -RUN apt-get update && apt-get install -y nginx gettext-base openssl && \ +RUN apt-get update && apt-get install -y nginx gettext-base openssl ca-certificates gosu && \ + update-ca-certificates && \ rm -rf /var/lib/apt/lists/* && \ - mkdir -p /app/data /app/uploads /app/nginx /app/nginx/logs /app/nginx/cache /app/nginx/client_body && \ + mkdir -p /app/data /app/uploads /app/data/.opk /app/nginx /app/nginx/logs /app/nginx/cache /app/nginx/client_body && \ chown -R node:node /app && \ - chmod 755 /app/data /app/uploads /app/nginx && \ + chmod 755 /app/data /app/uploads /app/data/.opk /app/nginx && \ touch /app/nginx/nginx.conf && \ chown node:node /app/nginx/nginx.conf @@ -80,6 +81,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ COPY docker/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh -USER node - CMD ["/entrypoint.sh"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 165c9ee2..2764db5d 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,6 +1,22 @@ #!/bin/sh set -e +PUID=${PUID:-1000} +PGID=${PGID:-1000} + +if [ "$(id -u)" = "0" ]; then + echo "Setting up user permissions (PUID: $PUID, PGID: $PGID)..." + + groupmod -o -g "$PGID" node 2>/dev/null || true + usermod -o -u "$PUID" node 2>/dev/null || true + + chown -R node:node /app/data /app/uploads /app/nginx 2>/dev/null || true + + echo "User node is now UID: $PUID, GID: $PGID" + + exec gosu node:node "$0" "$@" +fi + export PORT=${PORT:-8080} export ENABLE_SSL=${ENABLE_SSL:-false} export SSL_PORT=${SSL_PORT:-8443} @@ -19,8 +35,26 @@ fi envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /app/nginx/nginx.conf -mkdir -p /app/data /app/uploads -chmod 755 /app/data /app/uploads 2>/dev/null || true +mkdir -p /app/data /app/uploads /app/data/.opk +chmod 755 /app/data /app/uploads /app/data/.opk 2>/dev/null || true + +if [ -w /app/data ]; then + echo "Data directory is writable" +else + echo "WARNING: Data directory is not writable. OPKSSH may fail." + ls -ld /app/data +fi + +if [ -w /app/data/.opk ]; then + echo "OPKSSH directory is writable" +else + echo "WARNING: OPKSSH directory is not writable. OPKSSH authentication will fail." + ls -ld /app/data/.opk +fi + +if [ ! -d "/app/opkssh" ]; then + echo "WARNING: OPKSSH binary directory not found at /app/opkssh" +fi if [ "$ENABLE_SSL" = "true" ]; then echo "Checking SSL certificate configuration..." diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index 3e0b389f..13f76c65 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -213,6 +213,28 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/ssh/opkssh-chooser(/.*)?$ { + proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + 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 ~ ^/ssh/opkssh-callback(/.*)?$ { + proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + 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 /ssh/ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -228,6 +250,10 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; @@ -294,6 +320,8 @@ http { client_max_body_size 5G; client_body_timeout 300s; + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + proxy_pass http://127.0.0.1:30004; proxy_http_version 1.1; proxy_set_header Host $host; diff --git a/docker/nginx.conf b/docker/nginx.conf index 5637cb6c..a67aaf94 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -202,6 +202,28 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/ssh/opkssh-chooser(/.*)?$ { + proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + 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 ~ ^/ssh/opkssh-callback(/.*)?$ { + proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + 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 /ssh/ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -217,7 +239,10 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; proxy_set_header X-Real-IP $remote_addr; @@ -283,6 +308,8 @@ http { client_max_body_size 5G; client_body_timeout 300s; + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + proxy_pass http://127.0.0.1:30004; proxy_http_version 1.1; proxy_set_header Host $host; diff --git a/index.html b/index.html index 09531776..c3d2bf08 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - + =0.10" } diff --git a/package.json b/package.json index ec15a311..e2952faf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "termix", "private": true, - "version": "1.11.0", + "version": "1.11.1", "description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities", "author": "Karmaa", "main": "electron/main.cjs", @@ -17,6 +17,8 @@ "build": "vite build && tsc -p tsconfig.node.json", "build:backend": "tsc -p tsconfig.node.json", "dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js", + "dev:docker": "docker stop termix-dev 2>nul & docker rm termix-dev 2>nul & docker build -f docker/Dockerfile -t termix:dev --no-cache . && docker run -d --name termix-dev -p 3000:3000 -p 8080:8080 -p 30001-30006:30001-30006 -v \"%cd%\\db\\data:/app/data\" termix:dev", + "dev:docker:restart": "docker stop termix-dev 2>nul & docker rm termix-dev 2>nul & docker run -d --name termix-dev -p 8080:8080 -p 30001-30006:30001-30006 -v \"%cd%\\db\\data:/app/data\" termix:dev", "generate:openapi": "tsc -p tsconfig.node.json && node ./dist/backend/backend/swagger.js", "preview": "vite preview", "electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && electron .\"", @@ -82,6 +84,7 @@ "dotenv": "^17.2.0", "drizzle-orm": "^0.44.3", "express": "^5.1.0", + "https-proxy-agent": "^7.0.6", "i18n-auto-translation": "^2.2.3", "i18next": "^25.4.2", "i18next-browser-languagedetector": "^8.2.0", diff --git a/public/favicon.ico b/public/favicon.ico index d33a4ae57d4ecad9a1b5177271dbc47aaf6963ed..9b270324558ea386b3af9583b73d1512378d8c41 100644 GIT binary patch literal 361102 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 literal 171947 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 diff --git a/public/full-icon.png b/public/full-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bac3267a774a7a2f7814752a4f2cbaf30b5be5c0 GIT binary patch literal 37523 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 bi45=pTK=s^j=BJdfgmM$HMvSzi#PupK>l~# literal 0 HcmV?d00001 diff --git a/public/icon.ico b/public/icon.ico index 567b77b0617dec64e32bbffd17015e870da2aaf8..9b270324558ea386b3af9583b73d1512378d8c41 100644 GIT binary patch literal 361102 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 literal 361102 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 ZLVHbB)TvT&e+c~BZ(_PTW2fVl{{cRKikAQY literal 16007 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 diff --git a/public/icon.svg b/public/icon.svg index 7543df18..8ffb60cf 100644 --- a/public/icon.svg +++ b/public/icon.svg @@ -1 +1,29 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/1024x1024.png b/public/icons/1024x1024.png index 66f1cc013fdb758fea08245f21a4db2dec830091..1cfe9ddc05f09373c0973977fce49883c4c351a9 100644 GIT binary patch literal 48762 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≫a>mH9EsPXo7mHJYlKLp`CH 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 diff --git a/public/icons/128x128.png b/public/icons/128x128.png index 166c95430582c9d9f66a7dc4a6af8463fb3e12b2..250df98e7cb7947f8dda8f427ca54fc477ee481d 100644 GIT binary patch literal 3400 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 P00000NkvXXu0mjfq2awG delta 457 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

lSTn$9czy(j|~u&U;B;R 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 KOud0i-2VXUegtd) literal 8597 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 e<0mADU^Jqg0UH$f3>LLAsl4 z9uHG^x|Q7E3GzKhx%KBQzWww|0=_1S1&>rJMLjgrG)Ji*5)Kk9fIfE@{nB6Pf2^az zJ_=8@l7H?oR7ugE4G|3ZNu^TMLsCV)P{j4} z_Xq`=c=eT+`Q+n|*fuwNgL>)I5WKXA|Cbe-){7XSAmL~$Qc7AP292OojS#dYRxmSp;TZsbDNe(h?Yo*mPm+Z;|O~p0f#!|ayf($2qEx1k21>z@V`l;5u+oDK&1}wqncQ&#otCCq2J700000NkvXXu0mjf!F#W; delta 964 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 JPDHLkV1k+jY{~!t delta 1537 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%0AF ze=g!|2JMy?x_Ghm)@%IPQ9$s@hI+XMw{DHqhmZwRlAtIRp-_oY8n(Bzd|zxR!4UlqF80$=Ak1|9BDX?2EFd)S` z6%AaD4(I(K_^`rC0;Ix>QqH%J8fCynjV4Hkg3~EF$0I_?(jY7(1V22NrBreU2RtN+aF$fYcHw#A7Y)8gbr0uexI z_#b7l^Y8&VOT)j^so^v`g;^R1!YnLEPR3};BO(fG5>g_L|GRRiMg%!)i@_mvKg}f& z@{-LW$S)qwD@@)pD9i$8Ri&dO;1YQjA?GhsMO&}Kb}5mEY(xI#D*18*UfSuuJ|xly z!-A|NLKBPs6&J297F~2Is<2*|@E_KRRU`0B!1f{Qxk9j7#mCIB+*oAT!-pWmQDDBC zN7?$J1Tk@xg~U|U=-;P&UWXYIM~z}ZK%ww35WZMTi)+IR3ptS^%mHCAa)(2C;*>MF zc!QG@B5h>)A|0iCp_Wgd4!KJ?i;Rpni@@*Tg$KzS>gt@xTbz&#Q4QD5CHyu;VT^%6 zA{5~fn1G$K%EZ69u`ux9BuC zw_4f7nH8@lu=?KN#8*bzzxxNX@gm#aBcLqC_Rvf7@j?kl-!W-?TKak4U6~nPGR|q= z;0l({cy?#!lym|pN*CMwAic$%_6jomRQc<_rn z=E{*{K!@ug2j4RBPC{=zKER(}2Y9WbY1`Iwva0LfWGp^v8}Pk2B%GX_h|2Y^q$)LV zFUQ-1cfTCps>|cdihC#P9Q1>Ro+K-dlT=6KjBJQ#U4lt#u9Y+zYDGt77*@7eCL0X)S$4$11P z^M$V8&&g{qPl@#K7giD3zK~F{Aw5)n*fo;ZVO7r+N-~r;92YY;GAcnZ8T+Z2*DbcB zE0R40FI+pETDyusGOY&Q+##4O*#yUps zsOyFgK;~dacp-HvZ^62^`F3CTzMJ~Ump)!?WO?G4a$#pdwDhK>P4KV7?L-G%YD6?2 zXMqEK9aiT#qj_zx@cD#s^bZc(w}Cr+t*y$rE}$GVha%Ae;9JxWTB|ISJ+Jvu-kr5N zy*zGi%kVcv`YVq)8IT@pO3)aCg9;~Q*r5+7VnlTO?_QQUp1->BHgJXfNkJ-FvZC=t z#Bm{^l%G_fP5oDOl*H79#!4>?Y0qbF#P-IVOV3>D76%R=oy93kFg!n>OQLk>i5T5@ z(9HY{!&kT$`qaJKB-V>%nMR~E_D03kv;;A4(MQSu6&JB?1b&j5yns=z|J6>btXrU; zcz-zKZ!>Aly~35J`sC=Aw|o}L?9?*g&u@bPiy!MGvJTpfB8^CHp6OV9C+iZo-K&jD znNf5jzRpR>Z!S^C0oo}-0%rAE^rzj6yPP-E`z|lmrM~8xy6rbh-XBZ<-l3J=ASZVC;xwT}srb zywkn;f0i&gb7uS)etm$ubHywr!()P_#1{L$=>f>W0UU@lxB)yf4_Y5pz~v>2it2vnQ8F`9nL3y0 zZ6BS^Aqo^HwRCjoWdp0Y7gi-SqE&f=!_S|LcyxKs zYhC0i7*P!HidpSGQM9NPD<`k|@9d!u6>4)OT-RCP)Zz`#VKlO)`6K$%Xtr@lYBG%q z7Ev2EM#~DTt`o>-m>*x01k2Uq6L8;Z6AXObG%3JVH7mp>JdDXWDl^=JH9$>4%RNC*qPF z(Q4F6N_xD?*0IZcKL?l^!-*$P8*w`fsV_I9`ya1Zx7ezgnt>G(dD?n=C(;O3;U5Fi z_pIF^H#=x9lW-ajI5VHcqSf=3p9VQ+O7O;+Ez`Q zKt)GJtQ?8!vh8r0nv+c1U%;^VuR1%F@3*m3jf=@S{bE1qFsPmM2K-qvEeQ2EDRhK_fl5Zl)uqW!!)>>!s82vBvrJzwxmyQu5d}LH^%zS&yI`%a2oqLU*`NL zYJicM`F!yGFg%3@6{89k-8!-}LFOn5A*~dk3vfdf%-$Zp+@t;5|N}HZ0*C0|{da zbAD~qv6BuH>jMs>Fso(fu&gNaX(elk^RclvC_*SjFdBcWV6KC#hJp?4N31vuX8K&h zpHngMn39=?=00GQi@833?&YAkI(YMv>JU$RgUMhAQd|&jTGF;74U)d}@enBSbnBaq z&-J@PwJNEd_=(Cf%(%LQ#~f+YIa&2c(7IZW%Jy5F|I0-oTv`iu#HQT3AUSZJK)?GO zdnz22^X$YJXj!u`=*7)}WYBhN(wF)cA($(0LgqCysqZyaF^&9|aQ`g|1xj<3oPN$W z5rHhLmb9P0zGgHsP~q0@lnvx6i%3eYT_ZtZii36EW=90bw+!LdL9I^lxT1cD~QL1K`Itt4RNMIUjc+(KtWDtYO{RiAys^8~>uBJH_{) zm*&bbh9ahtV2SS65<)5!p1~6RpQJC!kQvn0pR&Ey$N+aRrbFmW$7UAg+B6EP>ib=% z;rOqSa9fNSicTt$|NZ5QK9hF&7tv4QO_R6~(XWwKpL(FcAJq6%SExby;^$c@+LJ24 z|I$~f;j49Z+h^%g6}~QRc&~i7OY_;Tl-U_{)=HIXpBt3%Hoev+!qg7z`>;r_A6Jb& zcfL6+EZ3}Z2+JSS+WPL1B1vN6`CLV)ks^oUx$&de`_7pLWoU2L@E@+loCW+bgjJBn zvonUDijQFN|9qz&TfflMZgsQQ6Qp02s$hRjDc1?{1{D?FQSyNgthD8t+JO620y$hN zIQvB9p{SHqu1HyJ^8Pkn3}~q^B;`xNCa>XrQT7~42oNgXdvu?Zn*n<* zX@&Mu>lvIYM7F-RvsGMssWDc2<$Kr2BqU=W2KYS|e>C0te=)72R3peo`#r7v()>F+ zKot?mEj$#FIt%tRWf#L!$*C_3n9NyEJi35)Wag3_I`4~3n%k0$5sxPU=$J{A5(I~; z1vTqZ(;ewtnwW*XFGEm)Q>WUp_^JjWCFNFgt$UJ-y)YTHF0borahZd(XO8I8zd#!iU4_q-I zNZLmixE+`>KE6?A$s28_C9k+?_u@zQo5~!`#eG(5uBo>GL|2<6>#wpU3%(ZtJRtcx zEH~yX4no~GefrxUZU7Oj6q?r-n9oP`{Lu=vJzC)=7lRwAcmkj$rnv2UlnJ})8q z=QjCDS~_s42@O-O^_EAUbD)b%d0cdbSXvkip2w!l$N25L|2C6Tt8jd_o394(G|bAq zDs{BvzOj^J*2JSS99t5*WN)U&DoaUAXKc!><>M1QIh0g}Ikes*j7T~K;6q#1FT4#KL3q(zf`hPU<-IyA{E| zz)n;mj4wj62I+zmhQ-QUvd>Zhkl(w&dD_VZu`StwRtiUssO}|fgzZuC3Z>yI_(kCO z_5<)<+nycQd?)xu6)9%p49-D%M9bsS4XI3B9pLvX<}Z}F{O{pbOO%ri)@+ug%WwdY z#+t8JrH+*x2OYeQ0N%L3ZK;m{`PNrO*Bn#Oz7BM=dIq%*fUc@o+KO!8ktR$+<->g| zauHsRH4fW%Y?2xm``K$frS~e!Xv)NwR32m@WTl-rT%h8~#GFWpW9rMLc1-Zhxt{22 z%Ah#=p|p$)p|M^~5x0-FI7KSNrObZa9oM(ppSd$p78M)lCB4N;~78GlQP{u^DQH69VGn|!6lmNv&#l_m;a<1d{$XE@66l_*O1%c~o{K1n~ix*GWz zw@zmL8u2`hh#jXWBs4+o&5x?R69bzXbE8ePX8XdLV69pL95fo@(8t=Hs0$3JNXr?c zgz@!$3ga4Fc79u&lkP^pM#@rm7kIM6rGk%y<|-mtOr41}07}9%Y}6H;CI{tzWGjh) zf*Kj^+1i?`pnuyGbFa(2$sIExPx%39rWJyJssAob)zcftXa<{_jLoEG!M}VaJK}-1J^M3dHWV=7=7QxdT31U91cpi^^*(JUpe-vE~MF z`k(FK^vreR<|xU^Y6V?tVV>~3n;E$1krWW4HUN!6h4x8ys zv-;%Kfo8}d{^-G(uAg=UUimQH127&X{8YW=bBuKb`s&6zSy|1LyD?xg@>Nfke^8Nx zLe8c^NJAVAP)_Is-1=(&$&F{d9(Z{PHRm0@vD>F1WqXERK7$l4QqCe9 z{Pdp|If6v4$;&P z?w3Qn7dVG|j<^v?g@D`4&PKJgI!mZr1WjSunlS65h(HhdiPsm(Q<9XOYlQAf-?%XCZS;OD> z?t=3NVyGpPf275^J3)y=n8oVniHxMX*9JwjoXww=XK0pGtFhz-F(NmKBJlr6A{_Iw zSiNBM;>|2~j$Ho4Jvn@)`yzjKEAi{2ARTUy&LNxd&=+t1YxDLu&M{Fm610u*LVLjM&V3GKiCEzjuquI04&wQnBN*mpzIgM=j`zusiC@AhoG@oi=xJ^9# znR_GZnrUr_%OXj43CGO=BQFjGC2Izxs=&Z|sGbVbCBcA_z5O5k{&;3G?M!#R7_bLZ zU4y#D6p|qB9?aMIMrOiwZ@BHfu_N1ALFkx!UP&w1_Lw_7PBZ*9=VKB6)}p&y*{QqT z$9WzQb4Q>9kKaB`f8xjPS2~g}Xm>|}nEFCqvu`Rz=ea)6+@M33z2Q_BQx4fU%M%e_ zz1Xow@bB+YBtIBkM#p?A54mO^5+S)oJ?qO5;YQHNl*g4o; zJ_VL;rjG9ezuh+l%)VW5c;&4czrD)YZW8-_2HJIP$NB=HNG*wYZ^p7HDkC=2|=Oj5w#1{1Fev%oA9>> zxagAGTJG-_Wt99!EPn62xgAx8Vse=$utJgKz)Vpk$?C2vqCzUEZ2#PLu<@NaY zkm}ojF@`nS8(xf0SwCwToTHo)Q=8W3F-^(xvpZ^3P)9|z6+~UM_#cr?D08q0w5l3? z_dNH?XT&+1Oy%Qr6Ws;EA%E#ye@ofmSFAv;j*PVSRy|9pDL{-Hr2VeQb_1!R#{`Mw z3}d28-!4cKevZ<7UzB?gf3zQUl36r(@~DMJ?AWC8XIreAt^Oyd@!nEgKLPFeQ==P18jOynbK_eE z$j?bM|9naF-BVjp$odgW?&rQ+$_Ya+WQwnBwHtoxKkKBbWG#MP$pe4mlQfmQvCfcm z)6dIX?*ityhc)q(OfB1|Exa#D)v9kjVT_=sC*eJ&q~!1{7+*pX8h>>0!tVht2EU(l z*&FFEB>RtE_V`@)8`-S>)9~zVS=q}{l|=mjLcUU?<->xqj@YoXIbytiGCZV6eO+UF zN{~n-c+TK?VRwX^M@Ub)_Khl0ZG(JDs)|Hv^oSsX>2W{w^&!uMI*zQj!@kdCbkUdX zS#m@yJRUA@dEW|@)0C-waIxc;Fo6p^QQxtteSd!C7qHN%-6 z>%QCErbe&U)^cbC1t{ZAM!qsvxf^0*3O1o9QfBVSR%g$l^%#RArC<(dU|ceyGKWEWMGRE!jxV}s~} ze#@%P$ouei??lB$Kf}8_z0XGZ`-ZQ_3*M-ah|aqi`47g*@>Sc;$i|VtmH2(xfZsR@ z*Udk)Xcr+)>p9WIU*7^JX!lgLaE|k1Joiz(D8jvO+s)*81;!{-(}pDS4*bWJ4rw^O zm+LS)xFg?Lx#-I!tO7@Wk`mM+bS~o8W!)aMtBxr6kI)XQQIeB0#&jo+JwKOMVEWR_ zBJKSx1rH|`ow9+^mnTO$M>dt7b}wWa^Q#xnM7al zOv;o&GVIu|z)jAr9)aN~uT$pu_e@z{-njMNe4Q$gM7P0nlyt`1F1wXKmnoJWr*EG) z7w~6~Mb;;{h!45YH;zJ`?3fZveRFW547^uZD7hftx}EIB%d@lkB$(tr>=Rwa6r9^O z2xGG)ASr#PKYcX6D=RS};RPQV$b(UkRMM#`_K((s+jz>~SC{6`-wF(PzT9-;ZgsQR zo!cqbzdMv{emZ$>Y#KJwYxCtv$$(vkA+1NxsL>)d)Mz5aPknam_~v6DDp4Gj*V~6} zc>8gDePC;|<=kX!`lf{EptJOi*c*`SDUyVRaVj+@tYKzK&P{d|KeMHK-yH$625ou8 zz4_%N+Y}Ak0XK#GDtk*btChWd3@n0nd``&-RoWH*2-|7y^i&R06OP=QeLn4PeOja) zCdb+K?BJ$h`c#zk!p~p0hO1YvnnPT#N)5&*yl)R(-4>ZFE~YJ5C8?GxHMVfa(q=Mj zeK{%rsYPy^o6T!eA&OP@^VbYjo`~Qud~^(@B%X$GKIn9m>o6d7-_vJ?pgD|s8-6rYl-4ZOUvk`Z%h^evF~pM zVT#^d;M&~M4tP+zQg%ScrJXu~A8kkg9Ma2y5*dSqQcYfZ*#(WQ+0+xHEDEDfi)4I8 zj=W}`o!A)irC9Xttc2n@KPm3|vsAE2pG?)I`ExU4OcvOOh+3=YvLi=J9b(5UBg4Yx z-UQ=G3fGd{974d6eoN+UY?}=Z(aaqY%07gV_n^A%l5t*Xy>hM*{L~^J&eQ~LYH;4v zge&*v)70zM5(BU_a$1nggDTSQSJ9LzQ{VQ81{dXK_gttle2rP<`QZUlJLECzii+0q z&SAyOuY2RJ6bRxv3aAD1sr3HAcW{fO-21oAY^o79cdWD@-1O6$VkMYPrc5!#w1qhF zx(#vwa~A`P?V(XckLu@$XWGw3^v0`A>xJ3sljNMWw!L|nR07dOZ0D66L}tYs$#Pzc z$3b|w;+X$bAWtEcQ5WJmLNo}elNv34IrRN|HK{U1sY*p3t3=Qj3!5Vyco9Gn$myi@ zO->~T+LJI(+nv|0Fjc%-0Y6S47aZI_4Z?>shlNEZ1>+s=q3zl3n8oKR9-dWfUZnInLsb(Sn z{Fiw|w5ZXJSs@ZID4AWOFxUWF{8)-4`RZ0GlL`fXT|%}mBAXo z^DU3GQw!!=>SBR!s1N{4=s;b?W(^O}{oI?ci+P`Qc3iLeaOu|?qYW%GNNNor@sS4J z;Jrd~&z<_+3r&e9ZjB_MObwc{3EUXuL=ZV}%-2*rlRsxrTi0_3^!!6w+DpHHGe++| zG1@^cb{uLeMXschUcV#r^ny#0fRS5fmFiXHq1}nV@4p9svp&>ETs4lef)wly;mCRs6!^uZ=D&x~@}NHiCqn8axC6AF6^s`UPWj1* z9J%iCqg+@KQ$0L@{rZ%W@xkcvPUMs_GA$T?QRF||tH3&{B-Vsf#G=s|f~aLNNNoQu z`|vz!nJy@@0=5h!X%3higQ$NgBh12bmz2@yKi^sVaa!lXAy$I)0K#)KL_wMV>(tn~ z>ioo~!}Sga;!A^BxI_PQiXG-x8_Yt2B)uJC4OIQ>A%goxaI2`Fe$aa%B>px_(FyCt zM9|)GE)+-GzsJ?MG8fPNKV}EQ(;X(}wFV|e867{5TQ8mv`TN)*_5D~0v}4OqVruI6 z=k6Tv`?`uM4@Qy0{KM-94eIzB1V%0(UV{oZ`~0bYE1Yp3$`m*B4A7|)5c~J4g191p z$G=vuG@+t_PXo;XCMU3@Q2fL9{qOx$gSue`1BWv1sqBQ%+kad-!i=LK(p;2s-sN8O z?v)M~X7|m=w(qD~W~HO|{rAhHUmg`J>z+?_6xW-vwVlkioyB4Jwf{W={`;d2NmbA- z{7|K?PSwx6S9d=EyaQM<%087wAKITKX-0*BtnzW z$)+Dan0|)ghfjs!YkEXa!{s6tS~4=Sa-bBNhIek6^upahXUcNvm6a8^ds$RQgf1%R zcGoQE9ejF@w!yGFYm2F0S$|{eE$58+4i^eqMDixZf8$DeSPoU zr50qyjbL-~mgFNfM@8qJ!i+Tc92}Ov-x&D`wQ5Wk{ zHD6eeU_-+OSr$6JeWQ++U4N9DpD%I>>Gfz?+#6oRFpyK?hHS9p4MQ;Pf>y(sL}+9U z8vv0ouARsw>$t5g z@AZrnOuV4k)JponF;kxza8e?ijj#44eS$6>aCvqKX=do}3^)TD@Q_PvH&Cu9WbWsMKGL4DytU z<&RiDOLi?%P~wsc^6zw{Y|#ObjqzQm008gX4lPGLof$#25!Uh?zDrE!b zgFk6q)LfbDn{o41#>pk>Y&&~=BHT_Q94d#3Nw3V#+)9d^Ki7>LH=sd~5Ltb#_hMHU z#&4_5Yzl6qk68Qrvr<(!6Cy7^x*&w{u=n@hD|WWA@y_U)+105W^8Qr%{+Og~c6L3Y zw8){@Sa`wD1>`dPRV^(`?BLahb@!p!|6!;9!iwK^S8aQ9GckxLgfb!Fm&oy2m&s-M zCwHOCdiy3a8G%~*=fZb)%`QlB zLyY9aaGP-%8dR2NYxkLVm)HV>pjQ_r7RG!W5)fAL$hKSFg1*-lw9y$>DYx>jnE;_+ zCuwPOY`OH_{t>EvvwYLkDGjsj8xT)Gm@zs*l{AX%&}-_eTz>&-&k46p@x!(hUO9MR(AfZApGZ#D$LAJE|^R)d$@`E)OWP_k~-38huZ_` zu}zbiZ4zw^x>B(1|IuFlmBBVT>^q`XHh>DgLUP!pJ>b$Ykxy-Ef39z~cgE~s zJAGxfOWSpS_XPAH_CuquqEP5p(KY&^J!8wZ!9jw$Astyu6f0XW<*m6(Bq~TtHLTV; zgxtmUg1?({wt>+D4wX;3vb0^7C)Bi3B$}7TTO?g3NF;5+sr}nMl%t;|2{*UlR2z2C z980Tg$sjph*PWv;kXulYccF%N+OKYCI%uE|ddZcqzptnW4~qH#g8TZMvop7~aqjVx=XYLwFP!S)}r^wtw;BKR9xScdu>$g?^ zd~L1dbm{S7^z@YULO>#`2q@5NxXugDaRgs7qyuG|noRKzUp!S6QW;n`l#V6A2EF1& z9vHI4ezC+8f6f*Ck$3?UezD-qISpiAU-($vgH2kemSib>Go_=1^B z=ZHB8>Ns@Uz*9xOib7JUV#vaAmFd&`L?gD&&K7TaXH(lG3yLmdRn)_g3*msbY(e}A z>fsimnq8G(w7h%!Q%KvMoaT+`kNIhBlC|}eiX?7Q_W(UiY==<(a+8AK@pg8K7y9A1 z>4wpN#9&|d@7lfFaQEyy+=f@({~$AuCNe5tf2w22P&4_vyRBA^VO4P1A36>5XEW=} z|M+!m@m(fCeVK8Cd%ggrC4^Nz4xPa^3;q5WP&zW=Cp#_kbLV)#eN)G!Xy584!EHJZ zJmCt@*BLite^SX%aGCrHk1+X&PH+3c*h0J)a}e8$>}+_4!GU7O)+uCahqA*)d~|OI zGSH!nQO~4zBr&mKyFEL*#~YqU+a3faT?hvPO|Z@He}CL>FDWVE^(K?Q^ivhq z@VBwEo8gt5HVl2HV`=&7G16Ftb`)&BlGOzrJn(#gtB1!_;hT5*n0+OK#DMASs!?E`+;<|4a(~4?&a|E@;XkWFb4pzb?U5Uu!0CR!lo$E zK}m>Y;Lz;)B6=Db6|YDFMIfLJbv#s8x0~M9)^>ksYRac`Xv+fK`@$|shT*0LPgBOc z$Mj_^9it*i5DdN%>x#5J3??$>>8WLPp@7b$H-?F6HQQ|(8jl61VO25D%qxRMc?>KHiOhn^k|HBMx-F+{GSM?A1VTFLho!QX8UrMRZwq3l2E=_B0i0Ppz- zODti~iKN10;eY^N@ti5m4T)6^uiOJS*!As;k7H6gQmg3ISe^UF@$m-H@DCrE&Oz5O~gP4pZ{dZm6R6~P=16AlF2&{9D zn0xTX&L=sUSuoyaHVWt#z-Sm}K>~OENND9D*>j#)mFu)_m8^$~?4fX@w|lFpDdfPy zXZ!bb{|TJQH_`U7=H})Vpxea8aM%iqo3#dALLYCCjV*@UyT~jAf<;Vt_#xwSLsuMsnQDR!xt8`V}T~nxvN**)OetCFD}KuB%9dG*wApRv$sc2zuyMO%sxGL=guA1 zJiVRpl$A%1UrE?taVFG?(6VZIwOksmafS_&V9?#Pw!&uRKcX(H1XSeuP z4;Nh3Hmj&VO~*g$FDzUt(9_e~FP@p8SqYN{qz(DAY4ARPKZ)`+Cq!T6vA;-;rOfxbz-jx?Lme`=^>CqFf2ttbo zRc^$``9y18W%-!L9kJ7%YaO4+D6DcTj}D>js3+3(rIi2t@FhIA@?x~I8eU$01lvzZ zB!1;a?^77W~I)|v{8Dh-1uVv)xXWS7= zhH;Gy{l|ty+u@BR=_Vk#0$baxtcuo1S&zl)jlEcSqt}KaN$i|3ghQs^AK&CNC%w5~ z+X8PMf>s;bWTZuH=V77+_V9&57SN0wCLE)~39kvF!dOLncYU9RCh>!;#Kf7v`g6kT zX+!=S((>K&9)Xz5%`=}sMw0jaG<3Vol9H46RKcT}9ZQj=?mrn9=TqQWIW!_pwiqs) zB32v>k@+at?5?@crrGq=)c$CF{UGoC;zyDG=wS0pJ5kbFfp;w}$MkQgFJ3GL?ZA_? zN$R_s$cw>iu;c{=1Z1Wcj*Zlx!>lJ#Dq3J^pviN3Bt0c%_arJ<`Yss+K90t+@9uj5 z>sRF$*60p6r>jO6PVXpL1GJo5NY2UO?L4czwZ$-QO)_eVxdU8_5dHpGA<96$KL8C} z2XA_f{4Pva9hKM#D~xVR;5#nu^ZR?AycjeTr@wi&?-OZp3P6kh+x*#?kNNVlK-Jds z=g)gx9C}t7>>Ir7<{aWiqAR4`hm7wgZKrn-t{$|4 z``mi-F5b1{^iG1X;|$)k@B}VFVZ>C~b)`oj7{**j-aohkq%?@V8=&_l6J(L_|bxpr*{KS?{A*1s%klJVT2eXETtl*6px$Psi~nfNyD<=3>({e{}isr9Z0cdJg>3=H}*tyElwD;{Zx2O_XKpHZx9k zyA#Kc9XtM@S@_uy?EVnKi4a0WL`38*a?VxLF-2HOS z`NgKG|McqBD;M2;9elx^%j@fFiyUCan2B53?amQ1?f^SxMkl4Bb;~WMPMX;VL$LRN zoesf{0fm_bAedPIj4ieW5fRx}d<)>)00*+WH}0&v=UI1u)15PeLI0(>x!DC^;O^`0 jKIiV|*Vot97$g5Lf58|72!EqA00000NkvXXu0mjfm9ArB diff --git a/public/icons/64x64.png b/public/icons/64x64.png index 13da126a1e7c259cef7e76d6af5bf8e919105dba..9fcfda6b89f869de0b2eba893f8fd10fc141a8bc 100644 GIT binary patch delta 1733 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 PV-(N9GzKfL?@BD7mk+$*;b1y~9rjIkmJnfvw5{ zmW8vdYFqZU3ijGui)%k=?tR3(kWfPaZhkbRRklEB zkShLuq8wL4=n(g)kyj)gE_L2_b~+6(a-vUo>RG_NQ%$m@YHx>6=V&X2jUWwrBPeO% ztlYI)`8LUERjwX|((>}pL2tH%_tp#cK7O9*_*FBu^YEgK)j03+o3^J(V*D)$Qyw2Z zsvRmVy8OlD6ZX;0n1*&nw0IoD1RI)*o=}Cu8~R9^<-J~Oz2JTs%L#C@@OhDx`Q^(cFJGQ6dZ1nF^Z$<4wAcrU^*B-Cc?48|!;ADG z=F{4`-q7bChTt1h!fe%@6cDLThLkLv7$?FmBtpL!7-9A1fJ5OJ2R|@Lvw9 zpXsj~AK4h4$^SJ2yxsq|tvIN;IPfpasP813(eq;Zk1mhQi_9weXytiJ0(4LdgMBCq z4$#it&sJTB}!cyV!oou8vXqzzBHf%Kbgxg&S~aDsywT=O@KT;+eM_4teJXWJ1lG(J(}{)it;4UDjW1cHc!Wy(rAOevamdKf-rQBfQ1l=4Zx#%;u)W9T}@H zLCWd-lno*};z(fA`y5|np`_Ud3XD`Z=irjYyheV$cFQviGi~a=sdAd2x>h`Pek7~nXiRYPH%jDQFZvOXT;vG zx?PUPVy}Gv=BNBg*<0y&^LO*d_OBy>9R@NP;6vX@M8Ko;pHhgcCLn5PqHO@+yqRtY zxM>KIf;)OgJn1O(zYhW>$ZE3>vLOGq=Z{Ub2)l2Q{QylbHLiVWaQ#SqukBf&9r3`{ z>7m>WujG>MWTwreU3;W#UNkVKX;W>nvXnJ8Iigwj{}ZjuDRu)^SSg-^(} zr4n^n07qaoyStVXo6_!X4Ci#ui9KYqfQm5C)Tg()sCPIVD?PZp4TO^?QQ&(Ssb5^0 z>vraX%D~}znX$mfL7O9PeARwLO~qv1bP zor?*GLXr36M*7BGS6ZZ)L_kpN>l{@czvY*1jLnSgZA%xwSw$6FzHU0Q_tW1B+{CNf zosvXJ6-UVUVc>$s4#WTU_fG1>if&cWNrdcP>B0+}e8b zd*2~f*B`9fqW<0T*NPHo0W|n4_e#~RZ`L50y8N9Km7ZkMq?9q8^u#f~2jK{`MuWGL#(O+GNt#*%E{tKUgARRsTl`+vI+r5AojHjPHJJDT5) zM>qL2aoTEv=8S9t%RjF#v6eYp;~xjE0IYtTGXDEBH?BHh>m(DC0XPHH&^LW-QaJ|+ z)Dkzn3B5jlCx8FP;4`LiuKh1$JLCbzu1^A{_(v4`J}*+cRO#J1@vmK0APAztfKN`U z6&FuB%K*fUS>x@Wd;CDHnymBf!*Di8$EHjtL@NTT3uDcxD1^j z-~4+U5Qu4|m$Lv?xahIY590A~Ep#BDD@<5SR3x=eUxESR@9#hSXI@fcd-kZtpKKb=UC?CbAWIj@iFkdFX!loc`sn+dD+dHL$qo#on%D(^*sUuyQix|&wBxq~m1 zg7(FCan} zR8a9hc*3a}m+FVFIrRfy$k)Bp1KL0p(A5_>U!^32rgMq4Hc9aVh6=|nXeJSh9=QD; z9UL2*+1(KuQuEx|Ot~Alb4uy$>yLju_Zk2Pk_IJjvJF7+@O>GNl?euLL{q0I?9QXK zudNDgg7XK!F9mJZ<#i(NE}s7`BNWiPJ7X!n@(*;xb5m=h`13MI^R!gmY*}OfiH{|;Gdmrg$H2i~rNvI$+ zj2encrZL~*NVtm>>pOh1jbhvIo7?w4r)W;?aA-`ti_@5xZo52O`|3>jEW5A*#ufSNtt`x6vJsTM%jvU;Tl)qN%d8@?kk(l?!dbLl`q2 zM2aj<0gbMU0q9lRi)HI>-}_s6W=O`UtNlg7WqL4#J5(3~N5%t^gNRZuMmZzYZNCsg zP-`rQ*ZM=(ANSnqjO_4!n(~T&w9bGwx&u0w_On~9g0_+&z|E=jY34IB`VO@$yo&He0Wm?~% zF7ydpQrHDG;83lPnWz0tT3z@abqr62!BI387Wwp?_pwil^z&bY0E7T-%*&=Wu|Rh~ zz^3*mF9PexAxfo)Adw*=9%n!F99?W=TO`!srW4ESSfpkx3!&dkpl6X1Wp4a z{*hQ49UL8<2E=m`+*4`&6%AIt;|p}RJ$A^2?b+sbd;? z{`U9R=Z`Hegamp}bh5Jwaol{cw9a540p95gz7PR}!Aj1~6`bDu+MssZ%je_IF7;1e zgn*nJ^;HsdJVLF`LB}4l`GpYmk${Dwqud4mnk|1^8rAU8*p9q*r@l_=P`XM$_}R6?OmN)-NyY#0pN$psdodG?x& zTc`Di9jL@=>Ww+bEw)9&S?XTULc_JBEB%#Yb7MpPS%SyQnZx*-3{870g_crAy2keC zs}^q!JTx@LHs7H`^rqfvxB4*_79wt`dRO!iiIdJ%ouh-FkulZ1ofH(opKK~#*=;dd zJvnLrc5~i!7kvAAWtZv5D)>6I?CN9_DQLL^@v7p;9cU@EZIZ3 zVb+FmA{tM2Z>cBkfx}MGrp)hfCa&734)r^$>U(pVJ5IYlBQ(nfuUFmqb!=~SYh&Q1 zu1H9-slG&Qrhr8LTE}@-X87b4xRL6Vc!`9GWUrg?Q|qjWEWwFaDNVmc16_k(Nd3HQ zP$6t%R!+c#>YGIFw3V&^`RR7tuFe{6giM3%cr9(f238zhop(V>c>no3euA=>gddLAUi^Fe+;qyL|(mA~d zd~4$>Qpb0lV>B&#_9ibZ6!z?T;dg#GS!DP z-F-`a$c)-|PA$zj{SIV)89vaZx5)B7L8$Uc=yFYc4FA@l`{#>Fpp#x*t@iLB=B9M# z0~Li(2`Y;pnfSU1R;BDj=6`fC^+JV!jb>*~_wvFxM}IOf;aIw9omzI_ZLOV6*jL7m zr-!banJD7A_gY#Di)`@E*yNE;Sm1;FwN012g>i?M7>04qaKP^77He&JfK4yYrx{_(Jwl_wS@RCGha^!7JtXo z;e156bJvTk{?#=0l$ULiy)fED7FQolb9lbe1z>9{CXjqacsdMseT&4{4gM!i6>dgR zni|c;Z7VgZYg?|{8J1oFh#EHNt%kmACYVj$XZ>`}NZv;T$OGt?$TEj?o+n$b^K~`) zJGRX24o=uN;*e`zvXx50?wN@&yFwlZ8z|4Xb54LS zeruI7AoTMx%jS;d=A`BJH)#t+^OLiXb7UrEv?&zAi9=wKCiTlNBA>}`hAK|Yk#;6h zH&-x=x%3O=_6xnT6pP?&{8aS8OJ37nA{&g)tNToL>^wdZO^s$B^Sa(6%Q|-0*@t89 zNYNZe|DVf}6PwT?CacAYC4xm(_~T$8#+xVDTn(BPQ&p z!!?DF&meWRO-B~H$3}9;MhLGSny_%2&@rY4H>o5@AEj^NCvZg4$s?V=fY*HE=v&)! zZF)NU>oyFAA7pj zL`%r#2xCyile;z9cl~M6a;Gd`FdE0eP37)oZTQ z$e#~3B}}Ls&RB1jU3_M*@T_SxJ*{a*e2}&8w@45b~J^a306*ysViU;1?})X5hPcJ@QyEK3(=-E?8ean!om`*EE$Q zW+vQX4!Yc~a2qRT84q!MW0TI(&{EnZTb~&_&_LQ*qWt+zS67O?DYbH0^-qS-L<@AW zgDL>$qxZu{(l!p6xB@xEb3yx}W2erQG|v8+wFuYwl!D}CrHYQYQnAZJ7vJAfu(iZ- zD{woL`Dv!suX}P@f9`=qKJIaw^9?*Imub>F2kDEa!0`?Im?$qovkk5{+B3vM*);a_ zf^kHlfcp)jpbCS%5grhy?ru8I4?j{84-bcaQ5M64RKpnFRE{6NUoiU zIC46CBg<4%lMu8WygCx@fzyINhF&p;vo`V5x9*O~Tzic5Jj^x%j8Q>%>g)C5A4_lQA{h1pq!hYuHoE{4wjvIPcPe!$VH*du5CZvjk1+)SF*IzEJreT@c4Z zhyah0yES!y7lxekJEuLdD_2O@Umg8g#D}=@Al|oK-Pb5Rfy8UV_8+Dl4nO;L%=E3A zEu8}Qb{{IJ!qIP;8Pv=`&nPoCDal6{`HojWAtZD=ocQ`Wtg4>D9xp2%R#i^F!Lqx& zQ5w^usre+p3%@lfKM%-ckJMC^{nz@JLO&qVIPK>D1%xSWDYEjJlJ$2_ni$xgbS0fm zNqsGEB;-SYO{gSTg+Q9)$=M=NGoQXi26&PF-hB7dI;h@FdSy#;^J~o?;1?hA)yd&& z5qjh4oO6jR!H73>5XS#b*Oj8D!{$aGZ8{kIT}uA;&Z)N}_Sz9)8!@5B(6-~Pk^-y; z=xl9BTY#LrOHr>y+ONjmjHf>LLK2KpZO!0Oa8AxV7C9?JJ21f9{Ghl zX8Fx+x%g#eLvCJeW z4x1p2e`7;6wLu^J37jESRco4S$nCuh`TofD7(+#VW4Hbz$z;Z84Omg?(=2qvT zm7{uVOVJGX5F9_rCgUTVNKSNuH1$>sRbU3YP+`w>)Yn9I=mBJJEq;BDbGMJVuCmFk zHR_pXg{IiLV@aV6p5xZO4WMucJ0rLD#c5}=NPkH_p}JQ&nxpyZ7U$~Jo~R9)z8y2k zjO{)05_gJ1<#%DYcL4V103~o!0GzLJ*s(P{m`=oIA!E_e!Q(v+tBsn7CcZ|?pJxMr z1)t3ctjX38>I&Klu9E9qe)02t&6=+NpLaZ^nh`o-$cbovKAA{)XoB=%2xb-{ZG|k= z)O`j4xvU~k>ZVk|!R{QSLE&I83FTB>G*PzShkZ*Th=dl&d=us%9*hg;gVR2_HSw$* zvbW#AOQ0l3=iO?d1B~sfvY=72aI)Bg)pv*XrjyrKFoB-IW5a->0KPWShgxj6aYW*I z*FaH4&_F7kzar!iPo>sy>bn>Q_uJuhwFW^>ajVz!0=i-2}-LnMK z%{GZ!iExBMbC zu+BKhpSP|y^aU&KSs~APmh<$i;Dk(qGzM`6$$0M8%|ehUD>f;+%f)tL8Ji8~Kpkrg z`Ww4GC;Kv^(}ZrH49+cI%S#SS0zcxId+Ujo1VPrhIiA8~C>(um{F^v5N?yd03~@~7 zLWT$rm?3kn)2rV8RG#oH)alP}hCu(Qy2{4oC8S$XrypaR&5urc!CT<%5LD0wvYCmb z^I2OrJVS*5Ao&25N~E312mbZ}{!Y;<3NvMNWL4wEJkfA(BrUf%oR$iUha+_8pwVkw z{3dMoFuNi|6+Q{g>!lK%ITK0`*K}*vUir+qEd#b(qNH7ZZDg z@qDO7q^lhGBx&G)*N%CC8xA-vCgk*`)&I9S|^<7V1h4(jthpy zxEt+<&yE5}p?n9&UW=!da#`-}Bm4zMI)?cZm@5SFNF<3wGv61gKp7NbX!Q4HPM}08 zRO)!3Dr#eb%b6oH9rBlSr;jKZo1bHZjO(eln&yc1u}j4~u_t-*Kiww8zk5$^!f z>S}a{FLV0K1DX+jk04u*hl0|4uwqdf8)uONwPoAcIi0hp{%#=!0LmSw`B<$;dDI-V zwf+xFDDpb=M{ovN$Y@}NxN}o`>kqO{Ru=@hKOpKTT28mxh>wSKWz!#o@!Tp%)9#M* z`nD%oS<EVYRA{Ia$9gQCOeSPX9n*d~;&Q8RfO*>D^xlN8iGAYupVF4|&m^HahYS z6bs>HAVN0n3Oe$zg*)!x{b%98Nm^^lf|GBJuH$4m%;%C_GZ_*ud)w*JF*PXh zwfJOoFa*j!9)gSxRu|&eP^N-L%YTntC6@~dB4E1jUA>%`%-CSQ^?c*xeWqc$c>~b* zZcReb0}IJkzKq>^Er$!f#k&^{?D>iw!<-^S+TFqM>DOA<#TTKeLU#nuY zQFEec1X%^X<6)kRQ+f7Bj+oY|i1yLD2M~hZ+y|}dk;Mrgvy$g%n;+v4a93kL4aQ<= znZ?{1Z6;Y{7Rb%nd-giwHKLf|Bb;H0KqS*0-8l~+#U-6j;pU!_i))SUvnMQdvTUROzD z(}!6mHbbi3qSu{pZfL@OwWfiKsh_q2$S*}(#|jGxP<$c$zV!-5*I#mts0wMzWhPO; zf_dthk26Bg*6ZwSQZ^SQ1egK5Y?%FdXZgfuBXY}iVqTDqf%QD=J$ECr@=@fzr{)yD zfQ9!*rk!=2(k+VA%1@mgxkV@(UyAM_F0;eskJ3=lw*gIwf!N!irUpqW>4nyAg+PPY zQQ2u{clRXJ)rj1b?%EY(45fQQ7DgX{ft4``0Kb?ter?wZiv$Wxy-E2@uZW+b8WB#D zAoEy`hqnU80`u;Ds;k@E5m!Q2C;R?ip{z1^QyIF>SW)O)Sjb<|sDgk|4s2T-L9^{N zD1ehH5YZ63m`jqRPRr|JPV>S3??(~<`=D?ABgtE3gQ^lYemTH5P@b^<-f4DI5QOa5 z*KAD-E?W7cgCuEl%;F;tB4C~dT5NXY2aV6Uu`9W@=>xG`J?B%INvBER9+DHysvxk? zNGGl2g3I$t1xV^8KqXd~BwIFKjC1t;$>6b2tmx^?$+;Ak?I8{Q-%{(|(w3Ut8hwOK zJNHm7$AkJv+>=rM_wBu*a3-Q3qJ`xPl822El?>J8`kkviZG7_Qo_aRuBE~G_c0Oyk z2gQt8HoSJfIf+x*SIYF~c$`6~y5Xh5EP#~tGhG*VCg+0$^DzD-n&^u}C6aj02U{qM ziRZopAT`hZ_0$dZ@mGf`G>>$Fz63}LEg=IL*%!1`+1d_sAAn!+bKt5vLUVMs;-_x& zRwfa^C6=zgZy!*NT=V2BJcQF`Ge2K=T4jkf=&n8hy&{1?djf#ey7CCkfo30DKM%ZH=~&tTfBy!o_k8-RmONaRcaJXsr*+@cy)M_&TeDNpZWxm|HeaMdr7Q zGdM|`mVZTV=NHWfN+c9GYe$X;y~XcXT`F>p7xXQ#NKY4rBBuJz)<{XuiBE!30^B%+ zo(b!|NR-@NoJ-q+Y~n4SCm++^Ooj-Hyq>I`#B|GmV~w5!j#TJ$*udj&uY5pXzCBIL zsTXZgKMX)i5Ds-j9#q&JKN_ZnClM);@H{kZq*+1bv3uhP!u>Q!OQe<+0Xv3=%AZGv zGnYg7K@(qB>99G%v;9pVsN{4%pw1XP@G~trb@m@~AKp?bAA2-c6+_Q2Oz;sCA=S|Z zSKD~ahPXHfg^!s}04bzM6r5;W?uh&Ww1U`mM)UKyP+@@C|Kd@ShR27AD9+#-F6h~x z$jO)G8R^1W-$)IfsJW{obO{|co+?oK9IQo3U`Xhvs=;P04-JkYTmgG1q9Q41*kd{% zb?iIYDuEV;1d)(ZpB8t!^z`^|kbAKa@gS-@usGgmh=h_lcc_c%4T&_jWFo|mk9n3%H1SgPn@NgG`YE0E= zWG(**$(?ofK(~n9QJ(FFM^%;d_nDzD+0WH;|0}w#G)hyxy*7y@N(Sea=ftL9kZV|7Wsasfm(Rlz zVKlibo5{Cc{2(75XVab)iN#`qVIvuf1N;S1qZvpF9)u-GSSWT^@dEv`PS!74!&acR zoLjbRIP&}_kY0s5pXWG^hrCPh1g-YLqYmfM9)+iX3(LP_Ya(%euq2;hp*TDS9Qn&j z!Pe7z+MXj5P8<%j*SnH2`6`7LShY=VQY){#JAX3_3DvU#3sh0>A3)T8kt)WcI0+Qc z^2*-9#b&tAgA2MyXfQTYUcQ?FLIG{bRct0-5N&9DBTuLRnjmy_N?8&zP! zPO z#XpOp?x2A(M_rr&tYlpPC<<$Iyj2H?Tqi}V^I?x{{V{*13dVV58}%-yZF@3xgC`cQnDvMUwS!Dx`F-gIc6S3yr#xNE_AL zPu%V12_J4n!Nova%sJukc=8#DFpjYT+Apx{oD%30v3u*x@DQZdPEq?VTg;VF2DNu*JEJ6q!3jt;Wy548qwGHzuS&jTFoyQ9A7(LwuZ)JSd@33=USzYm3Y5qxe1AAp7ao*2#i_apHrZ~@$>G~?9QMM@19 z3Pl09V}NvmHSj|i83mXv6!6YI;dPbTLH=>HDTO(g(jn(d5HQJL5@eeB|CMbj(Y9(x z0gT^!p~~k9Qcf-8nuityHRpsHZ+t?>%YD?F38>3pOtjzQZ?}Y0Rbv6tKl3y{Db5H~ z?J6{w47j)KhwSS}ATgTWgM;Jk!ND~HMj34{duq8bcoI5l4TVLf(jlPd*`^;12z5S3 zJv%)-)UaGMyS(%*uCJ+0)Z#oYo(^)D$^CRX*PNUzm5)2rv6J~Y8Nqb`-a(+EzV_0x zn+I2PuD_4Ps!^DHVc?*1w@f{QdOiIQt!KeirwTN$OoFTenrRHfP!?AW;H8ZBK|cw? z1oW(r2$DE%B(AuV`9SL4IS-mvH!Cn)^YUq==k={02wi+R$w|fqlVqe|sA(vqIr6IL zAx}U-%21#u0a>r=PZQmoqRi;|aE!iqUU6ly?yC-r425ByC~rC2b0(SB!6*x8py9%W z{0G_=t@B=cJR^MlNmQGo!LpGJnG-XdaU^Q38wNAyeR=yKnk%yqXa2y!$T;z53L9aVD0nx%ur>?GXU&S-;P*!4G zBTLvYBf;S11KG}m^*;z>u04?I0Dt<(xMgdw^(P)EGw+UMnPy%GRjn+$;h=fk(WWV9 zXMj9I@h=flom7yGjRl$+A_Xr}i*-M}?TV7=4Y5uGs%+ZmSI~e(eKoLCr|N`e<9P** z(7|+wCYIB$4Ws2-RJizTY#hgf)em3G5(L8}{MI-bym2Z*s;VR=*UUu9hH;-#%!vWO z7&Yf4EF1ZVslgE(vafIPp5vgzL*J2PB*^d+lrs>%M;Et;(g&=O{WDFKDuJ#{K%x%} z(iygDTw=W!47<$4yN0&(Xr>BOG*%f{K5N6YKz-y_sPJL>}yAj9Z&FFw+)YpZSwZL#xeP z#XvWJ#m?e|0^FfJ?J5wOMgxX^d_g_*KZ0W*mkW0P#;=3XNL_&O)+} zu&WI8|E3kMOF&M=ATcz8%jr{46VD?;t?BVcGk^?5Hmcb$x3fJM$olr`A+46UMva++ z!}5RHMBM{|L65@?Z{P-l)44=>sz5JP8-g%8_4_N^)K{)~r}xRrufVK8!dMrKEr8~8 zT@du5z0BP7fNlavZ~}I=GT9oJtd{K5{+~<1TGC4pAUZj9__T?lI&<~Z{!zBs|J3KYD%xZukqCgm7tZgC&pI_Z>*X^X8t-ca`e{A1Eo54Y(Iw2B z1B?xIbo}W9U3G%|Q-E$2=o5Cc)wWO^W{C_r$3z=}Jp;*e&%K>qRk-v9n3yf(ozv9F z2@Gug`M#i6CRT#!D9MENOGr4hyiw)A&RI{uey;+BGfA~axs+c_4Gl|#gI*Q|D0)5H zv4|r3N~$Cqs4+l*(;RJHKCuEw&CLiK3nEg=xaRTbrUO&Uf!9}5p5%XHjuA0 zG-1`tV8>f~>pOXMj`JN8Q?JFzT!fK?sv@&`0`CQxc7)g$5E9mvKjbgaBG`%}vU7{C?l`ZAB3%rs7dk~que66})xIDK{Jp$E?N?Wt_F z$Awu!X8|q*c#yzUU3K0BUqsEuE2i=s@R#B%0NX$D**KcZw$i4EfXM1oT};@XQZvHG z2D5>~jx2YNjf%dIiMmY>M6y&tH84E`%KT9vC#{6IxBHRl?LnLXJ*C1pj&JupN3z>V zkeJ9uKdMFNQoRa50Xjh2Ko?~}Goh4xv=;_*0(K4$XRQ$Y`NMMpbOWAx!{*~4=|TS6 z6NW7sjY3^YZr=)Ksv;re3&BYUZu~$45sZO0zaXZ5oSXMKJf8xR|73-X5tslRre|Z+ z)&VqjpClGPqqQ_kSU3&=I}U`iX}0i?hYx6V)uLytS}VwZW6z1Bz3#-bVtM1B(b~G1 zVerGPVjmzM_>qZMq&r?**xjnx+F1JX#VQf0o%mhtbW@CKhNpKzaLkEUq9hoRefbxz zw*1VZf^$w&KQlX1?0NUR5~T+)sFt9FjePc1&d(6DqwX{D-oMD}U!5u*Z#5CLXqbiOmIAgVS@>D7NF)_ifP&r{d-Gq%Pa0~33^#L$La7ySEBDrt3fekI&cY$=y-F{oixGLo|zy87BHWawIaPI$=i z0Yj7KgvL<^aJ>@^6(2Bk4vI13Dy=(h++QKH{-KzCF7Q|M8=uJaeq;zYPtHpeQg{Eh zhGc=Q-vq8n*nwczfkp&Ks_LH)DPH8yYz`+~`v;3pwZJI~{I z&@P7zo&4sT*n4ii_wsJ>U4gx;e8u--LC$h0-)J3e0}N>+&c`edBY`!d3lVKh6?{=* z1{HM}w~lMp?Q^=foS7M%`Sxc}Xl;$XPeu%INBJXWQGLK1SlE{#744LOyoJ^%MPvL}6NfZtlG7lNb>}04=a)eBoE0u&J zWk|wZ`{?((-yh!}_xZki|G4{6PuY9Fd%bI3>wVv~n01D_6F+V<)3z!0UfHCa-4ZF8 z%2paEkq{-nc_fj=h$?1`2jz~nrl8V`)7JZ0erjrWD?MN#GN@X!{GL^!l*Xl#Wn{43 zXI+KG$ebkx_Q19L<%IK*uw&v`R~gI%8%r%*Vl9)MEuBTgo!=P3am>bWb!`N&SGIHp zT|Y3L9TiEDDnQG8J7$cLoFhq%Qag3u)d+QEduIB>9kZ-UU(W>lptn{zu13VO?Hi^d zV82tyz_thj=9h|3t*c;{d7RTxZPKh*iv6!(#+XM5te(5q@ubQ>0-Dgy8AXfa+LGKo3+o_F@<`u)6g;3Wetg| zM^Z*@Q=51Cnx8XYc^FA>9og_+_J#towtn{cWugf0xs6D{(Lx8+3y-d1A4OvIi&D5| zPuuFxHQ?S7*d-UGvS%>Q8ejTdzomqys)>B!r1D(qxtWm~`HN{kJ{|SLM{x;3&-k+| zUsrwYcqtUToYdIByL>cxc)Dzx)e{xOwqql?2JLL>$XJ3cZ)uxqm zr?LmP7np(gE3Q-*0CuB^J3eUM#Wo{zGgiQhvAHH?&Gjzg+e6`_iV+_vcqSX)!l@tW zww+$a2AD^Kb?o$#2nc|+f(k0c~zAgbdJ?1CKmg|Nme6HN#z1yDCp|V`4(k#7(%<@+`uJC8Nl}=R5Ee7mV3LGoJBss(!yo)feE9cgWLPc#@=+kxryXoi zee4>FuZ&6#4lcHyHJ7H{m)YAcm&noyjYNB64$f^3OGb2uE<1ewQ&$d~=CcDO9X`L< z^nG~BH@0%YN+Qi$;Rxycp=4Do>A37CRwFgm3>x7?vjmUR&Z+w0Mk1WEoy@eBGcM1r zC$GGtE()x7I$laqgr)epZ$@mV zVs(uT`jr@F7}iDfO^xLNK=X61G7}kBzYiUo9#?q|r4*r&JZBml)Qb2!sCsQ_+#I$Z z+c%FSbwKo_aAagb-kem@V(|;`+XWE^X#pvHcUfb;_e<$-FUdM6J^wn6F7mYnO#~7= zZ)TX~UOmhk^&~g4G*@bCAGJxph;|V_cg5?|zT?X|_Z)k>xAbZogf~XqeL(r?Q}KbW zUJQlP!_G&oPNWXWJ^eL8+Q;3s=JogItmBon@2r>Y@NeRoPunR*L^Vv0yIl;;oGbEh z$q+6}C_|L610862)j}E?IX1IL)MA>KL}uUKRvJh@AD|EF-EP|L*|VOnH%zZ!w3Zau zh1sJXImte6mfEsFY4ms8GkCO3XCwZ)_;{^qlH+1ZAn4$&`_IC47Ar~_U#Hs<7uaS|Hut0 zRy+8xBM0x$=19@Hsv_ufmLr%>6HDEd*F{~Qys~;|_D+iT<%h0eyOe_yw)!HcJyVAQ z_OXgU!47 zElEV&YTL3s(|Gkpxus+ZqqmRG%H-@T!{32}a>nL+;P3!Ab47VJM#c3UeV4&=kj|RG zC^vNeo~G{dwbftFiDageagu)_1EqeKhYSqlOvt@PPtes+esKQ&!+H+I?`CDNz8*oPPe>6gm*;RCfZDgw)I<1Zdb zrztCuC~csi1dLL$tmpjGw{ZH9fRTkSd+zl5yI{f9_B$RSguCu*e@osncv-9yEjcf1WKxMe(gU zSBaCSpM|a{^MtSR9e$fN2i46#WHt1G9%Fsm^6L=P)D;xqD89EA@>0s1J(dxTASyE- zLDUm9DIi~1Q1ZuF*`ZQylWNXK2|lbCUUf3QM6*Z3abNc-P3FG5*BGvYuQS}5k{$02 z|J2PHOOY@KvYM*Mzi3z@R+iwA0lKR+A+>vCgbhCSspG8zRI2~jwYD(Xc=b-(R%YAB z@59FdLVhNlyfQ^)yB~JuD|Y6jnhv2g;5yJ*DqHfE(U;MSQGwBv+PXkq8!4gqls)c% z-cKUmQjN-|Y3#E$-gSkJ8rE^!{Iw2WRxAsYBG~{Yc_{_;)p4=+YL*{<+1{9$oplWB z09I}cSfhbMx!fRSAOAK5DhB@)oS2x>Do*{X9h_J!5q28Hj`~3cuwM~szvMTA;S}NhhW_D1=dFC82g<>KY zkfPG0j(Jm&Q@&s2EUWCk!kKMg-z>efX%MK(SHAodl5kM$0~QC4vl4&rt>7I?j-2A; z$pic5vwuZio$S*Q(?|Z#t=wf9>C?agac>LPx?XR1#xA!r5<(x(Vq;raya6v{PnA$X zImeo@+m-&_mwc|SeRr$0Z+Wr%muHu7XJNGQ+9p%DYX0(RnUu}2w;w<9nIjG#*inqC z*-NArEd@UYYdh#Mf$=6#4KRw4j!}D@W^nYx>$FQpIrbe^%3t=0M zSMi`Kv$w4T_-WWCyrc75m`|tP2pyt^42m#OHtD2F~ z+oRx`kQ@}T*KCV~`cvV5SZY%3f;Q~W{Qa;$Vr{{3rRQ$Mb5NhbTT3;Xl!%Y-?L#kG zmb)hy((>Tmud!f-Uw=5YR22^%fbD_LX$L;v5L$6kSGHv~wCLE`Kf)YcBCMm1+PZAs zDRtXh!K1SKSb2}4N5xvOV*1W(oCLTTfHfMHm;^L|B?}Fi~7czvwsRNhB zfSH8T2f#V_7mJdDyp$ZgP81{io~Y*Ch*Er?d-EWuyz%7?cgIEMyB6*$QITwBGy$C2 z(c2u!L6_P0gFJIQUtcHbJ2+?jP&qi6{<7WdyTXmlR}sHYS32Um0M+heC1PDyU28b3 zs!l3fBCa@Ydk8|czce&8ZIP6G-);zG#)m!Qhxjo^S-_{a`@lgzDTV__^n~C`l7;9iM z#KjrwD+H(CkQdY{Hzia%f%(aT-wuBAUCWLX`OPonOY0B+dfyEQJ#^qmT%<&aL@pI7 zl<%CTG8Y;`~z5&IMoLzBS{mnK*C{T4l}q))9N zvd)Z=%q3&hRAXl8a1r$Bg&7rw|GXU#8nr&+E}z${8ROJ27YC6YZ(f-Zx%^e)QqA8b z$K8yG&9sP*(4FP$QbXUn4X#?|KcKnV zk6VsmuR)68m|idDWk`)WsS1$ftvwDZqZP0@^m*y*C+PAy0~2V~K4bG7 zqw$iGYOp&|;FA}Hv!sSbBt^9Q3EUcbX01S}`4B&5NRBT7s)XLW`FPX|xL;#$g}U}X1Gk2WJy1y)?oU2iqK z-mJYbcIN8E^G6R!v7yQix|1Uy&!j~&FzJLX}Vym7Ec1~vBznVw}14!ax(Hsd~zflOX3qw4ZT@im%@8>}BGg*W?Z$GSy1{T(anNW-zx#R zXfO(JSsJ}R{s=NlD0OC-^Dnv;clAlfqzyL6211VHS*Q*YhMgGj%#(3SHDfNQK3t3v z95AW%u61|S*zZ>{;E&!bo*L0FRg;E|eBodNR*zSm^)@r7S|)X>z^0S>^(TfK6U7kK z#e*%zIf-VMFC~tPC4omt1w}|H1l>=3vw13?Inba5i?4gtzsGpIK)a+=r{N+Q3Uc)r zs=4D|>49UoKKu{?$&ul%1$fOXK{0Imc)`UHqvBiaMCa8Nc->)(GV?q3RQ^xpJq|?m zj^&Uh`MyUOHZJfwiTf$$u5q5&|7kwq@3r&Olf~h!hFd?$mM-N+e$ZL+3)6eQA&gY# zVPIlfJ%Q_0YEE^HgZ1RSvYyLGLO^J-5DXPcJ?iW6pP);_a@M~*1SZX{jK6r?uUdHh z0K0Hf2^#~{hgG9E*Dv#)c)EEcj)AdXrkj?O_`paO0RhW?XH?I!U>P8da^caJ$ZXhm ze)fzlGd&xaz1o@O0SGU-tDM9V<&2Mg<6O_9VF7Uwq%^yCtT}bj+uzUrW(Uc$$Boes z0>3jPk+Y1mtU&U1j}LC``!v7=y!k%2Q-@jy*$J?CAemQo?0aZ4n;a_+9sOFLf3Mo! z7bJ&RmRp@;2GtlrjTfl3dusMs01vGMojYT9FYs%v2!9fF7%8CW6(V%&gYF3RK?+ox5%Qy)#6e`Z z`?^C({to2rxRM>;dIZuB49aVp42hQtqnss6XwETk_*F1c3~PF+r-2l^SMVX$1fzNb zvJnfKksXZW=29%22fN}G+2bHPQ@mAc2#47w1hOCZLnfSRsPqsGgCjF46q3cyeQ5?7 zTt@=^?B7w?c%hP__t2#ehNh@)!&(aLxy;EJ1x$(K0`GOKTo=3nIPj+UXmfvsa4kEM_Z=Yz zi|Bq5(ZUd_S}_>eYozTlWgG*4B=6bPtn^F%tM=Oa0VGnTt;OG<=^V`AWU5F>e^LJ+ z9bzDZ)uuC;Hx749XF$diEb$pN*60#8I2_NT#&qIFrJ;>N9>$MOM#Sc@>wZ!(cn3Bh zo8>u_MiK@2S%e=S$K9X<>oR$|ApD6TgeT8})s$^bBTepWUJ{yNTIZsKEHnhiwY2=2 zWVq*#eOB!adjzpnRDGoC=%F|SC|0n6AP6k1nQYU!8rd7!tBF9i%6Z0; z94eQM3UO$#aO8R2sj$}eA`!o39JuI}dYA!kt1;C+_oL1k{WYvhJ*SpV8hYd`4PGYZ zLyFE&%{biT;ywoL4jz{EtmqELQWTZ_!Fmi=O{KtDs64u-OR8FdkNOHp>iaMQtY|!s z4Z5NOe#Y{NIPf#_g@7BBZ5RzLkOZb@Dnv<(RB&cQSLQ|~m3;y&BO@f&z4l7BEv~(j zlHDpr^YT(sl)>ter{)497wzX8RGXb=mQp}_)5qP^gVY%ncKq^?0;E^5u)Lp?1ZCTs zUCWmdi<#8nyQe2d4PS!s6;xBB`9n#5D=O$IzYkYZ1HA|2LT~D=x}udG82d)trLmO9 z#HfxjtiPa%d9D9vWWc7M5(hHkwpX-gY@rH>b|D!?5oD-_eD8i08>U}TLA8QJf=Am- z{*)50PjCDtnYr11%Axc7oF(<4!m9=>%41Uzehe27_-{GplV<@QZF5+_(UD@ks>tBT z)xUlHF0eG-H}30^Rd0i%hpO3~#CDK+AxXSv{Z7o|6D$2|84~yfUjI|tKG}e1M}Ep< zklX6gxJ9>eB_0wKklKn+zwWOFYi7^nMYyYY$*4^ryG0SIPL&*olDUC;lo7`ep5xui zY44@;>C~QV+<;)`IxQ2b_9&>cagor^7em1`c58dpvOCG<4eRt$2qG!s1EOk76;w6W zhBL{uuW4nNVo;>lYvzD?X3_uH{iwt${ZzfiZodOnTm{;7-O->eX zIU0TgrRc+z=d4-INaKk&w!I_p;yME#wKU^BLbI#FLi!pPPMz#ulasL=gl|ki)Wg+K z3d|w({L4HH8;3tno&7nD4XWn#;ejQVjvD#s63XU+3~(b$G#E7n?Y(LlT=*%PN78$h zK)-^^CR?)%%YLsagV30RA;YhN@acD z6d?IEBGl9xd38?Zpn>uW<7hQLog(S{^=(*}`OV7ms(?li)NGa$TR#O@{pVO%K81B3 z;GANwpU<&D&9ETkq!LAGZ*)w@S@JxiPM%wT19%YtgBRKht+iJd{wy*WBrIO3eLBTj zeaywjphBybvl~3+iha#9X8>2g-p*oj8igeikMvrF7O6zBP|Df-T+uh%r}7Ip|2wFa zha`1P*fBxe?oaG!UMJB{V+rfs+#${UK0NO(_r-6^uXx`%q~H!cGqGflIoPMeFFTLd z%2C(6hpVNPNl8WgKPZh-3dy`$-6%~+uWl1L*5 zs{lVrT~B%;xpmo{1FSV-3NflNvAC8on;&*(92gHAp#do`zvub<9&9&-w=~V~@ij|; zI~_iAk541%zfC~4@9%jDio(xN9#ZHqI*NuR7OM46f_zjb&S6XjZ_^I=3)t3xb)(@! z6gT1e89Ld}8rB)ue*8D8x=pRe$t%^*UOP?Y#5qmAy#kz2(;Gnq6G}7@SG|l@BR6G5 z*$9V^4%G3AYUC{g(63o$7)UU`f|wQ~sl$tkXK;-hNnWyk_1MQ1wu0=*L)y8Q1VZac zw5A~FTI^CqA{$nW|3;H(z{bNE;oXaxBZQuF+uTg=p`EF}G>;f;e9Bs#WI z1l`Bd*X~pb9gB-36u2?%&BB2SxfiRcBMSLP&Hb*zHLU|1TpUsVY0L2di810LVe;si z^=1;il5GCHOtX(FD=K;RvT zbJ4GnBMKx*7=<}4)>%X*QJ`J-+wVVdgfA|#QtwaeJx0Ww;aVzT@Tz0z$e{CLGMY0~ zw&AZHFMxXq3Xg=aDoFl~EfgU;cW3--X*)FcgK#V;+31moGcAbN`s^e{*cge5uY|Yr zmpO6#0{=kEECJ9GQg~Tv^=;BXjha&S^ShsjNA11qioB}}5i1aYf%(`#AbRE`Wav7u-v+(Z;&tjifI<7^I<`{}<5NiIAR}{iq`z`Pxcl&LG>oa%Q0f(v=%~ayb2NuzBOx& z?$z%BQuiR5St~h-lEp+@^c&uZr0Gt?kRdsAMDUPp39|PzHDwmC1m3!Hn(ylFjl`$^ z2l!W`pa-cTqj`>!L%je>ka@46NBzMe>N{!l7?_MPcQurud z)AD2WNlFWs%tRuF*2^6cuo6H5KwT+27v&#jU+}Q$XDd4DLL{$u;l~lF|{n( zg0LrwMEC$@xDSr7AQ<)&-LwV#l;NY_Y_(_P|DjI^c5iHD3&Q$(5)yGCioAk~FNuQs zKlc$;eu##VhCc<2C=~}kWrh5of0QLHbrW_DQn0|jBmNGHG(Y>ND!_F+S%P5fg{Z-G zH!^z0NVAu-AmMUH=Me)3dQ_=-1Z7Z=yO8XCXM{<1Yf~o=;?PO~TNBuZA&nI1Wsi*_ zEo@#-2j#RP$jB$iry2X27K1RRy&gm=+s_1H5f4`Ka{8<#EFYir(x8Yx8O4z4{1SRc zLZ-6}3N;pqMUVAV{npZP6$UTlv;=6Nd^A4`81v?WzHEI)V^>O?I)tjKk^m_roNOvK zr$pN3PPT1FSG6`9%mlg$SDof#feiTi`ugv`wr{{=`${=%I*DVnfd?a2EV_q1Rsu|4|EF*)Wus6U_98G59 zKdTA5+c8cU$pd_%C1PI>9GGkg%*j0IbIoOgGJNKjFfxcROB733zEmiyj?2+o@h=FA zieRbgNW_458Tf3}Kr{K&>VqvaKnBy z`0uan?cF|E*-PX1i!NcbnvYoB($1~zjXu3d6CzHL_vxNFL+aYKXIxWjsMq8p3!kq2 zi2G!mRwyp|&E=|bCEd~Ibq0DFUpG}{o8$#OPNOpjvfZvl-IF;zyBkyb9)+p3@bAsO zt+i|Cx~U!?Gd7kWv$8KGV@bfFCAtMuSoFn&)VaQxab>wZfM_)ZjWl^9tK}OEmT1z$ z=whKqu(gRv>3uN+(~6UrBJHL~`EE+G^5d+L*ef`kI3@H072*>WwM;6v(}hOwW)g+a zozyCm09|gO$0c-zb~fwrXrF(2xO7<9y zQD@Ufqr{5ibv^O~(+PGS8723Hk#?DINr|vV%D;pW0K6tF`iqIt#Z%_mO^*esWtfrs zVK>dlMkr-XnXqrZB)^jrr8eO|X22}uQ6@*d*C@L^rhuaQyInGV;VMQr1uY!Ia9>be z-9*Y%`;>9%{j!I`G<@#RFcTRo`@9%gIblr`Q*;Vrk|8XXW(xhcA7H!nb+O$VMod`b znGYs~6xE)S_T2VFI3Iw;X-MJnu;@b4++cbP*u8dp2a{>qzsnp;?`ASRW&pSmD^^FN zXwXJd&?_4mrkWilo{Z@sXw>)bqCJ8*9QQ>G zsXt)heVX<%X<4DY|9xR2HTEN;`Pb!<p*6uG3 zBBPO!#r*3H|Gz#!A-tF_5_q_p4celT%_P$RKA@BIPj=Q{b~7#%pOtt44T_5o(^l1Gek-JoYhV#Vo($awZaV0GI{5aR{2mqP$4cfj_5T^^|G%mB zzjc6g`V<08T}D>r4JBC+)(i-G_(HP2iBt>&3qpHoL5gwcWs8RXk=$P>BK;*18lG#^ zBPMHGW^32kH0c*vlndo@eW^&zsIcR-$I^5&F3I^_4%4`XG=jBS zMjlnBlN~%7IO^R}IbO<`#lZ!gYVFl*Wn??QdCD!wq-f{>_3aV5>D`}e;=B{<;s@lT ztQAP>an*gNiAI5Xc^pdOS7YAR z$lY$_{!;l%pO0Esuk@V>C__-w=mjkLTKC!|jIh%Q==MwIw1y~zORu1bv}-c%evy#B zf57}xx+8v@U((9$#5`1_WINgAWlxu5YD(qM4}?qYeC^uDopd$$Q>e)-Oo34Ssgw4? z#|%u@*4ImxQ#0#U-W(qu8+&A8^Dcw|d#umb;Jm^2)%igs*=t&ZgM-O90yZ*Wj+l`m zoQym8Z-jtJ`{3;3(Yc9$!?zBcN57)b#?6d|)c(aBEIU(GwXzM-0&HE|(yKQLN|an-2*?;ngyGY#5k zDfGkS|${GjzR z@EDm@Ixo=R{}Q&E47VWxn_E2@uB79MBK?QOTIIk0OvMe~zztK8eoxagb>#Wfkv#)+ z&VoLfbMK!qZkRRRQa;i%MpndW6vpB(=|7_4vgHQ?eg5aD8H{_se~NR$lbY!soF(Qz zpV9_U*`WO91(wvi_~TtA?D;@RY!-EJ_FWi2T<5m2v{no ztUhxM`KZIB{aT1UM1605*;#Q*YYi{>LrZJ1K3`z6P3ao-tiyiUI643y8PA~#=|3*5 z@ zn8GZeCSGBohad#*!)TISilWF=;gN(^~CI2&oj4HF_ET8^M^Z*QNGl{LIF`wAM{ABRUGhCThO-~&Dz#Hp7EYDc^k zF%y0t%YDpwubaP!Mh4!O4t)+ zapT4fPdQHAK{Km|at89JPL&?u2{U45W`=nlOqtu{RuTRtZJoM=sFkGoc@JdHH8nNh zc*@8+e*~&_b#)=#&cP>@BA%I=nTgQ9OC3THID??s2c|%;`nUSe)|R}IQW<;xq&-o> zP6?Emaj{{}NPBza+pN5VJY~HR$OF)-8Fyf)3in;jZR$F4FNvWVLvxI#{(Y3&(_fO6q zpyxKbZ46I21D@6>)py4o02frJL)RZ8?=Xv;7wz+H!e5z(=_GQil&OTSe}g?nfPq_2 zEe#n~DKYiPH=h4g9t9RMFE#TcPM+#yhc@nmzJZ~!wG*K*|o%ou-qQQ`z4Q{KvqGCz-)_UoHI|!=yn@8e3Mex?l zJU0TIZ=lNW4>y{ZTepV16<9SGT2t^nQ}V9}a?;Z9pdT;|JarDQZOF8m)e4HQS`?&h z93NP-=-V>DW3Pms})Ky|y zeBPrkTZNO0>j4Bh#LBg|Q}QBRC16aSJdx&74l;txW%m%RA)bhz$rnMd3Ib<}goK7h zXai!5U2bz8Ig)}e$K!3Yva)FSUK47kaX6A9L+G5yb-LDf?);s*caagsHB;T%9*Iyl zS0id!eEj|UbkJ#qfC-Ol*Gv$R9XTCnnLU@0k@4>NjRXEO;Ts;Vu9`$pdLTE~izRpm zs8&~3!>VvCh#@qdw1=5FFy0~cI1D4(a>is`;P=5(V z#t=MXTs}-3NujiVuL8u<2O9Y0O?t2M8Aapumog{qeF^#t>bH%Fm8Y~5j}CAJPChz& z^Uyy6XFYlHWU4t*^RQ9*a8YPT$h0TM5*Z|3xepP$kd{iiBMRTj%7J^$lJPwR;Ic{} z0Bn?Uble&r_509HzNhXAXNmYj+{(Wh?bY&%`zI#0!ekc^@st5+yKv!xlJD2sm92j~ z?fr+%k)Ry>;z@fC&b#~DuFqb7Uic`S(sE}fOqAgHoW7B z`5926;!S(#gj|znK!C-lo$no`)2G4HI~F*46mjX(aHU*Lc!K_2y<|j>Ul9GucA@`rl-#4gnhu6+jk;y z+UX{idFBT(Cr&l7*BN2F_mJ=c6t0uf|VKV%H zLITdL1JqoaX|+e*1)K7<5oQNN1~+c?jd<31sKjU*UikeiJ3AYCBJ?02nBO*@ot;$z zl0qVqMWes#&Jj1vwZIq#S4_x5oJUif?*ZSOExVRQH_5>wgUq3^u~P6? zfTI9Y6@r0j_~U3Me?LEWFxCLic$052v2FJ{qW6?QH5U~ZuPFk#5PFv{HOHA9{z!xw zG=R)JPA@Gz1{n1S3bF#NJJ`_?eYp|V9Jj{$EK1EleoT@D!J_d|LRf#{KF;G9k`bWc z6YVNd?t3uT1S6YQjjpxtqeplZ>XfG;CIvEM$sTKeR_ZX>Dz-${4Ev zb}M)B_SVnO&j%-9;KPU9%C?;l`@kvx@n^v`h$z~JuMLkkUd>MyG|niJkwpjyvjhhQ z(nNfehzXF^LhMw6^qVP|Hz|DSDtZ1~Jqrx~*|TR67Vxagh`o8!|0{t50%uUjk0klNpMUm0zAA+0|NQ}u2hHILxB+iN>B@)5^V!O!zH@JZ z#ehXXyqfTUoA16G6aLgD_Sn+W(%sNbR?9Fu;O*fi#Pfuouea~s^#bbv<|yQ=Y)oZQa0iOxNj{_x@7Mm*v>!6kkqP!;DkHdl*MzjftX|08O211JULMzQJ; zKh+5n)JMZTcvE{co9uQ<*p9j13s|1sPAcdFlYl&U^X}qgldsk-mRd@iijkXpxJk9E z8T(tEs@BiM&d&Zjwt##ObfH;d&UDSiKJo@m_lw3EH3X9$GsWu2;;JRW=+}YgO*-1!6 z_sd+{*w{mj-F@74QL1sjOqoS_grr9U>CyJVO|wFiQw44;wFH0w;u*Mp+@IV&Sx3O8 zTG!dMj83V7&kxrXzP3y^Yl9wfcd8EdnLSv4R5gb7PN|l#Cb?75($nWG%+Jru)mVad zflK)f;kwXi!{fZ}U;eh@ksuW=0!N9NSk5~dY8yW5R0I4L63)Q&nb=ms%2JCh8~%QY zc)gAEJFA(?eNVm9ER6bTZb|QC?ES6{)4P~u{o$Ezl}jrn-6QdQ(_Z*AlxuP%^CklRjJJYnOeUM}AA z^e(ztk2DU;U=C={t=c!(akcz`AbbG_iHJtL#Osq&Gvx|NC3Fv!{J(n}8bVu@mh*e> zMC03vY$^_w(NWgLi}&ml<0H)XZbagD?*c1UjcXbmiI;92p5oa{ZFr^QmwTQrs(1>h zUn*#8i|By!C419=#a@vi3{xHM(W6IaRa8{^6lyX_lF-gMOWc+3c`8}nOIi0w&LCoV z3USpzr__CUE8cVFu>#9bERuN0p4&7eiC$#gg~_Q1-AiJ|nc#X~Z)|ATxQ3S8%eh>7 zJ2jKxHYQP5nyUcjc=`)*kJTdA*DqS9ZfA{K8D+v5EY(lMViGyjj$GRaJ^tWLRaq}p zT@%nNJ7DAATrPD@8J^7SRv{4}+6QX8s94pK1%0k;2_Mbgy|8(@S9A}kzl%QDB$B!Q$O;1mU z3br6ATY!e}lDLx)6&1zEs*{K1;N(;qpPI^o18IBsRBqhTsTm8~Y;SLWDVDBDWmZ0X zXk&eyn6@fRLf{qZ$?b>uHWw$S4{#*FSAf26a|9;p$>pIuPP^LK%}$VY`=7P~4bx-ro zi;z%aCCQyA-3p7-*G(6U^86vo@~!`CZ|3qLzQ550d#WSm3z1$YB)HB-rP5z)KuJMa zS^53c)V9BDXiC8$66`durZ$AoKi}vUUb9)S%JYY~ChPsP;9rLci#L0{A$JH@%tzv` zPe8!>f&KgUzxBwH{b(E>9zLq`Cjqxc!cO(qrY(^g@h!s_`Ai>;>ufGHgA0%Xe$dIx zF<+9bSk+8!Pg$q7oc(<`{)gn-Y?X>_4YEiC&%+*acv;udDgw9;9@Xje8V9nYe_w^e~p!3*fnIQU%7p98ziQKGbZ6>A9 z3>!t6C9EACXATe+PwpYwoOtF7QPsM_qD{~)S+fz^g)VtbO-*gV0mWq9J@8}CV%E#V zdje?E#2LR9T9k7=TOapQJ*?36gpKOK~TYDf#6vNJmKyWJLouBJl)n1yYpZY{>$Q+|1 zeO|m!vfiemfsU5;AuC&Fw9pf`p47JnnF`^r3D?mG`XW0s;-q)dz^qU^qtmMkkij`+ zw~ou=NqRmq=C@Y%oDXq%Syfn^gE@8bmcweEfpk|}>MAFRO}@^CgOb^Pku2*x<~>or zba-<6EG7V!QQlWw;{&~;6VQCw;CZ38E6IA$yq)T6+QNhJ z){ZfEeo^*)VIlNMf$GVcGsw5q$eXqvRjb_3nYwpUb%RC zB@JFycOJ&|E7W_By=(FqEDF>KX~5Z&w69g>A-#}`h*#CGpznSf08~ldX(u4DC_sO@ z3#|52@vZ9q0DX7ciTX?U&M8%IOcjX}Yo!BsZ)xSPv_jfRg}+vad3<`L!q!K=W+T%& z2YEgBPUDuN?w=)ae-B>jjh4Rv1E>;-Z9W(zvkAkWBklwecd~(?k=4TK)n5^!Tq5q^ zG4HwOI&m~@O%ZqABKEsl7*}1?vRWv) z1bJ9h$@!Cn#TgQcQ+TPxFJUk-DOmksjq{4@W@7dXR2T2%0{ASh*f1SW% z-`3zuH&V21xE$_=`pA_$&5L}cQImMx;+?hziU;w6s*Oa2Sw>)%ZKtDuoJael3>_mq zRIMJk%Z%pfdFpK=D#%Q;oN9ZyNg?`2P4e}(gdLKO{9s$J_h$y!uSVk|uDa5Ds(I}J zY_3Ya{(>NZnEXip&MBTN8-{}DpDR=NK>C9VvD6Fier0bwqvPV@Qgy?qUcg8q&Pn8p zYa1T2TaOFwLd+1sHX_74(o$jDaNP&>aZaek`W*)03($6mr8 zs2zU4IJ^^yZyTGMamSGCyOY7yjV+<@<}vS`UYOkhD&;OpwW%*RU6sR8wJZLesu_Nu$Z!ZZ`D6=_ZozlBIJ4Efq{aV4+EqVLOd9dHFX4f@A39!#d3aQ;xjzTCxks%&WVa zK!tBX;=LgJj@AXlUp7Qp2?^)+PKN#JwG={MyH$X^LdZToZ=KwX@Lg@1HTu=mc3i=B z^7_}{>$~m26yvw4t0@kYTY~p3ypMDZq_ZJT^6HwpiyJczR27>q z!Sl%1UoA>`ce8KCPV9j~3`1qA-yPhgiW1R1)_-K39{>NpwgXlB#LOpA?iQJeDZ^NK8rA>h;!tSAxH* z%PC)TPq~k$`&U>ktgrcvR@%=}(0tn6w^CC5w&Ul}TL6s=eeS$wrG^vM$bTpiZMW93LXd<3IFY*VqAO7|Y30Z}Qw9%UCW4P6@1mse?6J>ZzecSDJ=C(Yvvc+Kh7-Cy61_@pR;lL#15e(0H#m4U zB0?4N>Wjk11aSJFnkDYQpKjOWYte4j2{N=VdD(6mcel5bftTQ&nV#+x5|W|&ZeA{N z)+q5jRpRUi#X}GL{QQ30)fayG;swQ8lG&4wA)6=-}Lc+vf`VucvtWDVBn_zj&M&8MOQ?DQOPO3L&dUMm81~osJ{6PIvMZ0Nj?nLYMsvbqFa01m`GqH0ja)WZ( zrMygsn8YK`1Rmag=lCt+asG=lG=_D_5kzS-^guQpBkgb4crbL<g%9 zN4y{1kDKV)@bABqYkG&FR>(q1L!H8y2VXeYGwEo1!EKFuGwfajpW}5{h@2k&99Tjx=&Jsg+d?UicaZrK~{Y8$Bm8PjjV=8W=`_HnW;K)dq0?2IAu(JQ|a@y zQ_&14z8}hWl`0n6X@BtV8uQ=FEib=yDP9w+)QjqQI!)R7>B=)cy~1}V+vN{Un->l> zrk*c;;E^%2kL^mrjjL3OJ`?pKK?MyBmYx{_Y>&8Iw8eMEO<(K%M?L3j!&RVH4b!!Y= z&ahzA4LEt2)utrg#ic4mX?o3VIU9H5MCWTOkCn<3%i}v|o{Pl2noVW+gi0bin!69R z5Bs$sivNcXFSXr%3E`A(8J@F0C=+KXqb#FFBkw%EoZr4+vv3mf(GRF74=N`9;`l)E z!+Iq3bbCW`_h;4gS5DlouC|h`l22HL)|1U#^6`lXb#--(dqvCbdvq>1m*;)^I4hHe z-$ecPE7&*IMcaKFmUNkabNyiPni*`52~y^EjdhK^|MO=1!N)wI(G03ivA18%@_o>L ztXkDMd)1VNua&y?$&NRphM?e;^tFX*%A+Ha+7uI3ZAvUo#y2V}C7SVg4==BL-0eGe z1_lRTt}Tp~LXPdIc8v}!OW=12zUilvE$73I4&qMo0Fx##^d{FDq@V1-rz3k{Rc`tFQ3XzhHwG@*pre}e#91#%l2(jH*`=j+tdA<;*xJ~dc>OXdCL zG1wk{dEb|+>okEI`!&a223ff*8mE>oJIqeT%%pGxtIbXK8H+vmwzT9{$WebOC+|@9 zoa|DF(F%%IpW#mr!}HS@cqtu1+3$aDi*BD_Dtfxi@n)+b?4K?o_T_jZQS;Y_-`p=BETI>PyRNAIxnZ~9sn5YLl0KbN!!N?Fm!ns{ zolkI_dET|$MpbDja4b~~r7q{qNN1~$=LRy-6w>5r;%$QX>7H*qwF*B=X`27R z17lqj?}7WBttTLwS`y?fQrMy3*Z%?ic}VW-1jpl6=i4{WeO;V6)CswLTMbk_LLru=tBgj^dvR2P{;1ciB zws}Ox(m7E^Z~);j4US|?u@yhAThL=gf`cQ&W%wygj zb=32(2A9OO3QgR?T#~;OxJwn%9%_A>)5*>q+@9&v(sRv3E`3Dm)Tx-SyT@5{#ZR8Q z(Nx*PBe9FqGM0;Dz^d`KAFY5i;1hO==huwoUf+^b?z`hK;g5ET@t|h0cDC_iduKC9Mnz5g^aZSaS|M(qTzf91$>VC+Z~;8Wx(EW7w5byIr*| zOhmBDS*Uvi{$piro(pB%B_(JwS+=`+GI8V~)8D6G4@S!j%6kZcbw7`du_ozr z;xj0)|HSJEERb<5AfqDCn3Zr5%G65RXF0`(#(*y=?(P2iKgl)}9v(-)|5di(v}Am| zf3oe4Td5s9bnQRcw$@IalDlU%3p(cM(S~2%6Z`Y?#1be^O>K2^b?-`_gdRqRe+%d^8ES=yjVW^~D z8`1Uk$?hlTQSLMOOAS0lA}`1_7LTV74}C*boLpR7+GIRgUve5#R}vfqo@tZdCi*XhT^G`D4tc`!p{Dqwwegc03Lm3Ms4|t%x3e`>)(kN6UB7_ zv%V6X3yjC3vBs``DnCHaqTrUwVvu+j+)5*Cr8z<*bg;Nz+2hkVQBd};&=X|0q9J$Z z%OB`PauKf+rCG(u+&)C_8Bsj#Fv{fZV8<-&R0N4dV`5pX?N+#w`S#;^d3m2?*mj8a zF73#RAOi42+gVVzY06f_m?@q<$x!JK?OxhRj5yMrT|#zZQ{^?2<{9A?YCFX?Q5H6~ zo=A-9tblkY_C8~`YSJ~K>VZjI339N~BQXtu(ePS07k%Qy;laU4nSn5e4GDoA z&fD3gJfGdB8H3z$d>i50SX1MCW0|E`$TACdHcjcv@w1(!j;vj##sK=#TNpM*L( z7v}B#qihu$%9jLiGl|$=F1>f^Dwx5aSSh5H_I4e$4=oPY4K z3;f9r{J=6qgFqik>2z#e0hk*_V5(TG$GbV;xAWp7_L{kz!!t3+wet%(hqmTjTWoM$ zW>yIv8B~L;jV5pEuKUtU>v;OOD>CH=Jmw!i4ogmi(?Gt_9A^r zVXY!>EJ3WH);jfBnyaU{Ck)3^Cu`cPj zntUL(zkMi$SNu27JQ0wCbIx1^_A&_46LQ>HQ0Y1DmF6v3 z@YfsXnf{L-iK?)J9bn>uhmo+tsHQVxzuvBo?b`9O?$E@<4H5VHQgN#N5sPCqL=BL3 zJiCAX>}z!R{yIT0jGz!%fk~By;Tj1dugb}~(TPQ0+=9b=+}%dAokHJo!$ygyp~DdW?8 z2c?*@vV&9{GarJbE<<{Wj@~pD!21fYFK$7+(?e8^QXdU38l)P|J1B~^9!YeHo`;+= zJ`D!b*yqGoPyWuqdvg2p$)&&M z&@}b=)3xcAY+b*VTfe57&sr}IfwyOWxa8U=@Zvh!Dqc*VP@t6c{VP7R-|q>5HPAxO zF0lhAv#aX{zv0`Repw;r==vt^^xlUBpCG#f5=X{fS(8GrMbxY*MeBS>`CY6Qv0~f& zJfH5>JthnAufK&1#*D12LJqHS+ zs^h*lVzkq(>(4-Mb*W4ka%0ZMU;WKe>?a)~ql5@{6r-`$>T&~GN&8>UBpg|6Efa&} z=(UPjHvWHkWNWL`b)w(*IzAt3$I^FE|xu) zxXno)<9LH}M=8q{JJ?OZcne+V5w%_&p_S1g6OLT_Xsx4xE*oAkxcBHC?a!W258LZs zgyNBFZPxwjb*!jsH|_4$US}!(*{^ETA=yvfSuY$fBN8c`dN220RS+%vn3lTkrH(^9 zYtz^+t?2SE4xKT&DKb|~co>V9m*dE83FR=xI1+QBtjjgunm)f%m8j$0R7(%r?;!)L_gkAGu=D{B||E}Ha_|DM& zovj&G7W!UF#UuS|1r;KNvYL!U_?@E6oP-oxQ4!mu7Yp2ah4m+B=^u_O-8SanofW=2$| z=qNmUbu8k+`B--v>&2|D99FbB{U7_xgUo2b8GromU;RR}ql?U0!c_@$GA^S3!;4Ku zd^iBlOMDB2%0$+S!Cg5m%7cOh2iC_1GlTH~j=`G~&z0k2c*5_^a-ardtmAajGBe8Q zo;o(w@!ZTnxX@VJC3k7%yWQDQ^KqEodY&TR7`FM!PdWSd?CF9a06M!|zwB$N!F?~T z+isgbbAPAo}$CdaEnR^7cY{PNL-P1$&che8b3-ki;R5J-j^d;l0=kdaYNUwjn@ zvy4ue_KfB2jIkFK1btFBGTv>n_i&lq_=_>`=upE|e=U8YJ%Ck}R? zT-Z1yCviLe=o$koshjBS9VfzvmHJh;oF!%WH%g~{uVrmvDT!kVvR2ddpN!)5?e!*@ z#>ncau#rcIGp6244d?Psd9rbME^mL@UDV=DOsa?x0}(z-N={DemGQ@VTHWkh%w@_q z=~vP(mYk%WA$lSbN*hamOkr>Eh#h9#uEhV={B@%%NpkLWcLwO{aF`kpiQ08(sji83SCc>l>!YoKDE=iO!M zmAEDSsz}O7R!uKzm(^R$C(wyFtQ)Rgd;a*Lyv^12AYqA-d1))t9Ct}J>b&D=_oKDZ z(>&5JuIo}(-09E3(}zO-j}6EFgYL6 z&SGQHyW?tLZC?7^DLiszPc^yj2a)6Xzf+9Lv?*j)&=wj?=?fg-+@Db#pdJ=ieEodl^#oC)$Wty>WuXXA&@p|gXyNC~kZCc+%{~EO@ z+Irqw`hHb=Sj-~maY;d@cumS&Qe}ycQrrr)%uzpiVc!!nu8DA2eWjKU8KXNMf#SIc ztrF=HfoJ+vD0JD)3i$YaZtmqR$Fmw>RrO=hYXF&E)&gSJ_LjVhfFnE1c)$ zACoW8!bgC=T`RosWK&i^L45Ia$0MOlrm2qJY%D_cxyD-@rIxUEl?tPNaICw;VrN>F z>s@KLRyfNqFP2)2-1xGlm>JL)S|p&dH|1RT*qY|_Ncx8Vl7>xrB(i4HgB$;gknWWQ zlP|f?Or6Ei)i=we)RRJ4U4uW8pX|Ff<#VZR_~eaGJSm$NS9n)Wsx70Q;+-nE61q6< z{u#r2OFPE3EMTd#lv|XhZ$Av=pJIhBFy*hW+)BNYY8oOPmr4I!y7fN6sn|eL%M$U5vrPKR zlWAr5Dg9wtbtdb@?5-m^dLG87HiZdxsAByJfaR@<%X(-yY5hp@*1Gh~MC6a4i}yyC zN?Tge8!uCq7qFnjAV65hb4cBPTD^?M0`}Th*#*%T5Btd&uPu{4czu>S=1j z)vM3lcCt14HLS6BTr=tl%O%etW9_uqyRhZTg9;Hwp&vz1bYz`a{&*SQK0B?qruOCf z&!-&<%duH@*J~witYw+Qavkz!sBvp8ky}h*K;L^!aw}Ch)ifCZB1OS=g)P0h;Gmy9 z_D0%sGtMlE$h%nI%?z%Ba>=>>mZgc(0T?_Lj-aFUxY=?|?Rc%_qX>ddA*dLh+|HQ$ z!S~GczBu4r)mIIJ%rg|Fg;yO{LYDMfc+>#2l5~3JFIeihKgFdokANk1cJfN*||| z$#P!DXJP_ECFc&FxvE%S79p$uA!B35BZBWH9Q6WDF;WDfE=7^`7>HU2H+-*Qff?sr zVl;DTpn>%l-owaCXEd&Q60MLE1tVvc9NlB>lDpkv-Cn5v&z=6UFEh)3{#cu9>n%N5 zfkGu#mR^saMT8M2X(hvC4d$fr3EDG8x9i_NPd;y!g3F{`WWUq!@$reQb$UTRyvuJ+kK>^uo;06*-7iN33iI^@y97%z=*!a3AJ6p` zz2UlEjwB*LU_o(*e$(z=SX%whDW4l{h+m&`8=k3UU(->nWH0G?#~4RRV?fTu#(VTt zu-!Owj))8=%FU4pt4n_JC(ez>B~H)GYpkz?bSV_hhI-kC0l@E#{-)2*#wQh|6QS(D zvAnBKGH?o4GHHS)6%CyoD9>M(nVdO#cAf=1IXC)l?IASjP2y8TBXiq_mWgnj=XXCo zTlz&cLi`0ZY*T^AvsCpjhGnopjr_5$TMk~F<1I7;`rwJt_S}Xa?MCZ@U!|ElevKYF ziLc#*SvP#hqiuMgir0kAEvHsIN1?DN@9On>N7`ip8t!Cj7lMu$J`GSl=1FU2L%14a zSEpU6!Oj6)r2Ouc^pZXydj%6|9T&CLBCZLw`Yqd|8_9veWYShm28vQvpQ0AtEtzf1 z<6eAp){IP0qY$UW2TGg z$K0Oi?KdfWaT-ZDytQ78XPh($*?s;b^3vI@=JVW!blh}y!Y*--&Q>jqUfZ{5oc|F` zPM}t($==1{F2RXvP*yn8`XuS+ZIv(aG2c7Salg~dpr_|arTE#c3KmE2SBsA?(kqwF zL<)DlqmR!#7sP!_yCd;9H2Zggj}!Y{V*F56N&>a>T0^Nrp&Pg0TPD^zUDkH- zE;-P5>%=|Mr7=`ik64Ud02>jJpy}O&;u#Z+{e$0$4ei3_Fcz^T``?uxOhcSx@~U4Y z5tx0N>DHeZ`vuI94iAbYB5S*lF0FX70-{3^FXqMSp?@5la+mM2`BA!>d{Hb3H1egGmQ8tE zWlUhIICx#j8D{L??<0^H3sg|ZK+^MQm4N1J)4`-TD{hC}_PByB`Vy*;QPw6?oB2Pj zMH8%?ovL!Ehj0NHgR$3x+pM%IHZvT_b9fnFEcdI1I>tv>YE^7ypac?r=dC|bzN9(l z7l^FF0_n@Z$qo*s^5p)O1{;!;(_J5tu$}kde4%V|CzyQWv0X=aOYAo@3I)#57vGW= zU>T~GTWm}LGg1}K4{S3IZ!T`Kp~TX{@{Hdc8pE~9+jstNWopAX_LeZ&& znXcTHBq@#VXSS4TE#>o?);fJdLGS)}s0cMRA-+rx(Ho5Y?d$vHv&CP0L)n49%pi(t zG1lu!XawDhTOPJb5;wEjWUfR7Mu0iJ;|H{(WBeucS+fY@pIL;h0DWga$teaYu!Qrn z{7;MC8cU7{{(~0i~;HvU!IbLM27>(X~+@u4N;VrUzA zrj~k=V6<5dUq+_;|E-_EJ4Qy)J2k=**jEPQ`SZP0@<-C;qCPOVqBv>}#xSkwq?ENN zLPuEIVg4Wd3Au{M#c(ezHN8;V8|e~n?2@~?6HH_mscW%b= zQmOEtnE-jHM*UEqdY1BO2Yy!tXGeYNll8aZ;2T2T(v?4!{4@PTtawch3tmuzJw>hC z;0*U&ch8?2v(HySXF{FcXF;v0*K)0wahs;K=p; zpO%e5a13rQ^t$%DLQ!S1eV%uinD2MWNyxJsUVWr>`v8|L7?5l&ce--FO+fsifChuA zs;bv^x4`no#uB))ZT411ay+NKONDU|gsdaEt&fZlV~3L{uJVgzloR07aMPtD%5ST$ z;?odhj>efkOZp?HAIRev3k7|syg0#uJYg=#ls8Y*MIJA=9YJt<4lW%s)O`1fg~UaB zg){JH7Qy?T`_krfWhHszv`75l@ZPnDB*3d3aIs|2;pk!kxT-a%j;__rdF7Vm`K&az zSy;z73Ix1*wEFd|7e|*y=}HYD65i7@8X0h5h)WiPC{6iFz9n!53;5kEP2N@J$0iLp zmCAwVA99OK`l)`IWNPn19D#fgvY5YYB>;jy@op4xmmxj+)EFI(?5r;g5N4tX=s)N3 zE(l!1BAYvHe4QDFZpvu+9!H`a)iQ%CIlT)sc=D0RV)W!qK_<>cNrL{ipFe-9Ln`AH z`nM2BC@e0wfI(XOSY2}7#uH44cZ%B0;QMEym|Vg%U+6=h6+gni;-5p1cCW^W-{FEe z3sl58PX`AFx1fB^ITE8w=oo{H{?slV=M2?z-jNX6NYlh2i97K+jy3kk}?0q7&DKN**!Xn}Uc2d>-&TKUP4` zmRVXv?ar_i@=5)3tfa@g@h#GGmjU>;10WB&b>}OntDWtg05KeLj9H6eV_%NTxZ5m8Kj+i)9-UVy>68w!-q|VF{1+q z@VJP;bq#IuSMmv4;cOnGXhXfz2A0I|ukj${sY6+;ivw#Q)AI>Lwq!HaItWcZv=|^by{wVc3MVqw4&lDc z_V)I@wkNsn8+g^khIh4A#|h>55FcFGPd{KV7#(JMt3-(WED=I<%m;0D0a0P_+DO?D z??w=vIw9x>`7EV`=vi?WI>EA)XvB75xxW#*0U#gqD&klkVexX8`_|QeiQWH)6hBEk zJazZ~YxotH#ZB;E_;uH7_y8V~_%HnWtDXF1?vmfkpN&)R=|5MOCW+4;x+zlNkzb?} zN@{DFJ^!Wl-rFR8HO?1N7j8e2b9`WbZ$R4VS5uzkP;IWx`P^Y`RPy5=7hSt(pHw3qX zs@C6g8e7{{1WC2ASuzO*5;))!)6vnJPLv&cAuZj!xort3hiD=jiF*viP(?v0h{5RVumf>+#_K|AU?n zX5CJM070|5kT#)b#@LgfiJ;dJ5tzHuM81Ch{wLEqB|EQEzD$R6`jlRuW>9)x0~Lkl ztLt3dwZFOo{}4P>R<@z*BeZb;Bd;rTdJA5 zuchlB>_YTmL*;!4&l2SJtWoeB)i(zaG(w+a6CHC+AwS{J%`%8?$c0%_y@H59mFJ&) zsQ2QqBt;-3s5puJN&mLMZFXCAR;9(J)#~V6ou#h8R!UzY3_X3K)iCKbsA;+fLMPuI z!=iK-s%@Y1gyvtaYo&dlq^vwI@6$OIAo61O31npI#q(H-x!!{&l&}}!UGp4A@?l(_YD4hdN>BJ-R(qwt6BW9Ni2m~U%Kc@LK1Twd$KQP#fT3w#;-9ZbzIOX_z6UJulFPS>t z*no4BN)xmuhh_;qfdQ6wr-YwX0mR>Pb%c&Pp$llM4Y@>G2CQqE$@(ZD&@3V%LR7-@ zR5NC4iOXv?Xvz3`4F38$a#+5G@pvX zi#G=3;q*G;J5Uy=Km?(~y?gg6GX3!Xa^vm|L2=&_qt?cBRp)f$;_r}brBq2hA8-N%< z`L|0!0U+Vs-S>+z`_jWJEdi$|&f<76DsLdELuW{`mFxQ7Y#ca%3?^F4H`4RHenf_; zFqtSrKLo}$i;OW+I3QL6yGyxC`_P%;!Cy~O8g>U`zC@K;2>m^>`)zA^85aLx!F2X3 zRC*jwK>c63XfOtmk(W1p*Uo}uIu@2kCs8K?{j8y0dUKhOlxEl1GLiTa(Do9ur`{wOo?1s;1^U2@Wqvkj6 z+)cdk3wP#|$0)nUKu(D5V(c$A&zVN&nfCz${LF7ZUV1pbE7tc*03eXqaQB{@TWvGk zt8ph+x%dnUv1cUfW&pU#({Wt6IXS-fX5RbhxH+c5+?zDG@e?wSg>mC|XO1~Tf0z4|8+x-G-`=|0Phd|V=BsS;EGBF0v`?1Fi}X6# zDX)tZJX~vu{Y40)$al%oH7Y=uH%Fo_AP^=Ll|~)f17@9js}MxVbp7u`9O5IQtuEFA-0N0h81wXpMu(sHjkf`B1B5wt0zYI*hJE9vA0_FC34pthZt&=)9sI zuTOdtZ&Lodco5#~xv{>i`(Oi zpBvkLMAIK(qkC;@{~p<`!&PUHIU_dDV}G5+!R*C7#K2Deyf)EvoA%|YVUm+exYI~E zCV*#1pbAL*B?E4@9;eo=&T|c$Bm~ZFQvv@jZEwq+3Qa4_HUnM7!vb+rF3gsA5e9AS zF)HAgfCNMap;?`GdWd|{u zx9&O4jDK~waPUaWhLUXj_u8`)|M8VX+pa13j}7F$yhlmc zjN79T5X_@n7lNRSLKZi%ZSR!x(rtp%!GHH_7uFkgE(Fy9oZV*aa0oIcQ8%%l)@EY& zhVUm2!FtCozUSX;0)Y>UcCvv)TUmQzb5BCtl7GEDq|Q0xHARh^O1v?#j-aL)AGh1z z*k3;V6oACM|0vRi<-t;=rJWJHgZOawoISCn2>g-|?eqkqxPmuACuGy4>h&v6+LPVcr=n58ARz+nSBz9oH9ojWOYo9LY5QBy9 zb@l-NbI1xyVUYOT`M}3#JTb_Ci5SYZ=K5tf=AoI26|A>@;^DTOK&LtxK1vA&X_OV> z&nkXaE_`r&Eu4nqN2Yz=_f?D2E70M}0R5Dzi1*Uft7{mA?wuT{EZTk*?-v82^vkjb zrk2XHYYO(GY!{Y!6Md$|JRXM>ZTRp zZMh`?-?QrbHm*cAH$SrMZ*iTv`v2S9b_KGW%3{p zH7FFW^-6WiZ&NIl2@Adn9rzId$Xo>@_u%#nv@E%HvK4=eiMUprpq3kls0Vu>QxZ39 z?e6&RFFY8pC;#`F?Bibrf}jJ>KY1&waudf}t?V!_&L$AMPCRT#Tmk~x?%bBMhfMm9 zEQvS7xG8^tGXbC#5%KO73okS?qP7dW~v1%&3{wkNXFwI;rxI3e=T=b zF_7Q;)~4#jr}PQLThPO1P?8$AhOFZ6pUzr*ocgzhyLNVA<>hqriI)x0XK=0?A^!O< zl$cmSy{(V^^Knj+6b83tVjD%d)1w_|WIzEVru;rR(-nHlnEUOaZLElF z6tR7MTd6g^Pn)W7;4y#!C*5hGwTuF@ssgjJW+W~t3`Ya>^!qyCBAUBlaljzNk9)P9 z{i;t&>rroO5wwt3o`3YnZ0CiTUnI0tlB*{$oz&)`wa5F(^81W%Ck}@$f}Tt8b?F7#(@k1e z&q)qIsJV4X?GGi__Jr13{+Qc*C$?#q(*BTtJeI$O^FE38y5I(S+RS);R`+yQcR#xI zFQvXFrh^hwDA=1MfAC!XN8Xe5XCwE%uY>sWT-UkDH2GwsJs&w}gqOi;WcKI`5=2Yd zbMu-m`)fO_{(n3J(uYcPd80-CBi8b!YVVb@l8o$ZT=6PUDV7RoyKPG5@h_Iv)5F#Q zT$Wf*tunL4|KK*-L*lZsy_90_6e>pUf(MI9C8*gNNJmr!_wCpqQqEc0lUdqE$ml9v zWn-mcl@JF!NmS$f4qmy1gVma)5UwNcQ8P~tppK#xbs-*v1m-$fC>`z-ukqIo1aZBv za4Y)xxEW*Foe%`@_ugzL^*-9_!ljXcc60|4xGF7L^UjDECM?OcnA3uW^VzAPtUL<3 ze7XMo%#-K!gB*<84*)KWno%W^TVYg{P7S?SEEQ<=w!XeNKJ5l6VeZiR=cR2gGc;Iq zd~FJB<@3GQWIVI$nC&kcf5>@WrWB4^;lfDEAinAt6y|!6^(NOMH)4ElSFTZT7`cD@}{gWfm%HlHeoO!t z+2$oNPU`<8`v33$Z8Y40hLoqPsZ73J4Qn)p4DM@is?vdMX2 z%9IhgG2iq=!*-Z92B%^ur!`QqnyyDS=?qI5pnvRUa;_n)eLX^12zglS!hp2oSrPV9 z+De4O+JzG}-8&eE6759NS{8C1(1&`}!Y~c4o-4)AG$Q>?l2xpWhKi^9yRUOcC;Nfib+v97QcC${qUgR80YCN>^L+9CsF?f+K01#@2S&aEvkTM0AAZS_GB z?1oW^I}}U|(RWmd#IwMPl{7oIZta9x#?;^TFqQFU1~m)-AIVV{zg9kasTqNwc`59>{dt30nmxB?+5u_lpFi zF+o4pcYXX0>}-zJD~xWp%U%*$kfSe_D5CuyR%W^m2n$iSW3^nA-1gIZiy3IEm&EUz2vENrBN< zCU484p~5L)GP?{osqGM3jEg%r%ib9ghd0#)@7x*3h6EgDcG$*tN?!UC#)jaxvoNhk zmg^Wu?e>I;dMwvdT(N%WMj2`e8c%j_0{=h^rG3uAXK8byENx!N`x6GYRwKmniyzG> zZimOw>HL1>6!P7R%#tdm*wKNlH0$G?)}mC~nAmzutpYPI?pS?Gs1i@PWh7}x97TD} z8ln!e$UEYld1l-*zJ`QH94?Y~Juyz*6jcx>QPGS{VbGrXEOMh()VhAC5ECQU8;JxY zuj!@RccPK(m*FJnJD}-{cYb!%W>?!4Kh$)}J+>nddARt>iP)sAuC2huGpo}vE2*|J z0x9uD$l)xj3UfCgK%!MT&-QU+2w}H|cfmVvj)gwh#K2(F2KQKUSGV~UZMpFfOE1ng z7M67#v(cO@X71xLfU>fbOe#faCLA0;2rA82xfN*T4Bffbd6X(brsgvr&(0rk#Ij!} z0ApF@ZHfl6hJ2jL%-{GT#XEpfHKq3EbT}hp5(Ai>OqED^Q=pX5QLg0lM5E{SrG&mb zzJLSiea5ct{A)pdsb0+7Eby{WZ&Yn1=57&sem)cV#SyfT^O&TLVdvL^>Gr)7fDEa z!rZfBc_ov1Q#MSLPLH#@^RDQuPKVeGoW&C96Cix(HYv`H$w( zVByhKIyrOCSw}_~4Q_V|IZr6~erH;%;o#1fiP>Xdv6sm}3Qp#hSC_Fpx44{>DJB#Z zvo(0hgerp-4j-eR&f``lNfWCine~VxYvkdIXU%!`tdho4x~fB3dK%AHwe1XEcvu#) z^`g$@%aP^P%X>F!Sy<0K9J(lRoeT%~8|X01i!}D65Jk2=^$EkzcdqH2%D3eFdSzX| zepTW37om=B1`$S1szRh-l5Ck%X|}S-=tmZowR*?1tR`e1!LL|5Y!e~MM9Q|r9 zQcO%e=J}wo+jTO)f?6t*?HGP6zGwed@iUW&;{Bh72l76wW4vlYb~MTIo>9LRZW%YU z@8AsrNyZVj)D{_~&3mghqd)h=Jg(sGl>p{?6UPN4zDRrApDbvyy*jdcMu_8h+LFR-ceQ#WwjH(L) z4pnQHbdu!#a*h(8&x_@EPVum1Al0+ZBjsnF-`8UzR`~@?&|~~U@W0c%+XMW-xRbq< zVW|jBys&nz)ai~ku$28B0SuR%*48&O!KTAten-i_9iIg_P4dt8b9xbw((y|u*zHz?u@GAGJ{(t3r3#`03n<+b2g3xn0>JBBb@wqrN2T!XxLBKk+`_#P zzj<8_ft>1>{vp|lk|a-?Z;etvn$jDg9wf+p+z)~`MwOWthR_TX^$Wu>B*OQF1(UDS zR<2e=9uDYDdPdw`6yI{8l||p}-VzN&it8t<^JSr^cc!kXB%;PI{;a3$DTi*tRHfM@ zo@(I9#6dKnH7#d{N{)8;cBkW;n4!bXWPIFOD7U-j(T_ate|pDsBQ&Izu8L zp-ff!uUfREYiFN-d++e^r3c2e>mZ#p$>|d*&j<(zY+O&P1VZ|KUtjYM*^RjPg&9-rq9p;V zBwxlaN%tlb#=7f_OJA32iQ8@(DWc{rdd0pf&=GlUQ9)dsL^Vytr{!9_+yP6#qd!Jo z+sh>~9-DNwCa>)F?sax`fnAlWIJ=JR@0UQKlwez|TCBD6GTUh~dglch9nG?*XTDuO z4ZoBm@1AtjJ!cco#T&5BXUH$dMr2JFO2=~U+=nT!8CvB2&YD9jqt4x79jDCcPrJJp z-#Cp0!-+~QHG-?y`Mi>i@d(YM*alQj*`t)pl{|ntAQdrOGMZaIZ^B|ZsZk$dRqS!j zi^*K5IeZG8Y2P0$JZU^AU(?cFoA>j4pMI{oHSZn9F^kO;XXm}Y9fo;%pFN9&YURQl z9H)?H^^4O!)OOT+FxpIYp!WY z4lQG6G6r**h0V>?~>eUEB&ZbCM0uq*UBCFsdr}bJAy{ z%T;pxns8p1J!JgVd?PDM^u^WIFs=^>v=?^0K-8__kqkzHqf}YhOa&&QjUR_a~%WzK%dR2;zVd1q?ZJ1b?ZC8>z_{Mm&pMOd`aP5>}%`ki9 zzG1R$qNp7a7%tI$Q<=HBw%{vBVL(V^{AaEvp+}^q{IxYBDH*6l7(ZEeVe+rh(A4wh zkvgc5k*rf)AWaGhYT$Kzn7tAvmD1qESLMj)Lv47;c*HU_1m8~dJc>6|AttfBTITBI zuAPC_HVeOfr}cnY#e?Idtu15}qGZa6(fZ@mTjR-GI6#QA;jGROq5lkE6SUco2vEv9 zM#D=VmCT~9hIiY?!RFejqQm0n6VzPVSk3!a@3wWhWmTLP1VZxrL?IqBZ!AymBPC52 z^NAT7fh5pr6&-6h#e;+tF3*`AG!4fUt%yW~7+~UaG?Vn~`a8z;{$IaDAtDBzy~K8= z|D8zF0p8Q}C~?9Zaig1Zen~%0LTqZV^!>xLaq;uwSt0ni{pZwCi6Q#t1#wjSxUy5g zl#amgQs}KSX@URYQfOdi;-rlCiX@8mDmMLO#VOCvKW%^CJDAw8I>+*XM~XX+6L{J` zew>z>$*Qjzfw7GG-;cZ}D}~|gQ`QVc@k-ZP=e%cLn{@j_0ukX&-+Z4oNcf^Dx5>KO zSYyFN|ogz4gK&hK*g&-K>1gN4;VsmbFBIFjCX z7l;(g;mfJqe8q0aWZCK6xrq}P$(LgF&04b#vQi~)8zmUNAuG}BSf*eXC$t{SR7_Ke zGK_|D_09QPy6!W_wA+6fbzfN>U#V_c1fBjzmCOG!J5^!>FUxe?ES*Iz?~`Ro$3?Qf z#hc}8j492dw=AUaRoXaQ`rTZ@;t7vQsBpR)BtbTR{Sq+4H&!Pg_?!KFz;7Z+8hk3? zQ0ca;CTTa(Qh(%MLQTr);a^O+d}xHI7%SpzN1)uS%s2QpdgL6)x1vXo3wMm~*f%KfjLowebQ*E9C zx#gm!oR&_N=qb#`NzLBz?BaKq^rOt=qJb7=?+(}khbA_hUvQro|99k za}Bjq3J1!?5!WH@my>b)j%VKLmg~T#zAw*utJD>AB`C-fvG*03)lGjO@5VYd*X7`6%e5a}@^YW8f`OAN zmYox~xYBA{%Ik2DDJ{S(3d`fBcqS5tHRH*0G_a}3M?#>Rj z1zn-1eYAYE+*{xJR~x4bYS%n}@58^>N$*=2NHk0>{XsxxiM0rk^tiI{{bT;`4*5UY zmF8(Vc%D3p^D3fzJ?9+==GC1D@MoB+v}cKySfD+g_?2I|X-EzJH9acvIXTh_2A}}P zs~(}yYGB; z<*B>_&Gy|H*4j!N6Pr(e1=n>NMoUI(N|T3HjT!|eS>*;d6RFq)Ij>wtx`!^B%^8ep z>8%;7cu(E)bt77;9BsHgG$A@zIMI-KcVkjn=5^Z1jciQ5tm4UM`Y3@o>nXMY0zk(ZUZu1axdF^h zObP>UmAVPP&vBHw$@le~xpgXr0m6Cw)l2j79Q;ofiNJc%cjkQ2PZ;T~o9k|*%0#t39q%d6#XZ*$9|>*a{qBHn4+ zsg{fs4&A$XnR?18zg!4bVT*Q;Yewl|Hy_XEc$KOh?HD=w&2f*M0i#G)Z|Y^(!FcLl zhq%k+4E8dxie7zit0jEG{QX7-KCsHtq@mTc-JU3%W?ry`E_Ry?ggJbJm*+#9@)U_)akuG3r;p~5Gf{kw(};cviz#DGU(Kd)!%}> zS@UtANa84D$++SCd#|j&eW-d)IW3fo=IPhHl*`WCRiZctk2(!5#DQi-q^pyGC1yfW zh0qr$nOJi#o)CDOoCHIpSkh6k|FY3|T#m@T+Kp*_5_vLgdf@q>aO_IlS^Ri>h~Wz> z%`>35LBCDt0{2+7dFzk)JLyeMEb1i6bMJayG5TGx@QwXFby)gF_wzA^xyNjkn|3x1 zJQ2w}wF>_^DoQL2i&9v;Xuk5P>zLmBT)FZ2&ZVdZ-o>~%Se^VZDYPp6cJgzO)pl(DZjng*sY2+2(OYt&fW9Eofy)97Y3UY~u} zUjQ?`z0a}tsG?Nt9Dmh)B`Q7sj2b|ce()M2JO%hmoEY49%z9uVoheBwQBwCVnO-Wc z0l}G;tnD;vU~vhCuQvy&7(Vlro&#c448zcVmW(_v1s3=IJ|{X^T_`aL^)ktN-HWWq zkgsa>bHIjb&(x~=s;c@L*&w$}3RtXG$voR6letacJhXpr^kwb0RTBH(eW4??^-otT}<5o<4{wv971Ku=0M7Fmh%!IZN5i-cm+SKvc$2v;-vU zLDZR)=C9z@VdLs^S$edd0A73NWFDmo@T)^Ok#XlZ3vdBU)1XxL3Wg<6$I7aW75&zP zVWxzO=RWlnxT=G3Cc#blIgSn=O#3G`n9o8LQTtBuPIXknqTwzBvJPFV}FQHsMFXI z!gkuIsIXMBU!QyZCHthLhB24Z{w5!fCKn^fgRs)HdYGsPWJk<{_5yD6^NLfFKv>4> z#srr`@cN4BdzK#=T&{YN$^OD*C=8*HTQDkKAH2n7ocz!Eddj_T-4#LF9Lng|za=^g zSv#0N$vijAH6!PB(BMkkz^Ox;Me{&R-5fV&b5dCy`Si_NqE6P%IaHfy)YU$Xw+U35 z`|Ql3NkTNoejlgs<;f`?RvcpNbQyHgjK#j=|C45Dh!TxxLqx}BT`RpaH-+8PUeCl$ zJ&yFlM55ip@RQvuJ%5~v#w3H!(gz304o5z-B^hkvSiSu5x!*P^sH`jaKi;GCWck7^ zy_`Ten!#{;y-~Mz{8pN*5=`b2a)$fWMclG3SnqCR6g62%T}zv@vka7#?Eg$bGWb#$ zd~%U9a!OmVA@!Wnp}7}%WR@}&B_Z__l$wX@fvAKe>qi|SgOyH04_KZx`AaRVKa#xN zwPhI?a;`B1l|vPQA8h4*W~)sOJk5gZ^6DqU-JMjFp<3u%?#ACVxfA`S-v3ESat=@X-Y znC`l~tPPr%Jg4@*wfE)mP_OUb#vt1flI+Y3*~U84Df_;ZlcY0+3WcG~8bd`UVlb9L zh@piRON&wV7^O_vN}WoGWI4*7RD|dH=$!BJyuQ!*KEK!Rd48|oAHSBF`}4U!^SSQ( zy07bAulIEo-zvCRPgePSa`*G2@-2lH6KOC7)OxehuTa0HJEH%y{OQxD8~Z;Q`=;)0 zc+9$!cW9@VEeFkskoi;YIvniW zHgGiRjZC`c_`3@VW5;Zah2U(35u%QX?94y6%dpaAFv4?e!QuC}fqM6j=#a-0_t89h zqgiL%d9M(j0`n4S+jY|Ks&c{{)69OhIW;L&`Rls~c9MNQ^cg9%GE;%ElaC^Xm;){X zFH3(v=GS%4N8VxcKFg_GAvGNjv}>%tfRG7aZqOME{Wc}J9F+jYjXfc;bK^k=eMPv# z6=rOUw0q?#zK0G85OB@WIBe;5%Y_2j(Jb_2eKXa#>)t$f?NmMeZI54vU zf3>*PVlMP(E7`mXd-7=RsgGN_GoHtmyWVdT=Xns14ShF>l+1`2FH`@e&y3SIRhLmW zSsznVV_@way48Lqq~j+_8TV!-nc;`LeOlS&cZQu$WUt+|f1kP7#p5n`X+J(5^cv0l zz%qx*$TJ6fEC!dQ@4vvlyfN(^E>!o10{R1>=gW_NCg}Y*{gGC&S~Kd~Uk@d{@?u zoQ&--e)uwIHh!&GiuS<}6X9t%Nx7cHD!Kk7JyZO_k(bkzRgq?j-`R`c9Og@K_myl2 zji^U>!YX?85rZw=VL4Rvq`Yg!^VfFk&;z!VPbVa&Kn;R(GFm0r4c|5b;8)MK zH=l(UWBjIt2|nY?_|V0CyZ!=+!xl1M?r z=k`qB?CF2Jwl_+)s;~BZ>CF?Gs#;oF<#+DH(l}c|cT$*lxrPLKx9-@HQMHyak&iuj zSSHUcR$|)NX)}pbUR@14k$X4y-Vu7wzB+-v z=9iSP>0JB3-sn50&#X;;>YK&eg~;mG>?zV!3ME1bTN$062fh_OYV~$ z218oZ8hS82Co{v3i{(wcBd2jY-UIQ2y|ut5-rn#B5E&}ojB_I|;K0(AY<+NwK_cNl zpFp;nuW4eIIvKn4Vmv)PlT;4WI#eD>9}IY8-zncB>F~RORJi1ISwah5mlcGYs{1Hn zI_G8f#OP+EQL=3epyo->Cc0!+K*aVwpMfByJ zo5`^MHb70%mKn@8o3A(p8@-rLf@3h{#61Np))xnrkvCA6DT;3YLd)mvs%YUTnA$gZ zaqm@YqGwBe_OjK*wPIcQiFv#>U|7Qd>y~IKG>HUeM*ExPUr+IPy~8n&QC3f9z% z72^mry)L{UtIco7z%>hwLP1=^tROxp0pDCygmuyAQn|bd#=P96@ZXiXpSSSaNrG@*erHn@MHi)e z+7@!8H1t^#aM}!(6^B(YtgbT_y4U6Q;uA82yi?^ahyN}*Ga&bKUF^A(mgrGxtscRW zpC@L-sVUdif=)%OOG3}3HH^*``-uu?Y1}W@9{VPpxW;H`LSX!>c;7ry3l=%oP-<9#)<<|WeDyr#vp%e~d&QN)I z^HBN~d;U0@y&0k*h0DSm?5~c@eM$|wR+WqV-zsC8JLGbxRJ+2IHfE;2^ETqFeZ|hS zgNXIYn4JKOw^Bo>y=n=d<|YO<@QdzAT0#TQj8D<#nkB~0 zN&}~m4XGE5^9Rg#!gR*Km6*7C@aftvjt$6?mh$uzIZ)zI`Cj+igLr$LHc=5B&pz=^ zZs=eNMd?!9$+nIa9DDV2IN;)3w5Ks|!gBOU;?vOkNd(xvD|)SGk(1Ms@5K##5lJ3#>@5jl7FS<-cp&xbePO7JTBSTzZfdZJ&4q5eE-Pf4m>|I%aRTXr9)}F` z>V9bRkkK}S)0#38VtMY$CMFIif^+L5bH>YYF0W?%!ik3;L;WKXz15$}9ZvI2K+IPR z?#9lUy>F8q5M8Syx?F430K>#hfx)DT=y%ViyBe1_E1d>KGQV%2`9z5%8U@7p4T zUms1qbj#GvxZXTxAA1*j*TtJ{Ol+rm;u$QjjEyZn7?2x8LT5I~G(hiFgDHEQ-89yD zj~-Zq>(9O}QeztLfW5fjjpAKj+O`<+6-bAANlC$xzWZ~f9Sp3)s!2{ohG?-g0hhz@ zNw|9?PK{-bY$(7{zp*%WlP4Iy=t(7GecPQZ5^d>Xt0q=^EWLTpOY89+)O{(uo+!(U zkwREG)Vr0&q`=9ABu<_8B~ex)0-JLHVOR}lRjM<+NQzUzna;(qGIg!2tvf1WIR<1& zjx@5fm3r+%NmAg+sRB#x+L}S&|r)t46HmmkJnmQO39S+9O?U$h|i74PW-v^!+ zGh#XVx-r0!W!ZXklhIJ{0BGyM2|h zb7(tD*jboHvtI#Xd_q(yI^HHOk2fNVh7q*{oXrBvqe7cSY3gZLIB!NoZ3?zL2F+zG zpA(H(PryC0S3oCY3%ies#;jKFLz-xWE5B`)zB4g586<^z9<3c|NIhjl@l>KJ@>p*->i{yuqsKKMeoJ% zcwOHK9AOs63M;)YGTuR)rQ@ZoY%I2kjRjzMY8@QD5AT^AZS}Z!;oYVqE&^cn2;-;s zH-j`^>Ez37VFn>9je0;zl8)_^Iy?lzH7eaC>?+^JMM>#w3;g;NGZGi?DgXyfD1*&U zqynuw94&X75cn}LpY&6PBD!W_O83AiRDu*7(O*kXBRz)x(mAnAUW4<=I+2Rp#UgT7 z-wgLHbrbQ=Dq91J5~p{wrzv;Xri?6Pn)WbE*X}vEVJygeA?PN-lcF79UvIeSkKLJ*CYmn*bDdm}iX}z!O7-+qFpQZJX1T@vmeX0md99nS?In6^m+eBT1 zV54W(7Tup6eZPyc>U;A+RW-F>njkpo2RKpDI?qUzkZXTi7M_=o?tKuRS%2GKEkiV41AvZD8O@22(j^ zaxAS(gZv1bRkBsyVFNqrV1jRC{7a%37@vW9)5;9*xeH{L#e$7IV__9j#TEeB;`~QG z%@KfiEE9z&z6TDVU@HFMao^fC1S2}_MS2}#VO&yj+2dEz zLx_3`*6>9IZR3Gl1yz48hR zlN2ky8;J@R5i7m{NAI}QH@X=)3@VDEnZ`aU&eh$Hr)t*^T(KoQKcY4$fJe!M{l z3J9UR8|LrcdN=vvT-_%GYJ)7ei(ZBWTMh(FKn+VvG#WnTPu&jaH--JmAPRU&P;Xe8 z2mlibc+Vh|WFqXOoD?wOFARvaXGw;iID5FSe!Ec5axS)W=iu1`9h<>X$?8C?oHOxN@UI+jh zE+baFi89Kjq9K%S$Zw=Q;PQ`tKBxge7}1@lMhJ+@)>X+4`OpO>@ z`;g~R)UoO|1pDCV{lw>^3MxXky>%wJ3 zY$8YBdwF?1ugA{{0@!;RLQ0$R?W57=lELx;4E+rP2e@EW1u-l#Mh$#i50aSz6mato zi(T;F0PZ_H29WlOic%08GfKVdcH?xXrnS2dga@AD#~ih=UfAZEHQwKN0r5d%YTB_C zoDza##rKm;Z36B}Gq|(`H0KZkM?u`$M$C8ns<(=ZjX7M9m6cuOyOkDxPV|0uw1=hj zLf7~2#{J(%Hh0&CY9e?J-v$Ak#K-sz+MEz z0&Y_0al||dSA<>%re@=v24~soLlFSChl!2YnD35C#{miMYlxqTCw(gttM_1_=W*{b zfey>Q@dNkiCr9|})ct)d+aAxMBxH<`ZzV8X%HL zpHH&6p@z(Io@=$vZlD4h z3)b6`RF46?O-I+)uV0e@(YBp%QU~52>{zoHP}wtaDsxdcF`d?0jolf?VbRqXt1(=OYnxWdDJXqiN zqSFjOEr+_286g*+kNpBTxiNu(Ye=aG&XT!ohh906><0 zDgAB1aMXTCP6m|k?bUVBT0n?1w$KM{V#lA1UP$52YPA0o%X+cina|eHrVYjvc=W&SJlUGX&6%k_7+EBRN*Pis9F6{GP>3H`bK7YMr zF}1i@T@lz>3X&%P#X#oAWeT4rmtiur=zbXMjDt>>0c4EjEWgH^qoEYbUyP>u$x4h3aIMobS5xlJ{&i+dv(iWrAF%gCN_dOo`d+x zOINO3xgaYqpMNEeo?ACj*<&hlid)JOlyP}uY01KCK=(FoOO}ot00$p+VGs!66S-SMGQ`^^!1$vXFd~NbLw8}5zqEf5WhZ(yBdJajF8a-l$BS&3X271`L?6oES1d_ zr+L4N1R^~^t8dw>k4y95-)Mu23Y|3a5*+C($4>XK8+mep5zHEhyQ>(DD2`AW_K`*? zU_p-^|8n{Wxse<3RlsaGL|NgPUIUVpMdG*&`!_DlYqqk$^~0i#C*mmN*0K7M5;F+E zIQ6>xy;^CGo|6B)&o71A$3ar}P^)0JJxiIdUYv>-T^2YgvpyFAHe;JB3c_5 zz3|L^;0W8}j-&aqf|dI#Ksfsce_4B0)Ks`$$R6oM+p8DRwA9^f0$K-xF!0e(8OZtCZX{p?8=o-*;eyfB8TXVsAa0@A1wJH4B5QZvxUHB0s& z{bO_}+i*bqqQH>32>jzgv_;T`r`CLf3lBMZt7liADs?MCc1;20OQetMBqG4(S$!+0 z%8RbrHrFiP{8Pl25CTLqL+zmxxd<1CD7)^n1E_90DiLLr1CesNy5J+n=nRodFPeyA zvEqrmD(h7ARudc7poN@r;f}u2T5i7MNER=znBV1iBEO1^2BGw_o}PXzyfvE06KrT> zsX`<2qYSgQTeGq~KoXzdDFi5CoXYL9p1cItR zYZA+VAmI;!$4%^QRDoaySyfg0KT1%8sQH7S<~CPz&pd>ns@{Gl3M4dk?c5TG5Y*7p zB9;~E>l1)TsJVl^27G58QdigcgILIxV@H6k{5)qruKexK$Fn?ff4f*s#E$>-^@o4H z{O@=554AxG_wj$L&smSAx=Fg5W@l_b17?$PA)@rhsqqq(^BvJvJ$n5F+Wywdy!UdhO+=>DBoGV!iTN}7LEMNp==@oxAgxm`baC9mUu zqp>mjS0x(4D?CPaTUuk|F@J;~Ix4bT!Ydl9!R)#9gwVM2| z<(Aeza7ziJ4*lI9GAF3?ivdrZYexp{fmYG8JEu1XeG&2pcY*RIXDB!pYqDoO+= zFE7g=gd@6Y7`&~!*8zjoDEy8W6D%@lo|%%)iHbOG1NgwqN4&ZN>oLz)#U#jua2(vN<0w0ncJ;l_Fwfb zv-ez)NMCDf|5fIXdo?0l+;#uw?3I-o)ZV|!URiNz{OkPsPk)_1{ny$5llwm$`2Wj+ mf2lfuCppvb_w>I?-TOEC;riG5Rg2OhPSV~B? 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�_A+OZH<{J< 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 diff --git a/public/icons/icon.ico b/public/icons/icon.ico index 567b77b0617dec64e32bbffd17015e870da2aaf8..9b270324558ea386b3af9583b73d1512378d8c41 100644 GIT binary patch literal 361102 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 literal 361102 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 z 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 literal 0 HcmV?d00001 diff --git a/src/backend/dashboard.ts b/src/backend/dashboard.ts index f875a038..207f7248 100644 --- a/src/backend/dashboard.ts +++ b/src/backend/dashboard.ts @@ -27,12 +27,7 @@ app.use( origin: (origin, callback) => { if (!origin) return callback(null, true); - const allowedOrigins = [ - "http://localhost:5173", - "http://localhost:3000", - "http://127.0.0.1:5173", - "http://127.0.0.1:3000", - ]; + const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; if (allowedOrigins.includes(origin)) { return callback(null, true); @@ -90,6 +85,9 @@ app.use(authManager.createAuthMiddleware()); */ app.get("/uptime", async (req, res) => { try { + const startTime = Date.now(); + const userId = (req as AuthenticatedRequest).userId; + const uptimeMs = Date.now() - serverStartTime; const uptimeSeconds = Math.floor(uptimeMs / 1000); const days = Math.floor(uptimeSeconds / 86400); diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index e4d25af6..22e9c232 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -25,6 +25,7 @@ import { UserDataExport } from "../utils/user-data-export.js"; import { AutoSSLSetup } from "../utils/auto-ssl-setup.js"; import { eq, and } from "drizzle-orm"; import { parseUserAgent } from "../utils/user-agent-parser.js"; +import { getProxyAgent } from "../utils/proxy-agent.js"; import { users, sshData, @@ -42,7 +43,7 @@ import type { GitHubAPIResponse, AuthenticatedRequest, } from "../../types/index.js"; -import { getDb } from "./db/index.js"; +import { getDb, DatabaseSaveTrigger } from "./db/index.js"; import Database from "better-sqlite3"; const app = express(); @@ -57,12 +58,7 @@ app.use( origin: (origin, callback) => { if (!origin) return callback(null, true); - const allowedOrigins = [ - "http://localhost:5173", - "http://localhost:3000", - "http://127.0.0.1:5173", - "http://127.0.0.1:3000", - ]; + const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; if (allowedOrigins.includes(origin)) { return callback(null, true); @@ -166,12 +162,14 @@ async function fetchGitHubAPI( } try { - const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, { + const url = `${GITHUB_API_BASE}${endpoint}`; + const response = await fetch(url, { headers: { Accept: "application/vnd.github+json", "User-Agent": "TermixUpdateChecker/1.0", "X-GitHub-Api-Version": "2022-11-28", }, + agent: getProxyAgent(url), }); if (!response.ok) { @@ -696,19 +694,39 @@ app.post("/database/export", authenticateJWT, async (req, res) => { tags TEXT, pin INTEGER NOT NULL DEFAULT 0, auth_type TEXT NOT NULL, + force_keyboard_interactive TEXT, password TEXT, key TEXT, key_password TEXT, key_type TEXT, + sudo_password TEXT, autostart_password TEXT, autostart_key TEXT, autostart_key_password TEXT, credential_id INTEGER, + override_credential_username INTEGER, enable_terminal INTEGER NOT NULL DEFAULT 1, enable_tunnel INTEGER NOT NULL DEFAULT 1, tunnel_connections TEXT, + jump_hosts TEXT, enable_file_manager INTEGER NOT NULL DEFAULT 1, + enable_docker INTEGER NOT NULL DEFAULT 0, + show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1, + show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0, + show_tunnel_in_sidebar INTEGER NOT NULL DEFAULT 0, + show_docker_in_sidebar INTEGER NOT NULL DEFAULT 0, + show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0, default_path TEXT, + stats_config TEXT, + terminal_config TEXT, + quick_actions TEXT, + notes TEXT, + use_socks5 INTEGER, + socks5_host TEXT, + socks5_port INTEGER, + socks5_username TEXT, + socks5_password TEXT, + socks5_proxy_chain TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -808,8 +826,8 @@ app.post("/database/export", authenticateJWT, async (req, res) => { .from(sshData) .where(eq(sshData.userId, userId)); const insertHost = exportDb.prepare(` - INSERT INTO ssh_data (id, user_id, name, ip, port, username, folder, tags, pin, auth_type, password, key, key_password, key_type, autostart_password, autostart_key, autostart_key_password, credential_id, enable_terminal, enable_tunnel, tunnel_connections, enable_file_manager, default_path, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO ssh_data (id, user_id, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, stats_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); for (const host of sshHosts) { @@ -830,19 +848,39 @@ app.post("/database/export", authenticateJWT, async (req, res) => { decrypted.tags || null, decrypted.pin ? 1 : 0, decrypted.authType, + decrypted.forceKeyboardInteractive || null, decrypted.password || null, decrypted.key || null, decrypted.key_password || null, decrypted.keyType || null, + decrypted.sudoPassword || null, decrypted.autostartPassword || null, decrypted.autostartKey || null, decrypted.autostartKeyPassword || null, decrypted.credentialId || null, - decrypted.enableTerminal ? 1 : 0, - decrypted.enableTunnel ? 1 : 0, + Boolean(decrypted.overrideCredentialUsername) ? 1 : 0, + Boolean(decrypted.enableTerminal) ? 1 : 0, + Boolean(decrypted.enableTunnel) ? 1 : 0, decrypted.tunnelConnections || null, - decrypted.enableFileManager ? 1 : 0, + decrypted.jumpHosts || null, + Boolean(decrypted.enableFileManager) ? 1 : 0, + Boolean(decrypted.enableDocker) ? 1 : 0, + Boolean(decrypted.showTerminalInSidebar) ? 1 : 0, + Boolean(decrypted.showFileManagerInSidebar) ? 1 : 0, + Boolean(decrypted.showTunnelInSidebar) ? 1 : 0, + Boolean(decrypted.showDockerInSidebar) ? 1 : 0, + Boolean(decrypted.showServerStatsInSidebar) ? 1 : 0, decrypted.defaultPath || null, + decrypted.statsConfig || null, + decrypted.terminalConfig || null, + decrypted.quickActions || null, + decrypted.notes || null, + Boolean(decrypted.useSocks5) ? 1 : 0, + decrypted.socks5Host || null, + decrypted.socks5Port || null, + decrypted.socks5Username || null, + decrypted.socks5Password || null, + decrypted.socks5ProxyChain || null, decrypted.createdAt, decrypted.updatedAt, ); @@ -1231,19 +1269,45 @@ app.post( tags: host.tags, pin: Boolean(host.pin), authType: host.auth_type, + forceKeyboardInteractive: host.force_keyboard_interactive, password: host.password, key: host.key, keyPassword: host.key_password, keyType: host.key_type, + sudoPassword: host.sudo_password, autostartPassword: host.autostart_password, autostartKey: host.autostart_key, autostartKeyPassword: host.autostart_key_password, - credentialId: null, + credentialId: host.credential_id || null, + overrideCredentialUsername: Boolean( + host.override_credential_username, + ), enableTerminal: Boolean(host.enable_terminal), enableTunnel: Boolean(host.enable_tunnel), tunnelConnections: host.tunnel_connections, + jumpHosts: host.jump_hosts, enableFileManager: Boolean(host.enable_file_manager), + enableDocker: Boolean(host.enable_docker), + showTerminalInSidebar: Boolean(host.show_terminal_in_sidebar), + showFileManagerInSidebar: Boolean( + host.show_file_manager_in_sidebar, + ), + showTunnelInSidebar: Boolean(host.show_tunnel_in_sidebar), + showDockerInSidebar: Boolean(host.show_docker_in_sidebar), + showServerStatsInSidebar: Boolean( + host.show_server_stats_in_sidebar, + ), defaultPath: host.default_path, + statsConfig: host.stats_config, + terminalConfig: host.terminal_config, + quickActions: host.quick_actions, + notes: host.notes, + useSocks5: Boolean(host.use_socks5), + socks5Host: host.socks5_host, + socks5Port: host.socks5_port, + socks5Username: host.socks5_username, + socks5Password: host.socks5_password, + socks5ProxyChain: host.socks5_proxy_chain, createdAt: host.created_at || new Date().toISOString(), updatedAt: new Date().toISOString(), }; @@ -1483,6 +1547,19 @@ app.post( } result.success = true; + + try { + await DatabaseSaveTrigger.forceSave("database_import"); + } catch (saveError) { + apiLogger.error( + "Failed to persist imported data to disk", + saveError, + { + operation: "import_force_save_failed", + userId, + }, + ); + } } finally { if (importDb) { importDb.close(); diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 3d031dc3..11afd2f4 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -270,7 +270,7 @@ async function initializeCompleteDatabase(): Promise { folder TEXT, tags TEXT, auth_type TEXT NOT NULL, - username TEXT NOT NULL, + username TEXT, password TEXT, key TEXT, key_password TEXT, @@ -414,9 +414,11 @@ async function initializeCompleteDatabase(): Promise { `); try { - sqlite.prepare("DELETE FROM sessions").run(); + sqlite + .prepare("DELETE FROM sessions WHERE expires_at < datetime('now')") + .run(); } catch (e) { - databaseLogger.warn("Could not clear sessions on startup", { + databaseLogger.warn("Could not clear expired sessions on startup", { operation: "db_init_session_cleanup_failed", error: e, }); @@ -585,6 +587,13 @@ const migrateSchema = () => { addColumnIfNotExists("ssh_data", "socks5_password", "TEXT"); addColumnIfNotExists("ssh_data", "socks5_proxy_chain", "TEXT"); + addColumnIfNotExists("ssh_data", "host_key_fingerprint", "TEXT"); + addColumnIfNotExists("ssh_data", "host_key_type", "TEXT"); + addColumnIfNotExists("ssh_data", "host_key_algorithm", "TEXT DEFAULT 'sha256'"); + addColumnIfNotExists("ssh_data", "host_key_first_seen", "TEXT"); + addColumnIfNotExists("ssh_data", "host_key_last_verified", "TEXT"); + addColumnIfNotExists("ssh_data", "host_key_changed_count", "INTEGER DEFAULT 0"); + addColumnIfNotExists( "ssh_data", "show_terminal_in_sidebar", @@ -619,6 +628,68 @@ const migrateSchema = () => { addColumnIfNotExists("ssh_credentials", "system_key", "TEXT"); addColumnIfNotExists("ssh_credentials", "system_key_password", "TEXT"); + try { + const tableInfo = sqlite.prepare("PRAGMA table_info(ssh_credentials)").all() as Array<{ + cid: number; + name: string; + type: string; + notnull: number; + dflt_value: string | null; + pk: number; + }>; + const usernameCol = tableInfo.find((col) => col.name === "username"); + + if (usernameCol && usernameCol.notnull === 1) { + const tempTableName = "ssh_credentials_temp_migration"; + const allColumns = tableInfo.map((col) => col.name).join(", "); + + sqlite.exec(`PRAGMA foreign_keys = OFF`); + sqlite.exec(` + CREATE TABLE ${tempTableName} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + folder TEXT, + tags TEXT, + auth_type TEXT NOT NULL, + username TEXT, + password TEXT, + key TEXT, + key_password TEXT, + key_type TEXT, + usage_count INTEGER NOT NULL DEFAULT 0, + last_used TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + private_key TEXT, + public_key TEXT, + detected_key_type TEXT, + system_password TEXT, + system_key TEXT, + system_key_password TEXT, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + + INSERT INTO ${tempTableName} SELECT ${allColumns} FROM ssh_credentials; + + DROP TABLE ssh_credentials; + + ALTER TABLE ${tempTableName} RENAME TO ssh_credentials; + `); + sqlite.exec(`PRAGMA foreign_keys = ON`); + + databaseLogger.info("Successfully migrated ssh_credentials table to remove username NOT NULL constraint", { + operation: "schema_migration_username_nullable", + }); + } + } catch (migrationError) { + databaseLogger.warn("Failed to migrate ssh_credentials username column", { + operation: "schema_migration", + error: migrationError, + }); + } + addColumnIfNotExists("file_manager_recent", "host_id", "INTEGER NOT NULL"); addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL"); addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL"); @@ -925,6 +996,37 @@ const migrateSchema = () => { } } + try { + sqlite.prepare("SELECT id FROM opkssh_tokens LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS opkssh_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + host_id INTEGER NOT NULL, + ssh_cert TEXT NOT NULL, + private_key TEXT NOT NULL, + email TEXT, + sub TEXT, + issuer TEXT, + audience TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TEXT NOT NULL, + last_used TEXT, + UNIQUE(user_id, host_id), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create opkssh_tokens table", { + operation: "schema_migration", + error: createError, + }); + } + } + try { const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>; diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 5ab3153e..474f0a31 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -118,6 +118,13 @@ export const sshData = sqliteTable("ssh_data", { socks5Password: text("socks5_password"), socks5ProxyChain: text("socks5_proxy_chain"), + hostKeyFingerprint: text("host_key_fingerprint"), + hostKeyType: text("host_key_type"), + hostKeyAlgorithm: text("host_key_algorithm").default("sha256"), + hostKeyFirstSeen: text("host_key_first_seen"), + hostKeyLastVerified: text("host_key_last_verified"), + hostKeyChangedCount: integer("host_key_changed_count").default(0), + createdAt: text("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), @@ -192,7 +199,7 @@ export const sshCredentials = sqliteTable("ssh_credentials", { folder: text("folder"), tags: text("tags"), authType: text("auth_type").notNull(), - username: text("username").notNull(), + username: text("username"), password: text("password"), key: text("key", { length: 16384 }), private_key: text("private_key", { length: 16384 }), @@ -491,3 +498,27 @@ export const sessionRecordings = sqliteTable("session_recordings", { .default(false), terminationReason: text("termination_reason"), }); + +export const opksshTokens = sqliteTable("opkssh_tokens", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + hostId: integer("host_id") + .notNull() + .references(() => sshData.id, { onDelete: "cascade" }), + + sshCert: text("ssh_cert", { length: 8192 }).notNull(), + privateKey: text("private_key", { length: 8192 }).notNull(), + + email: text("email"), + sub: text("sub"), + issuer: text("issuer"), + audience: text("audience"), + + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + expiresAt: text("expires_at").notNull(), + lastUsed: text("last_used"), +}); diff --git a/src/backend/database/routes/alerts.ts b/src/backend/database/routes/alerts.ts index 16ada79f..fe9d8dbb 100644 --- a/src/backend/database/routes/alerts.ts +++ b/src/backend/database/routes/alerts.ts @@ -10,6 +10,7 @@ import { eq, and } from "drizzle-orm"; import fetch from "node-fetch"; import { authLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; +import { getProxyAgent } from "../../utils/proxy-agent.js"; class AlertCache { private cache: Map = new Map(); @@ -60,6 +61,7 @@ async function fetchAlertsFromGitHub(): Promise { Accept: "application/json", "User-Agent": "TermixAlertChecker/1.0", }, + agent: getProxyAgent(url), }); if (!response.ok) { diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts index ca20719a..dacd6854 100644 --- a/src/backend/database/routes/credentials.ts +++ b/src/backend/database/routes/credentials.ts @@ -149,18 +149,13 @@ router.post( keyType, } = req.body; - if ( - !isNonEmptyString(userId) || - !isNonEmptyString(name) || - !isNonEmptyString(username) - ) { + if (!isNonEmptyString(userId) || !isNonEmptyString(name)) { authLogger.warn("Invalid credential creation data validation failed", { operation: "credential_create", userId, hasName: !!name, - hasUsername: !!username, }); - return res.status(400).json({ error: "Name and username are required" }); + return res.status(400).json({ error: "Name is required" }); } if (!["password", "key"].includes(authType)) { @@ -227,7 +222,7 @@ router.post( folder: folder?.trim() || null, tags: Array.isArray(tags) ? tags.join(",") : tags || "", authType, - username: username.trim(), + username: username?.trim() || null, password: plainPassword, key: plainKey, private_key: keyInfo?.privateKey || plainKey, @@ -508,6 +503,12 @@ router.put( authLogger.warn("Invalid request for credential update"); return res.status(400).json({ error: "Invalid request" }); } + authLogger.info("Updating SSH credential", { + operation: "credential_update", + userId, + credentialId: parseInt(id), + changes: Object.keys(updateData), + }); try { const existing = await db @@ -538,7 +539,7 @@ router.put( : updateData.tags || ""; } if (updateData.username !== undefined) - updateFields.username = updateData.username.trim(); + updateFields.username = updateData.username?.trim() || null; if (updateData.authType !== undefined) updateFields.authType = updateData.authType; if (updateData.keyType !== undefined) @@ -614,17 +615,11 @@ router.put( ); const credential = updated[0]; - authLogger.success( - `SSH credential updated: ${credential.name} (${credential.authType}) by user ${userId}`, - { - operation: "credential_update_success", - userId, - credentialId: parseInt(id), - name: credential.name, - authType: credential.authType, - username: credential.username, - }, - ); + authLogger.success("SSH credential updated", { + operation: "credential_update_success", + userId, + credentialId: parseInt(id), + }); res.json(formatCredentialOutput(updated[0])); } catch (err) { @@ -673,6 +668,11 @@ router.delete( authLogger.warn("Invalid request for credential deletion"); return res.status(400).json({ error: "Invalid request" }); } + authLogger.info("Deleting SSH credential", { + operation: "credential_delete", + userId, + credentialId: parseInt(id), + }); try { const credentialToDelete = await db @@ -752,17 +752,11 @@ router.delete( ); const credential = credentialToDelete[0]; - authLogger.success( - `SSH credential deleted: ${credential.name} (${credential.authType}) by user ${userId}`, - { - operation: "credential_delete_success", - userId, - credentialId: parseInt(id), - name: credential.name, - authType: credential.authType, - username: credential.username, - }, - ); + authLogger.success("SSH credential deleted", { + operation: "credential_delete_success", + userId, + credentialId: parseInt(id), + }); res.json({ message: "Credential deleted successfully" }); } catch (err) { @@ -846,7 +840,7 @@ router.post( .update(sshData) .set({ credentialId: parseInt(credentialId), - username: credential.username as string, + username: (credential.username as string) || "", authType: (credential.auth_type || credential.authType) as string, password: null, key: null, @@ -961,7 +955,7 @@ function formatCredentialOutput( : [] : [], authType: credential.authType || credential.auth_type, - username: credential.username, + username: credential.username || null, publicKey: credential.public_key || credential.publicKey, keyType: credential.key_type || credential.keyType, detectedKeyType: credential.detected_key_type || credential.detectedKeyType, diff --git a/src/backend/database/routes/network-topology.ts b/src/backend/database/routes/network-topology.ts index b21e27ab..d6683124 100644 --- a/src/backend/database/routes/network-topology.ts +++ b/src/backend/database/routes/network-topology.ts @@ -4,6 +4,7 @@ import { getDb } from "../db/index.js"; import { networkTopology } from "../db/schema.js"; import { AuthManager } from "../../utils/auth-manager.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; +import { databaseLogger } from "../../utils/logger.js"; const router = express.Router(); const authManager = AuthManager.getInstance(); @@ -83,7 +84,10 @@ router.get( return res.json(null); } } catch (error) { - console.error("Error fetching network topology:", error); + databaseLogger.error("Failed to fetch network topology", error, { + operation: "network_topology_fetch", + userId: (req as AuthenticatedRequest).userId, + }); return res.status(500).json({ error: "Failed to fetch network topology", details: (error as Error).message, @@ -176,7 +180,10 @@ router.post( return res.json({ success: true }); } catch (error) { - console.error("Error saving network topology:", error); + databaseLogger.error("Failed to save network topology", error, { + operation: "network_topology_save", + userId: (req as AuthenticatedRequest).userId, + }); return res.status(500).json({ error: "Failed to save network topology", details: (error as Error).message, diff --git a/src/backend/database/routes/rbac.ts b/src/backend/database/routes/rbac.ts index bf72755a..03879bab 100644 --- a/src/backend/database/routes/rbac.ts +++ b/src/backend/database/routes/rbac.ts @@ -117,10 +117,12 @@ router.post( .limit(1); if (host.length === 0) { - databaseLogger.warn("Attempt to share host not owned by user", { - operation: "share_host", + databaseLogger.warn("Permission denied", { + operation: "rbac_permission_denied", userId, - hostId, + resource: "host", + resourceId: hostId, + action: "share", }); return res.status(403).json({ error: "Not host owner" }); } @@ -218,6 +220,13 @@ router.post( userId, ); } + databaseLogger.info("Permission granted", { + operation: "rbac_permission_grant", + adminId: userId, + hostId, + resource: "host", + action: "view", + }); return res.json({ success: true, @@ -254,6 +263,13 @@ router.post( userId, ); } + databaseLogger.success("Host shared successfully", { + operation: "rbac_host_share_success", + userId, + hostId, + targetUserId: targetType === "user" ? targetUserId : undefined, + permissionLevel, + }); res.json({ success: true, @@ -328,6 +344,12 @@ router.delete( } await db.delete(hostAccess).where(eq(hostAccess.id, accessId)); + databaseLogger.info("Permission revoked", { + operation: "rbac_permission_revoke", + adminId: userId, + hostId, + accessId, + }); res.json({ success: true, message: "Access revoked" }); } catch (error) { @@ -990,6 +1012,13 @@ router.post( } permissionManager.invalidateUserPermissionCache(targetUserId); + databaseLogger.info("Role assigned to user", { + operation: "rbac_role_assign", + adminId: currentUserId, + targetUserId, + roleId, + roleName: role[0].name, + }); res.json({ success: true, @@ -1082,6 +1111,12 @@ router.delete( ); permissionManager.invalidateUserPermissionCache(targetUserId); + databaseLogger.info("Role removed from user", { + operation: "rbac_role_remove", + adminId: req.userId!, + targetUserId, + roleId, + }); res.json({ success: true, diff --git a/src/backend/database/routes/snippets.ts b/src/backend/database/routes/snippets.ts index 6d3a2228..a7cf8ed4 100644 --- a/src/backend/database/routes/snippets.ts +++ b/src/backend/database/routes/snippets.ts @@ -4,7 +4,7 @@ import { db } from "../db/index.js"; import { snippets, snippetFolders } from "../db/schema.js"; import { eq, and, desc, asc, sql } from "drizzle-orm"; import type { Request, Response } from "express"; -import { authLogger } from "../../utils/logger.js"; +import { authLogger, databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; const router = express.Router(); @@ -1043,9 +1043,8 @@ router.post( }; const result = await db.insert(snippets).values(insertData).returning(); - - authLogger.success(`Snippet created: ${name} by user ${userId}`, { - operation: "snippet_create_success", + databaseLogger.info("Command snippet created", { + operation: "snippet_create", userId, snippetId: result[0].id, name, @@ -1156,16 +1155,11 @@ router.put( .select() .from(snippets) .where(eq(snippets.id, parseInt(id))); - - authLogger.success( - `Snippet updated: ${updated[0].name} by user ${userId}`, - { - operation: "snippet_update_success", - userId, - snippetId: parseInt(id), - name: updated[0].name, - }, - ); + databaseLogger.info("Command snippet updated", { + operation: "snippet_update", + userId, + snippetId: parseInt(id), + }); res.json(updated[0]); } catch (err) { @@ -1227,16 +1221,11 @@ router.delete( await db .delete(snippets) .where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId))); - - authLogger.success( - `Snippet deleted: ${existing[0].name} by user ${userId}`, - { - operation: "snippet_delete_success", - userId, - snippetId: parseInt(id), - name: existing[0].name, - }, - ); + databaseLogger.info("Command snippet deleted", { + operation: "snippet_delete", + userId, + snippetId: parseInt(id), + }); res.json({ success: true }); } catch (err) { diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index dad37113..5a8a0d1b 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -28,7 +28,7 @@ import { } from "drizzle-orm"; import type { Request, Response } from "express"; import multer from "multer"; -import { sshLogger } from "../../utils/logger.js"; +import { sshLogger, databaseLogger } from "../../utils/logger.js"; import { SimpleDBOps } from "../../utils/simple-db-ops.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { PermissionManager } from "../../utils/permission-manager.js"; @@ -374,6 +374,12 @@ router.post( socks5ProxyChain, overrideCredentialUsername, } = hostData; + databaseLogger.info("Creating SSH host", { + operation: "host_create", + userId, + name, + ip, + }); if ( !isNonEmptyString(userId) || @@ -509,19 +515,12 @@ router.post( const resolvedHost = (await resolveHostCredentials(baseHost, userId)) || baseHost; - - sshLogger.success( - `SSH host created: ${name} (${ip}:${port}) by user ${userId}`, - { - operation: "host_create_success", - userId, - hostId: createdHost.id as number, - name, - ip, - port, - authType: effectiveAuthType, - }, - ); + databaseLogger.success("SSH host created", { + operation: "host_create_success", + userId, + hostId: createdHost.id as number, + name, + }); try { const axios = (await import("axios")).default; @@ -857,6 +856,12 @@ router.put( socks5ProxyChain, overrideCredentialUsername, } = hostData; + databaseLogger.info("Updating SSH host", { + operation: "host_update", + userId, + hostId: parseInt(hostId), + changes: Object.keys(hostData), + }); if ( !isNonEmptyString(userId) || @@ -1088,19 +1093,11 @@ router.put( const resolvedHost = (await resolveHostCredentials(baseHost, userId)) || baseHost; - - sshLogger.success( - `SSH host updated: ${name} (${ip}:${port}) by user ${userId}`, - { - operation: "host_update_success", - userId, - hostId: parseInt(hostId), - name, - ip, - port, - authType: effectiveAuthType, - }, - ); + databaseLogger.success("SSH host updated", { + operation: "host_update_success", + userId, + hostId: parseInt(hostId), + }); try { const axios = (await import("axios")).default; @@ -1530,6 +1527,11 @@ router.delete( }); return res.status(400).json({ error: "Invalid userId or id" }); } + databaseLogger.info("Deleting SSH host", { + operation: "host_delete", + userId, + hostId: parseInt(hostId), + }); try { const hostToDelete = await db .select() @@ -1582,17 +1584,11 @@ router.delete( .where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId))); const host = hostToDelete[0]; - sshLogger.success( - `SSH host deleted: ${host.name} (${host.ip}:${host.port}) by user ${userId}`, - { - operation: "host_delete_success", - userId, - hostId: parseInt(hostId), - name: host.name, - ip: host.ip, - port: host.port, - }, - ); + databaseLogger.success("SSH host deleted", { + operation: "host_delete_success", + userId, + hostId: parseInt(hostId), + }); try { const axios = (await import("axios")).default; @@ -2373,7 +2369,6 @@ async function resolveHostCredentials( if (sharedCred) { const resolvedHost: Record = { ...host, - authType: sharedCred.authType, password: sharedCred.password, key: sharedCred.key, keyPassword: sharedCred.keyPassword, @@ -2420,7 +2415,6 @@ async function resolveHostCredentials( const credential = credentials[0]; const resolvedHost: Record = { ...host, - authType: credential.auth_type || credential.authType, password: credential.password, key: credential.key, keyPassword: credential.key_password || credential.keyPassword, @@ -2638,6 +2632,11 @@ router.put( .limit(1); if (existing.length > 0) { + databaseLogger.info("Updating SSH folder", { + operation: "folder_update", + userId, + folderId: existing[0].id, + }); await db .update(sshFolders) .set({ @@ -2647,6 +2646,11 @@ router.put( }) .where(and(eq(sshFolders.userId, userId), eq(sshFolders.name, name))); } else { + databaseLogger.info("Creating SSH folder", { + operation: "folder_create", + userId, + name, + }); await db.insert(sshFolders).values({ userId, name, @@ -2705,6 +2709,11 @@ router.delete( if (!isNonEmptyString(userId) || !folderName) { return res.status(400).json({ error: "Invalid folder name" }); } + databaseLogger.info("Deleting SSH folder", { + operation: "folder_delete", + userId, + folderId: folderName, + }); try { const hostsToDelete = await db @@ -2879,11 +2888,13 @@ router.post( } if ( - !["password", "key", "credential", "none"].includes(hostData.authType) + !["password", "key", "credential", "none", "opkssh"].includes( + hostData.authType, + ) ) { results.failed++; results.errors.push( - `Host ${i + 1}: Invalid authType. Must be 'password', 'key', 'credential', or 'none'`, + `Host ${i + 1}: Invalid authType. Must be 'password', 'key', 'credential', 'none', or 'opkssh'`, ); continue; } @@ -3297,4 +3308,835 @@ router.get( }, ); +/** + * @openapi + * /ssh/opkssh/token/{hostId}: + * get: + * summary: Get OPKSSH token status for a host + * tags: [SSH] + * security: + * - bearerAuth: [] + * parameters: + * - name: hostId + * in: path + * required: true + * schema: + * type: integer + * description: Host ID + * responses: + * 200: + * description: Token status retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * exists: + * type: boolean + * description: Whether a valid token exists + * expiresAt: + * type: string + * format: date-time + * description: Token expiration timestamp + * email: + * type: string + * description: User email from OIDC identity + * 404: + * description: No valid token found + * 500: + * description: Internal server error + */ +router.get( + "/ssh/opkssh/token/:hostId", + authenticateJWT, + requireDataAccess, + async (req: AuthenticatedRequest, res: Response) => { + const userId = req.userId; + const hostId = parseInt( + Array.isArray(req.params.hostId) + ? req.params.hostId[0] + : req.params.hostId, + ); + + if (!userId || isNaN(hostId)) { + return res.status(400).json({ error: "Invalid request" }); + } + + try { + const { opksshTokens } = await import("../db/schema.js"); + const token = await db + .select() + .from(opksshTokens) + .where( + and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)), + ) + .limit(1); + + if (!token || token.length === 0) { + return res.status(404).json({ exists: false }); + } + + const tokenData = token[0]; + const expiresAt = new Date(tokenData.expiresAt); + + if (expiresAt < new Date()) { + await db + .delete(opksshTokens) + .where( + and( + eq(opksshTokens.userId, userId), + eq(opksshTokens.hostId, hostId), + ), + ); + return res.status(404).json({ exists: false }); + } + + res.json({ + exists: true, + expiresAt: tokenData.expiresAt, + email: tokenData.email, + }); + } catch (error) { + sshLogger.error("Error retrieving OPKSSH token status", error, { + operation: "opkssh_token_status_error", + userId, + hostId, + }); + res.status(500).json({ error: "Internal server error" }); + } + }, +); + +/** + * @openapi + * /ssh/opkssh/token/{hostId}: + * delete: + * summary: Delete OPKSSH token for a host + * tags: [SSH] + * security: + * - bearerAuth: [] + * parameters: + * - name: hostId + * in: path + * required: true + * schema: + * type: integer + * description: Host ID + * responses: + * 200: + * description: Token deleted successfully + * 500: + * description: Internal server error + */ +router.delete( + "/ssh/opkssh/token/:hostId", + authenticateJWT, + requireDataAccess, + async (req: AuthenticatedRequest, res: Response) => { + const userId = req.userId; + const hostId = parseInt( + Array.isArray(req.params.hostId) + ? req.params.hostId[0] + : req.params.hostId, + ); + + if (!userId || isNaN(hostId)) { + return res.status(400).json({ error: "Invalid request" }); + } + + try { + const { deleteOPKSSHToken } = await import("../../ssh/opkssh-auth.js"); + await deleteOPKSSHToken(userId, hostId); + res.json({ success: true }); + } catch (error) { + sshLogger.error("Error deleting OPKSSH token", error, { + operation: "opkssh_token_delete_error", + userId, + hostId, + }); + res.status(500).json({ error: "Internal server error" }); + } + }, +); + +function rewriteOPKSSHHtml( + html: string, + requestId: string, + routePrefix: "opkssh-chooser" | "opkssh-callback", +): string { + const basePath = `/ssh/${routePrefix}/${requestId}`; + + html = html.replace(/action=["']?\//g, `action="${basePath}/`); + html = html.replace(/href=["']?\//g, `href="${basePath}/`); + html = html.replace(/src=["']?\//g, `src="${basePath}/`); + + html = html.replace( + /href=["']?http:\/\/localhost:\d+\/([^"'\s]*)/g, + `href="${basePath}/$1`, + ); + html = html.replace( + /action=["']?http:\/\/localhost:\d+\/([^"'\s]*)/g, + `action="${basePath}/$1`, + ); + html = html.replace( + /src=["']?http:\/\/localhost:\d+\/([^"'\s]*)/g, + `src="${basePath}/$1`, + ); + + html = html.replace( + /(window\.location\.href\s*=\s*["'])http:\/\/localhost:\d+\/([^"']*)(["'])/g, + `$1${basePath}/$2$3`, + ); + html = html.replace( + /(window\.location\s*=\s*["'])http:\/\/localhost:\d+\/([^"']*)(["'])/g, + `$1${basePath}/$2$3`, + ); + html = html.replace( + /(fetch\(["'])http:\/\/localhost:\d+\/([^"']*)(["'])/g, + `$1${basePath}/$2$3`, + ); + + html = html.replace( + /(location\.assign\(["'])http:\/\/localhost:\d+\/([^"']*)(["']\))/g, + `$1${basePath}/$2$3`, + ); + html = html.replace( + /(location\.replace\(["'])http:\/\/localhost:\d+\/([^"']*)(["']\))/g, + `$1${basePath}/$2$3`, + ); + + html = html.replace( + /(]+http-equiv=["']refresh["'][^>]+content=["'][^;]+;\s*url=)http:\/\/localhost:\d+\/([^"']+)(["'][^>]*>)/gi, + `$1${basePath}/$2$3`, + ); + + html = html.replace( + /(data-[\w-]+=["'])http:\/\/localhost:\d+\/([^"']*)(["'])/g, + `$1${basePath}/$2$3`, + ); + + const baseTag = ``; + + if (html.includes("]*>/i, baseTag); + } else { + html = html.replace(//i, `${baseTag}`); + } + + return html; +} + +/** + * @openapi + * /opkssh-chooser/{requestId}: + * get: + * summary: Proxy OPKSSH provider chooser page and all related resources + * tags: [SSH] + * parameters: + * - name: requestId + * in: path + * required: true + * schema: + * type: string + * description: Authentication request ID + * responses: + * 200: + * description: Chooser page content + * 404: + * description: Session not found + * 500: + * description: Proxy error + */ + +router.use( + "/opkssh-chooser/:requestId", + async (req: Request, res: Response) => { + const requestId = Array.isArray(req.params.requestId) + ? req.params.requestId[0] + : req.params.requestId; + + try { + const { getActiveAuthSession } = await import("../../ssh/opkssh-auth.js"); + const session = getActiveAuthSession(requestId); + + if (!session) { + res.status(404).send(` + + + + Session Not Found + + + + +

+ + + `); + return; + } + + const axios = (await import("axios")).default; + + const fullPath = req.originalUrl || req.url; + const pathAfterRequestId = + fullPath.split(`/ssh/opkssh-chooser/${requestId}`)[1] || ""; + const targetPath = pathAfterRequestId || "/chooser"; + + if (!session.localPort || session.localPort === 0) { + sshLogger.error("OPKSSH session has no local port", { + operation: "opkssh_chooser_proxy", + requestId, + sessionStatus: session.status, + }); + res.status(500).send(` + + + + Error + + + + +
+

Authentication Error

+

Failed to load authentication page. OPKSSH process may not be ready yet. Please try again.

+
+ + + `); + return; + } + + const targetUrl = `http://localhost:${session.localPort}${targetPath}`; + + const response = await axios({ + method: req.method, + url: targetUrl, + headers: { + ...req.headers, + host: `localhost:${session.localPort}`, + }, + data: req.body, + timeout: 10000, + validateStatus: () => true, + maxRedirects: 0, + responseType: "arraybuffer", + }); + + Object.entries(response.headers).forEach(([key, value]) => { + if (key.toLowerCase() === "transfer-encoding") { + return; + } + if (key.toLowerCase() === "location") { + const location = value as string; + if (location.startsWith("/")) { + res.setHeader(key, `/ssh/opkssh-chooser/${requestId}${location}`); + } else { + const localhostMatch = location.match( + /^http:\/\/localhost:(\d+)(\/.*)?$/, + ); + if (localhostMatch) { + const port = parseInt(localhostMatch[1], 10); + const path = localhostMatch[2] || "/"; + if (session.callbackPort && port === session.callbackPort) { + res.setHeader(key, `/ssh/opkssh-callback/${requestId}${path}`); + } else if (port === session.localPort) { + res.setHeader(key, `/ssh/opkssh-chooser/${requestId}${path}`); + } else { + const isCallback = + path.includes("login") || path.includes("callback"); + const prefix = isCallback + ? "opkssh-callback" + : "opkssh-chooser"; + res.setHeader(key, `/ssh/${prefix}/${requestId}${path}`); + } + } else { + res.setHeader(key, value as string); + } + } + } else { + res.setHeader(key, value as string); + } + }); + + const contentType = response.headers["content-type"] || ""; + if (contentType.includes("text/html")) { + const html = rewriteOPKSSHHtml( + response.data.toString("utf-8"), + requestId, + "opkssh-chooser", + ); + res.status(response.status).send(html); + } else { + res.status(response.status).send(response.data); + } + } catch (error) { + sshLogger.error("Error proxying OPKSSH chooser", error, { + operation: "opkssh_chooser_proxy_error", + requestId, + }); + res.status(500).send(` + + + + Error + + + + +
+

Error

+

Failed to load authentication page. Please try again.

+
+ + + `); + } + }, +); + +/** + * @openapi + * /opkssh-callback: + * get: + * summary: Static OAuth callback from OIDC provider for OPKSSH authentication + * tags: [SSH] + * responses: + * 200: + * description: Callback processed successfully + * 404: + * description: No active authentication session found + * 500: + * description: Authentication failed + */ +router.get("/opkssh-callback", async (req: Request, res: Response) => { + try { + const { getActiveSessionsAll } = await import("../../ssh/opkssh-auth.js"); + + const allSessions = getActiveSessionsAll(); + const session = allSessions[allSessions.length - 1]; + + if (!session) { + res.status(404).send("No active authentication session found"); + return; + } + + if (!session.callbackPort) { + res.status(503).send("OPKSSH callback listener not ready yet"); + return; + } + + const axios = (await import("axios")).default; + const queryString = req.url.includes("?") + ? req.url.substring(req.url.indexOf("?")) + : ""; + const targetUrl = `http://localhost:${session.callbackPort}/login-callback${queryString}`; + + sshLogger.info("Proxying OAuth callback to OPKSSH", { + operation: "opkssh_static_callback", + callbackPort: session.callbackPort, + targetUrl, + }); + + const response = await axios({ + method: req.method, + url: targetUrl, + headers: { + ...req.headers, + host: `localhost:${session.callbackPort}`, + }, + data: req.body, + timeout: 10000, + validateStatus: () => true, + maxRedirects: 0, + responseType: "arraybuffer", + }); + + Object.entries(response.headers).forEach(([key, value]) => { + if (key.toLowerCase() !== "transfer-encoding") { + res.setHeader(key, value as string); + } + }); + + res.status(response.status).send(response.data); + } catch (error) { + sshLogger.error("Error handling OPKSSH static callback", error, { + operation: "opkssh_static_callback_error", + }); + res.status(500).send("Authentication callback failed"); + } +}); + +/** + * @openapi + * /opkssh-callback/{requestId}: + * get: + * summary: OAuth callback from OIDC provider for OPKSSH authentication (handles all sub-paths) + * tags: [SSH] + * parameters: + * - name: requestId + * in: path + * required: true + * schema: + * type: string + * description: Authentication request ID + * responses: + * 200: + * description: Callback processed successfully + * 404: + * description: Invalid authentication session + * 500: + * description: Authentication failed + */ +router.use( + "/opkssh-callback/:requestId", + async (req: Request, res: Response) => { + const requestId = Array.isArray(req.params.requestId) + ? req.params.requestId[0] + : req.params.requestId; + + try { + const { getActiveAuthSession } = await import("../../ssh/opkssh-auth.js"); + const session = getActiveAuthSession(requestId); + + if (!session) { + res.status(404).send(` + + + + Session Not Found + + + + +
+

Session Not Found

+

Authentication session expired or invalid.

+

Please close this window and try again.

+
+ + + `); + return; + } + + const axios = (await import("axios")).default; + const fullPath = req.originalUrl || req.url; + const pathAfterRequestId = + fullPath.split(`/ssh/opkssh-callback/${requestId}`)[1] || ""; + const targetPath = pathAfterRequestId || "/login-callback"; + + if (!session.callbackPort || session.callbackPort === 0) { + sshLogger.error("OPKSSH callback session has no callback port", { + operation: "opkssh_callback_proxy", + requestId, + sessionStatus: session.status, + }); + res.status(500).send(` + + + + Error + + + + +
+

Callback Error

+

OPKSSH callback listener not ready. Please try authenticating again.

+
+ + + `); + return; + } + + const targetUrl = `http://localhost:${session.callbackPort}${targetPath}`; + + const response = await axios({ + method: req.method, + url: targetUrl, + headers: { + ...req.headers, + host: `localhost:${session.callbackPort}`, + }, + data: req.body, + timeout: 10000, + validateStatus: () => true, + maxRedirects: 0, + responseType: "arraybuffer", + }); + + Object.entries(response.headers).forEach(([key, value]) => { + if (key.toLowerCase() === "transfer-encoding") { + return; + } + if (key.toLowerCase() === "location") { + const location = value as string; + if (location.startsWith("/")) { + res.setHeader(key, `/ssh/opkssh-callback/${requestId}${location}`); + } else { + res.setHeader(key, value as string); + } + } else { + res.setHeader(key, value as string); + } + }); + + const contentType = response.headers["content-type"] || ""; + if (contentType.includes("text/html")) { + const html = rewriteOPKSSHHtml( + response.data.toString("utf-8"), + requestId, + "opkssh-callback", + ); + res.status(response.status).send(html); + } else { + res.status(response.status).send(response.data); + } + } catch (error) { + sshLogger.error("Error handling OPKSSH OAuth callback", error, { + operation: "opkssh_oauth_callback_error", + requestId, + }); + + res.status(500).send(` + + + + Error + + + + +
+

Error

+

An unexpected error occurred. Please try again.

+
+ + + `); + } + }, +); + export default router; diff --git a/src/backend/database/routes/terminal.ts b/src/backend/database/routes/terminal.ts index 99a885ba..be48359e 100644 --- a/src/backend/database/routes/terminal.ts +++ b/src/backend/database/routes/terminal.ts @@ -4,7 +4,7 @@ import { db } from "../db/index.js"; import { commandHistory } from "../db/schema.js"; import { eq, and, desc, sql } from "drizzle-orm"; import type { Request, Response } from "express"; -import { authLogger } from "../../utils/logger.js"; +import { authLogger, databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; const router = express.Router(); @@ -269,9 +269,8 @@ router.delete( eq(commandHistory.hostId, hostIdNum), ), ); - - authLogger.success(`Command history cleared for host ${hostId}`, { - operation: "command_history_clear_success", + databaseLogger.info("Terminal history cleared", { + operation: "terminal_history_clear", userId, hostId: hostIdNum, }); diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index cb51fa05..10612b17 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -40,6 +40,50 @@ import { loginRateLimiter } from "../../utils/login-rate-limiter.js"; const authManager = AuthManager.getInstance(); +/** + * Get OIDC configuration from environment variables. + * Returns null if required env vars are not set. + */ +function getOIDCConfigFromEnv(): { + client_id: string; + client_secret: string; + issuer_url: string; + authorization_url: string; + token_url: string; + userinfo_url: string; + identifier_path: string; + name_path: string; + scopes: string; +} | null { + const client_id = process.env.OIDC_CLIENT_ID; + const client_secret = process.env.OIDC_CLIENT_SECRET; + const issuer_url = process.env.OIDC_ISSUER_URL; + const authorization_url = process.env.OIDC_AUTHORIZATION_URL; + const token_url = process.env.OIDC_TOKEN_URL; + + if ( + !client_id || + !client_secret || + !issuer_url || + !authorization_url || + !token_url + ) { + return null; + } + + return { + client_id, + client_secret, + issuer_url, + authorization_url, + token_url, + userinfo_url: process.env.OIDC_USERINFO_URL || "", + identifier_path: process.env.OIDC_IDENTIFIER_PATH || "sub", + name_path: process.env.OIDC_NAME_PATH || "name", + scopes: process.env.OIDC_SCOPES || "openid email profile", + }; +} + async function verifyOIDCToken( idToken: string, issuerUrl: string, @@ -256,6 +300,10 @@ router.post("/create", async (req, res) => { } const { username, password } = req.body; + authLogger.info("User registration attempt", { + operation: "user_register_attempt", + username, + }); if (!isNonEmptyString(username) || !isNonEmptyString(password)) { authLogger.warn( @@ -277,9 +325,10 @@ router.post("/create", async (req, res) => { .from(users) .where(eq(users.username, username)); if (existing && existing.length > 0) { - authLogger.warn(`Attempt to create duplicate username: ${username}`, { - operation: "user_create", + authLogger.warn("Registration failed - username exists", { + operation: "user_register_failed", username, + reason: "username_exists", }); return res.status(409).json({ error: "Username already exists" }); } @@ -367,15 +416,12 @@ router.post("/create", async (req, res) => { }); } - authLogger.success( - `Traditional user created: ${username} (is_admin: ${isFirstUser})`, - { - operation: "user_create", - username, - isAdmin: isFirstUser, - userId: id, - }, - ); + authLogger.success("User registration successful", { + operation: "user_register_success", + userId: id, + username, + isAdmin: isFirstUser, + }); res.json({ message: "User created", is_admin: isFirstUser, @@ -589,6 +635,16 @@ router.delete("/oidc-config", authenticateJWT, async (req, res) => { */ router.get("/oidc-config", async (req, res) => { try { + const envConfig = getOIDCConfigFromEnv(); + if (envConfig) { + return res.json({ + client_id: envConfig.client_id, + issuer_url: envConfig.issuer_url, + authorization_url: envConfig.authorization_url, + scopes: envConfig.scopes, + }); + } + const row = db.$client .prepare("SELECT value FROM settings WHERE key = 'oidc_config'") .get(); @@ -699,14 +755,20 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => { */ router.get("/oidc/authorize", async (req, res) => { try { - const row = db.$client - .prepare("SELECT value FROM settings WHERE key = 'oidc_config'") - .get(); - if (!row) { - return res.status(404).json({ error: "OIDC not configured" }); - } + const envConfig = getOIDCConfigFromEnv(); + let config; - const config = JSON.parse((row as Record).value as string); + if (envConfig) { + config = envConfig; + } else { + const row = db.$client + .prepare("SELECT value FROM settings WHERE key = 'oidc_config'") + .get(); + if (!row) { + return res.status(404).json({ error: "OIDC not configured" }); + } + config = JSON.parse((row as Record).value as string); + } const state = nanoid(); const nonce = nanoid(); @@ -760,6 +822,10 @@ router.get("/oidc/authorize", async (req, res) => { */ router.get("/oidc/callback", async (req, res) => { const { code, state } = req.query; + authLogger.info("OIDC login callback received", { + operation: "oidc_login_request", + state, + }); if (!isNonEmptyString(code) || !isNonEmptyString(state)) { return res.status(400).json({ error: "Code and state are required" }); @@ -791,16 +857,22 @@ router.get("/oidc/callback", async (req, res) => { .prepare("DELETE FROM settings WHERE key = ?") .run(`oidc_redirect_${state}`); - const configRow = db.$client - .prepare("SELECT value FROM settings WHERE key = 'oidc_config'") - .get(); - if (!configRow) { - return res.status(500).json({ error: "OIDC not configured" }); - } + const envConfig = getOIDCConfigFromEnv(); + let config; - const config = JSON.parse( - (configRow as Record).value as string, - ); + if (envConfig) { + config = envConfig; + } else { + const configRow = db.$client + .prepare("SELECT value FROM settings WHERE key = 'oidc_config'") + .get(); + if (!configRow) { + return res.status(500).json({ error: "OIDC not configured" }); + } + config = JSON.parse( + (configRow as Record).value as string, + ); + } const tokenResponse = await fetch(config.token_url, { method: "POST", @@ -1121,11 +1193,10 @@ router.get("/oidc/callback", async (req, res) => { deviceInfo: deviceInfo.deviceInfo, }); - authLogger.success("OIDC user authenticated", { - operation: "oidc_login_success", + authLogger.success("OIDC login successful", { + operation: "oidc_login_complete", userId: userRecord.id, - deviceType: deviceInfo.type, - deviceInfo: deviceInfo.deviceInfo, + username: userRecord.username, }); let frontendUrl = (redirectUri as string).replace( @@ -1205,6 +1276,10 @@ router.get("/oidc/callback", async (req, res) => { router.post("/login", async (req, res) => { const { username, password } = req.body; const clientIp = req.ip || req.socket.remoteAddress || "unknown"; + authLogger.info("User login request received", { + operation: "user_login_request", + username, + }); if (!isNonEmptyString(username) || !isNonEmptyString(password)) { authLogger.warn("Invalid traditional login attempt", { @@ -1361,14 +1436,12 @@ router.post("/login", async (req, res) => { loginRateLimiter.resetAttempts(clientIp, username); - authLogger.success(`User logged in successfully: ${username}`, { - operation: "user_login_success", - username, + const payload = await authManager.verifyJWTToken(token); + authLogger.success("User login successful", { + operation: "user_login_complete", userId: userRecord.id, - dataUnlocked: true, - deviceType: deviceInfo.type, - deviceInfo: deviceInfo.deviceInfo, - ip: clientIp, + username, + sessionId: payload?.sessionId, }); const response: Record = { @@ -2375,6 +2448,10 @@ router.post("/complete-reset", async (req, res) => { router.post("/change-password", authenticateJWT, async (req, res) => { const userId = (req as AuthenticatedRequest).userId; const { oldPassword, newPassword } = req.body; + authLogger.info("Password change request", { + operation: "password_change_request", + userId, + }); if (!userId) { return res.status(401).json({ error: "User not authenticated" }); @@ -2393,6 +2470,11 @@ router.post("/change-password", authenticateJWT, async (req, res) => { const isMatch = await bcrypt.compare(oldPassword, user[0].password_hash); if (!isMatch) { + authLogger.warn("Password change failed - old password incorrect", { + operation: "password_change_failed", + userId, + reason: "old_password_wrong", + }); return res.status(401).json({ error: "Incorrect current password" }); } @@ -2412,6 +2494,10 @@ router.post("/change-password", authenticateJWT, async (req, res) => { await db.update(users).set({ password_hash }).where(eq(users.id, userId)); authManager.logoutUser(userId); + authLogger.success("Password changed successfully", { + operation: "password_change_complete", + userId, + }); res.json({ message: "Password changed successfully. Please log in again." }); }); @@ -2527,9 +2613,12 @@ router.post("/make-admin", authenticateJWT, async (req, res) => { }); } - authLogger.success( - `User ${username} made admin by ${adminUser[0].username}`, - ); + authLogger.info("Admin privileges granted", { + operation: "admin_grant", + adminId: userId, + targetUserId: targetUser[0].id, + targetUsername: username, + }); res.json({ message: `User ${username} is now an admin` }); } catch (err) { authLogger.error("Failed to make user admin", err); @@ -2613,9 +2702,12 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => { }); } - authLogger.success( - `Admin status removed from ${username} by ${adminUser[0].username}`, - ); + authLogger.info("Admin privileges revoked", { + operation: "admin_revoke", + adminId: userId, + targetUserId: targetUser[0].id, + targetUsername: username, + }); res.json({ message: `Admin status removed from ${username}` }); } catch (err) { authLogger.error("Failed to remove admin status", err); @@ -2753,6 +2845,10 @@ router.post("/totp/enable", authenticateJWT, async (req, res) => { totp_backup_codes: JSON.stringify(backupCodes), }) .where(eq(users.id, userId)); + authLogger.info("Two-factor authentication enabled", { + operation: "totp_enable", + userId, + }); res.json({ message: "TOTP enabled successfully", @@ -2843,6 +2939,10 @@ router.post("/totp/disable", authenticateJWT, async (req, res) => { totp_backup_codes: null, }) .where(eq(users.id, userId)); + authLogger.info("Two-factor authentication disabled", { + operation: "totp_disable", + userId, + }); res.json({ message: "TOTP disabled successfully" }); } catch (err) { @@ -3205,9 +3305,12 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => { await deleteUserAndRelatedData(targetUserId); - authLogger.success( - `User ${username} deleted by admin ${adminUser[0].username}`, - ); + authLogger.warn("User account deleted by admin", { + operation: "admin_delete_user", + adminId: userId, + targetUserId, + targetUsername: username, + }); res.json({ message: `User ${username} deleted successfully` }); } catch (err) { authLogger.error("Failed to delete user", err); diff --git a/src/backend/ssh/auth-manager.ts b/src/backend/ssh/auth-manager.ts index 518e8916..54d7549c 100644 --- a/src/backend/ssh/auth-manager.ts +++ b/src/backend/ssh/auth-manager.ts @@ -1,6 +1,6 @@ import { Client } from "ssh2"; import type { WebSocket } from "ws"; -import { sshLogger } from "../utils/logger.js"; +import { sshLogger, authLogger } from "../utils/logger.js"; import { getDb } from "../database/db/index.js"; import { sshCredentials, sshData } from "../database/db/schema.js"; import { eq, and } from "drizzle-orm"; @@ -210,6 +210,11 @@ export class SSHAuthManager { operation: "ssh_keyboard_interactive_totp_retry", hostId: this.context.hostId, }); + authLogger.warn("TOTP verification failed for SSH session", { + operation: "terminal_totp_failed", + userId: this.context.userId, + hostId: this.context.hostId, + }); this.sendLog("auth", "warning", "Invalid TOTP code"); @@ -262,6 +267,11 @@ export class SSHAuthManager { }, 180000); this.sendLog("auth", "info", "TOTP verification required"); + authLogger.info("TOTP verification prompt sent to client", { + operation: "terminal_totp_prompt", + userId: this.context.userId, + hostId: this.context.hostId, + }); this.context.ws.send( JSON.stringify({ diff --git a/src/backend/ssh/docker-console.ts b/src/backend/ssh/docker-console.ts index d6f7a6e1..e45cb559 100644 --- a/src/backend/ssh/docker-console.ts +++ b/src/backend/ssh/docker-console.ts @@ -9,7 +9,7 @@ import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { systemLogger } from "../utils/logger.js"; import type { SSHHost } from "../../types/index.js"; -const dockerConsoleLogger = systemLogger; +const sshLogger = systemLogger; interface SSHSession { client: SSHClient; @@ -17,6 +17,7 @@ interface SSHSession { isConnected: boolean; containerId?: string; shell?: string; + hostId?: number; } const activeSessions = new Map(); @@ -121,7 +122,7 @@ async function createJumpHostChain( try { jumpHost.jumpHosts = JSON.parse(jumpHost.jumpHosts); } catch (e) { - dockerConsoleLogger.error("Failed to parse jump hosts", e, { + sshLogger.error("Failed to parse jump hosts", e, { hostId: jumpHost.id, }); jumpHost.jumpHosts = []; @@ -226,6 +227,11 @@ async function createJumpHostChain( wss.on("connection", async (ws: WebSocket, req) => { const userId = (req as any).userId; const sessionId = `docker-console-${Date.now()}-${Math.random()}`; + sshLogger.info("Docker console WebSocket connected", { + operation: "docker_console_connect", + sessionId, + userId, + }); let sshSession: SSHSession | null = null; @@ -251,7 +257,7 @@ wss.on("connection", async (ws: WebSocket, req) => { try { hostConfig.jumpHosts = JSON.parse(hostConfig.jumpHosts); } catch (e) { - dockerConsoleLogger.error("Failed to parse jump hosts", e, { + sshLogger.error("Failed to parse jump hosts", e, { hostId: hostConfig.id, }); hostConfig.jumpHosts = []; @@ -383,6 +389,7 @@ wss.on("connection", async (ws: WebSocket, req) => { stream: null, isConnected: true, containerId, + hostId: hostConfig.id, }; activeSessions.set(sessionId, sshSession); @@ -417,7 +424,7 @@ wss.on("connection", async (ws: WebSocket, req) => { ); }); } catch { - dockerConsoleLogger.warn( + sshLogger.warn( `Requested shell ${shell} not found, detecting available shell`, { operation: "shell_validation", @@ -435,6 +442,13 @@ wss.on("connection", async (ws: WebSocket, req) => { sshSession.shell = shellToUse; const execCommand = `docker exec -it ${containerId} /bin/${shellToUse}`; + sshLogger.info("Attaching to Docker container", { + operation: "docker_attach", + sessionId, + userId, + hostId: hostConfig.id, + containerId, + }); client.exec( execCommand, @@ -447,15 +461,11 @@ wss.on("connection", async (ws: WebSocket, req) => { }, (err, stream) => { if (err) { - dockerConsoleLogger.error( - "Failed to create docker exec", - err, - { - operation: "docker_exec", - sessionId, - containerId, - }, - ); + sshLogger.error("Failed to create docker exec", err, { + operation: "docker_exec", + sessionId, + containerId, + }); ws.send( JSON.stringify({ @@ -467,6 +477,13 @@ wss.on("connection", async (ws: WebSocket, req) => { } sshSession!.stream = stream; + sshLogger.success("Docker container attached", { + operation: "docker_attach_success", + sessionId, + userId, + hostId: hostConfig.id, + containerId, + }); stream.on("data", (data: Buffer) => { if (ws.readyState === WebSocket.OPEN) { @@ -510,7 +527,7 @@ wss.on("connection", async (ws: WebSocket, req) => { }, ); } catch (error) { - dockerConsoleLogger.error("Failed to connect to container", error, { + sshLogger.error("Failed to connect to container", error, { operation: "console_connect", sessionId, containerId: message.data.containerId, @@ -570,13 +587,13 @@ wss.on("connection", async (ws: WebSocket, req) => { } default: - dockerConsoleLogger.warn("Unknown message type", { + sshLogger.warn("Unknown message type", { operation: "ws_message", type: message.type, }); } } catch (error) { - dockerConsoleLogger.error("WebSocket message error", error, { + sshLogger.error("WebSocket message error", error, { operation: "ws_message", sessionId, }); @@ -591,6 +608,13 @@ wss.on("connection", async (ws: WebSocket, req) => { }); ws.on("close", () => { + sshLogger.info("Docker console disconnected", { + operation: "docker_console_disconnect", + sessionId, + userId, + hostId: sshSession?.hostId, + containerId: sshSession?.containerId, + }); if (sshSession) { if (sshSession.stream) { sshSession.stream.end(); @@ -601,7 +625,7 @@ wss.on("connection", async (ws: WebSocket, req) => { }); ws.on("error", (error) => { - dockerConsoleLogger.error("WebSocket error", error, { + sshLogger.error("WebSocket error", error, { operation: "ws_error", sessionId, }); diff --git a/src/backend/ssh/docker.ts b/src/backend/ssh/docker.ts index 9914e117..234d8cc7 100644 --- a/src/backend/ssh/docker.ts +++ b/src/backend/ssh/docker.ts @@ -13,8 +13,9 @@ import { AuthManager } from "../utils/auth-manager.js"; import { createSocks5Connection } from "../utils/socks5-helper.js"; import type { AuthenticatedRequest, SSHHost } from "../../types/index.js"; import type { LogEntry, ConnectionStage } from "../../types/connection-log.js"; +import { SSHHostKeyVerifier } from "./host-key-verifier.js"; -const dockerLogger = logger; +const sshLogger = logger; function createConnectionLog( type: "info" | "success" | "warning" | "error", @@ -79,7 +80,7 @@ function cleanupSession(sessionId: string) { const session = sshSessions[sessionId]; if (session) { if (session.activeOperations > 0) { - dockerLogger.warn( + sshLogger.warn( `Deferring session cleanup for ${sessionId} - ${session.activeOperations} active operations`, { operation: "cleanup_deferred", @@ -161,7 +162,7 @@ async function resolveJumpHost( return host; } catch (error) { - dockerLogger.error("Failed to resolve jump host", error, { + sshLogger.error("Failed to resolve jump host", error, { operation: "resolve_jump_host", hostId, userId, @@ -188,7 +189,7 @@ async function createJumpHostChain( for (let i = 0; i < jumpHostConfigs.length; i++) { if (!jumpHostConfigs[i]) { - dockerLogger.error(`Jump host ${i + 1} not found`, undefined, { + sshLogger.error(`Jump host ${i + 1} not found`, undefined, { operation: "jump_host_chain", hostId: jumpHosts[i].hostId, }); @@ -203,6 +204,15 @@ async function createJumpHostChain( const jumpClient = new SSHClient(); clients.push(jumpClient); + const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier( + jumpHostConfig.id, + jumpHostConfig.ip, + jumpHostConfig.port || 22, + null, + userId, + true, + ); + const connected = await new Promise((resolve) => { const timeout = setTimeout(() => { resolve(false); @@ -215,7 +225,7 @@ async function createJumpHostChain( jumpClient.on("error", (err) => { clearTimeout(timeout); - dockerLogger.error(`Jump host ${i + 1} connection failed`, err, { + sshLogger.error(`Jump host ${i + 1} connection failed`, err, { operation: "jump_host_connect", hostId: jumpHostConfig.id, ip: jumpHostConfig.ip, @@ -229,6 +239,7 @@ async function createJumpHostChain( username: jumpHostConfig.username, tryKeyboard: true, readyTimeout: 30000, + hostVerifier: jumpHostVerifier, }; if (jumpHostConfig.authType === "password" && jumpHostConfig.password) { @@ -275,7 +286,7 @@ async function createJumpHostChain( return currentClient; } catch (error) { - dockerLogger.error("Failed to create jump host chain", error, { + sshLogger.error("Failed to create jump host chain", error, { operation: "jump_host_chain", }); clients.forEach((c) => c.end()); @@ -286,13 +297,27 @@ async function createJumpHostChain( async function executeDockerCommand( session: SSHSession, command: string, + sessionId?: string, + userId?: string, + hostId?: number, ): Promise { + const startTime = Date.now(); + sshLogger.info("Executing Docker command", { + operation: "docker_command_exec", + sessionId, + userId, + hostId, + command: command.split(" ")[1], + }); return new Promise((resolve, reject) => { session.client.exec(command, (err, stream) => { if (err) { - dockerLogger.error("Docker command execution error", err, { + sshLogger.error("Docker command execution error", err, { operation: "execute_docker_command", - command, + sessionId, + userId, + hostId, + command: command.split(" ")[1], }); return reject(err); } @@ -302,14 +327,25 @@ async function executeDockerCommand( stream.on("close", (code: number) => { if (code !== 0) { - dockerLogger.error("Docker command failed", undefined, { + sshLogger.error("Docker command failed", undefined, { operation: "execute_docker_command", - command, + sessionId, + userId, + hostId, + command: command.split(" ")[1], exitCode: code, stderr, }); reject(new Error(stderr || `Command exited with code ${code}`)); } else { + sshLogger.success("Docker command completed", { + operation: "docker_command_success", + sessionId, + userId, + hostId, + command: command.split(" ")[1], + duration: Date.now() - startTime, + }); resolve(stdout); } }); @@ -323,9 +359,12 @@ async function executeDockerCommand( }); stream.on("error", (streamErr: Error) => { - dockerLogger.error("Docker command stream error", streamErr, { + sshLogger.error("Docker command stream error", streamErr, { operation: "execute_docker_command", - command, + sessionId, + userId, + hostId, + command: command.split(" ")[1], }); reject(streamErr); }); @@ -350,12 +389,7 @@ app.use( return callback(null, true); } - const allowedOrigins = [ - "http://localhost:5173", - "http://localhost:3000", - "http://127.0.0.1:5173", - "http://127.0.0.1:3000", - ]; + const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; if (allowedOrigins.includes(origin)) { return callback(null, true); @@ -429,13 +463,10 @@ app.post("/docker/ssh/connect", async (req, res) => { const connectionLogs: Array> = []; if (!userId) { - dockerLogger.error( - "Docker SSH connection rejected: no authenticated user", - { - operation: "docker_connect_auth", - sessionId, - }, - ); + sshLogger.error("Docker SSH connection rejected: no authenticated user", { + operation: "docker_connect_auth", + sessionId, + }); connectionLogs.push( createConnectionLog( "error", @@ -460,7 +491,7 @@ app.post("/docker/ssh/connect", async (req, res) => { } if (!sessionId || !hostId) { - dockerLogger.warn("Missing Docker SSH connection parameters", { + sshLogger.warn("Missing Docker SSH connection parameters", { operation: "docker_connect", sessionId, hasHostId: !!hostId, @@ -512,7 +543,7 @@ app.post("/docker/ssh/connect", async (req, res) => { ); if (!accessInfo.hasAccess) { - dockerLogger.warn("User does not have access to host", { + sshLogger.warn("User does not have access to host", { operation: "docker_connect", hostId, userId, @@ -531,7 +562,7 @@ app.post("/docker/ssh/connect", async (req, res) => { try { host.jumpHosts = JSON.parse(host.jumpHosts); } catch (e) { - dockerLogger.error("Failed to parse jump hosts", e, { + sshLogger.error("Failed to parse jump hosts", e, { hostId: host.id, }); host.jumpHosts = []; @@ -539,7 +570,7 @@ app.post("/docker/ssh/connect", async (req, res) => { } if (!host.enableDocker) { - dockerLogger.warn("Docker not enabled for host", { + sshLogger.warn("Docker not enabled for host", { operation: "docker_connect", hostId, userId, @@ -618,7 +649,7 @@ app.post("/docker/ssh/connect", async (req, res) => { }; } } catch (error) { - dockerLogger.error("Failed to resolve shared credential", error, { + sshLogger.error("Failed to resolve shared credential", error, { operation: "docker_connect", hostId, userId, @@ -664,6 +695,14 @@ app.post("/docker/ssh/connect", async (req, res) => { readyTimeout: 60000, tcpKeepAlive: true, tcpKeepAliveInitialDelay: 30000, + hostVerifier: await SSHHostKeyVerifier.createHostVerifier( + hostId, + host.ip, + host.port || 22, + null, + userId, + false, + ), }; if (resolvedCredentials.authType === "none") { @@ -671,6 +710,94 @@ app.post("/docker/ssh/connect", async (req, res) => { if (resolvedCredentials.password) { config.password = resolvedCredentials.password; } + } else if (resolvedCredentials.authType === "opkssh") { + try { + const { getOPKSSHToken } = await import("./opkssh-auth.js"); + const token = await getOPKSSHToken(userId, hostId); + + if (!token) { + connectionLogs.push( + createConnectionLog( + "error", + "docker_auth", + "OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.", + ), + ); + return res.status(401).json({ + error: + "OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.", + requiresOPKSSHAuth: true, + connectionLogs, + }); + } + + const { promises: fs } = await import("fs"); + const path = await import("path"); + const os = await import("os"); + + const tempDir = os.tmpdir(); + const keyPath = path.join(tempDir, `opkssh-docker-${userId}-${hostId}`); + const certPath = `${keyPath}-cert.pub`; + + await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 }); + await fs.writeFile(certPath, token.sshCert, { mode: 0o600 }); + + config.privateKey = await fs.readFile(keyPath); + connectionLogs.push( + createConnectionLog( + "info", + "docker_auth", + "Using OPKSSH certificate authentication", + ), + ); + + setTimeout(async () => { + try { + const cleanupResults = await Promise.allSettled([ + fs.unlink(keyPath), + fs.unlink(certPath), + ]); + + cleanupResults.forEach((result, index) => { + if (result.status === "rejected") { + sshLogger.warn(`Failed to cleanup OPKSSH temp file`, { + operation: "opkssh_temp_cleanup_failed", + file: index === 0 ? "keyPath" : "certPath", + sessionId, + error: result.reason, + }); + } + }); + } catch (error) { + sshLogger.error("Failed to cleanup OPKSSH temp files", { + operation: "opkssh_temp_cleanup_error", + sessionId, + error, + }); + } + }, 60000); + } catch (opksshError) { + sshLogger.error("OPKSSH authentication error for Docker", { + operation: "docker_connect", + sessionId, + hostId, + error: + opksshError instanceof Error + ? opksshError.message + : "Unknown error", + }); + connectionLogs.push( + createConnectionLog( + "error", + "docker_auth", + `OPKSSH authentication failed: ${opksshError instanceof Error ? opksshError.message : "Unknown error"}`, + ), + ); + return res.status(500).json({ + error: "OPKSSH authentication failed", + connectionLogs, + }); + } } else if ( resolvedCredentials.authType === "key" && resolvedCredentials.sshKey @@ -680,7 +807,7 @@ app.post("/docker/ssh/connect", async (req, res) => { !resolvedCredentials.sshKey.includes("-----BEGIN") || !resolvedCredentials.sshKey.includes("-----END") ) { - dockerLogger.error("Invalid SSH key format", { + sshLogger.error("Invalid SSH key format", { operation: "docker_connect", sessionId, hostId, @@ -707,7 +834,7 @@ app.post("/docker/ssh/connect", async (req, res) => { config.passphrase = resolvedCredentials.keyPassword; } } catch (error) { - dockerLogger.error("SSH key processing error", error, { + sshLogger.error("SSH key processing error", error, { operation: "docker_connect", sessionId, hostId, @@ -725,14 +852,11 @@ app.post("/docker/ssh/connect", async (req, res) => { }); } } else if (resolvedCredentials.authType === "key") { - dockerLogger.error( - "SSH key authentication requested but no key provided", - { - operation: "docker_connect", - sessionId, - hostId, - }, - ); + sshLogger.error("SSH key authentication requested but no key provided", { + operation: "docker_connect", + sessionId, + hostId, + }); connectionLogs.push( createConnectionLog( "error", @@ -814,7 +938,7 @@ app.post("/docker/ssh/connect", async (req, res) => { client.on("error", (err) => { if (responseSent) { - dockerLogger.error( + sshLogger.error( "Docker SSH connection error after response sent", err, { @@ -832,7 +956,7 @@ app.post("/docker/ssh/connect", async (req, res) => { } responseSent = true; - dockerLogger.error("Docker SSH connection failed", err, { + sshLogger.error("Docker SSH connection failed", err, { operation: "docker_connect", sessionId, hostId, @@ -888,6 +1012,15 @@ app.post("/docker/ssh/connect", async (req, res) => { `Authentication failed: ${err.message}`, ), ); + } else if (err.message.includes("verification failed")) { + errorStage = "handshake"; + connectionLogs.push( + createConnectionLog( + "error", + errorStage, + `SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.`, + ), + ); } else { connectionLogs.push( createConnectionLog( @@ -1177,7 +1310,7 @@ app.post("/docker/ssh/connect", async (req, res) => { return; } } catch (socks5Error) { - dockerLogger.error("SOCKS5 connection failed", socks5Error, { + sshLogger.error("SOCKS5 connection failed", socks5Error, { operation: "docker_socks5_connect", sessionId, hostId, @@ -1238,7 +1371,7 @@ app.post("/docker/ssh/connect", async (req, res) => { host.port || 22, (err, stream) => { if (err) { - dockerLogger.error("Failed to forward through jump host", err, { + sshLogger.error("Failed to forward through jump host", err, { operation: "docker_jump_forward", sessionId, hostId, @@ -1269,7 +1402,7 @@ app.post("/docker/ssh/connect", async (req, res) => { client.connect(config); } } catch (error) { - dockerLogger.error("Docker SSH connection error", error, { + sshLogger.error("Docker SSH connection error", error, { operation: "docker_connect", sessionId, hostId, @@ -1359,7 +1492,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => { const userId = (req as any).userId; if (!userId) { - dockerLogger.error("TOTP verification rejected: no authenticated user", { + sshLogger.error("TOTP verification rejected: no authenticated user", { operation: "docker_totp_auth", sessionId, }); @@ -1373,7 +1506,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => { const session = pendingTOTPSessions[sessionId]; if (!session) { - dockerLogger.warn("TOTP session not found or expired", { + sshLogger.warn("TOTP session not found or expired", { operation: "docker_totp_verify", sessionId, userId, @@ -1389,7 +1522,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => { try { session.client.end(); } catch {} - dockerLogger.warn("TOTP session timeout before code submission", { + sshLogger.warn("TOTP session timeout before code submission", { operation: "docker_totp_verify", sessionId, userId, @@ -1471,7 +1604,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => { }, ); } catch (error) { - dockerLogger.warn("Failed to log Docker activity (TOTP)", { + sshLogger.warn("Failed to log Docker activity (TOTP)", { operation: "activity_log_error", userId: session.userId, hostId: session.hostId, @@ -1490,7 +1623,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => { delete pendingTOTPSessions[sessionId]; - dockerLogger.error("TOTP verification failed", { + sshLogger.error("TOTP verification failed", { operation: "docker_totp_verify", sessionId, userId, @@ -1504,7 +1637,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => { if (!responseSent) { responseSent = true; delete pendingTOTPSessions[sessionId]; - dockerLogger.warn("TOTP verification timeout", { + sshLogger.warn("TOTP verification timeout", { operation: "docker_totp_verify", sessionId, userId, @@ -1549,13 +1682,10 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => { const userId = (req as any).userId; if (!userId) { - dockerLogger.error( - "Warpgate verification rejected: no authenticated user", - { - operation: "docker_warpgate_auth", - sessionId, - }, - ); + sshLogger.error("Warpgate verification rejected: no authenticated user", { + operation: "docker_warpgate_auth", + sessionId, + }); return res.status(401).json({ error: "Authentication required" }); } @@ -1566,7 +1696,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => { const session = pendingTOTPSessions[sessionId]; if (!session) { - dockerLogger.warn("Warpgate session not found or expired", { + sshLogger.warn("Warpgate session not found or expired", { operation: "docker_warpgate_verify", sessionId, userId, @@ -1586,7 +1716,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => { try { session.client.end(); } catch {} - dockerLogger.warn("Warpgate session timeout before completion", { + sshLogger.warn("Warpgate session timeout before completion", { operation: "docker_warpgate_verify", sessionId, userId, @@ -1658,7 +1788,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => { }, ); } catch (error) { - dockerLogger.warn("Failed to log Docker activity (Warpgate)", { + sshLogger.warn("Failed to log Docker activity (Warpgate)", { operation: "activity_log_error", userId: session.userId, hostId: session.hostId, @@ -1677,7 +1807,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => { delete pendingTOTPSessions[sessionId]; - dockerLogger.error("Warpgate verification failed", { + sshLogger.error("Warpgate verification failed", { operation: "docker_warpgate_verify", sessionId, userId, @@ -1693,7 +1823,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => { if (!responseSent) { responseSent = true; delete pendingTOTPSessions[sessionId]; - dockerLogger.warn("Warpgate verification timeout", { + sshLogger.warn("Warpgate verification timeout", { operation: "docker_warpgate_verify", sessionId, userId, @@ -1840,12 +1970,21 @@ app.get("/docker/validate/:sessionId", async (req, res) => { const versionOutput = await executeDockerCommand( session, "docker --version", + sessionId, + userId, + session.hostId, ); const versionMatch = versionOutput.match(/Docker version ([^\s,]+)/); const version = versionMatch ? versionMatch[1] : "unknown"; try { - await executeDockerCommand(session, "docker ps >/dev/null 2>&1"); + await executeDockerCommand( + session, + "docker ps >/dev/null 2>&1", + sessionId, + userId, + session.hostId, + ); session.activeOperations--; return res.json({ @@ -1892,7 +2031,7 @@ app.get("/docker/validate/:sessionId", async (req, res) => { } } catch (error) { session.activeOperations--; - dockerLogger.error("Docker validation error", error, { + sshLogger.error("Docker validation error", error, { operation: "docker_validate", sessionId, userId, @@ -1962,7 +2101,13 @@ app.get("/docker/containers/:sessionId", async (req, res) => { const allFlag = all ? "-a " : ""; const command = `docker ps ${allFlag}--format '{"id":"{{.ID}}","name":"{{.Names}}","image":"{{.Image}}","status":"{{.Status}}","state":"{{.State}}","ports":"{{.Ports}}","created":"{{.CreatedAt}}"}'`; - const output = await executeDockerCommand(session, command); + const output = await executeDockerCommand( + session, + command, + sessionId, + userId, + session.hostId, + ); const containers = output .split("\n") @@ -1971,7 +2116,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => { try { return JSON.parse(line); } catch (e) { - dockerLogger.warn("Failed to parse container line", { + sshLogger.warn("Failed to parse container line", { operation: "parse_container", line, }); @@ -1985,7 +2130,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => { res.json(containers); } catch (error) { session.activeOperations--; - dockerLogger.error("Failed to list Docker containers", error, { + sshLogger.error("Failed to list Docker containers", error, { operation: "list_containers", sessionId, userId, @@ -2048,7 +2193,13 @@ app.get("/docker/containers/:sessionId/:containerId", async (req, res) => { try { const command = `docker inspect ${containerId}`; - const output = await executeDockerCommand(session, command); + const output = await executeDockerCommand( + session, + command, + sessionId, + userId, + session.hostId, + ); const details = JSON.parse(output); session.activeOperations--; @@ -2072,7 +2223,7 @@ app.get("/docker/containers/:sessionId/:containerId", async (req, res) => { }); } - dockerLogger.error("Failed to get container details", error, { + sshLogger.error("Failed to get container details", error, { operation: "get_container_details", sessionId, containerId, @@ -2136,7 +2287,21 @@ app.post( session.activeOperations++; try { - await executeDockerCommand(session, `docker start ${containerId}`); + sshLogger.info("Docker container operation", { + operation: "docker_container_op", + sessionId, + userId, + hostId: session.hostId, + containerId, + action: "start", + }); + await executeDockerCommand( + session, + `docker start ${containerId}`, + sessionId, + userId, + session.hostId, + ); session.activeOperations--; @@ -2156,7 +2321,7 @@ app.post( }); } - dockerLogger.error("Failed to start container", error, { + sshLogger.error("Failed to start container", error, { operation: "start_container", sessionId, containerId, @@ -2222,7 +2387,21 @@ app.post( session.activeOperations++; try { - await executeDockerCommand(session, `docker stop ${containerId}`); + sshLogger.info("Docker container operation", { + operation: "docker_container_op", + sessionId, + userId, + hostId: session.hostId, + containerId, + action: "stop", + }); + await executeDockerCommand( + session, + `docker stop ${containerId}`, + sessionId, + userId, + session.hostId, + ); session.activeOperations--; @@ -2242,7 +2421,7 @@ app.post( }); } - dockerLogger.error("Failed to stop container", error, { + sshLogger.error("Failed to stop container", error, { operation: "stop_container", sessionId, containerId, @@ -2308,7 +2487,21 @@ app.post( session.activeOperations++; try { - await executeDockerCommand(session, `docker restart ${containerId}`); + sshLogger.info("Docker container operation", { + operation: "docker_container_op", + sessionId, + userId, + hostId: session.hostId, + containerId, + action: "restart", + }); + await executeDockerCommand( + session, + `docker restart ${containerId}`, + sessionId, + userId, + session.hostId, + ); session.activeOperations--; @@ -2328,7 +2521,7 @@ app.post( }); } - dockerLogger.error("Failed to restart container", error, { + sshLogger.error("Failed to restart container", error, { operation: "restart_container", sessionId, containerId, @@ -2394,7 +2587,21 @@ app.post( session.activeOperations++; try { - await executeDockerCommand(session, `docker pause ${containerId}`); + sshLogger.info("Docker container operation", { + operation: "docker_container_op", + sessionId, + userId, + hostId: session.hostId, + containerId, + action: "pause", + }); + await executeDockerCommand( + session, + `docker pause ${containerId}`, + sessionId, + userId, + session.hostId, + ); session.activeOperations--; @@ -2414,7 +2621,7 @@ app.post( }); } - dockerLogger.error("Failed to pause container", error, { + sshLogger.error("Failed to pause container", error, { operation: "pause_container", sessionId, containerId, @@ -2480,7 +2687,21 @@ app.post( session.activeOperations++; try { - await executeDockerCommand(session, `docker unpause ${containerId}`); + sshLogger.info("Docker container operation", { + operation: "docker_container_op", + sessionId, + userId, + hostId: session.hostId, + containerId, + action: "unpause", + }); + await executeDockerCommand( + session, + `docker unpause ${containerId}`, + sessionId, + userId, + session.hostId, + ); session.activeOperations--; @@ -2500,7 +2721,7 @@ app.post( }); } - dockerLogger.error("Failed to unpause container", error, { + sshLogger.error("Failed to unpause container", error, { operation: "unpause_container", sessionId, containerId, @@ -2571,10 +2792,21 @@ app.delete( session.activeOperations++; try { + sshLogger.info("Docker container operation", { + operation: "docker_container_op", + sessionId, + userId, + hostId: session.hostId, + containerId, + action: "remove", + }); const forceFlag = force ? "-f " : ""; await executeDockerCommand( session, `docker rm ${forceFlag}${containerId}`, + sessionId, + userId, + session.hostId, ); session.activeOperations--; @@ -2604,7 +2836,7 @@ app.delete( }); } - dockerLogger.error("Failed to remove container", error, { + sshLogger.error("Failed to remove container", error, { operation: "remove_container", sessionId, containerId, @@ -2706,7 +2938,13 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => { command += ` --until ${until}`; } - const logs = await executeDockerCommand(session, command); + const logs = await executeDockerCommand( + session, + command, + sessionId, + userId, + session.hostId, + ); session.activeOperations--; @@ -2726,7 +2964,7 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => { }); } - dockerLogger.error("Failed to get container logs", error, { + sshLogger.error("Failed to get container logs", error, { operation: "get_logs", sessionId, containerId, @@ -2793,7 +3031,13 @@ app.get( try { const command = `docker stats ${containerId} --no-stream --format '{"cpu":"{{.CPUPerc}}","memory":"{{.MemUsage}}","memoryPercent":"{{.MemPerc}}","netIO":"{{.NetIO}}","blockIO":"{{.BlockIO}}","pids":"{{.PIDs}}"}'`; - const output = await executeDockerCommand(session, command); + const output = await executeDockerCommand( + session, + command, + sessionId, + userId, + session.hostId, + ); const rawStats = JSON.parse(output.trim()); const memoryParts = rawStats.memory.split(" / "); @@ -2835,7 +3079,7 @@ app.get( }); } - dockerLogger.error("Failed to get container stats", error, { + sshLogger.error("Failed to get container stats", error, { operation: "get_stats", sessionId, containerId, @@ -2856,7 +3100,7 @@ app.listen(PORT, async () => { try { await authManager.initialize(); } catch (err) { - dockerLogger.error("Failed to initialize Docker backend", err, { + sshLogger.error("Failed to initialize Docker backend", err, { operation: "startup", }); } diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index d06c6abc..af2d93b5 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -12,6 +12,7 @@ import { AuthManager } from "../utils/auth-manager.js"; import type { AuthenticatedRequest } from "../../types/index.js"; import { createSocks5Connection } from "../utils/socks5-helper.js"; import type { LogEntry, ConnectionStage } from "../../types/connection-log.js"; +import { SSHHostKeyVerifier } from "./host-key-verifier.js"; function createConnectionLog( type: "info" | "success" | "warning" | "error", @@ -118,12 +119,7 @@ app.use( origin: (origin, callback) => { if (!origin) return callback(null, true); - const allowedOrigins = [ - "http://localhost:5173", - "http://localhost:3000", - "http://127.0.0.1:5173", - "http://127.0.0.1:3000", - ]; + const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; if (origin.startsWith("https://")) { return callback(null, true); @@ -244,6 +240,15 @@ async function createJumpHostChain( const jumpClient = new SSHClient(); clients.push(jumpClient); + const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier( + jumpHostConfig.id, + jumpHostConfig.ip, + jumpHostConfig.port || 22, + null, + userId, + true, + ); + const connected = await new Promise((resolve) => { const timeout = setTimeout(() => { resolve(false); @@ -270,6 +275,7 @@ async function createJumpHostChain( username: jumpHostConfig.username, tryKeyboard: true, readyTimeout: 30000, + hostVerifier: jumpHostVerifier, }; if (jumpHostConfig.authType === "password" && jumpHostConfig.password) { @@ -785,6 +791,14 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { readyTimeout: 60000, tcpKeepAlive: true, tcpKeepAliveInitialDelay: 30000, + hostVerifier: await SSHHostKeyVerifier.createHostVerifier( + hostId, + ip, + port, + null, + userId, + false, + ), env: { TERM: "xterm-256color", LANG: "en_US.UTF-8", @@ -910,6 +924,92 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { connectionLogs.push( createConnectionLog("info", "sftp_auth", "Using password authentication"), ); + } else if (resolvedCredentials.authType === "opkssh") { + try { + const { getOPKSSHToken } = await import("./opkssh-auth.js"); + const token = await getOPKSSHToken(userId, hostId); + + if (!token) { + connectionLogs.push( + createConnectionLog( + "error", + "sftp_auth", + "OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.", + ), + ); + return res.status(401).json({ + error: + "OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.", + requiresOPKSSHAuth: true, + connectionLogs, + }); + } + + const { promises: fs } = await import("fs"); + const path = await import("path"); + const os = await import("os"); + + const tempDir = os.tmpdir(); + const keyPath = path.join(tempDir, `opkssh-fm-${userId}-${hostId}`); + const certPath = `${keyPath}-cert.pub`; + + await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 }); + await fs.writeFile(certPath, token.sshCert, { mode: 0o600 }); + + config.privateKey = await fs.readFile(keyPath); + connectionLogs.push( + createConnectionLog( + "info", + "sftp_auth", + "Using OPKSSH certificate authentication", + ), + ); + + setTimeout(async () => { + try { + const cleanupResults = await Promise.allSettled([ + fs.unlink(keyPath), + fs.unlink(certPath), + ]); + + cleanupResults.forEach((result, index) => { + if (result.status === "rejected") { + fileLogger.warn(`Failed to cleanup OPKSSH temp file`, { + operation: "opkssh_temp_cleanup_failed", + file: index === 0 ? "keyPath" : "certPath", + sessionId, + error: result.reason, + }); + } + }); + } catch (error) { + fileLogger.error("Failed to cleanup OPKSSH temp files", { + operation: "opkssh_temp_cleanup_error", + sessionId, + error, + }); + } + }, 60000); + } catch (opksshError) { + fileLogger.error("OPKSSH authentication error for file manager", { + operation: "file_connect", + sessionId, + hostId, + error: + opksshError instanceof Error ? opksshError.message : "Unknown error", + }); + connectionLogs.push( + createConnectionLog( + "error", + "sftp_auth", + `OPKSSH authentication failed: ${opksshError instanceof Error ? opksshError.message : "Unknown error"}`, + ), + ); + return res.status(500).json({ + error: "OPKSSH authentication failed", + connectionLogs, + }); + } } else if (resolvedCredentials.authType === "none") { connectionLogs.push( createConnectionLog( @@ -968,6 +1068,15 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { client.on("ready", () => { if (responseSent) return; responseSent = true; + fileLogger.info("File manager SSH connection established", { + operation: "file_ssh_connected", + sessionId, + userId, + hostId, + ip, + port, + username, + }); connectionLogs.push( createConnectionLog( "success", @@ -1104,6 +1213,16 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { { errorCode: (err as any).code, errorLevel: (err as any).level }, ), ); + } else if (err.message.includes("verification failed")) { + errorStage = "handshake"; + connectionLogs.push( + createConnectionLog( + "error", + errorStage, + `SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.`, + { errorCode: (err as any).code, errorLevel: (err as any).level }, + ), + ); } else { connectionLogs.push( createConnectionLog( @@ -1133,6 +1252,12 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { }); client.on("close", () => { + fileLogger.info("File manager SSH connection closed", { + operation: "file_ssh_disconnected", + sessionId, + userId, + hostId, + }); if (sshSessions[sessionId]) sshSessions[sessionId].isConnected = false; cleanupSession(sessionId); }); @@ -1892,6 +2017,12 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => { */ app.post("/ssh/file_manager/ssh/disconnect", (req, res) => { const { sessionId } = req.body; + const userId = (req as AuthenticatedRequest).userId; + fileLogger.info("File manager disconnection requested", { + operation: "file_disconnect_request", + sessionId, + userId, + }); cleanupSession(sessionId); res.json({ status: "success", message: "SSH connection disconnected" }); }); @@ -2037,6 +2168,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { const sessionId = req.query.sessionId as string; const sshConn = sshSessions[sessionId]; const sshPath = decodeURIComponent((req.query.path as string) || "/"); + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -2048,9 +2180,14 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { sshConn.lastActive = Date.now(); sshConn.activeOperations++; - const trySFTP = () => { try { + fileLogger.info("Opening SFTP channel", { + operation: "file_sftp_open", + sessionId, + userId, + path: sshPath, + }); sshConn.client.sftp((err, sftp) => { if (err) { fileLogger.warn( @@ -2062,6 +2199,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { sftp.readdir(sshPath, (readdirErr, list) => { if (readdirErr) { + sftp.end(); fileLogger.warn( `SFTP readdir failed, trying fallback: ${readdirErr.message}`, ); @@ -2115,11 +2253,24 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { } if (symlinks.length === 0) { + sftp.end(); sshConn.activeOperations--; return res.json({ files, path: sshPath }); } let resolved = 0; + let responded = false; + + const sendResponse = () => { + if (responded) return; + responded = true; + sftp.end(); + sshConn.activeOperations--; + res.json({ files, path: sshPath }); + }; + + const readlinkTimeout = setTimeout(sendResponse, 5000); + for (const link of symlinks) { sftp.readlink(link.path, (linkErr, target) => { resolved++; @@ -2127,8 +2278,8 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { files[link.index].linkTarget = target; } if (resolved === symlinks.length) { - sshConn.activeOperations--; - res.json({ files, path: sshPath }); + clearTimeout(readlinkTimeout); + sendResponse(); } }); } @@ -2502,6 +2653,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { const sessionId = req.query.sessionId as string; const sshConn = sshSessions[sessionId]; const filePath = decodeURIComponent(req.query.path as string); + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -2515,6 +2667,12 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { return res.status(400).json({ error: "File path is required" }); } + fileLogger.info("Reading file", { + operation: "file_read", + sessionId, + userId, + path: filePath, + }); sshConn.lastActive = Date.now(); const MAX_READ_SIZE = 500 * 1024 * 1024; @@ -2613,6 +2771,13 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { } const isBinary = detectBinary(binaryData); + fileLogger.success("File read successfully", { + operation: "file_read_success", + sessionId, + userId, + path: filePath, + bytes: binaryData.length, + }); if (isBinary) { const base64Content = binaryData.toString("base64"); @@ -2668,6 +2833,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { const { sessionId, path: filePath, content } = req.body; const sshConn = sshSessions[sessionId]; + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -2685,10 +2851,25 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { return res.status(400).json({ error: "File content is required" }); } + const contentLength = + typeof content === "string" ? content.length : Buffer.byteLength(content); + fileLogger.info("Writing file", { + operation: "file_write", + sessionId, + userId, + path: filePath, + bytes: contentLength, + }); sshConn.lastActive = Date.now(); const trySFTP = () => { try { + fileLogger.info("Opening SFTP channel", { + operation: "file_sftp_open", + sessionId, + userId, + path: filePath, + }); sshConn.client.sftp((err, sftp) => { if (err) { fileLogger.warn( @@ -2734,6 +2915,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { writeStream.on("error", (streamErr) => { if (hasError || hasFinished) return; hasError = true; + sftp.end(); fileLogger.warn( `SFTP write failed, trying fallback method: ${streamErr.message}`, ); @@ -2743,6 +2925,14 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { writeStream.on("finish", () => { if (hasError || hasFinished) return; hasFinished = true; + sftp.end(); + fileLogger.success("File written successfully", { + operation: "file_write_success", + sessionId, + userId, + path: filePath, + bytes: fileBuffer.length, + }); if (!res.headersSent) { res.json({ message: "File written successfully", @@ -2755,6 +2945,14 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { writeStream.on("close", () => { if (hasError || hasFinished) return; hasFinished = true; + sftp.end(); + fileLogger.success("File written successfully", { + operation: "file_write_success", + sessionId, + userId, + path: filePath, + bytes: fileBuffer.length, + }); if (!res.headersSent) { res.json({ message: "File written successfully", @@ -2913,6 +3111,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { const { sessionId, path: filePath, content, fileName } = req.body; const sshConn = sshSessions[sessionId]; + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -2938,9 +3137,23 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { const fullPath = filePath.endsWith("/") ? filePath + fileName : filePath + "/" + fileName; + const uploadStartTime = Date.now(); + fileLogger.info("File upload started", { + operation: "file_upload_start", + sessionId, + userId, + path: fullPath, + bytes: contentSize, + }); const trySFTP = () => { try { + fileLogger.info("Opening SFTP channel", { + operation: "file_sftp_open", + sessionId, + userId, + path: fullPath, + }); sshConn.client.sftp((err, sftp) => { if (err) { fileLogger.warn( @@ -2977,6 +3190,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { writeStream.on("error", (streamErr) => { if (hasError || hasFinished) return; hasError = true; + sftp.end(); fileLogger.warn( `SFTP write failed, trying fallback method: ${streamErr.message}`, { @@ -2993,6 +3207,15 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { writeStream.on("finish", () => { if (hasError || hasFinished) return; hasFinished = true; + sftp.end(); + fileLogger.success("File upload completed", { + operation: "file_upload_complete", + sessionId, + userId, + path: fullPath, + bytes: fileBuffer.length, + duration: Date.now() - uploadStartTime, + }); if (!res.headersSent) { res.json({ message: "File uploaded successfully", @@ -3005,6 +3228,15 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { writeStream.on("close", () => { if (hasError || hasFinished) return; hasFinished = true; + sftp.end(); + fileLogger.success("File upload completed", { + operation: "file_upload_complete", + sessionId, + userId, + path: fullPath, + bytes: fileBuffer.length, + duration: Date.now() - uploadStartTime, + }); if (!res.headersSent) { res.json({ message: "File uploaded successfully", @@ -3376,6 +3608,7 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => { app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => { const { sessionId, path: folderPath, folderName } = req.body; const sshConn = sshSessions[sessionId]; + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -3394,6 +3627,12 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => { const fullPath = folderPath.endsWith("/") ? folderPath + folderName : folderPath + "/" + folderName; + fileLogger.info("Creating directory", { + operation: "file_mkdir", + sessionId, + userId, + path: fullPath, + }); const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); const createCommand = `mkdir -p '${escapedPath}' && echo "SUCCESS" && exit 0`; @@ -3430,6 +3669,12 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => { stream.on("close", (code) => { if (outputData.includes("SUCCESS")) { + fileLogger.success("Directory created successfully", { + operation: "file_mkdir_success", + sessionId, + userId, + path: fullPath, + }); if (!res.headersSent) { res.json({ message: "Folder created successfully", @@ -3456,6 +3701,12 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => { return; } + fileLogger.success("Directory created successfully", { + operation: "file_mkdir_success", + sessionId, + userId, + path: fullPath, + }); if (!res.headersSent) { res.json({ message: "Folder created successfully", @@ -3508,6 +3759,7 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => { app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => { const { sessionId, path: itemPath, isDirectory } = req.body; const sshConn = sshSessions[sessionId]; + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -3521,6 +3773,13 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => { return res.status(400).json({ error: "Item path is required" }); } + fileLogger.info("Deleting item", { + operation: "file_delete", + sessionId, + userId, + path: itemPath, + type: isDirectory ? "directory" : "file", + }); sshConn.lastActive = Date.now(); const escapedPath = itemPath.replace(/'/g, "'\"'\"'"); @@ -3598,6 +3857,12 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => { } if (outputData.includes("SUCCESS") || code === 0) { + fileLogger.success("Item deleted successfully", { + operation: "file_delete_success", + sessionId, + userId, + path: itemPath, + }); res.json({ message: "Item deleted successfully", path: itemPath, @@ -3663,6 +3928,7 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => { app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => { const { sessionId, oldPath, newName } = req.body; const sshConn = sshSessions[sessionId]; + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -3682,6 +3948,13 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => { const oldDir = oldPath.substring(0, oldPath.lastIndexOf("/") + 1); const newPath = oldDir + newName; + fileLogger.info("Renaming item", { + operation: "file_rename", + sessionId, + userId, + from: oldPath, + to: newPath, + }); const escapedOldPath = oldPath.replace(/'/g, "'\"'\"'"); const escapedNewPath = newPath.replace(/'/g, "'\"'\"'"); @@ -3719,6 +3992,13 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => { stream.on("close", (code) => { if (outputData.includes("SUCCESS")) { + fileLogger.success("Item renamed successfully", { + operation: "file_rename_success", + sessionId, + userId, + from: oldPath, + to: newPath, + }); if (!res.headersSent) { res.json({ message: "Item renamed successfully", @@ -3746,6 +4026,13 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => { return; } + fileLogger.success("Item renamed successfully", { + operation: "file_rename_success", + sessionId, + userId, + from: oldPath, + to: newPath, + }); if (!res.headersSent) { res.json({ message: "Item renamed successfully", @@ -3959,6 +4246,7 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => { */ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => { const { sessionId, path: filePath, hostId, userId } = req.body; + const downloadStartTime = Date.now(); if (!sessionId || !filePath) { fileLogger.warn("Missing download parameters", { @@ -3969,6 +4257,13 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => { return res.status(400).json({ error: "Missing download parameters" }); } + fileLogger.info("File download started", { + operation: "file_download_start", + sessionId, + userId, + path: filePath, + }); + const sshConn = sshSessions[sessionId]; if (!sshConn || !sshConn.isConnected) { fileLogger.warn("SSH session not found or not connected for download", { @@ -3983,6 +4278,12 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => { sshConn.lastActive = Date.now(); scheduleSessionCleanup(sessionId); + fileLogger.info("Opening SFTP channel", { + operation: "file_sftp_open", + sessionId, + userId, + path: filePath, + }); sshConn.client.sftp((err, sftp) => { if (err) { @@ -3992,6 +4293,7 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => { sftp.stat(filePath, (statErr, stats) => { if (statErr) { + sftp.end(); fileLogger.error("File stat failed for download:", statErr); return res .status(500) @@ -4027,23 +4329,24 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => { sftp.readFile(filePath, (readErr, data) => { if (readErr) { + sftp.end(); fileLogger.error("File read failed for download:", readErr); return res .status(500) .json({ error: `Failed to read file: ${readErr.message}` }); } + sftp.end(); const base64Content = data.toString("base64"); const fileName = filePath.split("/").pop() || "download"; - - fileLogger.success("File downloaded successfully", { - operation: "file_download", + fileLogger.success("File download completed", { + operation: "file_download_complete", sessionId, - filePath, - fileName, - fileSize: stats.size, - hostId, userId, + hostId, + path: filePath, + bytes: stats.size, + duration: Date.now() - downloadStartTime, }); res.json({ diff --git a/src/backend/ssh/host-key-verifier.ts b/src/backend/ssh/host-key-verifier.ts new file mode 100644 index 00000000..a9057f8f --- /dev/null +++ b/src/backend/ssh/host-key-verifier.ts @@ -0,0 +1,471 @@ +import type { WebSocket } from "ws"; +import { db } from "../database/db/index.js"; +import { sshData } from "../database/db/schema.js"; +import { eq } from "drizzle-orm"; +import { sshLogger } from "../utils/logger.js"; +import crypto from "crypto"; + +interface HostKeyVerificationData { + scenario: "new" | "changed"; + ip: string; + port: number; + hostname?: string; + fingerprint: string; + oldFingerprint?: string; + keyType: string; + oldKeyType?: string; + algorithm: string; +} + +interface VerificationResponse { + action: "accept" | "reject"; +} + +export class SSHHostKeyVerifier { + /** + * Creates a hostVerifier callback for ssh2 Client.connect() + * + * @param hostId - Database ID of the host (null for quick connect) + * @param ip - IP address or hostname + * @param port - SSH port + * @param ws - WebSocket for user prompts (null for non-interactive connections) + * @param userId - User ID for logging + * @param isJumpHost - If true, auto-accepts without prompting + * @returns async hostVerifier callback + */ + static async createHostVerifier( + hostId: number | null, + ip: string, + port: number, + ws: WebSocket | null, + userId: string, + isJumpHost: boolean = false, + ): Promise<(hostkey: Buffer, verify: (valid: boolean) => void) => void> { + return (hostkey: Buffer, verify: (valid: boolean) => void): void => { + (async () => { + try { + const fingerprint = hostkey.toString("hex"); + const keyType = this.getKeyType(hostkey); + const algorithm = "sha256"; + + if (!hostId) { + sshLogger.info( + "Host key verification skipped (no hostId - quick connect)", + { + operation: "host_key_skip", + ip, + port, + fingerprint, + keyType, + userId, + }, + ); + verify(true); + return; + } + + const host = await db.query.sshData.findFirst({ + where: eq(sshData.id, hostId), + }); + + if (!host) { + sshLogger.warn( + "Host not found in database during key verification", + { + operation: "host_key_no_host", + hostId, + ip, + port, + userId, + }, + ); + verify(true); + return; + } + + if (!host.hostKeyFingerprint) { + if (isJumpHost) { + await this.storeHostKey(hostId, fingerprint, keyType, algorithm); + sshLogger.info("Jump host key auto-accepted and stored", { + operation: "host_key_stored", + hostId, + ip, + port, + fingerprint, + keyType, + userId, + isJumpHost: true, + }); + verify(true); + return; + } + + if (!ws) { + sshLogger.warn( + "No WebSocket available for host key verification prompt", + { + operation: "host_key_no_ws", + hostId, + ip, + port, + userId, + }, + ); + verify(true); + return; + } + + const accepted = await this.promptUserForNewKey( + ws, + ip, + port, + host.name || undefined, + fingerprint, + keyType, + algorithm, + ); + + if (accepted) { + await this.storeHostKey(hostId, fingerprint, keyType, algorithm); + sshLogger.info("New host key accepted by user and stored", { + operation: "host_key_stored", + hostId, + ip, + port, + fingerprint, + keyType, + userId, + }); + } else { + sshLogger.warn("User rejected new host key", { + operation: "host_key_rejected", + hostId, + ip, + port, + fingerprint, + keyType, + userId, + }); + } + + verify(accepted); + return; + } + + if (host.hostKeyFingerprint === fingerprint) { + await db + .update(sshData) + .set({ + hostKeyLastVerified: new Date().toISOString(), + }) + .where(eq(sshData.id, hostId)); + + sshLogger.info("Host key verified successfully", { + operation: "host_key_verified", + hostId, + ip, + port, + fingerprint, + keyType, + userId, + }); + + verify(true); + return; + } + + sshLogger.error("Host key mismatch detected - SECURITY WARNING", { + operation: "host_key_mismatch", + hostId, + ip, + port, + oldFingerprint: host.hostKeyFingerprint, + newFingerprint: fingerprint, + oldKeyType: host.hostKeyType, + newKeyType: keyType, + userId, + changeCount: host.hostKeyChangedCount || 0, + }); + + if (isJumpHost) { + await this.updateHostKey( + hostId, + fingerprint, + keyType, + algorithm, + host.hostKeyChangedCount || 0, + ); + sshLogger.warn("Jump host key changed - auto-accepted", { + operation: "host_key_updated", + hostId, + ip, + port, + fingerprint, + keyType, + userId, + isJumpHost: true, + }); + verify(true); + return; + } + + if (!ws) { + sshLogger.error( + "Host key changed - please connect via Terminal to verify the new key", + { + operation: "host_key_no_ws_reject", + hostId, + ip, + port, + userId, + message: + "SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.", + }, + ); + verify(false); + return; + } + + const accepted = await this.promptUserForChangedKey( + ws, + ip, + port, + host.name || undefined, + fingerprint, + host.hostKeyFingerprint, + keyType, + host.hostKeyType || "unknown", + algorithm, + ); + + if (accepted) { + await this.updateHostKey( + hostId, + fingerprint, + keyType, + algorithm, + host.hostKeyChangedCount || 0, + ); + sshLogger.warn("Changed host key accepted by user", { + operation: "host_key_updated", + hostId, + ip, + port, + oldFingerprint: host.hostKeyFingerprint, + newFingerprint: fingerprint, + userId, + changeCount: (host.hostKeyChangedCount || 0) + 1, + }); + } else { + sshLogger.error("User rejected changed host key", { + operation: "host_key_change_rejected", + hostId, + ip, + port, + userId, + }); + } + + verify(accepted); + } catch (error) { + sshLogger.error("Error in host key verification", error, { + operation: "host_key_error", + hostId, + ip, + port, + userId, + }); + verify(false); + } + })(); + }; + } + + private static async storeHostKey( + hostId: number, + fingerprint: string, + keyType: string, + algorithm: string, + ): Promise { + await db + .update(sshData) + .set({ + hostKeyFingerprint: fingerprint, + hostKeyType: keyType, + hostKeyAlgorithm: algorithm, + hostKeyFirstSeen: new Date().toISOString(), + hostKeyLastVerified: new Date().toISOString(), + }) + .where(eq(sshData.id, hostId)); + } + + private static async updateHostKey( + hostId: number, + fingerprint: string, + keyType: string, + algorithm: string, + currentChangeCount: number, + ): Promise { + await db + .update(sshData) + .set({ + hostKeyFingerprint: fingerprint, + hostKeyType: keyType, + hostKeyAlgorithm: algorithm, + hostKeyLastVerified: new Date().toISOString(), + hostKeyChangedCount: currentChangeCount + 1, + }) + .where(eq(sshData.id, hostId)); + } + + private static async promptUserForNewKey( + ws: WebSocket, + ip: string, + port: number, + hostname: string | undefined, + fingerprint: string, + keyType: string, + algorithm: string, + ): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(() => { + ws.removeListener("message", messageHandler); + sshLogger.warn("Host key verification timeout (new key)", { + operation: "host_key_timeout", + ip, + port, + }); + resolve(false); + }, 60000); + + const messageHandler = (data: Buffer) => { + try { + const message = JSON.parse(data.toString()); + + if (message.type === "host_key_verification_response") { + clearTimeout(timeout); + ws.removeListener("message", messageHandler); + + const response = message.data as VerificationResponse; + resolve(response.action === "accept"); + } + } catch (error) { + sshLogger.error( + "Error parsing host key verification response", + error, + ); + } + }; + + ws.on("message", messageHandler); + + const verificationData: HostKeyVerificationData = { + scenario: "new", + ip, + port, + hostname, + fingerprint, + keyType, + algorithm, + }; + + ws.send( + JSON.stringify({ + type: "host_key_verification_required", + data: verificationData, + }), + ); + }); + } + + private static async promptUserForChangedKey( + ws: WebSocket, + ip: string, + port: number, + hostname: string | undefined, + fingerprint: string, + oldFingerprint: string, + keyType: string, + oldKeyType: string, + algorithm: string, + ): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(() => { + ws.removeListener("message", messageHandler); + sshLogger.error("Host key verification timeout (changed key)", { + operation: "host_key_timeout", + ip, + port, + }); + resolve(false); + }, 120000); + + const messageHandler = (data: Buffer) => { + try { + const message = JSON.parse(data.toString()); + + if (message.type === "host_key_verification_response") { + clearTimeout(timeout); + ws.removeListener("message", messageHandler); + + const response = message.data as VerificationResponse; + resolve(response.action === "accept"); + } + } catch (error) { + sshLogger.error( + "Error parsing host key verification response", + error, + ); + } + }; + + ws.on("message", messageHandler); + + const verificationData: HostKeyVerificationData = { + scenario: "changed", + ip, + port, + hostname, + fingerprint, + oldFingerprint, + keyType, + oldKeyType, + algorithm, + }; + + ws.send( + JSON.stringify({ + type: "host_key_changed", + data: verificationData, + }), + ); + }); + } + + private static getKeyType(key: Buffer): string { + try { + if (key.length < 4) { + return "unknown"; + } + + const typeLength = key.readUInt32BE(0); + if (typeLength > key.length - 4 || typeLength > 256) { + return "unknown"; + } + + const keyType = key.toString("utf8", 4, 4 + typeLength); + + if ( + (keyType && keyType.startsWith("ssh-")) || + keyType.startsWith("ecdsa-") + ) { + return keyType; + } + + return "unknown"; + } catch (error) { + sshLogger.error("Error parsing SSH key type", error); + return "unknown"; + } + } +} diff --git a/src/backend/ssh/opkssh-auth.ts b/src/backend/ssh/opkssh-auth.ts new file mode 100644 index 00000000..317617d4 --- /dev/null +++ b/src/backend/ssh/opkssh-auth.ts @@ -0,0 +1,803 @@ +import { spawn, ChildProcess } from "child_process"; +import { randomUUID } from "crypto"; +import { WebSocket } from "ws"; +import { IncomingMessage } from "http"; +import { OPKSSHBinaryManager } from "../utils/opkssh-binary-manager.js"; +import { sshLogger } from "../utils/logger.js"; +import { getDb } from "../database/db/index.js"; +import { opksshTokens } from "../database/db/schema.js"; +import { eq, and } from "drizzle-orm"; +import { UserCrypto } from "../utils/user-crypto.js"; +import { FieldCrypto } from "../utils/field-crypto.js"; +import { promises as fs } from "fs"; +import path from "path"; +import axios from "axios"; + +const AUTH_TIMEOUT = 60 * 1000; + +interface OPKSSHAuthSession { + requestId: string; + userId: string; + hostId: number; + hostname: string; + process: ChildProcess; + localPort: number; + callbackPort: number; + remoteRedirectUri: string; + status: + | "starting" + | "waiting_for_auth" + | "authenticating" + | "completed" + | "error"; + ws: WebSocket; + stdoutBuffer: string; + privateKeyBuffer: string; + sshCertBuffer: string; + identity: { + email?: string; + sub?: string; + issuer?: string; + audience?: string; + }; + createdAt: Date; + approvalTimeout: NodeJS.Timeout; + cleanup: () => Promise; +} + +const activeAuthSessions = new Map(); +const cleanupInProgress = new Set(); + +export function getRequestOrigin(req: IncomingMessage): string { + const protoHeader = + req.headers["x-forwarded-proto"] || + ((req.socket as any).encrypted ? "https" : "http"); + const proto = + typeof protoHeader === "string" + ? protoHeader.split(",")[0].trim() + : String(protoHeader); + + const portHeader = req.headers["x-forwarded-port"]; + const port = + typeof portHeader === "string" + ? portHeader.split(",")[0].trim() + : undefined; + + const hostHeaderRaw = + req.headers["x-forwarded-host"] || req.headers.host || "localhost"; + const hostHeader = + typeof hostHeaderRaw === "string" + ? hostHeaderRaw.split(",")[0].trim() + : String(hostHeaderRaw); + + if (port) { + const hostWithoutPort = hostHeader.split(":")[0]; + const isDefaultPort = + (proto === "http" && port === "80") || + (proto === "https" && port === "443"); + return isDefaultPort + ? `${proto}://${hostWithoutPort}` + : `${proto}://${hostWithoutPort}:${port}`; + } + + return `${proto}://${hostHeader}`; +} + +function getOPKConfigPath(): string { + const dataDir = + process.env.DATA_DIR || path.join(process.cwd(), "db", "data"); + return path.join(dataDir, ".opk", "config.yml"); +} + +async function ensureOPKConfigDir(): Promise { + const configPath = getOPKConfigPath(); + const configDir = path.dirname(configPath); + await fs.mkdir(configDir, { recursive: true }); +} + +async function createTemplateConfig(): Promise { + const configPath = getOPKConfigPath(); + const template = ` +# OPKSSH Configuration +# OPKSSH Documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md +# Termix Documentation: https://docs.termix.site/opkssh +`; + + try { + await ensureOPKConfigDir(); + await fs.writeFile(configPath, template, "utf8"); + sshLogger.info(`Created template OPKSSH config at ${configPath}`); + } catch (error) { + sshLogger.warn("Failed to create template OPKSSH config", error); + } +} + +async function checkOPKConfigExists(): Promise<{ + exists: boolean; + error?: string; + configPath?: string; +}> { + const configPath = getOPKConfigPath(); + const isDocker = + !!process.env.DATA_DIR && process.env.DATA_DIR.startsWith("/app"); + const dockerHint = isDocker + ? "\n\nDocker: Ensure /app/data is mounted as a volume with write permissions for node:node user." + : ""; + + try { + const content = await fs.readFile(configPath, "utf8"); + + if (!content.includes("providers:")) { + return { + exists: false, + configPath, + error: `OPKSSH configuration is missing 'providers' section. Please edit the config file at:\n${configPath}\n\n.`, + }; + } + + const lines = content.split("\n"); + + const hasUncommentedProvider = lines.some((line) => { + const trimmed = line.trim(); + return ( + trimmed.startsWith("- alias:") || + (trimmed.startsWith("issuer:") && !line.trimStart().startsWith("#")) + ); + }); + + if (!hasUncommentedProvider) { + return { + exists: false, + configPath, + error: `OPKSSH configuration has no active providers. Please edit the config file at:\n${configPath}\n\nUncomment and configure at least one OIDC provider.\nSee documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md${dockerHint}`, + }; + } + + if (!content.includes("redirect_uris:")) { + return { + exists: false, + configPath, + error: `OPKSSH configuration is missing 'redirect_uris' field.`, + }; + } + + return { exists: true, configPath }; + } catch { + await createTemplateConfig(); + return { + exists: false, + configPath, + error: `OPKSSH configuration not found. A template config file has been created at:\n${configPath}\n\nPlease edit this file and configure your OIDC provider (Google, GitHub, Microsoft, etc.).\nSee documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md${dockerHint}`, + }; + } +} + +export async function startOPKSSHAuth( + userId: string, + hostId: number, + hostname: string, + ws: WebSocket, + requestOrigin: string, +): Promise { + try { + await ensureOPKConfigDir(); + const configDir = path.dirname(getOPKConfigPath()); + await fs.access(configDir, fs.constants.R_OK | fs.constants.W_OK); + } catch (error) { + sshLogger.error("OPKSSH directory not accessible", error); + const isDocker = + !!process.env.DATA_DIR && process.env.DATA_DIR.startsWith("/app"); + const dockerHint = isDocker + ? "\n\nDocker: Ensure /app/data is mounted as a volume with write permissions for node:node user." + : ""; + ws.send( + JSON.stringify({ + type: "opkssh_error", + error: `OPKSSH directory initialization failed: ${error.message}${dockerHint}`, + }), + ); + return ""; + } + + const configCheck = await checkOPKConfigExists(); + if (!configCheck.exists) { + ws.send( + JSON.stringify({ + type: "opkssh_config_error", + requestId: "", + error: configCheck.error, + instructions: configCheck.error, + }), + ); + return ""; + } + + const requestId = randomUUID(); + const remoteRedirectUri = `${requestOrigin}/ssh/opkssh-callback`; + + const session: Partial = { + requestId, + userId, + hostId, + hostname, + localPort: 0, + callbackPort: 0, + remoteRedirectUri, + status: "starting", + ws, + stdoutBuffer: "", + privateKeyBuffer: "", + sshCertBuffer: "", + identity: {}, + createdAt: new Date(), + }; + + try { + const binaryPath = OPKSSHBinaryManager.getBinaryPath(); + const configPath = getOPKConfigPath(); + const configDir = path.dirname(configPath); + + const args = [ + "login", + "--print-key", + `--config-path=${configPath}`, + `--remote-redirect-uri=${remoteRedirectUri}`, + ]; + + const opksshProcess = spawn(binaryPath, args, { + stdio: ["ignore", "pipe", "pipe"], + env: { + ...process.env, + }, + }); + session.process = opksshProcess; + + const cleanup = async () => { + await cleanupAuthSession(requestId); + }; + session.cleanup = cleanup; + + const timeout = setTimeout(async () => { + sshLogger.warn(`OPKSSH auth timeout for session ${requestId}`); + ws.send( + JSON.stringify({ + type: "opkssh_timeout", + requestId, + }), + ); + await cleanup(); + }, AUTH_TIMEOUT); + + session.approvalTimeout = timeout; + + ws.on("close", () => { + cleanup(); + }); + + activeAuthSessions.set(requestId, session as OPKSSHAuthSession); + + opksshProcess.stdout?.on("data", (data) => { + const output = data.toString(); + handleOPKSSHOutput(requestId, output); + }); + + opksshProcess.stderr?.on("data", async (data) => { + const stderr = data.toString(); + + if (stderr.includes("Opening browser to")) { + handleOPKSSHOutput(requestId, stderr); + } + + if (stderr.includes("listening on")) { + handleOPKSSHOutput(requestId, stderr); + } + + if (stderr.includes("provider not found") || stderr.includes("config")) { + ws.send( + JSON.stringify({ + type: "opkssh_config_error", + requestId, + error: + "OPKSSH configuration error. Please verify your config file contains valid OIDC provider settings.", + instructions: + "See documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md", + }), + ); + cleanup(); + } + + if ( + stderr.includes("level=error") || + stderr.includes("Error:") || + stderr.includes("failed") + ) { + const isXdgOpenError = stderr.includes('exec: "xdg-open"'); + if (!isXdgOpenError) { + if ( + stderr.includes("bind: address already in use") || + stderr.includes("error logging in") || + stderr.includes("failed to start") + ) { + await cleanup(); + } + } + } + }); + + opksshProcess.on("error", (error) => { + ws.send( + JSON.stringify({ + type: "opkssh_error", + requestId, + error: `OPKSSH process error: ${error.message}`, + }), + ); + cleanup(); + }); + + opksshProcess.on("exit", (code) => { + if (code !== 0 && session.status !== "completed") { + ws.send( + JSON.stringify({ + type: "opkssh_error", + requestId, + error: `OPKSSH process exited with code ${code}`, + }), + ); + } + cleanup(); + }); + + return requestId; + } catch (error) { + sshLogger.error(`Failed to start OPKSSH auth session`, error); + ws.send( + JSON.stringify({ + type: "opkssh_error", + requestId, + error: `Failed to start OPKSSH authentication: ${error instanceof Error ? error.message : "Unknown error"}`, + }), + ); + return ""; + } +} + +function handleOPKSSHOutput(requestId: string, output: string): void { + const session = activeAuthSessions.get(requestId); + if (!session) { + return; + } + + session.stdoutBuffer += output; + + const chooserUrlMatch = session.stdoutBuffer.match( + /Opening browser to http:\/\/localhost:(\d+)\/chooser/, + ); + if (chooserUrlMatch && session.status === "starting") { + const actualPort = parseInt(chooserUrlMatch[1], 10); + const localChooserUrl = `http://localhost:${actualPort}/chooser`; + + session.localPort = actualPort; + + const baseUrl = session.remoteRedirectUri.replace( + /\/ssh\/opkssh-callback$/, + "", + ); + const proxiedChooserUrl = `${baseUrl}/ssh/opkssh-chooser/${requestId}`; + + session.status = "waiting_for_auth"; + session.ws.send( + JSON.stringify({ + type: "opkssh_status", + requestId, + stage: "chooser", + url: proxiedChooserUrl, + localUrl: localChooserUrl, + message: "Please authenticate in your browser", + }), + ); + } + + const callbackPortMatch = session.stdoutBuffer.match( + /listening on http:\/\/127\.0\.0\.1:(\d+)\//, + ); + if (callbackPortMatch && !session.callbackPort) { + session.callbackPort = parseInt(callbackPortMatch[1], 10); + } + + if (output.includes("BEGIN OPENSSH PRIVATE KEY")) { + session.status = "authenticating"; + session.ws.send( + JSON.stringify({ + type: "opkssh_status", + requestId, + stage: "authenticating", + message: "Processing authentication...", + }), + ); + } + + const privateKeyMatch = session.stdoutBuffer.match( + /(-----BEGIN OPENSSH PRIVATE KEY-----[\s\S]*?-----END OPENSSH PRIVATE KEY-----)/, + ); + if (privateKeyMatch) { + session.privateKeyBuffer = privateKeyMatch[1].trim(); + } + + const certMatch = session.stdoutBuffer.match( + /(ecdsa-sha2-nistp256-cert-v01@openssh\.com\s+[A-Za-z0-9+/=]+|ssh-rsa-cert-v01@openssh\.com\s+[A-Za-z0-9+/=]+|ssh-ed25519-cert-v01@openssh\.com\s+[A-Za-z0-9+/=]+)/, + ); + if (certMatch) { + session.sshCertBuffer = certMatch[1].trim(); + } + + const identityMatch = session.stdoutBuffer.match( + /Email, sub, issuer, audience:\s*\n?\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)/, + ); + if (identityMatch) { + session.identity = { + email: identityMatch[1], + sub: identityMatch[2], + issuer: identityMatch[3], + audience: identityMatch[4], + }; + } + + if (session.privateKeyBuffer && session.sshCertBuffer) { + if (!session.privateKeyBuffer.includes("BEGIN OPENSSH PRIVATE KEY")) { + sshLogger.error(`Invalid private key extracted [${requestId}]`, { + bufferPrefix: session.privateKeyBuffer.substring(0, 50), + }); + session.ws.send( + JSON.stringify({ + type: "opkssh_error", + requestId, + error: "Failed to extract valid private key from OPKSSH output", + }), + ); + return; + } + + if (!session.sshCertBuffer.match(/-cert-v01@openssh\.com/)) { + sshLogger.error(`Invalid SSH certificate extracted [${requestId}]`, { + bufferPrefix: session.sshCertBuffer.substring(0, 50), + }); + session.ws.send( + JSON.stringify({ + type: "opkssh_error", + requestId, + error: "Failed to extract valid SSH certificate from OPKSSH output", + }), + ); + return; + } + + storeOPKSSHToken(session); + } +} + +async function storeOPKSSHToken(session: OPKSSHAuthSession): Promise { + try { + const db = getDb(); + const userCrypto = UserCrypto.getInstance(); + + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); + + const userDataKey = userCrypto.getUserDataKey(session.userId); + if (!userDataKey) { + throw new Error("User data key not found"); + } + + const tokenId = `opkssh-${session.userId}-${session.hostId}`; + + const encryptedCert = FieldCrypto.encryptField( + session.sshCertBuffer, + userDataKey, + tokenId, + "ssh_cert", + ); + const encryptedKey = FieldCrypto.encryptField( + session.privateKeyBuffer, + userDataKey, + tokenId, + "private_key", + ); + + await db + .insert(opksshTokens) + .values({ + userId: session.userId, + hostId: session.hostId, + sshCert: encryptedCert, + privateKey: encryptedKey, + email: session.identity.email, + sub: session.identity.sub, + issuer: session.identity.issuer, + audience: session.identity.audience, + expiresAt: expiresAt.toISOString(), + }) + .onConflictDoUpdate({ + target: [opksshTokens.userId, opksshTokens.hostId], + set: { + sshCert: encryptedCert, + privateKey: encryptedKey, + email: session.identity.email, + sub: session.identity.sub, + issuer: session.identity.issuer, + audience: session.identity.audience, + expiresAt: expiresAt.toISOString(), + createdAt: new Date().toISOString(), + }, + }); + + session.status = "completed"; + session.ws.send( + JSON.stringify({ + type: "opkssh_completed", + requestId: session.requestId, + expiresAt: expiresAt.toISOString(), + }), + ); + + try { + await axios.post( + "http://localhost:30006/activity/log", + { + type: "opkssh_authentication", + hostId: session.hostId, + hostName: session.hostname, + status: "approved", + }, + { + headers: { + Authorization: `Bearer ${process.env.INTERNAL_AUTH_TOKEN}`, + }, + }, + ); + } catch (activityError) { + sshLogger.warn("Failed to log OPKSSH activity", activityError); + } + + await session.cleanup(); + } catch (error) { + sshLogger.error( + `Failed to store OPKSSH token for session ${session.requestId}`, + error, + ); + session.ws.send( + JSON.stringify({ + type: "opkssh_error", + requestId: session.requestId, + error: "Failed to store authentication token", + }), + ); + await session.cleanup(); + } +} + +export async function getOPKSSHToken( + userId: string, + hostId: number, +): Promise<{ sshCert: string; privateKey: string } | null> { + try { + const db = getDb(); + const token = await db + .select() + .from(opksshTokens) + .where( + and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)), + ) + .limit(1); + + if (!token || token.length === 0) { + return null; + } + + const tokenData = token[0]; + const expiresAt = new Date(tokenData.expiresAt); + + if (expiresAt < new Date()) { + await db + .delete(opksshTokens) + .where( + and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)), + ); + return null; + } + + const userCrypto = UserCrypto.getInstance(); + const userDataKey = userCrypto.getUserDataKey(userId); + if (!userDataKey) { + throw new Error("User data key not found"); + } + + const tokenId = `opkssh-${userId}-${hostId}`; + const decryptedCert = FieldCrypto.decryptField( + tokenData.sshCert, + userDataKey, + tokenId, + "ssh_cert", + ); + const decryptedKey = FieldCrypto.decryptField( + tokenData.privateKey, + userDataKey, + tokenId, + "private_key", + ); + + await db + .update(opksshTokens) + .set({ lastUsed: new Date().toISOString() }) + .where( + and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)), + ); + + return { + sshCert: decryptedCert, + privateKey: decryptedKey, + }; + } catch (error) { + sshLogger.error(`Failed to retrieve OPKSSH token`, error); + return null; + } +} + +export async function deleteOPKSSHToken( + userId: string, + hostId: number, +): Promise { + const db = getDb(); + await db + .delete(opksshTokens) + .where( + and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)), + ); +} + +export async function invalidateOPKSSHToken( + userId: string, + hostId: number, + reason: string, +): Promise { + try { + const db = getDb(); + await db + .delete(opksshTokens) + .where( + and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)), + ); + } catch (error) { + sshLogger.error(`Failed to invalidate OPKSSH token`, { + userId, + hostId, + reason, + error, + }); + } +} + +export async function handleOAuthCallback( + requestId: string, + queryString: string, +): Promise<{ success: boolean; message?: string }> { + const session = activeAuthSessions.get(requestId); + + if (!session) { + return { success: false, message: "Invalid authentication session" }; + } + + try { + const callbackUrl = `http://localhost:${session.localPort}/login-callback?${queryString}`; + await axios.get(callbackUrl, { + timeout: 10000, + validateStatus: () => true, + }); + return { success: true }; + } catch { + session.ws.send( + JSON.stringify({ + type: "opkssh_error", + requestId, + error: "Failed to complete authentication", + }), + ); + await session.cleanup(); + return { success: false, message: "Authentication failed" }; + } +} + +async function cleanupAuthSession(requestId: string): Promise { + if (cleanupInProgress.has(requestId)) { + return; + } + + cleanupInProgress.add(requestId); + + try { + const session = activeAuthSessions.get(requestId); + if (!session) { + cleanupInProgress.delete(requestId); + return; + } + + if (session.approvalTimeout) { + clearTimeout(session.approvalTimeout); + } + + if (session.process) { + try { + session.process.kill("SIGTERM"); + await new Promise((resolve) => { + const killTimeout = setTimeout(() => { + if (session.process && !session.process.killed) { + session.process.kill("SIGKILL"); + } + resolve(); + }, 3000); + + session.process.once("exit", () => { + clearTimeout(killTimeout); + resolve(); + }); + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } catch (killError) { + sshLogger.warn( + `Failed to kill OPKSSH process for session ${requestId}`, + killError, + ); + } + } + + activeAuthSessions.delete(requestId); + } finally { + cleanupInProgress.delete(requestId); + } +} + +export function cancelAuthSession(requestId: string): void { + const session = activeAuthSessions.get(requestId); + if (session) { + session.cleanup(); + } +} + +export function getActiveAuthSession( + requestId: string, +): OPKSSHAuthSession | undefined { + return activeAuthSessions.get(requestId); +} + +export function getActiveSessionsForUser(userId: string): OPKSSHAuthSession[] { + const sessions: OPKSSHAuthSession[] = []; + for (const session of activeAuthSessions.values()) { + if (session.userId === userId) { + sessions.push(session); + } + } + return sessions; +} + +export function getActiveSessionsAll(): OPKSSHAuthSession[] { + return Array.from(activeAuthSessions.values()); +} + +export async function getUserIdFromRequest(req: any): Promise { + try { + const { AuthManager } = await import("../utils/auth-manager.js"); + const authManager = AuthManager.getInstance(); + + const token = + req.cookies?.jwt || req.headers.authorization?.replace("Bearer ", ""); + if (!token) { + return null; + } + + const decoded = await authManager.verifyJWTToken(token); + return decoded?.userId || null; + } catch (error) { + return null; + } +} diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 628cd427..1b50a8fe 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -23,6 +23,7 @@ import { collectLoginStats } from "./widgets/login-stats-collector.js"; import { collectPortsMetrics } from "./widgets/ports-collector.js"; import { collectFirewallMetrics } from "./widgets/firewall-collector.js"; import { createSocks5Connection } from "../utils/socks5-helper.js"; +import { SSHHostKeyVerifier } from "./host-key-verifier.js"; function createConnectionLog( type: "info" | "success" | "warning" | "error", @@ -125,6 +126,15 @@ async function createJumpHostChain( const jumpClient = new Client(); clients.push(jumpClient); + const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier( + jumpHostConfig.id, + jumpHostConfig.ip, + jumpHostConfig.port || 22, + null, + userId, + true, + ); + const connected = await new Promise((resolve) => { const timeout = setTimeout(() => { resolve(false); @@ -151,6 +161,7 @@ async function createJumpHostChain( username: jumpHostConfig.username, tryKeyboard: true, readyTimeout: 30000, + hostVerifier: jumpHostVerifier, }; if (jumpHostConfig.authType === "password" && jumpHostConfig.password) { @@ -378,8 +389,8 @@ class SSHConnectionPool { private async createConnection( host: SSHHostWithCredentials, ): Promise { + const config = await buildSshConfig(host); return new Promise(async (resolve, reject) => { - const config = buildSshConfig(host); const client = new Client(); const timeout = setTimeout(() => { client.end(); @@ -1023,6 +1034,20 @@ class PollingManager { const statusOnly = options?.statusOnly ?? false; const viewerUserId = options?.viewerUserId; + const enabledCollectors: string[] = []; + if (statsConfig.statusCheckEnabled) enabledCollectors.push("status"); + if (!statusOnly && statsConfig.metricsEnabled) { + enabledCollectors.push( + "cpu", + "memory", + "disk", + "network", + "uptime", + "processes", + "system", + ); + } + const existingConfig = this.pollingConfigs.get(host.id); if (existingConfig) { @@ -1150,12 +1175,10 @@ class PollingManager { const latestConfig = this.pollingConfigs.get(refreshedHost.id); if (latestConfig && latestConfig.statsConfig.metricsEnabled) { const backoffInfo = pollingBackoff.getBackoffInfo(refreshedHost.id); - statsLogger.error("Failed to collect metrics for host", { - operation: "metrics_poll_failed", + statsLogger.error("Stats collector connection failed", error, { + operation: "stats_connect_failed", hostId: refreshedHost.id, - hostName: refreshedHost.name, - error: errorMessage, - backoff: backoffInfo, + retryInfo: backoffInfo, }); } } @@ -1172,6 +1195,7 @@ class PollingManager { clearInterval(config.metricsTimer); config.metricsTimer = undefined; } + this.pollingConfigs.delete(hostId); if (clearData) { this.statusStore.delete(hostId); @@ -1333,12 +1357,7 @@ app.use( origin: (origin, callback) => { if (!origin) return callback(null, true); - const allowedOrigins = [ - "http://localhost:5173", - "http://localhost:3000", - "http://127.0.0.1:5173", - "http://127.0.0.1:3000", - ]; + const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; if (allowedOrigins.includes(origin)) { return callback(null, true); @@ -1530,7 +1549,15 @@ async function resolveHostCredentials( if (credentials.length > 0) { const credential = credentials[0]; baseHost.credentialId = credential.id; - baseHost.authType = credential.auth_type || credential.authType; + baseHost.authType = + credential.auth_type || + credential.authType || + (credential.password + ? "password" + : credential.key || + (credential as Record).private_key + ? "key" + : "none"); if (!host.overrideCredentialUsername) { baseHost.username = credential.username; @@ -1551,6 +1578,13 @@ async function resolveHostCredentials( } } else { addLegacyCredentials(baseHost, host); + if (baseHost.authType === "credential") { + baseHost.authType = baseHost.password + ? "password" + : baseHost.key + ? "key" + : "none"; + } } } } catch (error) { @@ -1558,6 +1592,13 @@ async function resolveHostCredentials( `Failed to resolve credential ${host.credentialId} for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`, ); addLegacyCredentials(baseHost, host); + if (baseHost.authType === "credential") { + baseHost.authType = baseHost.password + ? "password" + : baseHost.key + ? "key" + : "none"; + } } } else { addLegacyCredentials(baseHost, host); @@ -1582,7 +1623,9 @@ function addLegacyCredentials( baseHost.keyType = host.keyType; } -function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig { +async function buildSshConfig( + host: SSHHostWithCredentials, +): Promise { const base: ConnectConfig = { host: host.ip, port: host.port, @@ -1593,6 +1636,14 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig { readyTimeout: 60000, tcpKeepAlive: true, tcpKeepAliveInitialDelay: 30000, + hostVerifier: await SSHHostKeyVerifier.createHostVerifier( + host.id, + host.ip, + host.port, + null, + host.userId || "", + false, + ), env: { TERM: "xterm-256color", LANG: "en_US.UTF-8", @@ -1687,6 +1738,25 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig { throw new Error(`Invalid SSH key format for host ${host.ip}`); } } else if (host.authType === "none") { + } else if (host.authType === "opkssh") { + } else if (host.authType === "credential") { + if (host.password) { + base.password = host.password; + } else if (host.key) { + const cleanKey = host.key + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + (base as Record).privateKey = Buffer.from( + cleanKey, + "utf8", + ); + if (host.keyPassword) { + (base as Record).passphrase = host.keyPassword; + } + } else { + throw new Error(`Credential for host ${host.ip} could not be resolved`); + } } else { throw new Error( `Unsupported authentication type '${host.authType}' for host ${host.ip}`, @@ -1809,12 +1879,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ }; try { ports = await collectPortsMetrics(client); - } catch (e) { - statsLogger.debug("Failed to collect ports metrics", { - operation: "ports_metrics_failed", - error: e instanceof Error ? e.message : String(e), - }); - } + } catch (e) {} let firewall: { type: "iptables" | "nftables" | "none"; @@ -1842,12 +1907,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ }; try { firewall = await collectFirewallMetrics(client); - } catch (e) { - statsLogger.debug("Failed to collect firewall metrics", { - operation: "firewall_metrics_failed", - error: e instanceof Error ? e.message : String(e), - }); - } + } catch (e) {} const result = { cpu, @@ -2349,7 +2409,7 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => { return res.json({ success: true, connectionLogs }); } - const config = buildSshConfig(host); + const config = await buildSshConfig(host); const client = new Client(); const connectionPromise = new Promise<{ @@ -2524,6 +2584,15 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => { `Authentication failed: ${errorMessage}`, ), ); + } else if (errorMessage.includes("verification failed")) { + errorStage = "handshake"; + connectionLogs.push( + createConnectionLog( + "error", + errorStage, + `SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.`, + ), + ); } else { connectionLogs.push( createConnectionLog( diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index 37bc2b88..6ade781c 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -10,12 +10,13 @@ import axios from "axios"; import { getDb } from "../database/db/index.js"; import { sshCredentials, sshData } from "../database/db/schema.js"; import { eq, and } from "drizzle-orm"; -import { sshLogger } from "../utils/logger.js"; +import { sshLogger, authLogger } from "../utils/logger.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { AuthManager } from "../utils/auth-manager.js"; import { UserCrypto } from "../utils/user-crypto.js"; import { createSocks5Connection } from "../utils/socks5-helper.js"; import { SSHAuthManager } from "./auth-manager.js"; +import { SSHHostKeyVerifier } from "./host-key-verifier.js"; interface ConnectToHostData { cols: number; @@ -70,6 +71,11 @@ async function resolveJumpHost( hostId: number, userId: string, ): Promise { + sshLogger.info("Resolving jump host", { + operation: "terminal_jumphost_resolve", + userId, + hostId, + }); try { const hosts = await SimpleDBOps.select( getDb() @@ -159,6 +165,15 @@ async function createJumpHostChain( const jumpClient = new Client(); clients.push(jumpClient); + const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier( + jumpHostConfig.id, + jumpHostConfig.ip, + jumpHostConfig.port || 22, + null, + userId, + true, + ); + const connected = await new Promise((resolve) => { const timeout = setTimeout(() => { resolve(false); @@ -166,6 +181,13 @@ async function createJumpHostChain( jumpClient.on("ready", () => { clearTimeout(timeout); + sshLogger.success("Jump host connection established", { + operation: "terminal_jumphost_connected", + userId, + hostId: jumpHostConfig.id, + ip: jumpHostConfig.ip, + depth: i, + }); resolve(true); }); @@ -185,6 +207,7 @@ async function createJumpHostChain( username: jumpHostConfig.username, tryKeyboard: true, readyTimeout: 30000, + hostVerifier: jumpHostVerifier, }; if (jumpHostConfig.authType === "password" && jumpHostConfig.password) { @@ -279,6 +302,7 @@ const wss = new WebSocketServer({ wss.on("connection", async (ws: WebSocket, req) => { let userId: string | undefined; + let sessionId: string | undefined; try { const url = parseUrl(req.url!, true); @@ -296,6 +320,7 @@ wss.on("connection", async (ws: WebSocket, req) => { } userId = payload.userId; + sessionId = payload.sessionId; } catch (error) { sshLogger.error( "WebSocket JWT verification failed during connection", @@ -327,6 +352,11 @@ wss.on("connection", async (ws: WebSocket, req) => { } const userWs = userConnections.get(userId)!; userWs.add(ws); + sshLogger.info("Terminal WebSocket connection established", { + operation: "terminal_ws_connect", + sessionId, + userId, + }); let sshConn: Client | null = null; let sshStream: ClientChannel | null = null; @@ -342,8 +372,14 @@ wss.on("connection", async (ws: WebSocket, req) => { let warpgateAuthPromptSent = false; let warpgateAuthTimeout: NodeJS.Timeout | null = null; let isAwaitingAuthCredentials = false; + let opksshTempFiles: { keyPath: string; certPath: string } | null = null; ws.on("close", () => { + sshLogger.info("Terminal WebSocket disconnected", { + operation: "terminal_ws_disconnect", + sessionId, + userId, + }); const userWs = userConnections.get(userId); if (userWs) { userWs.delete(ws); @@ -355,7 +391,17 @@ wss.on("connection", async (ws: WebSocket, req) => { cleanupSSH(); }); - ws.on("message", (msg: RawData) => { + function resetConnectionState() { + isConnecting = false; + isConnected = false; + isKeyboardInteractive = false; + keyboardInteractiveResponded = false; + keyboardInteractiveFinish = null; + totpPromptSent = false; + warpgateAuthPromptSent = false; + } + + ws.on("message", async (msg: RawData) => { const currentDataKey = userCrypto.getUserDataKey(userId); if (!currentDataKey) { ws.send( @@ -568,6 +614,116 @@ wss.on("connection", async (ws: WebSocket, req) => { break; } + case "opkssh_start_auth": { + const opksshData = data as { hostId: number }; + try { + const { startOPKSSHAuth, getRequestOrigin } = + await import("./opkssh-auth.js"); + const db = getDb(); + const hostRow = await db + .select() + .from(sshData) + .where(eq(sshData.id, opksshData.hostId)) + .limit(1); + if (!hostRow || hostRow.length === 0) { + sshLogger.error( + `Host ${opksshData.hostId} not found for OPKSSH auth`, + { + operation: "opkssh_start_auth_host_not_found", + userId, + hostId: opksshData.hostId, + }, + ); + ws.send( + JSON.stringify({ + type: "opkssh_error", + requestId: "", + error: "Host not found", + }), + ); + break; + } + const hostname = hostRow[0].name || hostRow[0].ip; + const requestOrigin = getRequestOrigin(req); + await startOPKSSHAuth( + userId, + opksshData.hostId, + hostname, + ws, + requestOrigin, + ); + } catch (error) { + sshLogger.error("Failed to start OPKSSH auth", error, { + operation: "opkssh_start_auth_error", + userId, + hostId: opksshData.hostId, + }); + ws.send( + JSON.stringify({ + type: "opkssh_error", + requestId: "", + error: "Failed to start OPKSSH authentication", + }), + ); + } + break; + } + + case "opkssh_cancel": { + const cancelData = data as { requestId: string }; + try { + const { cancelAuthSession } = await import("./opkssh-auth.js"); + cancelAuthSession(cancelData.requestId); + resetConnectionState(); + } catch (error) { + sshLogger.error("Failed to cancel OPKSSH auth", error, { + operation: "opkssh_cancel_error", + userId, + }); + } + break; + } + + case "opkssh_browser_opened": { + break; + } + + case "opkssh_auth_completed": { + const completedData = data as { + hostId: number; + cols?: number; + rows?: number; + hostConfig?: any; + }; + + resetConnectionState(); + + const reconnectConfig: ConnectToHostData = { + cols: completedData.cols || 80, + rows: completedData.rows || 24, + hostConfig: + completedData.hostConfig || + ({ id: completedData.hostId, userId } as any), + }; + + handleConnectToHost(reconnectConfig).catch((error) => { + sshLogger.error("Failed to reconnect after OPKSSH auth", error, { + operation: "opkssh_reconnect_error", + userId, + hostId: completedData.hostId, + }); + ws.send( + JSON.stringify({ + type: "error", + message: + "Failed to connect after authentication: " + + (error instanceof Error ? error.message : "Unknown error"), + }), + ); + }); + break; + } + default: sshLogger.warn("Unknown message type received", { operation: "websocket_message_unknown_type", @@ -591,6 +747,12 @@ wss.on("connection", async (ws: WebSocket, req) => { authType, credentialId, } = hostConfig; + sshLogger.info("Resolving SSH host configuration", { + operation: "terminal_host_resolve", + sessionId, + userId, + hostId: id, + }); const sendLog = ( stage: string, @@ -745,7 +907,21 @@ wss.on("connection", async (ws: WebSocket, req) => { sshConn.on("ready", () => { clearTimeout(connectionTimeout); - + sshLogger.success("SSH connection established", { + operation: "terminal_ssh_connected", + sessionId, + userId, + hostId: id, + ip, + }); + if (totpPromptSent) { + authLogger.success("TOTP verification successful for SSH session", { + operation: "terminal_totp_success", + sessionId, + userId, + hostId: id, + }); + } sendLog("handshake", "success", "SSH handshake completed"); sendLog("auth", "success", `Authentication successful for ${username}`); sendLog("connected", "success", "Connection established"); @@ -858,6 +1034,13 @@ wss.on("connection", async (ws: WebSocket, req) => { } sshStream = stream; + sshLogger.success("Terminal shell channel opened", { + operation: "terminal_shell_opened", + sessionId, + userId, + hostId: id, + termType: "xterm-256color", + }); stream.on("data", (data: Buffer) => { try { @@ -987,6 +1170,57 @@ wss.on("connection", async (ws: WebSocket, req) => { keyboardInteractiveResponded, }); + if ( + resolvedCredentials.authType === "opkssh" && + err.message.includes("All configured authentication methods failed") + ) { + sshLogger.warn("OPKSSH authentication failed - invalidating token", { + operation: "opkssh_auth_failed", + hostId: id, + userId, + error: err.message, + }); + + (async () => { + try { + const { invalidateOPKSSHToken } = await import("./opkssh-auth.js"); + await invalidateOPKSSHToken(userId, id, "SSH auth failed"); + } catch (invalidateError) { + sshLogger.error("Failed to invalidate OPKSSH token", { + operation: "opkssh_token_invalidation_error", + userId, + hostId: id, + error: invalidateError, + }); + } + })(); + + clearTimeout(connectionTimeout); + if (sshConn) { + try { + sshConn.end(); + } catch (e) {} + sshConn = null; + } + resetConnectionState(); + + sendLog( + "auth", + "error", + "OPKSSH certificate authentication failed. Please authenticate again.", + ); + + ws.send( + JSON.stringify({ + type: "opkssh_auth_required", + hostId: id, + message: + "OPKSSH authentication failed or expired. Please authenticate again.", + }), + ); + return; + } + if ( authMethodNotAvailable && resolvedCredentials.authType === "none" && @@ -1005,8 +1239,7 @@ wss.on("connection", async (ws: WebSocket, req) => { } catch (e) {} sshConn = null; } - isConnecting = false; - isConnected = false; + resetConnectionState(); ws.send( JSON.stringify({ type: "auth_method_not_available", @@ -1031,8 +1264,7 @@ wss.on("connection", async (ws: WebSocket, req) => { } catch (e) {} sshConn = null; } - isConnecting = false; - isConnected = false; + resetConnectionState(); ws.send( JSON.stringify({ type: "auth_method_not_available", @@ -1056,6 +1288,7 @@ wss.on("connection", async (ws: WebSocket, req) => { error: err.message, }, ); + resetConnectionState(); return; } @@ -1069,6 +1302,13 @@ wss.on("connection", async (ws: WebSocket, req) => { err.message.includes("authentication") || err.message.includes("Authentication") ) { + authLogger.error("SSH authentication failed", err, { + operation: "terminal_ssh_auth_failed", + sessionId, + userId, + hostId: id, + authType: resolvedCredentials.authType, + }); sendLog("auth", "error", `Authentication failed: ${err.message}`); } else { sendLog("error", "error", `Connection failed: ${err.message}`); @@ -1116,6 +1356,12 @@ wss.on("connection", async (ws: WebSocket, req) => { sshConn.on("close", () => { clearTimeout(connectionTimeout); + sshLogger.info("SSH connection closed", { + operation: "terminal_ssh_disconnected", + sessionId, + userId, + hostId: id, + }); if (isAwaitingAuthCredentials) { cleanupSSH(connectionTimeout); @@ -1209,6 +1455,14 @@ wss.on("connection", async (ws: WebSocket, req) => { tcpKeepAlive: true, tcpKeepAliveInitialDelay: 30000, timeout: 120000, + hostVerifier: await SSHHostKeyVerifier.createHostVerifier( + id, + ip, + port, + ws, + userId, + false, + ), env: { TERM: "xterm-256color", LANG: "en_US.UTF-8", @@ -1229,6 +1483,10 @@ wss.on("connection", async (ws: WebSocket, req) => { "ecdh-sha2-nistp384", "ecdh-sha2-nistp256", "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group18-sha512", + "diffie-hellman-group17-sha512", + "diffie-hellman-group16-sha512", + "diffie-hellman-group15-sha512", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1", "diffie-hellman-group-exchange-sha1", @@ -1287,7 +1545,6 @@ wss.on("connection", async (ws: WebSocket, req) => { if (!hostConfig.forceKeyboardInteractive) { connectConfig.password = resolvedCredentials.password; } - sendLog("auth", "info", "Using password authentication"); } else if ( resolvedCredentials.authType === "key" && @@ -1336,6 +1593,60 @@ wss.on("connection", async (ws: WebSocket, req) => { }), ); return; + } else if (resolvedCredentials.authType === "opkssh") { + sendLog("auth", "info", "Using OPKSSH certificate authentication"); + try { + const { getOPKSSHToken } = await import("./opkssh-auth.js"); + const token = await getOPKSSHToken(userId, id); + + if (!token) { + sendLog( + "auth", + "info", + "No valid OPKSSH token found, requesting authentication", + ); + ws.send( + JSON.stringify({ + type: "opkssh_auth_required", + hostId: id, + }), + ); + return; + } + + sendLog("auth", "info", "Using cached OPKSSH certificate"); + + const { promises: fs } = await import("fs"); + const path = await import("path"); + const os = await import("os"); + + const tempDir = os.tmpdir(); + const keyPath = path.join(tempDir, `opkssh-${userId}-${id}`); + const certPath = `${keyPath}-cert.pub`; + + await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 }); + await fs.writeFile(certPath, token.sshCert, { mode: 0o600 }); + + opksshTempFiles = { keyPath, certPath }; + connectConfig.privateKey = await fs.readFile(keyPath); + } catch (opksshError) { + sshLogger.error("OPKSSH authentication error", opksshError, { + operation: "opkssh_auth_error", + userId, + hostId: id, + }); + ws.send( + JSON.stringify({ + type: "error", + message: + "OPKSSH authentication failed: " + + (opksshError instanceof Error + ? opksshError.message + : "Unknown error"), + }), + ); + return; + } } else { sendLog("auth", "info", "Using keyboard-interactive authentication"); sshLogger.error("No valid authentication method provided"); @@ -1440,6 +1751,16 @@ wss.on("connection", async (ws: WebSocket, req) => { "Starting SSH session through jump host", ); sendLog("auth", "info", `Authenticating as ${username}`); + sshLogger.info("Initiating SSH connection", { + operation: "terminal_ssh_connect_attempt", + sessionId, + userId, + hostId: id, + ip, + port, + username, + authType: resolvedCredentials.authType, + }); sshConn.connect(connectConfig); }); } catch (error) { @@ -1459,6 +1780,16 @@ wss.on("connection", async (ws: WebSocket, req) => { } else { sendLog("handshake", "info", "Starting SSH session"); sendLog("auth", "info", `Authenticating as ${username}`); + sshLogger.info("Initiating SSH connection", { + operation: "terminal_ssh_connect_attempt", + sessionId, + userId, + hostId: id, + ip, + port, + username, + authType: resolvedCredentials.authType, + }); sshConn.connect(connectConfig); } } @@ -1529,15 +1860,43 @@ wss.on("connection", async (ws: WebSocket, req) => { sshConn = null; } - totpPromptSent = false; - warpgateAuthPromptSent = false; - isKeyboardInteractive = false; - keyboardInteractiveResponded = false; - keyboardInteractiveFinish = null; - isConnecting = false; - isConnected = false; + resetConnectionState(); isCleaningUp = false; isAwaitingAuthCredentials = false; + + if (opksshTempFiles) { + const tempFilesToClean = opksshTempFiles; + opksshTempFiles = null; + + (async () => { + try { + const { promises: fs } = await import("fs"); + const cleanupResults = await Promise.allSettled([ + fs.unlink(tempFilesToClean.keyPath), + fs.unlink(tempFilesToClean.certPath), + ]); + + cleanupResults.forEach((result, index) => { + if (result.status === "rejected") { + sshLogger.warn(`Failed to cleanup OPKSSH temp file`, { + operation: "opkssh_temp_cleanup_failed", + file: index === 0 ? "keyPath" : "certPath", + path: + index === 0 + ? tempFilesToClean.keyPath + : tempFilesToClean.certPath, + error: result.reason, + }); + } + }); + } catch (error) { + sshLogger.error("Failed to cleanup OPKSSH temp files", { + operation: "opkssh_temp_cleanup_error", + error, + }); + } + })(); + } } // Note: PTY-level keepalive (writing \x00 to the stream) was removed. diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index 5674fb4c..77aea0bd 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -30,12 +30,7 @@ app.use( origin: (origin, callback) => { if (!origin) return callback(null, true); - const allowedOrigins = [ - "http://localhost:5173", - "http://localhost:3000", - "http://127.0.0.1:5173", - "http://127.0.0.1:3000", - ]; + const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; if (allowedOrigins.includes(origin)) { return callback(null, true); @@ -294,7 +289,12 @@ async function cleanupTunnelResources( if (verification?.timeout) clearTimeout(verification.timeout); try { verification?.conn.end(); - } catch (error) {} + } catch (error) { + tunnelLogger.error("Error during tunnel cleanup", error, { + operation: "tunnel_cleanup_error", + tunnelName, + }); + } tunnelVerifications.delete(tunnelName); } @@ -359,7 +359,12 @@ async function handleDisconnect( const verification = tunnelVerifications.get(tunnelName); if (verification?.timeout) clearTimeout(verification.timeout); verification?.conn.end(); - } catch (error) {} + } catch (error) { + tunnelLogger.error("Error during tunnel cleanup", error, { + operation: "tunnel_cleanup_error", + tunnelName, + }); + } tunnelVerifications.delete(tunnelName); } @@ -519,6 +524,16 @@ async function connectSSHTunnel( ): Promise { const tunnelName = tunnelConfig.name; const tunnelMarker = getTunnelMarker(tunnelName); + tunnelLogger.info("Tunnel creation request received", { + operation: "tunnel_create_request", + userId: tunnelConfig.sourceUserId, + hostId: tunnelConfig.sourceHostId, + tunnelName, + tunnelType: tunnelConfig.tunnelType || "remote", + sourcePort: tunnelConfig.sourcePort, + endpointHost: tunnelConfig.endpointHost, + endpointPort: tunnelConfig.endpointPort, + }); if (manualDisconnects.has(tunnelName)) { return; @@ -875,6 +890,12 @@ async function connectSSHTunnel( conn.on("ready", () => { clearTimeout(connectionTimeout); + tunnelLogger.info("Creating new SSH connection for tunnel", { + operation: "tunnel_connection_create", + userId: tunnelConfig.sourceUserId, + hostId: tunnelConfig.sourceHostId, + tunnelName, + }); const isAlreadyVerifying = tunnelVerifications.has(tunnelName); if (isAlreadyVerifying) { @@ -933,6 +954,14 @@ async function connectSSHTunnel( } activeTunnels.set(tunnelName, conn); + tunnelLogger.success("Tunnel port binding successful", { + operation: "tunnel_port_bound", + userId: tunnelConfig.sourceUserId, + hostId: tunnelConfig.sourceHostId, + tunnelName, + sourcePort: tunnelConfig.sourcePort, + endpointPort: tunnelConfig.endpointPort, + }); setTimeout(() => { if ( @@ -940,6 +969,12 @@ async function connectSSHTunnel( activeTunnels.has(tunnelName) ) { tunnelConnecting.delete(tunnelName); + tunnelLogger.success("Tunnel creation complete", { + operation: "tunnel_create_complete", + userId: tunnelConfig.sourceUserId, + hostId: tunnelConfig.sourceHostId, + tunnelName, + }); broadcastTunnelStatus(tunnelName, { connected: true, @@ -1279,6 +1314,13 @@ async function killRemoteTunnelByMarker( callback: (err?: Error) => void, ) { const tunnelMarker = getTunnelMarker(tunnelName); + tunnelLogger.info("Killing remote tunnel process", { + operation: "tunnel_remote_kill", + userId: tunnelConfig.sourceUserId, + hostId: tunnelConfig.sourceHostId, + tunnelName, + marker: tunnelMarker, + }); let resolvedSourceCredentials = { password: tunnelConfig.sourcePassword, @@ -1418,10 +1460,24 @@ async function killRemoteTunnelByMarker( stream.on("close", () => { if (!foundProcesses) { + tunnelLogger.warn("Remote tunnel process not found", { + operation: "tunnel_remote_not_found", + userId: tunnelConfig.sourceUserId, + hostId: tunnelConfig.sourceHostId, + tunnelName, + marker: tunnelMarker, + }); conn.end(); callback(); return; } + tunnelLogger.info("Remote tunnel process found, proceeding to kill", { + operation: "tunnel_remote_found", + userId: tunnelConfig.sourceUserId, + hostId: tunnelConfig.sourceHostId, + tunnelName, + marker: tunnelMarker, + }); const killCmds = [ `pkill -TERM -f '${tunnelMarker}'`, @@ -1452,6 +1508,13 @@ async function killRemoteTunnelByMarker( tunnelLogger.warn( `Some tunnel processes may still be running for '${tunnelName}'`, ); + } else { + tunnelLogger.success("Remote tunnel process killed", { + operation: "tunnel_remote_killed", + userId: tunnelConfig.sourceUserId, + hostId: tunnelConfig.sourceHostId, + tunnelName, + }); } conn.end(); callback(); @@ -1843,6 +1906,12 @@ app.post( } } + tunnelLogger.info("Tunnel stop request received", { + operation: "tunnel_stop_request", + userId, + hostId: config?.sourceHostId, + tunnelName, + }); manualDisconnects.add(tunnelName); retryCounters.delete(tunnelName); retryExhaustedTunnels.delete(tunnelName); @@ -1853,6 +1922,12 @@ app.post( } await cleanupTunnelResources(tunnelName, true); + tunnelLogger.info("Tunnel cleanup completed", { + operation: "tunnel_cleanup_complete", + userId, + hostId: config?.sourceHostId, + tunnelName, + }); broadcastTunnelStatus(tunnelName, { connected: false, diff --git a/src/backend/starter.ts b/src/backend/starter.ts index 10bb8802..215e11dd 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -10,6 +10,7 @@ import { SystemCrypto } from "./utils/system-crypto.js"; import { systemLogger, versionLogger } from "./utils/logger.js"; (async () => { + const initStartTime = Date.now(); try { dotenv.config({ quiet: true }); @@ -23,6 +24,12 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; } } catch (error) {} + systemLogger.info("Termix backend initialization started", { + operation: "backend_init_start", + nodeEnv: process.env.NODE_ENV || "production", + port: process.env.PORT || 4090, + }); + let version = "unknown"; const versionSources = [ @@ -88,14 +95,41 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; await systemCrypto.initializeInternalAuthToken(); await AutoSSLSetup.initialize(); + systemLogger.success("SSL setup completed", { + operation: "backend_init_ssl", + sslEnabled: process.env.SSL_ENABLED === "true", + }); const dbModule = await import("./database/db/index.js"); await dbModule.initializeDatabase(); + systemLogger.success("Database initialized", { + operation: "backend_init_db", + }); const authManager = AuthManager.getInstance(); await authManager.initialize(); DataCrypto.initialize(); + const { OPKSSHBinaryManager } = + await import("./utils/opkssh-binary-manager.js"); + try { + await OPKSSHBinaryManager.ensureBinary(); + } catch (error) { + const dataDir = + process.env.DATA_DIR || path.join(process.cwd(), "db", "data"); + systemLogger.warn( + "Failed to initialize OPKSSH binary - OPKSSH authentication will not be available", + { + operation: "opkssh_binary_init_failed", + error: error instanceof Error ? error.message : "Unknown error", + stack: error instanceof Error ? error.stack : undefined, + platform: process.platform, + arch: process.arch, + dataDir, + }, + ); + } + await import("./database/database.js"); await import("./ssh/terminal.js"); @@ -106,6 +140,13 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; await import("./ssh/docker-console.js"); await import("./dashboard.js"); + systemLogger.success("Termix backend started successfully", { + operation: "backend_init_complete", + port: process.env.PORT || 4090, + ssl: process.env.SSL_ENABLED === "true", + duration: Date.now() - initStartTime, + }); + process.on("SIGINT", () => { systemLogger.info( "Received SIGINT signal, initiating graceful shutdown...", diff --git a/src/backend/swagger.ts b/src/backend/swagger.ts index cd30ad91..c631c1b4 100644 --- a/src/backend/swagger.ts +++ b/src/backend/swagger.ts @@ -2,6 +2,7 @@ import swaggerJSDoc from "swagger-jsdoc"; import path from "path"; import { fileURLToPath } from "url"; import { promises as fs } from "fs"; +import { systemLogger } from "./utils/logger.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -125,6 +126,10 @@ const swaggerOptions: swaggerJSDoc.Options = { async function generateOpenAPISpec() { try { + systemLogger.info("Generating OpenAPI specification", { + operation: "openapi_generate_start", + }); + const swaggerSpec = swaggerJSDoc(swaggerOptions); const outputPath = path.join(projectRoot, "openapi.json"); @@ -134,8 +139,14 @@ async function generateOpenAPISpec() { JSON.stringify(swaggerSpec, null, 2), "utf-8", ); + + systemLogger.success("OpenAPI specification generated", { + operation: "openapi_generate_success", + }); } catch (error) { - console.error("Failed to generate OpenAPI specification:", error); + systemLogger.error("Failed to generate OpenAPI specification", error, { + operation: "openapi_generation", + }); process.exit(1); } } diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index 277c5731..7a8c0d55 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -2,7 +2,7 @@ import jwt from "jsonwebtoken"; import { UserCrypto } from "./user-crypto.js"; import { SystemCrypto } from "./system-crypto.js"; import { DataCrypto } from "./data-crypto.js"; -import { databaseLogger } from "./logger.js"; +import { databaseLogger, authLogger } from "./logger.js"; import type { Request, Response, NextFunction } from "express"; import { db } from "../database/db/index.js"; import { sessions } from "../database/db/schema.js"; @@ -346,6 +346,11 @@ class AuthManager { async revokeSession(sessionId: string): Promise { try { + authLogger.info("User session invalidated", { + operation: "user_logout", + sessionId, + }); + await db.delete(sessions).where(eq(sessions.id, sessionId)); try { @@ -387,6 +392,12 @@ class AuthManager { (s) => !exceptSessionId || s.id !== exceptSessionId, ).length; + authLogger.info("All user sessions invalidated", { + operation: "user_logout_all", + userId, + sessionCount: deletedCount, + }); + if (exceptSessionId) { await db .delete(sessions) diff --git a/src/backend/utils/database-file-encryption.ts b/src/backend/utils/database-file-encryption.ts index 8ace6c46..a0217578 100644 --- a/src/backend/utils/database-file-encryption.ts +++ b/src/backend/utils/database-file-encryption.ts @@ -372,89 +372,19 @@ class DatabaseFileEncryption { encryptedPath: string, targetPath?: string, ): Promise { - if (!fs.existsSync(encryptedPath)) { - throw new Error( - `Encrypted database file does not exist: ${encryptedPath}`, - ); - } - - const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`; - if (!fs.existsSync(metadataPath)) { - throw new Error(`Metadata file does not exist: ${metadataPath}`); - } - const decryptedPath = targetPath || encryptedPath.replace(this.ENCRYPTED_FILE_SUFFIX, ""); try { - const metadataContent = fs.readFileSync(metadataPath, "utf8"); - const metadata: EncryptedFileMetadata = JSON.parse(metadataContent); + const decryptedBuffer = await this.decryptDatabaseToBuffer(encryptedPath); - const encryptedData = fs.readFileSync(encryptedPath); - - if ( - metadata.dataSize !== undefined && - encryptedData.length !== metadata.dataSize - ) { - databaseLogger.error( - "Encrypted file size mismatch - possible corrupted write or mismatched metadata", - null, - { - operation: "database_file_size_mismatch", - encryptedPath, - actualSize: encryptedData.length, - expectedSize: metadata.dataSize, - }, - ); - throw new Error( - `Encrypted file size mismatch: expected ${metadata.dataSize} bytes but got ${encryptedData.length} bytes. ` + - `This indicates corrupted files or interrupted write operation.`, - ); - } - - let key: Buffer; - if (metadata.version === "v2") { - key = await this.systemCrypto.getDatabaseKey(); - } else if (metadata.version === "v1") { - databaseLogger.warn( - "Decrypting legacy v1 encrypted database - consider upgrading", - { - operation: "decrypt_legacy_v1", - path: encryptedPath, - }, - ); - if (!metadata.salt) { - throw new Error("v1 encrypted file missing required salt field"); - } - const salt = Buffer.from(metadata.salt, "hex"); - const fixedSeed = - process.env.DB_FILE_KEY || "termix-database-file-encryption-seed-v1"; - key = crypto.pbkdf2Sync(fixedSeed, salt, 100000, 32, "sha256"); - } else { - throw new Error(`Unsupported encryption version: ${metadata.version}`); - } - - const decipher = crypto.createDecipheriv( - metadata.algorithm, - key, - Buffer.from(metadata.iv, "hex"), - ) as crypto.DecipherGCM; - decipher.setAuthTag(Buffer.from(metadata.tag, "hex")); - - const decrypted = Buffer.concat([ - decipher.update(encryptedData), - decipher.final(), - ]); - - fs.writeFileSync(decryptedPath, decrypted); + fs.writeFileSync(decryptedPath, decryptedBuffer); databaseLogger.info("Database file decrypted successfully", { operation: "database_file_decryption", encryptedPath, decryptedPath, - encryptedSize: encryptedData.length, - decryptedSize: decrypted.length, - fingerprintPrefix: metadata.fingerprint, + decryptedSize: decryptedBuffer.length, }); return decryptedPath; @@ -480,12 +410,14 @@ class DatabaseFileEncryption { try { const metadataContent = fs.readFileSync(metadataPath, "utf8"); const metadata: EncryptedFileMetadata = JSON.parse(metadataContent); - return ( + if ( metadata.version === this.VERSION && metadata.algorithm === this.ALGORITHM - ); + ) { + return true; + } } catch { - return false; + // .meta parse failed, fall through to single-file detection } } @@ -526,11 +458,32 @@ class DatabaseFileEncryption { } try { - const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`; - const metadataContent = fs.readFileSync(metadataPath, "utf8"); - const metadata: EncryptedFileMetadata = JSON.parse(metadataContent); - const fileStats = fs.statSync(encryptedPath); + let metadata: EncryptedFileMetadata | null = null; + + const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`; + if (fs.existsSync(metadataPath)) { + try { + const metadataContent = fs.readFileSync(metadataPath, "utf8"); + metadata = JSON.parse(metadataContent); + } catch { + // .meta parse failed, try single-file format + } + } + + if (!metadata) { + const fileBuffer = fs.readFileSync(encryptedPath); + const metadataLength = fileBuffer.readUInt32BE(0); + const metadataEnd = 4 + metadataLength; + const metadataJson = fileBuffer + .subarray(4, metadataEnd) + .toString("utf8"); + metadata = JSON.parse(metadataJson); + } + + if (!metadata) { + return null; + } return { version: metadata.version, diff --git a/src/backend/utils/field-crypto.ts b/src/backend/utils/field-crypto.ts index 36b7c5a2..bf524913 100644 --- a/src/backend/utils/field-crypto.ts +++ b/src/backend/utils/field-crypto.ts @@ -47,6 +47,12 @@ class FieldCrypto { "public_key", "publicKey", ]), + opkssh_tokens: new Set([ + "ssh_cert", + "sshCert", + "private_key", + "privateKey", + ]), }; static encryptField( diff --git a/src/backend/utils/opkssh-binary-manager.ts b/src/backend/utils/opkssh-binary-manager.ts new file mode 100644 index 00000000..dd55ed0f --- /dev/null +++ b/src/backend/utils/opkssh-binary-manager.ts @@ -0,0 +1,226 @@ +import { promises as fs } from "fs"; +import path from "path"; +import { createWriteStream } from "fs"; +import { pipeline } from "stream/promises"; +import { systemLogger } from "./logger.js"; + +const OPKSSH_REPO = "openpubkey/opkssh"; + +function getBinaryDir(): string { + const dataDir = + process.env.DATA_DIR || path.join(process.cwd(), "db", "data"); + return path.join(dataDir, "opkssh"); +} + +function getVersionFile(): string { + return path.join(getBinaryDir(), "version.txt"); +} + +interface GitHubAsset { + name: string; + browser_download_url: string; +} + +interface GitHubRelease { + tag_name: string; + assets: GitHubAsset[]; +} + +export class OPKSSHBinaryManager { + private static binaryPath: string | null = null; + + static async ensureBinary(): Promise { + if (this.binaryPath) { + return this.binaryPath; + } + + const binaryName = this.getBinaryName(); + const expectedPath = path.join(getBinaryDir(), binaryName); + + try { + await fs.access(expectedPath); + const needsUpdate = await this.checkForUpdate(); + if (needsUpdate) { + systemLogger.info("Newer OPKSSH version available, updating...", { + operation: "opkssh_binary_update_start", + }); + await this.downloadBinary(); + } + + this.binaryPath = expectedPath; + return expectedPath; + } catch { + systemLogger.info("OPKSSH binary not found, downloading...", { + operation: "opkssh_binary_download_start", + }); + await this.downloadBinary(); + this.binaryPath = expectedPath; + return expectedPath; + } + } + + static async downloadBinary(): Promise { + try { + await fs.mkdir(getBinaryDir(), { recursive: true }); + + const release = await this.getLatestRelease(); + + const asset = this.findMatchingAsset(release.assets); + if (!asset) { + throw new Error( + `No matching OPKSSH binary found for platform ${process.platform} ${process.arch}`, + ); + } + + const binaryName = this.getBinaryName(); + const binaryPath = path.join(getBinaryDir(), binaryName); + + const response = await fetch(asset.browser_download_url); + if (!response.ok) { + throw new Error(`Failed to download: ${response.statusText}`); + } + + if (!response.body) { + throw new Error("Response body is null"); + } + + const fileStream = createWriteStream(binaryPath); + await pipeline( + response.body as unknown as NodeJS.ReadableStream, + fileStream, + ); + + await fs.chmod(binaryPath, 0o755); + + await fs.writeFile(getVersionFile(), release.tag_name, "utf8"); + + systemLogger.info( + `OPKSSH binary downloaded successfully to ${binaryPath}`, + { + operation: "opkssh_binary_download_complete", + path: binaryPath, + version: release.tag_name, + }, + ); + } catch (error) { + systemLogger.error("Failed to download OPKSSH binary", error, { + operation: "opkssh_binary_download_error", + }); + throw error; + } + } + + static getBinaryPath(): string { + if (!this.binaryPath) { + throw new Error( + "OPKSSH binary not initialized. Call ensureBinary() first.", + ); + } + return this.binaryPath; + } + + private static async checkForUpdate(): Promise { + try { + let localVersion: string | null = null; + try { + localVersion = await fs.readFile(getVersionFile(), "utf8"); + localVersion = localVersion.trim(); + } catch { + return true; + } + + const release = await this.getLatestRelease(); + const latestVersion = release.tag_name; + + if (localVersion !== latestVersion) { + return true; + } + + return false; + } catch (error) { + systemLogger.warn("Failed to check for OPKSSH updates", { + operation: "opkssh_update_check_failed", + error: error instanceof Error ? error.message : "Unknown error", + }); + return false; + } + } + + private static async getLatestRelease(): Promise { + const url = `https://api.github.com/repos/${OPKSSH_REPO}/releases/latest`; + const response = await fetch(url, { + headers: { + "User-Agent": "Termix", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch release info: ${response.statusText}`); + } + + return (await response.json()) as GitHubRelease; + } + + private static findMatchingAsset(assets: GitHubAsset[]): GitHubAsset | null { + const platform = process.platform; + const arch = process.arch; + + const osMap: Record = { + win32: "windows", + linux: "linux", + darwin: "darwin", + }; + + const archMap: Record = { + x64: "amd64", + arm64: "arm64", + }; + + const mappedOs = osMap[platform]; + const mappedArch = archMap[arch]; + + if (!mappedOs || !mappedArch) { + return null; + } + + const patterns = [ + `opkssh-${mappedOs}-${mappedArch}.exe`, + `opkssh-${mappedOs}-${mappedArch}`, + `opkssh_${mappedOs}_${mappedArch}.exe`, + `opkssh_${mappedOs}_${mappedArch}`, + ]; + + for (const pattern of patterns) { + const asset = assets.find( + (a) => a.name.toLowerCase() === pattern.toLowerCase(), + ); + if (asset) { + return asset; + } + } + + return null; + } + + private static getBinaryName(): string { + const platform = process.platform; + const arch = process.arch; + + const osMap: Record = { + win32: "windows", + linux: "linux", + darwin: "darwin", + }; + + const archMap: Record = { + x64: "amd64", + arm64: "arm64", + }; + + const mappedOs = osMap[platform] || platform; + const mappedArch = archMap[arch] || arch; + + const extension = platform === "win32" ? ".exe" : ""; + return `opkssh-${mappedOs}-${mappedArch}${extension}`; + } +} diff --git a/src/backend/utils/proxy-agent.ts b/src/backend/utils/proxy-agent.ts new file mode 100644 index 00000000..71a9d6e7 --- /dev/null +++ b/src/backend/utils/proxy-agent.ts @@ -0,0 +1,32 @@ +import { HttpsProxyAgent } from "https-proxy-agent"; +import type { Agent } from "http"; + +export function getProxyAgent(targetUrl?: string): Agent | undefined { + const proxyUrl = + process.env.https_proxy || + process.env.HTTPS_PROXY || + process.env.http_proxy || + process.env.HTTP_PROXY; + + if (!proxyUrl) return undefined; + + if (targetUrl) { + const noProxy = process.env.no_proxy || process.env.NO_PROXY || ""; + const hostname = new URL(targetUrl).hostname.toLowerCase(); + + for (const entry of noProxy.split(",")) { + const trimmed = entry.trim().toLowerCase(); + if (!trimmed) continue; + + const normalized = trimmed + .replace(/^\*\./, "") + .replace(/^\./, ""); + + if (hostname === normalized || hostname.endsWith(`.${normalized}`)) { + return undefined; + } + } + } + + return new HttpsProxyAgent(proxyUrl); +} diff --git a/src/backend/utils/user-crypto.ts b/src/backend/utils/user-crypto.ts index 593fd08b..b536bcf7 100644 --- a/src/backend/utils/user-crypto.ts +++ b/src/backend/utils/user-crypto.ts @@ -303,9 +303,8 @@ class UserCrypto { await this.storeKEKSalt(userId, newKekSalt); await this.storeEncryptedDEK(userId, newEncryptedDEK); - const { saveMemoryDatabaseToFile } = await import( - "../database/db/index.js" - ); + const { saveMemoryDatabaseToFile } = + await import("../database/db/index.js"); await saveMemoryDatabaseToFile(); oldKEK.fill(0); @@ -341,9 +340,8 @@ class UserCrypto { await this.storeKEKSalt(userId, newKekSalt); await this.storeEncryptedDEK(userId, newEncryptedDEK); - const { saveMemoryDatabaseToFile } = await import( - "../database/db/index.js" - ); + const { saveMemoryDatabaseToFile } = + await import("../database/db/index.js"); await saveMemoryDatabaseToFile(); newKEK.fill(0); @@ -364,15 +362,6 @@ class UserCrypto { } } - /** - * Convert a password-based user's encryption to DUAL-AUTH encryption. - * This is used when linking an OIDC account to a password account for dual-auth. - * - * IMPORTANT: This does NOT delete the password-based KEK salt! - * The user needs to maintain BOTH password and OIDC authentication methods. - * We keep the password KEK salt so password login still works. - * We also store the DEK encrypted with OIDC system key for OIDC login. - */ async convertToOIDCEncryption(userId: string): Promise { try { const existingEncryptedDEK = await this.getEncryptedDEK(userId); @@ -427,9 +416,8 @@ class UserCrypto { }, ); - const { saveMemoryDatabaseToFile } = await import( - "../database/db/index.js" - ); + const { saveMemoryDatabaseToFile } = + await import("../database/db/index.js"); await saveMemoryDatabaseToFile(); } catch (error) { databaseLogger.error("Failed to convert to OIDC encryption", error, { diff --git a/src/hooks/use-service-worker.ts b/src/hooks/use-service-worker.ts index d21e8a30..f7d12781 100644 --- a/src/hooks/use-service-worker.ts +++ b/src/hooks/use-service-worker.ts @@ -1,5 +1,6 @@ import { useEffect, useState, useCallback } from "react"; import { isElectron } from "@/ui/main-axios"; +import { getBasePath } from "@/lib/base-path"; interface ServiceWorkerState { isSupported: boolean; @@ -41,7 +42,9 @@ export function useServiceWorker(): ServiceWorkerState { const registerSW = async () => { try { - const registration = await navigator.serviceWorker.register("/sw.js"); + const registration = await navigator.serviceWorker.register( + `${getBasePath()}/sw.js`, + ); setState((prev) => ({ ...prev, isRegistered: true })); registration.addEventListener("updatefound", () => diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index 39ba04e0..c15e938e 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -3,38 +3,40 @@ import { initReactI18next } from "react-i18next"; import LanguageDetector from "i18next-browser-languagedetector"; import enTranslation from "../locales/en.json"; -import afTranslation from "../locales/translated/af.json"; -import arTranslation from "../locales/translated/ar.json"; -import bnTranslation from "../locales/translated/bn.json"; -import bgTranslation from "../locales/translated/bg.json"; -import caTranslation from "../locales/translated/ca.json"; -import csTranslation from "../locales/translated/cs.json"; -import daTranslation from "../locales/translated/da.json"; -import deTranslation from "../locales/translated/de.json"; -import elTranslation from "../locales/translated/el.json"; -import esTranslation from "../locales/translated/es.json"; -import fiTranslation from "../locales/translated/fi.json"; -import frTranslation from "../locales/translated/fr.json"; -import heTranslation from "../locales/translated/he.json"; -import hiTranslation from "../locales/translated/hi.json"; -import huTranslation from "../locales/translated/hu.json"; -import idTranslation from "../locales/translated/id.json"; -import itTranslation from "../locales/translated/it.json"; -import jaTranslation from "../locales/translated/ja.json"; -import koTranslation from "../locales/translated/ko.json"; -import nlTranslation from "../locales/translated/nl.json"; -import noTranslation from "../locales/translated/no.json"; -import plTranslation from "../locales/translated/pl.json"; -import ptTranslation from "../locales/translated/pt.json"; -import roTranslation from "../locales/translated/ro.json"; -import ruTranslation from "../locales/translated/ru.json"; -import srTranslation from "../locales/translated/sr.json"; -import svTranslation from "../locales/translated/sv.json"; -import thTranslation from "../locales/translated/th.json"; -import trTranslation from "../locales/translated/tr.json"; -import ukTranslation from "../locales/translated/uk.json"; -import viTranslation from "../locales/translated/vi.json"; -import zhTranslation from "../locales/translated/zh.json"; +import afTranslation from "../locales/translated/af_ZA.json"; +import arTranslation from "../locales/translated/ar_SA.json"; +import bnTranslation from "../locales/translated/bn_BD.json"; +import bgTranslation from "../locales/translated/bg_BG.json"; +import caTranslation from "../locales/translated/ca_ES.json"; +import csTranslation from "../locales/translated/cs_CZ.json"; +import daTranslation from "../locales/translated/da_DK.json"; +import deTranslation from "../locales/translated/de_DE.json"; +import elTranslation from "../locales/translated/el_GR.json"; +import esESTranslation from "../locales/translated/es_ES.json"; +import fiTranslation from "../locales/translated/fi_FI.json"; +import frTranslation from "../locales/translated/fr_FR.json"; +import heTranslation from "../locales/translated/he_IL.json"; +import hiTranslation from "../locales/translated/hi_IN.json"; +import huTranslation from "../locales/translated/hu_HU.json"; +import idTranslation from "../locales/translated/id_ID.json"; +import itTranslation from "../locales/translated/it_IT.json"; +import jaTranslation from "../locales/translated/ja_JP.json"; +import koTranslation from "../locales/translated/ko_KR.json"; +import nlTranslation from "../locales/translated/nl_NL.json"; +import noTranslation from "../locales/translated/no_NO.json"; +import plTranslation from "../locales/translated/pl_PL.json"; +import ptPTTranslation from "../locales/translated/pt_PT.json"; +import ptBRTranslation from "../locales/translated/pt_BR.json"; +import roTranslation from "../locales/translated/ro_RO.json"; +import ruTranslation from "../locales/translated/ru_RU.json"; +import srTranslation from "../locales/translated/sr_SP.json"; +import svSETranslation from "../locales/translated/sv_SE.json"; +import thTranslation from "../locales/translated/th_TH.json"; +import trTranslation from "../locales/translated/tr_TR.json"; +import ukTranslation from "../locales/translated/uk_UA.json"; +import viTranslation from "../locales/translated/vi_VN.json"; +import zhCNTranslation from "../locales/translated/zh_CN.json"; +import zhTWTranslation from "../locales/translated/zh_TW.json"; i18n .use(LanguageDetector) @@ -51,7 +53,7 @@ i18n "da", "de", "el", - "es", + "es-ES", "fi", "fr", "he", @@ -64,16 +66,18 @@ i18n "nl", "no", "pl", - "pt", + "pt-PT", + "pt-BR", "ro", "ru", "sr", - "sv", + "sv-SE", "th", "tr", "uk", "vi", - "zh", + "zh-CN", + "zh-TW", ], fallbackLng: "en", debug: false, @@ -117,8 +121,8 @@ i18n el: { translation: elTranslation, }, - es: { - translation: esTranslation, + "es-ES": { + translation: esESTranslation, }, fi: { translation: fiTranslation, @@ -156,8 +160,11 @@ i18n pl: { translation: plTranslation, }, - pt: { - translation: ptTranslation, + "pt-PT": { + translation: ptPTTranslation, + }, + "pt-BR": { + translation: ptBRTranslation, }, ro: { translation: roTranslation, @@ -168,8 +175,8 @@ i18n sr: { translation: srTranslation, }, - sv: { - translation: svTranslation, + "sv-SE": { + translation: svSETranslation, }, th: { translation: thTranslation, @@ -183,8 +190,11 @@ i18n vi: { translation: viTranslation, }, - zh: { - translation: zhTranslation, + "zh-CN": { + translation: zhCNTranslation, + }, + "zh-TW": { + translation: zhTWTranslation, }, }, diff --git a/src/lib/base-path.ts b/src/lib/base-path.ts new file mode 100644 index 00000000..0352e12c --- /dev/null +++ b/src/lib/base-path.ts @@ -0,0 +1,5 @@ +export function getBasePath(): string { + const base = import.meta.env.BASE_URL || "/"; + if (base === "./" || base === "/") return ""; + return base.endsWith("/") ? base.slice(0, -1) : base; +} diff --git a/src/lib/db-health-monitor.ts b/src/lib/db-health-monitor.ts index 298a84c1..971e501e 100644 --- a/src/lib/db-health-monitor.ts +++ b/src/lib/db-health-monitor.ts @@ -6,6 +6,8 @@ class DatabaseHealthMonitor { private lastCheckTime: number = 0; private checkInProgress: boolean = false; private listeners: Map = new Map(); + private consecutiveErrorTimer: ReturnType | null = null; + private confirmedUnhealthy: boolean = false; private constructor() {} @@ -40,10 +42,13 @@ class DatabaseHealthMonitor { } } - reportDatabaseError(error: any, wasAuthenticated: boolean = false) { + reportSessionExpired() { + this.emit("session-expired", { timestamp: Date.now() }); + } + + reportDatabaseError(error: any, _wasAuthenticated: boolean = false) { const errorMessage = error?.response?.data?.error || error?.message || ""; const errorCode = error?.response?.data?.code || error?.code; - const httpStatus = error?.response?.status; const isDatabaseError = errorMessage.toLowerCase().includes("database") || @@ -58,35 +63,44 @@ class DatabaseHealthMonitor { (errorMessage.toLowerCase().includes("network error") && error?.response === undefined); - const isAuthenticationLost = - wasAuthenticated && - httpStatus === 401 && - (errorCode === "AUTH_REQUIRED" || - errorCode === "SESSION_EXPIRED" || - errorCode === "SESSION_NOT_FOUND" || - errorMessage === "Missing authentication token" || - errorMessage === "Invalid token" || - errorMessage === "Authentication required"); + if (!(isDatabaseError || isBackendUnreachable)) { + return; + } - if ( - (isDatabaseError || isBackendUnreachable || isAuthenticationLost) && - this.dbHealthy - ) { - this.dbHealthy = false; - this.emit("database-connection-lost", { - error: errorMessage || "Backend server unreachable", - code: errorCode, - timestamp: Date.now(), - }); + if (this.dbHealthy && !this.consecutiveErrorTimer) { + this.consecutiveErrorTimer = setTimeout(() => { + this.consecutiveErrorTimer = null; + if (this.dbHealthy) { + this.dbHealthy = false; + this.confirmedUnhealthy = true; + this.emit("database-connection-lost", { + error: errorMessage || "Backend server unreachable", + code: errorCode, + timestamp: Date.now(), + }); + } + }, 10000); } } reportDatabaseSuccess() { - if (!this.dbHealthy) { + this.clearErrorTimer(); + + if (this.confirmedUnhealthy) { this.dbHealthy = true; + this.confirmedUnhealthy = false; this.emit("database-connection-restored", { timestamp: Date.now(), }); + } else if (!this.dbHealthy) { + this.dbHealthy = true; + } + } + + private clearErrorTimer(): void { + if (this.consecutiveErrorTimer !== null) { + clearTimeout(this.consecutiveErrorTimer); + this.consecutiveErrorTimer = null; } } @@ -96,8 +110,10 @@ class DatabaseHealthMonitor { reset() { this.dbHealthy = true; + this.confirmedUnhealthy = false; this.lastCheckTime = 0; this.checkInProgress = false; + this.clearErrorTimer(); } } diff --git a/src/locales/en.json b/src/locales/en.json index 04ae2541..856d8d4a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1,5 +1,6 @@ { "credentials": { + "credentials": "Credentials", "credentialsViewer": "Credentials Viewer", "manageYourSSHCredentials": "Manage your SSH credentials securely", "addCredential": "Add Credential", @@ -99,6 +100,10 @@ "deploymentProcessDescription": "This will safely add the public key to the target host's ~/.ssh/authorized_keys file without overwriting existing keys. The operation is reversible.", "chooseHostToDeploy": "Choose a host to deploy to...", "deploying": "Deploying...", + "copyDeployCommand": "Copy Deploy Command", + "copyDeployCommandDescription": "Copy a shell command to manually add the public key to a host's authorized_keys file. Useful when password authentication is disabled.", + "deployCommandCopied": "Deploy command copied to clipboard", + "manualDeployInfo": "Paste this command in the target host's terminal (e.g., Proxmox console, IPMI, or physical access) to add the SSH key.", "name": "Name", "noHostsAvailable": "No hosts available", "noHostsMatchSearch": "No hosts match your search", @@ -298,7 +303,9 @@ "createFolder": "Create Folder", "editFolder": "Edit Folder", "editFolderDescription": "Customize your snippet folder", - "createFolderDescription": "Organize your snippets into folders" + "createFolderDescription": "Organize your snippets into folders", + "confirmExecution": "Execute \"{{name}}\"?", + "confirmExecutionDesc": "Are you sure you want to execute this snippet?" }, "commandHistory": { "title": "History", @@ -402,6 +409,7 @@ "required": "Required", "optional": "Optional", "connect": "Connect", + "copied": "Copied", "connecting": "Connecting...", "creating": "Creating...", "clear": "Clear", @@ -494,6 +502,7 @@ "checking": "Checking...", "checkingDatabase": "Checking database connection...", "checkingAuthentication": "Checking authentication...", + "backendReconnected": "Server connection restored", "actions": "Actions", "remove": "Remove", "revoke": "Revoke", @@ -814,6 +823,7 @@ }, "hosts": { "title": "Host Manager", + "hosts": "Hosts", "sshHosts": "SSH Hosts", "noHosts": "No SSH Hosts", "noHostsMessage": "You haven't added any SSH hosts yet. Click \"Add Host\" to get started.", @@ -844,7 +854,7 @@ "failedToImportJson": "Failed to import JSON file", "connectionDetails": "Connection Details", "organization": "Organization", - "ipAddress": "IP Address", + "ipAddress": "IP Address or Hostname", "port": "Port", "name": "Name", "username": "Username", @@ -923,6 +933,8 @@ "key": "Key", "credential": "Credential", "none": "None", + "opkssh": "OPKSSH", + "opksshAuthDescription": "Prompts you for OPKSSH web-auth upon every connect. Your auth token will last for 24 hours until it must be renewed.", "selectCredential": "Select Credential", "selectCredentialPlaceholder": "Choose a credential...", "credentialRequired": "Credential is required when using credential authentication", @@ -1093,6 +1105,7 @@ "noneAuthTitle": "Keyboard-Interactive Authentication", "noneAuthDescription": "This authentication method will use keyboard-interactive authentication when connecting to the SSH server.", "noneAuthDetails": "Keyboard-interactive authentication allows the server to prompt you for credentials during connection. This is useful for servers that require multi-factor authentication or if you do not want to save credentials locally.", + "opksshAuthTitle": "OPKSSH Authentication", "forceKeyboardInteractive": "Force Keyboard-Interactive", "forceKeyboardInteractiveDesc": "Forces the use of keyboard-interactive authentication. This is sometimes required for servers that use Two-Factor Authentication (TOTP/2FA).", "overrideCredentialUsername": "Override Credential Username", @@ -1349,6 +1362,15 @@ "warpgateOpenBrowser": "Open in Browser", "warpgateContinue": "I've Completed Authentication", "warpgateTimeout": "Warpgate authentication timeout. Please reconnect.", + "opksshAuthRequired": "OPKSSH Authentication Required", + "opksshAuthDescription": "Complete authentication in your browser to continue. This session will remain valid for 24 hours.", + "opksshAuthUrl": "Authentication URL", + "opksshOpenBrowser": "Open Browser to Authenticate", + "opksshWaitingForAuth": "Waiting for authentication in browser...", + "opksshAuthenticating": "Processing authentication...", + "opksshTimeout": "Authentication timed out. Please try again.", + "opksshAuthFailed": "Authentication failed. Please check your credentials and try again.", + "opksshConfigMissing": "OPKSSH configuration not found. Please create ~/.opk/config.yml with your OIDC provider settings. See documentation: https://github.com/openpubkey/opkssh#configuration", "sudoPasswordPopupTitle": "Insert Password?", "sudoPasswordPopupHint": "Press Enter to insert, Esc to dismiss", "sudoPasswordPopupConfirm": "Insert", @@ -1370,7 +1392,8 @@ "automaticFallback": "Automatically trying {{method}} authentication...", "totpTimeout": "TOTP verification timeout. Please reconnect.", "passwordTimeout": "Password verification timeout. Please reconnect.", - "connectionRejected": "Connection rejected by server. Please check your authentication and network configuration." + "connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.", + "hostKeyRejected": "SSH host key verification rejected. Connection cancelled." }, "fileManager": { "title": "File Manager", @@ -1973,6 +1996,20 @@ "passwordResetSuccess": "Password Reset Successful", "passwordResetSuccessDesc": "Your password has been reset successfully. You can now log in with your new password." }, + "hostKey": { + "verifyNewHost": "Verify SSH Host Key", + "keyChangedWarning": "SSH Host Key Changed", + "firstConnectionTitle": "First time connecting to this host", + "firstConnectionDescription": "The authenticity of this host cannot be established. Verify the fingerprint matches what you expect.", + "keyChangedDescription": "The host key for this server has changed since your last connection. This could indicate a security issue.", + "previousKey": "Previous Key", + "newFingerprint": "New Fingerprint", + "fingerprint": "Fingerprint", + "verifyInstructions": "If you trust this host, click Accept to continue and save this fingerprint for future connections.", + "securityWarning": "Security Warning", + "acceptAndContinue": "Accept & Continue", + "acceptNewKey": "Accept New Key & Continue" + }, "errors": { "notFound": "Page not found", "unauthorized": "Unauthorized access", @@ -2065,6 +2102,8 @@ "terminalSettings": "Terminal", "hostSidebarSettings": "Host & Sidebar", "snippetsSettings": "Snippets", + "confirmSnippetExecution": "Confirm Snippet Execution", + "confirmSnippetExecutionDesc": "Show confirmation dialog before executing snippets", "updateSettings": "Updates", "disableUpdateCheck": "Disable Update Check", "disableUpdateCheckDesc": "Stop checking for new versions on startup and dashboard. Reduces network requests.", @@ -2085,7 +2124,7 @@ }, "placeholders": { "enterCode": "000000", - "ipAddress": "127.0.0.1", + "ipAddress": "192.168.1.1 or example.com", "port": "22", "maxRetries": "3", "retryInterval": "10", diff --git a/src/locales/translated/af.json b/src/locales/translated/af_ZA.json similarity index 100% rename from src/locales/translated/af.json rename to src/locales/translated/af_ZA.json diff --git a/src/locales/translated/ar.json b/src/locales/translated/ar_SA.json similarity index 100% rename from src/locales/translated/ar.json rename to src/locales/translated/ar_SA.json diff --git a/src/locales/translated/bg.json b/src/locales/translated/bg_BG.json similarity index 100% rename from src/locales/translated/bg.json rename to src/locales/translated/bg_BG.json diff --git a/src/locales/translated/bn.json b/src/locales/translated/bn_BD.json similarity index 100% rename from src/locales/translated/bn.json rename to src/locales/translated/bn_BD.json diff --git a/src/locales/translated/ca.json b/src/locales/translated/ca_ES.json similarity index 100% rename from src/locales/translated/ca.json rename to src/locales/translated/ca_ES.json diff --git a/src/locales/translated/cs.json b/src/locales/translated/cs_CZ.json similarity index 100% rename from src/locales/translated/cs.json rename to src/locales/translated/cs_CZ.json diff --git a/src/locales/translated/da.json b/src/locales/translated/da_DK.json similarity index 100% rename from src/locales/translated/da.json rename to src/locales/translated/da_DK.json diff --git a/src/locales/translated/de.json b/src/locales/translated/de_DE.json similarity index 100% rename from src/locales/translated/de.json rename to src/locales/translated/de_DE.json diff --git a/src/locales/translated/el.json b/src/locales/translated/el_GR.json similarity index 100% rename from src/locales/translated/el.json rename to src/locales/translated/el_GR.json diff --git a/src/locales/translated/en.json b/src/locales/translated/en.json deleted file mode 100644 index 140542f9..00000000 --- a/src/locales/translated/en.json +++ /dev/null @@ -1,2665 +0,0 @@ -{ - "credentials": { - "credentialsViewer": "Credentials Viewer", - "manageYourSSHCredentials": "Manage your SSH credentials securely", - "addCredential": "Add Credential", - "createCredential": "Create Credential", - "editCredential": "Edit Credential", - "viewCredential": "View Credential", - "duplicateCredential": "Duplicate Credential", - "deleteCredential": "Delete Credential", - "updateCredential": "Update Credential", - "credentialName": "Credential Name", - "credentialDescription": "Description", - "username": "Username", - "searchCredentials": "Search credentials...", - "selectFolder": "Select Folder", - "selectAuthType": "Select Auth Type", - "allFolders": "All Folders", - "allAuthTypes": "All Auth Types", - "uncategorized": "Uncategorized", - "totalCredentials": "Total", - "keyBased": "Key-based", - "passwordBased": "Password-based", - "folders": "Folders", - "noCredentialsMatchFilters": "No credentials match your filters", - "noCredentialsYet": "No credentials created yet", - "createFirstCredential": "Create your first credential", - "failedToFetchCredentials": "Failed to fetch credentials", - "credentialDeletedSuccessfully": "Credential deleted successfully", - "failedToDeleteCredential": "Failed to delete credential", - "confirmDeleteCredential": "Are you sure you want to delete credential \"{{name}}\"?", - "credentialCreatedSuccessfully": "Credential created successfully", - "credentialUpdatedSuccessfully": "Credential updated successfully", - "failedToSaveCredential": "Failed to save credential", - "failedToFetchCredentialDetails": "Failed to fetch credential details", - "failedToFetchHostsUsing": "Failed to fetch hosts using this credential", - "loadingCredentials": "Loading credentials...", - "retry": "Retry", - "noCredentials": "No Credentials", - "noCredentialsMessage": "You haven't added any credentials yet. Click \"Add Credential\" to get started.", - "sshCredentials": "SSH Credentials", - "credentialsCount": "{{count}} credentials", - "refresh": "Refresh", - "passwordRequired": "Password is required", - "sshKeyRequired": "SSH key is required", - "credentialAddedSuccessfully": "Credential \"{{name}}\" added successfully", - "savingCredential": "Saving credential...", - "updatingCredential": "Updating credential...", - "general": "General", - "description": "Description", - "folder": "Folder", - "tags": "Tags", - "addTagsSpaceToAdd": "Add tags (press space to add)", - "password": "Password", - "key": "Key", - "sshPrivateKey": "SSH Private Key", - "upload": "Upload", - "updateKey": "Update Key", - "keyPassword": "Key Password", - "keyType": "Key Type", - "keyTypeRSA": "RSA", - "keyTypeECDSA": "ECDSA", - "keyTypeEd25519": "Ed25519", - "basicInfo": "Basic Info", - "authentication": "Authentication", - "organization": "Organization", - "basicInformation": "Basic Information", - "basicInformationDescription": "Enter the basic information for this credential", - "authenticationMethod": "Authentication Method", - "authenticationMethodDescription": "Choose how you want to authenticate with SSH servers", - "organizationDescription": "Organize your credentials with folders and tags", - "enterCredentialName": "Enter credential name", - "enterCredentialDescription": "Enter description (optional)", - "enterUsername": "Enter username", - "nameIsRequired": "Credential name is required", - "usernameIsRequired": "Username is required", - "authenticationType": "Authentication Type", - "passwordAuthDescription": "Use password authentication", - "sshKeyAuthDescription": "Use SSH key authentication", - "passwordIsRequired": "Password is required", - "sshKeyIsRequired": "SSH key is required", - "sshKeyType": "SSH Key Type", - "privateKey": "Private Key", - "enterPassword": "Enter password", - "enterPrivateKey": "Enter private key", - "keyPassphrase": "Key Passphrase", - "enterKeyPassphrase": "Enter key passphrase (optional)", - "keyPassphraseOptional": "Optional: leave empty if your key has no passphrase", - "leaveEmptyToKeepCurrent": "Leave empty to keep current value", - "uploadKeyFile": "Upload Key File", - "generateKeyPairButton": "Generate Key Pair", - "generateKeyPair": "Generate Key Pair", - "generateKeyPairDescription": "Generate a new SSH key pair. If you want to protect the key with a passphrase, enter it in the Key Password field below first.", - "deploySSHKey": "Deploy SSH Key", - "deploySSHKeyDescription": "Deploy public key to target server", - "sourceCredential": "Source Credential", - "targetHost": "Target Host", - "deploymentProcess": "Deployment Process", - "deploymentProcessDescription": "This will safely add the public key to the target host's ~/.ssh/authorized_keys file without overwriting existing keys. The operation is reversible.", - "chooseHostToDeploy": "Choose a host to deploy to...", - "deploying": "Deploying...", - "name": "Name", - "noHostsAvailable": "No hosts available", - "noHostsMatchSearch": "No hosts match your search", - "sshKeyGenerationNotImplemented": "SSH key generation feature coming soon", - "connectionTestingNotImplemented": "Connection testing feature coming soon", - "testConnection": "Test Connection", - "selectOrCreateFolder": "Select or create folder", - "noFolder": "No folder", - "orCreateNewFolder": "Or create new folder", - "addTag": "Add tag", - "saving": "Saving...", - "credentialId": "Credential ID", - "overview": "Overview", - "security": "Security", - "usage": "Usage", - "securityDetails": "Security Details", - "securityDetailsDescription": "View encrypted credential information", - "credentialSecured": "Credential Secured", - "credentialSecuredDescription": "All sensitive data is encrypted with AES-256", - "passwordAuthentication": "Password Authentication", - "keyAuthentication": "Key Authentication", - "securityReminder": "Security Reminder", - "securityReminderText": "Never share your credentials. All data is encrypted at rest.", - "hostsUsingCredential": "Hosts Using This Credential", - "noHostsUsingCredential": "No hosts are currently using this credential", - "timesUsed": "Times Used", - "lastUsed": "Last Used", - "connectedHosts": "Connected Hosts", - "created": "Created", - "lastModified": "Last Modified", - "usageStatistics": "Usage Statistics", - "copiedToClipboard": "{{field}} copied to clipboard", - "failedToCopy": "Failed to copy to clipboard", - "sshKey": "SSH Key", - "createCredentialDescription": "Create a new SSH credential for secure access", - "editCredentialDescription": "Update the credential information", - "listView": "List", - "folderView": "Folders", - "unknownCredential": "Unknown", - "confirmRemoveFromFolder": "Are you sure you want to remove \"{{name}}\" from folder \"{{folder}}\"? The credential will be moved to \"Uncategorized\".", - "removedFromFolder": "Credential \"{{name}}\" removed from folder successfully", - "failedToRemoveFromFolder": "Failed to remove credential from folder", - "folderRenamed": "Folder \"{{oldName}}\" renamed to \"{{newName}}\" successfully", - "failedToRenameFolder": "Failed to rename folder", - "movedToFolder": "Credential \"{{name}}\" moved to \"{{folder}}\" successfully", - "failedToMoveToFolder": "Failed to move credential to folder", - "sshPublicKey": "SSH Public Key", - "publicKeyNote": "Public key is optional but recommended for key validation", - "publicKeyUploaded": "Public Key Uploaded", - "uploadPublicKey": "Upload Public Key", - "uploadPrivateKeyFile": "Upload Private Key File", - "uploadPublicKeyFile": "Upload Public Key File", - "privateKeyRequiredForGeneration": "Private key is required to generate public key", - "failedToGeneratePublicKey": "Failed to generate public key", - "generatePublicKey": "Generate from Private Key", - "publicKeyGeneratedSuccessfully": "Public key generated successfully", - "detectedKeyType": "Detected key type", - "detectingKeyType": "detecting...", - "optional": "Optional", - "generateKeyPairNew": "Generate New Key Pair", - "generateEd25519": "Generate Ed25519", - "generateECDSA": "Generate ECDSA", - "generateRSA": "Generate RSA", - "keyTypeEcdsaP256": "ECDSA P-256 (SSH)", - "keyTypeEcdsaP384": "ECDSA P-384 (SSH)", - "keyTypeEcdsaP521": "ECDSA P-521 (SSH)", - "keyTypeDsa": "DSA (SSH)", - "keyTypeRsaSha256": "RSA-SHA2-256", - "keyTypeRsaSha512": "RSA-SHA2-512", - "keyPairGeneratedSuccessfully": "{{keyType}} key pair generated successfully", - "failedToGenerateKeyPair": "Failed to generate key pair", - "generateKeyPairNote": "Generate a new SSH key pair directly. This will replace any existing keys in the form.", - "invalidKey": "Invalid Key", - "detectionError": "Detection Error", - "removing": "Removing:", - "clickToEditCredential": "Click to edit credential", - "dragToMoveBetweenFolders": "Drag to move between folders", - "keyBasedOnlyForDeployment": "Only SSH key-based credentials can be deployed", - "publicKeyRequiredForDeployment": "Public key is required for deployment", - "selectTargetHost": "Please select a target host", - "keyDeployedSuccessfully": "SSH key deployed successfully", - "deploymentFailed": "Deployment failed", - "failedToDeployKey": "Failed to deploy SSH key", - "clickToRenameFolder": "Click to rename folder", - "renameFolder": "Rename folder", - "idLabel": "ID:" - }, - "quickConnect": { - "title": "Quick Connect", - "description": "Connect directly to a terminal or file manager session without saving a host configuration", - "ipAddress": "IP Address or Hostname", - "port": "Port", - "username": "Username", - "password": "Password", - "key": "SSH Private Key", - "keyPassword": "Key Password (Optional)", - "keyType": "Key Type", - "uploadFile": "Upload File", - "pasteKey": "Paste Key", - "credential": "Credential", - "overrideUsername": "Override Credential Username", - "overrideUsernameDesc": "Use a different username than the one stored in the credential", - "connectTerminal": "Connect to Terminal", - "connectFileManager": "Connect to File Manager", - "cancel": "Cancel", - "passwordRequired": "Password is required", - "keyRequired": "SSH key is required", - "credentialRequired": "Credential selection is required", - "connectionEstablished": "Connection established successfully", - "connectionFailed": "Failed to establish connection", - "autoDetect": "Auto Detect", - "authentication": "Authentication" - }, - "dragIndicator": { - "error": "Error: {{error}}", - "dragging": "Dragging {{fileName}}", - "preparing": "Preparing {{fileName}}", - "readySingle": "Ready to download {{fileName}}", - "readyMultiple": "Ready to download {{count}} files", - "batchDrag": "Drag {{count}} files to desktop", - "dragToDesktop": "Drag to desktop", - "canDragAnywhere": "You can drag files anywhere on your desktop" - }, - "sshTools": { - "title": "SSH Tools", - "closeTools": "Close SSH Tools", - "keyRecording": "Key Recording", - "startKeyRecording": "Start Key Recording", - "stopKeyRecording": "Stop Key Recording", - "selectTerminals": "Select terminals:", - "typeCommands": "Type commands (all keys supported):", - "commandsWillBeSent": "Commands will be sent to {{count}} selected terminal(s).", - "settings": "Settings", - "enableRightClickCopyPaste": "Enable right‑click copy/paste", - "shareIdeas": "Have ideas for what should come next for ssh tools? Share them on", - "scripts": { - "inputPlaceholder": "e.g., System Commands, Docker Scripts" - } - }, - "snippets": { - "title": "Snippets", - "new": "New Snippet", - "create": "Create Snippet", - "edit": "Edit Snippet", - "run": "Run", - "empty": "No snippets yet", - "emptyHint": "Create a snippet to save commonly used commands", - "searchSnippets": "Search snippets...", - "name": "Name", - "description": "Description", - "content": "Command", - "namePlaceholder": "e.g., Restart Nginx", - "descriptionPlaceholder": "Optional description", - "contentPlaceholder": "e.g., sudo systemctl restart nginx", - "nameRequired": "Name is required", - "contentRequired": "Command is required", - "createDescription": "Create a new command snippet for quick execution", - "editDescription": "Edit this command snippet", - "deleteConfirmTitle": "Delete Snippet", - "deleteConfirmDescription": "Are you sure you want to delete \"{{name}}\"?", - "createSuccess": "Snippet created successfully", - "updateSuccess": "Snippet updated successfully", - "deleteSuccess": "Snippet deleted successfully", - "createFailed": "Failed to create snippet", - "updateFailed": "Failed to update snippet", - "deleteFailed": "Failed to delete snippet", - "failedToFetch": "Failed to fetch snippets", - "executeSuccess": "Executing: {{name}}", - "copySuccess": "Copied \"{{name}}\" to clipboard", - "runTooltip": "Execute this snippet in the terminal", - "copyTooltip": "Copy snippet to clipboard", - "editTooltip": "Edit this snippet", - "deleteTooltip": "Delete this snippet", - "newFolder": "New Folder", - "reorderSameFolder": "Can only reorder snippets within the same folder", - "reorderSuccess": "Snippets reordered successfully", - "reorderFailed": "Failed to reorder snippets", - "deleteFolderConfirm": "Delete folder \"{{name}}\"? All snippets will be moved to Uncategorized.", - "deleteFolderSuccess": "Folder deleted successfully", - "deleteFolderFailed": "Failed to delete folder", - "updateFolderSuccess": "Folder updated successfully", - "createFolderSuccess": "Folder created successfully", - "updateFolderFailed": "Failed to update folder", - "createFolderFailed": "Failed to create folder", - "selectTerminals": "Select Terminals (optional)", - "executeOnSelected": "Execute on {{count}} selected terminal(s)", - "executeOnCurrent": "Execute on current terminal (click to select multiple)", - "folder": "Folder", - "selectFolder": "Select a folder or leave empty", - "noFolder": "No folder (Uncategorized)", - "folderName": "Folder Name", - "folderNameRequired": "Folder name is required", - "folderColor": "Folder Color", - "folderIcon": "Folder Icon", - "preview": "Preview", - "updateFolder": "Update Folder", - "createFolder": "Create Folder", - "editFolder": "Edit Folder", - "editFolderDescription": "Customize your snippet folder", - "createFolderDescription": "Organize your snippets into folders" - }, - "commandHistory": { - "title": "History", - "searchPlaceholder": "Search commands...", - "noTerminal": "No active terminal", - "noTerminalHint": "Open a terminal to see its command history.", - "empty": "No command history yet", - "emptyHint": "Execute commands in the active terminal to build its history.", - "noResults": "No commands found", - "noResultsHint": "No commands matching \"{{query}}\"", - "deleteSuccess": "Command deleted from history", - "deleteFailed": "Failed to delete command.", - "deleteTooltip": "Delete command", - "tabHint": "Use Tab in Terminal to autocomplete from command history if enabled in User Profile", - "authRequiredRefresh": "Authentication required. Please refresh the page.", - "dataAccessLockedReauth": "Data access locked. Please re-authenticate.", - "loading": "Loading command history...", - "error": "Error Loading History" - }, - "splitScreen": { - "title": "Split Screen", - "none": "None", - "twoSplit": "2-Way", - "threeSplit": "3-Way", - "fourSplit": "4-Way", - "availableTabs": "Available Tabs", - "dragTabsHint": "Drag tabs to the layout cells below to assign them", - "layout": "Split Screen Layout", - "dropHere": "Drop tab here", - "apply": "Apply Split", - "clear": "Clear Split", - "selectMode": "Select a split screen mode", - "helpText": "Choose how many tabs you want to view at once", - "success": "Split screen applied successfully", - "cleared": "Split screen cleared", - "error": { - "noAssignments": "Please assign at least one tab to the layout", - "fillAllSlots": "Please fill all {{count}} slots before applying" - } - }, - "homepage": { - "loggedInTitle": "Logged in!", - "loggedInMessage": "You are logged in! Use the sidebar to access all available tools. To get started, create an SSH Host in the SSH Manager tab. Once created, you can connect to that host using the other apps in the sidebar.", - "failedToLoadAlerts": "Failed to load alerts", - "failedToDismissAlert": "Failed to dismiss alert" - }, - "serverConfig": { - "title": "Server Configuration", - "description": "Configure the Termix server URL to connect to your backend services", - "serverUrl": "Server URL", - "enterServerUrl": "Please enter a server URL", - "testConnectionFirst": "Please test the connection first", - "connectionSuccess": "Connection successful!", - "connectionFailed": "Connection failed", - "connectionError": "Connection error occurred", - "connected": "Connected", - "disconnected": "Disconnected", - "configSaved": "Configuration saved successfully", - "saveFailed": "Failed to save configuration", - "saveError": "Error saving configuration", - "saving": "Saving...", - "saveConfig": "Save Configuration", - "helpText": "Enter the URL where your Termix server is running (e.g., http://localhost:30001 or https://your-server.com)", - "warning": "Warning", - "notValidatedWarning": "URL not validated - ensure it's correct", - "changeServer": "Change Server", - "mustIncludeProtocol": "Server URL must start with http:// or https://" - }, - "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", - "checkingUpdates": "Checking for updates...", - "refresh": "Refresh", - "updateRequired": "Update Required", - "updateDismissed": "Update notification dismissed", - "noUpdatesFound": "No updates found" - }, - "common": { - "close": "Close", - "minimize": "Minimize", - "online": "Online", - "offline": "Offline", - "continue": "Continue", - "maintenance": "Maintenance", - "degraded": "Degraded", - "discord": "Discord", - "error": "Error", - "warning": "Warning", - "info": "Info", - "success": "Success", - "loading": "Loading...", - "required": "Required", - "optional": "Optional", - "connect": "Connect", - "connecting": "Connecting...", - "creating": "Creating...", - "clear": "Clear", - "toggleSidebar": "Toggle Sidebar", - "sidebar": "Sidebar", - "home": "Home", - "expired": "Expired", - "expiresToday": "Expires today", - "expiresTomorrow": "Expires in {{days}} days", - "updateAvailable": "Update Available", - "sshPath": "SSH Path", - "localPath": "Local Path", - "appName": "Termix", - "openInNewTab": "Open in New Tab", - "resetSidebarWidth": "Reset sidebar width", - "dragToResizeSidebar": "Drag to resize sidebar", - "noAuthCredentials": "No authentication credentials available for this SSH host", - "noReleases": "No Releases", - "updatesAndReleases": "Updates & Releases", - "newVersionAvailable": "A new version ({{version}}) is available.", - "failedToFetchUpdateInfo": "Failed to fetch update information", - "preRelease": "Pre-release", - "loginFailed": "Login failed", - "noReleasesFound": "No releases found.", - "yourBackupCodes": "Your Backup Codes", - "sendResetCode": "Send Reset Code", - "verifyCode": "Verify Code", - "resetPassword": "Reset Password", - "resetCode": "Reset Code", - "newPassword": "New Password", - "folder": "Folder", - "file": "File", - "renamedSuccessfully": "renamed successfully", - "deletedSuccessfully": "deleted successfully", - "noTunnelConnections": "No tunnel connections configured", - "sshTools": "SSH Tools", - "english": "English", - "chinese": "Chinese", - "german": "German", - "cancel": "Cancel", - "username": "Username", - "name": "Name", - "login": "Login", - "logout": "Logout", - "register": "Register", - "password": "Password", - "version": "Version", - "confirmPassword": "Confirm Password", - "back": "Back", - "email": "Email", - "submit": "Submit", - "change": "Change", - "save": "Save", - "saving": "Saving...", - "delete": "Delete", - "edit": "Edit", - "add": "Add", - "search": "Search", - "confirm": "Confirm", - "yes": "Yes", - "no": "No", - "ok": "OK", - "enabled": "Enabled", - "disabled": "Disabled", - "important": "Important", - "notEnabled": "Not Enabled", - "settingUp": "Setting up...", - "next": "Next", - "previous": "Previous", - "refresh": "Refresh", - "settings": "Settings", - "profile": "Profile", - "help": "Help", - "about": "About", - "language": "Language", - "autoDetect": "Auto-detect", - "changeAccountPassword": "Change your account password", - "passwordResetTitle": "Password Reset", - "passwordResetDescription": "You are about to reset your password. This will log you out of all active sessions.", - "enterSixDigitCode": "Enter the 6-digit code from the docker container logs for user:", - "enterNewPassword": "Enter your new password for user:", - "passwordsDoNotMatch": "Passwords do not match", - "passwordMinLength": "Password must be at least 6 characters long", - "passwordResetSuccess": "Password reset successfully! You can now log in with your new password.", - "failedToInitiatePasswordReset": "Failed to initiate password reset", - "failedToVerifyResetCode": "Failed to verify reset code", - "failedToCompletePasswordReset": "Failed to complete password reset", - "documentation": "Documentation", - "retry": "Retry", - "checking": "Checking...", - "checkingDatabase": "Checking database connection...", - "checkingAuthentication": "Checking authentication...", - "actions": "Actions", - "remove": "Remove", - "revoke": "Revoke", - "create": "Create", - "update": "Update" - }, - "nav": { - "home": "Home", - "hosts": "Hosts", - "credentials": "Credentials", - "terminal": "Terminal", - "docker": "Docker", - "tunnels": "Tunnels", - "fileManager": "File Manager", - "serverStats": "Server Stats", - "admin": "Admin", - "userProfile": "User Profile", - "tools": "Tools", - "snippets": "Snippets", - "newTab": "New Tab", - "splitScreen": "Split Screen", - "closeTab": "Close Tab", - "sshManager": "SSH Manager", - "hostManager": "Host Manager", - "cannotSplitTab": "Cannot split this tab", - "tabNavigation": "Tab Navigation", - "hostTabTitle": "{{username}}@{{ip}}:{{port}}", - "copyPassword": "Copy Password", - "copySudoPassword": "Copy Sudo Password", - "passwordCopied": "Password copied to clipboard", - "sudoPasswordCopied": "Sudo password copied to clipboard", - "noPasswordAvailable": "No password available" - }, - "admin": { - "title": "Admin Settings", - "oidc": "OIDC", - "users": "Users", - "userManagement": "User Management", - "makeAdmin": "Make Admin", - "removeAdmin": "Remove Admin", - "deleteUser": "Delete user {{username}}? This cannot be undone.", - "allowRegistration": "Allow Registration", - "oidcSettings": "OIDC Settings", - "clientId": "Client ID", - "clientSecret": "Client Secret", - "issuerUrl": "Issuer URL", - "authorizationUrl": "Authorization URL", - "tokenUrl": "Token URL", - "updateSettings": "Update Settings", - "confirmDelete": "Are you sure you want to delete this user?", - "confirmMakeAdmin": "Are you sure you want to make {{username}} an admin?", - "confirmRemoveAdmin": "Are you sure you want to remove admin status from {{username}}?", - "externalAuthentication": "External Authentication (OIDC)", - "configureExternalProvider": "Configure external identity provider for OIDC/OAuth2 authentication.", - "userIdentifierPath": "User Identifier Path", - "displayNamePath": "Display Name Path", - "scopes": "Scopes", - "saving": "Saving...", - "saveConfiguration": "Save Configuration", - "reset": "Reset", - "success": "Success", - "loading": "Loading...", - "refresh": "Refresh", - "loadingUsers": "Loading users...", - "username": "Username", - "type": "Type", - "actions": "Actions", - "external": "External", - "local": "Local", - "adminManagement": "Admin Management", - "makeUserAdmin": "Make User Admin", - "adding": "Adding...", - "currentAdmins": "Current Admins", - "adminBadge": "Admin", - "removeAdminButton": "Remove Admin", - "general": "General", - "userRegistration": "User Registration", - "allowNewAccountRegistration": "Allow new account registration", - "allowPasswordLogin": "Allow username/password login", - "allowPasswordReset": "Allow password reset via reset code", - "missingRequiredFields": "Missing required fields: {{fields}}", - "oidcConfigurationUpdated": "OIDC configuration updated successfully!", - "failedToFetchOidcConfig": "Failed to fetch OIDC configuration", - "failedToFetchRegistrationStatus": "Failed to fetch registration status", - "failedToFetchPasswordLoginStatus": "Failed to fetch password login status", - "failedToFetchUsers": "Failed to fetch users", - "oidcConfigurationDisabled": "OIDC configuration disabled successfully!", - "failedToUpdateOidcConfig": "Failed to update OIDC configuration", - "failedToDisableOidcConfig": "Failed to disable OIDC configuration", - "enterUsernameToMakeAdmin": "Enter username to make admin", - "userIsNowAdmin": "User {{username}} is now an admin", - "failedToMakeUserAdmin": "Failed to make user admin", - "removeAdminStatus": "Remove admin status from {{username}}?", - "adminStatusRemoved": "Admin status removed from {{username}}", - "failedToRemoveAdminStatus": "Failed to remove admin status", - "userDeletedSuccessfully": "User {{username}} deleted successfully", - "failedToDeleteUser": "Failed to delete user", - "overrideUserInfoUrl": "Override User Info URL (not required)", - "failedToFetchSessions": "Failed to fetch sessions", - "sessionRevokedSuccessfully": "Session revoked successfully", - "failedToRevokeSession": "Failed to revoke session", - "confirmRevokeSession": "Are you sure you want to revoke this session?", - "confirmRevokeAllSessions": "Are you sure you want to revoke all sessions for this user?", - "failedToRevokeSessions": "Failed to revoke sessions", - "sessionsRevokedSuccessfully": "Sessions revoked successfully", - "linkToPasswordAccount": "Link to Password Account", - "linkOIDCDialogTitle": "Link OIDC Account to Password Account", - "linkOIDCDialogDescription": "Link {{username}} (OIDC user) to an existing password account. This will enable dual authentication for the password account.", - "createUser": "Create User", - "createUserDescription": "Create a new local user with username and password", - "enterUsername": "Enter username", - "enterPassword": "Enter password", - "userCreatedSuccessfully": "User {{username}} created successfully", - "failedToCreateUser": "Failed to create user", - "manageUser": "Manage User", - "manageUserDescription": "Manage user settings, roles, and permissions", - "authType": "Authentication Type", - "adminStatus": "Admin Status", - "userId": "User ID", - "regularUser": "Regular User", - "adminPrivileges": "Administrator Privileges", - "administratorRole": "Administrator Role", - "administratorRoleDescription": "Grant full system access and management privileges", - "passwordManagement": "Password Management", - "passwordResetWarning": "Resetting a user's password will delete all their data (SSH hosts, credentials, settings). This action cannot be undone.", - "resetUserPassword": "Reset User Password", - "resettingPassword": "Resetting...", - "passwordResetInitiated": "Password reset initiated for {{username}}. Reset code sent.", - "failedToResetPassword": "Failed to initiate password reset", - "sessionManagement": "Session Management", - "revokeAllSessions": "Revoke All Sessions", - "revokeAllSessionsDescription": "Force logout from all devices and sessions", - "revoking": "Revoking...", - "revoke": "Revoke All", - "dangerZone": "Danger Zone", - "deleteUserTitle": "Delete User Account", - "deleteUserWarning": "Permanently delete this user account and all associated data. This action cannot be undone.", - "deleting": "Deleting...", - "cannotDeleteSelf": "You cannot delete your own account", - "cannotRemoveLastAdmin": "Cannot remove the last administrator", - "cannotRemoveOwnAdmin": "You cannot remove your own admin privileges", - "cannotModifyOwnAdminStatus": "You cannot modify your own admin status", - "dualAuth": "Dual Auth", - "externalOIDC": "External (OIDC)", - "localPassword": "Local Password", - "confirmRevokeOwnSessions": "Are you sure you want to revoke all your own sessions? You will be logged out.", - "linkOIDCWarningTitle": "Warning: OIDC User Data Will Be Deleted", - "linkOIDCActionDeleteUser": "Delete the OIDC user account and all their data", - "linkOIDCActionAddCapability": "Add OIDC login capability to the target password account", - "linkOIDCActionDualAuth": "Allow the password account to login with both password and OIDC", - "linkTargetUsernameLabel": "Target Password Account Username", - "linkTargetUsernamePlaceholder": "Enter username of password account", - "linkAccountsButton": "Link Accounts", - "linkingAccounts": "Linking...", - "accountsLinkedSuccessfully": "OIDC user {{oidcUsername}} has been linked to {{targetUsername}}", - "failedToLinkAccounts": "Failed to link accounts", - "linkTargetUsernameRequired": "Target username is required", - "unlinkOIDCTitle": "Unlink OIDC Authentication", - "unlinkOIDCDescription": "Remove OIDC authentication from {{username}}? The user will only be able to login with username/password after this.", - "unlinkOIDCSuccess": "OIDC unlinked from {{username}}", - "failedToUnlinkOIDC": "Failed to unlink OIDC", - "databaseSecurity": "Database Security", - "encryptionStatus": "Encryption Status", - "encryptionEnabled": "Encryption Enabled", - "enabled": "Enabled", - "disabled": "Disabled", - "keyId": "Key ID", - "created": "Created", - "migrationStatus": "Migration Status", - "migrationCompleted": "Migration completed", - "migrationRequired": "Migration required", - "deviceProtectedMasterKey": "Environment-Protected Master Key", - "legacyKeyStorage": "Legacy Key Storage", - "masterKeyEncryptedWithDeviceFingerprint": "Master key encrypted with environment fingerprint (KEK protection active)", - "keyNotProtectedByDeviceBinding": "Key not protected by environment binding (upgrade recommended)", - "valid": "Valid", - "initializeDatabaseEncryption": "Initialize Database Encryption", - "enableAes256EncryptionWithDeviceBinding": "Enable AES-256 encryption with environment-bound master key protection. This creates enterprise-grade security for SSH keys, passwords, and authentication tokens.", - "featuresEnabled": "Features enabled:", - "aes256GcmAuthenticatedEncryption": "AES-256-GCM authenticated encryption", - "deviceFingerprintMasterKeyProtection": "Environment fingerprint master key protection (KEK)", - "pbkdf2KeyDerivation": "PBKDF2 key derivation with 100K iterations", - "automaticKeyManagement": "Automatic key management and rotation", - "initializing": "Initializing...", - "initializeEnterpriseEncryption": "Initialize Enterprise Encryption", - "migrateExistingData": "Migrate Existing Data", - "encryptExistingUnprotectedData": "Encrypt existing unprotected data in your database. This process is safe and creates automatic backups.", - "testMigrationDryRun": "Verify Encryption Compatibility", - "migrating": "Migrating...", - "migrateData": "Migrate Data", - "securityInformation": "Security Information", - "sshPrivateKeysEncryptedWithAes256": "SSH private keys and passwords are encrypted with AES-256-GCM", - "userAuthTokensProtected": "User authentication tokens and 2FA secrets are protected", - "masterKeysProtectedByDeviceFingerprint": "Master encryption keys are protected by device fingerprint (KEK)", - "keysBoundToServerInstance": "Keys are bound to current server environment (migratable via environment variables)", - "pbkdf2HkdfKeyDerivation": "PBKDF2 + HKDF key derivation with 100K iterations", - "backwardCompatibleMigration": "All data remains backward compatible during migration", - "enterpriseGradeSecurityActive": "Enterprise-Grade Security Active", - "masterKeysProtectedByDeviceBinding": "Your master encryption keys are protected by environment fingerprinting. This uses server hostname, paths, and other environment info to generate protection keys. To migrate servers, set the DB_ENCRYPTION_KEY environment variable on the new server.", - "important": "Important", - "keepEncryptionKeysSecure": "Ensure data security: regularly backup your database files and server configuration. To migrate to a new server, set the DB_ENCRYPTION_KEY environment variable on the new environment, or maintain the same hostname and directory structure.", - "loadingEncryptionStatus": "Loading encryption status...", - "testMigrationDescription": "Verify that existing data can be safely migrated to encrypted format without actually modifying any data", - "serverMigrationGuide": "Server Migration Guide", - "migrationInstructions": "To migrate encrypted data to a new server: 1) Backup database files, 2) Set environment variable DB_ENCRYPTION_KEY=\"your-key\" on new server, 3) Restore database files", - "environmentProtection": "Environment Protection", - "environmentProtectionDesc": "Protects encryption keys based on server environment info (hostname, paths, etc.), migratable via environment variables", - "verificationCompleted": "Compatibility verification completed - no data was changed", - "verificationInProgress": "Verification completed", - "dataMigrationCompleted": "Data migration completed successfully!", - "verificationFailed": "Compatibility verification failed", - "migrationFailed": "Migration failed", - "runningVerification": "Running compatibility verification...", - "startingMigration": "Starting migration...", - "hardwareFingerprintSecurity": "Hardware Fingerprint Security", - "hardwareBoundEncryption": "Hardware-Bound Encryption Active", - "masterKeysNowProtectedByHardwareFingerprint": "Master keys are now protected by real hardware fingerprinting instead of environment variables", - "cpuSerialNumberDetection": "CPU serial number detection", - "motherboardUuidIdentification": "Motherboard UUID identification", - "diskSerialNumberVerification": "Disk serial number verification", - "biosSerialNumberCheck": "BIOS serial number check", - "stableMacAddressFiltering": "Stable MAC address filtering", - "databaseFileEncryption": "Database File Encryption", - "dualLayerProtection": "Dual-Layer Protection Active", - "bothFieldAndFileEncryptionActive": "Both field-level and file-level encryption are now active for maximum security", - "fieldLevelAes256Encryption": "Field-level AES-256 encryption for sensitive data", - "fileLevelDatabaseEncryption": "File-level database encryption with hardware binding", - "hardwareBoundFileKeys": "Hardware-bound file encryption keys", - "automaticEncryptedBackups": "Automatic encrypted backup creation", - "createEncryptedBackup": "Create Encrypted Backup", - "creatingBackup": "Creating Backup...", - "backupCreated": "Backup Created", - "encryptedBackupCreatedSuccessfully": "Encrypted backup created successfully", - "backupCreationFailed": "Backup creation failed", - "databaseMigration": "Database Migration", - "exportForMigration": "Export for Migration", - "exportDatabaseForHardwareMigration": "Export database as SQLite file with decrypted data for migration to new hardware", - "exportDatabase": "Export SQLite Database", - "exporting": "Exporting...", - "exportCreated": "SQLite Export Created", - "exportContainsDecryptedData": "SQLite export contains decrypted data - keep secure!", - "databaseExportedSuccessfully": "SQLite database exported successfully", - "databaseExportFailed": "SQLite database export failed", - "importFromMigration": "Import from Migration", - "importDatabaseFromAnotherSystem": "Import SQLite database from another system or hardware", - "importDatabase": "Import SQLite Database", - "importing": "Importing...", - "selectedFile": "Selected SQLite File", - "importWillReplaceExistingData": "SQLite import will replace existing data - backup recommended!", - "pleaseSelectImportFile": "Please select a SQLite import file", - "databaseImportedSuccessfully": "SQLite database imported successfully", - "databaseImportFailed": "SQLite database import failed", - "manageEncryptionAndBackups": "Manage encryption keys, database security, and backup operations", - "activeSecurityFeatures": "Currently active security measures and protections", - "deviceBindingTechnology": "Advanced hardware-based key protection technology", - "backupAndRecovery": "Secure backup creation and database recovery options", - "crossSystemDataTransfer": "Export and import databases across different systems", - "noMigrationNeeded": "No migration needed", - "encryptionKey": "Encryption Key", - "keyProtection": "Key Protection", - "active": "Active", - "legacy": "Legacy", - "dataStatus": "Data Status", - "encrypted": "Encrypted", - "needsMigration": "Needs Migration", - "ready": "Ready", - "initializeEncryption": "Initialize Encryption", - "initialize": "Initialize", - "test": "Test", - "migrate": "Migrate", - "backup": "Backup", - "createBackup": "Create Backup", - "exportImport": "Export/Import", - "export": "Export", - "import": "Import", - "passwordRequired": "Password required", - "confirmExport": "Confirm Export", - "exportDescription": "Export SSH hosts and credentials as SQLite file", - "importDescription": "Import SQLite file with incremental merge (skips duplicates)", - "criticalWarning": "Critical Warning", - "cannotDisablePasswordLoginWithoutOIDC": "Cannot disable password login without OIDC configured! You must configure OIDC authentication before disabling password login, or you will lose access to Termix.", - "confirmDisablePasswordLogin": "Are you sure you want to disable password login? Make sure OIDC is properly configured and working before proceeding, or you will lose access to your Termix instance.", - "passwordLoginDisabled": "Password login disabled successfully", - "passwordLoginAndRegistrationDisabled": "Password login and new account registration disabled successfully", - "requiresPasswordLogin": "Requires password login enabled", - "passwordLoginDisabledWarning": "Password login is disabled. Ensure OIDC is properly configured or you will not be able to log in to Termix.", - "oidcRequiredWarning": "CRITICAL: Password login is disabled. If you reset or misconfigure OIDC, you will lose all access to Termix and brick your instance. Only proceed if you are absolutely certain.", - "confirmDisableOIDCWarning": "WARNING: You are about to disable OIDC while password login is also disabled. This will brick your Termix instance and you will lose all access. Are you absolutely sure you want to proceed?", - "failedToUpdatePasswordLoginStatus": "Failed to update password login status", - "loadingSessions": "Loading sessions...", - "noActiveSessions": "No active sessions found.", - "device": "Device", - "user": "User", - "lastActive": "Last Active", - "expires": "Expires", - "revoked": "Revoked", - "revokeAllUserSessionsTitle": "Revoke all sessions for this user", - "revokeAll": "Revoke All", - "linkOidcToPasswordAccount": "Link OIDC Account to Password Account", - "linkOidcToPasswordAccountDescription": "Link {{username}} (OIDC user) to an existing password account. This will enable dual authentication for the password account.", - "linkOidcWarningTitle": "Warning: OIDC User Data Will Be Deleted", - "linkOidcWarningDescription": "This action will:", - "linkOidcActionDeleteUser": "Delete the OIDC user account and all their data", - "linkOidcActionAddCapability": "Add OIDC login capability to the target password account", - "linkOidcActionDualAuth": "Allow the password account to login with both password and OIDC", - "passwordMinLength": "Password must be at least 6 characters", - "currentRoles": "Current Roles", - "noRolesAssigned": "No roles assigned", - "assignNewRole": "Assign New Role" - }, - "hosts": { - "title": "Host Manager", - "sshHosts": "SSH Hosts", - "noHosts": "No SSH Hosts", - "noHostsMessage": "You haven't added any SSH hosts yet. Click \"Add Host\" to get started.", - "loadingHosts": "Loading hosts...", - "failedToLoadHosts": "Failed to load hosts", - "retry": "Retry", - "refresh": "Refresh", - "optional": "Optional", - "hostsCount": "{{count}} hosts", - "importJson": "Import JSON", - "importing": "Importing...", - "importJsonTitle": "Import SSH Hosts from JSON", - "importJsonDesc": "Upload a JSON file to bulk import multiple SSH hosts (max 100).", - "downloadSample": "Download Sample", - "formatGuide": "Format Guide", - "exportCredentialWarning": "Warning: Host \"{{name}}\" uses credential authentication. The exported file will not include the credential data and will need to be manually reconfigured after import. Do you want to continue?", - "exportSensitiveDataWarning": "Warning: Host \"{{name}}\" contains sensitive authentication data (password/SSH key). The exported file will include this data in plaintext. Please keep the file secure and delete it after use. Do you want to continue?", - "uncategorized": "Uncategorized", - "confirmDelete": "Are you sure you want to delete \"{{name}}\" ?", - "failedToDeleteHost": "Failed to delete host", - "failedToExportHost": "Failed to export host. Please ensure you're logged in and have access to the host data.", - "jsonMustContainHosts": "JSON must contain a \"hosts\" array or be an array of hosts", - "noHostsInJson": "No hosts found in JSON file", - "maxHostsAllowed": "Maximum 100 hosts allowed per import", - "importCompleted": "Import completed: {{success}} successful, {{failed}} failed", - "importFailed": "Import failed", - "importError": "Import error", - "failedToImportJson": "Failed to import JSON file", - "connectionDetails": "Connection Details", - "organization": "Organization", - "ipAddress": "IP Address", - "port": "Port", - "name": "Name", - "username": "Username", - "folder": "Folder", - "tags": "Tags", - "pin": "Pin", - "notes": "Notes", - "expirationDate": "Expiration Date", - "passwordRequired": "Password is required when using password authentication", - "sshKeyRequired": "SSH Private Key is required when using key authentication", - "keyTypeRequired": "Key Type is required when using key authentication", - "mustSelectValidSshConfig": "Must select a valid SSH configuration from the list", - "addHost": "Add Host", - "editHost": "Edit Host", - "cloneHost": "Clone Host", - "updateHost": "Update Host", - "hostUpdatedSuccessfully": "Host \"{{name}}\" updated successfully!", - "hostAddedSuccessfully": "Host \"{{name}}\" added successfully!", - "hostDeletedSuccessfully": "Host \"{{name}}\" deleted successfully!", - "failedToSaveHost": "Failed to save host. Please try again.", - "savingHost": "Saving host...", - "updatingHost": "Updating host...", - "cloningHost": "Cloning host...", - "enableTerminal": "Enable Terminal", - "enableTerminalDesc": "Enable/disable host visibility in Terminal tab", - "enableTunnel": "Enable Tunnel", - "enableTunnelDesc": "Enable/disable host visibility in Tunnel tab", - "enableFileManager": "Enable File Manager", - "enableFileManagerDesc": "Enable/disable host visibility in File Manager tab", - "enableDockerDesc": "Enable/disable host visibility in Docker tab", - "enableDocker": "Enable Docker", - "defaultPath": "Default Path", - "defaultPathDesc": "Default directory when opening file manager for this host", - "tunnelConnections": "Tunnel Connections", - "connection": "Connection", - "remove": "Remove", - "sourcePort": "Source Port", - "sourcePortDesc": " (Source refers to the Current Connection Details in the General tab)", - "endpointPort": "Endpoint Port", - "endpointSshConfig": "Endpoint SSH Configuration", - "tunnelForwardDescription": "This tunnel will forward traffic from port {{sourcePort}} on the source machine (current connection details in general tab) to port {{endpointPort}} on the endpoint machine.", - "maxRetries": "Max Retries", - "maxRetriesDescription": "Maximum number of retry attempts for tunnel connection.", - "retryInterval": "Retry Interval (seconds)", - "retryIntervalDescription": "Time to wait between retry attempts.", - "autoStartContainer": "Auto Start on Container Launch", - "autoStartDesc": "Automatically start this tunnel when the container launches", - "addConnection": "Add Tunnel Connection", - "tunnelType": "Tunnel Type", - "tunnelTypeLocal": "Local (-L)", - "tunnelTypeRemote": "Remote (-R)", - "tunnelTypeLocalDesc": "Forward local port to remote endpoint", - "tunnelTypeRemoteDesc": "Forward remote port to local machine", - "tunnelForwardDescriptionLocal": "This tunnel will forward traffic from local port {{sourcePort}} to port {{endpointPort}} on the endpoint machine.", - "tunnelForwardDescriptionRemote": "This tunnel will forward traffic from port {{sourcePort}} on the source machine (current connection details in general tab) to port {{endpointPort}} on the endpoint machine.", - "sshpassRequired": "Sshpass Required For Password Authentication", - "sshpassRequiredDesc": "For password authentication in tunnels, sshpass must be installed on the system.", - "otherInstallMethods": "Other installation methods:", - "debianUbuntuEquivalent": "(Debian/Ubuntu) or the equivalent for your OS.", - "or": "or", - "centosRhelFedora": "CentOS/RHEL/Fedora", - "macos": "macOS", - "windows": "Windows (WSL, no official binary)", - "sshServerConfigRequired": "SSH Server Configuration Required", - "sshServerConfigDesc": "For tunnel connections, the SSH server must be configured to allow port forwarding:", - "gatewayPortsYes": "to bind remote ports to all interfaces", - "allowTcpForwardingYes": "to enable port forwarding", - "permitRootLoginYes": "if using root user for tunneling", - "editSshConfig": "Edit the SSH configuration file:", - "sshConfigPath": "/etc/ssh/sshd_config", - "restartSshService": "Then restart the SSH service:", - "restartSshCommand": "sudo systemctl restart sshd", - "upload": "Upload", - "authentication": "Authentication", - "password": "Password", - "key": "Key", - "credential": "Credential", - "none": "None", - "selectCredential": "Select Credential", - "selectCredentialPlaceholder": "Choose a credential...", - "credentialRequired": "Credential is required when using credential authentication", - "credentialDescription": "Selecting a credential will overwrite the current username and use the credential's authentication details.", - "cannotChangeAuthAsSharedUser": "Cannot change authentication as shared user", - "sshPrivateKey": "SSH Private Key", - "keyPassword": "Key Password", - "keyType": "Key Type", - "autoDetect": "Auto-detect", - "rsa": "RSA", - "ed25519": "ED25519", - "ecdsaNistP256": "ECDSA NIST P-256", - "ecdsaNistP384": "ECDSA NIST P-384", - "ecdsaNistP521": "ECDSA NIST P-521", - "dsa": "DSA", - "rsaSha2256": "RSA SHA2-256", - "rsaSha2512": "RSA SHA2-512", - "uploadFile": "Upload File", - "pasteKey": "Paste Key", - "updateKey": "Update Key", - "existingKey": "Existing Key (click to change)", - "existingCredential": "Existing Credential (click to change)", - "addTagsSpaceToAdd": "add tags (space to add)", - "terminalBadge": "Terminal", - "tunnelBadge": "Tunnel", - "fileManagerBadge": "File Manager", - "general": "General", - "terminal": "Terminal", - "tunnel": "Tunnel", - "fileManager": "File Manager", - "serverStats": "Server Stats", - "hostViewer": "Host Viewer", - "enableServerStats": "Enable Server Stats", - "enableServerStatsDesc": "Enable/disable server statistics collection for this host", - "displayItems": "Display Items", - "displayItemsDesc": "Choose which metrics to display on the server stats page", - "enableCpu": "CPU Usage", - "enableMemory": "Memory Usage", - "enableDisk": "Disk Usage", - "enableNetwork": "Network Statistics (Coming Soon)", - "enableProcesses": "Process Count (Coming Soon)", - "enableUptime": "Uptime (Coming Soon)", - "enableHostname": "Hostname (Coming Soon)", - "enableOs": "Operating System (Coming Soon)", - "customCommands": "Custom Commands (Coming Soon)", - "customCommandsDesc": "Define custom shutdown and reboot commands for this server", - "shutdownCommand": "Shutdown Command", - "rebootCommand": "Reboot Command", - "confirmRemoveFromFolder": "Are you sure you want to remove \"{{name}}\" from folder \"{{folder}}\"? The host will be moved to \"No Folder\".", - "removedFromFolder": "Host \"{{name}}\" removed from folder successfully", - "failedToRemoveFromFolder": "Failed to remove host from folder", - "folderRenamed": "Folder \"{{oldName}}\" renamed to \"{{newName}}\" successfully", - "failedToRenameFolder": "Failed to rename folder", - "editFolderAppearance": "Edit Folder Appearance", - "editFolderAppearanceDesc": "Customize the color and icon for folder", - "folderColor": "Folder Color", - "folderIcon": "Folder Icon", - "preview": "Preview", - "folderAppearanceUpdated": "Folder appearance updated successfully", - "failedToUpdateFolderAppearance": "Failed to update folder appearance", - "deleteAllHostsInFolder": "Delete All Hosts in Folder", - "confirmDeleteAllHostsInFolder": "Are you sure you want to delete all {{count}} hosts in folder \"{{folder}}\"? This action cannot be undone.", - "allHostsInFolderDeleted": "Deleted {{count}} hosts from folder \"{{folder}}\" successfully", - "failedToDeleteHostsInFolder": "Failed to delete hosts in folder", - "movedToFolder": "Host \"{{name}}\" moved to \"{{folder}}\" successfully", - "failedToMoveToFolder": "Failed to move host to folder", - "clickToRenameFolder": "Click to rename folder", - "renameFolder": "Rename folder", - "removeFromFolder": "Remove from folder \"{{folder}}\"", - "editHostTooltip": "Edit host", - "deleteHostTooltip": "Delete host", - "exportHostTooltip": "Export host", - "cloneHostTooltip": "Clone host", - "clickToEditHost": "Click to edit host", - "dragToMoveBetweenFolders": "Drag to move between folders", - "exportedHostConfig": "Exported host configuration for {{name}}", - "openTerminal": "Open Terminal", - "openFileManager": "Open File Manager", - "openTunnels": "Open Tunnels", - "openServerDetails": "Open Server Details", - "statistics": "Statistics", - "enabledWidgets": "Enabled Widgets", - "openServerStats": "Open Server Stats", - "enabledWidgetsDesc": "Select which statistics widgets to display for this host", - "monitoringConfiguration": "Monitoring Configuration", - "monitoringConfigurationDesc": "Configure how often server statistics and status are checked", - "statusCheckEnabled": "Enable Status Monitoring", - "statusCheckEnabledDesc": "Check if the server is online or offline", - "statusCheckInterval": "Status Check Interval", - "statusCheckIntervalDesc": "How often to check if host is online (5s - 1h)", - "metricsEnabled": "Enable Metrics Monitoring", - "metricsEnabledDesc": "Collect CPU, RAM, disk, and other system statistics", - "metricsInterval": "Metrics Collection Interval", - "metricsIntervalDesc": "How often to collect server statistics (5s - 1h)", - "intervalSeconds": "seconds", - "intervalMinutes": "minutes", - "intervalValidation": "Monitoring intervals must be between 5 seconds and 1 hour (3600 seconds)", - "monitoringDisabled": "Server monitoring is disabled for this host", - "enableMonitoring": "Enable monitoring in Host Manager → Statistics tab", - "monitoringDisabledBadge": "Monitoring Off", - "statusMonitoring": "Status", - "metricsMonitoring": "Metrics", - "terminalCustomization": "Terminal Customization", - "appearance": "Appearance", - "behavior": "Behavior", - "advanced": "Advanced", - "themePreview": "Theme Preview", - "theme": "Theme", - "selectTheme": "Select theme", - "chooseColorTheme": "Choose a color theme for the terminal", - "fontFamily": "Font Family", - "selectFont": "Select font", - "selectFontDesc": "Select the font to use in the terminal", - "fontSize": "Font Size", - "fontSizeValue": "Font Size: {{value}}px", - "adjustFontSize": "Adjust the terminal font size", - "letterSpacing": "Letter Spacing", - "letterSpacingValue": "Letter Spacing: {{value}}px", - "adjustLetterSpacing": "Adjust spacing between characters", - "lineHeight": "Line Height", - "lineHeightValue": "Line Height: {{value}}", - "adjustLineHeight": "Adjust spacing between lines", - "cursorStyle": "Cursor Style", - "selectCursorStyle": "Select cursor style", - "cursorStyleBlock": "Block", - "cursorStyleUnderline": "Underline", - "cursorStyleBar": "Bar", - "chooseCursorAppearance": "Choose the cursor appearance", - "cursorBlink": "Cursor Blink", - "enableCursorBlink": "Enable cursor blinking animation", - "scrollbackBuffer": "Scrollback Buffer", - "scrollbackBufferValue": "Scrollback Buffer: {{value}} lines", - "scrollbackBufferDesc": "Number of lines to keep in scrollback history", - "bellStyle": "Bell Style", - "selectBellStyle": "Select bell style", - "bellStyleNone": "None", - "bellStyleSound": "Sound", - "bellStyleVisual": "Visual", - "bellStyleBoth": "Both", - "bellStyleDesc": "How to handle terminal bell (BEL character, \\x07). Programs trigger this when completing tasks, encountering errors, or for notifications. \"Sound\" plays an audio beep, \"Visual\" flashes the screen briefly, \"Both\" does both, \"None\" disables bell alerts.", - "rightClickSelectsWord": "Right Click Selects Word", - "rightClickSelectsWordDesc": "Right-clicking selects the word under cursor", - "fastScrollModifier": "Fast Scroll Modifier", - "selectModifier": "Select modifier", - "modifierAlt": "Alt", - "modifierCtrl": "Ctrl", - "modifierShift": "Shift", - "fastScrollModifierDesc": "Modifier key for fast scrolling", - "fastScrollSensitivity": "Fast Scroll Sensitivity", - "fastScrollSensitivityValue": "Fast Scroll Sensitivity: {{value}}", - "fastScrollSensitivityDesc": "Scroll speed multiplier when modifier is held", - "minimumContrastRatio": "Minimum Contrast Ratio", - "minimumContrastRatioValue": "Minimum Contrast Ratio: {{value}}", - "minimumContrastRatioDesc": "Automatically adjust colors for better readability", - "sshAgentForwarding": "SSH Agent Forwarding", - "sshAgentForwardingDesc": "Forward SSH authentication agent to remote host", - "backspaceMode": "Backspace Mode", - "selectBackspaceMode": "Select backspace mode", - "backspaceModeNormal": "Normal (DEL)", - "backspaceModeControlH": "Control-H (^H)", - "backspaceModeDesc": "Backspace key behavior for compatibility", - "startupSnippet": "Startup Snippet", - "selectSnippet": "Select snippet", - "searchSnippets": "Search snippets...", - "snippetNone": "None", - "noneAuthTitle": "Keyboard-Interactive Authentication", - "noneAuthDescription": "This authentication method will use keyboard-interactive authentication when connecting to the SSH server.", - "noneAuthDetails": "Keyboard-interactive authentication allows the server to prompt you for credentials during connection. This is useful for servers that require multi-factor authentication or if you do not want to save credentials locally.", - "forceKeyboardInteractive": "Force Keyboard-Interactive", - "forceKeyboardInteractiveDesc": "Forces the use of keyboard-interactive authentication. This is sometimes required for servers that use Two-Factor Authentication (TOTP/2FA).", - "overrideCredentialUsername": "Override Credential Username", - "overrideCredentialUsernameDesc": "Use a different username than the one stored in the credential. This allows you to use the same credential with different usernames.", - "jumpHosts": "Jump Hosts", - "jumpHostsDescription": "Jump hosts (also known as bastion hosts) allow you to connect to a target server through one or more intermediate servers. This is useful for accessing servers behind firewalls or in private networks.", - "jumpHostChain": "Jump Host Chain", - "addJumpHost": "Add Jump Host", - "selectServer": "Select Server", - "searchServers": "Search servers...", - "noServerFound": "No server found", - "jumpHostsOrder": "Connections will be made in order: Jump Host 1 → Jump Host 2 → ... → Target Server", - "socks5Proxy": "SOCKS5 Proxy", - "socks5Description": "Configure SOCKS5 proxy for SSH connection. All traffic will be routed through the specified proxy server.", - "enableSocks5": "Enable SOCKS5 Proxy", - "enableSocks5Description": "Use SOCKS5 proxy for this SSH connection", - "socks5Host": "Proxy Host", - "socks5Port": "Proxy Port", - "socks5Username": "Proxy Username", - "socks5Password": "Proxy Password", - "socks5UsernameOptional": "Optional: leave empty if proxy doesn't require authentication", - "socks5PasswordOptional": "Optional: leave empty if proxy doesn't require authentication", - "socks5ProxyChain": "Proxy Chain", - "socks5ProxyChainDescription": "Configure a chain of SOCKS proxies. Each proxy in the chain will connect through the previous one.", - "socks5ProxyMode": "Proxy Mode", - "socks5UseSingleProxy": "Use Single Proxy", - "socks5UseProxyChain": "Use Proxy Chain", - "socks5UsePreset": "Use Saved Preset", - "socks5SelectPreset": "Select Preset", - "socks5ManagePresets": "Manage Presets", - "socks5ProxyNode": "Proxy {{number}}", - "socks5AddProxy": "Add Proxy to Chain", - "socks5RemoveProxy": "Remove Proxy", - "socks5ProxyType": "Proxy Type", - "socks5SaveAsPreset": "Save as Preset", - "socks5SavePresetTitle": "Save Proxy Chain as Preset", - "socks5SavePresetDescription": "Save the current proxy chain configuration as a reusable preset", - "socks5PresetName": "Preset Name", - "socks5PresetDescription": "Description (optional)", - "socks5PresetCreated": "Proxy chain preset created", - "socks5PresetUpdated": "Proxy chain preset updated", - "socks5PresetDeleted": "Proxy chain preset deleted", - "socks5PresetSaved": "Preset \"{{name}}\" saved successfully", - "socks5PresetSaveError": "Failed to save preset", - "socks5PresetNameRequired": "Preset name is required", - "socks5EmptyChainError": "Cannot save an empty proxy chain", - "socks5ProxyChainEmpty": "Add at least one proxy to the chain", - "socks5HostDescription": "Hostname or IP address of the SOCKS proxy server", - "socks5PortDescription": "Port number of the SOCKS proxy server (default: 1080)", - "addProxyNode": "Add Proxy Node", - "noProxyNodes": "No proxy nodes configured. Click 'Add Proxy Node' to add one.", - "proxyNode": "Proxy Node", - "proxyType": "Proxy Type", - "quickActions": "Quick Actions", - "quickActionsDescription": "Quick actions allow you to create custom buttons that execute SSH snippets on this server. These buttons will appear at the top of the Server Stats page for quick access.", - "quickActionsList": "Quick Actions List", - "addQuickAction": "Add Quick Action", - "quickActionName": "Action name", - "noSnippetFound": "No snippet found", - "quickActionsOrder": "Quick action buttons will appear in the order listed above on the Server Stats page", - "sidebarCustomization": "Sidebar Button Customization", - "sidebarCustomizationDesc": "Choose which actions appear as quick buttons in the sidebar. Actions not shown as buttons will appear in the dropdown menu.", - "showTerminalInSidebar": "Show Terminal Button", - "showTerminalInSidebarDesc": "Display terminal as a quick button in the sidebar", - "showFileManagerInSidebar": "Show File Manager Button", - "showFileManagerInSidebarDesc": "Display file manager as a quick button in the sidebar", - "showTunnelInSidebar": "Show Tunnel Button", - "showTunnelInSidebarDesc": "Display tunnel management as a quick button in the sidebar", - "showDockerInSidebar": "Show Docker Button", - "showDockerInSidebarDesc": "Display docker management as a quick button in the sidebar", - "showServerStatsInSidebar": "Show Server Stats Button", - "showServerStatsInSidebarDesc": "Display server statistics as a quick button in the sidebar", - "atLeastOneActionRequired": "At least one enabled action must be shown in the sidebar", - "advancedAuthSettings": "Advanced Authentication Settings", - "sudoPasswordAutoFill": "Sudo Password Auto-Fill", - "sudoPasswordAutoFillDesc": "Automatically offer to insert SSH password when sudo prompts for password", - "sudoPassword": "Sudo Password", - "sudoPasswordDesc": "Optional password for sudo commands (useful with key authentication)", - "socks4": "SOCKS4", - "socks5": "SOCKS5", - "executeSnippetOnConnect": "Execute a snippet when the terminal connects", - "autoMosh": "Auto-MOSH", - "autoMoshDesc": "Automatically run MOSH command on connect", - "moshCommand": "MOSH Command", - "moshCommandDesc": "The MOSH command to execute", - "environmentVariables": "Environment Variables", - "environmentVariablesDesc": "Set custom environment variables for the terminal session", - "variableName": "Variable name", - "variableValue": "Value", - "addVariable": "Add Variable", - "docker": "Docker", - "openDocker": "Open Docker", - "copyFullScreenUrl": "Copy Full-Screen URL", - "fullScreenUrlCopied": "Full-screen URL copied to clipboard", - "failedToCopyUrl": "Failed to copy URL to clipboard", - "copyTerminalUrl": "Copy Terminal URL", - "copyFileManagerUrl": "Copy File Manager URL", - "copyTunnelUrl": "Copy Tunnel URL", - "copyServerStatsUrl": "Copy Server Stats URL", - "copyDockerUrl": "Copy Docker URL", - "fullScreenUrlTooltip": "Copy URL to open this app in full-screen mode", - "notEnabled": "Docker is not enabled for this host. Enable it in Host Settings to use Docker features.", - "validating": "Validating Docker...", - "error": "Error", - "errorCode": "Error code: {{code}}", - "version": "Docker v{{version}}", - "current": "Current", - "used_limit": "Used / Limit", - "percentage": "Percentage", - "input": "Input", - "output": "Output", - "read": "Read", - "write": "Write", - "pids": "PIDs", - "id": "ID", - "state": "State", - "console": "Console", - "containerMustBeRunning": "Container must be running to connect to console", - "authenticationRequired": "Authentication required", - "connectedTo": "Connected to {{containerName}}", - "disconnected": "Disconnected", - "consoleError": "Console error", - "errorMessage": "Error: {{message}}", - "failedToConnect": "Failed to connect to console", - "disconnectedFromContainer": "Disconnected from container console.", - "containerNotRunning": "Container is not running", - "startContainerToAccess": "Start the container to access the console", - "selectShell": "Select shell", - "bash": "Bash", - "sh": "Sh", - "ash": "Ash", - "connecting": "Connecting...", - "connect": "Connect", - "disconnect": "Disconnect", - "notConnected": "Not connected", - "clickToConnect": "Click Connect to start an interactive shell", - "connectingTo": "Connecting to {{containerName}}...", - "containerMustBeRunningToViewStats": "Container must be running to view stats", - "failedToFetchStats": "Failed to fetch stats", - "noContainersFound": "No containers found", - "noContainersFoundHint": "Start by creating containers on your server", - "searchPlaceholder": "Search by name, image, or ID...", - "filterByStatusPlaceholder": "Filter by status", - "allContainersCount": "All ({{count}})", - "statusCount": "{{status}} ({{count}})", - "noContainersMatchFilters": "No containers match your filters", - "noContainersMatchFiltersHint": "Try adjusting your search or filter", - "containerStarted": "Container {{name}} started", - "failedToStartContainer": "Failed to start container: {{error}}", - "containerStopped": "Container {{name}} stopped", - "failedToStopContainer": "Failed to stop container: {{error}}", - "containerRestarted": "Container {{name}} restarted", - "failedToRestartContainer": "Failed to restart container: {{error}}", - "containerUnpaused": "Container {{name}} unpaused", - "containerPaused": "Container {{name}} paused", - "failedToTogglePauseContainer": "Failed to {{action}} container: {{error}}", - "containerRemoved": "Container {{name}} removed", - "failedToRemoveContainer": "Failed to remove container: {{error}}", - "image": "Image:", - "idLabel": "ID:", - "ports": "Ports:", - "noPorts": "None", - "created": "Created:", - "start": "Start", - "stop": "Stop", - "unpause": "Unpause", - "pause": "Pause", - "restart": "Restart", - "removeContainer": "Remove Container", - "confirmRemoveContainer": "Are you sure you want to remove container \"{{name}}\"?", - "runningContainerWarning": "Warning: This container is currently running and will be force-removed.", - "removing": "Removing:", - "containerNotFound": "Container not found", - "backToList": "Back to list", - "logs": "Logs", - "stats": "Stats", - "consoleTab": "Console", - "failedToFetchLogs": "Failed to fetch logs: {{error}}", - "failedToDownloadLogs": "Failed to download logs: {{error}}", - "linesToShow": "Lines to show", - "last50Lines": "Last 50 lines", - "last100Lines": "Last 100 lines", - "last500Lines": "Last 500 lines", - "last1000Lines": "Last 1000 lines", - "allLogs": "All logs", - "showTimestamps": "Show Timestamps", - "autoRefresh": "Auto Refresh", - "filterLogsPlaceholder": "Filter logs...", - "noLogsAvailable": "No logs available" - }, - "terminal": { - "title": "Terminal", - "connect": "Connect to Host", - "disconnect": "Disconnect", - "clear": "Clear", - "copy": "Copy", - "paste": "Paste", - "find": "Find", - "fullscreen": "Fullscreen", - "splitHorizontal": "Split Horizontal", - "splitVertical": "Split Vertical", - "closePanel": "Close Panel", - "reconnect": "Reconnect", - "sessionEnded": "Session Ended", - "connectionLost": "Connection Lost", - "error": "ERROR: {{message}}", - "disconnected": "Disconnected", - "connectionClosed": "Connection closed", - "connectionError": "Connection error: {{message}}", - "connected": "Connected", - "sshConnected": "SSH connection established", - "authError": "Authentication failed: {{message}}", - "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", - "connectionTimeout": "Connection timeout", - "terminalTitle": "Terminal - {{host}}", - "terminalWithPath": "Terminal - {{host}}:{{path}}", - "runTitle": "Running {{command}} - {{host}}", - "totpRequired": "Two-Factor Authentication Required", - "totpCodeLabel": "Verification Code", - "totpPlaceholder": "000000", - "totpVerify": "Verify", - "warpgateAuthRequired": "Warpgate Authentication Required", - "warpgateSecurityKey": "Security Key", - "warpgateAuthUrl": "Authentication URL", - "warpgateOpenBrowser": "Open in Browser", - "warpgateContinue": "I've Completed Authentication", - "warpgateTimeout": "Warpgate authentication timeout. Please reconnect.", - "sudoPasswordPopupTitle": "Insert Password?", - "sudoPasswordPopupHint": "Press Enter to insert, Esc to dismiss", - "sudoPasswordPopupConfirm": "Insert", - "sudoPasswordPopupDismiss": "Dismiss", - "websocketAbnormalClose": "Connection closed unexpectedly. This may be due to a reverse proxy or SSL configuration issue. Please check server logs.", - "totpInvalid": "Invalid code. Please try again.", - "authMethodFallback": "{{method}} authentication failed. Trying alternative method...", - "authenticationRequired": "Authentication required", - "connectionLogTitle": "Connection Log", - "connectionLogCopy": "Copy logs to clipboard", - "connectionLogClear": "Clear logs", - "connectionLogEmpty": "No connection logs yet", - "connectionLogConnecting": "Connecting to host...", - "connectionLogCopied": "Connection logs copied to clipboard", - "connectionLogCopyFailed": "Failed to copy logs to clipboard", - "allAuthMethodsFailed": "All authentication methods failed. Please check your credentials.", - "methodNotSupported": "Server does not support {{method}} authentication. Available methods: {{available}}", - "retryWithMethod": "Retry with {{method}}", - "automaticFallback": "Automatically trying {{method}} authentication...", - "totpTimeout": "TOTP verification timeout. Please reconnect.", - "passwordTimeout": "Password verification timeout. Please reconnect.", - "connectionRejected": "Connection rejected by server. Please check your authentication and network configuration." - }, - "fileManager": { - "title": "File Manager", - "file": "File", - "folder": "Folder", - "connectToSsh": "Connect to SSH to use file operations", - "uploadFile": "Upload File", - "downloadFile": "Download", - "extractArchive": "Extract Archive", - "extractingArchive": "Extracting {{name}}...", - "archiveExtractedSuccessfully": "{{name}} extracted successfully", - "extractFailed": "Extract failed", - "compressFile": "Compress File", - "compressFiles": "Compress Files", - "compressFilesDesc": "Compress {{count}} items into an archive", - "archiveName": "Archive Name", - "enterArchiveName": "Enter archive name...", - "compressionFormat": "Compression Format", - "selectedFiles": "Selected files", - "andMoreFiles": "and {{count}} more...", - "compress": "Compress", - "compressingFiles": "Compressing {{count}} items into {{name}}...", - "filesCompressedSuccessfully": "{{name}} created successfully", - "compressFailed": "Compression failed", - "edit": "Edit", - "preview": "Preview", - "previous": "Previous", - "next": "Next", - "pageXOfY": "Page {{current}} of {{total}}", - "zoomOut": "Zoom Out", - "zoomIn": "Zoom In", - "newFile": "New File", - "newFolder": "New Folder", - "rename": "Rename", - "renameItem": "Rename Item", - "deleteItem": "Delete Item", - "currentPath": "Current Path", - "uploadFileTitle": "Upload File", - "maxFileSize": "Max: 1GB (JSON) / 5GB (Binary) - Large files supported", - "removeFile": "Remove File", - "clickToSelectFile": "Click to select a file", - "chooseFile": "Choose File", - "uploading": "Uploading...", - "downloading": "Downloading...", - "uploadingFile": "Uploading {{name}}...", - "uploadingLargeFile": "Uploading large file {{name}} ({{size}})...", - "downloadingFile": "Downloading {{name}}...", - "creatingFile": "Creating {{name}}...", - "creatingFolder": "Creating {{name}}...", - "deletingItem": "Deleting {{type}} {{name}}...", - "renamingItem": "Renaming {{type}} {{oldName}} to {{newName}}...", - "createNewFile": "Create New File", - "fileName": "File Name", - "creating": "Creating...", - "createFile": "Create File", - "createNewFolder": "Create New Folder", - "folderName": "Folder Name", - "createFolder": "Create Folder", - "warningCannotUndo": "Warning: This action cannot be undone", - "itemPath": "Item Path", - "thisIsDirectory": "This is a directory (will delete recursively)", - "deleting": "Deleting...", - "currentPathLabel": "Current Path", - "newName": "New Name", - "thisIsDirectoryRename": "This is a directory", - "renaming": "Renaming...", - "fileUploadedSuccessfully": "File \"{{name}}\" uploaded successfully", - "failedToUploadFile": "Failed to upload file", - "fileDownloadedSuccessfully": "File \"{{name}}\" downloaded successfully", - "failedToDownloadFile": "Failed to download file", - "noFileContent": "No file content received", - "filePath": "File Path", - "fileCreatedSuccessfully": "File \"{{name}}\" created successfully", - "failedToCreateFile": "Failed to create file", - "folderCreatedSuccessfully": "Folder \"{{name}}\" created successfully", - "failedToCreateFolder": "Failed to create folder", - "failedToCreateItem": "Failed to create item", - "operationFailed": "{{operation}} operation failed for {{name}}: {{error}}", - "failedToResolveSymlink": "Failed to resolve symlink", - "itemDeletedSuccessfully": "{{type}} deleted successfully", - "itemsDeletedSuccessfully": "{{count}} items deleted successfully", - "failedToDeleteItems": "Failed to delete items", - "sudoPasswordRequired": "Administrator Password Required", - "enterSudoPassword": "Enter sudo password to continue this operation", - "sudoPassword": "Sudo password", - "sudoOperationFailed": "Sudo operation failed", - "sudoAuthFailed": "Sudo authentication failed", - "deleteOperation": "Delete files/folders", - "dragFilesToUpload": "Drop files here to upload", - "emptyFolder": "This folder is empty", - "itemCount": "{{count}} items", - "selectedCount": "{{count}} selected", - "searchFiles": "Search files...", - "upload": "Upload", - "selectHostToStart": "Select a host to start file management", - "failedToConnect": "Failed to connect to SSH", - "failedToLoadDirectory": "Failed to load directory", - "noSSHConnection": "No SSH connection available", - "enterFolderName": "Enter folder name:", - "enterFileName": "Enter file name:", - "copy": "Copy", - "cut": "Cut", - "paste": "Paste", - "copyPath": "Copy Path", - "copyPaths": "Copy Paths", - "delete": "Delete", - "properties": "Properties", - "refresh": "Refresh", - "downloadFiles": "Download {{count}} files to Browser", - "copyFiles": "Copy {{count}} items", - "cutFiles": "Cut {{count}} items", - "deleteFiles": "Delete {{count}} items", - "filesCopiedToClipboard": "{{count}} items copied to clipboard", - "filesCutToClipboard": "{{count}} items cut to clipboard", - "pathCopiedToClipboard": "Path copied to clipboard", - "pathsCopiedToClipboard": "{{count}} paths copied to clipboard", - "failedToCopyPath": "Failed to copy path to clipboard", - "movedItems": "Moved {{count}} items", - "failedToDeleteItem": "Failed to delete item", - "itemRenamedSuccessfully": "{{type}} renamed successfully", - "failedToRenameItem": "Failed to rename item", - "download": "Download", - "permissions": "Permissions", - "size": "Size", - "modified": "Modified", - "path": "Path", - "confirmDelete": "Are you sure you want to delete {{name}}?", - "uploadSuccess": "File uploaded successfully", - "uploadFailed": "File upload failed", - "downloadSuccess": "File downloaded successfully", - "downloadFailed": "File download failed", - "permissionDenied": "Permission denied", - "checkDockerLogs": "Check the Docker logs for detailed error information", - "internalServerError": "Internal server error occurred", - "serverError": "Server Error", - "error": "Error", - "requestFailed": "Request failed with status code", - "unknownFileError": "unknown", - "cannotReadFile": "Cannot read file", - "noSshSessionId": "No SSH session ID available", - "noFilePath": "No file path available", - "noCurrentHost": "No current host available", - "fileSavedSuccessfully": "File saved successfully", - "saveTimeout": "Save operation timed out. The file may have been saved successfully, but the operation took too long to complete. Check the Docker logs for confirmation.", - "failedToSaveFile": "Failed to save file", - "deletedSuccessfully": "deleted successfully", - "connectToServer": "Connect to a Server", - "selectServerToEdit": "Select a server from the sidebar to start editing files", - "fileOperations": "File Operations", - "confirmDeleteMessage": "Are you sure you want to delete {{name}}?", - "confirmDeleteSingleItem": "Are you sure you want to permanently delete \"{{name}}\"?", - "confirmDeleteMultipleItems": "Are you sure you want to permanently delete {{count}} items?", - "confirmDeleteMultipleItemsWithFolders": "Are you sure you want to permanently delete {{count}} items? This includes folders and their contents.", - "confirmDeleteFolder": "Are you sure you want to permanently delete the folder \"{{name}}\" and all its contents?", - "deleteDirectoryWarning": "This will delete the folder and all its contents.", - "actionCannotBeUndone": "This action cannot be undone.", - "permanentDeleteWarning": "This action cannot be undone. The item(s) will be permanently deleted from the server.", - "recent": "Recent", - "pinned": "Pinned", - "folderShortcuts": "Folder Shortcuts", - "noRecentFiles": "No recent files.", - "noPinnedFiles": "No pinned files.", - "enterFolderPath": "Enter folder path", - "noShortcuts": "No shortcuts.", - "searchFilesAndFolders": "Search files and folders...", - "noFilesOrFoldersFound": "No files or folders found.", - "failedToConnectSSH": "Failed to connect to SSH", - "failedToReconnectSSH": "Failed to reconnect SSH session", - "failedToListFiles": "Failed to list files", - "fetchHomeDataTimeout": "Fetch home data timed out", - "sshStatusCheckTimeout": "SSH status check timed out", - "sshReconnectionTimeout": "SSH reconnection timed out", - "saveOperationTimeout": "Save operation timed out", - "cannotSaveFile": "Cannot save file", - "dragSystemFilesToUpload": "Drag system files here to upload", - "dragFilesToWindowToDownload": "Drag files outside window to download", - "openTerminalHere": "Open Terminal Here", - "run": "Run", - "saveToSystem": "Save as...", - "selectLocationToSave": "Select Location to Save", - "openTerminalInFolder": "Open Terminal in This Folder", - "openTerminalInFileLocation": "Open Terminal at File Location", - "terminalWithPath": "Terminal - {{host}}:{{path}}", - "runningFile": "Running - {{file}}", - "onlyRunExecutableFiles": "Can only run executable files", - "noHostSelected": "No host selected", - "starred": "Starred", - "shortcuts": "Shortcuts", - "directories": "Directories", - "removedFromRecentFiles": "Removed \"{{name}}\" from recent files", - "removeFailed": "Remove failed", - "unpinnedSuccessfully": "Unpinned \"{{name}}\" successfully", - "unpinFailed": "Unpin failed", - "removedShortcut": "Removed shortcut \"{{name}}\"", - "removeShortcutFailed": "Remove shortcut failed", - "clearedAllRecentFiles": "Cleared all recent files", - "clearFailed": "Clear failed", - "removeFromRecentFiles": "Remove from recent files", - "clearAllRecentFiles": "Clear all recent files", - "unpinFile": "Unpin file", - "removeShortcut": "Remove shortcut", - "saveFilesToSystem": "Save {{count}} files as...", - "pinFile": "Pin file", - "addToShortcuts": "Add to shortcuts", - "downloadToDefaultLocation": "Download to default location", - "pasteFailed": "Paste failed", - "noUndoableActions": "No undoable actions", - "undoCopySuccess": "Undid copy operation: Deleted {{count}} copied files", - "undoCopyFailedDelete": "Undo failed: Could not delete any copied files", - "undoCopyFailedNoInfo": "Undo failed: Could not find copied file information", - "undoMoveSuccess": "Undid move operation: Moved {{count}} files back to original location", - "undoMoveFailedMove": "Undo failed: Could not move any files back", - "undoMoveFailedNoInfo": "Undo failed: Could not find moved file information", - "undoDeleteNotSupported": "Delete operation cannot be undone: Files have been permanently deleted from server", - "undoTypeNotSupported": "Unsupported undo operation type", - "undoOperationFailed": "Undo operation failed", - "unknownError": "Unknown error", - "enterPath": "Enter path...", - "editPath": "Edit path", - "confirm": "Confirm", - "cancel": "Cancel", - "find": "Find...", - "replaceWith": "Replace with...", - "replace": "Replace", - "replaceAll": "Replace All", - "downloadInstead": "Download Instead", - "keyboardShortcuts": "Keyboard Shortcuts", - "searchAndReplace": "Search & Replace", - "editing": "Editing", - "navigation": "Navigation", - "code": "Code", - "search": "Search", - "findNext": "Find Next", - "findPrevious": "Find Previous", - "save": "Save", - "selectAll": "Select All", - "undo": "Undo", - "redo": "Redo", - "goToLine": "Go to Line", - "moveLineUp": "Move Line Up", - "moveLineDown": "Move Line Down", - "toggleComment": "Toggle Comment", - "indent": "Indent", - "outdent": "Outdent", - "autoComplete": "Auto Complete", - "imageLoadError": "Failed to load image", - "rotate": "Rotate", - "originalSize": "Original Size", - "startTyping": "Start typing...", - "unknownSize": "Unknown size", - "fileIsEmpty": "File is empty", - "largeFileWarning": "Large File Warning", - "largeFileWarningDesc": "This file is {{size}} in size, which may cause performance issues when opened as text.", - "fileNotFoundAndRemoved": "File \"{{name}}\" not found and has been removed from recent/pinned files", - "failedToLoadFile": "Failed to load file: {{error}}", - "serverErrorOccurred": "Server error occurred. Please try again later.", - "autoSaveFailed": "Auto-save failed", - "fileAutoSaved": "File auto-saved", - "moveFileFailed": "Failed to move {{name}}", - "moveOperationFailed": "Move operation failed", - "canOnlyCompareFiles": "Can only compare two files", - "comparingFiles": "Comparing files: {{file1}} and {{file2}}", - "dragFailed": "Drag operation failed", - "filePinnedSuccessfully": "File \"{{name}}\" pinned successfully", - "pinFileFailed": "Failed to pin file", - "fileUnpinnedSuccessfully": "File \"{{name}}\" unpinned successfully", - "unpinFileFailed": "Failed to unpin file", - "shortcutAddedSuccessfully": "Folder shortcut \"{{name}}\" added successfully", - "addShortcutFailed": "Failed to add shortcut", - "operationCompletedSuccessfully": "{{operation}} {{count}} items successfully", - "operationCompleted": "{{operation}} {{count}} items", - "downloadFileSuccess": "File {{name}} downloaded successfully", - "downloadFileFailed": "Download failed", - "moveTo": "Move to {{name}}", - "diffCompareWith": "Diff compare with {{name}}", - "dragOutsideToDownload": "Drag outside window to download ({{count}} files)", - "newFolderDefault": "NewFolder", - "newFileDefault": "NewFile.txt", - "successfullyMovedItems": "Successfully moved {{count}} items to {{target}}", - "move": "Move", - "searchInFile": "Search in file (Ctrl+F)", - "showKeyboardShortcuts": "Show keyboard shortcuts", - "startWritingMarkdown": "Start writing your markdown content...", - "loadingFileComparison": "Loading file comparison...", - "reload": "Reload", - "compare": "Compare", - "sideBySide": "Side by Side", - "inline": "Inline", - "fileComparison": "File Comparison: {{file1}} vs {{file2}}", - "fileTooLarge": "File too large: {{error}}", - "sshConnectionFailed": "SSH connection failed. Please check your connection to {{name}} ({{ip}}:{{port}})", - "loadFileFailed": "Failed to load file: {{error}}", - "connecting": "Connecting...", - "connectedSuccessfully": "Connected successfully", - "totpVerificationFailed": "TOTP verification failed", - "warpgateVerificationFailed": "Warpgate authentication failed", - "authenticationFailed": "Authentication failed", - "verificationCodePrompt": "Verification code:", - "changePermissions": "Change Permissions", - "changePermissionsDesc": "Modify file permissions for", - "currentPermissions": "Current Permissions", - "newPermissions": "New Permissions", - "owner": "Owner", - "group": "Group", - "others": "Others", - "read": "Read", - "write": "Write", - "execute": "Execute", - "permissionsChangedSuccessfully": "Permissions changed successfully", - "failedToChangePermissions": "Failed to change permissions" - }, - "tunnel": { - "log": { - "connecting": "Establishing tunnel connection...", - "sourceConnecting": "Connecting to source host {{host}}...", - "sourceConnected": "Connected to source host", - "endpointResolving": "Resolving endpoint host {{host}}...", - "endpointResolved": "Endpoint host resolved", - "forwarding": "Setting up port forwarding {{localPort}} → {{remotePort}}...", - "connected": "Tunnel established successfully", - "retrying": "Connection failed, retrying in {{countdown}}s (attempt {{attempt}}/{{max}})...", - "retryExhausted": "Max retry attempts reached", - "error": "Tunnel connection failed: {{error}}" - } - }, - "tunnels": { - "title": "SSH Tunnels", - "noSshTunnels": "No SSH Tunnels", - "createFirstTunnelMessage": "You haven't created any SSH tunnels yet. Configure tunnel connections in the Host Manager to get started.", - "connected": "Connected", - "disconnected": "Disconnected", - "connecting": "Connecting...", - "disconnecting": "Disconnecting...", - "unknownTunnelStatus": "Unknown", - "statusUnknown": "Unknown", - "unknown": "Unknown", - "error": "Error", - "failed": "Failed", - "retrying": "Retrying", - "waiting": "Waiting", - "waitingForRetry": "Waiting for retry", - "retryingConnection": "Retrying connection", - "canceling": "Canceling...", - "connect": "Connect", - "disconnect": "Disconnect", - "cancel": "Cancel", - "port": "Port", - "attempt": "Attempt {{current}} of {{max}}", - "nextRetryIn": "Next retry in {{seconds}} seconds", - "checkDockerLogs": "Check your Docker logs for the error reason, join the", - "orCreate": "or create a ", - "noTunnelConnections": "No tunnel connections configured", - "tunnelConnections": "Tunnel Connections", - "addTunnel": "Add Tunnel", - "editTunnel": "Edit Tunnel", - "deleteTunnel": "Delete Tunnel", - "tunnelName": "Tunnel Name", - "localPort": "Local Port", - "remoteHost": "Remote Host", - "remotePort": "Remote Port", - "autoStart": "Auto Start", - "status": "Status", - "active": "Active", - "inactive": "Inactive", - "start": "Start", - "stop": "Stop", - "restart": "Restart", - "connectionType": "Connection Type", - "local": "Local", - "remote": "Remote", - "dynamic": "Dynamic", - "unknownConnectionStatus": "Unknown", - "portMapping": "Port {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", - "endpointHostNotFound": "Endpoint host not found", - "discord": "Discord", - "githubIssue": "GitHub issue", - "forHelp": "for help" - }, - "serverStats": { - "title": "Server Statistics", - "cpu": "CPU", - "memory": "Memory", - "disk": "Disk", - "network": "Network", - "uptime": "Uptime", - "loadAverage": "Avg: {{avg1}}, {{avg5}}, {{avg15}}", - "processes": "Processes", - "connections": "Connections", - "usage": "Usage", - "available": "Available", - "total": "Total", - "free": "Free", - "used": "Used", - "percentage": "Percentage", - "refreshStatusAndMetrics": "Refresh status and metrics", - "refreshStatus": "Refresh Status", - "fileManagerAlreadyOpen": "File Manager already open for this host", - "openFileManager": "Open File Manager", - "connecting": "Connecting...", - "connectionFailed": "Failed to connect to server", - "cpuCores_one": "{{count}} CPU", - "cpuCores_other": "{{count}} CPUs", - "naCpus": "N/A CPU(s)", - "loadAverageNA": "Avg: N/A", - "cpuUsage": "CPU Usage", - "memoryUsage": "Memory Usage", - "diskUsage": "Disk Usage", - "rootStorageSpace": "Root Storage Space", - "of": "of", - "feedbackMessage": "Have ideas for what should come next for server management? Share them on", - "failedToFetchHostConfig": "Failed to fetch host configuration", - "failedToFetchStatus": "Failed to fetch server status", - "failedToFetchMetrics": "Failed to fetch server metrics", - "failedToFetchHomeData": "Failed to fetch home data", - "loadingMetrics": "Loading metrics...", - "refreshing": "Refreshing...", - "serverOffline": "Server Offline", - "cannotFetchMetrics": "Cannot fetch metrics from offline server", - "totpRequired": "TOTP Authentication Required", - "totpUnavailable": "Server Stats unavailable for TOTP-enabled servers", - "totpVerified": "TOTP verified, metrics collection started", - "totpFailed": "TOTP verification failed", - "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", - "addWidget": "Add Widget", - "saveLayout": "Save Layout", - "unsavedChanges": "Unsaved changes", - "layoutSaved": "Layout saved successfully", - "failedToSaveLayout": "Failed to save layout", - "systemInfo": "System Information", - "hostname": "Hostname", - "operatingSystem": "Operating System", - "kernel": "Kernel", - "totalUptime": "Total Uptime", - "seconds": "seconds", - "networkInterfaces": "Network Interfaces", - "noInterfacesFound": "No network interfaces found", - "totalProcesses": "Total Processes", - "running": "Running", - "noProcessesFound": "No processes found", - "loginStats": "SSH Login Statistics", - "totalLogins": "Total Logins", - "uniqueIPs": "Unique IPs", - "recentSuccessfulLogins": "Recent Successful Logins", - "recentFailedAttempts": "Recent Failed Attempts", - "noRecentLoginData": "No recent login data", - "from": "from", - "quickActions": "Quick Actions", - "executeQuickAction": "Execute {{name}}", - "executingQuickAction": "Executing {{name}}...", - "quickActionSuccess": "{{name}} completed successfully", - "quickActionFailed": "{{name}} failed", - "quickActionError": "Failed to execute {{name}}", - "ports": { - "title": "Listening Ports", - "protocol": "Protocol", - "port": "Port", - "address": "Address", - "state": "State", - "process": "Process", - "noData": "No listening ports data" - }, - "firewall": { - "title": "Firewall", - "active": "Active", - "inactive": "Inactive", - "notDetected": "Not Detected", - "policy": "Policy", - "rules": "rules", - "noRules": "No rules", - "noData": "No firewall data available", - "action": "Action", - "protocol": "Proto", - "port": "Port", - "source": "Source", - "accept": "ACCEPT", - "drop": "DROP", - "reject": "REJECT", - "anywhere": "Anywhere" - } - }, - "auth": { - "tagline": "SSH SERVER MANAGER", - "description": "Secure, powerful, and intuitive SSH connection management", - "welcomeBack": "Welcome back to TERMIX", - "createAccount": "Create your TERMIX account", - "continueExternal": "Continue with external provider", - "loginTitle": "Login to Termix", - "registerTitle": "Create Account", - "loginButton": "Login", - "registerButton": "Register", - "forgotPassword": "Forgot Password?", - "rememberMe": "Remember Me", - "noAccount": "Don't have an account?", - "hasAccount": "Already have an account?", - "loginSuccess": "Login successful", - "loginFailed": "Login failed", - "registerSuccess": "Registration successful", - "registerFailed": "Registration failed", - "logoutSuccess": "Logged out successfully", - "invalidCredentials": "Invalid username or password", - "accountCreated": "Account created successfully", - "passwordReset": "Password reset link sent", - "twoFactorAuth": "Two-Factor Authentication", - "enterCode": "Enter verification code", - "backupCode": "Or use backup code", - "verifyCode": "Verify Code", - "redirectingToApp": "Redirecting to app...", - "enableTwoFactor": "Enable Two-Factor Authentication", - "disableTwoFactor": "Disable Two-Factor Authentication", - "scanQRCode": "Scan this QR code with your authenticator app", - "backupCodes": "Backup Codes", - "saveBackupCodes": "Save these backup codes in a safe place", - "twoFactorEnabledSuccess": "Two-factor authentication enabled successfully!", - "twoFactorDisabled": "Two-factor authentication disabled", - "newBackupCodesGenerated": "New backup codes generated", - "backupCodesDownloaded": "Backup codes downloaded", - "pleaseEnterSixDigitCode": "Please enter a 6-digit code", - "invalidVerificationCode": "Invalid verification code", - "failedToDisableTotp": "Failed to disable TOTP", - "failedToGenerateBackupCodes": "Failed to generate backup codes", - "enterPassword": "Enter your password", - "lockedOidcAuth": "Locked (OIDC Auth)", - "twoFactorTitle": "Two-Factor Authentication", - "twoFactorProtected": "Your account is protected with two-factor authentication", - "twoFactorActive": "Two-factor authentication is currently active on your account", - "disable2FA": "Disable 2FA", - "disableTwoFactorWarning": "Disabling two-factor authentication will make your account less secure", - "passwordOrTotpCode": "Password or TOTP Code", - "or": "Or", - "generateNewBackupCodesText": "Generate new backup codes if you've lost your existing ones", - "generateNewBackupCodes": "Generate New Backup Codes", - "yourBackupCodes": "Your Backup Codes", - "download": "Download", - "setupTwoFactorTitle": "Set Up Two-Factor Authentication", - "sshAuthenticationRequired": "SSH Authentication Required", - "sshNoKeyboardInteractive": "Keyboard-Interactive Authentication Unavailable", - "sshAuthenticationFailed": "Authentication Failed", - "sshAuthenticationTimeout": "Authentication Timeout", - "sshNoKeyboardInteractiveDescription": "The server does not support keyboard-interactive authentication. Please provide your password or SSH key.", - "sshAuthFailedDescription": "The provided credentials were incorrect. Please try again with valid credentials.", - "sshTimeoutDescription": "The authentication attempt timed out. Please try again.", - "sshProvideCredentialsDescription": "Please provide your SSH credentials to connect to this server.", - "sshPasswordDescription": "Enter the password for this SSH connection.", - "sshKeyPasswordDescription": "If your SSH key is encrypted, enter the passphrase here.", - "step1ScanQR": "Step 1: Scan the QR code with your authenticator app", - "manualEntryCode": "Manual Entry Code", - "cannotScanQRText": "If you can't scan the QR code, enter this code manually in your authenticator app", - "nextVerifyCode": "Next: Verify Code", - "verifyAuthenticator": "Verify Your Authenticator", - "step2EnterCode": "Step 2: Enter the 6-digit code from your authenticator app", - "verificationCode": "Verification Code", - "back": "Back", - "verifyAndEnable": "Verify and Enable", - "saveBackupCodesTitle": "Save Your Backup Codes", - "step3StoreCodesSecurely": "Step 3: Store these codes in a safe place", - "importantBackupCodesText": "Save these backup codes in a secure location. You can use them to access your account if you lose your authenticator device.", - "completeSetup": "Complete Setup", - "notEnabledText": "Two-factor authentication adds an extra layer of security by requiring a code from your authenticator app when signing in.", - "enableTwoFactorButton": "Enable Two-Factor Authentication", - "addExtraSecurityLayer": "Add an extra layer of security to your account", - "firstUser": "First User", - "firstUserMessage": "You are the first user and will be made an admin. You can view admin settings in the sidebar user dropdown. If you think this is a mistake, check the docker logs, or create a GitHub issue.", - "external": "External", - "loginWithExternal": "Login with External Provider", - "loginWithExternalDesc": "Login using your configured external identity provider", - "externalNotSupportedInElectron": "External authentication is not supported in the Electron app yet. Please use the web version for OIDC login.", - "resetPasswordButton": "Reset Password", - "sendResetCode": "Send Reset Code", - "resetCodeDesc": "Enter your username to receive a password reset code. The code will be logged in the docker container logs.", - "resetCode": "Reset Code", - "verifyCodeButton": "Verify Code", - "enterResetCode": "Enter the 6-digit code from the docker container logs for user:", - "goToLogin": "Go to Login", - "newPassword": "New Password", - "confirmNewPassword": "Confirm Password", - "enterNewPassword": "Enter your new password for user:", - "signUp": "Sign Up", - "mobileApp": "Mobile App", - "loggingInToMobileApp": "Logging in to the mobile app", - "desktopApp": "Desktop App", - "loggingInToDesktopApp": "Logging in to the desktop app", - "loggingInToDesktopAppViaWeb": "Logging in to the desktop app via web interface", - "loadingServer": "Loading server...", - "authenticating": "Authenticating...", - "dataLossWarning": "Resetting your password this way will delete all your saved SSH hosts, credentials, and other encrypted data. This action cannot be undone. Only use this if you have forgotten your password and are not logged in.", - "authenticationDisabled": "Authentication Disabled", - "authenticationDisabledDesc": "All authentication methods are currently disabled. Please contact your administrator.", - "passwordResetSuccess": "Password Reset Successful", - "passwordResetSuccessDesc": "Your password has been reset successfully. You can now log in with your new password." - }, - "errors": { - "notFound": "Page not found", - "unauthorized": "Unauthorized access", - "forbidden": "Access forbidden", - "serverError": "Server error", - "networkError": "Network error", - "databaseConnection": "Could not connect to the database", - "unknownError": "Unknown error", - "loginFailed": "Login failed", - "failedPasswordReset": "Failed to initiate password reset", - "failedVerifyCode": "Failed to verify reset code", - "failedCompleteReset": "Failed to complete password reset", - "invalidTotpCode": "Invalid TOTP code", - "failedOidcLogin": "Failed to start OIDC login", - "failedUserInfo": "Failed to get user info after OIDC login", - "oidcAuthFailed": "OIDC authentication failed", - "noTokenReceived": "No token received from login", - "invalidAuthUrl": "Invalid authorization URL received from backend", - "invalidInput": "Invalid input", - "requiredField": "This field is required", - "minLength": "Minimum length is {{min}}", - "maxLength": "Maximum length is {{max}}", - "invalidEmail": "Invalid email address", - "passwordMismatch": "Passwords do not match", - "passwordLoginDisabled": "Username/password login is currently disabled", - "weakPassword": "Password is too weak", - "usernameExists": "Username already exists", - "emailExists": "Email already exists", - "loadFailed": "Failed to load data", - "saveError": "Failed to save", - "sessionExpired": "Session expired - please log in again", - "totpRateLimited": "Rate limited: Too many TOTP verification attempts. Please try again later.", - "totpRateLimitedWithTime": "Rate limited: Too many TOTP verification attempts. Please wait {{time}} seconds before trying again.", - "resetCodeRateLimited": "Rate limited: Too many verification attempts. Please try again later.", - "resetCodeRateLimitedWithTime": "Rate limited: Too many verification attempts. Please wait {{time}} seconds before trying again." - }, - "messages": { - "saveSuccess": "Saved successfully", - "saveError": "Failed to save", - "deleteSuccess": "Deleted successfully", - "deleteError": "Failed to delete", - "updateSuccess": "Updated successfully", - "updateError": "Failed to update", - "copySuccess": "Copied to clipboard", - "copyError": "Failed to copy", - "copiedToClipboard": "{{item}} copied to clipboard", - "connectionEstablished": "Connection established", - "connectionClosed": "Connection closed", - "reconnecting": "Reconnecting...", - "processing": "Processing...", - "pleaseWait": "Please wait...", - "registrationDisabled": "New account registration is currently disabled by an admin. Please log in or contact an administrator.", - "databaseConnected": "Database connected successfully", - "databaseConnectionFailed": "Failed to connect to the database server", - "checkServerConnection": "Please check your server connection and try again", - "resetCodeSent": "Reset code sent to Docker logs", - "codeVerified": "Code verified successfully", - "passwordResetSuccess": "Password reset successfully", - "loginSuccess": "Login successful", - "registrationSuccess": "Registration successful" - }, - "profile": { - "title": "User Profile", - "description": "Manage your account settings and security", - "security": "Security", - "changePassword": "Change Password", - "twoFactorAuth": "Two-Factor Authentication", - "accountInfo": "Account Information", - "role": "Role", - "admin": "Administrator", - "user": "User", - "authMethod": "Authentication Method", - "local": "Local", - "external": "External (OIDC)", - "externalAndLocal": "Dual Auth", - "selectPreferredLanguage": "Select your preferred language for the interface", - "fileColorCoding": "File Color Coding", - "fileColorCodingDesc": "Color-code files by type: folders (red), files (blue), symlinks (green)", - "commandAutocomplete": "Command Autocomplete", - "commandAutocompleteDesc": "Enable Tab key autocomplete suggestions for terminal commands based on your command history", - "defaultSnippetFoldersCollapsed": "Collapse Snippet Folders by Default", - "defaultSnippetFoldersCollapsedDesc": "When enabled, all snippet folders will be collapsed when you open the snippets tab", - "terminalSyntaxHighlighting": "Terminal Syntax Highlighting", - "showHostTags": "Show Host Tags", - "showHostTagsDesc": "Display tags under each host in the sidebar. Disable to hide all tags.", - "account": "Account", - "appearance": "Appearance", - "languageLocalization": "Language & Localization", - "fileManagerSettings": "File Manager", - "terminalSettings": "Terminal", - "hostSidebarSettings": "Host & Sidebar", - "snippetsSettings": "Snippets", - "updateSettings": "Updates", - "disableUpdateCheck": "Disable Update Check", - "disableUpdateCheckDesc": "Stop checking for new versions on startup and dashboard. Reduces network requests.", - "currentPassword": "Current Password", - "passwordChangedSuccess": "Password changed successfully! Please log in again.", - "failedToChangePassword": "Failed to change password. Please check your current password and try again.", - "theme": "Theme", - "themeLight": "Light", - "themeDark": "Dark", - "themeSystem": "System", - "appearanceDesc": "Select the color theme for the application", - "terminalSyntaxHighlightingDesc": "Automatically highlight commands, paths, IPs, and log levels in terminal output", - "enableCommandPaletteShortcut": "Enable Command Palette Shortcut", - "enableCommandPaletteShortcutDesc": "Double-tap left Shift to open the Command Palette for quick access to hosts" - }, - "user": { - "failedToLoadVersionInfo": "Failed to load version information" - }, - "placeholders": { - "enterCode": "000000", - "ipAddress": "127.0.0.1", - "port": "22", - "maxRetries": "3", - "retryInterval": "10", - "language": "Language", - "username": "username", - "hostname": "host name", - "folder": "folder", - "password": "password", - "keyPassword": "key password", - "sudoPassword": "sudo password (optional)", - "notes": "add notes about this host...", - "expirationDate": "Select expiration date", - "pastePrivateKey": "Paste your private key here...", - "pastePublicKey": "Paste your public key here...", - "credentialName": "My SSH Server", - "description": "SSH credential description", - "searchCredentials": "Search credentials by name, username, or tags...", - "sshConfig": "endpoint ssh configuration", - "homePath": "/home", - "clientId": "your-client-id", - "clientSecret": "your-client-secret", - "authUrl": "https://your-provider.com/application/o/authorize/", - "redirectUrl": "https://your-provider.com/application/o/termix/", - "tokenUrl": "https://your-provider.com/application/o/token/", - "userIdField": "sub", - "usernameField": "name", - "scopes": "openid email profile", - "userinfoUrl": "https://your-provider.com/application/o/userinfo/", - "enterUsername": "Enter username to make admin", - "searchHosts": "Search hosts by name, username, IP, folder, tags...", - "enterPassword": "Enter your password", - "totpCode": "6-digit TOTP code", - "searchHostsAny": "Search hosts (try: tag:prod, user:root, ip:192.168)...", - "confirmPassword": "Enter your password to confirm", - "typeHere": "Type here", - "fileName": "Enter file name (e.g., example.txt)", - "folderName": "Enter folder name", - "fullPath": "Enter full path to item", - "currentPath": "Enter current path to item", - "newName": "Enter new name", - "socks5Host": "127.0.0.1", - "socks5Username": "proxy username", - "socks5Password": "proxy password", - "socks5PresetName": "e.g., Work VPN Chain", - "socks5PresetDescription": "e.g., Proxy chain for accessing work servers", - "moshCommand": "mosh user@server", - "defaultPort": "22", - "defaultEndpointPort": "224", - "defaultMaxRetries": "3", - "defaultRetryInterval": "10" - }, - "leftSidebar": { - "failedToLoadHosts": "Failed to load hosts", - "noFolder": "No Folder", - "passwordRequired": "Password is required", - "failedToDeleteAccount": "Failed to delete account", - "failedToMakeUserAdmin": "Failed to make user admin", - "userIsNowAdmin": "User {{username}} is now an admin", - "removeAdminConfirm": "Are you sure you want to remove admin status from {{username}}?", - "deleteUserConfirm": "Are you sure you want to delete user {{username}}? This action cannot be undone.", - "deleteAccount": "Delete Account", - "closeDeleteAccount": "Close Delete Account", - "deleteAccountWarning": "This action cannot be undone. This will permanently delete your account and all associated data.", - "deleteAccountWarningDetails": "Deleting your account will remove all your data including SSH hosts, configurations, and settings. This action is irreversible.", - "deleteAccountWarningShort": "This action is not reversible and will permanently delete your account.", - "cannotDeleteAccount": "Cannot Delete Account", - "lastAdminWarning": "You are the last admin user. You cannot delete your account as this would leave the system without any administrators. Please make another user an admin first, or contact system support.", - "confirmPassword": "Confirm Password", - "deleting": "Deleting...", - "cancel": "Cancel" - }, - "interface": { - "sidebar": "Sidebar", - "toggleSidebar": "Toggle Sidebar", - "close": "Close", - "online": "Online", - "offline": "Offline", - "maintenance": "Maintenance", - "degraded": "Degraded", - "noTunnelConnections": "No tunnel connections configured", - "discord": "Discord", - "connectToSshForOperations": "Connect to SSH to use file operations", - "uploadFile": "Upload File", - "newFile": "New File", - "newFolder": "New Folder", - "rename": "Rename", - "deleteItem": "Delete Item", - "createNewFile": "Create New File", - "createNewFolder": "Create New Folder", - "renameItem": "Rename Item", - "clickToSelectFile": "Click to select a file", - "noSshHosts": "No SSH Hosts", - "sshHosts": "SSH Hosts", - "importSshHosts": "Import SSH Hosts from JSON", - "clientId": "Client ID", - "clientSecret": "Client Secret", - "error": "Error", - "warning": "Warning", - "deleteAccount": "Delete Account", - "closeDeleteAccount": "Close Delete Account", - "cannotDeleteAccount": "Cannot Delete Account", - "confirmPassword": "Confirm Password", - "deleting": "Deleting...", - "externalAuth": "External Authentication (OIDC)", - "configureExternalProvider": "Configure external identity provider for", - "waitingForRetry": "Waiting for retry", - "retryingConnection": "Retrying connection", - "resetSplitSizes": "Reset split sizes", - "sshManagerAlreadyOpen": "SSH Manager already open", - "disabledDuringSplitScreen": "Disabled during split screen", - "unknown": "Unknown", - "connected": "Connected", - "disconnected": "Disconnected", - "maxRetriesExhausted": "Max retries exhausted", - "endpointHostNotFound": "Endpoint host not found", - "administrator": "Administrator", - "user": "User", - "external": "External", - "local": "Local", - "saving": "Saving...", - "saveConfiguration": "Save Configuration", - "loading": "Loading...", - "refresh": "Refresh", - "adding": "Adding...", - "makeAdmin": "Make Admin", - "verifying": "Verifying...", - "verifyAndEnable": "Verify and Enable", - "secretKey": "Secret key", - "totpQrCode": "TOTP QR Code", - "passwordRequired": "Password is required when using password authentication", - "sshKeyRequired": "SSH Private Key is required when using key authentication", - "keyTypeRequired": "Key Type is required when using key authentication", - "validSshConfigRequired": "Must select a valid SSH configuration from the list", - "updateHost": "Update Host", - "addHost": "Add Host", - "editHost": "Edit Host", - "pinConnection": "Pin Connection", - "authentication": "Authentication", - "password": "Password", - "key": "Key", - "sshPrivateKey": "SSH Private Key", - "keyPassword": "Key Password", - "keyType": "Key Type", - "enableTerminal": "Enable Terminal", - "enableTunnel": "Enable Tunnel", - "enableFileManager": "Enable File Manager", - "defaultPath": "Default Path", - "tunnelConnections": "Tunnel Connections", - "maxRetries": "Max Retries", - "upload": "Upload", - "updateKey": "Update Key", - "productionFolder": "Production", - "databaseServer": "Database Server", - "developmentServer": "Development Server", - "developmentFolder": "Development", - "webServerProduction": "Web Server - Production", - "unknownError": "Unknown error", - "failedToInitiatePasswordReset": "Failed to initiate password reset", - "failedToVerifyResetCode": "Failed to verify reset code", - "failedToCompletePasswordReset": "Failed to complete password reset", - "invalidTotpCode": "Invalid TOTP code", - "failedToStartOidcLogin": "Failed to start OIDC login", - "failedToGetUserInfoAfterOidc": "Failed to get user info after OIDC login", - "loginWithExternalProvider": "Login with external provider", - "loginWithExternal": "Login with External Provider", - "sendResetCode": "Send Reset Code", - "verifyCode": "Verify Code", - "resetPassword": "Reset Password", - "login": "Login", - "signUp": "Sign Up", - "failedToUpdateOidcConfig": "Failed to update OIDC configuration", - "failedToMakeUserAdmin": "Failed to make user admin", - "failedToStartTotpSetup": "Failed to start TOTP setup", - "invalidVerificationCode": "Invalid verification code", - "failedToDisableTotp": "Failed to disable TOTP", - "failedToGenerateBackupCodes": "Failed to generate backup codes" - }, - "mobile": { - "selectHostToStart": "Select a host to start your terminal session", - "limitedSupportMessage": "Website mobile support is still in progress. Use the mobile app for a better experience.", - "mobileAppInProgress": "Mobile app is in progress", - "mobileAppInProgressDesc": "We're working on a dedicated mobile app to provide a better experience on mobile devices.", - "viewMobileAppDocs": "Install Mobile App", - "mobileAppDocumentation": "Mobile App Documentation" - }, - "dashboard": { - "title": "Dashboard", - "loading": "Loading dashboard...", - "github": "GitHub", - "support": "Support", - "discord": "Discord", - "donate": "Donate", - "serverOverview": "Server Overview", - "version": "Version", - "upToDate": "Up to Date", - "updateAvailable": "Update Available", - "uptime": "Uptime", - "database": "Database", - "healthy": "Healthy", - "error": "Error", - "totalServers": "Total Servers", - "totalTunnels": "Total Tunnels", - "totalCredentials": "Total Credentials", - "recentActivity": "Recent Activity", - "reset": "Reset", - "loadingRecentActivity": "Loading recent activity...", - "noRecentActivity": "No recent activity", - "quickActions": "Quick Actions", - "addHost": "Add Host", - "addCredential": "Add Credential", - "adminSettings": "Admin Settings", - "userProfile": "User Profile", - "serverStats": "Server Stats", - "loadingServerStats": "Loading server stats...", - "noServerData": "No server data available", - "cpu": "CPU", - "ram": "RAM", - "notAvailable": "N/A", - "customizeLayout": "Customize Dashboard", - "dashboardSettings": "Dashboard Settings", - "enableDisableCards": "Enable/Disable Cards", - "resetLayout": "Reset to Default", - "serverOverviewCard": "Server Overview", - "recentActivityCard": "Recent Activity", - "networkGraphCard": "Network Graph", - "networkGraph": "Network Graph", - "quickActionsCard": "Quick Actions", - "serverStatsCard": "Server Stats" - }, - "networkGraph": { - "title": "Network Graph", - "addHost": "Add Host", - "addGroup": "Add Group", - "addLink": "Add Link", - "deleteSelected": "Delete Selected", - "zoomIn": "Zoom In", - "zoomOut": "Zoom Out", - "resetView": "Reset View", - "exportJson": "Export JSON", - "importJson": "Import JSON", - "selectHost": "Select Host", - "chooseHost": "Choose a host...", - "parentGroup": "Parent Group", - "noGroup": "No Group", - "groupName": "Group Name", - "color": "Color", - "source": "Source", - "target": "Target", - "moveToGroup": "Move to Group", - "selectGroup": "Select group...", - "addConnection": "Add Connection", - "hostDetails": "Host Details", - "connectToHost": "Connect to Host", - "openTerminal": "Open Terminal", - "openFileManager": "Open File Manager", - "openTunnel": "Open Tunnels", - "openDocker": "Open Docker", - "openServerStats": "Open Server Details", - "moveToGroupAction": "Move to Group", - "removeFromGroup": "Remove from Group", - "addHostHere": "Add Host Here", - "editGroup": "Edit Group", - "delete": "Delete", - "cancel": "Cancel", - "add": "Add", - "create": "Create", - "update": "Update", - "move": "Move", - "connect": "Connect", - "close": "Close", - "createGroup": "Create Group", - "noAvailableHosts": "No available hosts", - "selectSourcePlaceholder": "Select Source...", - "selectTargetPlaceholder": "Select Target...", - "invalidFile": "Invalid File", - "hostAlreadyExists": "Host is already in the topology", - "sourceTargetMustDiffer": "Source and target must be different", - "connectionExists": "Connection already exists", - "unknown": "Unknown", - "name": "Name", - "ip": "IP", - "status": "Status", - "id": "ID", - "failedToLoadData": "Failed to load network topology data", - "failedToAddNode": "Failed to add node", - "sourceDifferentFromTarget": "Source and target must be different", - "exportJSON": "Export JSON", - "importJSON": "Import JSON", - "searchHost": "Search hosts...", - "noHostFound": "No hosts found", - "searchGroup": "Search groups...", - "noGroupFound": "No groups found", - "searchNode": "Search nodes...", - "noNodeFound": "No nodes found", - "terminal": "Terminal", - "fileManager": "File Manager", - "tunnel": "Tunnel", - "docker": "Docker", - "serverStats": "Server Stats" - }, - "rbac": { - "shareHost": "Share Host", - "shareHostTitle": "Share Host Access", - "shareHostDescription": "Grant temporary or permanent access to this host", - "targetUser": "Target User", - "selectUser": "Select a user to share with", - "duration": "Duration", - "durationHours": "Duration (hours)", - "neverExpires": "Never expires", - "permissionLevel": "Permission Level", - "permissionLevels": { - "readonly": "Read-Only", - "readonlyDesc": "Can view only, no command input", - "restricted": "Restricted", - "restrictedDesc": "Blocks dangerous commands (passwd, rm -rf, etc.)", - "monitored": "Monitored", - "monitoredDesc": "Records all commands but doesn't block (Recommended)", - "full": "Full Access", - "fullDesc": "No restrictions (Not recommended)" - }, - "blockedCommands": "Blocked Commands", - "blockedCommandsPlaceholder": "Enter commands to block, e.g., passwd, rm, dd", - "maxSessionDuration": "Max Session Duration (minutes)", - "createTempUser": "Create Temporary User", - "createTempUserDesc": "Creates a restricted user on the server instead of sharing your credentials. Requires sudo access. Most secure option.", - "expiresAt": "Expires At", - "expiresIn": "Expires in {{hours}} hours", - "expired": "Expired", - "grantedBy": "Granted By", - "accessLevel": "Access Level", - "lastAccessed": "Last Accessed", - "accessCount": "Access Count", - "revokeAccess": "Revoke Access", - "confirmRevokeAccess": "Are you sure you want to revoke access for {{username}}?", - "hostSharedSuccessfully": "Host shared successfully with {{username}}", - "hostAccessUpdated": "Host access updated", - "failedToShareHost": "Failed to share host", - "accessRevokedSuccessfully": "Access revoked successfully", - "failedToRevokeAccess": "Failed to revoke access", - "shared": "Shared", - "sharedHosts": "Shared Hosts", - "sharedWithMe": "Shared With Me", - "noSharedHosts": "No hosts shared with you", - "owner": "Owner", - "viewAccessList": "View Access List", - "accessList": "Access List", - "noAccessGranted": "No access has been granted for this host", - "noAccessGrantedMessage": "No users have been granted access to this host yet", - "manageAccessFor": "Manage access for", - "totalAccessRecords": "{{count}} access record(s)", - "neverAccessed": "Never", - "timesAccessed": "{{count}} time(s)", - "daysRemaining": "{{days}} day(s)", - "hoursRemaining": "{{hours}} hour(s)", - "failedToFetchAccessList": "Failed to fetch access list", - "currentAccess": "Current Access", - "securityWarning": "Security Warning", - "securityWarningMessage": "Sharing credentials gives the user full access to perform any operations on the server, including changing passwords and deleting files. Only share with trusted users.", - "tempUserRecommended": "We recommend enabling 'Create Temporary User' for better security.", - "roleManagement": "Role Management", - "manageRoles": "Manage Roles", - "manageRolesFor": "Manage roles for {{username}}", - "assignRole": "Assign Role", - "removeRole": "Remove Role", - "userRoles": "User Roles", - "permissions": "Permissions", - "systemRole": "System Role", - "customRole": "Custom Role", - "roleAssignedSuccessfully": "Role assigned to {{username}} successfully", - "failedToAssignRole": "Failed to assign role", - "roleRemovedSuccessfully": "Role removed from {{username}} successfully", - "failedToRemoveRole": "Failed to remove role", - "cannotRemoveSystemRole": "Cannot remove system role", - "cannotShareWithSelf": "Cannot share host with yourself", - "noCustomRolesToAssign": "No custom roles available. System roles are auto-assigned.", - "credentialSharingWarning": "Credential Authentication Not Supported for Sharing", - "credentialRequired": "Credential is required when sharing a host", - "credentialRequiredDescription": "This host does not use credential-based authentication. In order to share hosts, due to per-user-encryption, the host must use credential based authentication.", - "auditLogs": "Audit Logs", - "viewAuditLogs": "View Audit Logs", - "action": "Action", - "resourceType": "Resource Type", - "resourceName": "Resource Name", - "timestamp": "Timestamp", - "ipAddress": "IP Address", - "userAgent": "User Agent", - "success": "Success", - "failed": "Failed", - "details": "Details", - "noAuditLogs": "No audit logs available", - "sessionRecordings": "Session Recordings", - "viewRecording": "View Recording", - "downloadRecording": "Download Recording", - "dangerousCommand": "Dangerous Command Detected", - "commandBlocked": "Command Blocked", - "terminateSession": "Terminate Session", - "sessionTerminated": "Session terminated by host owner", - "sharedAccessExpired": "Your shared access to this host has expired", - "sharedAccessExpiresIn": "Shared access expires in {{hours}} hours", - "roles": { - "label": "Roles", - "admin": "Administrator", - "user": "User" - }, - "createRole": "Create Role", - "editRole": "Edit Role", - "roleName": "Role Name", - "displayName": "Display Name", - "description": "Description", - "assignRoles": "Assign Roles", - "userRoleAssignment": "User-Role Assignment", - "selectUserPlaceholder": "Select a user", - "searchUsers": "Search users...", - "noUserFound": "No user found", - "currentRoles": "Current Roles", - "noRolesAssigned": "No roles assigned", - "assignNewRole": "Assign New Role", - "selectRolePlaceholder": "Select a role", - "searchRoles": "Search roles...", - "noRoleFound": "No role found", - "assign": "Assign", - "roleCreatedSuccessfully": "Role created successfully", - "roleUpdatedSuccessfully": "Role updated successfully", - "roleDeletedSuccessfully": "Role deleted successfully", - "failedToLoadRoles": "Failed to load roles", - "failedToSaveRole": "Failed to save role", - "failedToDeleteRole": "Failed to delete role", - "roleDisplayNameRequired": "Role display name is required", - "roleNameRequired": "Role name is required", - "roleNameHint": "Use lowercase letters, numbers, underscores, and hyphens only", - "displayNamePlaceholder": "Developer", - "descriptionPlaceholder": "Software developers and engineers", - "confirmDeleteRole": "Delete Role", - "confirmDeleteRoleDescription": "Are you sure you want to delete the role '{{name}}'? This action cannot be undone.", - "confirmRemoveRole": "Remove Role", - "confirmRemoveRoleDescription": "Are you sure you want to remove this role from the user?", - "editRoleDescription": "Update role information", - "createRoleDescription": "Create a new custom role for grouping users", - "assignRolesDescription": "Manage role assignments for users", - "noRoles": "No roles found", - "selectRole": "Select Role", - "type": "Type", - "user": "User", - "role": "Role", - "saveHostFirst": "Save Host First", - "saveHostFirstDescription": "Please save the host before configuring sharing settings.", - "shareWithUser": "Share with User", - "shareWithRole": "Share with Role", - "share": "Share", - "target": "Target", - "expires": "Expires", - "never": "Never", - "noAccessRecords": "No access records found", - "sharedSuccessfully": "Shared successfully", - "failedToShare": "Failed to share", - "confirmRevokeAccessDescription": "Are you sure you want to revoke this access?", - "hours": "hours", - "sharing": "Sharing", - "selectUserAndRole": "Please select both a user and a role", - "view": "View Only", - "viewDesc": "Due to the Termix encryption system, other permission levels will come at a later date" - }, - "commandPalette": { - "searchPlaceholder": "Search for hosts or quick actions...", - "recentActivity": "Recent Activity", - "navigation": "Navigation", - "addHost": "Add Host", - "addCredential": "Add Credential", - "adminSettings": "Admin Settings", - "userProfile": "User Profile", - "updateLog": "Update Log", - "hosts": "Hosts", - "openServerDetails": "Open Server Details", - "openFileManager": "Open File Manager", - "edit": "Edit", - "links": "Links", - "github": "GitHub", - "support": "Support", - "discord": "Discord", - "donate": "Donate", - "press": "Press", - "toToggle": "to toggle", - "close": "Close", - "hostManager": "Host Manager", - "pressToToggle": "Press Left Shift twice to open the command palette" - }, - "docker": { - "notEnabled": "Docker is not enabled for this host", - "validating": "Validating Docker...", - "connectingToHost": "Connecting to host...", - "connecting": "Connecting...", - "error": "Error", - "errorCode": "Error code: {{code}}", - "version": "Docker {{version}}", - "connectionFailed": "Failed to connect to Docker", - "totpVerificationFailed": "TOTP verification failed. Please try again.", - "warpgateVerificationFailed": "Warpgate authentication failed. Please try again.", - "verificationCodePrompt": "Enter verification code", - "loadingContainers": "Loading containers...", - "containerStarted": "Container {{name}} started", - "failedToStartContainer": "Failed to start container {{name}}", - "containerStopped": "Container {{name}} stopped", - "failedToStopContainer": "Failed to stop container {{name}}", - "containerRestarted": "Container {{name}} restarted", - "failedToRestartContainer": "Failed to restart container {{name}}", - "containerPaused": "Container {{name}} paused", - "containerUnpaused": "Container {{name}} unpaused", - "failedToTogglePauseContainer": "Failed to toggle pause state for container {{name}}", - "containerRemoved": "Container {{name}} removed", - "failedToRemoveContainer": "Failed to remove container {{name}}", - "image": "Image", - "idLabel": "ID", - "ports": "Ports", - "noPorts": "No ports", - "created": "Created", - "start": "Start", - "stop": "Stop", - "pause": "Pause", - "unpause": "Unpause", - "restart": "Restart", - "remove": "Remove", - "removeContainer": "Remove Container", - "confirmRemoveContainer": "Are you sure you want to remove the container '{{name}}'? This action cannot be undone.", - "runningContainerWarning": "Warning: This container is currently running. Removing it will stop the container first.", - "removing": "Removing...", - "noContainersFound": "No containers found", - "noContainersFoundHint": "No Docker containers are available on this host", - "searchPlaceholder": "Search containers...", - "filterByStatusPlaceholder": "Filter by status", - "allContainersCount": "All ({{count}})", - "statusCount": "{{status}} ({{count}})", - "noContainersMatchFilters": "No containers match your filters", - "noContainersMatchFiltersHint": "Try adjusting your search or filter criteria", - "containerMustBeRunningToViewStats": "Container must be running to view statistics", - "failedToFetchStats": "Failed to fetch container statistics", - "containerNotRunning": "Container not running", - "startContainerToViewStats": "Start the container to view statistics", - "loadingStats": "Loading statistics...", - "errorLoadingStats": "Error loading statistics", - "noStatsAvailable": "No statistics available", - "cpuUsage": "CPU Usage", - "current": "Current", - "memoryUsage": "Memory Usage", - "usedLimit": "Used / Limit", - "percentage": "Percentage", - "networkIo": "Network I/O", - "input": "Input", - "output": "Output", - "blockIo": "Block I/O", - "read": "Read", - "write": "Write", - "pids": "PIDs", - "containerInformation": "Container Information", - "name": "Name", - "id": "ID", - "state": "State", - "disconnectedFromContainer": "Disconnected from container", - "containerMustBeRunning": "Container must be running to access console", - "authenticationRequired": "Authentication required", - "connectedTo": "Connected to {{containerName}}", - "disconnected": "Disconnected", - "consoleError": "Console error", - "errorMessage": "Error: {{message}}", - "failedToConnect": "Failed to connect to container", - "console": "Console", - "selectShell": "Select shell", - "bash": "Bash", - "sh": "sh", - "ash": "ash", - "connect": "Connect", - "disconnect": "Disconnect", - "notConnected": "Not connected", - "clickToConnect": "Click connect to start a shell session", - "connectingTo": "Connecting to {{containerName}}...", - "containerNotFound": "Container not found", - "backToList": "Back to List", - "logs": "Logs", - "stats": "Stats", - "consoleTab": "Console", - "startContainerToAccess": "Start the container to access the console", - "log": { - "connecting": "Connecting to Docker host...", - "authStarted": "Resolving authentication credentials...", - "totpRequired": "TOTP verification required", - "warpgateRequired": "Warpgate authentication required", - "sessionCreated": "Docker SSH session established", - "sessionReady": "Ready for Docker operations", - "error": "Docker connection failed: {{error}}" - } - }, - "stats": { - "log": { - "connecting": "Starting metrics collection...", - "existingSession": "Using existing metrics session", - "totpVerifying": "TOTP verification required", - "sessionEstablished": "Metrics session established", - "collecting": "Collecting server metrics", - "heartbeat": "Keeping connection alive", - "stopped": "Metrics collection stopped", - "error": "Metrics collection failed: {{error}}" - } - }, - "sftp": { - "log": { - "connecting": "Connecting to SFTP server...", - "jumpHost": "Connecting via jump host {{host}}...", - "connected": "SFTP connection established", - "authFailed": "Authentication failed", - "error": "SFTP operation failed: {{error}}" - } - }, - "theme": { - "switchToLight": "Switch to Light", - "switchToDark": "Switch to Dark" - } -} diff --git a/src/locales/translated/es.json b/src/locales/translated/es_ES.json similarity index 100% rename from src/locales/translated/es.json rename to src/locales/translated/es_ES.json diff --git a/src/locales/translated/fi.json b/src/locales/translated/fi_FI.json similarity index 100% rename from src/locales/translated/fi.json rename to src/locales/translated/fi_FI.json diff --git a/src/locales/translated/fr.json b/src/locales/translated/fr_FR.json similarity index 100% rename from src/locales/translated/fr.json rename to src/locales/translated/fr_FR.json diff --git a/src/locales/translated/he.json b/src/locales/translated/he_IL.json similarity index 100% rename from src/locales/translated/he.json rename to src/locales/translated/he_IL.json diff --git a/src/locales/translated/hi.json b/src/locales/translated/hi_IN.json similarity index 100% rename from src/locales/translated/hi.json rename to src/locales/translated/hi_IN.json diff --git a/src/locales/translated/hu.json b/src/locales/translated/hu_HU.json similarity index 100% rename from src/locales/translated/hu.json rename to src/locales/translated/hu_HU.json diff --git a/src/locales/translated/id.json b/src/locales/translated/id_ID.json similarity index 100% rename from src/locales/translated/id.json rename to src/locales/translated/id_ID.json diff --git a/src/locales/translated/it.json b/src/locales/translated/it_IT.json similarity index 100% rename from src/locales/translated/it.json rename to src/locales/translated/it_IT.json diff --git a/src/locales/translated/ja.json b/src/locales/translated/ja_JP.json similarity index 100% rename from src/locales/translated/ja.json rename to src/locales/translated/ja_JP.json diff --git a/src/locales/translated/ko.json b/src/locales/translated/ko_KR.json similarity index 100% rename from src/locales/translated/ko.json rename to src/locales/translated/ko_KR.json diff --git a/src/locales/translated/nl.json b/src/locales/translated/nl_NL.json similarity index 100% rename from src/locales/translated/nl.json rename to src/locales/translated/nl_NL.json diff --git a/src/locales/translated/no.json b/src/locales/translated/no_NO.json similarity index 100% rename from src/locales/translated/no.json rename to src/locales/translated/no_NO.json diff --git a/src/locales/translated/pl.json b/src/locales/translated/pl_PL.json similarity index 100% rename from src/locales/translated/pl.json rename to src/locales/translated/pl_PL.json diff --git a/src/locales/translated/pt.json b/src/locales/translated/pt_BR.json similarity index 100% rename from src/locales/translated/pt.json rename to src/locales/translated/pt_BR.json diff --git a/src/locales/translated/pt_PT.json b/src/locales/translated/pt_PT.json new file mode 100644 index 00000000..2da1d18b --- /dev/null +++ b/src/locales/translated/pt_PT.json @@ -0,0 +1,2665 @@ +{ + "credentials": { + "credentialsViewer": "Visualizador de credenciais", + "manageYourSSHCredentials": "Gerencie suas credenciais SSH de forma segura", + "addCredential": "Adicionar Credencial", + "createCredential": "Criar Credencial", + "editCredential": "Editar Credencial", + "viewCredential": "Ver Credencial", + "duplicateCredential": "Duplicar Credencial", + "deleteCredential": "Excluir Credencial", + "updateCredential": "Atualizar Credencial", + "credentialName": "Nome de Credencial", + "credentialDescription": "Descrição:", + "username": "Usuário:", + "searchCredentials": "Pesquisar credenciais...", + "selectFolder": "Selecionar pasta", + "selectAuthType": "Selecionar Tipo de Autenticação", + "allFolders": "Todas as pastas", + "allAuthTypes": "Todos os Tipos de Autenticação", + "uncategorized": "Descategorizado", + "totalCredentials": "Total:", + "keyBased": "Baseado em chave", + "passwordBased": "Baseada em senha", + "folders": "Pastas", + "noCredentialsMatchFilters": "Nenhuma credencial corresponde aos seus filtros", + "noCredentialsYet": "Nenhuma credencial criada ainda", + "createFirstCredential": "Crie sua primeira credencial", + "failedToFetchCredentials": "Falha ao obter credenciais", + "credentialDeletedSuccessfully": "Credencial excluída com sucesso", + "failedToDeleteCredential": "Falha ao excluir credencial", + "confirmDeleteCredential": "Tem certeza de que deseja excluir credencial \"{{name}}\"?", + "credentialCreatedSuccessfully": "Credencial criada com sucesso", + "credentialUpdatedSuccessfully": "Credencial atualizada com sucesso", + "failedToSaveCredential": "Falha ao salvar credencial", + "failedToFetchCredentialDetails": "Não foi possível obter detalhes de credenciais", + "failedToFetchHostsUsing": "Não foi possível obter os hosts usando esta credencial", + "loadingCredentials": "Carregando credenciais...", + "retry": "Repetir", + "noCredentials": "Sem Credenciais", + "noCredentialsMessage": "Você ainda não adicionou nenhuma credencial. Clique em \"Adicionar Credencial\" para começar.", + "sshCredentials": "Credenciais SSH", + "credentialsCount": "Credenciais {{count}}", + "refresh": "atualizar", + "passwordRequired": "A senha é obrigatória", + "sshKeyRequired": "Chave SSH é necessária", + "credentialAddedSuccessfully": "Credencial \"{{name}}\" adicionado com sucesso", + "savingCredential": "Salvando credencial...", + "updatingCredential": "Atualizando credencial...", + "general": "Gerais", + "description": "Descrição:", + "folder": "pasta", + "tags": "Etiquetas", + "addTagsSpaceToAdd": "Adicionar etiquetas (pressione espaço para adicionar)", + "password": "Palavra-passe", + "key": "Chave", + "sshPrivateKey": "Chave privada SSH", + "upload": "Transferir", + "updateKey": "Atualizar Chave", + "keyPassword": "Senha da Chave", + "keyType": "Tipo de chave", + "keyTypeRSA": "RSA", + "keyTypeECDSA": "ECDSA", + "keyTypeEd25519": "Ed25519", + "basicInfo": "Informação básica", + "authentication": "Autenticação", + "organization": "Cliente", + "basicInformation": "Informações Básicas", + "basicInformationDescription": "Insira as informações básicas para esta credencial", + "authenticationMethod": "Método de autenticação", + "authenticationMethodDescription": "Escolha como você deseja autenticar com servidores SSH", + "organizationDescription": "Organize suas credenciais com pastas e tags", + "enterCredentialName": "Insira o nome da credencial", + "enterCredentialDescription": "Inserir a descrição (opcional)", + "enterUsername": "Digite o usuário", + "nameIsRequired": "Nome de Credencial é necessário", + "usernameIsRequired": "Usuário é obrigatório", + "authenticationType": "Tipo de autenticação", + "passwordAuthDescription": "Usar autenticação de senha", + "sshKeyAuthDescription": "Usar autenticação por chave SSH", + "passwordIsRequired": "A senha é obrigatória", + "sshKeyIsRequired": "Chave SSH é necessária", + "sshKeyType": "Tipo de Chave SSH", + "privateKey": "Chave Privada", + "enterPassword": "Insira a senha", + "enterPrivateKey": "Inserir chave privada", + "keyPassphrase": "Senha Chave", + "enterKeyPassphrase": "Digite a senha chave (opcional)", + "keyPassphraseOptional": "Opcional: deixe em branco se a sua chave não tem nenhuma frase secreta", + "leaveEmptyToKeepCurrent": "Deixe vazio para manter o valor atual", + "uploadKeyFile": "Carregar arquivo chave", + "generateKeyPairButton": "Gerar par de chaves", + "generateKeyPair": "Gerar par de chaves", + "generateKeyPairDescription": "Gerar um novo par de chave SSH. Se você deseja proteger a chave com uma frase-senha, insira-o no campo de senha da chave abaixo primeiro.", + "deploySSHKey": "Chave SSH de deploy", + "deploySSHKeyDescription": "Implementar chave pública no servidor de destino", + "sourceCredential": "Credenciais de origem", + "targetHost": "Host de destino", + "deploymentProcess": "Processo de implantação", + "deploymentProcessDescription": "Isto irá adicionar com segurança a chave pública ao arquivo ~/.ssh/authorized_keys sem sobrescrever as chaves existentes. A operação é reversível.", + "chooseHostToDeploy": "Escolha um host para implantar em...", + "deploying": "Implementando...", + "name": "Nome:", + "noHostsAvailable": "Nenhum host disponível", + "noHostsMatchSearch": "Nenhum host corresponde à sua pesquisa", + "sshKeyGenerationNotImplemented": "Recurso de geração de chave SSH chegando em breve", + "connectionTestingNotImplemented": "Conexão testando recursos em breve", + "testConnection": "Testar Conexão", + "selectOrCreateFolder": "Selecionar ou criar pasta", + "noFolder": "Nenhuma pasta", + "orCreateNewFolder": "Ou criar nova pasta", + "addTag": "Adicionar tag", + "saving": "Salvando...", + "credentialId": "ID de credenciais", + "overview": "Geral", + "security": "Segurança", + "usage": "Utilização", + "securityDetails": "Detalhes de segurança", + "securityDetailsDescription": "Ver informações de credencial criptografadas", + "credentialSecured": "Credencial Segura", + "credentialSecuredDescription": "Todos os dados confidenciais são criptografados com AES-256", + "passwordAuthentication": "Autenticação de senha", + "keyAuthentication": "Autenticação por Chave", + "securityReminder": "Lembrete de segurança", + "securityReminderText": "Nunca compartilhe suas credenciais. Todos os dados são criptografados.", + "hostsUsingCredential": "Hosts usando esta credencial", + "noHostsUsingCredential": "Nenhum host está usando essa credencial atualmente", + "timesUsed": "Quantidade de usos", + "lastUsed": "Último uso", + "connectedHosts": "Hosts conectados", + "created": "Criado", + "lastModified": "Última Modificação", + "usageStatistics": "Estatísticas de uso", + "copiedToClipboard": "{{field}} copiado para área de transferência", + "failedToCopy": "Falha ao copiar para área de transferência", + "sshKey": "Chave SSH", + "createCredentialDescription": "Crie uma nova credencial SSH para acesso seguro", + "editCredentialDescription": "Atualizar informações de credencial", + "listView": "Lista", + "folderView": "Pastas", + "unknownCredential": "Desconhecido", + "confirmRemoveFromFolder": "Tem certeza de que deseja remover \"{{name}}\" da pasta \"{{folder}}\"? A credencial será movida para \"Sem categoria\".", + "removedFromFolder": "Credencial \"{{name}}\" removido da pasta com sucesso", + "failedToRemoveFromFolder": "Falha ao remover a credencial da pasta", + "folderRenamed": "Pasta \"{{oldName}}\" renomeada para \"{{newName}}\" com sucesso", + "failedToRenameFolder": "Falha ao renomear pasta", + "movedToFolder": "Credencial \"{{name}}\" movido para \"{{folder}}\" com sucesso", + "failedToMoveToFolder": "Falha ao mover credencial para pasta", + "sshPublicKey": "Chave pública SSH", + "publicKeyNote": "Chave pública é opcional, mas recomendada para validação de chave", + "publicKeyUploaded": "Chave pública enviada", + "uploadPublicKey": "Carregar chave pública", + "uploadPrivateKeyFile": "Carregar arquivo de chave privada", + "uploadPublicKeyFile": "Enviar arquivo de chave pública", + "privateKeyRequiredForGeneration": "A chave privada é necessária para gerar a chave pública", + "failedToGeneratePublicKey": "Falha ao gerar chave pública", + "generatePublicKey": "Gerar a partir da Chave Privada", + "publicKeyGeneratedSuccessfully": "Chave pública gerada com sucesso", + "detectedKeyType": "Tipo de chave detectado", + "detectingKeyType": "detectando...", + "optional": "Opcional", + "generateKeyPairNew": "Gerar um novo par de chaves", + "generateEd25519": "Gerar Ed25519", + "generateECDSA": "Gerar ECDSA", + "generateRSA": "Gerar RSA", + "keyTypeEcdsaP256": "ECDSA P-256 (SSH)", + "keyTypeEcdsaP384": "ECDSA P-384 (SSH)", + "keyTypeEcdsaP521": "ECDSA P-521 (SSH)", + "keyTypeDsa": "DSA (SSH)", + "keyTypeRsaSha256": "RSA-SHA2-256", + "keyTypeRsaSha512": "RSA-SHA2-512", + "keyPairGeneratedSuccessfully": "Par de chaves {{keyType}} gerado com sucesso", + "failedToGenerateKeyPair": "Falha ao gerar o par de chaves", + "generateKeyPairNote": "Gerar um novo par de chaves SSH diretamente. Isto irá substituir todas as chaves existentes no formulário.", + "invalidKey": "Chave inválida", + "detectionError": "Erro de Detecção", + "removing": "Removendo:", + "clickToEditCredential": "Clique para editar a credencial", + "dragToMoveBetweenFolders": "Arraste para mover entre as pastas", + "keyBasedOnlyForDeployment": "Apenas credenciais com base em chaves SSH podem ser implantadas", + "publicKeyRequiredForDeployment": "Chave pública é necessária para implantação", + "selectTargetHost": "Por favor, selecione um host de destino", + "keyDeployedSuccessfully": "Chave SSH implantada com sucesso", + "deploymentFailed": "A implantação falhou", + "failedToDeployKey": "Falha ao implantar a chave SSH", + "clickToRenameFolder": "Clique para renomear a pasta", + "renameFolder": "Renomear pasta", + "idLabel": "ID:" + }, + "quickConnect": { + "title": "Conexão rápida", + "description": "Conectar diretamente a uma sessão de terminal ou gerenciador de arquivos sem salvar uma configuração de host", + "ipAddress": "Endereço IP ou Nome de Host", + "port": "Porta", + "username": "Usuário:", + "password": "Palavra-passe", + "key": "Chave privada SSH", + "keyPassword": "Senha Chave (Opcional)", + "keyType": "Tipo de chave", + "uploadFile": "Enviar Arquivo", + "pasteKey": "Colar Chave", + "credential": "Credencial", + "overrideUsername": "Substituir o nome de usuário credencial", + "overrideUsernameDesc": "Use um nome de usuário diferente do armazenado na credencial", + "connectTerminal": "Conectar ao Terminal", + "connectFileManager": "Conectar ao Gerenciador de Arquivos", + "cancel": "cancelar", + "passwordRequired": "A senha é obrigatória", + "keyRequired": "Chave SSH é necessária", + "credentialRequired": "A seleção de credenciais é necessária", + "connectionEstablished": "Conexão estabelecida com sucesso", + "connectionFailed": "Falha ao estabelecer conexão", + "autoDetect": "Detecção automática", + "authentication": "Autenticação" + }, + "dragIndicator": { + "error": "Erro: {{error}}", + "dragging": "Arrastando {{fileName}}", + "preparing": "Preparando {{fileName}}", + "readySingle": "Pronto para baixar {{fileName}}", + "readyMultiple": "Pronto para baixar arquivos {{count}}", + "batchDrag": "Arraste {{count}} arquivos para desktop", + "dragToDesktop": "Arraste para desktops", + "canDragAnywhere": "Você pode arrastar arquivos em qualquer lugar da sua área de trabalho" + }, + "sshTools": { + "title": "Ferramentas SSH", + "closeTools": "Fechar Ferramentas SSH", + "keyRecording": "Gravação de Teclas", + "startKeyRecording": "Iniciar Gravação de Teclas", + "stopKeyRecording": "Parar Chave de Gravação", + "selectTerminals": "Selecionar terminais:", + "typeCommands": "Digite comandos (todas as chaves suportadas):", + "commandsWillBeSent": "Os comandos serão enviados para {{count}} terminal(is) selecionado(s).", + "settings": "Confirgurações", + "enableRightClickCopyPaste": "Habilitar direita,clique em copiar/colar", + "shareIdeas": "Tem ideias para o que deve vir depois com ferramentas ssh? Compartilhe elas nas", + "scripts": { + "inputPlaceholder": "ex.: Comandos do Sistema, Scripts Docker" + } + }, + "snippets": { + "title": "Trechos", + "new": "Novo Trecho", + "create": "Criar Snippet", + "edit": "Editar Trecho", + "run": "Executar", + "empty": "Nenhum trecho ainda", + "emptyHint": "Criar um snippet para salvar comandos comumente usados", + "searchSnippets": "Buscar snippets...", + "name": "Nome:", + "description": "Descrição:", + "content": "Comando", + "namePlaceholder": "ex.: Reiniciar o Nginx", + "descriptionPlaceholder": "Descrição opcional", + "contentPlaceholder": "ex.: sudo systemctl reinicializa o nginx", + "nameRequired": "O nome é obrigatório", + "contentRequired": "Comando é necessário", + "createDescription": "Criar um novo snippet de comando para a execução rápida", + "editDescription": "Editar este comando de snippet", + "deleteConfirmTitle": "Excluir Snippet", + "deleteConfirmDescription": "Tem certeza que deseja excluir \"{{name}}\"?", + "createSuccess": "Snippet criado com sucesso", + "updateSuccess": "Trecho atualizado com sucesso", + "deleteSuccess": "Snippet excluído com sucesso", + "createFailed": "Falha ao criar snippet", + "updateFailed": "Falha ao atualizar snippet", + "deleteFailed": "Falha ao excluir snippet", + "failedToFetch": "Falha ao buscar snippets", + "executeSuccess": "Executando: {{name}}", + "copySuccess": "Copiado \"{{name}}\" para área de transferência", + "runTooltip": "Execute esse snippet no terminal", + "copyTooltip": "Copiar snippet para área de transferência", + "editTooltip": "Editar este trecho", + "deleteTooltip": "Excluir este snippet", + "newFolder": "Adicionar uma pasta", + "reorderSameFolder": "Só é possível reordenar snippets dentro da mesma pasta", + "reorderSuccess": "Trechos reordenados com sucesso", + "reorderFailed": "Falha ao reordenar snippets", + "deleteFolderConfirm": "Excluir pasta \"{{name}}\"? Todos os snippets serão movidos para Uncategorizados.", + "deleteFolderSuccess": "Pasta excluída com sucesso", + "deleteFolderFailed": "Falha ao excluir pasta", + "updateFolderSuccess": "Pasta atualizada com sucesso", + "createFolderSuccess": "Pasta criada com sucesso", + "updateFolderFailed": "Falha ao atualizar a pasta", + "createFolderFailed": "Falha ao criar pasta", + "selectTerminals": "Selecione os Terminais (opcional)", + "executeOnSelected": "Executar no {{count}} terminal(s) selecionado(s)", + "executeOnCurrent": "Executar no terminal atual (clique para selecionar múltipla)", + "folder": "pasta", + "selectFolder": "Selecione uma pasta ou deixe vazio", + "noFolder": "Nenhuma pasta (Descategorizada)", + "folderName": "Nome da pasta", + "folderNameRequired": "O nome da pasta é obrigatório", + "folderColor": "Cor da pasta", + "folderIcon": "Ícone da pasta", + "preview": "Pré-visualizar", + "updateFolder": "Atualizar pasta", + "createFolder": "Criar pasta", + "editFolder": "Editar Pasta", + "editFolderDescription": "Personalize sua pasta de snippet", + "createFolderDescription": "Organize seus snippets em pastas" + }, + "commandHistory": { + "title": "Histórico", + "searchPlaceholder": "Comandos de pesquisa...", + "noTerminal": "Nenhum terminal ativo", + "noTerminalHint": "Abra um terminal para ver seu histórico de comandos.", + "empty": "Nenhum histórico de comandos ainda", + "emptyHint": "Execute comandos no terminal ativo para construir sua história.", + "noResults": "Nenhum comando encontrado", + "noResultsHint": "Sem comandos correspondentes a \"{{query}}\"", + "deleteSuccess": "Comando excluído do histórico", + "deleteFailed": "Falha ao excluir comando.", + "deleteTooltip": "Excluir comando", + "tabHint": "Use a aba Terminal para autocompletar do histórico de comandos se ativado no perfil do usuário", + "authRequiredRefresh": "Autenticação necessária. Por favor, atualize a página.", + "dataAccessLockedReauth": "Acesso aos dados bloqueado. Por favor, autentique-se novamente.", + "loading": "Carregando histórico do comando...", + "error": "Erro ao Carregar Histórico" + }, + "splitScreen": { + "title": "Dividir a tela", + "none": "Nenhuma", + "twoSplit": "2 vias", + "threeSplit": "3-Caminho", + "fourSplit": "4-Caminho", + "availableTabs": "Abas disponíveis", + "dragTabsHint": "Arraste as abas para o layout das células abaixo para atribui-las", + "layout": "Dividir layout da tela", + "dropHere": "Aba Solte aqui", + "apply": "Aplicar divisão", + "clear": "Limpar Divisão", + "selectMode": "Selecione o modo de tela dividida", + "helpText": "Escolha quantas abas você quer ver de uma vez", + "success": "Tela dividida aplicada com sucesso", + "cleared": "Tela dividida apagada", + "error": { + "noAssignments": "Por favor, atribua pelo menos uma aba para o layout", + "fillAllSlots": "Por favor, preencha todos os {{count}} slots antes de aplicar" + } + }, + "homepage": { + "loggedInTitle": "Sessão iniciada!", + "loggedInMessage": "Você está logado! Use a barra lateral para acessar todas as ferramentas disponíveis. Para começar, crie um host SSH na guia SSH Manager. Uma vez criado, você pode se conectar a esse host usando os outros apps na barra lateral.", + "failedToLoadAlerts": "Falha ao carregar alertas", + "failedToDismissAlert": "Falha ao descartar alerta" + }, + "serverConfig": { + "title": "Configuração Servidor", + "description": "Configure o URL do servidor do Termix para conectar aos seus serviços de backend", + "serverUrl": "URL do servidor", + "enterServerUrl": "Por favor, insira uma URL de servidor", + "testConnectionFirst": "Por favor, teste a conexão primeiro", + "connectionSuccess": "Conexão bem-sucedida!", + "connectionFailed": "Conexão falhou", + "connectionError": "Ocorreu um erro de conexão", + "connected": "Conectado", + "disconnected": "Desconectado", + "configSaved": "Configuração salva com sucesso", + "saveFailed": "Falha ao salvar a configuração", + "saveError": "Erro ao salvar configuração", + "saving": "Salvando...", + "saveConfig": "Salvar configuração", + "helpText": "Digite a URL onde o servidor do seu Termix está executando (por exemplo, http://localhost:30001 ou https://seu-servidor.com)", + "warning": "ATENÇÃO", + "notValidatedWarning": "URL não validada - certifique-se de que está correta", + "changeServer": "Alterar Servidor", + "mustIncludeProtocol": "O URL do servidor deve começar com http:// ou https://" + }, + "versionCheck": { + "error": "Erro de verificação de versão", + "checkFailed": "Não foi possível verificar se há atualizações", + "upToDate": "O aplicativo está atualizado", + "currentVersion": "Você está executando a versão {{version}}", + "updateAvailable": "Atualização disponível", + "newVersionAvailable": "Uma nova versão está disponível! Você está executando {{current}}, mas {{latest}} está disponível.", + "releasedOn": "Lançado em {{date}}", + "downloadUpdate": "Baixar atualização", + "dismiss": "Descartar", + "checking": "Verificando atualizações...", + "checkUpdates": "Procurar por atualizações", + "checkingUpdates": "Verificando atualizações...", + "refresh": "atualizar", + "updateRequired": "Atualização Necessária", + "updateDismissed": "Atualizar notificação dispensada", + "noUpdatesFound": "Nenhuma atualização encontrada" + }, + "common": { + "close": "FECHAR", + "minimize": "Minimize", + "online": "Disponível", + "offline": "Desconectado", + "continue": "Continuar", + "maintenance": "Manutenção", + "degraded": "Degradado", + "discord": "Discord", + "error": "ERRO", + "warning": "ATENÇÃO", + "info": "Informações", + "success": "Sucesso", + "loading": "Carregandochar@@0", + "required": "Obrigatório", + "optional": "Opcional", + "connect": "Conectar", + "connecting": "Conectandochar@@0", + "creating": "Criando...", + "clear": "Limpar", + "toggleSidebar": "Alternar barra lateral", + "sidebar": "Sidebar", + "home": "Residencial", + "expired": "Expirado", + "expiresToday": "Expira hoje", + "expiresTomorrow": "Expira em {{days}} dias", + "updateAvailable": "Atualização disponível", + "sshPath": "Caminho SSH", + "localPath": "Caminho local", + "appName": "Termix", + "openInNewTab": "Abrir em nova aba", + "resetSidebarWidth": "Reset sidebar width", + "dragToResizeSidebar": "Arraste para redimensionar a barra lateral", + "noAuthCredentials": "Não há credenciais de autenticação disponíveis para este host SSH", + "noReleases": "Sem lançamentos", + "updatesAndReleases": "Atualizações e Versões", + "newVersionAvailable": "Uma nova versão ({{version}}) está disponível.", + "failedToFetchUpdateInfo": "Falha ao buscar informações de atualização", + "preRelease": "Pré-lançamento", + "loginFailed": "Falha no login", + "noReleasesFound": "Nenhuma versão encontrada.", + "yourBackupCodes": "Seus códigos de recuperação", + "sendResetCode": "Enviar Código de Redefinição", + "verifyCode": "Verificar Código", + "resetPassword": "Redefinir a senha", + "resetCode": "Reset Code", + "newPassword": "Nova Palavra-Passe", + "folder": "pasta", + "file": "Arquivo", + "renamedSuccessfully": "renomeado com sucesso", + "deletedSuccessfully": "excluído com sucesso", + "noTunnelConnections": "Nenhuma conexão de túnel configurada", + "sshTools": "Ferramentas SSH", + "english": "Portuguese-Brazil", + "chinese": "chinês", + "german": "alemão", + "cancel": "cancelar", + "username": "Usuário:", + "name": "Nome:", + "login": "Conectar-se", + "logout": "Desconectar", + "register": "Cadastrar", + "password": "Palavra-passe", + "version": "Versão", + "confirmPassword": "Confirmar senha", + "back": "Anterior", + "email": "e-mail", + "submit": "submeter", + "change": "Troca", + "save": "Guardar", + "saving": "Salvando...", + "delete": "excluir", + "edit": "Alterar", + "add": "Adicionar", + "search": "Pesquisa", + "confirm": "Confirmar", + "yes": "sim", + "no": "Não", + "ok": "Certo", + "enabled": "Ativado", + "disabled": "Desabilitado", + "important": "Importante", + "notEnabled": "Não habilitado", + "settingUp": "Configurando ...", + "next": "Próximo", + "previous": "Anterior", + "refresh": "atualizar", + "settings": "Confirgurações", + "profile": "Perfil", + "help": "Socorro", + "about": "SOBRE", + "language": "IDIOMA", + "autoDetect": "Auto-detectar", + "changeAccountPassword": "Alterar a senha da sua conta", + "passwordResetTitle": "Recuperar senha", + "passwordResetDescription": "Você está prestes a redefinir sua senha. Isso irá deslogar você de todas as sessões ativas.", + "enterSixDigitCode": "Insira o código de 6 dígitos do contêiner docker para o usuário:", + "enterNewPassword": "Digite sua nova senha para o usuário:", + "passwordsDoNotMatch": "As senhas não coincidem", + "passwordMinLength": "A senha deve ter pelo menos 6 caracteres", + "passwordResetSuccess": "Senha redefinida com sucesso! Você agora pode entrar com sua nova senha.", + "failedToInitiatePasswordReset": "Falha ao iniciar a redefinição de senha", + "failedToVerifyResetCode": "Falha ao verificar código de redefinição", + "failedToCompletePasswordReset": "Falha ao concluir a redefinição de senha", + "documentation": "Documentação", + "retry": "Repetir", + "checking": "Verificandochar@@0", + "checkingDatabase": "Verificando conexão com o banco de dados...", + "checkingAuthentication": "Verificando autenticação...", + "actions": "Ações.", + "remove": "Excluir", + "revoke": "Revoke", + "create": "Crio", + "update": "Atualização" + }, + "nav": { + "home": "Residencial", + "hosts": "Anfitriões", + "credentials": "Credenciais", + "terminal": "Terminal", + "docker": "Atracador", + "tunnels": "Túneis", + "fileManager": "Gerenciador de Arquivos", + "serverStats": "Estatísticas do servidor", + "admin": "Administrador", + "userProfile": "Informações do Perfil", + "tools": "Ferramentas", + "snippets": "Trechos", + "newTab": "New Tab", + "splitScreen": "Dividir a tela", + "closeTab": "Fechar Aba", + "sshManager": "Gerenciador SSH", + "hostManager": "Gerenciador de Host", + "cannotSplitTab": "Não é possível dividir esta aba", + "tabNavigation": "Tab Navigation", + "hostTabTitle": "{{username}}@{{ip}}:{{port}}", + "copyPassword": "Copiar Senha", + "copySudoPassword": "Copiar Senha do Sudo", + "passwordCopied": "Senha copiada para área de transferência", + "sudoPasswordCopied": "Senha sudo copiada para área de transferência", + "noPasswordAvailable": "Nenhuma senha disponível" + }, + "admin": { + "title": "Configurações de administrador", + "oidc": "OCIDADE", + "users": "Utilizadores", + "userManagement": "Gerenciamento de usuários", + "makeAdmin": "Tornar administrador", + "removeAdmin": "Remover Administrador", + "deleteUser": "Excluir usuário {{username}}? Isso não pode ser desfeito.", + "allowRegistration": "Permitir Registro", + "oidcSettings": "Configurações OIDC", + "clientId": "ID do Cliente", + "clientSecret": "Segredo do Cliente", + "issuerUrl": "URL do Emissor", + "authorizationUrl": "URL de autorização", + "tokenUrl": "URL do token", + "updateSettings": "Configurações de atualização", + "confirmDelete": "Tem certeza de que deseja excluir este usuário?", + "confirmMakeAdmin": "Tem certeza que deseja tornar o administrador {{username}}?", + "confirmRemoveAdmin": "Tem certeza que deseja remover o status de administrador do {{username}}?", + "externalAuthentication": "Autenticação externa (OIDC)", + "configureExternalProvider": "Configurar provedor de identidade externo para autenticação OIDC/OAuth2.", + "userIdentifierPath": "Caminho do usuário", + "displayNamePath": "Exibir Caminho do Nome", + "scopes": "Âmbitos", + "saving": "Salvando...", + "saveConfiguration": "Salvar configuração", + "reset": "Reset", + "success": "Sucesso", + "loading": "Carregandochar@@0", + "refresh": "atualizar", + "loadingUsers": "Carregando usuários...", + "username": "Usuário:", + "type": "tipo", + "actions": "Ações.", + "external": "externo", + "local": "Localização", + "adminManagement": "Gerenciamento de administração", + "makeUserAdmin": "Tornar Usuário Administrador", + "adding": "Adicionando...", + "currentAdmins": "Administradores atuais", + "adminBadge": "Administrador", + "removeAdminButton": "Remover Administrador", + "general": "Gerais", + "userRegistration": "Registro de Usuário", + "allowNewAccountRegistration": "Permitir o registro de novas contas", + "allowPasswordLogin": "Permitir login do usuário/senha", + "allowPasswordReset": "Permitir redefinição de senha via código de redefinição", + "missingRequiredFields": "Faltam campos obrigatórios: {{fields}}", + "oidcConfigurationUpdated": "Configuração OIDC atualizada com sucesso!", + "failedToFetchOidcConfig": "Falha ao buscar a configuração OIDC", + "failedToFetchRegistrationStatus": "Não foi possível obter o status do registro", + "failedToFetchPasswordLoginStatus": "Falha ao obter o status de login da senha", + "failedToFetchUsers": "Falha ao buscar usuários", + "oidcConfigurationDisabled": "Configuração OIDC desativada com sucesso!", + "failedToUpdateOidcConfig": "Falha ao atualizar a configuração OIDC", + "failedToDisableOidcConfig": "Falha ao desativar a configuração OIDC", + "enterUsernameToMakeAdmin": "Insira o usuário para fazer o administrador", + "userIsNowAdmin": "Usuário {{username}} agora é um administrador", + "failedToMakeUserAdmin": "Falha ao tornar o usuário administrador", + "removeAdminStatus": "Remover status de administrador do {{username}}?", + "adminStatusRemoved": "Status de administrador removido da {{username}}", + "failedToRemoveAdminStatus": "Falha ao remover o status de administrador", + "userDeletedSuccessfully": "Usuário {{username}} excluído com sucesso", + "failedToDeleteUser": "Falha ao excluir usuário", + "overrideUserInfoUrl": "Substituir URL de informação de usuários (não é obrigatório)", + "failedToFetchSessions": "Falha ao buscar sessões", + "sessionRevokedSuccessfully": "Sessão revogada com sucesso", + "failedToRevokeSession": "Falha ao revogar sessão", + "confirmRevokeSession": "Tem certeza de que deseja revogar esta sessão?", + "confirmRevokeAllSessions": "Tem certeza de que deseja revogar todas as sessões deste usuário?", + "failedToRevokeSessions": "Falha ao revogar sessões", + "sessionsRevokedSuccessfully": "Sessões revogadas com sucesso", + "linkToPasswordAccount": "Link para a Conta de Senha", + "linkOIDCDialogTitle": "Vincular conta OIDC à conta de senha", + "linkOIDCDialogDescription": "Vincular {{username}} (usuário OIDC) a uma conta de senha existente. Isso ativará a verificação dupla para a conta de senha.", + "createUser": "Criar Usuário", + "createUserDescription": "Criar um novo usuário local com nome de usuário e senha", + "enterUsername": "Digite o usuário", + "enterPassword": "Insira a senha", + "userCreatedSuccessfully": "Usuário {{username}} criado com sucesso", + "failedToCreateUser": "Falha ao criar usuário", + "manageUser": "Gerenciar Usuário", + "manageUserDescription": "Gerenciar configurações, papéis e permissões do usuário", + "authType": "Tipo de autenticação", + "adminStatus": "Status do administrador", + "userId": "ID de usuário", + "regularUser": "Usuário Normal", + "adminPrivileges": "Privilégios do Administrador", + "administratorRole": "Função de Administrador", + "administratorRoleDescription": "Conceder privilégios de acesso e gerenciamento ao sistema completo", + "passwordManagement": "Gerenciamento de senhas", + "passwordResetWarning": "Redefinir a senha de um usuário apagará todos os dados (hosts SH, credenciais, configurações). Esta ação não pode ser desfeita.", + "resetUserPassword": "Redefinir Senha do Usuário", + "resettingPassword": "Redefinindo...", + "passwordResetInitiated": "Redefinição de senha iniciada para {{username}}. Redefinir código enviado.", + "failedToResetPassword": "Falha ao iniciar a redefinição de senha", + "sessionManagement": "Gerenciamento de sessão", + "revokeAllSessions": "Revogar todas as sessões", + "revokeAllSessionsDescription": "Forçar logout de todos os dispositivos e sessões", + "revoking": "Revogando...", + "revoke": "Revogar Todos", + "dangerZone": "Zona de Perigo", + "deleteUserTitle": "Excluir conta de usuário", + "deleteUserWarning": "Excluir permanentemente esta conta de usuário e todos os dados associados. Esta ação não pode ser desfeita.", + "deleting": "Excluindo...", + "cannotDeleteSelf": "Você não pode excluir sua própria conta", + "cannotRemoveLastAdmin": "Não é possível remover o último administrador", + "cannotRemoveOwnAdmin": "Você não pode remover seus próprios privilégios de administrador", + "cannotModifyOwnAdminStatus": "Você não pode modificar seu próprio status de administrador", + "dualAuth": "Dupla Autenticação", + "externalOIDC": "Externo (OIDC)", + "localPassword": "Senha local", + "confirmRevokeOwnSessions": "Tem certeza que deseja revogar todas as suas próprias sessões? Você será desconectado.", + "linkOIDCWarningTitle": "Aviso: Os dados de usuário OIDC serão excluídos", + "linkOIDCActionDeleteUser": "Excluir a conta de usuário OIDC e todos os seus dados", + "linkOIDCActionAddCapability": "Adicionar capacidade de login OIDC à conta de senha de destino", + "linkOIDCActionDualAuth": "Permitir que a conta de senha acesse com senha e OIDC", + "linkTargetUsernameLabel": "Usuário da Conta de Destino", + "linkTargetUsernamePlaceholder": "Insira o usuário da conta da senha", + "linkAccountsButton": "Vincular Contas", + "linkingAccounts": "Ligando...", + "accountsLinkedSuccessfully": "O usuário OIDC {{oidcUsername}} foi vinculado a {{targetUsername}}", + "failedToLinkAccounts": "Falha ao vincular contas", + "linkTargetUsernameRequired": "Nome de usuário alvo é necessário", + "unlinkOIDCTitle": "Desvincular Autenticação OIDC", + "unlinkOIDCDescription": "Remover autenticação OIDC do {{username}}? O usuário só será capaz de acessar com nome de usuário/senha depois disso.", + "unlinkOIDCSuccess": "OIDC desvinculado de {{username}}", + "failedToUnlinkOIDC": "Falha ao desvincular OIDC", + "databaseSecurity": "Segurança da base", + "encryptionStatus": "Status da Criptografia", + "encryptionEnabled": "Criptografia habilitada", + "enabled": "Ativado", + "disabled": "Desabilitado", + "keyId": "ID da chave", + "created": "Criado", + "migrationStatus": "Status da migração", + "migrationCompleted": "Migração concluída", + "migrationRequired": "Migração necessária", + "deviceProtectedMasterKey": "Chave Mestra Protegida pelo Ambiente", + "legacyKeyStorage": "Armazenamento de chave antiga", + "masterKeyEncryptedWithDeviceFingerprint": "Chave mestra criptografada com impressão digital ambiente (proteção KEK ativa)", + "keyNotProtectedByDeviceBinding": "Chave não protegida por vínculo de ambiente (atualização recomendada)", + "valid": "Valid", + "initializeDatabaseEncryption": "Inicializar Criptografia do Banco de Dados", + "enableAes256EncryptionWithDeviceBinding": "Habilite criptografia AES-256 com proteção de chave mestra vinculada ao ambiente. Isso cria segurança de nível empresarial para chaves SSH, senhas e tokens de autenticação.", + "featuresEnabled": "Funcionalidades habilitadas:", + "aes256GcmAuthenticatedEncryption": "Criptografia AES-256-GCM autenticada", + "deviceFingerprintMasterKeyProtection": "Proteção de chave mestra do ambiente (KEK)", + "pbkdf2KeyDerivation": "A derivação da chave PBKDF2 com 100K iterações", + "automaticKeyManagement": "Gerenciamento automático de chaves e rotação", + "initializing": "Inicializandochar@@0", + "initializeEnterpriseEncryption": "Inicializar Criptografia da Empresa", + "migrateExistingData": "Migrar Dados Existentes", + "encryptExistingUnprotectedData": "Criptografar dados desprotegidos existentes em seu banco de dados. Esse processo é seguro e cria backups automáticos.", + "testMigrationDryRun": "Verificar a compatibilidade de criptografia", + "migrating": "Migrando...", + "migrateData": "Migrar Dados", + "securityInformation": "Informações de segurança", + "sshPrivateKeysEncryptedWithAes256": "Chaves e senhas SSH são criptografadas com AES-256-GCM", + "userAuthTokensProtected": "Tokens de autenticação de usuário e segredos de 2FA estão protegidos", + "masterKeysProtectedByDeviceFingerprint": "Chaves criptográficas principais estão protegidas pela impressão digital do dispositivo (KEK)", + "keysBoundToServerInstance": "Chaves estão vinculadas ao ambiente atual do servidor (migrável através de variáveis de ambiente)", + "pbkdf2HkdfKeyDerivation": "PBKDF2 + derivação de chave HKDF com 100K iterações", + "backwardCompatibleMigration": "Todos os dados permanecem incompatíveis durante a migração", + "enterpriseGradeSecurityActive": "Segurança de notas empresariais ativa", + "masterKeysProtectedByDeviceBinding": "Suas chaves de criptografia mestra estão protegidas pela impressão digital. Isto usa o nome de host do servidor, caminhos e outras informações de ambiente para gerar chaves de proteção. Para migrar os servidores, defina a variável de ambiente DB_ENCRYPTION_KEY no novo servidor.", + "important": "Importante", + "keepEncryptionKeysSecure": "Assegure a segurança dos dados: faça um backup regular dos seus arquivos de banco de dados e da configuração do servidor. Para migrar para um novo servidor, defina a variável de ambiente DB_ENCRYPTION_KEY no novo ambiente, ou mantenha o mesmo nome de host e estrutura de diretório.", + "loadingEncryptionStatus": "Carregando status da criptografia...", + "testMigrationDescription": "Verifique se os dados existentes podem ser migrados com segurança para o formato criptografado sem modificar quaisquer dados", + "serverMigrationGuide": "Guia de Migração", + "migrationInstructions": "Para migrar dados criptografados para um novo servidor: 1) Fazer backup dos arquivos de banco de dados, 2) Definir a variável de ambiente DB_ENCRYPTION_KEY=\"sua-chave\" em novo servidor, 3) Restaurar os arquivos de banco de dados", + "environmentProtection": "Proteção do Ambiente", + "environmentProtectionDesc": "Protege chaves de criptografia com base na informação de ambiente do servidor (hostname, paths, etc.), migrável através de variáveis de ambiente", + "verificationCompleted": "Verificação de compatibilidade completada - nenhum dado foi alterado", + "verificationInProgress": "Verificação concluída", + "dataMigrationCompleted": "Migração de dados concluída com sucesso!", + "verificationFailed": "Verificação de compatibilidade falhou", + "migrationFailed": "Falha na migração", + "runningVerification": "Executando verificação de compatibilidade...", + "startingMigration": "Iniciando migração...", + "hardwareFingerprintSecurity": "Segurança da Impressão Digital Hardware", + "hardwareBoundEncryption": "Criptografia de Hardware-Bound Ativa", + "masterKeysNowProtectedByHardwareFingerprint": "Chaves mestres agora são protegidas por impressão digital de hardware real em vez de variáveis de ambiente", + "cpuSerialNumberDetection": "Detecção de números seriais da CPU", + "motherboardUuidIdentification": "Identificação do cliente UUID", + "diskSerialNumberVerification": "Verificação de número de série do disco", + "biosSerialNumberCheck": "Verificar número de série da BIOS", + "stableMacAddressFiltering": "Filtragem de endereço MAC estável", + "databaseFileEncryption": "Criptografia de Arquivo Base", + "dualLayerProtection": "Proteção de dupla camada ativa", + "bothFieldAndFileEncryptionActive": "A criptografia em nível de campo e nível de arquivo agora estão ativos para máxima segurança", + "fieldLevelAes256Encryption": "Field-level AES-256 criptografia para dados confidenciais", + "fileLevelDatabaseEncryption": "Criptografia do banco de dados de nível de arquivo com ligação de hardware", + "hardwareBoundFileKeys": "Chaves de criptografia para arquivos com hardware", + "automaticEncryptedBackups": "Criação de backup criptografado automático", + "createEncryptedBackup": "Criar Backup Criptografado", + "creatingBackup": "Criando Backup...", + "backupCreated": "Backup Criado", + "encryptedBackupCreatedSuccessfully": "Backup criptografado criado com sucesso", + "backupCreationFailed": "Criação do backup falhou", + "databaseMigration": "Migração de banco", + "exportForMigration": "Exportar para Migração", + "exportDatabaseForHardwareMigration": "Exportar banco de dados como arquivo SQLite com dados descriptografados para migração para novo hardware", + "exportDatabase": "Export SQLite Database", + "exporting": "Exportando...", + "exportCreated": "Exportação do SQLite criada", + "exportContainsDecryptedData": "A exportação SQLite contém dados descriptografados - mantenha-se seguro!", + "databaseExportedSuccessfully": "Banco de dados SQLite exportado com sucesso", + "databaseExportFailed": "Falha na exportação do banco de dados SQLite", + "importFromMigration": "Importar da migração", + "importDatabaseFromAnotherSystem": "Importar banco de dados SQLite a partir de outro sistema ou hardware", + "importDatabase": "Import SQLite Database", + "importing": "Importando...", + "selectedFile": "Arquivo SQLite selecionado", + "importWillReplaceExistingData": "A importação de SQLite substituirá os dados existentes - backup recomendado!", + "pleaseSelectImportFile": "Por favor selecione um arquivo de importação SQLite", + "databaseImportedSuccessfully": "Base de dados SQLite importada com sucesso", + "databaseImportFailed": "Falha na importação do banco de dados SQLite", + "manageEncryptionAndBackups": "Gerenciar chaves de criptografia, segurança do banco de dados e operações de backup", + "activeSecurityFeatures": "Medidas de segurança e proteções atuais", + "deviceBindingTechnology": "Tecnologia avançada de proteção de chaves", + "backupAndRecovery": "Opções de criação de backup seguro e recuperação do banco de dados", + "crossSystemDataTransfer": "Exportar e importar bancos de dados em diferentes sistemas", + "noMigrationNeeded": "Não é necessária migração", + "encryptionKey": "Chave de Criptografia", + "keyProtection": "Proteção por Chave", + "active": "ativo", + "legacy": "Antigo", + "dataStatus": "Situação dos dados", + "encrypted": "Criptografado", + "needsMigration": "Precisa de migração", + "ready": "pronto", + "initializeEncryption": "Iniciar Criptografia", + "initialize": "Inicializar", + "test": "teste", + "migrate": "Migrar", + "backup": "Backup", + "createBackup": "Criar Backup", + "exportImport": "Exportar/Importar", + "export": "Exportação", + "import": "Importação", + "passwordRequired": "Senha requerida", + "confirmExport": "Confirmar Exportação", + "exportDescription": "Exportar hosts SSH e credenciais como arquivo SQLite", + "importDescription": "Importar arquivo SQLite com merge incremental (ignora duplicatas)", + "criticalWarning": "Aviso Crítico", + "cannotDisablePasswordLoginWithoutOIDC": "Não é possível desabilitar o login da senha sem OIDC configurado! Você deve configurar a autenticação OIDC antes de desativar o login da senha, ou você perderá o acesso ao Termix.", + "confirmDisablePasswordLogin": "Tem certeza que deseja desativar o login por senha? Certifique-se de que OIDC está configurado corretamente e funcionando antes de prosseguir, ou você perderá o acesso à sua instância do Termix.", + "passwordLoginDisabled": "Login com senha desativado com sucesso", + "passwordLoginAndRegistrationDisabled": "Login de senha e registro de nova conta desativados com sucesso", + "requiresPasswordLogin": "Requer login com senha ativado", + "passwordLoginDisabledWarning": "Login de senha está desabilitado. Certifique-se de que OIDC está configurado corretamente ou você não será capaz de entrar no Termix.", + "oidcRequiredWarning": "CRITICAL: O login de senha está desativado. Se você redefinir ou configurar mal OIDC, você perderá todo o acesso ao Termix e brickará sua instância. Só prossiga se tiver a certeza absoluta.", + "confirmDisableOIDCWarning": "AVISO: Você está prestes a desativar OIDC enquanto o login por senha está desabilitado. Isto irá brickar sua instância do Termix e você irá perder todos os acesso. Tem certeza absoluta de que deseja continuar?", + "failedToUpdatePasswordLoginStatus": "Falha ao atualizar status da senha de login", + "loadingSessions": "Carregando sessões...", + "noActiveSessions": "Não foram encontradas sessões ativas.", + "device": "Dispositivo", + "user": "Usuário", + "lastActive": "Última atividade", + "expires": "Expira", + "revoked": "Revoked", + "revokeAllUserSessionsTitle": "Revogar todas as sessões para este usuário", + "revokeAll": "Revogar Todos", + "linkOidcToPasswordAccount": "Vincular conta OIDC à conta de senha", + "linkOidcToPasswordAccountDescription": "Vincular {{username}} (usuário OIDC) a uma conta de senha existente. Isso ativará a verificação dupla para a conta de senha.", + "linkOidcWarningTitle": "Aviso: Os dados de usuário OIDC serão excluídos", + "linkOidcWarningDescription": "Esta ação irá:", + "linkOidcActionDeleteUser": "Excluir a conta de usuário OIDC e todos os seus dados", + "linkOidcActionAddCapability": "Adicionar capacidade de login OIDC à conta de senha de destino", + "linkOidcActionDualAuth": "Permitir que a conta de senha acesse com senha e OIDC", + "passwordMinLength": "A senha deve ter pelo menos 6 caracteres", + "currentRoles": "Papéis Atuais", + "noRolesAssigned": "Nenhum cargo atribuído", + "assignNewRole": "Atribuir Nova Função" + }, + "hosts": { + "title": "Gerenciador de Host", + "sshHosts": "Hosts SSH", + "noHosts": "Nenhum host SSH", + "noHostsMessage": "Você ainda não adicionou nenhum host SSH. Clique em \"Adicionar Host\" para começar.", + "loadingHosts": "Carregando hosts...", + "failedToLoadHosts": "Falha ao carregar hosts", + "retry": "Repetir", + "refresh": "atualizar", + "optional": "Opcional", + "hostsCount": "Hosts {{count}}", + "importJson": "Importar JSON", + "importing": "Importando...", + "importJsonTitle": "Importar o SSH Hosts do JSON", + "importJsonDesc": "Carregar um arquivo JSON para importar em massa vários hosts SSH (máx. 100).", + "downloadSample": "Baixar Exemplo", + "formatGuide": "Guia de formatação", + "exportCredentialWarning": "Aviso: Host \"{{name}}\" usa autenticação de credenciais. O arquivo exportado não incluirá os dados de credenciais e precisará ser reconfigurado manualmente após a importação. Deseja continuar?", + "exportSensitiveDataWarning": "Aviso: Host \"{{name}}\" contém dados de autenticação confidenciais (senha / chave SSH). O arquivo exportado incluirá estes dados em texto simples. Mantenha o arquivo seguro e exclua-o após o uso. Deseja continuar?", + "uncategorized": "Descategorizado", + "confirmDelete": "Tem certeza que deseja excluir \"{{name}}\" ?", + "failedToDeleteHost": "Falha ao excluir host", + "failedToExportHost": "Falha ao exportar o host. Verifique se você está logado e tenha acesso aos dados do host.", + "jsonMustContainHosts": "JSON deve conter um array \"hosts\" ou ser um array de hosts", + "noHostsInJson": "Nenhum host encontrado no arquivo JSON", + "maxHostsAllowed": "Máximo de 100 hosts permitidos por importação", + "importCompleted": "Importação concluída: {{success}} com sucesso, {{failed}} falhou", + "importFailed": "Falha na importação", + "importError": "Erro ao importar", + "failedToImportJson": "Falha ao importar arquivo JSON", + "connectionDetails": "Detalhes da conexão", + "organization": "Cliente", + "ipAddress": "Endereço IP", + "port": "Porta", + "name": "Nome:", + "username": "Usuário:", + "folder": "pasta", + "tags": "Etiquetas", + "pin": "PIN", + "notes": "Observações", + "expirationDate": "Data de Validade", + "passwordRequired": "A senha é obrigatória ao usar a autenticação de senha", + "sshKeyRequired": "A chave privada SSH é necessária para a autenticação de chave", + "keyTypeRequired": "Tipo de chave é necessário ao usar autenticação de chave", + "mustSelectValidSshConfig": "Precisa selecionar uma configuração SSH válida da lista", + "addHost": "Adicionar Host", + "editHost": "Editar Host", + "cloneHost": "Clonar Host", + "updateHost": "Atualizar Host", + "hostUpdatedSuccessfully": "O host \"{{name}}\" foi atualizado com sucesso!", + "hostAddedSuccessfully": "Host \"{{name}}\" adicionado com sucesso!", + "hostDeletedSuccessfully": "Host \"{{name}}\" excluído com sucesso!", + "failedToSaveHost": "Falha ao salvar host. Por favor, tente novamente.", + "savingHost": "Salvando host...", + "updatingHost": "Atualizando host...", + "cloningHost": "Excluindo host...", + "enableTerminal": "Ativar Terminal", + "enableTerminalDesc": "Ativar/desativar a visibilidade do host na aba Terminal", + "enableTunnel": "Ativar Túnel", + "enableTunnelDesc": "Ativar/desativar a visibilidade do host na aba Túnel", + "enableFileManager": "Ativar Gerenciador de Arquivos", + "enableFileManagerDesc": "Ativar/desativar a visibilidade do host na aba Gerenciador de Arquivos", + "enableDockerDesc": "Ativar/desativar a visibilidade do host na aba Docker", + "enableDocker": "Ativar Docker", + "defaultPath": "Caminho Padrão", + "defaultPathDesc": "Diretório padrão ao abrir o gerenciador de arquivos para este host", + "tunnelConnections": "Conexões de túnel", + "connection": "Ligação", + "remove": "Excluir", + "sourcePort": "Porta de origem", + "sourcePortDesc": " (código-fonte refere-se aos detalhes da conexão atual na guia geral)", + "endpointPort": "Porta de Endpoint", + "endpointSshConfig": "Configuração do Endpoint SSH", + "tunnelForwardDescription": "Este túnel irá encaminhar o tráfego da porta {{sourcePort}} na máquina de origem (detalhes atuais da conexão em geral) para a porta {{endpointPort}} na máquina de fim de linha.", + "maxRetries": "Máximo de buscas", + "maxRetriesDescription": "Número máximo de tentativas de tentar conectar túneis novamente.", + "retryInterval": "Intervalo de Repetir (segundos)", + "retryIntervalDescription": "Tempo de espera entre tentativas novamente.", + "autoStartContainer": "Iniciar automaticamente no lançamento do contêiner", + "autoStartDesc": "Iniciar automaticamente este túnel quando o contêiner iniciar", + "addConnection": "Adicionar Conexão de Túnel", + "tunnelType": "Tipo de túnel", + "tunnelTypeLocal": "Local (-L)", + "tunnelTypeRemote": "Remoto (-R)", + "tunnelTypeLocalDesc": "Encaminhar porta local para o ponto de extremidade remota", + "tunnelTypeRemoteDesc": "Encaminhar porta remota para a máquina local", + "tunnelForwardDescriptionLocal": "Este túnel irá encaminhar o tráfego da porta local {{sourcePort}} para a porta {{endpointPort}} na máquina de extremidade.", + "tunnelForwardDescriptionRemote": "Este túnel irá encaminhar o tráfego da porta {{sourcePort}} na máquina de origem (detalhes atuais da conexão em geral) para a porta {{endpointPort}} na máquina de fim de linha.", + "sshpassRequired": "Sshpass necessário para autenticação de senha", + "sshpassRequiredDesc": "Para autenticação de senha nos túneis, o sshpass deve ser instalado no sistema.", + "otherInstallMethods": "Outros métodos de instalação:", + "debianUbuntuEquivalent": "(Debian/Ubuntu) ou o equivalente para seu sistema operacional.", + "or": "ou", + "centosRhelFedora": "CentOS/RHEL/Fedora", + "macos": "macOS", + "windows": "Windows (WSL, sem binário oficial)", + "sshServerConfigRequired": "Configuração do servidor SSH necessária", + "sshServerConfigDesc": "Para as conexões do túnel, o servidor SSH deve ser configurado para permitir encaminhamento de portas:", + "gatewayPortsYes": "vincular portas remotas a todas as interfaces", + "allowTcpForwardingYes": "para habilitar o encaminhamento de porta", + "permitRootLoginYes": "se estiver usando um usuário root para tunelamento", + "editSshConfig": "Editar o arquivo de configuração SSH:", + "sshConfigPath": "/etc/ssh/sshd_config", + "restartSshService": "Em seguida, reinicie o serviço SSH:", + "restartSshCommand": "sudo systemctl reinicia sshd", + "upload": "Transferir", + "authentication": "Autenticação", + "password": "Palavra-passe", + "key": "Chave", + "credential": "Credencial", + "none": "Nenhuma", + "selectCredential": "Selecionar Credencial", + "selectCredentialPlaceholder": "Escolha uma credencial...", + "credentialRequired": "A credencial é necessária para usar autenticação de credenciais", + "credentialDescription": "Selecionar uma credencial substituirá o nome de usuário atual e usará os detalhes de autenticação da credencial.", + "cannotChangeAuthAsSharedUser": "Não é possível alterar a autenticação como usuário compartilhado", + "sshPrivateKey": "Chave privada SSH", + "keyPassword": "Senha da Chave", + "keyType": "Tipo de chave", + "autoDetect": "Auto-detectar", + "rsa": "RSA", + "ed25519": "ED25519", + "ecdsaNistP256": "ECDSA NIST P-256", + "ecdsaNistP384": "ECDSA NIST P-384", + "ecdsaNistP521": "ECDSA NIST P-521", + "dsa": "DSA", + "rsaSha2256": "RSA SHA2-256", + "rsaSha2512": "RSA SHA2-512", + "uploadFile": "Enviar Arquivo", + "pasteKey": "Colar Chave", + "updateKey": "Atualizar Chave", + "existingKey": "Chave Existente (clique para alterar)", + "existingCredential": "Credencial Existente (clique para alterar)", + "addTagsSpaceToAdd": "adicionar etiquetas (espaço para adicionar)", + "terminalBadge": "Terminal", + "tunnelBadge": "Túnel", + "fileManagerBadge": "Gerenciador de Arquivos", + "general": "Gerais", + "terminal": "Terminal", + "tunnel": "Túnel", + "fileManager": "Gerenciador de Arquivos", + "serverStats": "Estatísticas do servidor", + "hostViewer": "Visualizador do Host", + "enableServerStats": "Habilitar estatísticas do servidor", + "enableServerStatsDesc": "Ativar/desativar estatísticas do servidor para este host", + "displayItems": "Exibir itens", + "displayItemsDesc": "Escolha quais métricas serão exibidas na página de estatísticas do servidor", + "enableCpu": "Uso da CPU", + "enableMemory": "Memória Utilizada", + "enableDisk": "Uso do disco", + "enableNetwork": "Estatísticas de Rede (Em Breve)", + "enableProcesses": "Contagem de processos (Em breve)", + "enableUptime": "Tempo de trabalho (Em breve)", + "enableHostname": "Hostname (Em breve)", + "enableOs": "Sistema Operacional (Em breve)", + "customCommands": "Comandos Personalizados (Em Breve)", + "customCommandsDesc": "Definir comandos personalizados de desligamento e reinicialização para este servidor", + "shutdownCommand": "Comando de desligamento", + "rebootCommand": "Comando de reinicialização", + "confirmRemoveFromFolder": "Tem certeza de que deseja remover \"{{name}}\" da pasta \"{{folder}}\"? O host será movido para \"Nenhuma pasta\".", + "removedFromFolder": "Host \"{{name}}\" removido da pasta com sucesso", + "failedToRemoveFromFolder": "Falha ao remover o host da pasta", + "folderRenamed": "Pasta \"{{oldName}}\" renomeada para \"{{newName}}\" com sucesso", + "failedToRenameFolder": "Falha ao renomear pasta", + "editFolderAppearance": "Editar aparência da pasta", + "editFolderAppearanceDesc": "Personalizar a cor e ícone para pasta", + "folderColor": "Cor da pasta", + "folderIcon": "Ícone da pasta", + "preview": "Pré-visualizar", + "folderAppearanceUpdated": "Aparência da pasta atualizada com sucesso", + "failedToUpdateFolderAppearance": "Falha ao atualizar a aparência da pasta", + "deleteAllHostsInFolder": "Excluir todos os hosts na pasta", + "confirmDeleteAllHostsInFolder": "Tem certeza que deseja excluir todos os hosts do {{count}} na pasta \"{{folder}}\"? Esta ação não pode ser desfeita.", + "allHostsInFolderDeleted": "{{count}} hosts apagados da pasta \"{{folder}}\" com sucesso", + "failedToDeleteHostsInFolder": "Falha ao excluir hosts da pasta", + "movedToFolder": "Host \"{{name}}\" movido para \"{{folder}}\" com sucesso", + "failedToMoveToFolder": "Falha ao mover host para a pasta", + "clickToRenameFolder": "Clique para renomear a pasta", + "renameFolder": "Renomear pasta", + "removeFromFolder": "Remover da pasta \"{{folder}}\"", + "editHostTooltip": "Editar host", + "deleteHostTooltip": "Excluir host", + "exportHostTooltip": "Exportar host", + "cloneHostTooltip": "Clonar host", + "clickToEditHost": "Clique para editar o host", + "dragToMoveBetweenFolders": "Arraste para mover entre as pastas", + "exportedHostConfig": "Configuração host exportada para {{name}}", + "openTerminal": "Abrir terminal", + "openFileManager": "Abrir Gerenciador de Arquivos", + "openTunnels": "Abrir túneis", + "openServerDetails": "Abrir Detalhes do Servidor", + "statistics": "estatísticas", + "enabledWidgets": "Widgets ativos", + "openServerStats": "Estatísticas do Servidor Aberto", + "enabledWidgetsDesc": "Selecione quais widgets de estatísticas a exibir para este host", + "monitoringConfiguration": "Configuração de Monitoramento", + "monitoringConfigurationDesc": "Configurar com que frequência estatísticas e status do servidor são verificados", + "statusCheckEnabled": "Habilitar monitoramento de status", + "statusCheckEnabledDesc": "Verifique se o servidor está online ou offline", + "statusCheckInterval": "Intervalo de verificação de status", + "statusCheckIntervalDesc": "Frequência para verificar se o host está online (5s - 1h)", + "metricsEnabled": "Habilitar monitoramento de métricas", + "metricsEnabledDesc": "Coletar estatísticas de CPU, RAM, disco e outros sistemas", + "metricsInterval": "Intervalo de Coleção de Métricas", + "metricsIntervalDesc": "Com que frequência coletar estatísticas do servidor (5s - 1h)", + "intervalSeconds": "segundos", + "intervalMinutes": "Minutos", + "intervalValidation": "Intervalos de monitoramento devem ser entre 5 segundos e 1 hora (3600 segundos)", + "monitoringDisabled": "Monitoramento do servidor está desabilitado para este host", + "enableMonitoring": "Habilitar monitoramento no Host Manager → Aba de Estatísticas", + "monitoringDisabledBadge": "Monitoramento desligado", + "statusMonitoring": "SItuação", + "metricsMonitoring": "Métricas", + "terminalCustomization": "Customização de Terminal", + "appearance": "Aparência", + "behavior": "Comportamento", + "advanced": "Avançado", + "themePreview": "Pré-visualização do tema", + "theme": "Tema", + "selectTheme": "Selecione um tema", + "chooseColorTheme": "Escolha um tema de cor para o terminal", + "fontFamily": "Família de fonte", + "selectFont": "Select font", + "selectFontDesc": "Selecione a fonte a ser usada no terminal", + "fontSize": "Font Size", + "fontSizeValue": "Tamanho da fonte: {{value}}px", + "adjustFontSize": "Ajustar o tamanho da fonte do terminal", + "letterSpacing": "Espaçamento das letras", + "letterSpacingValue": "Espaçamento das letras: {{value}}px", + "adjustLetterSpacing": "Ajustar espaçamento entre os caracteres", + "lineHeight": "Altura da linha", + "lineHeightValue": "Altura da linha: {{value}}", + "adjustLineHeight": "Ajustar espaçamento entre as linhas", + "cursorStyle": "Estilo do cursor", + "selectCursorStyle": "Selecionar estilo do cursor", + "cursorStyleBlock": "Bloquear", + "cursorStyleUnderline": "Sublinhar", + "cursorStyleBar": "Barras", + "chooseCursorAppearance": "Escolha a aparência do cursor", + "cursorBlink": "Pisca do Cursor", + "enableCursorBlink": "Ativar animação piscando o cursor", + "scrollbackBuffer": "Buffer de rolagem", + "scrollbackBufferValue": "Buffer: Linhas {{value}}", + "scrollbackBufferDesc": "Número de linhas para manter no histórico de rolagem de volta", + "bellStyle": "Estilo do sino", + "selectBellStyle": "Selecionar estilo do sino", + "bellStyleNone": "Nenhuma", + "bellStyleSound": "Som", + "bellStyleVisual": "Visual", + "bellStyleBoth": "Ambos", + "bellStyleDesc": "Como lidar com o terminal bell (personagem BEL, \\x07). Programas acionam essa opção ao concluir tarefas, encontrar erros ou para notificações. \"Som\" reproduz um sinal sonoro, \"Visual\" pisca a tela brevemente, \"Ambo\" faz ambos, \"Nenhum\" desativa os alertas do sino.", + "rightClickSelectsWord": "Clique com botão direito seleciona Palavra", + "rightClickSelectsWordDesc": "Clique direito seleciona a palavra sob cursor", + "fastScrollModifier": "Modificador de rolagem rápido", + "selectModifier": "Selecionar modificador", + "modifierAlt": "Alt", + "modifierCtrl": "Ctrl", + "modifierShift": "Turno", + "fastScrollModifierDesc": "Tecla modificadora para rolagem rápida", + "fastScrollSensitivity": "Sensibilidade de rolagem rápida", + "fastScrollSensitivityValue": "Sensibilidade rápida de rolagem: {{value}}", + "fastScrollSensitivityDesc": "Multiplicador de velocidade de rolagem quando o modificador é mantido", + "minimumContrastRatio": "Relação Mínima de Contraste", + "minimumContrastRatioValue": "Taxa Mínima de Contraste: {{value}}", + "minimumContrastRatioDesc": "Ajustar cores automaticamente para melhor legibilidade", + "sshAgentForwarding": "Encaminhamento de agente SSH", + "sshAgentForwardingDesc": "Encaminhar agente de autenticação SSH para host remoto", + "backspaceMode": "Modo Backspace", + "selectBackspaceMode": "Selecionar modo backspace", + "backspaceModeNormal": "Normal (DEL)", + "backspaceModeControlH": "Control-H (^H)", + "backspaceModeDesc": "Comportamento da chave Backspace para compatibilidade", + "startupSnippet": "Trecho de Inicialização", + "selectSnippet": "Selecionar snippet", + "searchSnippets": "Buscar snippets...", + "snippetNone": "Nenhuma", + "noneAuthTitle": "Autenticação interativa", + "noneAuthDescription": "Este método de autenticação usará a autenticação interativa de teclado ao conectar ao servidor SSH.", + "noneAuthDetails": "A autenticação interativa permite que o servidor lhe peça credenciais durante a conexão. Isso é útil para servidores que requerem autenticação multi-fatores ou você não quer salvar as credenciais localmente.", + "forceKeyboardInteractive": "Forçar teclado interativo", + "forceKeyboardInteractiveDesc": "Força o uso de autenticação interativa de teclado. Isso às vezes é necessário para servidores que usam a Autenticação de Dois Fatores (TOTP/2FA).", + "overrideCredentialUsername": "Substituir o nome de usuário credencial", + "overrideCredentialUsernameDesc": "Use um nome de usuário diferente do armazenado nas credenciais. Isso permite que você use a mesma credencial com nomes de usuário diferentes.", + "jumpHosts": "Hosts de Salto", + "jumpHostsDescription": "Pular hosts (também conhecidos como hosts) permitem que você conecte a um servidor alvo através de um ou mais servidores intermediários. Isso é útil para acessar servidores atrás de firewalls ou em redes privadas.", + "jumpHostChain": "Corrente de Host Salto", + "addJumpHost": "Adicionar Jump Host", + "selectServer": "Selecionar servidor", + "searchServers": "Pesquisar servidores...", + "noServerFound": "Nenhum servidor encontrado", + "jumpHostsOrder": "Conexões serão feitas em ordem: Saltar Host 1 → Jump Host 2 → ... → Servidor de destino", + "socks5Proxy": "Proxy SOCKS5", + "socks5Description": "Configurar o proxy SOCKS5 para conexão SSH. Todo o tráfego será encaminhado através do servidor proxy especificado.", + "enableSocks5": "Habilitar SOCKS5 Proxy", + "enableSocks5Description": "Use o proxy SOCKS5 para esta conexão SSH", + "socks5Host": "Servidor de Proxy", + "socks5Port": "Porta do Proxy", + "socks5Username": "Usuário do Proxy", + "socks5Password": "Senha do Proxy", + "socks5UsernameOptional": "Opcional: deixe em branco se o proxy não requer autenticação", + "socks5PasswordOptional": "Opcional: deixe em branco se o proxy não requer autenticação", + "socks5ProxyChain": "Corrente de Proxy", + "socks5ProxyChainDescription": "Configure uma cadeia de proxies SOCKS. Cada proxy na cadeia se conectará através do anterior.", + "socks5ProxyMode": "Modo Proxy", + "socks5UseSingleProxy": "Usar um Proxy Único", + "socks5UseProxyChain": "Usar Proxy Chain", + "socks5UsePreset": "Usar Predefinição Salva", + "socks5SelectPreset": "Selecione a predefinição", + "socks5ManagePresets": "Gerenciar predefinições", + "socks5ProxyNode": "Proxy {{number}}", + "socks5AddProxy": "Adicionar Proxy para Cadeia", + "socks5RemoveProxy": "Remover Proxy", + "socks5ProxyType": "Tipo de proxy", + "socks5SaveAsPreset": "Salvar como Predefinição", + "socks5SavePresetTitle": "Salvar cadeia de proxy como predefinição", + "socks5SavePresetDescription": "Salvar a configuração do processo de proxy atual como uma predefinição reutilizável", + "socks5PresetName": "Nome da predefinição", + "socks5PresetDescription": "Descrição (opcional)", + "socks5PresetCreated": "Predefinição de cadeia de proxy criada", + "socks5PresetUpdated": "Predefinição de cadeia de proxy atualizada", + "socks5PresetDeleted": "Predefinição de cadeia de proxy excluída", + "socks5PresetSaved": "Predefinição \"{{name}}\" salva com sucesso", + "socks5PresetSaveError": "Falha ao salvar predefinição", + "socks5PresetNameRequired": "Nome da predefinição é necessário", + "socks5EmptyChainError": "Não pode salvar uma cadeia de proxy vazia", + "socks5ProxyChainEmpty": "Adicionar pelo menos um proxy para a cadeia", + "socks5HostDescription": "Nome do host ou endereço IP do servidor proxy SOCKS", + "socks5PortDescription": "Número da porta do servidor proxy SOCKS (padrão: 1080)", + "addProxyNode": "Adicionar nó proxy", + "noProxyNodes": "Nenhum nó proxy configurado. Clique em 'Adicionar nó de proxy' para adicionar um.", + "proxyNode": "Nó Proxy", + "proxyType": "Tipo de proxy", + "quickActions": "Ações rápidas", + "quickActionsDescription": "Ações rápidas permitem que você crie botões personalizados que executam snippets SSH neste servidor. Esses botões aparecerão no topo da página de estatísticas do servidor para acesso rápido.", + "quickActionsList": "Lista de Ações Rápidas", + "addQuickAction": "Adicionar ação rápida", + "quickActionName": "Nome da ação", + "noSnippetFound": "Nenhum snippet encontrado", + "quickActionsOrder": "Os botões de ação rápida aparecerão na ordem listada acima na página de estatísticas do servidor", + "sidebarCustomization": "Personalização do botão barra lateral", + "sidebarCustomizationDesc": "Escolha quais ações aparecem como botões rápidos na barra lateral. As ações não aparecem como botões irão aparecer no menu suspenso.", + "showTerminalInSidebar": "Mostrar Botão do Terminal", + "showTerminalInSidebarDesc": "Exibir o terminal como um botão rápido na barra lateral", + "showFileManagerInSidebar": "Mostrar botão do Gerenciador de Arquivos", + "showFileManagerInSidebarDesc": "Exibir o gerenciador de arquivos como um botão rápido na barra lateral", + "showTunnelInSidebar": "Mostrar Botão do Túnel", + "showTunnelInSidebarDesc": "Exibir o gerenciamento do túnel como um botão rápido na barra lateral", + "showDockerInSidebar": "Mostrar Botão Docker", + "showDockerInSidebarDesc": "Exibir o gerenciamento de docker como um botão rápido na barra lateral", + "showServerStatsInSidebar": "Mostrar botão de estatísticas do servidor", + "showServerStatsInSidebarDesc": "Exibir as estatísticas do servidor como um botão rápido na barra lateral", + "atLeastOneActionRequired": "Pelo menos uma ação ativada deve ser mostrada na barra lateral", + "advancedAuthSettings": "Configurações avançadas de autenticação", + "sudoPasswordAutoFill": "Auto-preenchimento de Senha Sudo", + "sudoPasswordAutoFillDesc": "Oferecer automaticamente a senha de SSH quando sudo solicitar senha", + "sudoPassword": "Senha Sudo", + "sudoPasswordDesc": "Senha opcional para comandos sudo (útil com autenticação de chave)", + "socks4": "SOCKS4", + "socks5": "SOCKS5", + "executeSnippetOnConnect": "Executar um snippet quando o terminal estiver conectado", + "autoMosh": "Auto-MOSH", + "autoMoshDesc": "Executar o comando MOSH automaticamente ao conectar", + "moshCommand": "Comando MOSH", + "moshCommandDesc": "O comando MOSH para executar", + "environmentVariables": "Variáveis de Ambiente", + "environmentVariablesDesc": "Definir variáveis de ambiente personalizadas para a sessão do terminal", + "variableName": "Nome da variável", + "variableValue": "Valor", + "addVariable": "Adicionar Variável", + "docker": "Atracador", + "openDocker": "Abrir Docker", + "copyFullScreenUrl": "Copiar URL de Tela Inteira", + "fullScreenUrlCopied": "URL de tela cheia copiado para a área de transferência", + "failedToCopyUrl": "Falha ao copiar URL para área de transferência", + "copyTerminalUrl": "Copiar URL do Terminal", + "copyFileManagerUrl": "Copiar URL do Gerenciador de Arquivos", + "copyTunnelUrl": "Copiar URL do túnel", + "copyServerStatsUrl": "Copiar URL das Estatísticas do Servidor", + "copyDockerUrl": "Copiar URL do Docker", + "fullScreenUrlTooltip": "Copiar URL para abrir este app em tela cheia", + "notEnabled": "O Docker não está habilitado para este host. Habilite nas configurações de Host para usar os recursos Docker.", + "validating": "Validando o Docker...", + "error": "ERRO", + "errorCode": "Código de erro: {{code}}", + "version": "Docker v{{version}}", + "current": "Atual", + "used_limit": "Usado / Limite", + "percentage": "Porcentagem", + "input": "Input", + "output": "Saída", + "read": "Lido", + "write": "Salvar", + "pids": "PIDs", + "id": "ID", + "state": "Estado:", + "console": "Console", + "containerMustBeRunning": "O contêiner deve estar em execução para conectar ao console", + "authenticationRequired": "Autenticação requerida", + "connectedTo": "Conectado a {{containerName}}", + "disconnected": "Desconectado", + "consoleError": "Erro de console", + "errorMessage": "Erro: {{message}}", + "failedToConnect": "Falha ao conectar ao console", + "disconnectedFromContainer": "Desconectado do container console.", + "containerNotRunning": "O contêiner não está em execução", + "startContainerToAccess": "Iniciar o contêiner para acessar o console", + "selectShell": "Selecionar shell", + "bash": "Pancada", + "sh": "Sh", + "ash": "Cinzas", + "connecting": "Conectandochar@@0", + "connect": "Conectar", + "disconnect": "Desconectar", + "notConnected": "Não conectado", + "clickToConnect": "Clique em Conectar para iniciar um shell interativo", + "connectingTo": "Conectando a {{containerName}}...", + "containerMustBeRunningToViewStats": "O contêiner deve estar em execução para ver estatísticas", + "failedToFetchStats": "Falha ao buscar estatísticas", + "noContainersFound": "Nenhum contêiner encontrado", + "noContainersFoundHint": "Comece criando contêineres em seu servidor", + "searchPlaceholder": "Pesquisar por nome, imagem ou ID...", + "filterByStatusPlaceholder": "Filtrar por status", + "allContainersCount": "Todos ({{count}})", + "statusCount": "{{status}} ({{count}})", + "noContainersMatchFilters": "Nenhum contêiner corresponde aos seus filtros", + "noContainersMatchFiltersHint": "Tente ajustar sua pesquisa ou filtro", + "containerStarted": "Contêiner {{name}} iniciado", + "failedToStartContainer": "Falha ao iniciar o container: {{error}}", + "containerStopped": "O contêiner {{name}} parou", + "failedToStopContainer": "Falha ao interromper contêiner: {{error}}", + "containerRestarted": "Contêiner {{name}} reiniciado", + "failedToRestartContainer": "Falha ao reiniciar o container: {{error}}", + "containerUnpaused": "Recipiente {{name}} despausado", + "containerPaused": "Recipiente {{name}} pausado", + "failedToTogglePauseContainer": "Falha ao contêiner {{action}} : {{error}}", + "containerRemoved": "{{name}} do contêiner removido", + "failedToRemoveContainer": "Falha ao remover container: {{error}}", + "image": "Imagem:", + "idLabel": "ID:", + "ports": "Portos:", + "noPorts": "Nenhuma", + "created": "Criado:", + "start": "Iniciar", + "stop": "Interromper", + "unpause": "Unpause", + "pause": "Suspender", + "restart": "Reiniciar", + "removeContainer": "Remover Contêiner", + "confirmRemoveContainer": "Tem certeza de que deseja remover o contêiner \"{{name}}\"?", + "runningContainerWarning": "Aviso: Este contêiner está atualmente em execução e será forçado a remover.", + "removing": "Removendo:", + "containerNotFound": "Contêiner não encontrado", + "backToList": "Voltar para a lista", + "logs": "Registros", + "stats": "Estatísticas", + "consoleTab": "Console", + "failedToFetchLogs": "Falha ao obter os registros: {{error}}", + "failedToDownloadLogs": "Falha ao baixar os logs: {{error}}", + "linesToShow": "Linhas para mostrar", + "last50Lines": "Últimas 50 linhas", + "last100Lines": "Últimas 100 linhas", + "last500Lines": "Últimas 500 linhas", + "last1000Lines": "Últimas 1000 linhas", + "allLogs": "Todos os logs", + "showTimestamps": "Mostrar Horários", + "autoRefresh": "Atualização Automática", + "filterLogsPlaceholder": "Filtrar registros...", + "noLogsAvailable": "Não há registros disponíveis" + }, + "terminal": { + "title": "Terminal", + "connect": "Conectar ao Host", + "disconnect": "Desconectar", + "clear": "Limpar", + "copy": "copiar", + "paste": "Colar", + "find": "Localizar", + "fullscreen": "Tela cheia", + "splitHorizontal": "Divisão Horizontal", + "splitVertical": "Divisão Vertical", + "closePanel": "Fechar Painel", + "reconnect": "Reconectar", + "sessionEnded": "Sessão Encerrada", + "connectionLost": "Conexão Perdida", + "error": "ERRO: {{message}}", + "disconnected": "Desconectado", + "connectionClosed": "Conexão fechada", + "connectionError": "Erro de conexão: {{message}}", + "connected": "Conectado", + "sshConnected": "Conexão SSH estabelecida", + "authError": "Falha na autenticação: {{message}}", + "unknownError": "Ocorreu um erro desconhecido", + "messageParseError": "Falha ao analisar mensagem do servidor", + "websocketError": "Erro de conexão WebSocket", + "connecting": "Conectandochar@@0", + "reconnecting": "Reconectando... ({{attempt}}/{{max}})", + "reconnected": "Reconectado com sucesso", + "maxReconnectAttemptsReached": "Máximo de tentativas de reconexão alcançadas", + "connectionTimeout": "Conexão expirada", + "terminalTitle": "Terminal - {{host}}", + "terminalWithPath": "Terminal - {{host}}:{{path}}", + "runTitle": "Executando {{command}} - {{host}}", + "totpRequired": "Autenticação dupla requerida", + "totpCodeLabel": "Código de verificação", + "totpPlaceholder": "000000", + "totpVerify": "Verificar", + "warpgateAuthRequired": "Autenticação Warpgate Necessária", + "warpgateSecurityKey": "Chave de Segurança", + "warpgateAuthUrl": "URL de autenticação", + "warpgateOpenBrowser": "Abrir no Navegador", + "warpgateContinue": "Eu concluí a autenticação", + "warpgateTimeout": "A autenticação do Warpgate expirou. Por favor reconecte-se.", + "sudoPasswordPopupTitle": "Inserir senha?", + "sudoPasswordPopupHint": "Pressione Enter para inserir, Esc para dispensar", + "sudoPasswordPopupConfirm": "Insert", + "sudoPasswordPopupDismiss": "Descartar", + "websocketAbnormalClose": "A conexão foi fechada inesperadamente. Isso pode ser devido a um problema de configuração do proxy reverso ou SSL. Por favor, verifique os logs do servidor.", + "totpInvalid": "Código inválido. Por favor, tente novamente.", + "authMethodFallback": "A autenticação {{method}} falhou. Tentando método alternativo...", + "authenticationRequired": "Autenticação requerida", + "connectionLogTitle": "Registro de conexão", + "connectionLogCopy": "Copiar logs para área de transferência", + "connectionLogClear": "Limpar registros", + "connectionLogEmpty": "Ainda não há registros de conexão", + "connectionLogConnecting": "Conectando ao host...", + "connectionLogCopied": "Logs de conexão copiados para área de transferência", + "connectionLogCopyFailed": "Falha ao copiar os logs para a área de transferência", + "allAuthMethodsFailed": "Todos os métodos de autenticação falharam. Por favor, verifique suas credenciais.", + "methodNotSupported": "Servidor não suporta autenticação {{method}} . Métodos disponíveis: {{available}}", + "retryWithMethod": "Tente novamente com {{method}}", + "automaticFallback": "Experimentando automaticamente autenticação {{method}}...", + "totpTimeout": "A verificação TOTP expirou. Por favor, reconecte-se.", + "passwordTimeout": "Verificação de senha expirada. Por favor, reconecte-se.", + "connectionRejected": "Conexão rejeitada pelo servidor. Verifique sua autenticação e configuração de rede." + }, + "fileManager": { + "title": "Gerenciador de Arquivos", + "file": "Arquivo", + "folder": "pasta", + "connectToSsh": "Conecte-se a SSH para usar arquivos de operação", + "uploadFile": "Enviar Arquivo", + "downloadFile": "BAIXAR", + "extractArchive": "Extrair arquivo", + "extractingArchive": "Extraindo {{name}}...", + "archiveExtractedSuccessfully": "{{name}} extraído com sucesso", + "extractFailed": "Falha ao extrair", + "compressFile": "Compactar arquivo", + "compressFiles": "Compactar arquivos", + "compressFilesDesc": "Comprimir itens {{count}} em um arquivo", + "archiveName": "Nome do Arquivo", + "enterArchiveName": "Informe o nome do arquivo...", + "compressionFormat": "Formato de compressão", + "selectedFiles": "Arquivos selecionados", + "andMoreFiles": "e mais {{count}}...", + "compress": "Compactar", + "compressingFiles": "Comprimindo {{count}} itens em {{name}}...", + "filesCompressedSuccessfully": "{{name}} criado com sucesso", + "compressFailed": "Compressão falhou", + "edit": "Alterar", + "preview": "Pré-visualizar", + "previous": "Anterior", + "next": "Próximo", + "pageXOfY": "Página {{current}} de {{total}}", + "zoomOut": "Diminuir o zoom", + "zoomIn": "Aumentar zoom", + "newFile": "Novo arquivo", + "newFolder": "Adicionar uma pasta", + "rename": "Renomear", + "renameItem": "Renomear Item", + "deleteItem": "Excluir Item", + "currentPath": "Caminho Atual", + "uploadFileTitle": "Enviar Arquivo", + "maxFileSize": "Máx: 1GB (JSON) / 5GB (Binary) - Suportados arquivos grandes", + "removeFile": "Remover Arquivo", + "clickToSelectFile": "Clique para selecionar um arquivo", + "chooseFile": "Selecionar Arquivo", + "uploading": "Enviando...", + "downloading": "Baixando...", + "uploadingFile": "Enviando {{name}}...", + "uploadingLargeFile": "A carregar ficheiro grande {{name}} ({{size}})...", + "downloadingFile": "Baixando {{name}}...", + "creatingFile": "Criando {{name}}...", + "creatingFolder": "Criando {{name}}...", + "deletingItem": "Excluindo {{type}} {{name}}...", + "renamingItem": "Renomeando {{type}} {{oldName}} para {{newName}}...", + "createNewFile": "Criar novo arquivo", + "fileName": "Nome do arquivo", + "creating": "Criando...", + "createFile": "Criar arquivo", + "createNewFolder": "Criar Nova Pasta", + "folderName": "Nome da pasta", + "createFolder": "Criar pasta", + "warningCannotUndo": "Aviso: Esta ação não pode ser desfeita", + "itemPath": "Caminho do item", + "thisIsDirectory": "Este é um diretório (excluirá recursivamente)", + "deleting": "Excluindo...", + "currentPathLabel": "Caminho Atual", + "newName": "Novo Nome", + "thisIsDirectoryRename": "Este é um diretório", + "renaming": "Renomeando...", + "fileUploadedSuccessfully": "Arquivo \"{{name}}\" carregado com sucesso", + "failedToUploadFile": "Falha ao carregar arquivo", + "fileDownloadedSuccessfully": "Arquivo \"{{name}}\" baixado com sucesso", + "failedToDownloadFile": "Não foi possível baixar o arquivo", + "noFileContent": "Nenhum conteúdo recebido", + "filePath": "Caminho do Arquivo", + "fileCreatedSuccessfully": "Arquivo \"{{name}}\" criado com sucesso", + "failedToCreateFile": "Falha ao criar arquivo", + "folderCreatedSuccessfully": "Pasta \"{{name}}\" criada com sucesso", + "failedToCreateFolder": "Falha ao criar pasta", + "failedToCreateItem": "Falha ao criar item", + "operationFailed": "A operação {{operation}} falhou para {{name}}: {{error}}", + "failedToResolveSymlink": "Falha ao resolver link simbólico", + "itemDeletedSuccessfully": "{{type}} excluído com sucesso", + "itemsDeletedSuccessfully": "Itens {{count}} excluídos com sucesso", + "failedToDeleteItems": "Falha ao excluir itens", + "sudoPasswordRequired": "Senha do Administrador Necessária", + "enterSudoPassword": "Digite a senha sudo para continuar esta operação", + "sudoPassword": "Senha sudo", + "sudoOperationFailed": "Operação sudo falhou", + "sudoAuthFailed": "Falha na autenticação Sudo", + "deleteOperation": "Excluir arquivos/pastas", + "dragFilesToUpload": "Solte os arquivos aqui para enviar", + "emptyFolder": "Esta pasta está vazia", + "itemCount": "Itens de {{count}}", + "selectedCount": "{{count}} selecionado", + "searchFiles": "Pesquisar arquivos...", + "upload": "Transferir", + "selectHostToStart": "Selecione um host para iniciar o gerenciamento de arquivos", + "failedToConnect": "Falha ao conectar com SSH", + "failedToLoadDirectory": "Falha ao carregar diretório", + "noSSHConnection": "Nenhuma conexão SSH disponível", + "enterFolderName": "Inserir nome da pasta:", + "enterFileName": "Inserir nome do arquivo:", + "copy": "copiar", + "cut": "Recortar", + "paste": "Colar", + "copyPath": "Copiar Caminho", + "copyPaths": "Copiar caminhos", + "delete": "excluir", + "properties": "propriedades", + "refresh": "atualizar", + "downloadFiles": "Baixar arquivos {{count}} para o Navegador", + "copyFiles": "Copiar itens {{count}}", + "cutFiles": "Cortar {{count}} itens", + "deleteFiles": "Excluir {{count}} itens", + "filesCopiedToClipboard": "{{count}} itens copiados para a área de transferência", + "filesCutToClipboard": "{{count}} itens cortados na área de transferência", + "pathCopiedToClipboard": "Caminho copiado para área de transferência", + "pathsCopiedToClipboard": "{{count}} caminhos copiados para a área de transferência", + "failedToCopyPath": "Falha ao copiar caminho para área de transferência", + "movedItems": "Itens de {{count}} movidos", + "failedToDeleteItem": "Falha ao excluir item", + "itemRenamedSuccessfully": "{{type}} renomeado com sucesso", + "failedToRenameItem": "Falha ao renomear item", + "download": "BAIXAR", + "permissions": "Permissões", + "size": "Tamanho", + "modified": "Modificado", + "path": "Caminho", + "confirmDelete": "Tem certeza que deseja excluir {{name}}?", + "uploadSuccess": "Arquivo enviado com sucesso", + "uploadFailed": "Falha ao enviar arquivo", + "downloadSuccess": "Arquivo baixado com sucesso", + "downloadFailed": "Download de arquivo falhou", + "permissionDenied": "Permissão negada", + "checkDockerLogs": "Verifique os logs do Docker para informações detalhadas sobre o erro", + "internalServerError": "Ocorreu um erro interno do servidor", + "serverError": "Erro no Servidor", + "error": "ERRO", + "requestFailed": "Falha na solicitação com código de status", + "unknownFileError": "desconhecido", + "cannotReadFile": "Impossível ler arquivo", + "noSshSessionId": "Nenhum ID de sessão SSH disponível", + "noFilePath": "Nenhum caminho disponível", + "noCurrentHost": "Nenhum host atual disponível", + "fileSavedSuccessfully": "Arquivo salvo com sucesso", + "saveTimeout": "A operação de salvamento expirou. O arquivo pode ter sido salvo com sucesso, mas a operação demorou muito para ser concluída. Verifique os logs do Docker para confirmação.", + "failedToSaveFile": "Falha ao salvar arquivo", + "deletedSuccessfully": "excluído com sucesso", + "connectToServer": "Conectar a um servidor", + "selectServerToEdit": "Selecione um servidor na barra lateral para começar a editar arquivos", + "fileOperations": "Operações de arquivo", + "confirmDeleteMessage": "Tem certeza que deseja excluir {{name}}?", + "confirmDeleteSingleItem": "Tem certeza de que quer apagar permanentemente \"{{name}}\"?", + "confirmDeleteMultipleItems": "Tem certeza de que deseja excluir permanentemente itens {{count}}?", + "confirmDeleteMultipleItemsWithFolders": "Tem certeza de que deseja excluir permanentemente itens {{count}} ? Isso inclui pastas e seu conteúdo.", + "confirmDeleteFolder": "Tem a certeza de que pretende eliminar permanentemente a pasta \"{{name}}\" e todo o seu conteúdo?", + "deleteDirectoryWarning": "Isto irá apagar a pasta e todo o seu conteúdo.", + "actionCannotBeUndone": "Esta ação não pode ser desfeita.", + "permanentDeleteWarning": "Esta ação não pode ser desfeita. O(s) item(ns) serão excluídos permanentemente do servidor.", + "recent": "Recente", + "pinned": "Fixado", + "folderShortcuts": "Atalhos da pasta", + "noRecentFiles": "Nenhum arquivo recente.", + "noPinnedFiles": "Nenhum arquivo fixado.", + "enterFolderPath": "Insira o caminho da pasta", + "noShortcuts": "Sem atalhos.", + "searchFilesAndFolders": "Pesquisar arquivos e pastas...", + "noFilesOrFoldersFound": "Nenhum arquivo ou pasta encontrado.", + "failedToConnectSSH": "Falha ao conectar com SSH", + "failedToReconnectSSH": "Falha ao reconectar a sessão SSH", + "failedToListFiles": "Falha ao listar arquivos", + "fetchHomeDataTimeout": "Obter dados do home timeout excedido", + "sshStatusCheckTimeout": "A verificação de status SSH expirou", + "sshReconnectionTimeout": "A reconexão SSH expirou", + "saveOperationTimeout": "Salve a operação expirou", + "cannotSaveFile": "Impossível salvar arquivo", + "dragSystemFilesToUpload": "Arraste arquivos de sistema aqui para fazer o upload", + "dragFilesToWindowToDownload": "Arraste os arquivos fora da janela para baixar", + "openTerminalHere": "Abrir Terminal Aqui", + "run": "Executar", + "saveToSystem": "Salvar como:", + "selectLocationToSave": "Selecione o local para salvar", + "openTerminalInFolder": "Abrir Terminal nesta Pasta", + "openTerminalInFileLocation": "Abrir Terminal no Local do Arquivo", + "terminalWithPath": "Terminal - {{host}}:{{path}}", + "runningFile": "Executando - {{file}}", + "onlyRunExecutableFiles": "Só é possível executar arquivos executáveis", + "noHostSelected": "Nenhum host selecionado", + "starred": "Marcado", + "shortcuts": "Atalhos", + "directories": "Diretórios", + "removedFromRecentFiles": "Removido \"{{name}}\" dos arquivos recentes", + "removeFailed": "Falha ao remover", + "unpinnedSuccessfully": "\"{{name}}desafixado\" com sucesso", + "unpinFailed": "Desafixar falhou", + "removedShortcut": "Atalho \"{{name}} removido \"", + "removeShortcutFailed": "Falha ao remover atalho", + "clearedAllRecentFiles": "Todos os arquivos recentes foram removidos", + "clearFailed": "Falha ao apagar", + "removeFromRecentFiles": "Remover dos arquivos recentes", + "clearAllRecentFiles": "Limpar todos os arquivos recentes", + "unpinFile": "Desafixar arquivo", + "removeShortcut": "Remover atalho", + "saveFilesToSystem": "Salvar arquivos {{count}} como...", + "pinFile": "Fixar arquivo", + "addToShortcuts": "Adicionar a atalhos", + "downloadToDefaultLocation": "Baixar para o local padrão", + "pasteFailed": "A colagem falhou", + "noUndoableActions": "Nenhuma ação irreversível", + "undoCopySuccess": "Operação de cópia desfeita: {{count}} excluídos arquivos copiados", + "undoCopyFailedDelete": "Desfazer falhou: Não foi possível excluir nenhum arquivo copiado", + "undoCopyFailedNoInfo": "Desfazer falhou: Não foi possível encontrar informações do arquivo copiado", + "undoMoveSuccess": "Operação movida desfeita: arquivos {{count}} movidos de volta para o local original", + "undoMoveFailedMove": "Desfazer falhou: Não foi possível mover nenhum arquivo de volta", + "undoMoveFailedNoInfo": "Desfazer falhou: Não foi possível encontrar informação de arquivo movido", + "undoDeleteNotSupported": "Operação de exclusão não pode ser desfeita: Os arquivos foram excluídos permanentemente do servidor", + "undoTypeNotSupported": "Tipo de operação desfazer não suportado", + "undoOperationFailed": "Falha na operação", + "unknownError": "Erro desconhecido", + "enterPath": "Digite o caminho...", + "editPath": "Editar caminho", + "confirm": "Confirmar", + "cancel": "cancelar", + "find": "Localizar...", + "replaceWith": "Substituir com...", + "replace": "Substituir", + "replaceAll": "Substituir todos", + "downloadInstead": "Em vez disso, baixar", + "keyboardShortcuts": "Atalhos do teclado", + "searchAndReplace": "Pesquisar e substituir", + "editing": "Editando", + "navigation": "Navigation", + "code": "Código", + "search": "Pesquisa", + "findNext": "Localizar Próximo", + "findPrevious": "Localizar Anterior", + "save": "Guardar", + "selectAll": "Selecionar Todos", + "undo": "Desfazer", + "redo": "Refazer", + "goToLine": "Ir para a Linha", + "moveLineUp": "Mover Linha para Cima", + "moveLineDown": "Mover Linha para Baixo", + "toggleComment": "Alternar comentário", + "indent": "Indentar", + "outdent": "Desdentar", + "autoComplete": "Auto Complete", + "imageLoadError": "Não foi possível carregar a imagem", + "rotate": "Rotacionar", + "originalSize": "Tamanho Original", + "startTyping": "Comece a digitar...", + "unknownSize": "Tamanho desconhecido", + "fileIsEmpty": "O arquivo está vazio", + "largeFileWarning": "Aviso de arquivo grande", + "largeFileWarningDesc": "Este arquivo tem o tamanho {{size}} , o que pode causar problemas de desempenho quando aberto como texto.", + "fileNotFoundAndRemoved": "O arquivo \"{{name}}\" não foi encontrado e foi removido dos arquivos recentes/fixados", + "failedToLoadFile": "Falha ao carregar arquivo: {{error}}", + "serverErrorOccurred": "Ocorreu um erro no servidor. Tente novamente mais tarde.", + "autoSaveFailed": "Auto-salvamento falhou", + "fileAutoSaved": "Arquivo salvo automaticamente", + "moveFileFailed": "Falha ao mover {{name}}", + "moveOperationFailed": "Falha ao mover", + "canOnlyCompareFiles": "Só é possível comparar dois arquivos", + "comparingFiles": "Comparando arquivos: {{file1}} e {{file2}}", + "dragFailed": "Falha ao arrastar", + "filePinnedSuccessfully": "Arquivo \"{{name}}\" fixado com sucesso", + "pinFileFailed": "Falha ao fixar arquivo", + "fileUnpinnedSuccessfully": "Arquivo \"{{name}}\" desafixado com sucesso", + "unpinFileFailed": "Falha ao desafixar arquivo", + "shortcutAddedSuccessfully": "Atalho para a pasta \"{{name}}\" adicionado com sucesso", + "addShortcutFailed": "Falha ao adicionar atalho", + "operationCompletedSuccessfully": "{{operation}} Itens {{count}} com sucesso", + "operationCompleted": "{{operation}} Itens em {{count}}", + "downloadFileSuccess": "Arquivo {{name}} baixado com sucesso", + "downloadFileFailed": "Download falhou", + "moveTo": "Mover para {{name}}", + "diffCompareWith": "Comparar diferenças com {{name}}", + "dragOutsideToDownload": "Arraste fora do janela para baixar (arquivos{{count}})", + "newFolderDefault": "Pasta", + "newFileDefault": "NewFile.txt", + "successfullyMovedItems": "Itens de {{count}} movidos com sucesso para {{target}}", + "move": "Mover-se", + "searchInFile": "Procurar no arquivo (Ctrl+F)", + "showKeyboardShortcuts": "Exibir atalhos de teclado", + "startWritingMarkdown": "Comece a escrever o seu conteúdo markdown...", + "loadingFileComparison": "Carregando comparação de arquivo...", + "reload": "Reload", + "compare": "Comparar", + "sideBySide": "Lado a lado", + "inline": "Embutido", + "fileComparison": "Comparação de arquivos: {{file1}} vs {{file2}}", + "fileTooLarge": "Arquivo muito grande: {{error}}", + "sshConnectionFailed": "Falha na conexão SSH. Por favor, verifique sua conexão com {{name}} ({{ip}}:{{port}})", + "loadFileFailed": "Falha ao carregar arquivo: {{error}}", + "connecting": "Conectandochar@@0", + "connectedSuccessfully": "Conectado com sucesso", + "totpVerificationFailed": "Falha na verificação TOTP", + "warpgateVerificationFailed": "Falha na autenticação do Warpgate", + "authenticationFailed": "Falha na autenticação", + "verificationCodePrompt": "Código de verificação:", + "changePermissions": "Alterar permissões", + "changePermissionsDesc": "Modificar permissões de arquivos para", + "currentPermissions": "Permissões Atuais", + "newPermissions": "Novas permissões", + "owner": "Proprietário", + "group": "grupo", + "others": "Outros", + "read": "Lido", + "write": "Salvar", + "execute": "Executar", + "permissionsChangedSuccessfully": "Permissões alteradas com sucesso", + "failedToChangePermissions": "Falha ao alterar permissões" + }, + "tunnel": { + "log": { + "connecting": "Estabelecendo conexão de túnel...", + "sourceConnecting": "Conectando ao host de origem {{host}}...", + "sourceConnected": "Conectado ao host de origem", + "endpointResolving": "Resolvendo host do ponto final {{host}}...", + "endpointResolved": "Servidor de Endpoint resolvido", + "forwarding": "Configurando o encaminhamento de porta {{localPort}} → {{remotePort}}...", + "connected": "Túnel criado com sucesso", + "retrying": "Falha na conexão, tentando novamente em {{countdown}}s (tentativa {{attempt}}/{{max}})...", + "retryExhausted": "Máximo de tentativas de repetição atingido", + "error": "Falha na conexão de túnel: {{error}}" + } + }, + "tunnels": { + "title": "Túneis SSH", + "noSshTunnels": "Sem Túneis SSH", + "createFirstTunnelMessage": "Você ainda não criou nenhum túnel SSH. Configure as conexões de túnel no Gerenciador de Host para começar.", + "connected": "Conectado", + "disconnected": "Desconectado", + "connecting": "Conectandochar@@0", + "disconnecting": "Desconectando...", + "unknownTunnelStatus": "Desconhecido", + "statusUnknown": "Desconhecido", + "unknown": "Desconhecido", + "error": "ERRO", + "failed": "Falhou", + "retrying": "Retentando", + "waiting": "Esperando", + "waitingForRetry": "Aguardando nova tentativa", + "retryingConnection": "Tentando conexão novamente", + "canceling": "Cancelando...", + "connect": "Conectar", + "disconnect": "Desconectar", + "cancel": "cancelar", + "port": "Porta", + "attempt": "Tentativa {{current}} de {{max}}", + "nextRetryIn": "Próxima repetição em {{seconds}} segundos", + "checkDockerLogs": "Verifique os seus logs do Docker pelo motivo de erro, junte-se ao", + "orCreate": "ou criar uma ", + "noTunnelConnections": "Nenhuma conexão de túnel configurada", + "tunnelConnections": "Conexões de túnel", + "addTunnel": "Adicionar túnel", + "editTunnel": "Editar túnel", + "deleteTunnel": "Excluir túnel", + "tunnelName": "Nome do túnel", + "localPort": "Porta local", + "remoteHost": "Host Remoto", + "remotePort": "Porta remota", + "autoStart": "Início automático", + "status": "SItuação", + "active": "ativo", + "inactive": "Inativo", + "start": "Iniciar", + "stop": "Interromper", + "restart": "Reiniciar", + "connectionType": "Tipo de conexão", + "local": "Localização", + "remote": "Remoto", + "dynamic": "Dinâmico", + "unknownConnectionStatus": "Desconhecido", + "portMapping": "Porta {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", + "endpointHostNotFound": "Servidor de Endpoint não encontrado", + "discord": "Discord", + "githubIssue": "Problema no GitHub", + "forHelp": "para obter ajuda" + }, + "serverStats": { + "title": "Estatísticas do Servidor", + "cpu": "cpu", + "memory": "Memória", + "disk": "Disco", + "network": "Rede", + "uptime": "Tempo em atividade", + "loadAverage": "Média: {{avg1}}, {{avg5}}, {{avg15}}", + "processes": "processos", + "connections": "Conexões", + "usage": "Utilização", + "available": "Disponível", + "total": "Total:", + "free": "Gratuito", + "used": "Utilizado", + "percentage": "Porcentagem", + "refreshStatusAndMetrics": "Atualizar status e métricas", + "refreshStatus": "Atualizar status", + "fileManagerAlreadyOpen": "Gerenciador de arquivos já aberto para este host", + "openFileManager": "Abrir Gerenciador de Arquivos", + "connecting": "Conectandochar@@0", + "connectionFailed": "Falha ao conectar ao servidor", + "cpuCores_one": "CPU {{count}}", + "cpuCores_other": "{{count}} CPUs", + "naCpus": "N/A CPU(s)", + "loadAverageNA": "Média: N/A", + "cpuUsage": "Uso da CPU", + "memoryUsage": "Memória Utilizada", + "diskUsage": "Uso do disco", + "rootStorageSpace": "Espaço de armazenamento ROOT", + "of": "de", + "feedbackMessage": "Tem ideias para o que deve vir a seguir para o gerenciamento de servidores? Compartilhe em", + "failedToFetchHostConfig": "Falha ao buscar a configuração do host", + "failedToFetchStatus": "Falha ao obter o status do servidor", + "failedToFetchMetrics": "Falha ao buscar métricas do servidor", + "failedToFetchHomeData": "Falha ao obter dados de tela inicial", + "loadingMetrics": "Carregando métricas...", + "refreshing": "Atualizando...", + "serverOffline": "Servidor Offline", + "cannotFetchMetrics": "Não é possível buscar métricas do servidor offline", + "totpRequired": "Autenticação TOTP Necessária", + "totpUnavailable": "Estatísticas do Servidor indisponíveis para servidores TOTP habilitados", + "totpVerified": "Coleção de métricas TOTP verificada", + "totpFailed": "Falha na verificação TOTP", + "totpInvalidCode": "Código de verificação inválido", + "totpCancelled": "Coleção de métricas cancelada", + "authenticationFailed": "Falha na autenticação", + "noneAuthNotSupported": "As estatísticas do servidor não suportam tipo de autenticação 'nenhum'.", + "load": "Carregar", + "editLayout": "Editar Layout", + "cancelEdit": "cancelar", + "addWidget": "Adicionar Widget", + "saveLayout": "Gravar Layout", + "unsavedChanges": "Alterações não salvas", + "layoutSaved": "Layout salvo com sucesso", + "failedToSaveLayout": "Falha ao salvar layout", + "systemInfo": "Informação do Sistema", + "hostname": "Hostname", + "operatingSystem": "Sistema operacional", + "kernel": "Kernel", + "totalUptime": "Total em Feriado", + "seconds": "segundos", + "networkInterfaces": "Interfaces de Rede", + "noInterfacesFound": "Nenhuma interface de rede encontrada", + "totalProcesses": "Total de processos", + "running": "Executando", + "noProcessesFound": "Nenhum processo encontrado", + "loginStats": "Estatísticas de Login SSH", + "totalLogins": "Logins totais", + "uniqueIPs": "IPs únicos", + "recentSuccessfulLogins": "Logins recentes", + "recentFailedAttempts": "Tentativas Recentes", + "noRecentLoginData": "Nenhum dado de login recente", + "from": "De", + "quickActions": "Ações rápidas", + "executeQuickAction": "Executar {{name}}", + "executingQuickAction": "Executando {{name}}...", + "quickActionSuccess": "{{name}} completado com sucesso", + "quickActionFailed": "{{name}} falhou", + "quickActionError": "Falha ao executar {{name}}", + "ports": { + "title": "Porta de Escuta", + "protocol": "Protocol", + "port": "Porta", + "address": "Endereço", + "state": "Estado:", + "process": "processo", + "noData": "Sem dados de portas de escuta" + }, + "firewall": { + "title": "Firewall", + "active": "ativo", + "inactive": "Inativo", + "notDetected": "Não detectado", + "policy": "Política", + "rules": "regras", + "noRules": "Sem regras", + "noData": "Nenhum firewall disponível", + "action": "Acão", + "protocol": "Proto", + "port": "Porta", + "source": "fonte", + "accept": "ACEITAR", + "drop": "LARGAR", + "reject": "REJEITAR", + "anywhere": "Em qualquer lugar" + } + }, + "auth": { + "tagline": "GERENCIADOR DE SERVIDOR SSH", + "description": "Gerenciamento de conexão SSH seguro, poderoso e intuitivo", + "welcomeBack": "Bem-vindo de volta ao TERMIX", + "createAccount": "Criar sua conta TERMIX", + "continueExternal": "Continuar com o provedor externo", + "loginTitle": "Faça login para Termix", + "registerTitle": "Criar conta", + "loginButton": "Conectar-se", + "registerButton": "Cadastrar", + "forgotPassword": "Esqueceu a senha?", + "rememberMe": "Lembrar de mim", + "noAccount": "Não possui uma conta?", + "hasAccount": "Já possui uma conta?", + "loginSuccess": "Login bem-sucedido", + "loginFailed": "Falha no login", + "registerSuccess": "Registrado com sucesso", + "registerFailed": "Registro falhou", + "logoutSuccess": "Sessão encerrada com sucesso", + "invalidCredentials": "Usuário ou senha inválidos", + "accountCreated": "Conta criada com sucesso", + "passwordReset": "Link de redefinição de senha enviado", + "twoFactorAuth": "Autenticação dupla", + "enterCode": "Inserir código de verificação", + "backupCode": "Ou usar código de backup", + "verifyCode": "Verificar Código", + "redirectingToApp": "Redirecionando para o aplicativo...", + "enableTwoFactor": "Ativar autenticação dupla", + "disableTwoFactor": "Desativar autenticação dupla", + "scanQRCode": "Escaneie este código QR com o seu aplicativo de autenticação", + "backupCodes": "Códigos de recuperação", + "saveBackupCodes": "Salve esses códigos de backup em um local seguro", + "twoFactorEnabledSuccess": "Autenticação de dois fatores habilitada com sucesso!", + "twoFactorDisabled": "Autenticação de dois fatores desativada", + "newBackupCodesGenerated": "Novos códigos de backup gerados", + "backupCodesDownloaded": "Códigos de backup baixados", + "pleaseEnterSixDigitCode": "Por favor, insira um código de 6 dígitos", + "invalidVerificationCode": "Código de verificação inválido", + "failedToDisableTotp": "Falha ao desativar TOTP", + "failedToGenerateBackupCodes": "Falha ao gerar códigos de backup", + "enterPassword": "Digite sua senha", + "lockedOidcAuth": "Bloqueado (OIDC Auth)", + "twoFactorTitle": "Autenticação dupla", + "twoFactorProtected": "Sua conta está protegida com autenticação de dois fatores", + "twoFactorActive": "A autenticação de dois fatores está ativa em sua conta", + "disable2FA": "Desativar 2FA", + "disableTwoFactorWarning": "Desativar a autenticação de dois fatores tornará sua conta menos segura", + "passwordOrTotpCode": "Senha ou Código TOTP", + "or": "ou", + "generateNewBackupCodesText": "Gerar novos códigos de backup se você perdeu os já existentes", + "generateNewBackupCodes": "Gerar novos códigos de recuperação", + "yourBackupCodes": "Seus códigos de recuperação", + "download": "BAIXAR", + "setupTwoFactorTitle": "Configurar autenticação dupla", + "sshAuthenticationRequired": "Autenticação SSH necessária", + "sshNoKeyboardInteractive": "Autenticação Interativa do Keyboard-Indisponível", + "sshAuthenticationFailed": "Falha na autenticação", + "sshAuthenticationTimeout": "Timeout de autenticação", + "sshNoKeyboardInteractiveDescription": "O servidor não suporta autenticação interativa de teclado. Por favor, forneça sua senha ou chave SSH.", + "sshAuthFailedDescription": "As credenciais fornecidas estavam incorretas. Por favor, tente novamente com credenciais válidas.", + "sshTimeoutDescription": "A tentativa de autenticação expirou. Tente novamente.", + "sshProvideCredentialsDescription": "Por favor, forneça suas credenciais SSH para conectar a este servidor.", + "sshPasswordDescription": "Digite a senha para esta conexão SSH.", + "sshKeyPasswordDescription": "Se a sua chave SSH estiver criptografada, digite a senha aqui.", + "step1ScanQR": "Passo 1: Digitalize o código QR com o seu aplicativo de autenticador", + "manualEntryCode": "Código de Entrada Manual", + "cannotScanQRText": "Se você não puder escanear o código QR, insira este código manualmente no seu aplicativo autenticador", + "nextVerifyCode": "Próximo: Verificar código", + "verifyAuthenticator": "Verifique seu autenticador", + "step2EnterCode": "Passo 2: Insira o código de 6 dígitos do seu aplicativo autenticador", + "verificationCode": "Código de verificação", + "back": "Anterior", + "verifyAndEnable": "Verificar e Habilitar", + "saveBackupCodesTitle": "Salve seus códigos de recuperação", + "step3StoreCodesSecurely": "Etapa 3: Armazene esses códigos em um lugar seguro", + "importantBackupCodesText": "Salve esses códigos de backup em um local seguro. Você pode usá-los para acessar a sua conta se perder o seu dispositivo de autenticação.", + "completeSetup": "Configuração completa", + "notEnabledText": "A autenticação em duas etapas adiciona uma camada extra de segurança, exigindo um código do seu aplicativo de autenticação ao entrar.", + "enableTwoFactorButton": "Ativar autenticação dupla", + "addExtraSecurityLayer": "Adicionar uma camada extra de segurança à sua conta", + "firstUser": "Primeiro usuário", + "firstUserMessage": "Você é o primeiro usuário e será feito um administrador. Você pode ver as configurações de administrador na lista de usuários da barra lateral. Se você acha que isso é um erro, verifique os logs do docker ou crie uma questão no GitHub.", + "external": "externo", + "loginWithExternal": "Entrar com o provedor externo", + "loginWithExternalDesc": "Fazer login usando seu provedor de identidade externo configurado", + "externalNotSupportedInElectron": "A autenticação externa ainda não é suportada no aplicativo Electron. Por favor, use a versão web para logar OIDC.", + "resetPasswordButton": "Redefinir a senha", + "sendResetCode": "Enviar Código de Redefinição", + "resetCodeDesc": "Digite seu nome de usuário para receber um código de redefinição de senha. O código será logado nos logs do contêiner docker.", + "resetCode": "Reset Code", + "verifyCodeButton": "Verificar Código", + "enterResetCode": "Insira o código de 6 dígitos do contêiner docker para o usuário:", + "goToLogin": "Ir para o Login", + "newPassword": "Nova Palavra-Passe", + "confirmNewPassword": "Confirmar senha", + "enterNewPassword": "Digite sua nova senha para o usuário:", + "signUp": "Criar conta", + "mobileApp": "Aplicativo Móvel", + "loggingInToMobileApp": "Fazendo login no aplicativo móvel", + "desktopApp": "Aplicativo para computador", + "loggingInToDesktopApp": "Fazendo login no aplicativo para computador", + "loggingInToDesktopAppViaWeb": "Logando na área de trabalho através de interface web", + "loadingServer": "Carregando servidor...", + "authenticating": "Autenticando...", + "dataLossWarning": "Redefinir sua senha desta forma irá apagar todos os seus hosts, credenciais e outros dados criptografados salvos por SSH. Essa ação não pode ser desfeita. Apenas use isso se você esqueceu sua senha e não está logado.", + "authenticationDisabled": "Autenticação desabilitada", + "authenticationDisabledDesc": "Todos os métodos de autenticação estão desativados no momento. Entre em contato com o administrador.", + "passwordResetSuccess": "Senha redefinida com sucesso", + "passwordResetSuccessDesc": "Sua senha foi redefinida com sucesso. Você agora pode entrar com sua nova senha." + }, + "errors": { + "notFound": "Página não encontrada", + "unauthorized": "Acesso não autorizado", + "forbidden": "Acesso proibido", + "serverError": "Erro no servidor", + "networkError": "Erro de rede", + "databaseConnection": "Não foi possível conectar ao banco de dados", + "unknownError": "Erro desconhecido", + "loginFailed": "Falha no login", + "failedPasswordReset": "Falha ao iniciar a redefinição de senha", + "failedVerifyCode": "Falha ao verificar código de redefinição", + "failedCompleteReset": "Falha ao concluir a redefinição de senha", + "invalidTotpCode": "Invalid TOTP code", + "failedOidcLogin": "Falha ao iniciar o login OIDC", + "failedUserInfo": "Falha ao obter informações de usuário após login OIDC", + "oidcAuthFailed": "Falha na autenticação OIDC", + "noTokenReceived": "Nenhum token recebido do login", + "invalidAuthUrl": "URL de autorização inválida recebida do backend", + "invalidInput": "Invalid input", + "requiredField": "Este campo é obrigatório", + "minLength": "Tamanho mínimo de {{min}}", + "maxLength": "O comprimento máximo é {{max}}", + "invalidEmail": "Endereço de e-mail inválido", + "passwordMismatch": "As senhas não coincidem", + "passwordLoginDisabled": "Nome de usuário/senha está desativado no momento", + "weakPassword": "A senha é muito fraca", + "usernameExists": "Nome de usuário já existe", + "emailExists": "E-mail já existe", + "loadFailed": "Não foi possível carregar os dados", + "saveError": "Falha ao salvar", + "sessionExpired": "Sessão expirou - por favor faça o login novamente", + "totpRateLimited": "Taxa limitada: Muitas tentativas de verificação TOTP. Tente novamente mais tarde.", + "totpRateLimitedWithTime": "Taxa limitada: Muitas tentativas de verificação TOTP. Aguarde {{time}} segundos antes de tentar novamente.", + "resetCodeRateLimited": "Taxa limitada: Muitas tentativas de verificação. Por favor, tente novamente mais tarde.", + "resetCodeRateLimitedWithTime": "Taxa limitada: Muitas tentativas de verificação. Por favor, aguarde {{time}} segundos antes de tentar novamente." + }, + "messages": { + "saveSuccess": "Salvo com Sucesso", + "saveError": "Falha ao salvar", + "deleteSuccess": "Excluído com sucesso", + "deleteError": "Falha ao apagar", + "updateSuccess": "Atualizado com sucesso", + "updateError": "Falha ao atualizar", + "copySuccess": "Copiado para o clipboard", + "copyError": "Falha ao copiar", + "copiedToClipboard": "{{item}} copiado para área de transferência", + "connectionEstablished": "Conexão estabelecida", + "connectionClosed": "Conexão fechada", + "reconnecting": "Reconectando...", + "processing": "Processandochar@@0", + "pleaseWait": "Por favor, aguarde...", + "registrationDisabled": "O registro da nova conta está desativado por um administrador. Por favor, entre em contato com um administrador.", + "databaseConnected": "Banco de dados conectado com sucesso", + "databaseConnectionFailed": "Falha ao conectar ao servidor do banco de dados", + "checkServerConnection": "Por favor, verifique sua conexão com o servidor e tente novamente", + "resetCodeSent": "Redefinir código enviado para os logs Docker", + "codeVerified": "Código verificado com sucesso", + "passwordResetSuccess": "Senha redefinida com sucesso", + "loginSuccess": "Login bem-sucedido", + "registrationSuccess": "Registrado com sucesso" + }, + "profile": { + "title": "Informações do Perfil", + "description": "Gerencie as configurações e segurança da sua conta", + "security": "Segurança", + "changePassword": "Mudar a senha", + "twoFactorAuth": "Autenticação dupla", + "accountInfo": "Informações da Conta", + "role": "Funções", + "admin": "Administrador", + "user": "Usuário", + "authMethod": "Método de autenticação", + "local": "Localização", + "external": "Externo (OIDC)", + "externalAndLocal": "Dupla Autenticação", + "selectPreferredLanguage": "Selecione o idioma de sua preferência para a interface", + "fileColorCoding": "Codificação de cores", + "fileColorCodingDesc": "Arquivos de código de cor por tipo: pastas (vermelho), arquivos (azul), links simbólicos (verde)", + "commandAutocomplete": "Auto-completar comando", + "commandAutocompleteDesc": "Ativar sugestões de autocompletar da tecla de aba para comandos de terminal com base no seu histórico de comandos", + "defaultSnippetFoldersCollapsed": "Recolher pastas de fragmentos por padrão", + "defaultSnippetFoldersCollapsedDesc": "Quando ativado, todas as pastas de snippet serão recolhidas quando você abrir a aba de snippets", + "terminalSyntaxHighlighting": "Realce de Sintaxe Terminal", + "showHostTags": "Exibir Tags do Host", + "showHostTagsDesc": "Exibir tags em cada host na barra lateral. Desative para ocultar todas as tags.", + "account": "conta", + "appearance": "Aparência", + "languageLocalization": "Idioma e localização", + "fileManagerSettings": "Gerenciador de Arquivos", + "terminalSettings": "Terminal", + "hostSidebarSettings": "Host e Barra Lateral", + "snippetsSettings": "Trechos", + "updateSettings": "Atualizações", + "disableUpdateCheck": "Desativar verificação de atualizações", + "disableUpdateCheckDesc": "Parar de verificar novas versões na inicialização e no painel. Reduz as solicitações de rede.", + "currentPassword": "Palavra-passe Atual", + "passwordChangedSuccess": "Senha alterada com sucesso! Por favor, faça login novamente.", + "failedToChangePassword": "Falha ao alterar a senha. Por favor, verifique sua senha atual e tente novamente.", + "theme": "Tema", + "themeLight": "Fino", + "themeDark": "Escuro", + "themeSystem": "SISTEMA", + "appearanceDesc": "Selecione o tema de cores para o aplicativo", + "terminalSyntaxHighlightingDesc": "Destacar automaticamente comandos, caminhos, IPs e níveis de log na saída do terminal", + "enableCommandPaletteShortcut": "Ativar Atalho da Paleta de Comando", + "enableCommandPaletteShortcutDesc": "Toque duas vezes no botão esquerdo para abrir a Paleta de Comando para acesso rápido aos hosts" + }, + "user": { + "failedToLoadVersionInfo": "Falha ao carregar informações da versão" + }, + "placeholders": { + "enterCode": "000000", + "ipAddress": "127.0.0.1", + "port": "22", + "maxRetries": "3", + "retryInterval": "10", + "language": "IDIOMA", + "username": "usuário", + "hostname": "nome do host", + "folder": "Pasta", + "password": "Senha", + "keyPassword": "Senha da chave", + "sudoPassword": "senha sudo (opcional)", + "notes": "adicionar notas sobre este host...", + "expirationDate": "Selecionar data de validade", + "pastePrivateKey": "Cole sua chave privada aqui...", + "pastePublicKey": "Cole sua chave pública aqui...", + "credentialName": "Meu servidor SSH", + "description": "Descrição de credenciais SSH", + "searchCredentials": "Pesquisar credenciais por nome, nome de usuário, ou tags...", + "sshConfig": "configuração endpoint ssh", + "homePath": "/casa", + "clientId": "seu-ID-cliente", + "clientSecret": "seu-cliente-secreto", + "authUrl": "https://seu-provedor.com/application/o/authorize/", + "redirectUrl": "https://seu-provedor.com/application/o/termix/", + "tokenUrl": "https://seu-provedor.com/application/o/token/", + "userIdField": "sub", + "usernameField": "Nome", + "scopes": "perfil de e-mail openid", + "userinfoUrl": "https://seu-provedor.com/application/o/userinfo/", + "enterUsername": "Insira o usuário para fazer o administrador", + "searchHosts": "Pesquisar hosts por nome, nome de usuário, IP, pasta, tags...", + "enterPassword": "Digite sua senha", + "totpCode": "6-digit TOTP code", + "searchHostsAny": "Pesquisar hosts (tentar: tag:prod, usuário:root, ip:192.168)...", + "confirmPassword": "Digite sua senha para confirmar", + "typeHere": "Digite aqui", + "fileName": "Inserir nome do arquivo (por exemplo, exemplo.txt)", + "folderName": "Inserir nome da pasta", + "fullPath": "Digite o caminho completo para o item", + "currentPath": "Digite o caminho atual para o item", + "newName": "Digite o novo nome", + "socks5Host": "127.0.0.1", + "socks5Username": "usuário do proxy", + "socks5Password": "senha de proxy", + "socks5PresetName": "ex.: Cadeia VPN do trabalho", + "socks5PresetDescription": "ex.: Cadeia de proxy para acessar servidores de trabalho", + "moshCommand": "usuário mosh@server", + "defaultPort": "22", + "defaultEndpointPort": "224", + "defaultMaxRetries": "3", + "defaultRetryInterval": "10" + }, + "leftSidebar": { + "failedToLoadHosts": "Falha ao carregar hosts", + "noFolder": "Nenhuma Pasta", + "passwordRequired": "A senha é obrigatória", + "failedToDeleteAccount": "Falha ao excluir conta", + "failedToMakeUserAdmin": "Falha ao tornar o usuário administrador", + "userIsNowAdmin": "Usuário {{username}} agora é um administrador", + "removeAdminConfirm": "Tem certeza que deseja remover o status de administrador do {{username}}?", + "deleteUserConfirm": "Tem certeza que deseja excluir o usuário {{username}}? Esta ação não pode ser desfeita.", + "deleteAccount": "Excluir Conta", + "closeDeleteAccount": "Fechar Conta Deletada", + "deleteAccountWarning": "Esta ação não pode ser desfeita. Isto irá apagar permanentemente a sua conta e todos os dados associados.", + "deleteAccountWarningDetails": "Excluir sua conta irá remover todos os seus dados, incluindo hosts, configurações e configurações SSH. Esta ação é irreversível.", + "deleteAccountWarningShort": "Esta ação não é reversível e irá excluir permanentemente sua conta.", + "cannotDeleteAccount": "Não é possível excluir a conta", + "lastAdminWarning": "Você é o último usuário administrador. Você não pode excluir sua conta, pois isso deixaria o sistema sem nenhum administrador. Por favor, faça outro usuário um administrador primeiro ou entre em contato com o suporte do sistema.", + "confirmPassword": "Confirmar senha", + "deleting": "Excluindo...", + "cancel": "cancelar" + }, + "interface": { + "sidebar": "Sidebar", + "toggleSidebar": "Alternar barra lateral", + "close": "FECHAR", + "online": "Disponível", + "offline": "Desconectado", + "maintenance": "Manutenção", + "degraded": "Degradado", + "noTunnelConnections": "Nenhuma conexão de túnel configurada", + "discord": "Discord", + "connectToSshForOperations": "Conecte-se a SSH para usar arquivos de operação", + "uploadFile": "Enviar Arquivo", + "newFile": "Novo arquivo", + "newFolder": "Adicionar uma pasta", + "rename": "Renomear", + "deleteItem": "Excluir Item", + "createNewFile": "Criar novo arquivo", + "createNewFolder": "Criar Nova Pasta", + "renameItem": "Renomear Item", + "clickToSelectFile": "Clique para selecionar um arquivo", + "noSshHosts": "Nenhum host SSH", + "sshHosts": "Hosts SSH", + "importSshHosts": "Importar o SSH Hosts do JSON", + "clientId": "ID do Cliente", + "clientSecret": "Segredo do Cliente", + "error": "ERRO", + "warning": "ATENÇÃO", + "deleteAccount": "Excluir Conta", + "closeDeleteAccount": "Fechar Conta Deletada", + "cannotDeleteAccount": "Não é possível excluir a conta", + "confirmPassword": "Confirmar senha", + "deleting": "Excluindo...", + "externalAuth": "Autenticação externa (OIDC)", + "configureExternalProvider": "Configurar provedor de identidade externo para", + "waitingForRetry": "Aguardando nova tentativa", + "retryingConnection": "Tentando conexão novamente", + "resetSplitSizes": "Redefinir tamanhos divididos", + "sshManagerAlreadyOpen": "SSH Manager já aberto", + "disabledDuringSplitScreen": "Desativado durante a tela dividida", + "unknown": "Desconhecido", + "connected": "Conectado", + "disconnected": "Desconectado", + "maxRetriesExhausted": "Máximo de tentativas esgotadas", + "endpointHostNotFound": "Servidor de Endpoint não encontrado", + "administrator": "Administrador", + "user": "Usuário", + "external": "externo", + "local": "Localização", + "saving": "Salvando...", + "saveConfiguration": "Salvar configuração", + "loading": "Carregandochar@@0", + "refresh": "atualizar", + "adding": "Adicionando...", + "makeAdmin": "Tornar administrador", + "verifying": "Verificando...", + "verifyAndEnable": "Verificar e Habilitar", + "secretKey": "Chave secreta", + "totpQrCode": "TOTP QR Code", + "passwordRequired": "A senha é obrigatória ao usar a autenticação de senha", + "sshKeyRequired": "A chave privada SSH é necessária para a autenticação de chave", + "keyTypeRequired": "Tipo de chave é necessário ao usar autenticação de chave", + "validSshConfigRequired": "Precisa selecionar uma configuração SSH válida da lista", + "updateHost": "Atualizar Host", + "addHost": "Adicionar Host", + "editHost": "Editar Host", + "pinConnection": "Fixar Conexão", + "authentication": "Autenticação", + "password": "Palavra-passe", + "key": "Chave", + "sshPrivateKey": "Chave privada SSH", + "keyPassword": "Senha da Chave", + "keyType": "Tipo de chave", + "enableTerminal": "Ativar Terminal", + "enableTunnel": "Ativar Túnel", + "enableFileManager": "Ativar Gerenciador de Arquivos", + "defaultPath": "Caminho Padrão", + "tunnelConnections": "Conexões de túnel", + "maxRetries": "Máximo de buscas", + "upload": "Transferir", + "updateKey": "Atualizar Chave", + "productionFolder": "Produção", + "databaseServer": "Banco de dados", + "developmentServer": "Servidor de desenvolvimento", + "developmentFolder": "Desenvolvimento", + "webServerProduction": "Web Server - Produção", + "unknownError": "Erro desconhecido", + "failedToInitiatePasswordReset": "Falha ao iniciar a redefinição de senha", + "failedToVerifyResetCode": "Falha ao verificar código de redefinição", + "failedToCompletePasswordReset": "Falha ao concluir a redefinição de senha", + "invalidTotpCode": "Invalid TOTP code", + "failedToStartOidcLogin": "Falha ao iniciar o login OIDC", + "failedToGetUserInfoAfterOidc": "Falha ao obter informações de usuário após login OIDC", + "loginWithExternalProvider": "Entrar com um provedor externo", + "loginWithExternal": "Entrar com o provedor externo", + "sendResetCode": "Enviar Código de Redefinição", + "verifyCode": "Verificar Código", + "resetPassword": "Redefinir a senha", + "login": "Conectar-se", + "signUp": "Criar conta", + "failedToUpdateOidcConfig": "Falha ao atualizar a configuração OIDC", + "failedToMakeUserAdmin": "Falha ao tornar o usuário administrador", + "failedToStartTotpSetup": "Falha ao iniciar a configuração TOTP", + "invalidVerificationCode": "Código de verificação inválido", + "failedToDisableTotp": "Falha ao desativar TOTP", + "failedToGenerateBackupCodes": "Falha ao gerar códigos de backup" + }, + "mobile": { + "selectHostToStart": "Selecione um host para iniciar sua sessão de terminal", + "limitedSupportMessage": "O suporte móvel para sites ainda está em andamento. Use o aplicativo móvel para uma experiência melhor.", + "mobileAppInProgress": "App móvel em andamento", + "mobileAppInProgressDesc": "Estamos trabalhando em um aplicativo de celular dedicado para fornecer uma melhor experiência em dispositivos móveis.", + "viewMobileAppDocs": "Instalar aplicativo móvel", + "mobileAppDocumentation": "Documentação para aplicativos móveis" + }, + "dashboard": { + "title": "Painel", + "loading": "Carregando painel...", + "github": "GitHub", + "support": "SUPORTE", + "discord": "Discord", + "donate": "Doar", + "serverOverview": "Visão Geral do Servidor", + "version": "Versão", + "upToDate": "Até a data", + "updateAvailable": "Atualização disponível", + "uptime": "Tempo em atividade", + "database": "Banco", + "healthy": "Saudável", + "error": "ERRO", + "totalServers": "Total de Servidores", + "totalTunnels": "Total de túneis", + "totalCredentials": "Credenciais totais", + "recentActivity": "Atividade recente", + "reset": "Reset", + "loadingRecentActivity": "Carregando atividade recente...", + "noRecentActivity": "Nenhuma atividade recente", + "quickActions": "Ações rápidas", + "addHost": "Adicionar Host", + "addCredential": "Adicionar Credencial", + "adminSettings": "Configurações de administrador", + "userProfile": "Informações do Perfil", + "serverStats": "Estatísticas do servidor", + "loadingServerStats": "Carregando estatísticas do servidor...", + "noServerData": "Nenhum dado de servidor disponível", + "cpu": "cpu", + "ram": "RAM", + "notAvailable": "N/D", + "customizeLayout": "Personalizar Painel", + "dashboardSettings": "Configurações do painel", + "enableDisableCards": "Ativar/Desativar cartões", + "resetLayout": "Restaurar ao Padrão", + "serverOverviewCard": "Visão Geral do Servidor", + "recentActivityCard": "Atividade recente", + "networkGraphCard": "Gráfico da Rede", + "networkGraph": "Gráfico da Rede", + "quickActionsCard": "Ações rápidas", + "serverStatsCard": "Estatísticas do servidor" + }, + "networkGraph": { + "title": "Gráfico da Rede", + "addHost": "Adicionar Host", + "addGroup": "Adicionar Grupo", + "addLink": "Adicionar Link", + "deleteSelected": "Excluir Selecionados", + "zoomIn": "Aumentar zoom", + "zoomOut": "Diminuir o zoom", + "resetView": "Redefinir visualização", + "exportJson": "Exportar JSON", + "importJson": "Importar JSON", + "selectHost": "Selecione o Host", + "chooseHost": "Escolha um host...", + "parentGroup": "Grupo Pai", + "noGroup": "Nenhum grupo", + "groupName": "Nome do Grupo", + "color": "Cor", + "source": "fonte", + "target": "Target", + "moveToGroup": "Mover para o grupo", + "selectGroup": "Selecionar grupo...", + "addConnection": "Adicionar conexão", + "hostDetails": "Detalhes do Host", + "connectToHost": "Conectar ao Host", + "openTerminal": "Abrir terminal", + "openFileManager": "Abrir Gerenciador de Arquivos", + "openTunnel": "Abrir túneis", + "openDocker": "Abrir Docker", + "openServerStats": "Abrir Detalhes do Servidor", + "moveToGroupAction": "Mover para o grupo", + "removeFromGroup": "Remover do grupo", + "addHostHere": "Adicionar Host Aqui", + "editGroup": "Editar Grupo", + "delete": "excluir", + "cancel": "cancelar", + "add": "Adicionar", + "create": "Crio", + "update": "Atualização", + "move": "Mover-se", + "connect": "Conectar", + "close": "FECHAR", + "createGroup": "Criar grupo", + "noAvailableHosts": "Nenhum host disponível", + "selectSourcePlaceholder": "Selecione a Fonte...", + "selectTargetPlaceholder": "Select Target...", + "invalidFile": "Arquivo inválido", + "hostAlreadyExists": "O host já está na topologia", + "sourceTargetMustDiffer": "Origem e alvo devem ser diferentes", + "connectionExists": "Conexão já existe", + "unknown": "Desconhecido", + "name": "Nome:", + "ip": "PI", + "status": "SItuação", + "id": "ID", + "failedToLoadData": "Falha ao carregar dados topologia de rede", + "failedToAddNode": "Falha ao adicionar nó", + "sourceDifferentFromTarget": "Origem e alvo devem ser diferentes", + "exportJSON": "Exportar JSON", + "importJSON": "Importar JSON", + "searchHost": "Procurar hosts...", + "noHostFound": "Nenhum host encontrado", + "searchGroup": "Procurar grupos...", + "noGroupFound": "Nenhum grupo encontrado", + "searchNode": "Buscar nós...", + "noNodeFound": "Nenhum nó encontrado", + "terminal": "Terminal", + "fileManager": "Gerenciador de Arquivos", + "tunnel": "Túnel", + "docker": "Atracador", + "serverStats": "Estatísticas do servidor" + }, + "rbac": { + "shareHost": "Host de compartilhamento", + "shareHostTitle": "Compartilhar Acesso ao Host", + "shareHostDescription": "Conceder acesso temporário ou permanente a este host", + "targetUser": "Usuário Alvo", + "selectUser": "Selecione um usuário para compartilhar com", + "duration": "Duração", + "durationHours": "Duração (horas)", + "neverExpires": "Nunca Expirar", + "permissionLevel": "Nível de permissão", + "permissionLevels": { + "readonly": "Somente leitura", + "readonlyDesc": "Pode ver apenas, sem entrada de comando", + "restricted": "Restrito", + "restrictedDesc": "Bloqueia comandos perigosos (passwd, rm -rf, etc.)", + "monitored": "Monitorado", + "monitoredDesc": "Registra todos os comandos mas não bloqueia (recomendado)", + "full": "Acesso total", + "fullDesc": "Sem restrições (não recomendado)" + }, + "blockedCommands": "Comandos Bloqueados", + "blockedCommandsPlaceholder": "Digite comandos para bloquear, por exemplo, senha, rm, dd", + "maxSessionDuration": "Duração Máxima da Sessão (minutos)", + "createTempUser": "Criar usuário temporário", + "createTempUserDesc": "Cria um usuário restrito no servidor em vez de compartilhar suas credenciais. Requer acesso sudo. Opção mais segura.", + "expiresAt": "Expira em", + "expiresIn": "Expira em {{hours}} horas", + "expired": "Expirado", + "grantedBy": "Concedido por", + "accessLevel": "Nível de Acesso", + "lastAccessed": "Último acesso", + "accessCount": "Contagem de Acesso", + "revokeAccess": "Revogar Acesso", + "confirmRevokeAccess": "Tem certeza de que deseja revogar o acesso por {{username}}?", + "hostSharedSuccessfully": "Hospedeiro compartilhado com {{username}}", + "hostAccessUpdated": "Acesso ao host atualizado", + "failedToShareHost": "Falha ao compartilhar host", + "accessRevokedSuccessfully": "Acesso revogado com sucesso", + "failedToRevokeAccess": "Falha ao revogar acesso", + "shared": "Compartilhado", + "sharedHosts": "Hosts compartilhados", + "sharedWithMe": "Compartilhado comigo", + "noSharedHosts": "Nenhum host compartilhado com você", + "owner": "Proprietário", + "viewAccessList": "Ver lista de acesso", + "accessList": "Lista de Acesso", + "noAccessGranted": "Não foi concedido acesso para este host", + "noAccessGrantedMessage": "Nenhum usuário recebeu acesso a este host ainda", + "manageAccessFor": "Gerenciar acesso para", + "totalAccessRecords": "Registros de acesso {{count}}", + "neverAccessed": "nunca", + "timesAccessed": "{{count}} vez(es)", + "daysRemaining": "{{days}} dia(s)", + "hoursRemaining": "{{hours}} hora(s)", + "failedToFetchAccessList": "Falha ao buscar lista de acesso", + "currentAccess": "Acesso atual", + "securityWarning": "Aviso de segurança", + "securityWarningMessage": "Compartilhar credenciais dá ao usuário acesso total para executar qualquer operação no servidor, incluindo a alteração de senhas e a exclusão de arquivos. Apenas compartilhe com usuários confiáveis.", + "tempUserRecommended": "Recomendamos ativar 'Criar Usuário Temporário' para uma melhor segurança.", + "roleManagement": "Gerenciamento de funções", + "manageRoles": "Gerenciar Funções", + "manageRolesFor": "Gerenciar cargos para {{username}}", + "assignRole": "Atribuir Função", + "removeRole": "Remover Papel", + "userRoles": "Papéis do Usuário", + "permissions": "Permissões", + "systemRole": "Função do Sistema", + "customRole": "Função Personalizada", + "roleAssignedSuccessfully": "Função atribuída a {{username}} com sucesso", + "failedToAssignRole": "Falha ao atribuir papel", + "roleRemovedSuccessfully": "Função removida de {{username}} com sucesso", + "failedToRemoveRole": "Falha ao remover papel", + "cannotRemoveSystemRole": "Não é possível remover função do sistema", + "cannotShareWithSelf": "Não é possível compartilhar host consigo mesmo", + "noCustomRolesToAssign": "Nenhuma função personalizada disponível. Cargos do sistema são auto-atribuídos.", + "credentialSharingWarning": "Autenticação de Credencial Não Suportada para Compartilhamento", + "credentialRequired": "É necessária credencial ao compartilhar um host", + "credentialRequiredDescription": "Este host não usa autenticação baseada em credenciais. A fim de compartilhar hosts, devido a criptografia por usuário, o host deve usar autenticação baseada em credenciais.", + "auditLogs": "Registros de Auditoria", + "viewAuditLogs": "Visualizar logs de auditoria", + "action": "Acão", + "resourceType": "Tipo de Recurso", + "resourceName": "Nome do Recurso", + "timestamp": "Timestamp", + "ipAddress": "Endereço IP", + "userAgent": "Agente do usuário", + "success": "Sucesso", + "failed": "Falhou", + "details": "detalhes", + "noAuditLogs": "Não há logs de auditoria disponíveis", + "sessionRecordings": "Gravações da Sessão", + "viewRecording": "Ver Gravação", + "downloadRecording": "Baixar Gravação", + "dangerousCommand": "Comando perigoso detectado", + "commandBlocked": "Comando bloqueado", + "terminateSession": "Terminar sessão", + "sessionTerminated": "Sessão terminada pelo proprietário do host", + "sharedAccessExpired": "Seu acesso compartilhado com este host expirou", + "sharedAccessExpiresIn": "O acesso compartilhado expira em {{hours}} horas", + "roles": { + "label": "Papéis", + "admin": "Administrador", + "user": "Usuário" + }, + "createRole": "Criar Papel", + "editRole": "Editar Permissão", + "roleName": "Nome da Função", + "displayName": "Nome para exibição", + "description": "Descrição:", + "assignRoles": "Atribuir Papéis", + "userRoleAssignment": "Atribuição de Função", + "selectUserPlaceholder": "Selecione um usuário", + "searchUsers": "Procurar usuários...", + "noUserFound": "Nenhum usuário encontrado", + "currentRoles": "Papéis Atuais", + "noRolesAssigned": "Nenhum cargo atribuído", + "assignNewRole": "Atribuir Nova Função", + "selectRolePlaceholder": "Selecione uma função", + "searchRoles": "Pesquisar funções...", + "noRoleFound": "Nenhuma função encontrada", + "assign": "Atribuir", + "roleCreatedSuccessfully": "Papel criado com sucesso", + "roleUpdatedSuccessfully": "Função atualizada com sucesso", + "roleDeletedSuccessfully": "Função apagada com sucesso", + "failedToLoadRoles": "Falha ao carregar funções", + "failedToSaveRole": "Falha ao salvar papel", + "failedToDeleteRole": "Falha ao excluir papel", + "roleDisplayNameRequired": "Nome de exibição da função é obrigatório", + "roleNameRequired": "Nome da função é obrigatório", + "roleNameHint": "Use apenas letras minúsculas, números, sublinhados e hífens", + "displayNamePlaceholder": "Desenvolvedores", + "descriptionPlaceholder": "Desenvolvedores e engenheiros de software", + "confirmDeleteRole": "Excluir Papel", + "confirmDeleteRoleDescription": "Você tem certeza que deseja excluir a função '{{name}}'? Esta ação não pode ser desfeita.", + "confirmRemoveRole": "Remover Papel", + "confirmRemoveRoleDescription": "Você tem certeza que deseja remover esta função do usuário?", + "editRoleDescription": "Atualizar informações das funções", + "createRoleDescription": "Criar um novo cargo personalizado para agrupar usuários", + "assignRolesDescription": "Gerenciar atribuições de papéis para os usuários", + "noRoles": "Nenhum cargo encontrado", + "selectRole": "Selecione a função", + "type": "tipo", + "user": "Usuário", + "role": "Funções", + "saveHostFirst": "Salvar o Host primeiro", + "saveHostFirstDescription": "Por favor, salve o host antes de configurar configurações de compartilhamento.", + "shareWithUser": "Compartilhar com Usuário", + "shareWithRole": "Compartilhar com Função", + "share": "Compartilhar", + "target": "Target", + "expires": "Expira", + "never": "nunca", + "noAccessRecords": "Nenhum registro de acesso encontrado", + "sharedSuccessfully": "Compartilhado com sucesso", + "failedToShare": "Falha ao compartilhar", + "confirmRevokeAccessDescription": "Tem certeza de que deseja revogar esse acesso?", + "hours": "horas", + "sharing": "Compartilhando", + "selectUserAndRole": "Por favor, selecione um usuário e um papel", + "view": "Apenas Visualizar", + "viewDesc": "Devido ao sistema de criptografia do Termix, outros níveis de permissão virão em uma data posterior" + }, + "commandPalette": { + "searchPlaceholder": "Procurar por hosts ou ações rápidas...", + "recentActivity": "Atividade recente", + "navigation": "Navigation", + "addHost": "Adicionar Host", + "addCredential": "Adicionar Credencial", + "adminSettings": "Configurações de administrador", + "userProfile": "Informações do Perfil", + "updateLog": "Atualizar registro", + "hosts": "Anfitriões", + "openServerDetails": "Abrir Detalhes do Servidor", + "openFileManager": "Abrir Gerenciador de Arquivos", + "edit": "Alterar", + "links": "Links", + "github": "GitHub", + "support": "SUPORTE", + "discord": "Discord", + "donate": "Doar", + "press": "Pressionar", + "toToggle": "para alternar", + "close": "FECHAR", + "hostManager": "Gerenciador de Host", + "pressToToggle": "Pressione Shift Esquerdo duas vezes para abrir a paleta de comandos" + }, + "docker": { + "notEnabled": "O Docker não está habilitado para este host", + "validating": "Validando o Docker...", + "connectingToHost": "Conectando ao host...", + "connecting": "Conectandochar@@0", + "error": "ERRO", + "errorCode": "Código de erro: {{code}}", + "version": "{{version}} Docker", + "connectionFailed": "Falha ao conectar ao Docker", + "totpVerificationFailed": "Falha na verificação TOTP. Tente novamente.", + "warpgateVerificationFailed": "A autenticação do Warpgate falhou. Por favor, tente novamente.", + "verificationCodePrompt": "Inserir código de verificação", + "loadingContainers": "Carregando contêineres...", + "containerStarted": "Contêiner {{name}} iniciado", + "failedToStartContainer": "Falha ao iniciar o contêiner {{name}}", + "containerStopped": "O contêiner {{name}} parou", + "failedToStopContainer": "Falha ao parar o contêiner {{name}}", + "containerRestarted": "Contêiner {{name}} reiniciado", + "failedToRestartContainer": "Falha ao reiniciar o contêiner {{name}}", + "containerPaused": "Recipiente {{name}} pausado", + "containerUnpaused": "Recipiente {{name}} despausado", + "failedToTogglePauseContainer": "Falha ao alternar estado de pausa para o contêiner {{name}}", + "containerRemoved": "{{name}} do contêiner removido", + "failedToRemoveContainer": "Falha ao remover contêiner {{name}}", + "image": "Imagem:", + "idLabel": "ID", + "ports": "Portas", + "noPorts": "Nenhuma porta", + "created": "Criado", + "start": "Iniciar", + "stop": "Interromper", + "pause": "Suspender", + "unpause": "Unpause", + "restart": "Reiniciar", + "remove": "Excluir", + "removeContainer": "Remover Contêiner", + "confirmRemoveContainer": "Você tem certeza que deseja remover o contêiner '{{name}}'? Esta ação não pode ser desfeita.", + "runningContainerWarning": "Aviso: Este contêiner está em execução no momento. Removê-lo irá parar o contêiner primeiro.", + "removing": "Removendo...", + "noContainersFound": "Nenhum contêiner encontrado", + "noContainersFoundHint": "Nenhum contêiner Docker está disponível neste host", + "searchPlaceholder": "Procurar contêineres...", + "filterByStatusPlaceholder": "Filtrar por status", + "allContainersCount": "Todos ({{count}})", + "statusCount": "{{status}} ({{count}})", + "noContainersMatchFilters": "Nenhum contêiner corresponde aos seus filtros", + "noContainersMatchFiltersHint": "Tente ajustar sua pesquisa ou critério de filtro", + "containerMustBeRunningToViewStats": "O contêiner deve estar em execução para ver as estatísticas", + "failedToFetchStats": "Falha ao obter estatísticas do contêiner", + "containerNotRunning": "Contêiner não executando", + "startContainerToViewStats": "Inicie o contêiner para ver estatísticas", + "loadingStats": "Carregando estatísticas...", + "errorLoadingStats": "Erro ao carregar estatísticas", + "noStatsAvailable": "Não há estatísticas disponíveis", + "cpuUsage": "Uso da CPU", + "current": "Atual", + "memoryUsage": "Memória Utilizada", + "usedLimit": "Usado / Limite", + "percentage": "Porcentagem", + "networkIo": "I/O de rede", + "input": "Input", + "output": "Saída", + "blockIo": "Bloco I/O", + "read": "Lido", + "write": "Salvar", + "pids": "PIDs", + "containerInformation": "Informação do contêiner", + "name": "Nome:", + "id": "ID", + "state": "Estado:", + "disconnectedFromContainer": "Desconectado do contêiner", + "containerMustBeRunning": "O contêiner deve estar em execução para acessar o console", + "authenticationRequired": "Autenticação requerida", + "connectedTo": "Conectado a {{containerName}}", + "disconnected": "Desconectado", + "consoleError": "Erro de console", + "errorMessage": "Erro: {{message}}", + "failedToConnect": "Falha ao conectar ao contêiner", + "console": "Console", + "selectShell": "Selecionar shell", + "bash": "Pancada", + "sh": "estrito", + "ash": "cinzas", + "connect": "Conectar", + "disconnect": "Desconectar", + "notConnected": "Não conectado", + "clickToConnect": "Clique em conectar para iniciar uma sessão shell", + "connectingTo": "Conectando a {{containerName}}...", + "containerNotFound": "Contêiner não encontrado", + "backToList": "Voltar para a Lista", + "logs": "Registros", + "stats": "Estatísticas", + "consoleTab": "Console", + "startContainerToAccess": "Iniciar o contêiner para acessar o console", + "log": { + "connecting": "Conectando ao host do Docker...", + "authStarted": "Resolvendo credenciais de autenticação...", + "totpRequired": "Verificação TOTP necessária", + "warpgateRequired": "Autenticação do Warpgate necessária", + "sessionCreated": "Sessão SSH do Docker estabelecida", + "sessionReady": "Pronto para operações do Docker", + "error": "Conexão com o Docker falhou: {{error}}" + } + }, + "stats": { + "log": { + "connecting": "Iniciando coleção de métricas...", + "existingSession": "Usando sessão de métricas existentes", + "totpVerifying": "Verificação TOTP necessária", + "sessionEstablished": "Sessão de métricas estabelecida", + "collecting": "Coletando métricas do servidor", + "heartbeat": "Mantendo a conexão viva", + "stopped": "Coleção de métricas parada", + "error": "Coleção de métricas falhou: {{error}}" + } + }, + "sftp": { + "log": { + "connecting": "Conectando ao servidor SFTP...", + "jumpHost": "Conectando via host pulo {{host}}...", + "connected": "Conexão SFTP estabelecida", + "authFailed": "Falha na autenticação", + "error": "Operação SFTP falhou: {{error}}" + } + }, + "theme": { + "switchToLight": "Trocar para Claro", + "switchToDark": "Mudar para escuro" + } +} diff --git a/src/locales/translated/ro.json b/src/locales/translated/ro_RO.json similarity index 100% rename from src/locales/translated/ro.json rename to src/locales/translated/ro_RO.json diff --git a/src/locales/translated/ru.json b/src/locales/translated/ru_RU.json similarity index 100% rename from src/locales/translated/ru.json rename to src/locales/translated/ru_RU.json diff --git a/src/locales/translated/sr.json b/src/locales/translated/sr_SP.json similarity index 100% rename from src/locales/translated/sr.json rename to src/locales/translated/sr_SP.json diff --git a/src/locales/translated/sv.json b/src/locales/translated/sv_SE.json similarity index 100% rename from src/locales/translated/sv.json rename to src/locales/translated/sv_SE.json diff --git a/src/locales/translated/th.json b/src/locales/translated/th_TH.json similarity index 100% rename from src/locales/translated/th.json rename to src/locales/translated/th_TH.json diff --git a/src/locales/translated/tr.json b/src/locales/translated/tr_TR.json similarity index 100% rename from src/locales/translated/tr.json rename to src/locales/translated/tr_TR.json diff --git a/src/locales/translated/uk.json b/src/locales/translated/uk_UA.json similarity index 100% rename from src/locales/translated/uk.json rename to src/locales/translated/uk_UA.json diff --git a/src/locales/translated/vi.json b/src/locales/translated/vi_VN.json similarity index 100% rename from src/locales/translated/vi.json rename to src/locales/translated/vi_VN.json diff --git a/src/locales/translated/zh_CN.json b/src/locales/translated/zh_CN.json new file mode 100644 index 00000000..b429a2c2 --- /dev/null +++ b/src/locales/translated/zh_CN.json @@ -0,0 +1,2665 @@ +{ + "credentials": { + "credentialsViewer": "凭证查看器", + "manageYourSSHCredentials": "安全地管理您的 SSH 凭证", + "addCredential": "添加凭证", + "createCredential": "创建凭证", + "editCredential": "编辑凭证", + "viewCredential": "查看凭证", + "duplicateCredential": "重复凭证", + "deleteCredential": "删除凭证", + "updateCredential": "更新凭证", + "credentialName": "凭证名称", + "credentialDescription": "描述", + "username": "用户名", + "searchCredentials": "搜索凭证...", + "selectFolder": "选择文件夹", + "selectAuthType": "选择身份验证类型", + "allFolders": "所有文件夹", + "allAuthTypes": "所有身份验证类型", + "uncategorized": "未分类", + "totalCredentials": "全部", + "keyBased": "基于密钥", + "passwordBased": "基于密码", + "folders": "文件夹", + "noCredentialsMatchFilters": "没有符合您筛选条件的凭证", + "noCredentialsYet": "尚未创建任何凭证", + "createFirstCredential": "创建您的第一个凭证", + "failedToFetchCredentials": "获取凭证失败", + "credentialDeletedSuccessfully": "凭证已成功删除", + "failedToDeleteCredential": "删除凭证失败", + "confirmDeleteCredential": "您确定要删除凭证“{{name}}”吗?", + "credentialCreatedSuccessfully": "凭证创建成功", + "credentialUpdatedSuccessfully": "凭证已成功更新", + "failedToSaveCredential": "保存凭证失败", + "failedToFetchCredentialDetails": "获取凭证详细信息失败", + "failedToFetchHostsUsing": "使用此凭证获取主机失败", + "loadingCredentials": "正在加载凭证...", + "retry": "重试", + "noCredentials": "没有凭证", + "noCredentialsMessage": "您尚未添加任何凭证。点击“添加凭证”开始操作。", + "sshCredentials": "SSH凭证", + "credentialsCount": "{{count}} 凭证", + "refresh": "刷新", + "passwordRequired": "需要密码", + "sshKeyRequired": "需要 SSH 密钥", + "credentialAddedSuccessfully": "凭证“{{name}}”已成功添加", + "savingCredential": "正在保存凭据...", + "updatingCredential": "正在更新凭据...", + "general": "通用", + "description": "描述", + "folder": "文件夹", + "tags": "标签", + "addTagsSpaceToAdd": "添加标签(按空格键添加)", + "password": "密码", + "key": "密钥", + "sshPrivateKey": "SSH 私钥", + "upload": "上传", + "updateKey": "更新密钥", + "keyPassword": "密钥密码", + "keyType": "密钥类型", + "keyTypeRSA": "RSA", + "keyTypeECDSA": "ECDSA", + "keyTypeEd25519": "Ed25519", + "basicInfo": "基本信息", + "authentication": "验证", + "organization": "组织", + "basicInformation": "基本信息", + "basicInformationDescription": "请输入此凭证的基本信息", + "authenticationMethod": "身份验证方式", + "authenticationMethodDescription": "选择你希望用于 SSH 服务器的认证方式", + "organizationDescription": "使用文件夹和标签整理你的凭证", + "enterCredentialName": "请输入凭证名称", + "enterCredentialDescription": "输入描述(可选)", + "enterUsername": "请输入用户名", + "nameIsRequired": "凭证名称为必填项", + "usernameIsRequired": "用户名是必填项", + "authenticationType": "身份验证类型", + "passwordAuthDescription": "使用密码验证", + "sshKeyAuthDescription": "使用 SSH 密钥验证", + "passwordIsRequired": "密码是必填项", + "sshKeyIsRequired": "需要 SSH 密钥", + "sshKeyType": "SSH密钥类型", + "privateKey": "私钥", + "enterPassword": "输入密码", + "enterPrivateKey": "输入私钥", + "keyPassphrase": "密钥口令", + "enterKeyPassphrase": "输入密钥密码(可选)", + "keyPassphraseOptional": "可选:如果您的密钥没有密码,请留空。", + "leaveEmptyToKeepCurrent": "留空以保留当前值", + "uploadKeyFile": "上传密钥文件", + "generateKeyPairButton": "生成密钥对", + "generateKeyPair": "生成密钥对", + "generateKeyPairDescription": "生成新的 SSH 密钥对。如果您想使用密码保护密钥,请先在下方的“密钥密码”字段中输入密码。", + "deploySSHKey": "部署 SSH 密钥", + "deploySSHKeyDescription": "将公钥部署至目标服务器", + "sourceCredential": "来源凭证", + "targetHost": "目标主机", + "deploymentProcess": "部署过程", + "deploymentProcessDescription": "这将安全地将公钥添加到目标主机的 ~/.ssh/authorized_keys 文件中,而不会覆盖现有密钥。此操作可逆。", + "chooseHostToDeploy": "选择要部署到的主机……", + "deploying": "正在部署……", + "name": "名称", + "noHostsAvailable": "暂无可用主机", + "noHostsMatchSearch": "没有符合您搜索条件的主机", + "sshKeyGenerationNotImplemented": "即将推出SSH密钥生成功能", + "connectionTestingNotImplemented": "即将推出连接测试功能", + "testConnection": "测试连接", + "selectOrCreateFolder": "选择或创建文件夹", + "noFolder": "没有文件夹", + "orCreateNewFolder": "或者创建一个新文件夹", + "addTag": "添加标签", + "saving": "保存中...", + "credentialId": "凭证 ID", + "overview": "概述", + "security": "安全", + "usage": "用法", + "securityDetails": "安全详情", + "securityDetailsDescription": "查看加密凭证信息", + "credentialSecured": "凭证已安全", + "credentialSecuredDescription": "所有敏感数据均采用 AES-256 加密", + "passwordAuthentication": "密码认证", + "keyAuthentication": "密钥认证", + "securityReminder": "安全提醒", + "securityReminderText": "切勿泄露您的登录凭证。所有数据在存储时均已加密。", + "hostsUsingCredential": "使用此凭证的主机", + "noHostsUsingCredential": "目前没有主机使用此凭证", + "timesUsed": "使用次数", + "lastUsed": "上次使用", + "connectedHosts": "已连接主机", + "created": "创建", + "lastModified": "最后修改时间", + "usageStatistics": "使用情况统计", + "copiedToClipboard": "{{field}} 已复制到剪贴板", + "failedToCopy": "复制到剪贴板失败", + "sshKey": "SSH密钥", + "createCredentialDescription": "创建新的 SSH 凭证以实现安全访问", + "editCredentialDescription": "更新凭证信息", + "listView": "列表", + "folderView": "文件夹", + "unknownCredential": "未知", + "confirmRemoveFromFolder": "您确定要从文件夹“{{folder}}”中删除“{{name}}”吗?凭证将被移动到“未分类”文件夹。", + "removedFromFolder": "已成功从文件夹中移除凭证“{{name}}”。", + "failedToRemoveFromFolder": "无法从文件夹中删除凭证", + "folderRenamed": "文件夹“{{oldName}}”已成功重命名为“{{newName}}”。", + "failedToRenameFolder": "文件夹重命名失败", + "movedToFolder": "凭证“{{name}}”已成功移至“{{folder}}”。", + "failedToMoveToFolder": "无法将凭证移动到文件夹", + "sshPublicKey": "SSH 公钥", + "publicKeyNote": "公钥是可选的,但建议用于密钥验证。", + "publicKeyUploaded": "公钥已上传", + "uploadPublicKey": "上传公钥", + "uploadPrivateKeyFile": "上传私钥文件", + "uploadPublicKeyFile": "上传公钥文件", + "privateKeyRequiredForGeneration": "生成公钥需要私钥。", + "failedToGeneratePublicKey": "生成公钥失败", + "generatePublicKey": "从私钥生成", + "publicKeyGeneratedSuccessfully": "公钥生成成功", + "detectedKeyType": "检测到密钥类型", + "detectingKeyType": "正在检测中……", + "optional": "可选的", + "generateKeyPairNew": "生成新的密钥对", + "generateEd25519": "生成 Ed25519", + "generateECDSA": "生成 ECDSA", + "generateRSA": "生成 RSA", + "keyTypeEcdsaP256": "ECDSA P-256 (SSH)", + "keyTypeEcdsaP384": "ECDSA P-384 (SSH)", + "keyTypeEcdsaP521": "ECDSA P-521 (SSH)", + "keyTypeDsa": "DSA (SSH)", + "keyTypeRsaSha256": "RSA-SHA2-256", + "keyTypeRsaSha512": "RSA-SHA2-512", + "keyPairGeneratedSuccessfully": "{{keyType}}密钥对已成功生成", + "failedToGenerateKeyPair": "密钥对生成失败", + "generateKeyPairNote": "直接生成新的 SSH 密钥对。此操作将替换表单中的所有现有密钥。", + "invalidKey": "无效密钥", + "detectionError": "检测错误", + "removing": "移除:", + "clickToEditCredential": "点击编辑凭证", + "dragToMoveBetweenFolders": "拖动即可在文件夹之间移动。", + "keyBasedOnlyForDeployment": "仅可部署基于 SSH 密钥的凭证。", + "publicKeyRequiredForDeployment": "部署需要公钥", + "selectTargetHost": "请选择目标主机", + "keyDeployedSuccessfully": "SSH密钥已成功部署", + "deploymentFailed": "部署失败", + "failedToDeployKey": "部署 SSH 密钥失败", + "clickToRenameFolder": "点击重命名文件夹", + "renameFolder": "重命名文件夹", + "idLabel": "ID:" + }, + "quickConnect": { + "title": "快速连接", + "description": "直接连接到终端或文件管理器会话而不保存主机配置", + "ipAddress": "IP地址或主机名", + "port": "端口", + "username": "用户名", + "password": "密码", + "key": "SSH 私钥", + "keyPassword": "密钥密码 (可选)", + "keyType": "密钥类型", + "uploadFile": "上传文件", + "pasteKey": "粘贴密钥", + "credential": "凭据", + "overrideUsername": "覆盖凭据用户名", + "overrideUsernameDesc": "使用不同于凭据中存储的用户名", + "connectTerminal": "连接到终端", + "connectFileManager": "连接到文件管理器", + "cancel": "取消", + "passwordRequired": "密码是必需的", + "keyRequired": "需要 SSH 密钥", + "credentialRequired": "凭据选择是必需的", + "connectionEstablished": "成功建立连接", + "connectionFailed": "无法建立连接", + "autoDetect": "自动检测", + "authentication": "认证" + }, + "dragIndicator": { + "error": "错误:{{error}}", + "dragging": "拖拽 {{fileName}}", + "preparing": "准备 {{fileName}}", + "readySingle": "准备下载 {{fileName}}", + "readyMultiple": "准备下载 {{count}} 个文件", + "batchDrag": "将 {{count}} 个文件拖至桌面", + "dragToDesktop": "拖到桌面", + "canDragAnywhere": "您可以将文件拖放到桌面上的任何位置。" + }, + "sshTools": { + "title": "SSH 工具", + "closeTools": "关闭 SSH 工具", + "keyRecording": "按键录制", + "startKeyRecording": "开始按键录制", + "stopKeyRecording": "停止按键录制", + "selectTerminals": "选择终端:", + "typeCommands": "输入命令(支持所有密钥类型):", + "commandsWillBeSent": "命令将发送到 {{count}} 个已选定的终端。", + "settings": "设置", + "enableRightClickCopyPaste": "启用右键单击复制/粘贴", + "shareIdeas": "对 SSH 工具的未来发展方向有什么想法吗?欢迎分享!", + "scripts": { + "inputPlaceholder": "例如:系统命令、Docker脚本" + } + }, + "snippets": { + "title": "代码片段", + "new": "新代码片段", + "create": "创建代码片段", + "edit": "编辑代码片段", + "run": "运行", + "empty": "目前还没有代码片段", + "emptyHint": "创建代码片段来保存常用命令", + "searchSnippets": "搜索代码片段……", + "name": "名称", + "description": "描述", + "content": "命令", + "namePlaceholder": "例如,重启 Nginx", + "descriptionPlaceholder": "可选描述", + "contentPlaceholder": "例如,sudo systemctl restart nginx", + "nameRequired": "名称是必填项", + "contentRequired": "命令内容是必填项", + "createDescription": "创建一个新的代码片段以便快速执行", + "editDescription": "编辑此代码片段", + "deleteConfirmTitle": "删除代码片段", + "deleteConfirmDescription": "您确定要删除“{{name}}”吗?", + "createSuccess": "代码片段创建成功", + "updateSuccess": "代码片段已成功更新", + "deleteSuccess": "代码片段已成功删除", + "createFailed": "创建代码片段失败", + "updateFailed": "更新代码片段失败", + "deleteFailed": "删除代码片段失败", + "failedToFetch": "获取代码片段失败", + "executeSuccess": "执行:{{name}}", + "copySuccess": "已将“{{name}}”复制到剪贴板", + "runTooltip": "在终端中执行此代码片段", + "copyTooltip": "复制代码片段到剪贴板", + "editTooltip": "编辑此代码片段", + "deleteTooltip": "删除此片段", + "newFolder": "新建文件夹", + "reorderSameFolder": "只能对同一文件夹内的代码片段进行重新排序", + "reorderSuccess": "代码片段已成功重新排序", + "reorderFailed": "重新排序代码片段失败", + "deleteFolderConfirm": "删除文件夹“{{name}}”?所有代码片段将被移至“未分类”。", + "deleteFolderSuccess": "文件夹已成功删除", + "deleteFolderFailed": "删除文件夹失败", + "updateFolderSuccess": "文件夹已成功更新", + "createFolderSuccess": "文件夹创建成功", + "updateFolderFailed": "更新文件夹失败", + "createFolderFailed": "创建文件夹失败", + "selectTerminals": "选择终端(可选)", + "executeOnSelected": "在 {{count}} 个已选定的终端上执行", + "executeOnCurrent": "在当前终端上执行(点击选择多个)", + "folder": "文件夹", + "selectFolder": "选择一个文件夹,或者留空。", + "noFolder": "无文件夹(未分类)", + "folderName": "文件夹名称", + "folderNameRequired": "文件夹名称为必填项", + "folderColor": "文件夹颜色", + "folderIcon": "文件夹图标", + "preview": "预览", + "updateFolder": "更新文件夹", + "createFolder": "创建文件夹", + "editFolder": "编辑文件夹", + "editFolderDescription": "自定义代码片段文件夹", + "createFolderDescription": "将你的代码片段归类到文件夹中" + }, + "commandHistory": { + "title": "历史", + "searchPlaceholder": "搜索命令中……", + "noTerminal": "没有活动的终端", + "noTerminalHint": "打开终端查看其命令历史记录。", + "empty": "暂无命令历史记录", + "emptyHint": "在当前终端中执行命令,以生成其命令历史记录。", + "noResults": "未找到命令", + "noResultsHint": "没有与“{{query}}”匹配的命令", + "deleteSuccess": "命令已从历史记录中删除", + "deleteFailed": "删除命令失败。", + "deleteTooltip": "删除命令", + "tabHint": "若用户配置文件中已启用该功能,可在终端中使用 Tab 键从命令历史记录中自动补全命令。", + "authRequiredRefresh": "需要进行身份验证。请刷新页面。", + "dataAccessLockedReauth": "数据访问已锁定。请重新验证身份。", + "loading": "正在加载命令历史记录...", + "error": "加载历史记录时出错" + }, + "splitScreen": { + "title": "分屏", + "none": "不分屏", + "twoSplit": "2分屏", + "threeSplit": "3分屏", + "fourSplit": "4分屏", + "availableTabs": "可选选项卡", + "dragTabsHint": "将选项卡拖到下面的布局单元格中进行分配", + "layout": "分屏布局", + "dropHere": "将选项卡拖动到这", + "apply": "应用分屏布局", + "clear": "清空分屏布局", + "selectMode": "选择分屏模式", + "helpText": "选择您想要同时查看的选项卡数量", + "success": "分屏布局已成功应用", + "cleared": "分屏布局已清空", + "error": { + "noAssignments": "请为该布局分配至少一个选项卡", + "fillAllSlots": "请在应用前补充所有 {{count}} 个分屏窗口" + } + }, + "homepage": { + "loggedInTitle": "已登录!", + "loggedInMessage": "您已登录!使用侧边栏访问所有可用工具。首先,请在“SSH 管理器”选项卡中创建一个 SSH 主机。创建完成后,您可以使用侧边栏中的其他应用程序连接到该主机。", + "failedToLoadAlerts": "警报加载失败", + "failedToDismissAlert": "未能关闭警报" + }, + "serverConfig": { + "title": "服务器配置", + "description": "配置 Termix 服务器 URL 以连接到您的后端服务", + "serverUrl": "服务器 URL", + "enterServerUrl": "请输入服务器URL", + "testConnectionFirst": "请先测试连接。", + "connectionSuccess": "连接成功!", + "connectionFailed": "连接失败", + "connectionError": "连接错误", + "connected": "已连接", + "disconnected": "断开连接", + "configSaved": "配置已成功保存", + "saveFailed": "配置保存失败", + "saveError": "保存配置时出错", + "saving": "保存中...", + "saveConfig": "保存配置", + "helpText": "输入您的 Termix 服务器运行所对应的 URL(例如,http://localhost:30001 或 https://your-server.com)", + "warning": "警告", + "notValidatedWarning": "URL未验证 - 请确保其正确", + "changeServer": "变更服务器", + "mustIncludeProtocol": "服务器 URL 必须以 http:// 或 https:// 开头。" + }, + "versionCheck": { + "error": "版本检查错误", + "checkFailed": "检查更新失败", + "upToDate": "应用已更新至最新版本", + "currentVersion": "您正在运行版本 {{version}}", + "updateAvailable": "更新可用", + "newVersionAvailable": "新版本已发布!您当前运行的是 {{current}} 版本,但 {{latest}} 版本可用。", + "releasedOn": "发布日期:{{date}}", + "downloadUpdate": "下载更新", + "dismiss": "关闭", + "checking": "正在检查更新...", + "checkUpdates": "检查更新", + "checkingUpdates": "正在检查更新...", + "refresh": "刷新", + "updateRequired": "需要更新", + "updateDismissed": "更新通知已关闭", + "noUpdatesFound": "未找到更新" + }, + "common": { + "close": "关闭", + "minimize": "最小化", + "online": "在线", + "offline": "离线", + "continue": "继续", + "maintenance": "维护", + "degraded": "降级", + "discord": "Discord", + "error": "错误", + "warning": "警告", + "info": "信息", + "success": "成功", + "loading": "加载中...", + "required": "必需的", + "optional": "选修的", + "connect": "连接", + "connecting": "正在连接...", + "creating": "正在创建……", + "clear": "清除", + "toggleSidebar": "切换侧边栏", + "sidebar": "侧边栏", + "home": "首页", + "expired": "已到期", + "expiresToday": "今天到期", + "expiresTomorrow": "{{days}} 天后过期", + "updateAvailable": "更新可用", + "sshPath": "SSH路径", + "localPath": "本地路径", + "appName": "Termixe", + "openInNewTab": "在新标签中打开", + "resetSidebarWidth": "重置侧边栏宽度", + "dragToResizeSidebar": "拖动即可调整侧边栏大小", + "noAuthCredentials": "此 SSH 主机无可用的身份验证凭证", + "noReleases": "无发布", + "updatesAndReleases": "更新与发布", + "newVersionAvailable": "新版本({{version}})可用。", + "failedToFetchUpdateInfo": "获取更新信息失败", + "preRelease": "预发布", + "loginFailed": "登录失败", + "noReleasesFound": "未找到任何版本。", + "yourBackupCodes": "您的备用验证码", + "sendResetCode": "发送重置验证码", + "verifyCode": "验证码", + "resetPassword": "重置密码", + "resetCode": "重置验证码", + "newPassword": "新密码", + "folder": "文件夹", + "file": "文件", + "renamedSuccessfully": "重命名成功", + "deletedSuccessfully": "已成功删除", + "noTunnelConnections": "未配置隧道连接", + "sshTools": "SSH 工具", + "english": "英语", + "chinese": "中文", + "german": "德语", + "cancel": "取消", + "username": "用户名", + "name": "名称", + "login": "登录", + "logout": "注销", + "register": "注册", + "password": "密码", + "version": "版本", + "confirmPassword": "确认密码", + "back": "返回", + "email": "电子邮件", + "submit": "提交", + "change": "改变", + "save": "保存", + "saving": "保存中...", + "delete": "删除", + "edit": "编辑", + "add": "添加", + "search": "搜索", + "confirm": "确认", + "yes": "否", + "no": "否", + "ok": "好的", + "enabled": "已启用", + "disabled": "已禁用", + "important": "重要的", + "notEnabled": "未启用", + "settingUp": "正在设置中……", + "next": "下一个", + "previous": "上一个", + "refresh": "刷新", + "settings": "设置", + "profile": "配置", + "help": "帮助", + "about": "关于", + "language": "语言", + "autoDetect": "自动检测", + "changeAccountPassword": "更改您的帐户密码", + "passwordResetTitle": "密码重置", + "passwordResetDescription": "您即将重置密码。这将使您退出所有活动会话。", + "enterSixDigitCode": "请输入来自 Docker 容器日志中用户的 6 位验证码:", + "enterNewPassword": "请输入用户的新密码:", + "passwordsDoNotMatch": "密码不匹配", + "passwordMinLength": "密码长度必须至少为 6 个字符。", + "passwordResetSuccess": "密码重置成功!您现在可以使用新密码登录。", + "failedToInitiatePasswordReset": "密码重置失败", + "failedToVerifyResetCode": "重置验证码验证失败", + "failedToCompletePasswordReset": "密码重置失败", + "documentation": "文档", + "retry": "重试", + "checking": "检查...", + "checkingDatabase": "正在检查数据库连接...", + "checkingAuthentication": "正在检查认证...", + "actions": "激活", + "remove": "移除", + "revoke": "撤销", + "create": "创建", + "update": "更新" + }, + "nav": { + "home": "首页", + "hosts": "主机", + "credentials": "凭证", + "terminal": "终端", + "docker": "停靠栏", + "tunnels": "隧道", + "fileManager": "文件管理器", + "serverStats": "服务器统计信息", + "admin": "admin", + "userProfile": "用户配置", + "tools": "工具", + "snippets": "片段", + "newTab": "新选项卡", + "splitScreen": "分屏", + "closeTab": "关闭选项卡", + "sshManager": "SSH 管理器", + "hostManager": "主机管理器", + "cannotSplitTab": "无法拆分此选项卡", + "tabNavigation": "选项卡导航", + "hostTabTitle": "{{username}}@{{ip}}:{{port}}", + "copyPassword": "复制密码", + "copySudoPassword": "复制Sudo密码", + "passwordCopied": "密码已复制到剪贴板", + "sudoPasswordCopied": "Sudo 密码已复制到剪贴板", + "noPasswordAvailable": "没有可用密码" + }, + "admin": { + "title": "管理员设置", + "oidc": "OIDC", + "users": "用户", + "userManagement": "用户管理", + "makeAdmin": "设为管理员", + "removeAdmin": "移除管理员", + "deleteUser": "删除用户 {{username}}?此操作无法撤销。", + "allowRegistration": "允许注册", + "oidcSettings": "OIDC 设置", + "clientId": "客户端ID", + "clientSecret": "客户端密钥", + "issuerUrl": "发行者网址", + "authorizationUrl": "授权 URL", + "tokenUrl": "令牌网址", + "updateSettings": "更新设置", + "confirmDelete": "您确定要删除此用户吗?", + "confirmMakeAdmin": "你确定要将 {{username}} 设为管理员吗?", + "confirmRemoveAdmin": "你确定要移除 {{username}} 的管理员身份吗?", + "externalAuthentication": "外部认证(OIDC)", + "configureExternalProvider": "配置用于 OIDC/OAuth2 身份验证的外部身份提供程序。", + "userIdentifierPath": "用户标识符路径", + "displayNamePath": "显示名称路径", + "scopes": "权限范围", + "saving": "保存中...", + "saveConfiguration": "保存配置", + "reset": "重置", + "success": "成功", + "loading": "加载中...", + "refresh": "刷新", + "loadingUsers": "正在加载用户...", + "username": "用户名", + "type": "类型", + "actions": "操作", + "external": "外部", + "local": "本地的", + "adminManagement": "管理员管理", + "makeUserAdmin": "将用户设为管理员", + "adding": "添加中……", + "currentAdmins": "当前管理员", + "adminBadge": "管理员", + "removeAdminButton": "移除管理员", + "general": "通用", + "userRegistration": "用户注册", + "allowNewAccountRegistration": "允许新账户注册", + "allowPasswordLogin": "允许用户名/密码登录", + "allowPasswordReset": "允许重置密码", + "missingRequiredFields": "缺少必填字段:{{fields}}", + "oidcConfigurationUpdated": "OIDC配置更新成功!", + "failedToFetchOidcConfig": "获取 OIDC 配置失败", + "failedToFetchRegistrationStatus": "获取注册状态失败", + "failedToFetchPasswordLoginStatus": "获取密码登录状态失败", + "failedToFetchUsers": "获取用户失败", + "oidcConfigurationDisabled": "OIDC配置已成功禁用!", + "failedToUpdateOidcConfig": "更新 OIDC 配置失败", + "failedToDisableOidcConfig": "禁用 OIDC 配置失败", + "enterUsernameToMakeAdmin": "输入用户名以创建管理员", + "userIsNowAdmin": "用户 {{username}} 现在是管理员", + "failedToMakeUserAdmin": "创建管理员用户失败", + "removeAdminStatus": "是否移除 {{username}} 的管理员身份?", + "adminStatusRemoved": "已移除 {{username}} 的管理员身份", + "failedToRemoveAdminStatus": "移除管理员状态失败", + "userDeletedSuccessfully": "用户 {{username}} 已成功删除", + "failedToDeleteUser": "删除用户失败", + "overrideUserInfoUrl": "覆盖用户信息 URL(非必填)", + "failedToFetchSessions": "获取sessions失败", + "sessionRevokedSuccessfully": "sessions已成功撤销", + "failedToRevokeSession": "撤销会话失败", + "confirmRevokeSession": "您确定要撤销本次会话吗?", + "confirmRevokeAllSessions": "您确定要撤销该用户的所有会话吗?", + "failedToRevokeSessions": "撤销会话失败", + "sessionsRevokedSuccessfully": "会话已成功撤销", + "linkToPasswordAccount": "关联到密码帐户", + "linkOIDCDialogTitle": "将 OIDC 帐户关联到密码帐户", + "linkOIDCDialogDescription": "将 {{username}}(OIDC 用户)关联至现有密码账户。此操作将为该密码账户启用双重认证。", + "createUser": "创建用户", + "createUserDescription": "使用用户名和密码创建一个新的本地用户", + "enterUsername": "请输入用户名", + "enterPassword": "输入密码", + "userCreatedSuccessfully": "用户 {{username}} 创建成功", + "failedToCreateUser": "创建用户失败", + "manageUser": "管理用户", + "manageUserDescription": "管理用户设置、角色和权限", + "authType": "身份验证类型", + "adminStatus": "管理员状态", + "userId": "用户ID", + "regularUser": "普通用户", + "adminPrivileges": "管理员权限", + "administratorRole": "管理员角色", + "administratorRoleDescription": "授予完整的系统访问权与管理权限", + "passwordManagement": "密码管理", + "passwordResetWarning": "重置用户密码会删除其所有数据(SSH 主机、凭证、设置)。此操作无法撤销。", + "resetUserPassword": "重置用户密码", + "resettingPassword": "正在重置……", + "passwordResetInitiated": "已对 {{username}} 发起密码重置。已发送重置码。", + "failedToResetPassword": "密码重置失败", + "sessionManagement": "会话管理", + "revokeAllSessions": "撤销所有会话", + "revokeAllSessionsDescription": "强制从所有设备和会话中注销", + "revoking": "撤销……", + "revoke": "撤销所有", + "dangerZone": "危险操作", + "deleteUserTitle": "删除用户帐户", + "deleteUserWarning": "永久删除此用户帐户及其所有关联数据。此操作无法撤销。", + "deleting": "正在删除……", + "cannotDeleteSelf": "您无法删除自己的帐户", + "cannotRemoveLastAdmin": "无法移除最后一位管理员", + "cannotRemoveOwnAdmin": "您无法移除自己的管理员权限。", + "cannotModifyOwnAdminStatus": "您无法修改自己的管理员状态", + "dualAuth": "双重认证", + "externalOIDC": "外部(OIDC)", + "localPassword": "本地密码", + "confirmRevokeOwnSessions": "你确定要撤销自己的所有会话吗?你将被登出。", + "linkOIDCWarningTitle": "警告:OIDC用户数据将被删除", + "linkOIDCActionDeleteUser": "删除 OIDC 用户帐户及其所有数据", + "linkOIDCActionAddCapability": "向目标密码帐户添加 OIDC 登录功能", + "linkOIDCActionDualAuth": "允许密码帐户同时使用密码和 OIDC 登录", + "linkTargetUsernameLabel": "目标密码帐户用户名", + "linkTargetUsernamePlaceholder": "请输入用户名或密码帐户", + "linkAccountsButton": "关联账户", + "linkingAccounts": "正在连接……", + "accountsLinkedSuccessfully": "OIDC 用户 {{oidcUsername}} 已关联至 {{targetUsername}}", + "failedToLinkAccounts": "账户关联失败", + "linkTargetUsernameRequired": "目标用户名是必填项", + "unlinkOIDCTitle": "取消 OIDC 身份验证", + "unlinkOIDCDescription": "是否移除 {{username}} 的 OIDC 认证?操作后该用户将仅能通过用户名 / 密码登录。", + "unlinkOIDCSuccess": "OIDC 与 {{username}} 解除关联", + "failedToUnlinkOIDC": "取消关联 OIDC 失败", + "databaseSecurity": "数据库安全", + "encryptionStatus": "加密状态", + "encryptionEnabled": "已启用加密", + "enabled": "已启用", + "disabled": "已禁用", + "keyId": "密钥 ID", + "created": "已创建", + "migrationStatus": "迁移状态", + "migrationCompleted": "迁移完成", + "migrationRequired": "需要迁移", + "deviceProtectedMasterKey": "环境保护主密钥", + "legacyKeyStorage": "传统密钥存储", + "masterKeyEncryptedWithDeviceFingerprint": "主密钥已通过环境指纹加密(KEK 保护已激活)", + "keyNotProtectedByDeviceBinding": "密钥未受环境绑定保护(建议升级)", + "valid": "有效的", + "initializeDatabaseEncryption": "初始化数据库加密", + "enableAes256EncryptionWithDeviceBinding": "启用基于环境的主密钥保护的 AES-256 加密。这可为 SSH 密钥、密码和身份验证令牌创建企业级安全保障。", + "featuresEnabled": "已启用功能:", + "aes256GcmAuthenticatedEncryption": "AES-256-GCM 认证加密", + "deviceFingerprintMasterKeyProtection": "环境指纹主密钥保护(KEK)", + "pbkdf2KeyDerivation": "使用 10 万次迭代的 PBKDF2 密钥派生", + "automaticKeyManagement": "密钥自动管理与轮换", + "initializing": "正在初始化...", + "initializeEnterpriseEncryption": "初始化企业级加密", + "migrateExistingData": "迁移现有数据", + "encryptExistingUnprotectedData": "对数据库中现有的未加密数据进行加密。此过程安全可靠,并会自动创建备份。", + "testMigrationDryRun": "验证加密兼容性", + "migrating": "正在迁移……", + "migrateData": "迁移数据", + "securityInformation": "安全信息", + "sshPrivateKeysEncryptedWithAes256": "SSH 私钥与密码均采用 AES-256-GCM 算法加密", + "userAuthTokensProtected": "用户认证令牌与双因素认证密钥均已受保护", + "masterKeysProtectedByDeviceFingerprint": "主加密密钥已通过设备指纹(KEK)保护", + "keysBoundToServerInstance": "密钥已绑定至当前服务器环境(可通过环境变量迁移)", + "pbkdf2HkdfKeyDerivation": "采用 10 万轮迭代的 PBKDF2 + HKDF 密钥派生算法", + "backwardCompatibleMigration": "迁移过程中所有数据均保持向后兼容。", + "enterpriseGradeSecurityActive": "企业级安全已激活", + "masterKeysProtectedByDeviceBinding": "你的主加密密钥已通过环境指纹技术保护。该机制会利用服务器主机名、路径及其他环境信息生成保护密钥。如需迁移服务器,请在新服务器上设置 DB_ENCRYPTION_KEY 环境变量。", + "important": "重要的", + "keepEncryptionKeysSecure": "确保数据安全:定期备份数据库文件与服务器配置。如需迁移至新服务器,请在新环境中设置 DB_ENCRYPTION_KEY 环境变量,或保持相同的主机名与目录结构。", + "loadingEncryptionStatus": "正在加载加密状态...", + "testMigrationDescription": "验证现有数据是否可以安全地迁移到加密格式,而不会实际修改任何数据。", + "serverMigrationGuide": "服务器迁移指南", + "migrationInstructions": "要将加密数据迁移到新服务器:1) 备份数据库文件;2) 在新服务器上设置环境变量 DB_ENCRYPTION_KEY=\"您的密钥\";3) 恢复数据库文件", + "environmentProtection": "环境保护", + "environmentProtectionDesc": "根据服务器环境信息(主机名、路径等)保护加密密钥,可通过环境变量迁移。", + "verificationCompleted": "兼容性验证完成 - 无数据更改", + "verificationInProgress": "验证完成", + "dataMigrationCompleted": "数据迁移成功完成!", + "verificationFailed": "兼容性验证失败", + "migrationFailed": "迁移失败", + "runningVerification": "正在运行兼容性验证……", + "startingMigration": "开始迁移……", + "hardwareFingerprintSecurity": "硬件指纹安全", + "hardwareBoundEncryption": "硬件加密已激活", + "masterKeysNowProtectedByHardwareFingerprint": "主密钥现在由真正的硬件指纹识别技术而非环境变量来保护。", + "cpuSerialNumberDetection": "CPU序列号检测", + "motherboardUuidIdentification": "主板 UUID 识别", + "diskSerialNumberVerification": "磁盘序列号验证", + "biosSerialNumberCheck": "BIOS序列号检查", + "stableMacAddressFiltering": "稳定的MAC地址过滤", + "databaseFileEncryption": "数据库文件加密", + "dualLayerProtection": "双层主动防护", + "bothFieldAndFileEncryptionActive": "为了最大限度地保障安全,字段级和文件级加密现已同时启用。", + "fieldLevelAes256Encryption": "对敏感数据进行字段级 AES-256 加密", + "fileLevelDatabaseEncryption": "使用硬件绑定进行文件级数据库加密", + "hardwareBoundFileKeys": "硬件绑定的文件加密密钥", + "automaticEncryptedBackups": "自动创建加密备份", + "createEncryptedBackup": "创建加密备份", + "creatingBackup": "正在创建备份...", + "backupCreated": "备份已创建", + "encryptedBackupCreatedSuccessfully": "加密备份已成功创建", + "backupCreationFailed": "创建备份失败", + "databaseMigration": "数据库迁移", + "exportForMigration": "导出迁移", + "exportDatabaseForHardwareMigration": "将数据库导出为包含解密数据的 SQLite 文件,以便迁移到新硬件。", + "exportDatabase": "导出 SQLite 数据库", + "exporting": "导出中...", + "exportCreated": "SQLite导出已创建", + "exportContainsDecryptedData": "SQLite导出文件包含解密数据 - 请妥善保管!", + "databaseExportedSuccessfully": "SQLite数据库已成功导出", + "databaseExportFailed": "SQLite数据库导出失败", + "importFromMigration": "从迁移导入", + "importDatabaseFromAnotherSystem": "从其他系统或硬件导入 SQLite 数据库", + "importDatabase": "导入 SQLite 数据库", + "importing": "输入...", + "selectedFile": "选定的 SQLite 文件", + "importWillReplaceExistingData": "SQLite导入将会替换现有数据 - 建议备份!", + "pleaseSelectImportFile": "请选择 SQLite 导入文件", + "databaseImportedSuccessfully": "SQLite数据库已成功导入", + "databaseImportFailed": "SQLite数据库导入失败", + "manageEncryptionAndBackups": "管理加密密钥、数据库安全和备份操作", + "activeSecurityFeatures": "当前正在实施的安全措施和保护措施", + "deviceBindingTechnology": "先进的基于硬件的密钥保护技术", + "backupAndRecovery": "安全备份创建和数据库恢复选项", + "crossSystemDataTransfer": "在不同系统间导出和导入数据库", + "noMigrationNeeded": "无需迁移", + "encryptionKey": "加密密钥", + "keyProtection": "密钥保护", + "active": "操作", + "legacy": "遗产", + "dataStatus": "数据状态", + "encrypted": "加密", + "needsMigration": "需要迁移", + "ready": "准备好", + "initializeEncryption": "初始化加密", + "initialize": "初始化", + "test": "测试", + "migrate": "迁移", + "backup": "备份", + "createBackup": "创建备份", + "exportImport": "导出/导入", + "export": "导出", + "import": "导入", + "passwordRequired": "需要密码", + "confirmExport": "确认导出", + "exportDescription": "将 SSH 主机和凭证导出为 SQLite 文件", + "importDescription": "使用增量合并导入 SQLite 文件(跳过重复项)", + "criticalWarning": "严重警告", + "cannotDisablePasswordLoginWithoutOIDC": "未配置 OIDC 时无法禁用密码登录!您必须先配置 OIDC 身份验证,才能禁用密码登录,否则您将失去对 Termix 的访问权限。", + "confirmDisablePasswordLogin": "您确定要禁用密码登录吗?请确保 OIDC 配置正确且运行正常后再继续操作,否则您将失去对 Termix 实例的访问权限。", + "passwordLoginDisabled": "密码登录已成功禁用", + "passwordLoginAndRegistrationDisabled": "密码登录和新帐户注册功能已成功禁用", + "requiresPasswordLogin": "需要密码登录", + "passwordLoginDisabledWarning": "密码登录已禁用。请确保 OIDC 配置正确,否则您将无法登录 Termix。", + "oidcRequiredWarning": "重要提示:密码登录已禁用。如果您重置或错误配置 OIDC,您将失去对 Termix 的所有访问权限,并且您的实例将无法正常工作。请仅在您完全确定的情况下继续操作。", + "confirmDisableOIDCWarning": "警告:您即将禁用 OIDC,同时密码登录也已被禁用。这将导致您的 Termix 实例无法正常运行,您将失去所有访问权限。您确定要继续操作吗?", + "failedToUpdatePasswordLoginStatus": "更新密码登录状态失败", + "loadingSessions": "正在加载会话...", + "noActiveSessions": "未找到活动会话。", + "device": "设备", + "user": "用户", + "lastActive": "最后活跃时间", + "expires": "过期", + "revoked": "撤销", + "revokeAllUserSessionsTitle": "撤销此用户的所有会话", + "revokeAll": "撤销所有", + "linkOidcToPasswordAccount": "将 OIDC 帐户关联到密码帐户", + "linkOidcToPasswordAccountDescription": "将 {{username}}(OIDC 用户)关联到现有密码帐户。这将为该密码帐户启用双重身份验证。", + "linkOidcWarningTitle": "警告:OIDC用户数据将被删除", + "linkOidcWarningDescription": "此操作将:", + "linkOidcActionDeleteUser": "删除 OIDC 用户帐户及其所有数据", + "linkOidcActionAddCapability": "向目标密码帐户添加 OIDC 登录功能", + "linkOidcActionDualAuth": "允许密码帐户同时使用密码和 OIDC 登录", + "passwordMinLength": "密码长度必须至少为 6 个字符", + "currentRoles": "目前角色", + "noRolesAssigned": "未分配任何角色", + "assignNewRole": "分配新角色" + }, + "hosts": { + "title": "主机管理", + "sshHosts": "SSH主机", + "noHosts": "无 SSH 主机", + "noHostsMessage": "您尚未添加任何 SSH 主机。点击“添加主机”开始操作。", + "loadingHosts": "正在加载主机...", + "failedToLoadHosts": "主机加载失败", + "retry": "重试", + "refresh": "刷新", + "optional": "可选的", + "hostsCount": "{{count}} 台主机", + "importJson": "导入 JSON", + "importing": "导入中...", + "importJsonTitle": "从 JSON 导入 SSH 主机", + "importJsonDesc": "上传 JSON 文件以批量导入多个 SSH 主机(最多 100 个)。", + "downloadSample": "下载示例", + "formatGuide": "格式指南", + "exportCredentialWarning": "警告:主机“{{name}}”使用凭证身份验证。导出的文件将不包含凭证数据,导入后需要手动重新配置。是否继续?", + "exportSensitiveDataWarning": "警告:主机“{{name}}”包含敏感的身份验证数据(密码/SSH密钥)。导出的文件将以明文形式包含这些数据。请妥善保管该文件,并在使用后将其删除。是否继续?", + "uncategorized": "未分类", + "confirmDelete": "您确定要删除“{{name}}”吗?", + "failedToDeleteHost": "删除主机失败", + "failedToExportHost": "导出主机失败。请确保您已登录并拥有主机数据访问权限。", + "jsonMustContainHosts": "JSON 必须包含一个名为“hosts”的数组,或者本身就是一个 hosts 数组。", + "noHostsInJson": "JSON 文件中未找到主机", + "maxHostsAllowed": "每次导入最多允许 100 台主机", + "importCompleted": "导入完成:成功 {{success}} 个,失败 {{failed}} 个", + "importFailed": "导入失败", + "importError": "导入错误", + "failedToImportJson": "导入 JSON 文件失败", + "connectionDetails": "连接详情", + "organization": "组织", + "ipAddress": "IP地址", + "port": "端口", + "name": "名称", + "username": "用户名", + "folder": "文件夹", + "tags": "标签", + "pin": "置顶", + "notes": "描述", + "expirationDate": "截止日期", + "passwordRequired": "使用密码验证时,需要输入密码。", + "sshKeyRequired": "使用密钥认证时需要 SSH 私钥。", + "keyTypeRequired": "使用密钥认证时,需要指定密钥类型。", + "mustSelectValidSshConfig": "必须从列表中选择有效的 SSH 配置。", + "addHost": "添加主机", + "editHost": "编辑主机", + "cloneHost": "克隆主机", + "updateHost": "更新主机", + "hostUpdatedSuccessfully": "主机“{{name}}”已成功更新!", + "hostAddedSuccessfully": "主机“{{name}}”添加成功!", + "hostDeletedSuccessfully": "主机“{{name}}”已成功删除!", + "failedToSaveHost": "保存主机失败,请重试。", + "savingHost": "保存主机中……", + "updatingHost": "正在更新主机...", + "cloningHost": "克隆主机...", + "enableTerminal": "启用终端", + "enableTerminalDesc": "在终端选项卡中启用/禁用主机可见性", + "enableTunnel": "启用隧道", + "enableTunnelDesc": "在隧道选项卡中启用/禁用主机可见性", + "enableFileManager": "启用文件管理器", + "enableFileManagerDesc": "在文件管理器选项卡中启用/禁用主机可见性", + "enableDockerDesc": "在 Docker 选项卡中启用/禁用主机可见性", + "enableDocker": "启用 Docker", + "defaultPath": "默认路径", + "defaultPathDesc": "打开此主机的文件管理器时的默认目录", + "tunnelConnections": "隧道连接", + "connection": "连接", + "remove": "移除", + "sourcePort": "源端口", + "sourcePortDesc": "(来源指的是「常规」选项卡中的「当前连接详情」)", + "endpointPort": "端点端口", + "endpointSshConfig": "端点 SSH 配置", + "tunnelForwardDescription": "此隧道会将源机器(「常规」选项卡中的当前连接详情)上 {{sourcePort}} 端口的流量,转发至目标机器上的 {{endpointPort}} 端口。", + "maxRetries": "最大重试次数", + "maxRetriesDescription": "隧道连接的最大重试次数。", + "retryInterval": "重试间隔(秒)", + "retryIntervalDescription": "重试之间需要等待一段时间。", + "autoStartContainer": "容器启动时自动启动", + "autoStartDesc": "容器启动时自动启动此隧道", + "addConnection": "添加隧道连接", + "tunnelType": "隧道类型", + "tunnelTypeLocal": "本地(-L)", + "tunnelTypeRemote": "远程 (-R)", + "tunnelTypeLocalDesc": "转发本地端口到远程端点", + "tunnelTypeRemoteDesc": "转发远程端口到本地机", + "tunnelForwardDescriptionLocal": "此隧道会将本地端口 {{sourcePort}} 的流量转发到终端机器上的端口 {{endpointPort}} 。", + "tunnelForwardDescriptionRemote": "此隧道会将源机器(常规选项卡中的当前连接详细信息)上的端口 {{sourcePort}} 的流量转发到端点机器上的端口 {{endpointPort}} 。", + "sshpassRequired": "密码验证需要使用 SSH 密码", + "sshpassRequiredDesc": "对于隧道中的密码认证,系统必须安装 sshpass", + "otherInstallMethods": "其他安装方法:", + "debianUbuntuEquivalent": "(Debian/Ubuntu 或适用于您操作系统的同等版本)。", + "or": "或", + "centosRhelFedora": "CentOS/RHEL/Fedora", + "macos": "macOS", + "windows": "Windows (WSL, 无官方二进制)", + "sshServerConfigRequired": "需要配置 SSH 服务器", + "sshServerConfigDesc": "对于隧道连接,必须配置 SSH 服务器以允许端口转发:", + "gatewayPortsYes": "将远程端口绑定到所有接口", + "allowTcpForwardingYes": "启用端口转发", + "permitRootLoginYes": "如果使用root用户进行隧道连接", + "editSshConfig": "编辑 SSH 配置文件:", + "sshConfigPath": "/etc/ssh/sshd_config", + "restartSshService": "然后重启 SSH 服务:", + "restartSshCommand": "sudo systemctl 重启 sshd", + "upload": "上传", + "authentication": "验证", + "password": "密码", + "key": "密钥", + "credential": "凭证", + "none": "无", + "selectCredential": "选择凭证", + "selectCredentialPlaceholder": "选择一种凭证……", + "credentialRequired": "使用凭证身份验证时需要凭证。", + "credentialDescription": "选择凭证将覆盖当前用户名并使用该凭证的身份验证详细信息。", + "cannotChangeAuthAsSharedUser": "无法以共享用户身份更改身份验证", + "sshPrivateKey": "SSH 私钥", + "keyPassword": "密钥密码", + "keyType": "密钥类型", + "autoDetect": "自动检测", + "rsa": "RSA", + "ed25519": "ED25519", + "ecdsaNistP256": "ECDSA NIST P-256", + "ecdsaNistP384": "ECDSA NIST P-384", + "ecdsaNistP521": "ECDSA NIST P-521", + "dsa": "DSA", + "rsaSha2256": "RSA SHA2-256", + "rsaSha2512": "RSA SHA2-512", + "uploadFile": "上传文件", + "pasteKey": "粘贴密钥", + "updateKey": "更新密钥", + "existingKey": "现有密钥(点击更改)", + "existingCredential": "现有凭证(点击更改)", + "addTagsSpaceToAdd": "添加标签(空格添加)", + "terminalBadge": "终端", + "tunnelBadge": "隧道", + "fileManagerBadge": "文件管理器", + "general": "通用", + "terminal": "终端", + "tunnel": "隧道", + "fileManager": "文件管理器", + "serverStats": "服务器统计信息", + "hostViewer": "主机查看器", + "enableServerStats": "启用服务器统计信息", + "enableServerStatsDesc": "启用/禁用此主机的服务器统计信息收集", + "displayItems": "显示项", + "displayItemsDesc": "选择要在服务器统计页面上显示的指标", + "enableCpu": "CPU 使用率", + "enableMemory": "内存使用情况", + "enableDisk": "磁盘使用情况", + "enableNetwork": "网络统计数据(即将推出)", + "enableProcesses": "进程计数(即将推出)", + "enableUptime": "正常运行时间(即将推出)", + "enableHostname": "主机名(即将推出)", + "enableOs": "操作系统(即将推出)", + "customCommands": "自定义命令(即将推出)", + "customCommandsDesc": "为该服务器定义自定义关机和重启命令", + "shutdownCommand": "关闭命令", + "rebootCommand": "重启命令", + "confirmRemoveFromFolder": "您确定要从文件夹“{{folder}}”中删除“{{name}}”吗?主机将被移动到“无文件夹”区域。", + "removedFromFolder": "已成功从文件夹中移除主机“{{name}}”。", + "failedToRemoveFromFolder": "无法从文件夹中删除主机", + "folderRenamed": "文件夹“{{oldName}}”已成功重命名为“{{newName}}”。", + "failedToRenameFolder": "重命名文件夹失败", + "editFolderAppearance": "编辑文件夹外观", + "editFolderAppearanceDesc": "自定义文件夹的颜色和图标", + "folderColor": "文件夹颜色", + "folderIcon": "文件夹图标", + "preview": "预览", + "folderAppearanceUpdated": "文件夹外观已成功更新", + "failedToUpdateFolderAppearance": "文件夹外观更新失败", + "deleteAllHostsInFolder": "删除文件夹中的所有主机", + "confirmDeleteAllHostsInFolder": "您确定要删除文件夹“{{folder}}”中的所有 {{count}} 个主机吗?此操作无法撤销。", + "allHostsInFolderDeleted": "已成功从文件夹“{{folder}}”中删除 {{count}} 个主机", + "failedToDeleteHostsInFolder": "删除文件夹中的 hosts 文件失败", + "movedToFolder": "主机“{{name}}”已成功迁移至“{{folder}}”。", + "failedToMoveToFolder": "将主机移动到文件夹失败", + "clickToRenameFolder": "点击重命名文件夹", + "renameFolder": "重命名文件夹", + "removeFromFolder": "从文件夹“{{folder}}”中删除", + "editHostTooltip": "编辑主机", + "deleteHostTooltip": "删除主机", + "exportHostTooltip": "导出主机", + "cloneHostTooltip": "克隆主机", + "clickToEditHost": "点击编辑主机", + "dragToMoveBetweenFolders": "拖动即可在文件夹之间移动。", + "exportedHostConfig": "已导出 {{name}} 的主机配置", + "openTerminal": "打开终端", + "openFileManager": "打开文件管理器", + "openTunnels": "已开启隧道", + "openServerDetails": "打开服务器详情", + "statistics": "统计数据", + "enabledWidgets": "已启用的小部件", + "openServerStats": "打开服务器统计信息", + "enabledWidgetsDesc": "选择要为此主机显示的统计信息小部件", + "monitoringConfiguration": "监控配置", + "monitoringConfigurationDesc": "配置服务器统计信息和状态的检查频率", + "statusCheckEnabled": "启用状态监控", + "statusCheckEnabledDesc": "检查服务器是否在线或离线", + "statusCheckInterval": "状态检查间隔", + "statusCheckIntervalDesc": "检查主机是否在线的频率(5秒 - 1小时)", + "metricsEnabled": "启用指标监控", + "metricsEnabledDesc": "收集 CPU、RAM、磁盘和其他系统统计信息", + "metricsInterval": "指标收集间隔", + "metricsIntervalDesc": "服务器统计信息收集频率(5秒-1小时)", + "intervalSeconds": "秒", + "intervalMinutes": "分钟", + "intervalValidation": "监测间隔必须在 5 秒到 1 小时(3600 秒)之间。", + "monitoringDisabled": "此主机的服务器监控已禁用", + "enableMonitoring": "在主机管理器中启用监控 → 统计选项卡", + "monitoringDisabledBadge": "关闭监控", + "statusMonitoring": "状态", + "metricsMonitoring": "指标", + "terminalCustomization": "终端自定义", + "appearance": "外观", + "behavior": "行为", + "advanced": "高级", + "themePreview": "主题预览", + "theme": "主题", + "selectTheme": "选择主题", + "chooseColorTheme": "为终端选择颜色主题", + "fontFamily": "字体系列", + "selectFont": "选择字体", + "selectFontDesc": "选择终端中使用的字体。", + "fontSize": "字体大小", + "fontSizeValue": "字体大小:{{value}}px", + "adjustFontSize": "调整终端字体大小", + "letterSpacing": "字母间距", + "letterSpacingValue": "字母间距:{{value}}px", + "adjustLetterSpacing": "调整字符间距", + "lineHeight": "行高", + "lineHeightValue": "行高:{{value}}", + "adjustLineHeight": "调整行间距", + "cursorStyle": "光标样式", + "selectCursorStyle": "选择光标样式", + "cursorStyleBlock": "方块形", + "cursorStyleUnderline": "下划线形", + "cursorStyleBar": "竖线形", + "chooseCursorAppearance": "选择光标外观", + "cursorBlink": "光标闪烁", + "enableCursorBlink": "启用光标闪烁动画", + "scrollbackBuffer": "回滚缓冲区", + "scrollbackBufferValue": "回滚缓冲区:{{value}} 行", + "scrollbackBufferDesc": "回滚历史记录中要保留的行数", + "bellStyle": "提示音样式", + "selectBellStyle": "选择提示音样式", + "bellStyleNone": "无", + "bellStyleSound": "声音", + "bellStyleVisual": "视觉", + "bellStyleBoth": "两者皆有", + "bellStyleDesc": "终端提示音(BEL 字符,\\x07)的处理方式。程序在完成任务、遇到错误或发送通知时会触发该提示音。「声音」选项会播放音频提示音,「视觉」选项会使屏幕短暂闪烁,「两者皆有」会同时执行两种操作,「无」则禁用提示音告警。", + "rightClickSelectsWord": "右键单击选中单词", + "rightClickSelectsWordDesc": "右键单击会选中光标下方的单词", + "fastScrollModifier": "快速滚动修饰键", + "selectModifier": "选择修饰键", + "modifierAlt": "Alt", + "modifierCtrl": "Ctrl", + "modifierShift": "偏移", + "fastScrollModifierDesc": "快速滚动的修饰键", + "fastScrollSensitivity": "快速滚动灵敏度", + "fastScrollSensitivityValue": "快速滚动灵敏度:{{value}}", + "fastScrollSensitivityDesc": "按住修饰键时,滚动速度倍率会提高。", + "minimumContrastRatio": "最小对比度", + "minimumContrastRatioValue": "最小对比度:{{value}}", + "minimumContrastRatioDesc": "自动调整颜色以提高可读性", + "sshAgentForwarding": "SSH代理转发", + "sshAgentForwardingDesc": "转发 SSH 身份验证代理到远程主机", + "backspaceMode": "退格键模式", + "selectBackspaceMode": "选择退格键模式", + "backspaceModeNormal": "标准(DEL)", + "backspaceModeControlH": "控制(^H)", + "backspaceModeDesc": "为了兼容性,退格键的行为发生了变化", + "startupSnippet": "启动代码片段", + "selectSnippet": "选择代码片段", + "searchSnippets": "搜索代码片段……", + "snippetNone": "无", + "noneAuthTitle": "键盘交互认证", + "noneAuthDescription": "连接到 SSH 服务器时,此身份验证方法将使用键盘交互式身份验证。", + "noneAuthDetails": "键盘交互式身份验证允许服务器在连接过程中提示您输入凭证。这对于需要多因素身份验证的服务器或您不想在本地保存凭证的情况非常有用。", + "forceKeyboardInteractive": "强制键盘交互", + "forceKeyboardInteractiveDesc": "强制使用键盘交互式身份验证。对于使用双因素身份验证(TOTP/2FA)的服务器,有时需要这样做。", + "overrideCredentialUsername": "覆盖凭证用户名", + "overrideCredentialUsernameDesc": "使用与凭证中存储的用户名不同的用户名。这样,您就可以使用不同的用户名登录同一个凭证。", + "jumpHosts": "跳板主机", + "jumpHostsDescription": "跳转主机(也称为堡垒主机)允许您通过一个或多个中间服务器连接到目标服务器。这对于访问防火墙后或专用网络中的服务器非常有用。", + "jumpHostChain": "跳转宿主链", + "addJumpHost": "添加跳转主机", + "selectServer": "选择服务器", + "searchServers": "搜索服务器...", + "noServerFound": "未找到服务器", + "jumpHostsOrder": "连接将按以下顺序建立:跳转主机 1 → 跳转主机 2 → ... → 目标服务器", + "socks5Proxy": "SOCKS5代理", + "socks5Description": "配置 SOCKS5 代理以进行 SSH 连接。所有流量都将通过指定的代理服务器路由。", + "enableSocks5": "启用 SOCKS5 代理", + "enableSocks5Description": "此 SSH 连接请使用 SOCKS5 代理。", + "socks5Host": "代理主机", + "socks5Port": "代理端口", + "socks5Username": "代理用户名", + "socks5Password": "代理密码", + "socks5UsernameOptional": "可选:如果代理不需要身份验证,请留空。", + "socks5PasswordOptional": "可选:如果代理不需要身份验证,请留空。", + "socks5ProxyChain": "代理链", + "socks5ProxyChainDescription": "配置一个 SOCKS 代理链。链中的每个代理都会通过前一个代理连接。", + "socks5ProxyMode": "代理模式", + "socks5UseSingleProxy": "使用单一代理", + "socks5UseProxyChain": "使用代理链", + "socks5UsePreset": "使用已保存的预设", + "socks5SelectPreset": "选择预设", + "socks5ManagePresets": "管理预设", + "socks5ProxyNode": "代理 {{number}}", + "socks5AddProxy": "将代理添加到链", + "socks5RemoveProxy": "移除代理", + "socks5ProxyType": "代理类型", + "socks5SaveAsPreset": "另存为预设", + "socks5SavePresetTitle": "将代理链保存为预设", + "socks5SavePresetDescription": "将当前代理链配置保存为可重用的预设", + "socks5PresetName": "预设名称", + "socks5PresetDescription": "描述(可选)", + "socks5PresetCreated": "已创建代理链预设", + "socks5PresetUpdated": "代理链预设已更新", + "socks5PresetDeleted": "代理链预设已删除", + "socks5PresetSaved": "预设“{{name}}”已成功保存", + "socks5PresetSaveError": "保存预设失败", + "socks5PresetNameRequired": "预设名称为必填项", + "socks5EmptyChainError": "无法保存空的代理链", + "socks5ProxyChainEmpty": "至少向链中添加一个代理。", + "socks5HostDescription": "SOCKS代理服务器的主机名或IP地址", + "socks5PortDescription": "SOCKS代理服务器的端口号(默认值:1080)", + "addProxyNode": "添加代理节点", + "noProxyNodes": "未配置代理节点。点击“添加代理节点”添加一个。", + "proxyNode": "代理节点", + "proxyType": "代理类型", + "quickActions": "快速操作", + "quickActionsDescription": "快速操作功能允许您创建自定义按钮,这些按钮可以在此服务器上执行 SSH 代码片段。这些按钮将显示在服务器统计信息页面的顶部,以便快速访问。", + "quickActionsList": "快速操作列表", + "addQuickAction": "添加快速操作", + "quickActionName": "操作名称", + "noSnippetFound": "未找到代码片段", + "quickActionsOrder": "服务器统计页面上将按上述顺序显示快捷操作按钮。", + "sidebarCustomization": "侧边栏按钮", + "sidebarCustomizationDesc": "选择在侧边栏中显示快速按钮的动作。不显示为按钮的动作将出现在下拉菜单中。", + "showTerminalInSidebar": "显示终端按钮", + "showTerminalInSidebarDesc": "在侧边栏显示终端作为快捷按钮", + "showFileManagerInSidebar": "显示文件管理器按钮", + "showFileManagerInSidebarDesc": "在侧边栏显示文件管理器作为快捷按钮", + "showTunnelInSidebar": "显示隧道按钮", + "showTunnelInSidebarDesc": "在侧边栏显示隧道管理快速按钮", + "showDockerInSidebar": "显示 Docker 按钮", + "showDockerInSidebarDesc": "在侧边栏中以快捷按钮的形式显示 Docker 管理功能", + "showServerStatsInSidebar": "显示服务器统计信息按钮", + "showServerStatsInSidebarDesc": "在侧边栏中添加一个快捷按钮,显示服务器统计信息", + "atLeastOneActionRequired": "侧边栏中必须至少显示一个已启用的操作。", + "advancedAuthSettings": "高级身份验证设置", + "sudoPasswordAutoFill": "Sudo 密码自动填充", + "sudoPasswordAutoFillDesc": "当 sudo 提示输入密码时,自动提示输入 SSH 密码", + "sudoPassword": "Sudo 密码", + "sudoPasswordDesc": "sudo 命令的可选密码(与密钥认证结合使用时很有用)", + "socks4": "SOCKS4", + "socks5": "SOCKS5", + "executeSnippetOnConnect": "终端连接后执行一段代码片段", + "autoMosh": "自动 MOSH", + "autoMoshDesc": "连接时自动运行 MOSH 命令", + "moshCommand": "MOSH 命令", + "moshCommandDesc": "要执行的 MOSH 命令", + "environmentVariables": "环境变量", + "environmentVariablesDesc": "为终端会话设置自定义环境变量", + "variableName": "变量名", + "variableValue": "变量值", + "addVariable": "添加变量", + "docker": "停靠栏", + "openDocker": "打开 Docker", + "copyFullScreenUrl": "复制全屏网址", + "fullScreenUrlCopied": "全屏 URL 已复制到剪贴板", + "failedToCopyUrl": "无法将 URL 复制到剪贴板", + "copyTerminalUrl": "复制终端 URL", + "copyFileManagerUrl": "复制文件管理器 URL", + "copyTunnelUrl": "复制隧道 URL", + "copyServerStatsUrl": "复制服务器统计信息 URL", + "copyDockerUrl": "复制 Docker URL", + "fullScreenUrlTooltip": "复制 URL 以全屏模式打开此应用", + "notEnabled": "此主机未启用 Docker。请在主机设置中启用 Docker 以使用 Docker 功能。", + "validating": "正在验证 Docker...", + "error": "错误", + "errorCode": "错误代码:{{code}}", + "version": "Docker 版本 v{{version}}", + "current": "当前的", + "used_limit": "已用/限制", + "percentage": "百分比", + "input": "输入", + "output": "输出", + "read": "读", + "write": "写", + "pids": "PIDs", + "id": "ID", + "state": "状态", + "console": "控制台", + "containerMustBeRunning": "容器必须正在运行才能连接到控制台。", + "authenticationRequired": "需要身份验证", + "connectedTo": "连接到 {{containerName}}", + "disconnected": "断开连接", + "consoleError": "控制台错误", + "errorMessage": "错误:{{message}}", + "failedToConnect": "连接控制台失败", + "disconnectedFromContainer": "已断开与容器控制台的连接。", + "containerNotRunning": "容器未运行", + "startContainerToAccess": "启动容器以访问控制台", + "selectShell": "选择shell", + "bash": "巴什文", + "sh": "沙子", + "ash": "Ash", + "connecting": "正在连接...", + "connect": "连接", + "disconnect": "断开", + "notConnected": "未连接", + "clickToConnect": "点击“连接”启动交互式 shell", + "connectingTo": "正在连接到 {{containerName}}...", + "containerMustBeRunningToViewStats": "容器必须处于运行状态才能查看统计信息。", + "failedToFetchStats": "获取统计数据失败", + "noContainersFound": "未找到容器", + "noContainersFoundHint": "首先在服务器上创建容器。", + "searchPlaceholder": "按名称、镜像、ID 搜索……", + "filterByStatusPlaceholder": "按状态筛选", + "allContainersCount": "全部 ({{count}})", + "statusCount": "{{status}} ({{count}})", + "noContainersMatchFilters": "没有容器符合您的筛选条件", + "noContainersMatchFiltersHint": "尝试调整搜索或筛选条件", + "containerStarted": "容器 {{name}} 已启动", + "failedToStartContainer": "容器启动失败:{{error}}", + "containerStopped": "容器 {{name}} 已停止", + "failedToStopContainer": "停止容器失败:{{error}}", + "containerRestarted": "容器 {{name}} 已重启", + "failedToRestartContainer": "容器重启失败:{{error}}", + "containerUnpaused": "容器 {{name}} 已解除暂停", + "containerPaused": "容器 {{name}} 已暂停", + "failedToTogglePauseContainer": "{{action}} 容器失败:{{error}}", + "containerRemoved": "容器 {{name}} 已移除", + "failedToRemoveContainer": "移除容器失败:{{error}}", + "image": "镜像:", + "idLabel": "ID:", + "ports": "端口:", + "noPorts": "无", + "created": "创建时间:", + "start": "开始", + "stop": "停止", + "unpause": "取消暂停", + "pause": "暂停", + "restart": "重启", + "removeContainer": "移除容器", + "confirmRemoveContainer": "您确定要删除容器“{{name}}”吗?", + "runningContainerWarning": "警告:此容器当前正在运行,将被强制移除。", + "removing": "移除:", + "containerNotFound": "未找到容器", + "backToList": "返回列表", + "logs": "日志", + "stats": "统计数据", + "consoleTab": "控制台", + "failedToFetchLogs": "获取日志失败:{{error}}", + "failedToDownloadLogs": "日志下载失败:{{error}}", + "linesToShow": "显示行数", + "last50Lines": "最后50行", + "last100Lines": "最后100行", + "last500Lines": "最后500行", + "last1000Lines": "最后1000行", + "allLogs": "所有日志", + "showTimestamps": "显示时间戳", + "autoRefresh": "自动刷新", + "filterLogsPlaceholder": "过滤日志...", + "noLogsAvailable": "暂无日志数据" + }, + "terminal": { + "title": "终端", + "connect": "连接到主机", + "disconnect": "断开", + "clear": "清空", + "copy": "复制", + "paste": "粘贴", + "find": "寻找", + "fullscreen": "全屏", + "splitHorizontal": "水平分割", + "splitVertical": "垂直分割", + "closePanel": "关闭面板", + "reconnect": "重新连接", + "sessionEnded": "会话结束", + "connectionLost": "连接丢失", + "error": "错误:{{message}}", + "disconnected": "断开连接", + "connectionClosed": "连接已关闭", + "connectionError": "连接错误:{{message}}", + "connected": "已连接", + "sshConnected": "已建立 SSH 连接", + "authError": "身份验证失败:{{message}}", + "unknownError": "发生未知错误", + "messageParseError": "解析服务器消息失败", + "websocketError": "WebSocket 连接错误", + "connecting": "正在连接...", + "reconnecting": "正在重新连接... ({{attempt}}/{{max}})", + "reconnected": "已成功重新连接", + "maxReconnectAttemptsReached": "已达到最大重连尝试次数", + "connectionTimeout": "连接超时", + "terminalTitle": "终端 - {{host}}", + "terminalWithPath": "终端 - {{host}}:{{path}}", + "runTitle": "运行 {{command}} - {{host}}", + "totpRequired": "需要双因素身份验证", + "totpCodeLabel": "验证码", + "totpPlaceholder": "000000", + "totpVerify": "验证", + "warpgateAuthRequired": "需要传送门认证", + "warpgateSecurityKey": "安全密钥", + "warpgateAuthUrl": "身份验证 URL", + "warpgateOpenBrowser": "在浏览器中打开", + "warpgateContinue": "我已经完成身份验证", + "warpgateTimeout": "Warpgate身份验证超时,请重新连接。", + "sudoPasswordPopupTitle": "输入密码?", + "sudoPasswordPopupHint": "按 Enter 键插入,按 Esc 键关闭", + "sudoPasswordPopupConfirm": "确认", + "sudoPasswordPopupDismiss": "取消", + "websocketAbnormalClose": "连接意外关闭。这可能是由于反向代理或 SSL 配置问题导致的。请检查服务器日志。", + "totpInvalid": "验证码无效,请重试。", + "authMethodFallback": "{{method}} 身份验证失败。正在尝试其他方法……", + "authenticationRequired": "需要身份验证", + "connectionLogTitle": "连接日志", + "connectionLogCopy": "将日志复制到剪贴板", + "connectionLogClear": "清除日志", + "connectionLogEmpty": "暂无连接日志", + "connectionLogConnecting": "正在连接主机...", + "connectionLogCopied": "连接日志已复制到剪贴板", + "connectionLogCopyFailed": "无法将日志复制到剪贴板", + "allAuthMethodsFailed": "所有身份验证方式均失败。请检查您的凭据。", + "methodNotSupported": "服务器不支持 {{method}} 身份验证。可用方法: {{available}}", + "retryWithMethod": "使用 {{method}}重试", + "automaticFallback": "正在自动尝试 {{method}} 身份验证...", + "totpTimeout": "TOTP验证超时,请重新连接。", + "passwordTimeout": "密码验证超时,请重新连接。", + "connectionRejected": "服务器拒绝连接。请检查您的身份验证和网络配置。" + }, + "fileManager": { + "title": "文件管理器", + "file": "文件", + "folder": "文件夹", + "connectToSsh": "连接到 SSH 以使用文件操作", + "uploadFile": "上传文件", + "downloadFile": "下载", + "extractArchive": "解压归档文件", + "extractingArchive": "正在提取 {{name}}...", + "archiveExtractedSuccessfully": "{{name}} 提取成功", + "extractFailed": "提取失败", + "compressFile": "压缩文件", + "compressFiles": "压缩多个文件", + "compressFilesDesc": "将 {{count}} 个项目压缩为归档文件", + "archiveName": "归档文件名", + "enterArchiveName": "请输入归档文件名...", + "compressionFormat": "压缩格式", + "selectedFiles": "选定文件", + "andMoreFiles": "及 {{count}} 个其他文件...", + "compress": "压缩", + "compressingFiles": "正在将 {{count}} 个项目压缩为 {{name}}...", + "filesCompressedSuccessfully": "{{name}} 创建成功", + "compressFailed": "压缩失败", + "edit": "编辑", + "preview": "预览", + "previous": "上一页", + "next": "下一页", + "pageXOfY": "第 {{current}} 页 / 共 {{total}} 页", + "zoomOut": "缩小", + "zoomIn": "放大", + "newFile": "新建文件", + "newFolder": "新建文件夹", + "rename": "重命名", + "renameItem": "重命名项目", + "deleteItem": "删除项目", + "currentPath": "当前路径", + "uploadFileTitle": "上传文件", + "maxFileSize": "最大:1GB(JSON)/ 5GB(二进制) - 支持大文件上传", + "removeFile": "删除文件", + "clickToSelectFile": "点击选择文件", + "chooseFile": "选择文件", + "uploading": "正在上传...", + "downloading": "正在下载……", + "uploadingFile": "正在上传 {{name}}...", + "uploadingLargeFile": "正在上传大文件 {{name}} ({{size}})...", + "downloadingFile": "正在下载 {{name}}...", + "creatingFile": "正在创建文件 {{name}}...", + "creatingFolder": "正在创建文件夹 {{name}}...", + "deletingItem": "正在删除 {{type}} {{name}}...", + "renamingItem": "正在将 {{type}} {{oldName}} 重命名为 {{newName}}...", + "createNewFile": "创建新文件", + "fileName": "文件名", + "creating": "正在创建……", + "createFile": "创建文件", + "createNewFolder": "创建新文件夹", + "folderName": "文件夹名称", + "createFolder": "创建文件夹", + "warningCannotUndo": "警告:此操作无法撤销。", + "itemPath": "项目路径", + "thisIsDirectory": "这是一个目录(将递归删除)", + "deleting": "正在删除……", + "currentPathLabel": "当前路径", + "newName": "新名称", + "thisIsDirectoryRename": "这是一个目录", + "renaming": "重命名...", + "fileUploadedSuccessfully": "文件“{{name}}”已成功上传", + "failedToUploadFile": "文件上传失败", + "fileDownloadedSuccessfully": "文件“{{name}}”已成功下载", + "failedToDownloadFile": "文件下载失败", + "noFileContent": "未收到文件内容", + "filePath": "文件路径", + "fileCreatedSuccessfully": "文件“{{name}}”创建成功", + "failedToCreateFile": "文件创建失败", + "folderCreatedSuccessfully": "文件夹“{{name}}”已成功创建", + "failedToCreateFolder": "创建文件夹失败", + "failedToCreateItem": "创建项目失败", + "operationFailed": "{{operation}} {{name}} 操作失败,: {{error}}", + "failedToResolveSymlink": "解析符号链接失败", + "itemDeletedSuccessfully": "{{type}} 已成功删除", + "itemsDeletedSuccessfully": "已成功删除 {{count}} 个项目", + "failedToDeleteItems": "删除项目失败", + "sudoPasswordRequired": "需要管理员密码", + "enterSudoPassword": "请输入 sudo 密码以继续此操作", + "sudoPassword": "Sudo 密码", + "sudoOperationFailed": "sudo 操作失败", + "sudoAuthFailed": "sudo 身份验证失败", + "deleteOperation": "删除文件/文件夹", + "dragFilesToUpload": "文件拖放到这里即可上传", + "emptyFolder": "此文件夹为空。", + "itemCount": "{{count}} 个项目", + "selectedCount": "{{count}} 已选", + "searchFiles": "搜索文件...", + "upload": "上传", + "selectHostToStart": "选择主机以启动文件管理", + "failedToConnect": "SSH连接失败。", + "failedToLoadDirectory": "加载目录失败", + "noSSHConnection": "没有可用的 SSH 连接", + "enterFolderName": "请输入文件夹名称:", + "enterFileName": "请输入文件名:", + "copy": "复制", + "cut": "剪切", + "paste": "粘贴", + "copyPath": "复制路径", + "copyPaths": "复制路径", + "delete": "删除", + "properties": "属性", + "refresh": "刷新", + "downloadFiles": "将 {{count}} 个文件下载至浏览器", + "copyFiles": "复制 {{count}} 个项目", + "cutFiles": "剪切 {{count}} 个项目", + "deleteFiles": "删除 {{count}} 个项目", + "filesCopiedToClipboard": "已将 {{count}} 个项目复制到剪贴板", + "filesCutToClipboard": "{{count}} 个项目已剪切至剪贴板", + "pathCopiedToClipboard": "路径已复制到剪贴板", + "pathsCopiedToClipboard": "已将 {{count}} 个路径复制到剪贴板", + "failedToCopyPath": "无法将路径复制到剪贴板", + "movedItems": "已移动 {{count}} 个项目", + "failedToDeleteItem": "删除项目失败", + "itemRenamedSuccessfully": "{{type}} 已成功重命名", + "failedToRenameItem": "重命名项目失败", + "download": "下载", + "permissions": "权限", + "size": "大小", + "modified": "修改时间", + "path": "路径", + "confirmDelete": "你确定要删除 {{name}} 吗?", + "uploadSuccess": "文件上传成功", + "uploadFailed": "文件上传失败", + "downloadSuccess": "文件下载成功", + "downloadFailed": "文件下载失败", + "permissionDenied": "没有权限", + "checkDockerLogs": "查看 Docker 日志以获取详细的错误信息", + "internalServerError": "发生内部服务器错误", + "serverError": "服务器错误", + "error": "错误", + "requestFailed": "请求失败,状态码", + "unknownFileError": "未知", + "cannotReadFile": "无法读取文件", + "noSshSessionId": "没有可用的 SSH 会话 ID", + "noFilePath": "没有可用的文件路径", + "noCurrentHost": "当前没有可用主机", + "fileSavedSuccessfully": "文件已成功保存", + "saveTimeout": "保存操作超时。文件可能已成功保存,但操作耗时过长。请查看 Docker 日志以确认。", + "failedToSaveFile": "文件保存失败", + "deletedSuccessfully": "已成功删除", + "connectToServer": "连接到服务器", + "selectServerToEdit": "从侧边栏选择服务器即可开始编辑文件", + "fileOperations": "文件操作", + "confirmDeleteMessage": "您确定要删除 {{name}} 吗?", + "confirmDeleteSingleItem": "您确定要永久删除“{{name}}”吗?", + "confirmDeleteMultipleItems": "您确定要永久删除 {{count}} 个项目吗?", + "confirmDeleteMultipleItemsWithFolders": "您确定要永久删除 {{count}} 个项目吗?这包括文件夹及其内容。", + "confirmDeleteFolder": "您确定要永久删除文件夹“{{name}}”及其所有内容吗?", + "deleteDirectoryWarning": "这将删除该文件夹及其所有内容。", + "actionCannotBeUndone": "此操作无法撤销。", + "permanentDeleteWarning": "此操作不可撤销,相关项目将从服务器中被永久删除。", + "recent": "最近", + "pinned": "置顶", + "folderShortcuts": "文件夹快捷方式", + "noRecentFiles": "没有最近文件。", + "noPinnedFiles": "没有已置顶的文件。", + "enterFolderPath": "输入文件夹路径", + "noShortcuts": "暂无快捷方式。", + "searchFilesAndFolders": "搜索文件和文件夹……", + "noFilesOrFoldersFound": "未找到任何文件或文件夹。", + "failedToConnectSSH": "SSH连接失败。", + "failedToReconnectSSH": "SSH 会话重新连接失败", + "failedToListFiles": "文件列表失败", + "fetchHomeDataTimeout": "获取home数据超时", + "sshStatusCheckTimeout": "SSH 状态检查超时", + "sshReconnectionTimeout": "SSH 重新连接超时", + "saveOperationTimeout": "保存操作超时", + "cannotSaveFile": "无法保存文件", + "dragSystemFilesToUpload": "将系统文件拖到此处上传", + "dragFilesToWindowToDownload": "将文件拖出窗口即可下载", + "openTerminalHere": "在此处打开终端", + "run": "运行", + "saveToSystem": "另存为...", + "selectLocationToSave": "选择保存位置", + "openTerminalInFolder": "在此文件夹中打开终端", + "openTerminalInFileLocation": "打开终端,指向文件位置", + "terminalWithPath": "终端 - {{host}}:{{path}}", + "runningFile": "运行 - {{file}}", + "onlyRunExecutableFiles": "只能运行可执行文件", + "noHostSelected": "未选择主机", + "starred": "已标星", + "shortcuts": "快捷方式", + "directories": "目录", + "removedFromRecentFiles": "已将“{{name}}”从最近文件中移除", + "removeFailed": "移除失败", + "unpinnedSuccessfully": "已成功取消置顶“{{name}}”", + "unpinFailed": "取消置顶失败", + "removedShortcut": "已移除快捷方式“{{name}}”", + "removeShortcutFailed": "移除快捷方式失败", + "clearedAllRecentFiles": "已清除所有最近文件", + "clearFailed": "清除失败", + "removeFromRecentFiles": "从最近文件中删除", + "clearAllRecentFiles": "清除所有最近文件", + "unpinFile": "取消置顶文件", + "removeShortcut": "移除快捷方式", + "saveFilesToSystem": "将 {{count}} 个文件另存为...", + "pinFile": "置顶文件", + "addToShortcuts": "添加到快捷方式", + "downloadToDefaultLocation": "下载到默认位置", + "pasteFailed": "粘贴失败", + "noUndoableActions": "没有可撤销的操作", + "undoCopySuccess": "撤销复制操作:已删除 {{count}} 个已复制的文件", + "undoCopyFailedDelete": "撤销失败:无法删除任何已复制的文件", + "undoCopyFailedNoInfo": "撤销失败:找不到已复制的文件信息", + "undoMoveSuccess": "撤销移动操作:已将 {{count}} 个文件移回原始位置", + "undoMoveFailedMove": "撤销失败:无法将任何文件移回。", + "undoMoveFailedNoInfo": "撤销失败:找不到已移动的文件信息", + "undoDeleteNotSupported": "删除操作无法撤销:文件已从服务器永久删除。", + "undoTypeNotSupported": "不支持的撤销操作类型", + "undoOperationFailed": "撤销操作失败", + "unknownError": "未知错误", + "enterPath": "输入路径...", + "editPath": "编辑路径", + "confirm": "确认", + "cancel": "取消", + "find": "查找...", + "replaceWith": "替换为……", + "replace": "替换", + "replaceAll": "全部替换", + "downloadInstead": "下载", + "keyboardShortcuts": "快捷键", + "searchAndReplace": "查找与替换", + "editing": "编辑", + "navigation": "导航", + "code": "代码", + "search": "搜索", + "findNext": "查找下一个", + "findPrevious": "查找上一个", + "save": "保存", + "selectAll": "全选", + "undo": "撤销", + "redo": "重做", + "goToLine": "跳转到行", + "moveLineUp": "上移行", + "moveLineDown": "下移行", + "toggleComment": "切换注释", + "indent": "增加缩进", + "outdent": "减少缩进", + "autoComplete": "自动补全", + "imageLoadError": "图片加载失败", + "rotate": "旋转", + "originalSize": "原尺寸", + "startTyping": "开始输入……", + "unknownSize": "尺寸未知", + "fileIsEmpty": "文件为空", + "largeFileWarning": "大文件警告", + "largeFileWarningDesc": "此文件大小为 {{size}},以文本格式打开时可能会导致性能问题。", + "fileNotFoundAndRemoved": "文件“{{name}}”未找到,已从最近/置顶文件中移除。", + "failedToLoadFile": "文件加载失败:{{error}}", + "serverErrorOccurred": "服务器出错,请稍后再试。", + "autoSaveFailed": "自动保存失败", + "fileAutoSaved": "文件自动保存", + "moveFileFailed": "移动 {{name}} 失败", + "moveOperationFailed": "移动操作失败", + "canOnlyCompareFiles": "只能比较两个文件", + "comparingFiles": "正在比较文件:{{file1}} 和 {{file2}}", + "dragFailed": "拖拽操作失败", + "filePinnedSuccessfully": "文件“{{name}}”已成功固定", + "pinFileFailed": "文件固定失败", + "fileUnpinnedSuccessfully": "文件“{{name}}”已成功取消固定", + "unpinFileFailed": "取消固定文件失败", + "shortcutAddedSuccessfully": "文件夹快捷方式“{{name}}”已成功添加", + "addShortcutFailed": "添加快捷方式失败", + "operationCompletedSuccessfully": "{{operation}} {{count}} 个项目成功", + "operationCompleted": "{{operation}} {{count}} 个项目", + "downloadFileSuccess": "文件 {{name}} 下载成功", + "downloadFileFailed": "下载失败", + "moveTo": "移至 {{name}}", + "diffCompareWith": "比较与 {{name}} 的差异", + "dragOutsideToDownload": "向外拖动窗口即可下载({{count}} 个文件)", + "newFolderDefault": "新建文件夹", + "newFileDefault": "新建文件 txt", + "successfullyMovedItems": "已成功将 {{count}} 个项目移动到 {{target}}", + "move": "移动", + "searchInFile": "在文件中搜索(Ctrl+F)", + "showKeyboardShortcuts": "显示快捷键", + "startWritingMarkdown": "开始编写你的 Markdown 内容……", + "loadingFileComparison": "正在加载文件比较...", + "reload": "重新加载", + "compare": "对比", + "sideBySide": "左右分栏", + "inline": "行内对比", + "fileComparison": "文件对比:{{file1}} 与 {{file2}}", + "fileTooLarge": "文件过大:{{error}}", + "sshConnectionFailed": "SSH 连接失败,请检查你与 {{name}}({{ip}}:{{port}})的连接状态", + "loadFileFailed": "文件加载失败:{{error}}", + "connecting": "正在连接...", + "connectedSuccessfully": "连接成功", + "totpVerificationFailed": "TOTP验证失败", + "warpgateVerificationFailed": "Warpgate 身份验证失败", + "authenticationFailed": "身份验证失败", + "verificationCodePrompt": "验证码:", + "changePermissions": "更改权限", + "changePermissionsDesc": "修改文件权限", + "currentPermissions": "当前权限", + "newPermissions": "新权限", + "owner": "所有者", + "group": "用户组", + "others": "其他的", + "read": "读", + "write": "写", + "execute": "执行", + "permissionsChangedSuccessfully": "权限已成功更改", + "failedToChangePermissions": "更改权限失败" + }, + "tunnel": { + "log": { + "connecting": "建立隧道连接……", + "sourceConnecting": "正在连接到源主机 {{host}}...", + "sourceConnected": "已连接到源主机", + "endpointResolving": "正在解析端点主机 {{host}}...", + "endpointResolved": "端点主机已解析", + "forwarding": "设置端口转发 {{localPort}} → {{remotePort}}...", + "connected": "隧道已成功建成", + "retrying": "连接失败,将在 {{countdown}}秒后重试(尝试 {{attempt}}/{{max}})。", + "retryExhausted": "已达到最大重试次数", + "error": "隧道连接失败: {{error}}" + } + }, + "tunnels": { + "title": "SSH隧道", + "noSshTunnels": "没有 SSH 隧道", + "createFirstTunnelMessage": "您尚未创建任何 SSH 隧道。请在主机管理器中配置隧道连接以开始操作。", + "connected": "已连接", + "disconnected": "断开连接", + "connecting": "正在连接...", + "disconnecting": "正在断开连接……", + "unknownTunnelStatus": "未知", + "statusUnknown": "未知", + "unknown": "未知", + "error": "错误", + "failed": "失败的", + "retrying": "重试", + "waiting": "等待", + "waitingForRetry": "等待重试", + "retryingConnection": "正在重试连接", + "canceling": "正在取消……", + "connect": "连接", + "disconnect": "断开", + "cancel": "取消", + "port": "港口", + "attempt": "尝试 {{current}} 次,共 {{max}} 次", + "nextRetryIn": "下次重试时间为 {{seconds}} 秒", + "checkDockerLogs": "检查 Docker 日志以查找错误原因,并加入讨论。", + "orCreate": "或者创建一个", + "noTunnelConnections": "未配置隧道连接", + "tunnelConnections": "隧道连接", + "addTunnel": "添加隧道", + "editTunnel": "编辑隧道", + "deleteTunnel": "删除隧道", + "tunnelName": "隧道名称", + "localPort": "本地端口", + "remoteHost": "远程主机", + "remotePort": "远程端口", + "autoStart": "自动启动", + "status": "状态", + "active": "已激活", + "inactive": "未激活", + "start": "开始", + "stop": "停止", + "restart": "重启", + "connectionType": "连接类型", + "local": "本地", + "remote": "远程", + "dynamic": "动态", + "unknownConnectionStatus": "未知", + "portMapping": "端口 {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", + "endpointHostNotFound": "未找到目标主机", + "discord": "Discord", + "githubIssue": "GitHub 反馈", + "forHelp": "寻求帮助" + }, + "serverStats": { + "title": "服务器统计信息", + "cpu": "CPU", + "memory": "内存", + "disk": "磁盘", + "network": "网络", + "uptime": "运行时长", + "loadAverage": "平均负载:{{avg1}}、{{avg5}}、{{avg15}}", + "processes": "进程数", + "connections": "连接数", + "usage": "已使用", + "available": "可用", + "total": "总计", + "free": "空闲", + "used": "已使用", + "percentage": "占比", + "refreshStatusAndMetrics": "刷新状态和指标", + "refreshStatus": "刷新状态", + "fileManagerAlreadyOpen": "该主机的文件管理器已打开", + "openFileManager": "打开文件管理器", + "connecting": "正在连接...", + "connectionFailed": "连接服务器失败", + "cpuCores_one": "{{count}} CPU", + "cpuCores_other": "{{count}} CPU", + "naCpus": "CPU 信息未获取", + "loadAverageNA": "平均负载:未获取", + "cpuUsage": "CPU 使用率", + "memoryUsage": "内存使用率", + "diskUsage": "磁盘使用率", + "rootStorageSpace": "根目录存储空间", + "of": "共", + "feedbackMessage": "对服务器管理的未来发展方向有什么想法吗?欢迎分享!", + "failedToFetchHostConfig": "获取主机配置失败", + "failedToFetchStatus": "获取服务器状态失败", + "failedToFetchMetrics": "获取服务器指标失败", + "failedToFetchHomeData": "获取home数据失败", + "loadingMetrics": "正在加载指标...", + "refreshing": "正在刷新...", + "serverOffline": "服务器离线", + "cannotFetchMetrics": "无法从离线服务器获取指标", + "totpRequired": "需要 TOTP 身份验证", + "totpUnavailable": "启用 TOTP 的服务器无法获取服务器统计信息。", + "totpVerified": "TOTP 已验证,指标采集已开始", + "totpFailed": "TOTP验证失败", + "totpInvalidCode": "验证码无效", + "totpCancelled": "指标采集已取消", + "authenticationFailed": "身份验证失败", + "noneAuthNotSupported": "服务器统计信息不支持“无”身份验证类型。", + "load": "加载", + "editLayout": "编辑布局", + "cancelEdit": "取消", + "addWidget": "添加小部件", + "saveLayout": "保存布局", + "unsavedChanges": "存在未保存的更改", + "layoutSaved": "布局已成功保存", + "failedToSaveLayout": "布局保存失败", + "systemInfo": "系统信息", + "hostname": "主机名", + "operatingSystem": "操作系统", + "kernel": "核心", + "totalUptime": "总运行时长", + "seconds": "秒", + "networkInterfaces": "网络接口", + "noInterfacesFound": "未找到网络接口", + "totalProcesses": "进程总数", + "running": "运行中", + "noProcessesFound": "未找到任何进程", + "loginStats": "SSH 登录统计信息", + "totalLogins": "总登录次数", + "uniqueIPs": "独立 IP 数", + "recentSuccessfulLogins": "最近成功登录记录", + "recentFailedAttempts": "最近登录失败尝试", + "noRecentLoginData": "暂无最近登录数据", + "from": "来自", + "quickActions": "快速操作", + "executeQuickAction": "执行 {{name}}", + "executingQuickAction": "正在执行 {{name}}...", + "quickActionSuccess": "{{name}} 已成功完成", + "quickActionFailed": "{{name}} 失败", + "quickActionError": "执行 {{name}} 失败", + "ports": { + "title": "监听端口", + "protocol": "协议", + "port": "港口", + "address": "地址", + "state": "状态", + "process": "过程", + "noData": "无监听端口数据" + }, + "firewall": { + "title": "防火墙", + "active": "积极的", + "inactive": "非活跃状态", + "notDetected": "未检测到", + "policy": "政策", + "rules": "规则", + "noRules": "没有规则", + "noData": "没有可用的防火墙数据", + "action": "行动", + "protocol": "原型", + "port": "港口", + "source": "来源", + "accept": "接受", + "drop": "降低", + "reject": "拒绝", + "anywhere": "任何地方" + } + }, + "auth": { + "tagline": "SSH 服务器管理器", + "description": "安全、强大且直观的 SSH 连接管理", + "welcomeBack": "欢迎回到 TERMIX", + "createAccount": "创建您的 TERMIX 帐户", + "continueExternal": "通过第三方账号继续", + "loginTitle": "登录 Termix", + "registerTitle": "创建账户", + "loginButton": "登录", + "registerButton": "注册", + "forgotPassword": "忘记密码?", + "rememberMe": "记住我", + "noAccount": "还没有账号?", + "hasAccount": "已有账号?", + "loginSuccess": "登录成功", + "loginFailed": "登录失败", + "registerSuccess": "注册成功", + "registerFailed": "注册失败", + "logoutSuccess": "已成功登出", + "invalidCredentials": "用户名或密码无效", + "accountCreated": "账号创建成功", + "passwordReset": "密码重置链接已发送", + "twoFactorAuth": "双因素身份验证", + "enterCode": "请输入验证码", + "backupCode": "或者使用备用代码", + "verifyCode": "验证码", + "redirectingToApp": "正在重定向到应用程序...", + "enableTwoFactor": "启用双因素身份验证", + "disableTwoFactor": "禁用双因素身份验证", + "scanQRCode": "使用您的身份验证器应用程序扫描此二维码", + "backupCodes": "备用码", + "saveBackupCodes": "将这些备用码保存在安全的位置", + "twoFactorEnabledSuccess": "双因素认证已成功启用!", + "twoFactorDisabled": "双因素身份验证已禁用", + "newBackupCodesGenerated": "已生成新的备用码", + "backupCodesDownloaded": "备用码已下载", + "pleaseEnterSixDigitCode": "请输入6位数字验证码", + "invalidVerificationCode": "验证码无效", + "failedToDisableTotp": "禁用 TOTP 失败", + "failedToGenerateBackupCodes": "生成备用码失败", + "enterPassword": "请输入您的密码", + "lockedOidcAuth": "已锁定(OIDC 身份验证)", + "twoFactorTitle": "双因素身份验证", + "twoFactorProtected": "您的帐户已启用双重身份验证保护", + "twoFactorActive": "您的帐户目前已启用双重身份验证", + "disable2FA": "禁用 2FA", + "disableTwoFactorWarning": "禁用双重身份验证会降低您的帐户安全性。", + "passwordOrTotpCode": "密码或 TOTP 代码", + "or": "或", + "generateNewBackupCodesText": "若你已丢失现有备用码,可生成新的备用码", + "generateNewBackupCodes": "生成新备用码", + "yourBackupCodes": "你的备用码", + "download": "下载", + "setupTwoFactorTitle": "设置双因素身份验证", + "sshAuthenticationRequired": "需要 SSH 身份验证", + "sshNoKeyboardInteractive": "键盘交互认证不可用", + "sshAuthenticationFailed": "身份验证失败", + "sshAuthenticationTimeout": "身份验证超时", + "sshNoKeyboardInteractiveDescription": "服务器不支持键盘交互认证。请提供你的密码或 SSH 密钥。", + "sshAuthFailedDescription": "提供的凭证无效。请使用有效的凭证重试。", + "sshTimeoutDescription": "身份验证尝试超时,请重试。", + "sshProvideCredentialsDescription": "请提供您的 SSH 凭证以连接到此服务器。", + "sshPasswordDescription": "请输入此SSH连接的密码。", + "sshKeyPasswordDescription": "如果您的 SSH 密钥已加密,请在此处输入密码。", + "step1ScanQR": "步骤 1:使用您的身份验证器应用程序扫描二维码", + "manualEntryCode": "手动输入代码", + "cannotScanQRText": "如果无法扫描二维码,请在您的身份验证器应用程序中手动输入此代码。", + "nextVerifyCode": "下一步:验证代码", + "verifyAuthenticator": "验证您的身份验证器", + "step2EnterCode": "步骤 2:输入身份验证器应用程序中显示的 6 位数代码", + "verificationCode": "验证码", + "back": "返回", + "verifyAndEnable": "验证并启用", + "saveBackupCodesTitle": "保存你的备用码", + "step3StoreCodesSecurely": "步骤 3:将这些码保存在安全的地方", + "importantBackupCodesText": "请将这些备用码保存在安全的地方。如果您丢失了验证器设备,可以使用这些代码访问您的帐户。", + "completeSetup": "完成设置", + "notEnabledText": "双因素身份验证通过在登录时要求输入来自身份验证器应用程序的验证码来增加一层额外的安全保障。", + "enableTwoFactorButton": "启用双因素身份验证", + "addExtraSecurityLayer": "为您的账户增加一层额外的安全保障", + "firstUser": "首位用户", + "firstUserMessage": "您是第一个用户,将被授予管理员权限。您可以在侧边栏用户下拉菜单中查看管理员设置。如果您认为这是一个错误,请查看 Docker 日志或在 GitHub 上创建一个 issue。", + "external": "第三方", + "loginWithExternal": "通过第三方账号登录", + "loginWithExternalDesc": "使用你配置的第三方身份提供商登录", + "externalNotSupportedInElectron": "Electron 应用暂不支持第三方认证。请使用网页版进行 OIDC 登录。", + "resetPasswordButton": "重置密码", + "sendResetCode": "发送重置码", + "resetCodeDesc": "输入你的用户名以接收密码重置码。该码将记录在 docker 容器日志中。", + "resetCode": "重置码", + "verifyCodeButton": "验证码", + "enterResetCode": "请输入 Docker 容器日志中该用户的 6 位代码:", + "goToLogin": "前往登录", + "newPassword": "新密码", + "confirmNewPassword": "确认密码", + "enterNewPassword": "请输入用户的新密码:", + "signUp": "注册", + "mobileApp": "移动应用", + "loggingInToMobileApp": "正在登录移动应用", + "desktopApp": "桌面应用程序", + "loggingInToDesktopApp": "正在登录桌面应用", + "loggingInToDesktopAppViaWeb": "通过网页界面登录桌面应用程序", + "loadingServer": "正在加载服务器...", + "authenticating": "正在验证身份...", + "dataLossWarning": "使用此方法重置密码将删除您保存的所有 SSH 主机、凭证和其他加密数据。此操作无法撤销。仅当您忘记密码且未登录时才使用此方法。", + "authenticationDisabled": "身份验证已禁用", + "authenticationDisabledDesc": "所有身份验证方式目前均已禁用。请联系您的管理员。", + "passwordResetSuccess": "密码重置成功", + "passwordResetSuccessDesc": "您的密码已成功重置。您现在可以使用新密码登录。" + }, + "errors": { + "notFound": "页面未找到", + "unauthorized": "未经授权的访问", + "forbidden": "禁止访问", + "serverError": "服务器错误", + "networkError": "网络错误", + "databaseConnection": "无法连接到数据库", + "unknownError": "未知错误", + "loginFailed": "登录失败", + "failedPasswordReset": "密码重置失败", + "failedVerifyCode": "重置代码验证失败", + "failedCompleteReset": "密码重置失败", + "invalidTotpCode": "无效的 TOTP 代码", + "failedOidcLogin": "OIDC 登录启动失败", + "failedUserInfo": "OIDC登录后获取用户信息失败", + "oidcAuthFailed": "OIDC 身份验证失败", + "noTokenReceived": "登录未收到令牌", + "invalidAuthUrl": "从后端收到的授权 URL 无效", + "invalidInput": "输入无效", + "requiredField": "此字段是必需的", + "minLength": "最短长度为 {{min}}", + "maxLength": "最大长度为 {{max}}", + "invalidEmail": "无效的电子邮件地址", + "passwordMismatch": "密码不匹配", + "passwordLoginDisabled": "目前已禁用用户名/密码登录。", + "weakPassword": "密码强度不足", + "usernameExists": "用户名已存在", + "emailExists": "邮箱地址已存在", + "loadFailed": "数据加载失败", + "saveError": "保存失败", + "sessionExpired": "会话已过期 - 请重新登录", + "totpRateLimited": "频率限制:TOTP验证尝试次数过多。请稍后再试。", + "totpRateLimitedWithTime": "速率限制:TOTP 验证尝试次数过多。请稍等 {{time}} 秒后再试。", + "resetCodeRateLimited": "验证次数过多,请稍后再试。", + "resetCodeRateLimitedWithTime": "验证次数过多,请稍等 {{time}} 秒后再试。" + }, + "messages": { + "saveSuccess": "保存成功", + "saveError": "保存失败", + "deleteSuccess": "已成功删除", + "deleteError": "删除失败", + "updateSuccess": "更新成功", + "updateError": "更新失败", + "copySuccess": "已复制到剪贴板", + "copyError": "复制失败", + "copiedToClipboard": "{{item}} 已复制到剪贴板", + "connectionEstablished": "连接已建立", + "connectionClosed": "连接已关闭", + "reconnecting": "正在重新连接……", + "processing": "处理中...", + "pleaseWait": "请稍等...", + "registrationDisabled": "管理员已禁用新账号注册功能。请登录或联系管理员。", + "databaseConnected": "数据库连接成功", + "databaseConnectionFailed": "连接数据库服务器失败", + "checkServerConnection": "请检查您的服务器连接并重试。", + "resetCodeSent": "重置码已发送到 Docker 日志", + "codeVerified": "验证码验证成功", + "passwordResetSuccess": "密码重置成功", + "loginSuccess": "登录成功", + "registrationSuccess": "注册成功" + }, + "profile": { + "title": "用户资料", + "description": "管理您的帐户设置和安全", + "security": "安全设置", + "changePassword": "修改密码", + "twoFactorAuth": "双因素身份验证", + "accountInfo": "账号信息", + "role": "角色", + "admin": "管理员", + "user": "普通用户", + "authMethod": "认证方式", + "local": "本地", + "external": "第三方 (OIDC)", + "externalAndLocal": "双重认证", + "selectPreferredLanguage": "请选择您偏好的界面语言。", + "fileColorCoding": "文件颜色编码", + "fileColorCodingDesc": "按类型对文件进行颜色编码:文件夹(红色)、文件(蓝色)、符号链接(绿色)", + "commandAutocomplete": "命令自动补全", + "commandAutocompleteDesc": "启用 Tab 键根据命令历史为终端命令提供自动补全建议", + "defaultSnippetFoldersCollapsed": "默认折叠代码片段文件夹", + "defaultSnippetFoldersCollapsedDesc": "启用后,打开代码片段标签页时所有代码片段文件夹将默认折叠。", + "terminalSyntaxHighlighting": "终端语法高亮", + "showHostTags": "显示主机标签", + "showHostTagsDesc": "在侧边栏的每个主机下方显示标签。禁用则隐藏所有标签。", + "account": "账号", + "appearance": "外观", + "languageLocalization": "语言和本地化", + "fileManagerSettings": "文件管理器", + "terminalSettings": "终端", + "hostSidebarSettings": "主机和侧边栏", + "snippetsSettings": "代码片段", + "updateSettings": "更新", + "disableUpdateCheck": "禁用更新检查", + "disableUpdateCheckDesc": "停止在启动和控制面板中检查新版本。减少网络请求。", + "currentPassword": "当前密码", + "passwordChangedSuccess": "密码已成功更改!请重新登录。", + "failedToChangePassword": "密码更改失败。请检查您当前的密码并重试。", + "theme": "主题", + "themeLight": "浅色", + "themeDark": "深色", + "themeSystem": "跟随系统", + "appearanceDesc": "选择应用的配色主题", + "terminalSyntaxHighlightingDesc": "自动高亮终端输出中的命令、路径、IP 地址和日志级别", + "enableCommandPaletteShortcut": "启用命令面板快捷键", + "enableCommandPaletteShortcutDesc": "双击左Shift键打开命令面板,以便快速访问主机。" + }, + "user": { + "failedToLoadVersionInfo": "加载版本信息失败" + }, + "placeholders": { + "enterCode": "000000", + "ipAddress": "127.0.0.1", + "port": "22", + "maxRetries": "3", + "retryInterval": "10", + "language": "语言", + "username": "用户名", + "hostname": "主机名称", + "folder": "文件夹", + "password": "密码", + "keyPassword": "密钥密码", + "sudoPassword": "sudo 密码(可选)", + "notes": "添加关于此主机的备注信息...", + "expirationDate": "选择过期日期", + "pastePrivateKey": "把你的私钥粘贴到这里……", + "pastePublicKey": "把你的公钥粘贴到这里……", + "credentialName": "我的 SSH 服务器", + "description": "SSH凭证描述", + "searchCredentials": "按名称、用户名或标签搜索凭证……", + "sshConfig": "端点 SSH 配置", + "homePath": "/home", + "clientId": "您的客户端 ID", + "clientSecret": "你的客户密钥", + "authUrl": "https://your-provider.com/application/o/authorize/", + "redirectUrl": "https://your-provider.com/application/o/termix/", + "tokenUrl": "https://your-provider.com/application/o/token/", + "userIdField": "子项", + "usernameField": "名称", + "scopes": "openid 电子邮件配置文件", + "userinfoUrl": "https://your-provider.com/application/o/userinfo/", + "enterUsername": "输入要设为管理员的用户名", + "searchHosts": "按名称、用户名、IP 地址、文件夹、标签等搜索主机……", + "enterPassword": "请输入您的密码", + "totpCode": "6位数的TOTP代码", + "searchHostsAny": "搜索主机(尝试:tag:prod,user:root,ip:192.168)...", + "confirmPassword": "请输入密码以确认", + "typeHere": "在此输入", + "fileName": "请输入文件名(例如,example.txt)", + "folderName": "输入文件夹名称", + "fullPath": "请输入项目的完整路径", + "currentPath": "输入项目的当前路径", + "newName": "输入新名称", + "socks5Host": "127.0.0.1", + "socks5Username": "代理用户名", + "socks5Password": "代理密码", + "socks5PresetName": "例如,工作 VPN 链", + "socks5PresetDescription": "例如,用于访问工作服务器的代理链", + "moshCommand": "mosh 用户@服务器", + "defaultPort": "22", + "defaultEndpointPort": "224", + "defaultMaxRetries": "3", + "defaultRetryInterval": "10" + }, + "leftSidebar": { + "failedToLoadHosts": "主机加载失败", + "noFolder": "无文件夹", + "passwordRequired": "需要密码", + "failedToDeleteAccount": "删除帐户失败", + "failedToMakeUserAdmin": "创建用户管理员失败", + "userIsNowAdmin": "用户 {{username}} 已设为管理员", + "removeAdminConfirm": "确定要移除 {{username}} 的管理员权限吗?", + "deleteUserConfirm": "确定要删除用户 {{username}} 吗?此操作不可撤销。", + "deleteAccount": "删除账号", + "closeDeleteAccount": "关闭删除账号窗口", + "deleteAccountWarning": "此操作不可撤销,将永久删除你的账号及所有关联数据。", + "deleteAccountWarningDetails": "删除您的帐户将清除您的所有数据,包括 SSH 主机、配置和设置。此操作不可逆。", + "deleteAccountWarningShort": "此操作不可逆,将永久删除您的帐户。", + "cannotDeleteAccount": "无法删除帐户", + "lastAdminWarning": "您是最后一位管理员用户。您不能删除您的帐户,因为这会导致系统失去所有管理员。请先将其他用户设为管理员,或联系系统支持。", + "confirmPassword": "确认密码", + "deleting": "正在删除……", + "cancel": "取消" + }, + "interface": { + "sidebar": "侧边栏", + "toggleSidebar": "切换侧边栏", + "close": "关闭", + "online": "在线", + "offline": "离线", + "maintenance": "维护中", + "degraded": "降级状态", + "noTunnelConnections": "未配置隧道连接", + "discord": "Discord", + "connectToSshForOperations": "连接到 SSH 以使用文件操作", + "uploadFile": "上传文件", + "newFile": "新建文件", + "newFolder": "新建文件夹", + "rename": "重命名", + "deleteItem": "删除项目", + "createNewFile": "创建新文件", + "createNewFolder": "创建新文件夹", + "renameItem": "重命名项目", + "clickToSelectFile": "点击选择文件", + "noSshHosts": "暂无 SSH 主机", + "sshHosts": "SSH主机", + "importSshHosts": "从 JSON 导入 SSH 主机", + "clientId": "客户端 ID", + "clientSecret": "客户端密钥", + "error": "错误", + "warning": "警告", + "deleteAccount": "删除账号", + "closeDeleteAccount": "关闭删除账号窗口", + "cannotDeleteAccount": "无法删除账号", + "confirmPassword": "确认密码", + "deleting": "正在删除……", + "externalAuth": "第三方认证(OIDC)", + "configureExternalProvider": "为以下服务配置第三方身份提供商", + "waitingForRetry": "等待重试", + "retryingConnection": "正在重试连接", + "resetSplitSizes": "重置分栏尺寸", + "sshManagerAlreadyOpen": "SSH 管理器已打开", + "disabledDuringSplitScreen": "分屏模式下禁用", + "unknown": "未知", + "connected": "已连接", + "disconnected": "断开连接", + "maxRetriesExhausted": "最大重试次数已用尽", + "endpointHostNotFound": "未找到目标主机", + "administrator": "管理员", + "user": "普通用户", + "external": "第三方", + "local": "本地", + "saving": "保存中...", + "saveConfiguration": "保存配置", + "loading": "加载中...", + "refresh": "刷新", + "adding": "添加中……", + "makeAdmin": "设为管理员", + "verifying": "验证中...", + "verifyAndEnable": "验证并启用", + "secretKey": "密钥", + "totpQrCode": "TOTP二维码", + "passwordRequired": "使用密码验证时,需要输入密码。", + "sshKeyRequired": "使用密钥认证时需要 SSH 私钥。", + "keyTypeRequired": "使用密钥认证时,需要指定密钥类型。", + "validSshConfigRequired": "必须从列表中选择有效的 SSH 配置。", + "updateHost": "更新主机", + "addHost": "添加主机", + "editHost": "编辑主机", + "pinConnection": "固定连接", + "authentication": "身份验证", + "password": "密码", + "key": "密钥", + "sshPrivateKey": "SSH 私钥", + "keyPassword": "密钥密码", + "keyType": "密钥类型", + "enableTerminal": "启用终端", + "enableTunnel": "启用隧道", + "enableFileManager": "启用文件管理器", + "defaultPath": "默认路径", + "tunnelConnections": "隧道连接", + "maxRetries": "最大重试次数", + "upload": "上传", + "updateKey": "更新密钥", + "productionFolder": "生产环境", + "databaseServer": "数据库服务器", + "developmentServer": "开发服务器", + "developmentFolder": "开发环境", + "webServerProduction": "Web 服务器 - 生产环境", + "unknownError": "未知错误", + "failedToInitiatePasswordReset": "初始化密码重置失败", + "failedToVerifyResetCode": "验证重置码失败", + "failedToCompletePasswordReset": "完成密码重置失败", + "invalidTotpCode": "无效的 TOTP 代码", + "failedToStartOidcLogin": "OIDC 登录启动失败", + "failedToGetUserInfoAfterOidc": "OIDC登录后获取用户信息失败", + "loginWithExternalProvider": "通过第三方账号登录", + "loginWithExternal": "通过第三方账号登录", + "sendResetCode": "发送重置代码", + "verifyCode": "验证验证码", + "resetPassword": "重置密码", + "login": "登录", + "signUp": "注册", + "failedToUpdateOidcConfig": "更新 OIDC 配置失败", + "failedToMakeUserAdmin": "设置用户为管理员失败", + "failedToStartTotpSetup": "TOTP 设置启动失败", + "invalidVerificationCode": "验证码无效", + "failedToDisableTotp": "禁用 TOTP 失败", + "failedToGenerateBackupCodes": "生成备用码失败" + }, + "mobile": { + "selectHostToStart": "选择主机以启动终端会话", + "limitedSupportMessage": "网站移动端支持仍在开发中。请使用移动应用以获得更佳体验。", + "mobileAppInProgress": "移动应用正在开发中", + "mobileAppInProgressDesc": "我们正在开发一款专门的移动应用程序,以便在移动设备上提供更好的体验。", + "viewMobileAppDocs": "安装移动应用", + "mobileAppDocumentation": "移动应用文档" + }, + "dashboard": { + "title": "仪表盘", + "loading": "正在加载仪表盘...", + "github": "GitHub", + "support": "支持", + "discord": "Discord", + "donate": "捐赠", + "serverOverview": "服务器概览", + "version": "版本", + "upToDate": "最新", + "updateAvailable": "更新可用", + "uptime": "运行时长", + "database": "数据库", + "healthy": "健康", + "error": "错误", + "totalServers": "服务器总数", + "totalTunnels": "隧道总数", + "totalCredentials": "凭证总数", + "recentActivity": "最近活动", + "reset": "重置", + "loadingRecentActivity": "加载最近活动...", + "noRecentActivity": "暂无最近活动", + "quickActions": "快捷操作", + "addHost": "添加主机", + "addCredential": "添加凭证", + "adminSettings": "管理员设置", + "userProfile": "用户资料", + "serverStats": "服务器统计信息", + "loadingServerStats": "加载服务器统计信息...", + "noServerData": "暂无服务器数据", + "cpu": "CPU", + "ram": "内存", + "notAvailable": "未获取", + "customizeLayout": "自定义仪表盘", + "dashboardSettings": "仪表盘设置", + "enableDisableCards": "启用/禁用卡片", + "resetLayout": "重置为默认值", + "serverOverviewCard": "服务器概览", + "recentActivityCard": "近期活动", + "networkGraphCard": "网络图", + "networkGraph": "网络图", + "quickActionsCard": "快速操作", + "serverStatsCard": "服务器统计信息" + }, + "networkGraph": { + "title": "网络图", + "addHost": "添加主机", + "addGroup": "添加组", + "addLink": "添加链接", + "deleteSelected": "删除所选内容", + "zoomIn": "放大", + "zoomOut": "缩小", + "resetView": "重置视图", + "exportJson": "导出 JSON", + "importJson": "导入 JSON", + "selectHost": "选择主机", + "chooseHost": "选择主机...", + "parentGroup": "上级分组", + "noGroup": "没有群组", + "groupName": "群组名称", + "color": "颜色", + "source": "来源", + "target": "Target", + "moveToGroup": "移动到组", + "selectGroup": "选择组...", + "addConnection": "添加连接", + "hostDetails": "主机详细信息", + "connectToHost": "连接到主机", + "openTerminal": "打开终端", + "openFileManager": "打开文件管理器", + "openTunnel": "打开Tunnels", + "openDocker": "打开停靠栏", + "openServerStats": "打开服务器详细信息", + "moveToGroupAction": "移动到组", + "removeFromGroup": "从群组中删除", + "addHostHere": "在此处添加主机", + "editGroup": "编辑分组", + "delete": "删除", + "cancel": "取消", + "add": "添加", + "create": "创建", + "update": "更新", + "move": "移动", + "connect": "连接", + "close": "关闭", + "createGroup": "创建分组", + "noAvailableHosts": "没有可用主机", + "selectSourcePlaceholder": "选择源...", + "selectTargetPlaceholder": "Select Target...", + "invalidFile": "无效的文件", + "hostAlreadyExists": "主机已经在地形中", + "sourceTargetMustDiffer": "来源和目标必须是不同的", + "connectionExists": "连接已存在", + "unknown": "未知的", + "name": "名称", + "ip": "IP", + "status": "状态", + "id": "ID", + "failedToLoadData": "加载网络地形数据失败", + "failedToAddNode": "添加节点失败", + "sourceDifferentFromTarget": "来源和目标必须是不同的", + "exportJSON": "导出 JSON", + "importJSON": "导入 JSON", + "searchHost": "搜索主机.....", + "noHostFound": "未找到主机", + "searchGroup": "搜索群组...", + "noGroupFound": "未找到群组", + "searchNode": "搜索节点...", + "noNodeFound": "未找到节点", + "terminal": "终端", + "fileManager": "文件管理器", + "tunnel": "隧道", + "docker": "Docker", + "serverStats": "服务器统计信息" + }, + "rbac": { + "shareHost": "共享主机", + "shareHostTitle": "共享主机访问权限", + "shareHostDescription": "授予对此主机的临时或永久访问权限", + "targetUser": "目标用户", + "selectUser": "选择要共享的用户", + "duration": "有效期", + "durationHours": "有效期(小时)", + "neverExpires": "永不过期", + "permissionLevel": "权限级别", + "permissionLevels": { + "readonly": "只读", + "readonlyDesc": "仅可查看,无法输入命令", + "restricted": "受限模式", + "restrictedDesc": "屏蔽危险命令(passwd、rm -rf 等)", + "monitored": "监控模式", + "monitoredDesc": "记录所有命令但不屏蔽(推荐)", + "full": "完全访问", + "fullDesc": "无任何限制(不推荐)" + }, + "blockedCommands": "屏蔽的命令", + "blockedCommandsPlaceholder": "输入要屏蔽的命令,例如:passwd、rm、dd", + "maxSessionDuration": "最大会话时长(分钟)", + "createTempUser": "创建临时用户", + "createTempUserDesc": "在服务器上创建受限用户而非共享你的凭证。需要 sudo 权限。这是最安全的选项。", + "expiresAt": "过期时间", + "expiresIn": "{{hours}} 小时后过期", + "expired": "已过期", + "grantedBy": "授权人", + "accessLevel": "访问级别", + "lastAccessed": "最后访问时间", + "accessCount": "访问次数", + "revokeAccess": "撤销访问权限", + "confirmRevokeAccess": "确定要撤销 {{username}} 的访问权限吗?", + "hostSharedSuccessfully": "已成功与 {{username}} 共享主机", + "hostAccessUpdated": "主机访问权限已更新", + "failedToShareHost": "共享主机失败", + "accessRevokedSuccessfully": "访问权限已成功撤销", + "failedToRevokeAccess": "撤销访问权限失败", + "shared": "共享", + "sharedHosts": "共享主机", + "sharedWithMe": "共享给我的主机", + "noSharedHosts": "暂无共享给您的主机", + "owner": "所有者", + "viewAccessList": "查看访问列表", + "accessList": "访问列表", + "noAccessGranted": "此主机未被授予访问权限", + "noAccessGrantedMessage": "暂无用户被授予此主机的访问权限", + "manageAccessFor": "管理访问权限", + "totalAccessRecords": "{{count}} 条访问记录", + "neverAccessed": "从未访问", + "timesAccessed": "访问 {{count}} 次", + "daysRemaining": "剩余 {{days}} 天", + "hoursRemaining": "剩余 {{hours}} 小时", + "failedToFetchAccessList": "获取访问列表失败", + "currentAccess": "当前访问权限", + "securityWarning": "安全警告", + "securityWarningMessage": "共享凭证将赋予用户对服务器的完全访问权限,可以执行任何操作,包括更改密码和删除文件。请仅与受信任的用户共享凭证。", + "tempUserRecommended": "为了提高安全性,我们建议启用“创建临时用户”功能。", + "roleManagement": "角色管理", + "manageRoles": "管理角色", + "manageRolesFor": "管理 {{username}} 的角色", + "assignRole": "分配角色", + "removeRole": "移除角色", + "userRoles": "用户角色", + "permissions": "权限", + "systemRole": "系统角色", + "customRole": "自定义角色", + "roleAssignedSuccessfully": "已成功为 {{username}} 分配角色", + "failedToAssignRole": "角色分配失败", + "roleRemovedSuccessfully": "已成功从 {{username}} 移除角色", + "failedToRemoveRole": "移除角色失败", + "cannotRemoveSystemRole": "无法移除系统角色", + "cannotShareWithSelf": "无法将主机共享给自己", + "noCustomRolesToAssign": "暂无可用的自定义角色。系统角色为自动分配。", + "credentialSharingWarning": "共享功能不支持凭证认证", + "credentialRequired": "共享主机时需要凭证", + "credentialRequiredDescription": "此主机未使用基于凭证的身份验证。由于需要对每个用户进行加密,因此要共享主机,主机必须使用基于凭证的身份验证。", + "auditLogs": "审计日志", + "viewAuditLogs": "查看审计日志", + "action": "操作", + "resourceType": "资源类型", + "resourceName": "资源名称", + "timestamp": "时间戳", + "ipAddress": "IP地址", + "userAgent": "用户代理", + "success": "成功", + "failed": "失败", + "details": "详情", + "noAuditLogs": "暂无审计日志", + "sessionRecordings": "会话记录", + "viewRecording": "查看记录", + "downloadRecording": "下载记录", + "dangerousCommand": "检测到危险命令", + "commandBlocked": "命令已屏蔽", + "terminateSession": "终止会话", + "sessionTerminated": "会话已被主机所有者终止", + "sharedAccessExpired": "你对此主机的共享访问权限已过期", + "sharedAccessExpiresIn": "共享访问权限将在 {{hours}} 小时后过期", + "roles": { + "label": "角色", + "admin": "管理员", + "user": "普通用户" + }, + "createRole": "创建角色", + "editRole": "编辑角色", + "roleName": "角色名称", + "displayName": "显示名称", + "description": "描述", + "assignRoles": "分配角色", + "userRoleAssignment": "用户角色分配", + "selectUserPlaceholder": "选择用户", + "searchUsers": "搜索用户......", + "noUserFound": "未找到用户", + "currentRoles": "当前角色", + "noRolesAssigned": "未分配任何角色", + "assignNewRole": "分配新角色", + "selectRolePlaceholder": "选择角色", + "searchRoles": "搜索角色......", + "noRoleFound": "未找到角色", + "assign": "分配", + "roleCreatedSuccessfully": "角色创建成功", + "roleUpdatedSuccessfully": "角色更新成功", + "roleDeletedSuccessfully": "角色删除成功", + "failedToLoadRoles": "加载角色失败", + "failedToSaveRole": "保存角色失败", + "failedToDeleteRole": "删除角色失败", + "roleDisplayNameRequired": "角色显示名称为必填项", + "roleNameRequired": "角色名称为必填项", + "roleNameHint": "请仅使用小写字母、数字、下划线和连字符", + "displayNamePlaceholder": "开发人员", + "descriptionPlaceholder": "软件开发人员和工程师", + "confirmDeleteRole": "删除角色", + "confirmDeleteRoleDescription": "您确定要删除角色“{{name}}”吗?此操作无法撤销。", + "confirmRemoveRole": "移除角色", + "confirmRemoveRoleDescription": "您确定要从该用户中移除此角色吗?", + "editRoleDescription": "更新角色信息", + "createRoleDescription": "创建一个新的自定义角色,用于对用户进行分组", + "assignRolesDescription": "管理用户角色分配", + "noRoles": "未找到角色", + "selectRole": "共享给角色", + "type": "类型", + "user": "用户", + "role": "角色", + "saveHostFirst": "请先保存主机", + "saveHostFirstDescription": "请先保存主机,然后再配置共享设置。", + "shareWithUser": "共享给用户", + "shareWithRole": "分享角色", + "share": "共享", + "target": "目标", + "expires": "过期时间", + "never": "永不", + "noAccessRecords": "未找到访问记录", + "sharedSuccessfully": "共享成功", + "failedToShare": "共享失败", + "confirmRevokeAccessDescription": "您确定要撤销此访问权限吗?", + "hours": "小时", + "sharing": "共享", + "selectUserAndRole": "请同时选择用户和角色", + "view": "仅查看", + "viewDesc": "由于 Termix 加密系统的原因,其他权限级别将在稍后推出" + }, + "commandPalette": { + "searchPlaceholder": "搜索主机或快速操作......", + "recentActivity": "近期活动", + "navigation": "导航", + "addHost": "添加主机", + "addCredential": "添加凭证", + "adminSettings": "管理员设置", + "userProfile": "用户资料", + "updateLog": "更新日志", + "hosts": "主机", + "openServerDetails": "打开服务器详情", + "openFileManager": "打开文件管理器", + "edit": "编辑", + "links": "相关链接", + "github": "GitHub", + "support": "支持", + "discord": "Discord", + "donate": "捐赠", + "press": "按下", + "toToggle": "切换", + "close": "关闭", + "hostManager": "主机管理器", + "pressToToggle": "双击左 Shift 键打开命令面板" + }, + "docker": { + "notEnabled": "此主机未启用 Docker", + "validating": "正在验证 Docker......", + "connectingToHost": "正在连接主机......", + "connecting": "正在连接......", + "error": "错误", + "errorCode": "错误代码:{{code}}", + "version": "Docker {{version}}", + "connectionFailed": "连接 Docker 失败", + "totpVerificationFailed": "TOTP验证失败,请重试。", + "warpgateVerificationFailed": "Warpgate身份验证失败,请重试。", + "verificationCodePrompt": "请输入验证码", + "loadingContainers": "加载容器列表......", + "containerStarted": "容器 {{name}} 已启动", + "failedToStartContainer": "启动容器 {{name}} 失败", + "containerStopped": "容器 {{name}} 已停止", + "failedToStopContainer": "停止容器 {{name}} 失败", + "containerRestarted": "容器 {{name}} 已重启", + "failedToRestartContainer": "重启容器 {{name}} 失败", + "containerPaused": "容器 {{name}} 已暂停", + "containerUnpaused": "容器 {{name}} 已取消暂停", + "failedToTogglePauseContainer": "切换容器 {{name}} 暂停状态失败", + "containerRemoved": "容器 {{name}} 已移除", + "failedToRemoveContainer": "移除容器 {{name}} 失败", + "image": "镜像", + "idLabel": "ID", + "ports": "端口", + "noPorts": "无端口", + "created": "创建时间", + "start": "启动", + "stop": "停止", + "pause": "暂停", + "unpause": "取消暂停", + "restart": "重启", + "remove": "移除", + "removeContainer": "移除容器", + "confirmRemoveContainer": "您确定要移除容器“{{name}}”吗?此操作无法撤销。", + "runningContainerWarning": "警告:此容器当前正在运行,请先停止后再移除。", + "removing": "移除中……", + "noContainersFound": "未找到容器", + "noContainersFoundHint": "此主机上没有可用的 Docker 容器", + "searchPlaceholder": "搜索容器……", + "filterByStatusPlaceholder": "按状态筛选", + "allContainersCount": "全部 ({{count}})", + "statusCount": "{{status}} ({{count}})", + "noContainersMatchFilters": "没有容器符合您的筛选条件", + "noContainersMatchFiltersHint": "尝试调整搜索关键词或筛选条件", + "containerMustBeRunningToViewStats": "容器必须处于运行状态才能查看统计信息", + "failedToFetchStats": "获取容器统计信息失败", + "containerNotRunning": "容器未运行", + "startContainerToViewStats": "启动容器以查看统计信息", + "loadingStats": "正在加载统计信息……", + "errorLoadingStats": "加载统计信息时出错", + "noStatsAvailable": "暂无统计数据", + "cpuUsage": "CPU 使用率", + "current": "当前值", + "memoryUsage": "内存使用率", + "usedLimit": "已用/限制", + "percentage": "百分比", + "networkIo": "网络 I/O", + "input": "流入", + "output": "流出", + "blockIo": "块 I/O", + "read": "读取", + "write": "写入", + "pids": "PIDs", + "containerInformation": "容器信息", + "name": "名称", + "id": "ID", + "state": "状态", + "disconnectedFromContainer": "与容器断开连接", + "containerMustBeRunning": "容器必须正在运行才能访问控制台", + "authenticationRequired": "需要身份验证", + "connectedTo": "已连接到 {{containerName}}", + "disconnected": "已断开", + "consoleError": "控制台错误", + "errorMessage": "错误:{{message}}", + "failedToConnect": "连接容器失败", + "console": "控制台", + "selectShell": "选择 Shell", + "bash": "Bash", + "sh": "sh", + "ash": "ash", + "connect": "连接", + "disconnect": "断开连接", + "notConnected": "未连接", + "clickToConnect": "单击“连接”以启动 shell 会话", + "connectingTo": "正在连接到 {{containerName}}......", + "containerNotFound": "未找到容器", + "backToList": "返回列表", + "logs": "日志", + "stats": "统计信息", + "consoleTab": "控制台", + "startContainerToAccess": "启动容器以访问控制台", + "log": { + "connecting": "正在连接到 Docker 主机......", + "authStarted": "正在解析认证凭据...", + "totpRequired": "需要TOTP 验证", + "warpgateRequired": "需要 Warpgate 身份验证", + "sessionCreated": "已建立 Docker SSH 会话", + "sessionReady": "已准备好进行 Docker 操作", + "error": "Docker 连接失败: {{error}}" + } + }, + "stats": { + "log": { + "connecting": "开始收集指标数据……", + "existingSession": "使用现有指标会话", + "totpVerifying": "需要进行 TOTP 验证", + "sessionEstablished": "指标会议已建立", + "collecting": "收集服务器指标", + "heartbeat": "保持联系", + "stopped": "指标收集已停止", + "error": "指标收集失败: {{error}}" + } + }, + "sftp": { + "log": { + "connecting": "正在连接SFTP服务器......", + "jumpHost": "通过跳转主机连接 {{host}}......", + "connected": "已建立 SFTP 连接", + "authFailed": "身份验证失败", + "error": "SFTP 操作失败: {{error}}" + } + }, + "theme": { + "switchToLight": "切换为浅色主题", + "switchToDark": "切换为深色主题" + } +} diff --git a/src/locales/translated/zh.json b/src/locales/translated/zh_TW.json similarity index 89% rename from src/locales/translated/zh.json rename to src/locales/translated/zh_TW.json index 99e82cb5..a526b766 100644 --- a/src/locales/translated/zh.json +++ b/src/locales/translated/zh_TW.json @@ -3,7 +3,7 @@ "credentialsViewer": "憑證檢視器", "manageYourSSHCredentials": "安全地管理您的 SSH 憑證", "addCredential": "新增憑證", - "createCredential": "創建憑證", + "createCredential": "建立憑證", "editCredential": "編輯憑證", "viewCredential": "查看憑證", "duplicateCredential": "重複憑證", @@ -29,16 +29,16 @@ "credentialDeletedSuccessfully": "憑證已成功刪除", "failedToDeleteCredential": "刪除憑證失敗", "confirmDeleteCredential": "您確定要刪除憑證「{{name}}」嗎?", - "credentialCreatedSuccessfully": "憑證創建成功", + "credentialCreatedSuccessfully": "憑證建立成功", "credentialUpdatedSuccessfully": "憑證已成功更新", "failedToSaveCredential": "儲存憑證失敗", "failedToFetchCredentialDetails": "取得憑證詳細資訊失敗", "failedToFetchHostsUsing": "使用此憑證取得主機失敗", - "loadingCredentials": "正在加載憑證...", + "loadingCredentials": "正在載入憑證……", "retry": "重試", "noCredentials": "沒有憑證", "noCredentialsMessage": "您尚未新增任何憑證。點選「新增憑證」開始操作。", - "sshCredentials": "SSH憑證", + "sshCredentials": "SSH 憑證", "credentialsCount": "{{count}} 憑證", "refresh": "重新整理", "passwordRequired": "需要密碼", @@ -52,14 +52,14 @@ "tags": "標籤", "addTagsSpaceToAdd": "新增標籤(按空白鍵新增)", "password": "密碼", - "key": "鑰匙", + "key": "金鑰", "sshPrivateKey": "SSH 私鑰", "upload": "上傳", "updateKey": "更新金鑰", "keyPassword": "密鑰密碼", - "keyType": "關鍵類型", - "keyTypeRSA": "南非", - "keyTypeECDSA": "東非與中非經濟共同體", + "keyType": "金鑰類型", + "keyTypeRSA": "RSA", + "keyTypeECDSA": "ECDSA", "keyTypeEd25519": "Ed25519", "basicInfo": "基本訊息", "authentication": "驗證", @@ -70,7 +70,7 @@ "authenticationMethodDescription": "選擇您希望如何使用 SSH 伺服器進行身份驗證", "organizationDescription": "使用資料夾和標籤整理您的憑證", "enterCredentialName": "請輸入憑證名稱", - "enterCredentialDescription": "輸入描述(可選)", + "enterCredentialDescription": "輸入描述(選擇性)", "enterUsername": "請輸入使用者名稱", "nameIsRequired": "憑證名稱為必填項", "usernameIsRequired": "使用者名稱是必填項", @@ -79,15 +79,15 @@ "sshKeyAuthDescription": "使用 SSH 金鑰驗證", "passwordIsRequired": "需要密碼", "sshKeyIsRequired": "需要 SSH 金鑰", - "sshKeyType": "SSH金鑰類型", + "sshKeyType": "SSH 金鑰類型", "privateKey": "私鑰", "enterPassword": "輸入密碼", "enterPrivateKey": "輸入私鑰", "keyPassphrase": "密鑰口令", - "enterKeyPassphrase": "輸入密鑰密碼(可選)", + "enterKeyPassphrase": "輸入密鑰密碼(選擇性)", "keyPassphraseOptional": "可選:如果您的密鑰沒有密碼,請留空。", "leaveEmptyToKeepCurrent": "留空以保留當前值", - "uploadKeyFile": "上傳密鑰文件", + "uploadKeyFile": "上傳密鑰檔案", "generateKeyPairButton": "產生密鑰對", "generateKeyPair": "產生密鑰對", "generateKeyPairDescription": "產生新的 SSH 金鑰對。如果您想使用密碼保護金鑰,請先在下方的「金鑰密碼」欄位中輸入密碼。", @@ -100,9 +100,9 @@ "chooseHostToDeploy": "選擇要部署到的主機…", "deploying": "正在部署…", "name": "姓名", - "noHostsAvailable": "暫無可用房源", + "noHostsAvailable": "暫無可用主機", "noHostsMatchSearch": "沒有符合您搜尋條件的主機", - "sshKeyGenerationNotImplemented": "SSH金鑰產生功能即將推出", + "sshKeyGenerationNotImplemented": "SSH 金鑰產生功能即將推出", "connectionTestingNotImplemented": "連線測試功能即將推出", "testConnection": "測試連接", "selectOrCreateFolder": "選擇或建立資料夾", @@ -127,12 +127,12 @@ "timesUsed": "使用次數", "lastUsed": "上次使用", "connectedHosts": "已連接主機", - "created": "創建", + "created": "已建立", "lastModified": "最後修改日期", "usageStatistics": "使用情況統計", "copiedToClipboard": "{{field}} 已複製到剪貼簿", "failedToCopy": "複製到剪貼簿失敗", - "sshKey": "SSH金鑰", + "sshKey": "SSH 金鑰", "createCredentialDescription": "建立新的 SSH 憑證以實現安全訪問", "editCredentialDescription": "更新憑證資訊", "listView": "清單", @@ -149,15 +149,15 @@ "publicKeyNote": "公鑰是可選的,但建議用於金鑰驗證。", "publicKeyUploaded": "公鑰已上傳", "uploadPublicKey": "上傳公鑰", - "uploadPrivateKeyFile": "上傳私鑰文件", - "uploadPublicKeyFile": "上傳公鑰文件", + "uploadPrivateKeyFile": "上傳私鑰檔案", + "uploadPublicKeyFile": "上傳公鑰檔案", "privateKeyRequiredForGeneration": "產生公鑰需要私鑰。", "failedToGeneratePublicKey": "產生公鑰失敗", "generatePublicKey": "從私鑰生成", "publicKeyGeneratedSuccessfully": "公鑰產生成功", "detectedKeyType": "偵測到密鑰類型", "detectingKeyType": "正在檢測…", - "optional": "選修的", + "optional": "選擇性", "generateKeyPairNew": "產生新的密鑰對", "generateEd25519": "生成 Ed25519", "generateECDSA": "生成 ECDSA", @@ -165,7 +165,7 @@ "keyTypeEcdsaP256": "ECDSA P-256 (SSH)", "keyTypeEcdsaP384": "ECDSA P-384 (SSH)", "keyTypeEcdsaP521": "ECDSA P-521 (SSH)", - "keyTypeDsa": "DSA(SSH)", + "keyTypeDsa": "DSA (SSH)", "keyTypeRsaSha256": "RSA-SHA2-256", "keyTypeRsaSha512": "RSA-SHA2-512", "keyPairGeneratedSuccessfully": "{{keyType}} 金鑰對已成功生成", @@ -190,18 +190,18 @@ "title": "快速連接", "description": "無需儲存主機配置,即可直接連接到終端機或檔案管理器工作階段。", "ipAddress": "IP位址或主機名", - "port": "港口", + "port": "連接埠", "username": "使用者名稱", "password": "密碼", "key": "SSH 私鑰", - "keyPassword": "密鑰密碼(可選)", - "keyType": "關鍵類型", - "uploadFile": "上傳文件", - "pasteKey": "貼上鍵", + "keyPassword": "密鑰密碼(選擇性)", + "keyType": "金鑰類型", + "uploadFile": "上傳檔案", + "pasteKey": "貼上金鑰", "credential": "憑證", "overrideUsername": "覆蓋憑證使用者名稱", "overrideUsernameDesc": "請使用與憑證中儲存的使用者名稱不同的使用者名稱。", - "connectTerminal": "連接到終端", + "connectTerminal": "連接到終端機", "connectFileManager": "連接到檔案管理器", "cancel": "取消", "passwordRequired": "需要密碼", @@ -217,7 +217,7 @@ "dragging": "拖曳 {{fileName}}", "preparing": "準備 {{fileName}}", "readySingle": "準備下載 {{fileName}}", - "readyMultiple": "準備下載 {{count}} 文件", + "readyMultiple": "準備下載 {{count}} 個檔案", "batchDrag": "將 {{count}} 檔案拖曳到桌面", "dragToDesktop": "拖到桌面", "canDragAnywhere": "您可以將檔案拖曳到桌面上的任何位置。" @@ -225,10 +225,10 @@ "sshTools": { "title": "SSH 工具", "closeTools": "關閉 SSH 工具", - "keyRecording": "關鍵錄音", - "startKeyRecording": "開始按鍵錄音", - "stopKeyRecording": "停止按鍵錄音", - "selectTerminals": "選擇終端:", + "keyRecording": "按鍵記錄", + "startKeyRecording": "開始按鍵記錄", + "stopKeyRecording": "停止按鍵記錄", + "selectTerminals": "選擇終端機:", "typeCommands": "輸入命令(支援所有按鍵):", "commandsWillBeSent": "命令將傳送至 {{count}} 選取的終端機。", "settings": "設定", @@ -243,15 +243,15 @@ "new": "新程式碼片段", "create": "建立程式碼片段", "edit": "編輯片段", - "run": "跑步", + "run": "執行", "empty": "目前還沒有片段", - "emptyHint": "建立程式碼片段以保存常用命令", + "emptyHint": "建立程式碼片段以儲存常用命令", "searchSnippets": "搜尋摘要…", "name": "姓名", "description": "描述", "content": "命令", "namePlaceholder": "例如,重啟 Nginx", - "descriptionPlaceholder": "選用描述", + "descriptionPlaceholder": "選擇性描述", "contentPlaceholder": "例如,sudo systemctl restart nginx", "nameRequired": "姓名(必填)", "contentRequired": "需要命令", @@ -259,7 +259,7 @@ "editDescription": "編輯此命令片段", "deleteConfirmTitle": "刪除片段", "deleteConfirmDescription": "您確定要刪除“{{name}}”嗎?", - "createSuccess": "程式碼片段創建成功", + "createSuccess": "程式碼片段建立成功", "updateSuccess": "程式碼片段已成功更新", "deleteSuccess": "程式碼片段已成功刪除", "createFailed": "建立程式碼片段失敗", @@ -283,9 +283,9 @@ "createFolderSuccess": "資料夾建立成功", "updateFolderFailed": "更新資料夾失敗", "createFolderFailed": "建立資料夾失敗", - "selectTerminals": "選擇終端(選購)", + "selectTerminals": "選擇終端機(選擇性)", "executeOnSelected": "在選定的終端機上執行 {{count}} 操作", - "executeOnCurrent": "在目前終端上執行(點擊選擇多個)", + "executeOnCurrent": "在目前終端機上執行(點擊選擇多個)", "folder": "資料夾", "selectFolder": "選擇一個資料夾,或留空。", "noFolder": "無資料夾(未分類)", @@ -303,10 +303,10 @@ "commandHistory": { "title": "歷史", "searchPlaceholder": "搜尋命令…", - "noTerminal": "無活動終端", + "noTerminal": "無活動終端機", "noTerminalHint": "開啟終端機查看其命令歷史記錄。", "empty": "暫無命令歷史記錄", - "emptyHint": "在當前終端中執行命令以建立其歷史記錄。", + "emptyHint": "在當前終端機中執行命令以建立其歷史記錄。", "noResults": "未找到指令", "noResultsHint": "沒有與「{{query}}」相符的指令", "deleteSuccess": "命令已從歷史記錄中刪除", @@ -320,7 +320,7 @@ }, "splitScreen": { "title": "分割畫面", - "none": "沒有任何", + "none": "沒有", "twoSplit": "雙向", "threeSplit": "三向", "fourSplit": "四向", @@ -328,8 +328,8 @@ "dragTabsHint": "將選項卡拖曳到下面的佈局儲存格中進行分配", "layout": "分割畫面佈局", "dropHere": "在此處下拉標籤", - "apply": "應用程式拆分", - "clear": "清晰分割", + "apply": "套用拆分", + "clear": "清除分割", "selectMode": "選擇分割畫面模式", "helpText": "選擇您想要同時查看的標籤頁數量", "success": "分割畫面功能已成功應用", @@ -342,8 +342,8 @@ "homepage": { "loggedInTitle": "已登入!", "loggedInMessage": "您已登入!使用側邊欄存取所有可用工具。首先,請在「SSH 管理員」標籤中建立一個 SSH 主機。建立完成後,您可以使用側邊欄中的其他應用程式連接到該主機。", - "failedToLoadAlerts": "警報加載失敗", - "failedToDismissAlert": "未能關閉警報" + "failedToLoadAlerts": "警示載入失敗", + "failedToDismissAlert": "未能關閉警示" }, "serverConfig": { "title": "伺服器配置", @@ -357,7 +357,7 @@ "connected": "已連接", "disconnected": "斷開連接", "configSaved": "配置已成功儲存", - "saveFailed": "配置保存失敗", + "saveFailed": "配置儲存失敗", "saveError": "儲存配置時發生錯誤", "saving": "儲存...", "saveConfig": "儲存配置", @@ -400,10 +400,10 @@ "success": "成功", "loading": "載入中...", "required": "必需的", - "optional": "選修的", + "optional": "選擇性", "connect": "連接", "connecting": "正在連接...", - "creating": "正在創建…", + "creating": "正在建立…", "clear": "清除", "toggleSidebar": "切換側邊欄", "sidebar": "側邊欄", @@ -412,9 +412,9 @@ "expiresToday": "今天到期", "expiresTomorrow": "將在 {{days}} 天內過期", "updateAvailable": "更新可用", - "sshPath": "SSH路徑", + "sshPath": "SSH 路徑", "localPath": "本地路徑", - "appName": "特米克斯", + "appName": "Termix", "openInNewTab": "在新分頁中開啟", "resetSidebarWidth": "重置側邊欄寬度", "dragToResizeSidebar": "拖曳即可調整側邊欄大小", @@ -433,7 +433,7 @@ "resetCode": "重置程式碼", "newPassword": "新密碼", "folder": "資料夾", - "file": "文件", + "file": "檔案", "renamedSuccessfully": "重新命名成功", "deletedSuccessfully": "已成功刪除", "noTunnelConnections": "未配置隧道連接", @@ -445,7 +445,7 @@ "username": "使用者名稱", "name": "姓名", "login": "登入", - "logout": "註銷", + "logout": "登出", "register": "登記", "password": "密碼", "version": "版本", @@ -454,7 +454,7 @@ "email": "電子郵件", "submit": "提交", "change": "改變", - "save": "節省", + "save": "儲存", "saving": "儲存...", "delete": "刪除", "edit": "編輯", @@ -489,7 +489,7 @@ "failedToInitiatePasswordReset": "密碼重置失敗", "failedToVerifyResetCode": "重置程式碼驗證失敗", "failedToCompletePasswordReset": "密碼重置失敗", - "documentation": "文件", + "documentation": "說明文件", "retry": "重試", "checking": "檢查...", "checkingDatabase": "正在檢查資料庫連線...", @@ -497,19 +497,19 @@ "actions": "行動", "remove": "消除", "revoke": "撤銷", - "create": "創造", + "create": "建立", "update": "更新" }, "nav": { "home": "家", "hosts": "主持人", "credentials": "證書", - "terminal": "終端", + "terminal": "終端機", "docker": "Docker", "tunnels": "隧道", - "fileManager": "文件管理器", + "fileManager": "檔案管理器", "serverStats": "伺服器統計訊息", - "admin": "行政", + "admin": "管理員", "userProfile": "用戶個人資料", "tools": "工具", "snippets": "片段", @@ -562,12 +562,12 @@ "type": "類型", "actions": "行動", "external": "外部的", - "local": "當地的", + "local": "本地", "adminManagement": "管理員管理", "makeUserAdmin": "將使用者設為管理員", "adding": "添加…", "currentAdmins": "目前管理員", - "adminBadge": "行政", + "adminBadge": "管理員", "removeAdminButton": "移除管理員", "general": "一般的", "userRegistration": "用戶註冊", @@ -602,11 +602,11 @@ "linkToPasswordAccount": "連結到密碼帳戶", "linkOIDCDialogTitle": "將 OIDC 帳戶關聯到密碼帳戶", "linkOIDCDialogDescription": "將 {{username}} (OIDC 使用者)連結到現有密碼帳戶。這將為該密碼帳戶啟用雙重驗證。", - "createUser": "創建用戶", + "createUser": "建立用戶", "createUserDescription": "使用用戶名和密碼建立一個新的本機用戶", "enterUsername": "請輸入使用者名稱", "enterPassword": "輸入密碼", - "userCreatedSuccessfully": "使用者 {{username}} 創建成功", + "userCreatedSuccessfully": "使用者 {{username}} 建立成功", "failedToCreateUser": "建立使用者失敗", "manageUser": "管理用戶", "manageUserDescription": "管理使用者設定、角色和權限", @@ -625,7 +625,7 @@ "failedToResetPassword": "密碼重置失敗", "sessionManagement": "會話管理", "revokeAllSessions": "撤銷所有會話", - "revokeAllSessionsDescription": "強制從所有設備和會話中註銷", + "revokeAllSessionsDescription": "強制從所有設備和會話中登出", "revoking": "撤銷…", "revoke": "撤銷所有", "dangerZone": "危險區域", @@ -661,11 +661,11 @@ "enabled": "已啟用", "disabled": "已停用", "keyId": "密鑰 ID", - "created": "創建", + "created": "建立", "migrationStatus": "移民狀態", "migrationCompleted": "遷移完成", "migrationRequired": "需要遷移", - "deviceProtectedMasterKey": "環境保護型主鑰匙", + "deviceProtectedMasterKey": "環境保護主金鑰", "legacyKeyStorage": "傳統密鑰存儲", "masterKeyEncryptedWithDeviceFingerprint": "使用環境指紋加密的主金鑰(KEK保護已啟動)", "keyNotProtectedByDeviceBinding": "金鑰未受環境綁定保護(建議升級)", @@ -674,9 +674,9 @@ "enableAes256EncryptionWithDeviceBinding": "啟用基於環境的主金鑰保護的 AES-256 加密。這可為 SSH 金鑰、密碼和驗證令牌建立企業級安全性。", "featuresEnabled": "已啟用功能:", "aes256GcmAuthenticatedEncryption": "AES-256-GCM 認證加密", - "deviceFingerprintMasterKeyProtection": "環境指紋主鑰匙保護(KEK)", + "deviceFingerprintMasterKeyProtection": "環境指紋主金鑰保護(KEK)", "pbkdf2KeyDerivation": "使用 10 萬次迭代的 PBKDF2 金鑰派生", - "automaticKeyManagement": "自動鑰匙管理和輪換", + "automaticKeyManagement": "自動金鑰管理和輪換", "initializing": "正在初始化...", "initializeEnterpriseEncryption": "初始化企業加密", "migrateExistingData": "遷移現有數據", @@ -698,7 +698,7 @@ "loadingEncryptionStatus": "正在載入加密狀態...", "testMigrationDescription": "驗證現有資料是否可以安全地遷移到加密格式,而不會實際修改任何資料。", "serverMigrationGuide": "伺服器遷移指南", - "migrationInstructions": "若要將加密資料遷移到新伺服器:1) 備份資料庫檔案;2) 在新伺服器上設定環境變數 DB_ENCRYPTION_KEY=\"your-key\";3) 還原資料庫文件", + "migrationInstructions": "若要將加密資料遷移到新伺服器:1) 備份資料庫檔案;2) 在新伺服器上設定環境變數 DB_ENCRYPTION_KEY=\"your-key\";3) 還原資料庫檔案", "environmentProtection": "環境保護", "environmentProtectionDesc": "根據伺服器環境資訊(主機名稱、路徑等)保護加密金鑰,可透過環境變數遷移。", "verificationCompleted": "相容性驗證完成 - 無資料更改", @@ -718,19 +718,19 @@ "stableMacAddressFiltering": "穩定的MAC位址過濾", "databaseFileEncryption": "資料庫檔案加密", "dualLayerProtection": "雙層主動防護", - "bothFieldAndFileEncryptionActive": "為了最大限度地保障安全,欄位級和文件級加密現已同時啟用。", + "bothFieldAndFileEncryptionActive": "為了最大限度地保障安全,欄位級和檔案級加密現已同時啟用。", "fieldLevelAes256Encryption": "對敏感資料進行欄位級 AES-256 加密", "fileLevelDatabaseEncryption": "使用硬體綁定進行檔案層級資料庫加密", "hardwareBoundFileKeys": "硬體綁定的檔案加密金鑰", "automaticEncryptedBackups": "自動建立加密備份", "createEncryptedBackup": "建立加密備份", "creatingBackup": "正在建立備份...", - "backupCreated": "備份已創建", - "encryptedBackupCreatedSuccessfully": "加密備份已成功創建", + "backupCreated": "備份已建立", + "encryptedBackupCreatedSuccessfully": "加密備份已成功建立", "backupCreationFailed": "建立備份失敗", "databaseMigration": "資料庫遷移", "exportForMigration": "導出遷移", - "exportDatabaseForHardwareMigration": "將資料庫匯出為包含解密資料的 SQLite 文件,以便遷移到新硬體。", + "exportDatabaseForHardwareMigration": "將資料庫匯出為包含解密資料的 SQLite 檔案,以便遷移到新硬體。", "exportDatabase": "匯出 SQLite 資料庫", "exporting": "出口...", "exportCreated": "SQLite匯出已建立", @@ -741,9 +741,9 @@ "importDatabaseFromAnotherSystem": "從其他系統或硬體匯入 SQLite 資料庫", "importDatabase": "導入 SQLite 資料庫", "importing": "輸入...", - "selectedFile": "選定的 SQLite 文件", + "selectedFile": "選定的 SQLite 檔案", "importWillReplaceExistingData": "SQLite導入將會取代現有資料 - 建議備份!", - "pleaseSelectImportFile": "請選擇 SQLite 導入文件", + "pleaseSelectImportFile": "請選擇 SQLite 導入檔案", "databaseImportedSuccessfully": "SQLite資料庫已成功導入", "databaseImportFailed": "SQLite資料庫導入失敗", "manageEncryptionAndBackups": "管理加密金鑰、資料庫安全性和備份操作", @@ -753,7 +753,7 @@ "crossSystemDataTransfer": "在不同系統間匯出和匯入資料庫", "noMigrationNeeded": "無需遷移", "encryptionKey": "加密金鑰", - "keyProtection": "鑰匙保護", + "keyProtection": "金鑰保護", "active": "積極的", "legacy": "遺產", "dataStatus": "資料狀態", @@ -771,7 +771,7 @@ "import": "進口", "passwordRequired": "需要密碼", "confirmExport": "確認出口", - "exportDescription": "將 SSH 主機和憑證匯出為 SQLite 文件", + "exportDescription": "將 SSH 主機和憑證匯出為 SQLite 檔案", "importDescription": "使用增量合併導入 SQLite 檔案(跳過重複項)", "criticalWarning": "嚴重警告", "cannotDisablePasswordLoginWithoutOIDC": "未配置 OIDC 時無法停用密碼登入!您必須先設定 OIDC 身份驗證,才能停用密碼登錄,否則您將失去對 Termix 的存取權限。", @@ -806,14 +806,14 @@ }, "hosts": { "title": "主機管理器", - "sshHosts": "SSH主機", + "sshHosts": "SSH 主機", "noHosts": "無 SSH 主機", "noHostsMessage": "您尚未新增任何 SSH 主機。點選「新增主機」開始操作。", - "loadingHosts": "正在加載主機...", + "loadingHosts": "正在載入主機……", "failedToLoadHosts": "主機載入失敗", "retry": "重試", "refresh": "重新整理", - "optional": "選修的", + "optional": "選擇性", "hostsCount": "{{count}} 主機", "importJson": "導入 JSON", "importing": "輸入...", @@ -822,7 +822,7 @@ "downloadSample": "下載範例", "formatGuide": "格式指南", "exportCredentialWarning": "警告:主機「{{name}}」使用憑證驗證。匯出的檔案將不包含憑證數據,匯入後需要手動重新配置。是否繼續?", - "exportSensitiveDataWarning": "警告:主機「{{name}}」包含敏感的驗證資料(密碼/SSH金鑰)。匯出的文件將以明文形式包含這些資料。請妥善保管該文件,並在使用後刪除。是否繼續?", + "exportSensitiveDataWarning": "警告:主機「{{name}}」包含敏感的驗證資料(密碼/SSH金鑰)。匯出的檔案將以明文形式包含這些資料。請妥善保管該檔案,並在使用後刪除。是否繼續?", "uncategorized": "未分類", "confirmDelete": "您確定要刪除“{{name}}”嗎?", "failedToDeleteHost": "刪除主機失敗", @@ -837,12 +837,12 @@ "connectionDetails": "連線詳情", "organization": "組織", "ipAddress": "IP位址", - "port": "港口", + "port": "連接埠", "name": "姓名", "username": "使用者名稱", "folder": "資料夾", "tags": "標籤", - "pin": "別針", + "pin": "置頂", "notes": "筆記", "expirationDate": "截止日期", "passwordRequired": "使用密碼驗證時,需要輸入密碼。", @@ -860,11 +860,11 @@ "savingHost": "拯救主機…", "updatingHost": "正在更新主機...", "cloningHost": "克隆主機...", - "enableTerminal": "啟用終端", + "enableTerminal": "啟用終端機", "enableTerminalDesc": "在終端機選項卡中啟用/停用主機可見性", "enableTunnel": "啟用隧道", "enableTunnelDesc": "在隧道標籤中啟用/停用主機可見性", - "enableFileManager": "啟用文件管理器", + "enableFileManager": "啟用檔案管理器", "enableFileManagerDesc": "在檔案總管標籤中啟用/停用主機可見性", "enableDockerDesc": "在 Docker 標籤中啟用/停用主機可見性", "enableDocker": "啟用 Docker", @@ -887,7 +887,7 @@ "addConnection": "新增隧道連接", "tunnelType": "隧道類型", "tunnelTypeLocal": "本地(-L)", - "tunnelTypeRemote": "遙控器(-R)", + "tunnelTypeRemote": "遠端(-R)", "tunnelTypeLocalDesc": "將本機連接埠轉送到遠端端點", "tunnelTypeRemoteDesc": "將遠端連接埠轉送至本機", "tunnelForwardDescriptionLocal": "此隧道會將本機連接埠 {{sourcePort}} 的流量轉送至終端機上的連接埠 {{endpointPort}} 。", @@ -907,14 +907,14 @@ "permitRootLoginYes": "如果使用root使用者進行隧道連接", "editSshConfig": "編輯 SSH 設定檔:", "sshConfigPath": "/etc/ssh/sshd_config", - "restartSshService": "然後重啟SSH服務:", + "restartSshService": "然後重啟 SSH 服務:", "restartSshCommand": "sudo systemctl restart sshd", "upload": "上傳", "authentication": "驗證", "password": "密碼", - "key": "鑰匙", + "key": "金鑰", "credential": "憑證", - "none": "沒有任何", + "none": "沒有", "selectCredential": "選擇憑證", "selectCredentialPlaceholder": "選擇一種憑證…", "credentialRequired": "使用憑證驗證時需要憑證。", @@ -922,9 +922,9 @@ "cannotChangeAuthAsSharedUser": "無法以共享用戶身份更改身份驗證", "sshPrivateKey": "SSH 私鑰", "keyPassword": "密鑰密碼", - "keyType": "關鍵類型", + "keyType": "金鑰類型", "autoDetect": "自動偵測", - "rsa": "南非", + "rsa": "RSA", "ed25519": "ED25519", "ecdsaNistP256": "ECDSA NIST P-256", "ecdsaNistP384": "ECDSA NIST P-384", @@ -932,19 +932,19 @@ "dsa": "DSA", "rsaSha2256": "RSA SHA2-256", "rsaSha2512": "RSA SHA2-512", - "uploadFile": "上傳文件", - "pasteKey": "貼上鍵", + "uploadFile": "上傳檔案", + "pasteKey": "貼上金鑰", "updateKey": "更新金鑰", "existingKey": "現有密鑰(點擊更改)", "existingCredential": "現有憑證(點擊更改)", "addTagsSpaceToAdd": "新增標籤(此處留空)", - "terminalBadge": "終端", + "terminalBadge": "終端機", "tunnelBadge": "隧道", - "fileManagerBadge": "文件管理器", + "fileManagerBadge": "檔案管理器", "general": "一般的", - "terminal": "終端", + "terminal": "終端機", "tunnel": "隧道", - "fileManager": "文件管理器", + "fileManager": "檔案管理器", "serverStats": "伺服器統計訊息", "hostViewer": "主機檢視器", "enableServerStats": "啟用伺服器統計訊息", @@ -991,8 +991,8 @@ "clickToEditHost": "點選編輯主機", "dragToMoveBetweenFolders": "拖曳即可在資料夾之間移動。", "exportedHostConfig": "已匯出主機配置 {{name}}", - "openTerminal": "打開終端", - "openFileManager": "開啟文件管理器", + "openTerminal": "打開終端機", + "openFileManager": "開啟檔案管理器", "openTunnels": "開放式隧道", "openServerDetails": "開啟伺服器詳情", "statistics": "統計數據", @@ -1017,72 +1017,72 @@ "monitoringDisabledBadge": "關閉監控", "statusMonitoring": "地位", "metricsMonitoring": "指標", - "terminalCustomization": "終端定制", - "appearance": "外貌", + "terminalCustomization": "自訂終端機", + "appearance": "外觀", "behavior": "行為", - "advanced": "先進的", + "advanced": "進階", "themePreview": "主題預覽", "theme": "主題", "selectTheme": "選擇主題", - "chooseColorTheme": "為終端選擇顏色主題", + "chooseColorTheme": "為終端機選擇顏色主題", "fontFamily": "字體系列", "selectFont": "選擇字體", - "selectFontDesc": "選擇終端中使用的字體。", + "selectFontDesc": "選擇終端機中使用的字體。", "fontSize": "字體大小", "fontSizeValue": "字體大小: {{value}}像素", - "adjustFontSize": "調整終端字體大小", + "adjustFontSize": "調整終端機字體大小", "letterSpacing": "字母間距", "letterSpacingValue": "字母間距: {{value}}像素", "adjustLetterSpacing": "調整字元間距", "lineHeight": "行高", "lineHeightValue": "行高: {{value}}", "adjustLineHeight": "調整行間距", - "cursorStyle": "遊標樣式", - "selectCursorStyle": "選擇遊標樣式", - "cursorStyleBlock": "堵塞", - "cursorStyleUnderline": "強調", - "cursorStyleBar": "酒吧", - "chooseCursorAppearance": "選擇遊標外觀", - "cursorBlink": "遊標閃爍", - "enableCursorBlink": "啟用遊標閃爍動畫", + "cursorStyle": "游標樣式", + "selectCursorStyle": "選擇游標樣式", + "cursorStyleBlock": "方塊", + "cursorStyleUnderline": "底線", + "cursorStyleBar": "直線", + "chooseCursorAppearance": "選擇游標外觀", + "cursorBlink": "游標閃爍", + "enableCursorBlink": "啟用游標閃爍動畫", "scrollbackBuffer": "回滾緩衝區", "scrollbackBufferValue": "回滾緩衝區: {{value}} 行", "scrollbackBufferDesc": "回滾歷史記錄中要保留的行數", - "bellStyle": "貝爾風格", + "bellStyle": "響聲樣式", "selectBellStyle": "選擇鈴鐺樣式", - "bellStyleNone": "沒有任何", + "bellStyleNone": "沒有", "bellStyleSound": "聲音", "bellStyleVisual": "視覺的", "bellStyleBoth": "兩個都", - "bellStyleDesc": "如何處理終端提示音(BEL 字符, \\x07)。程式會在任務完成、遇到錯誤或發出通知時觸發此提示音。 「聲音」會發出提示音,「視覺」會短暫閃爍螢幕,「兩者」同時執行提示音和視覺效果,「無」則會停用提示音。", + "bellStyleDesc": "如何處理終端機提示音(BEL 字符, \\x07)。程式會在任務完成、遇到錯誤或發出通知時觸發此提示音。 「聲音」會發出提示音,「視覺」會短暫閃爍螢幕,「兩者」同時執行提示音和視覺效果,「無」則會停用提示音。", "rightClickSelectsWord": "右鍵選擇 Word", "rightClickSelectsWordDesc": "右鍵單擊可選中游標下的單字。", - "fastScrollModifier": "快速滾動修飾符", - "selectModifier": "選擇修飾符", - "modifierAlt": "另類", + "fastScrollModifier": "快速滾動特殊鍵", + "selectModifier": "選擇特殊鍵", + "modifierAlt": "Alt", "modifierCtrl": "Ctrl", - "modifierShift": "轉移", - "fastScrollModifierDesc": "快速滾動的修飾鍵", + "modifierShift": "Shift", + "fastScrollModifierDesc": "快速滾動的特殊鍵", "fastScrollSensitivity": "快速滾動靈敏度", "fastScrollSensitivityValue": "快速滾動靈敏度: {{value}}", - "fastScrollSensitivityDesc": "按住修飾鍵時,滾動速度倍率會提高。", + "fastScrollSensitivityDesc": "按住特殊鍵時,滾動速度倍率會提高。", "minimumContrastRatio": "最小對比度", "minimumContrastRatioValue": "最小對比: {{value}}", "minimumContrastRatioDesc": "自動調整顏色以提高可讀性", - "sshAgentForwarding": "SSH代理轉發", + "sshAgentForwarding": "SSH 代理轉發", "sshAgentForwardingDesc": "轉送 SSH 驗證代理到遠端主機", - "backspaceMode": "退格模式", - "selectBackspaceMode": "選擇退格模式", + "backspaceMode": "後退鍵模式", + "selectBackspaceMode": "選擇後退鍵模式", "backspaceModeNormal": "正常(DEL)", "backspaceModeControlH": "Control-H (^H)", - "backspaceModeDesc": "為了相容性,退格鍵的行為發生了變化", + "backspaceModeDesc": "為了相容性,後退鍵的行為發生了變化", "startupSnippet": "啟動片段", "selectSnippet": "選擇片段", "searchSnippets": "搜尋摘要…", - "snippetNone": "沒有任何", + "snippetNone": "沒有", "noneAuthTitle": "鍵盤互動式驗證", "noneAuthDescription": "連接到 SSH 伺服器時,此身份驗證方法將使用鍵盤互動式身份驗證。", - "noneAuthDetails": "鍵盤互動式驗證可讓伺服器在連線過程中提示您輸入憑證。這對於需要多因素身份驗證的伺服器或您不想在本地保存憑證的情況非常有用。", + "noneAuthDetails": "鍵盤互動式驗證可讓伺服器在連線過程中提示您輸入憑證。這對於需要多因素身份驗證的伺服器或您不想在本地儲存憑證的情況非常有用。", "forceKeyboardInteractive": "強制鍵盤交互", "forceKeyboardInteractiveDesc": "強制使用鍵盤互動式身份驗證。對於使用雙重認證(TOTP/2FA)的伺服器,有時需要這樣做。", "overrideCredentialUsername": "覆蓋憑證使用者名稱", @@ -1103,8 +1103,8 @@ "socks5Port": "代理端口", "socks5Username": "代理用戶名", "socks5Password": "代理密碼", - "socks5UsernameOptional": "可選:如果代理不需要身份驗證,請留空。", - "socks5PasswordOptional": "可選:如果代理不需要身份驗證,請留空。", + "socks5UsernameOptional": "選擇性:如果代理不需要身份驗證,請留空。", + "socks5PasswordOptional": "選擇性:如果代理不需要身份驗證,請留空。", "socks5ProxyChain": "代理鏈", "socks5ProxyChainDescription": "配置一個 SOCKS 代理鏈。鏈中的每個代理都會透過前一個代理連接。", "socks5ProxyMode": "代理模式", @@ -1118,17 +1118,17 @@ "socks5RemoveProxy": "移除代理", "socks5ProxyType": "代理類型", "socks5SaveAsPreset": "另存為預設", - "socks5SavePresetTitle": "將代理鏈保存為預設", + "socks5SavePresetTitle": "將代理鏈儲存為預設", "socks5SavePresetDescription": "將目前代理鏈配置儲存為可重複使用的預設", "socks5PresetName": "預設名稱", - "socks5PresetDescription": "描述(可選)", + "socks5PresetDescription": "描述(選擇性)", "socks5PresetCreated": "已建立代理鏈預設", "socks5PresetUpdated": "代理鏈預設已更新", "socks5PresetDeleted": "代理鏈預設已刪除", "socks5PresetSaved": "預設「{{name}}」已成功儲存", "socks5PresetSaveError": "儲存預設失敗", "socks5PresetNameRequired": "預設名稱為必填項", - "socks5EmptyChainError": "無法保存空的代理鏈", + "socks5EmptyChainError": "無法儲存空的代理鏈", "socks5ProxyChainEmpty": "至少在鏈中新增一個代理程式。", "socks5HostDescription": "SOCKS代理伺服器的主機名稱或IP位址", "socks5PortDescription": "SOCKS代理伺服器的連接埠號碼(預設值:1080)", @@ -1143,10 +1143,10 @@ "quickActionName": "動作名稱", "noSnippetFound": "未找到程式碼片段", "quickActionsOrder": "伺服器統計頁面上將按上述順序顯示快捷操作按鈕。", - "sidebarCustomization": "側邊欄按鈕自訂", + "sidebarCustomization": "自訂側邊欄按鈕", "sidebarCustomizationDesc": "選擇哪些操作在側邊欄顯示為快捷按鈕。未顯示為按鈕的操作將顯示在下拉式選單中。", - "showTerminalInSidebar": "顯示終端按鈕", - "showTerminalInSidebarDesc": "在側邊欄中以快捷按鈕的形式顯示終端", + "showTerminalInSidebar": "顯示終端機按鈕", + "showTerminalInSidebarDesc": "在側邊欄中以快捷按鈕的形式顯示終端機", "showFileManagerInSidebar": "顯示檔案總管按鈕", "showFileManagerInSidebarDesc": "在側邊欄中以快捷按鈕的形式顯示檔案管理器", "showTunnelInSidebar": "顯示隧道按鈕", @@ -1160,16 +1160,16 @@ "sudoPasswordAutoFill": "Sudo 密碼自動填充", "sudoPasswordAutoFillDesc": "當 sudo 提示輸入密碼時,自動提示輸入 SSH 密碼", "sudoPassword": "Sudo 密碼", - "sudoPasswordDesc": "sudo 指令的可選密碼(與金鑰認證結合使用時很有用)", - "socks4": "襪子4", - "socks5": "襪子5", - "executeSnippetOnConnect": "終端連接後執行一段程式碼片段", + "sudoPasswordDesc": "sudo 指令的選擇性密碼(與金鑰認證結合使用時很有用)", + "socks4": "SOCKS4", + "socks5": "SOCKS5", + "executeSnippetOnConnect": "終端機連接後執行一段程式碼片段", "autoMosh": "自動 MOSH", "autoMoshDesc": "連線時自動運行 MOSH 命令", "moshCommand": "MOSH 指令", "moshCommandDesc": "要執行的 MOSH 指令", "environmentVariables": "環境變數", - "environmentVariablesDesc": "為終端會話設定自訂環境變數", + "environmentVariablesDesc": "為終端機會話設定自訂環境變數", "variableName": "變數名", "variableValue": "價值", "addVariable": "新增變數", @@ -1178,8 +1178,8 @@ "copyFullScreenUrl": "複製全螢幕網址", "fullScreenUrlCopied": "全螢幕 URL 已複製到剪貼簿", "failedToCopyUrl": "無法將 URL 複製到剪貼簿", - "copyTerminalUrl": "複製終端 URL", - "copyFileManagerUrl": "複製文件管理器 URL", + "copyTerminalUrl": "複製終端機 URL", + "copyFileManagerUrl": "複製檔案管理器 URL", "copyTunnelUrl": "複製隧道 URL", "copyServerStatsUrl": "複製伺服器統計資料 URL", "copyDockerUrl": "複製 Docker URL", @@ -1199,7 +1199,7 @@ "pids": "PID", "id": "ID", "state": "狀態", - "console": "安慰", + "console": "控制台", "containerMustBeRunning": "容器必須正在運作才能連接到控制台。", "authenticationRequired": "需要身份驗證", "connectedTo": "連接到 {{containerName}}", @@ -1210,10 +1210,10 @@ "disconnectedFromContainer": "已中斷與容器控制台的連線。", "containerNotRunning": "容器未運行", "startContainerToAccess": "啟動容器以存取控制台", - "selectShell": "選擇外殼", - "bash": "巴什", - "sh": "什", - "ash": "灰", + "selectShell": "選擇 shell", + "bash": "Bash", + "sh": "Sh", + "ash": "Ash", "connecting": "正在連接...", "connect": "連接", "disconnect": "斷開", @@ -1244,8 +1244,8 @@ "image": "影像:", "idLabel": "ID:", "ports": "連接埠:", - "noPorts": "沒有任何", - "created": "創建時間:", + "noPorts": "沒有", + "created": "建立時間:", "start": "開始", "stop": "停止", "unpause": "暫停", @@ -1259,7 +1259,7 @@ "backToList": "返回列表", "logs": "紀錄", "stats": "統計數據", - "consoleTab": "安慰", + "consoleTab": "控制台", "failedToFetchLogs": "取得日誌失敗: {{error}}", "failedToDownloadLogs": "日誌下載失敗: {{error}}", "linesToShow": "線條顯示", @@ -1274,7 +1274,7 @@ "noLogsAvailable": "沒有可用日誌" }, "terminal": { - "title": "終端", + "title": "終端機", "connect": "連接到主機", "disconnect": "斷開", "clear": "清除", @@ -1303,7 +1303,7 @@ "reconnected": "已成功重新連接", "maxReconnectAttemptsReached": "已達最大重連嘗試次數", "connectionTimeout": "連線逾時", - "terminalTitle": "終端 - {{host}}", + "terminalTitle": "終端機 - {{host}}", "terminalWithPath": "終端機 - {{host}}:{{path}}", "runTitle": "運行 {{command}} - {{host}}", "totpRequired": "需要雙重認證", @@ -1340,11 +1340,11 @@ "connectionRejected": "伺服器拒絕連線。請檢查您的身份驗證和網路配置。" }, "fileManager": { - "title": "文件管理器", - "file": "文件", + "title": "檔案管理器", + "file": "檔案", "folder": "資料夾", "connectToSsh": "連接到 SSH 以使用檔案操作", - "uploadFile": "上傳文件", + "uploadFile": "上傳檔案", "downloadFile": "下載", "extractArchive": "提取存檔", "extractingArchive": "正在提取 {{name}}...", @@ -1356,11 +1356,11 @@ "archiveName": "存檔名稱", "enterArchiveName": "請輸入存檔名稱...", "compressionFormat": "壓縮格式", - "selectedFiles": "選定文件", - "andMoreFiles": "and {{count}} more...", + "selectedFiles": "選定檔案", + "andMoreFiles": "及 {{count}} 個其他檔案...", "compress": "壓縮", "compressingFiles": "將 {{count}} 個項目壓縮到 {{name}}個項目…", - "filesCompressedSuccessfully": "{{name}} 創建成功", + "filesCompressedSuccessfully": "{{name}} 建立成功", "compressFailed": "壓縮失敗", "edit": "編輯", "preview": "預覽", @@ -1369,30 +1369,30 @@ "pageXOfY": "第 {{current}} 頁,共 {{total}}頁", "zoomOut": "縮小", "zoomIn": "放大", - "newFile": "新文件", + "newFile": "新檔案", "newFolder": "新建資料夾", "rename": "重新命名", "renameItem": "重新命名項目", "deleteItem": "刪除項目", "currentPath": "目前路徑", - "uploadFileTitle": "上傳文件", - "maxFileSize": "最大:1GB(JSON)/ 5GB(二進位) - 支援大文件", - "removeFile": "刪除文件", - "clickToSelectFile": "點選選擇文件", - "chooseFile": "選擇文件", + "uploadFileTitle": "上傳檔案", + "maxFileSize": "最大:1GB(JSON)/ 5GB(二進位) - 支援大檔案", + "removeFile": "移除檔案", + "clickToSelectFile": "按下以選取檔案", + "chooseFile": "選擇檔案", "uploading": "正在上傳...", "downloading": "正在下載…", "uploadingFile": "正在上傳 {{name}}...", "uploadingLargeFile": "正在上傳大檔案 {{name}} ({{size}})...", "downloadingFile": "正在下載 {{name}}...", - "creatingFile": "創建 {{name}}...", + "creatingFile": "建立 {{name}}...", "creatingFolder": "創建 {{name}}...", "deletingItem": "刪除 {{type}} {{name}}...", "renamingItem": "將 {{type}} {{oldName}} 重新命名為 {{newName}}...", - "createNewFile": "建立新文件", + "createNewFile": "建立新檔案", "fileName": "檔案名稱", - "creating": "正在創建…", - "createFile": "建立文件", + "creating": "正在建立…", + "createFile": "建立檔案", "createNewFolder": "建立新資料夾", "folderName": "資料夾名稱", "createFolder": "建立資料夾", @@ -1405,14 +1405,14 @@ "thisIsDirectoryRename": "這是一個目錄", "renaming": "重命名...", "fileUploadedSuccessfully": "檔案「{{name}}」已成功上傳", - "failedToUploadFile": "文件上傳失敗", + "failedToUploadFile": "檔案上載失敗", "fileDownloadedSuccessfully": "檔案「{{name}}」已成功下載", - "failedToDownloadFile": "文件下載失敗", - "noFileContent": "未收到文件內容", - "filePath": "文件路徑", - "fileCreatedSuccessfully": "檔案“{{name}}”已成功創建", - "failedToCreateFile": "文件建立失敗", - "folderCreatedSuccessfully": "資料夾“{{name}}”已成功創建", + "failedToDownloadFile": "檔案下載失敗", + "noFileContent": "未收到檔案內容", + "filePath": "檔案路徑", + "fileCreatedSuccessfully": "檔案“{{name}}”已成功建立", + "failedToCreateFile": "檔案建立失敗", + "folderCreatedSuccessfully": "資料夾“{{name}}”已成功建立", "failedToCreateFolder": "建立資料夾失敗", "failedToCreateItem": "建立專案失敗", "operationFailed": "{{operation}} 操作對 {{name}}: {{error}}失敗", @@ -1426,34 +1426,34 @@ "sudoOperationFailed": "sudo 操作失敗", "sudoAuthFailed": "sudo 驗證失敗", "deleteOperation": "刪除檔案/資料夾", - "dragFilesToUpload": "文件拖放到這裡即可上傳", + "dragFilesToUpload": "將檔案拖曳至此處以上傳", "emptyFolder": "此資料夾為空。", "itemCount": "{{count}} 項目", "selectedCount": "{{count}} 已選擇", - "searchFiles": "搜尋文件...", + "searchFiles": "搜尋檔案...", "upload": "上傳", "selectHostToStart": "選擇主機以啟動檔案管理", - "failedToConnect": "SSH連線失敗。", + "failedToConnect": "SSH 連線失敗", "failedToLoadDirectory": "載入目錄失敗", "noSSHConnection": "沒有可用的 SSH 連接", "enterFolderName": "請輸入資料夾名稱:", "enterFileName": "請輸入檔案名稱:", "copy": "複製", - "cut": "切", + "cut": "剪下", "paste": "貼上", "copyPath": "複製路徑", "copyPaths": "複製路徑", "delete": "刪除", - "properties": "特性", + "properties": "內容", "refresh": "重新整理", "downloadFiles": "下載 {{count}} 檔案到瀏覽器", "copyFiles": "複製 {{count}} 項", "cutFiles": "剪力 {{count}} 項", "deleteFiles": "刪除 {{count}} 項", - "filesCopiedToClipboard": "{{count}} 項目已複製到剪貼簿", - "filesCutToClipboard": "{{count}} 項目已剪下至剪貼簿", + "filesCopiedToClipboard": "{{count}} 個項目已複製到剪貼簿", + "filesCutToClipboard": "{{count}} 個項目已剪下至剪貼簿", "pathCopiedToClipboard": "路徑已複製到剪貼簿", - "pathsCopiedToClipboard": "{{count}} paths copied to clipboard", + "pathsCopiedToClipboard": "{{count}} 個路徑已複製到剪貼簿", "failedToCopyPath": "無法將路徑複製到剪貼簿", "movedItems": "已移動 {{count}} 個項目", "failedToDeleteItem": "刪除項目失敗", @@ -1463,30 +1463,30 @@ "permissions": "權限", "size": "尺寸", "modified": "修改的", - "path": "小路", + "path": "路徑", "confirmDelete": "您確定要刪除 {{name}}嗎?", - "uploadSuccess": "文件上傳成功", - "uploadFailed": "文件上傳失敗", - "downloadSuccess": "文件下載成功", - "downloadFailed": "文件下載失敗", - "permissionDenied": "没有权限", + "uploadSuccess": "檔案上傳成功", + "uploadFailed": "檔案上傳失敗", + "downloadSuccess": "檔案下載成功", + "downloadFailed": "檔案下載失敗", + "permissionDenied": "權限被拒絕", "checkDockerLogs": "查看 Docker 日誌以取得詳細的錯誤訊息", "internalServerError": "發生內部伺服器錯誤", "serverError": "伺服器錯誤", "error": "錯誤", "requestFailed": "請求失敗,狀態碼", "unknownFileError": "未知", - "cannotReadFile": "無法讀取文件", + "cannotReadFile": "無法讀取檔案", "noSshSessionId": "沒有可用的 SSH 會話 ID", "noFilePath": "沒有可用的檔案路徑", "noCurrentHost": "目前沒有可用主機", - "fileSavedSuccessfully": "文件已成功儲存", - "saveTimeout": "儲存操作超時。文件可能已成功保存,但操作耗時過長。請查看 Docker 日誌以確認。", - "failedToSaveFile": "文件保存失敗", + "fileSavedSuccessfully": "成功儲存檔案", + "saveTimeout": "儲存操作超時。檔案可能已成功儲存,但操作耗時過長。請查看 Docker 日誌以確認。", + "failedToSaveFile": "儲存檔案失敗", "deletedSuccessfully": "已成功刪除", "connectToServer": "連接到伺服器", - "selectServerToEdit": "從側邊欄選擇伺服器即可開始編輯文件", - "fileOperations": "文件操作", + "selectServerToEdit": "從側邊欄選擇伺服器即可開始編輯檔案", + "fileOperations": "檔案操作", "confirmDeleteMessage": "您確定要刪除 {{name}}嗎?", "confirmDeleteSingleItem": "您確定要永久刪除“{{name}}”嗎?", "confirmDeleteMultipleItems": "您確定要永久刪除 {{count}} 項目嗎?", @@ -1498,59 +1498,59 @@ "recent": "最近的", "pinned": "置頂", "folderShortcuts": "資料夾快捷方式", - "noRecentFiles": "沒有最近文件。", - "noPinnedFiles": "沒有已置頂的文件。", + "noRecentFiles": "沒有最近檔案。", + "noPinnedFiles": "沒有已置頂的檔案。", "enterFolderPath": "輸入資料夾路徑", "noShortcuts": "沒有捷徑可走。", - "searchFilesAndFolders": "搜尋文件和資料夾…", - "noFilesOrFoldersFound": "未找到任何文件或資料夾。", - "failedToConnectSSH": "SSH連線失敗。", + "searchFilesAndFolders": "搜尋檔案和資料夾…", + "noFilesOrFoldersFound": "未找到任何檔案或資料夾。", + "failedToConnectSSH": "SSH 連線失敗", "failedToReconnectSSH": "SSH 會話重新連線失敗", - "failedToListFiles": "文件列表失敗", + "failedToListFiles": "檔案列表失敗", "fetchHomeDataTimeout": "取得家庭數據逾時", "sshStatusCheckTimeout": "SSH 狀態檢查逾時", "sshReconnectionTimeout": "SSH 重新連線逾時", "saveOperationTimeout": "儲存操作逾時", - "cannotSaveFile": "無法儲存文件", + "cannotSaveFile": "無法儲存檔案", "dragSystemFilesToUpload": "將系統檔案拖曳到此處上傳", "dragFilesToWindowToDownload": "將檔案拖出視窗即可下載", - "openTerminalHere": "在此處打開終端", - "run": "跑步", + "openTerminalHere": "在此處打開終端機", + "run": "執行", "saveToSystem": "另存為...", "selectLocationToSave": "選擇儲存位置", - "openTerminalInFolder": "在此資料夾中開啟終端", - "openTerminalInFileLocation": "打開終端,指向檔案位置", + "openTerminalInFolder": "在此資料夾中開啟終端機", + "openTerminalInFileLocation": "打開終端機,指向檔案位置", "terminalWithPath": "終端機 - {{host}}:{{path}}", "runningFile": "運行 - {{file}}", - "onlyRunExecutableFiles": "只能運行可執行文件", + "onlyRunExecutableFiles": "只能運行可執行檔案", "noHostSelected": "未選擇主機", "starred": "星標", "shortcuts": "快速方式", "directories": "目錄", - "removedFromRecentFiles": "從最近文件中移除“{{name}}”", + "removedFromRecentFiles": "從最近檔案中移除“{{name}}”", "removeFailed": "移除失敗", "unpinnedSuccessfully": "已成功取消固定“{{name}}”", "unpinFailed": "解除鎖定失敗", "removedShortcut": "已移除捷徑“{{name}}”", "removeShortcutFailed": "移除快捷方式失敗", - "clearedAllRecentFiles": "已清除所有最近文件", + "clearedAllRecentFiles": "已清除所有最近檔案", "clearFailed": "清除失敗", - "removeFromRecentFiles": "從最近文件中刪除", - "clearAllRecentFiles": "清除所有最近文件", - "unpinFile": "解壓縮文件", + "removeFromRecentFiles": "從最近檔案中刪除", + "clearAllRecentFiles": "清除所有最近檔案", + "unpinFile": "解壓縮檔案", "removeShortcut": "移除快捷方式", "saveFilesToSystem": "將 {{count}} 檔案另存為...", - "pinFile": "針文件", + "pinFile": "置頂檔案", "addToShortcuts": "新增到快捷方式", "downloadToDefaultLocation": "下載到預設位置", "pasteFailed": "貼上失敗", "noUndoableActions": "沒有可撤銷的操作", - "undoCopySuccess": "撤銷複製操作:已刪除 {{count}} 個已複製的文件", - "undoCopyFailedDelete": "撤銷失敗:無法刪除任何已複製的文件", - "undoCopyFailedNoInfo": "撤銷失敗:找不到已複製的文件訊息", + "undoCopySuccess": "撤銷複製操作:已刪除 {{count}} 個已複製的檔案", + "undoCopyFailedDelete": "撤銷失敗:無法刪除任何已複製的檔案", + "undoCopyFailedNoInfo": "撤銷失敗:找不到已複製的檔案訊息", "undoMoveSuccess": "撤銷移動操作:已將 {{count}} 個檔案移回原始位置", "undoMoveFailedMove": "撤銷失敗:無法將任何檔案移回。", - "undoMoveFailedNoInfo": "撤銷失敗:找不到已移動的文件訊息", + "undoMoveFailedNoInfo": "撤銷失敗:找不到已移動的檔案訊息", "undoDeleteNotSupported": "刪除操作無法撤銷:檔案已從伺服器永久刪除。", "undoTypeNotSupported": "不支援的撤銷操作類型", "undoOperationFailed": "撤銷操作失敗", @@ -1572,7 +1572,7 @@ "search": "搜尋", "findNext": "尋找下一個", "findPrevious": "找上一個", - "save": "節省", + "save": "儲存", "selectAll": "全選", "undo": "撤銷", "redo": "重做", @@ -1588,8 +1588,8 @@ "originalSize": "原尺寸", "startTyping": "開始輸入…", "unknownSize": "尺寸未知", - "fileIsEmpty": "文件為空", - "largeFileWarning": "大文件警告", + "fileIsEmpty": "空白檔案", + "largeFileWarning": "大檔案警告", "largeFileWarningDesc": "此檔案大小為 {{size}} ,以文字格式開啟時可能會導致效能問題。", "fileNotFoundAndRemoved": "檔案「{{name}}」未找到,已從最近/置頂檔案移除。", "failedToLoadFile": "檔案載入失敗: {{error}}", @@ -1598,13 +1598,13 @@ "fileAutoSaved": "文件自動儲存", "moveFileFailed": "移動失敗 {{name}}", "moveOperationFailed": "移動操作失敗", - "canOnlyCompareFiles": "只能比較兩個文件", - "comparingFiles": "正在比較文件: {{file1}} 和 {{file2}}", + "canOnlyCompareFiles": "只能比較兩個檔案", + "comparingFiles": "正在比較檔案: {{file1}} 和 {{file2}}", "dragFailed": "拖曳操作失敗", - "filePinnedSuccessfully": "文件「{{name}}」已成功固定", - "pinFileFailed": "文件固定失敗", - "fileUnpinnedSuccessfully": "文件「{{name}}」已成功取消固定", - "unpinFileFailed": "取消固定文件失敗", + "filePinnedSuccessfully": "檔案「{{name}}」已成功置頂", + "pinFileFailed": "檔案置頂失敗", + "fileUnpinnedSuccessfully": "檔案「{{name}}」已成功取消置頂", + "unpinFileFailed": "取消置頂檔案失敗", "shortcutAddedSuccessfully": "資料夾捷徑「{{name}}」已成功新增", "addShortcutFailed": "新增快捷方式失敗", "operationCompletedSuccessfully": "{{operation}} {{count}} 專案成功", @@ -1618,7 +1618,7 @@ "newFileDefault": "NewFile.txt", "successfullyMovedItems": "已成功將 {{count}} 個項目移至 {{target}}", "move": "移動", - "searchInFile": "在文件中搜尋(Ctrl+F)", + "searchInFile": "在檔案中搜尋(Ctrl+F)", "showKeyboardShortcuts": "顯示鍵盤快速鍵", "startWritingMarkdown": "開始寫你的 Markdown 內容…", "loadingFileComparison": "正在載入檔案比較...", @@ -1626,8 +1626,8 @@ "compare": "比較", "sideBySide": "並排", "inline": "排隊", - "fileComparison": "文件比較: {{file1}} vs {{file2}}", - "fileTooLarge": "文件過大: {{error}}", + "fileComparison": "檔案比較: {{file1}} vs {{file2}}", + "fileTooLarge": "檔案過大: {{error}}", "sshConnectionFailed": "SSH 連線失敗。請檢查您與 {{name}} ({{ip}}:{{port}}) 的連線。", "loadFileFailed": "檔案載入失敗: {{error}}", "connecting": "正在連接...", @@ -1641,7 +1641,7 @@ "currentPermissions": "目前權限", "newPermissions": "新權限", "owner": "擁有者", - "group": "團體", + "group": "群組", "others": "其他的", "read": "讀", "write": "寫", @@ -1664,7 +1664,7 @@ } }, "tunnels": { - "title": "SSH隧道", + "title": "SSH 隧道", "noSshTunnels": "沒有 SSH 隧道", "createFirstTunnelMessage": "您尚未建立任何 SSH 隧道。請在主機管理員中設定隧道連線以開始使用。", "connected": "已連接", @@ -1684,18 +1684,18 @@ "connect": "連接", "disconnect": "斷開", "cancel": "取消", - "port": "港口", + "port": "連接埠", "attempt": "試 {{current}} 的 {{max}}", "nextRetryIn": "下次重試將在 {{seconds}} 秒後進行", "checkDockerLogs": "檢查 Docker 日誌以查找錯誤原因,並加入討論。", - "orCreate": "或創建一個 ", + "orCreate": "或建立一個 ", "noTunnelConnections": "未配置隧道連接", "tunnelConnections": "隧道連接", "addTunnel": "添加隧道", "editTunnel": "編輯隧道", "deleteTunnel": "刪除隧道", "tunnelName": "隧道名稱", - "localPort": "本地港口", + "localPort": "本地連接埠", "remoteHost": "遠端主機", "remotePort": "遠端埠", "autoStart": "自動啟動", @@ -1706,7 +1706,7 @@ "stop": "停止", "restart": "重啟", "connectionType": "連接類型", - "local": "當地的", + "local": "本地", "remote": "偏僻的", "dynamic": "動態的", "unknownConnectionStatus": "未知", @@ -1724,7 +1724,7 @@ "network": "網路", "uptime": "正常運作時間", "loadAverage": "平均值: {{avg1}}, {{avg5}}, {{avg15}}", - "processes": "流程", + "processes": "程序", "connections": "連接", "usage": "用法", "available": "可用的", @@ -1732,8 +1732,8 @@ "free": "自由的", "used": "用過的", "percentage": "百分比", - "refreshStatusAndMetrics": "刷新狀態和指標", - "refreshStatus": "刷新狀態", + "refreshStatusAndMetrics": "重新整理狀態和指標", + "refreshStatus": "重新整理狀態", "fileManagerAlreadyOpen": "此主機的檔案管理器已開啟", "openFileManager": "開啟文件管理器", "connecting": "正在連接...", @@ -1752,8 +1752,8 @@ "failedToFetchStatus": "取得伺服器狀態失敗", "failedToFetchMetrics": "取得伺服器指標失敗", "failedToFetchHomeData": "取得家庭數據失敗", - "loadingMetrics": "正在加載指標...", - "refreshing": "令人耳目一新…", + "loadingMetrics": "正在載入指標……", + "refreshing": "正在重新整理……", "serverOffline": "伺服器離線", "cannotFetchMetrics": "無法從離線伺服器取得指標", "totpRequired": "需要 TOTP 驗證", @@ -1771,7 +1771,7 @@ "saveLayout": "儲存佈局", "unsavedChanges": "未儲存的更改", "layoutSaved": "佈局已成功儲存", - "failedToSaveLayout": "佈局保存失敗", + "failedToSaveLayout": "儲存佈局失敗", "systemInfo": "系統資訊", "hostname": "主機名稱", "operatingSystem": "作業系統", @@ -1780,8 +1780,8 @@ "seconds": "秒", "networkInterfaces": "網路介面", "noInterfacesFound": "未找到網路介面", - "totalProcesses": "總流程", - "running": "跑步", + "totalProcesses": "總程序數", + "running": "執行中", "noProcessesFound": "未找到任何進程", "loginStats": "SSH 登入統計訊息", "totalLogins": "總登入次數", @@ -1799,7 +1799,7 @@ "ports": { "title": "監聽埠", "protocol": "協定", - "port": "港口", + "port": "連接埠", "address": "地址", "state": "狀態", "process": "過程", @@ -1816,7 +1816,7 @@ "noData": "沒有可用的防火牆數據", "action": "行動", "protocol": "原型", - "port": "港口", + "port": "連接埠", "source": "來源", "accept": "接受", "drop": "降低", @@ -1844,7 +1844,7 @@ "registerFailed": "註冊失敗", "logoutSuccess": "已成功登出", "invalidCredentials": "使用者名稱或密碼無效", - "accountCreated": "帳戶創建成功", + "accountCreated": "帳號建立成功", "passwordReset": "密碼重設連結已發送", "twoFactorAuth": "雙重身份驗證", "enterCode": "請輸入驗證碼", @@ -1855,7 +1855,7 @@ "disableTwoFactor": "禁用雙重認證", "scanQRCode": "使用您的身份驗證器應用程式掃描此二維碼", "backupCodes": "備用代碼", - "saveBackupCodes": "將這些備份代碼保存在安全的地方。", + "saveBackupCodes": "將這些備份代碼儲存在安全的地方。", "twoFactorEnabledSuccess": "雙因素認證已成功啟用!", "twoFactorDisabled": "雙重認證已停用", "newBackupCodesGenerated": "產生新的備份程式碼", @@ -1886,7 +1886,7 @@ "sshAuthFailedDescription": "您提供的憑證不正確。請使用有效的憑證重試。", "sshTimeoutDescription": "身份驗證嘗試超時,請重試。", "sshProvideCredentialsDescription": "請提供您的 SSH 憑證以連接到此伺服器。", - "sshPasswordDescription": "請輸入此SSH連線的密碼。", + "sshPasswordDescription": "請輸入此 SSH 連線的密碼。", "sshKeyPasswordDescription": "如果您的 SSH 金鑰已加密,請在此輸入密碼。", "step1ScanQR": "步驟 1:使用您的身份驗證器應用程式掃描二維碼", "manualEntryCode": "手動輸入代碼", @@ -1897,9 +1897,9 @@ "verificationCode": "驗證碼", "back": "後退", "verifyAndEnable": "驗證並啟用", - "saveBackupCodesTitle": "保存您的備份代碼", - "step3StoreCodesSecurely": "步驟 3:將這些代碼保存在安全的地方", - "importantBackupCodesText": "請將這些備用代碼保存在安全的地方。如果您遺失了驗證器設備,可以使用這些代碼存取您的帳戶。", + "saveBackupCodesTitle": "儲存您的備份代碼", + "step3StoreCodesSecurely": "步驟 3:將這些代碼儲存在安全的地方", + "importantBackupCodesText": "請將這些備用代碼儲存在安全的地方。如果您遺失了驗證器設備,可以使用這些代碼存取您的帳戶。", "completeSetup": "完整設定", "notEnabledText": "雙重認證透過在登入時要求輸入來自身份驗證器應用程式的驗證碼來增加一層額外的安全性。", "enableTwoFactorButton": "啟用雙重認證", @@ -1963,7 +1963,7 @@ "usernameExists": "使用者名稱已存在", "emailExists": "電子郵件已存在", "loadFailed": "資料載入失敗", - "saveError": "保存失敗", + "saveError": "儲存失敗", "sessionExpired": "會話已過期 - 請重新登錄", "totpRateLimited": "頻率限制:TOTP驗證嘗試次數過多。請稍後再試。", "totpRateLimitedWithTime": "速率限制:TOTP 驗證嘗試次數過多。請稍等 {{time}} 秒後再試。", @@ -1971,8 +1971,8 @@ "resetCodeRateLimitedWithTime": "驗證次數過多,請稍等 {{time}} 秒後再試。" }, "messages": { - "saveSuccess": "保存成功", - "saveError": "保存失敗", + "saveSuccess": "儲存成功", + "saveError": "儲存失敗", "deleteSuccess": "已成功刪除", "deleteError": "刪除失敗", "updateSuccess": "更新成功", @@ -2003,27 +2003,27 @@ "twoFactorAuth": "雙重身份驗證", "accountInfo": "帳號資訊", "role": "角色", - "admin": "行政人員", + "admin": "管理員", "user": "使用者", "authMethod": "身份驗證方法", - "local": "當地的", + "local": "本地", "external": "外部(OIDC)", "externalAndLocal": "雙重認證", "selectPreferredLanguage": "請選擇您偏好的介面語言。", "fileColorCoding": "文件顏色編碼", - "fileColorCodingDesc": "依類型對文件進行顏色編碼:資料夾(紅色)、文件(藍色)、符號連結(綠色)", + "fileColorCodingDesc": "依類型對檔案進行顏色編碼:資料夾(紅色)、檔案(藍色)、符號連結(綠色)", "commandAutocomplete": "命令自動完成", "commandAutocompleteDesc": "啟用 Tab 鍵自動補全建議,根據您的指令歷史記錄提供終端指令建議", "defaultSnippetFoldersCollapsed": "預設折疊程式碼片段資料夾", "defaultSnippetFoldersCollapsedDesc": "啟用此功能後,打開程式碼片段標籤時,所有程式碼片段資料夾都會折疊起來。", - "terminalSyntaxHighlighting": "終端語法高亮顯示", + "terminalSyntaxHighlighting": "終端機語法高亮顯示", "showHostTags": "節目主持人標籤", "showHostTagsDesc": "在側邊欄中顯示每個主機下方的標籤。停用此選項可隱藏所有標籤。", "account": "帳戶", - "appearance": "外貌", + "appearance": "外觀", "languageLocalization": "語言和本地化", - "fileManagerSettings": "文件管理器", - "terminalSettings": "終端", + "fileManagerSettings": "檔案管理器", + "terminalSettings": "終端機", "hostSidebarSettings": "主機和側邊欄", "snippetsSettings": "片段", "updateSettings": "更新", @@ -2039,7 +2039,7 @@ "appearanceDesc": "選擇應用程式的顏色主題", "terminalSyntaxHighlightingDesc": "自動高亮顯示終端輸出中的指令、路徑、IP位址和日誌等級", "enableCommandPaletteShortcut": "啟用指令面板快速鍵", - "enableCommandPaletteShortcutDesc": "雙擊左Shift鍵開啟指令面板,以便快速存取主機。" + "enableCommandPaletteShortcutDesc": "雙擊左 Shift 鍵開啟指令面板,以便快速存取主機。" }, "user": { "failedToLoadVersionInfo": "版本資訊載入失敗" @@ -2056,8 +2056,8 @@ "folder": "資料夾", "password": "密碼", "keyPassword": "密鑰密碼", - "sudoPassword": "sudo 密碼(可選)", - "notes": "加上關於此房東的備註…", + "sudoPassword": "sudo 密碼(選擇性)", + "notes": "加上關於此主機的備註…", "expirationDate": "選擇到期日", "pastePrivateKey": "把你的私鑰貼到這裡…", "pastePublicKey": "把你的公鑰貼到這裡…", @@ -2129,17 +2129,17 @@ "noTunnelConnections": "未配置隧道連接", "discord": "Discord", "connectToSshForOperations": "連接到 SSH 以使用檔案操作", - "uploadFile": "上傳文件", - "newFile": "新文件", + "uploadFile": "上傳檔案", + "newFile": "新檔案", "newFolder": "新建資料夾", "rename": "重新命名", "deleteItem": "刪除項目", - "createNewFile": "建立新文件", + "createNewFile": "建立新檔案", "createNewFolder": "建立新資料夾", "renameItem": "重新命名項目", - "clickToSelectFile": "點選選擇文件", + "clickToSelectFile": "點選選擇檔案", "noSshHosts": "無 SSH 主機", - "sshHosts": "SSH主機", + "sshHosts": "SSH 主機", "importSshHosts": "從 JSON 匯入 SSH 主機", "clientId": "客戶ID", "clientSecret": "客戶機密", @@ -2162,10 +2162,10 @@ "disconnected": "斷開連接", "maxRetriesExhausted": "最大重試次數已用盡", "endpointHostNotFound": "未找到端點主機", - "administrator": "行政人員", + "administrator": "管理員", "user": "使用者", "external": "外部的", - "local": "當地的", + "local": "本地", "saving": "儲存...", "saveConfiguration": "儲存配置", "loading": "載入中...", @@ -2186,13 +2186,13 @@ "pinConnection": "引腳連接", "authentication": "驗證", "password": "密碼", - "key": "鑰匙", + "key": "金鑰", "sshPrivateKey": "SSH 私鑰", "keyPassword": "密鑰密碼", - "keyType": "關鍵類型", - "enableTerminal": "啟用終端", + "keyType": "金鑰類型", + "enableTerminal": "啟用終端機", "enableTunnel": "啟用隧道", - "enableFileManager": "啟用文件管理器", + "enableFileManager": "啟用檔案管理器", "defaultPath": "預設路徑", "tunnelConnections": "隧道連接", "maxRetries": "最大重試次數", @@ -2230,15 +2230,15 @@ "mobileAppInProgress": "行動應用程式正在開發中", "mobileAppInProgressDesc": "我們正在開發一款專門的行動應用程序,以便在行動裝置上提供更好的體驗。", "viewMobileAppDocs": "安裝行動應用", - "mobileAppDocumentation": "行動應用程式文件" + "mobileAppDocumentation": "行動應用程式檔案" }, "dashboard": { "title": "儀表板", - "loading": "正在加載儀錶板...", + "loading": "正在載入儀錶板……", "github": "GitHub", "support": "支援", "discord": "Discord", - "donate": "捐", + "donate": "贊助", "serverOverview": "伺服器概覽", "version": "版本", "upToDate": "最新", @@ -2248,11 +2248,11 @@ "healthy": "健康", "error": "錯誤", "totalServers": "伺服器總數", - "totalTunnels": "全隧道", + "totalTunnels": "隧道總數", "totalCredentials": "證書總數", "recentActivity": "近期活動", "reset": "重置", - "loadingRecentActivity": "正在加載最近的活動...", + "loadingRecentActivity": "正在載入最近的活動……", "noRecentActivity": "近期無活動", "quickActions": "快速操作", "addHost": "新增主機", @@ -2288,8 +2288,8 @@ "exportJson": "導出 JSON", "importJson": "導入 JSON", "selectHost": "選擇主機", - "chooseHost": "選擇房東…", - "parentGroup": "母集團", + "chooseHost": "選擇主機…", + "parentGroup": "父群組", "noGroup": "無組", "groupName": "組名", "color": "顏色", @@ -2300,8 +2300,8 @@ "addConnection": "新增連接", "hostDetails": "主機詳情", "connectToHost": "連接到主機", - "openTerminal": "打開終端", - "openFileManager": "開啟文件管理器", + "openTerminal": "打開終端機", + "openFileManager": "開啟檔案管理器", "openTunnel": "開放式隧道", "openDocker": "Open Docker", "openServerStats": "開啟伺服器詳情", @@ -2312,7 +2312,7 @@ "delete": "刪除", "cancel": "取消", "add": "添加", - "create": "創造", + "create": "建立", "update": "更新", "move": "移動", "connect": "連接", @@ -2321,7 +2321,7 @@ "noAvailableHosts": "沒有可用的主機。", "selectSourcePlaceholder": "選擇來源...", "selectTargetPlaceholder": "選擇目標...", - "invalidFile": "無效文件", + "invalidFile": "無效檔案", "hostAlreadyExists": "主機已在拓樸結構中", "sourceTargetMustDiffer": "源和目標必須不同", "connectionExists": "連線已存在", @@ -2337,12 +2337,12 @@ "importJSON": "導入 JSON", "searchHost": "搜尋主機...", "noHostFound": "未找到主機", - "searchGroup": "搜尋組...", + "searchGroup": "搜尋群組...", "noGroupFound": "未找到任何群組", "searchNode": "搜尋節點...", "noNodeFound": "未找到節點", - "terminal": "終端", - "fileManager": "文件管理器", + "terminal": "終端機", + "fileManager": "檔案管理器", "tunnel": "隧道", "docker": "Docker", "serverStats": "伺服器統計訊息" @@ -2448,10 +2448,10 @@ "sharedAccessExpiresIn": "共用存取權限將在 {{hours}} 小時後過期。", "roles": { "label": "角色", - "admin": "行政人員", + "admin": "管理員", "user": "使用者" }, - "createRole": "創建角色", + "createRole": "建立角色", "editRole": "編輯角色", "roleName": "角色名稱", "displayName": "顯示名稱", @@ -2468,11 +2468,11 @@ "searchRoles": "搜尋職位…", "noRoleFound": "未找到角色", "assign": "分配", - "roleCreatedSuccessfully": "角色創建成功", + "roleCreatedSuccessfully": "角色建立成功", "roleUpdatedSuccessfully": "角色已成功更新", "roleDeletedSuccessfully": "角色已成功刪除", "failedToLoadRoles": "角色載入失敗", - "failedToSaveRole": "角色保存失敗", + "failedToSaveRole": "角色儲存失敗", "failedToDeleteRole": "刪除角色失敗", "roleDisplayNameRequired": "角色顯示名稱為必填項", "roleNameRequired": "角色名稱為必填項", @@ -2520,7 +2520,7 @@ "updateLog": "更新日誌", "hosts": "主持人", "openServerDetails": "開啟伺服器詳情", - "openFileManager": "開啟文件管理器", + "openFileManager": "開啟檔案管理器", "edit": "編輯", "links": "連結", "github": "GitHub", @@ -2531,7 +2531,7 @@ "toToggle": "切換", "close": "關閉", "hostManager": "主機管理器", - "pressToToggle": "按兩次左Shift鍵開啟指令面板。" + "pressToToggle": "按兩次左 Shift 鍵開啟指令面板。" }, "docker": { "notEnabled": "此主機未啟用 Docker", @@ -2559,9 +2559,9 @@ "failedToRemoveContainer": "移除容器 {{name}}失敗", "image": "影像", "idLabel": "ID", - "ports": "港口", + "ports": "連接埠", "noPorts": "無埠", - "created": "創建", + "created": "已建立", "start": "開始", "stop": "停止", "pause": "暫停", @@ -2611,11 +2611,11 @@ "consoleError": "控制台錯誤", "errorMessage": "錯誤: {{message}}", "failedToConnect": "連線容器失敗", - "console": "安慰", - "selectShell": "選擇外殼", - "bash": "巴什", - "sh": "什", - "ash": "灰", + "console": "控制台", + "selectShell": "選擇 shell", + "bash": "Bash", + "sh": "sh", + "ash": "ash", "connect": "連接", "disconnect": "斷開", "notConnected": "未連接", @@ -2625,7 +2625,7 @@ "backToList": "返回列表", "logs": "紀錄", "stats": "統計數據", - "consoleTab": "安慰", + "consoleTab": "控制台", "startContainerToAccess": "啟動容器以存取控制台", "log": { "connecting": "正在連線到 Docker 主機...", diff --git a/src/types/index.ts b/src/types/index.ts index 9d3a11c9..843704dd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -23,7 +23,7 @@ export interface SSHHost { folder: string; tags: string[]; pin: boolean; - authType: "password" | "key" | "credential" | "none"; + authType: "password" | "key" | "credential" | "none" | "opkssh"; password?: string; key?: string; keyPassword?: string; @@ -95,7 +95,7 @@ export interface SSHHostData { folder?: string; tags?: string[]; pin?: boolean; - authType: "password" | "key" | "credential" | "none"; + authType: "password" | "key" | "credential" | "none" | "opkssh"; password?: string; key?: File | null; keyPassword?: string; @@ -150,7 +150,7 @@ export interface Credential { folder?: string; tags: string[]; authType: "password" | "key"; - username: string; + username?: string; password?: string; key?: string; publicKey?: string; @@ -170,7 +170,7 @@ export interface CredentialBackend { folder: string | null; tags: string; authType: "password" | "key"; - username: string; + username: string | null; password: string | null; key: string; private_key?: string; @@ -190,7 +190,7 @@ export interface CredentialData { folder?: string; tags: string[]; authType: "password" | "key"; - username: string; + username?: string; password?: string; key?: string; publicKey?: string; @@ -465,7 +465,7 @@ export type ErrorType = // AUTHENTICATION TYPES // ============================================================================ -export type AuthType = "password" | "key" | "credential" | "none"; +export type AuthType = "password" | "key" | "credential" | "none" | "opkssh"; export type KeyType = "rsa" | "ecdsa" | "ed25519"; @@ -486,11 +486,13 @@ export interface ApiResponse { export interface CredentialsManagerProps { onEditCredential?: (credential: Credential) => void; + onAddCredential?: () => void; } export interface CredentialEditorProps { editingCredential?: Credential | null; onFormSubmit?: () => void; + onBack?: () => void; } export interface CredentialViewerProps { @@ -523,6 +525,7 @@ export interface SSHManagerHostEditorProps { export interface SSHManagerHostViewerProps { onEditHost?: (host: SSHHost) => void; + onAddHost?: () => void; } export interface HostProps { diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index b068dcbb..62e91a70 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -59,12 +59,15 @@ function AppContent({ useEffect(() => { const handleDatabaseConnectionLost = () => { setDbConnectionFailed(true); - setIsAuthenticated(false); }; const handleDatabaseConnectionRestored = () => { setDbConnectionFailed(false); - window.location.reload(); + toast.success(t("common.backendReconnected")); + }; + + const handleSessionExpired = () => { + setIsAuthenticated(false); }; dbHealthMonitor.on( @@ -75,6 +78,7 @@ function AppContent({ "database-connection-restored", handleDatabaseConnectionRestored, ); + dbHealthMonitor.on("session-expired", handleSessionExpired); return () => { dbHealthMonitor.off( @@ -85,6 +89,7 @@ function AppContent({ "database-connection-restored", handleDatabaseConnectionRestored, ); + dbHealthMonitor.off("session-expired", handleSessionExpired); }; }, []); diff --git a/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx b/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx index e9b1a60f..1ca5c436 100644 --- a/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx +++ b/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx @@ -6,6 +6,7 @@ import { Download, Upload } from "lucide-react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { isElectron } from "@/ui/main-axios.ts"; +import { getBasePath } from "@/lib/base-path"; interface DatabaseSecurityTabProps { currentUser: { @@ -56,7 +57,7 @@ export function DatabaseSecurityTab({ ? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/export` : isDev ? `http://localhost:30001/database/export` - : `${window.location.protocol}//${window.location.host}/database/export`; + : `${window.location.protocol}//${window.location.host}${getBasePath()}/database/export`; const response = await fetch(apiUrl, { method: "POST", @@ -127,7 +128,7 @@ export function DatabaseSecurityTab({ ? `${(window as { configuredServerUrl?: string }).configuredServerUrl}/database/import` : isDev ? `http://localhost:30001/database/import` - : `${window.location.protocol}//${window.location.host}/database/import`; + : `${window.location.protocol}//${window.location.host}${getBasePath()}/database/import`; const formData = new FormData(); formData.append("file", importFile); diff --git a/src/ui/desktop/apps/dashboard/Dashboard.tsx b/src/ui/desktop/apps/dashboard/Dashboard.tsx index 69f9d3c3..0e968a5b 100644 --- a/src/ui/desktop/apps/dashboard/Dashboard.tsx +++ b/src/ui/desktop/apps/dashboard/Dashboard.tsx @@ -72,6 +72,9 @@ export function Dashboard({ const [totalServers, setTotalServers] = useState(0); const [totalTunnels, setTotalTunnels] = useState(0); const [totalCredentials, setTotalCredentials] = useState(0); + const [updateCheckDisabled, setUpdateCheckDisabled] = useState( + localStorage.getItem("disableUpdateCheck") === "true", + ); const [recentActivity, setRecentActivity] = useState( [], ); @@ -159,9 +162,10 @@ export function Dashboard({ const uptimeInfo = await getUptime(); setUptime(uptimeInfo.formatted); - const updateCheckDisabled = + const updateDisabled = localStorage.getItem("disableUpdateCheck") === "true"; - if (!updateCheckDisabled) { + setUpdateCheckDisabled(updateDisabled); + if (!updateDisabled) { const versionInfo = await getVersionInfo(); setVersionText(`v${versionInfo.localVersion}`); if ( @@ -170,6 +174,9 @@ export function Dashboard({ ) { setVersionStatus(versionInfo.status); } + } else { + const versionInfo = await getVersionInfo(); + setVersionText(`v${versionInfo.localVersion}`); } try { @@ -246,6 +253,10 @@ export function Dashboard({ return null; } + if (host.authType === "opkssh") { + return null; + } + const existingSession = viewerSessions.get(host.id); let sessionId = existingSession; @@ -395,36 +406,40 @@ export function Dashboard({ const handleAddHost = () => { const sshManagerTab = tabList.find((t) => t.type === "ssh_manager"); if (sshManagerTab) { - updateTab(sshManagerTab.id, { - initialTab: "add_host", - hostConfig: undefined, - }); setCurrentTab(sshManagerTab.id); + setTimeout(() => { + window.dispatchEvent(new CustomEvent("host-manager:add-host")); + }, 100); } else { const id = addTab({ type: "ssh_manager", title: "Host Manager", - initialTab: "add_host", + initialTab: "hosts", }); setCurrentTab(id); + setTimeout(() => { + window.dispatchEvent(new CustomEvent("host-manager:add-host")); + }, 100); } }; const handleAddCredential = () => { const sshManagerTab = tabList.find((t) => t.type === "ssh_manager"); if (sshManagerTab) { - updateTab(sshManagerTab.id, { - initialTab: "add_credential", - hostConfig: undefined, - }); setCurrentTab(sshManagerTab.id); + setTimeout(() => { + window.dispatchEvent(new CustomEvent("host-manager:add-credential")); + }, 100); } else { const id = addTab({ type: "ssh_manager", title: "Host Manager", - initialTab: "add_credential", + initialTab: "credentials", }); setCurrentTab(id); + setTimeout(() => { + window.dispatchEvent(new CustomEvent("host-manager:add-credential")); + }, 100); } }; @@ -558,7 +573,7 @@ export function Dashboard({
); } else if (card.id === "recent_activity") { diff --git a/src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx b/src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx index 23c49af0..0d4ac9d0 100644 --- a/src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx +++ b/src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx @@ -246,7 +246,6 @@ export function NetworkGraphCard({ setElements([...nodes, ...validEdges]); } catch (err) { console.error("Failed to load topology:", err); - setError(t("networkGraph.failedToLoadData")); } finally { setLoading(false); } diff --git a/src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx b/src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx index f3864719..8193e3e4 100644 --- a/src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx +++ b/src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx @@ -20,6 +20,7 @@ interface ServerOverviewCardProps { totalServers: number; totalTunnels: number; totalCredentials: number; + updateCheckDisabled?: boolean; } export function ServerOverviewCard({ @@ -31,6 +32,7 @@ export function ServerOverviewCard({ totalServers, totalTunnels, totalCredentials, + updateCheckDisabled = false, }: ServerOverviewCardProps): React.ReactElement { const { t } = useTranslation(); @@ -54,16 +56,20 @@ export function ServerOverviewCard({

{versionText}

- - + {!updateCheckDisabled && ( + <> + + + + )}
diff --git a/src/ui/desktop/apps/features/docker/components/ConsoleTerminal.tsx b/src/ui/desktop/apps/features/docker/components/ConsoleTerminal.tsx index acc82a67..4e0fbc9f 100644 --- a/src/ui/desktop/apps/features/docker/components/ConsoleTerminal.tsx +++ b/src/ui/desktop/apps/features/docker/components/ConsoleTerminal.tsx @@ -12,6 +12,7 @@ import { SelectValue, } from "@/components/ui/select.tsx"; import { Card, CardContent } from "@/components/ui/card.tsx"; +import { getBasePath } from "@/lib/base-path"; import { Terminal as TerminalIcon, Power, PowerOff } from "lucide-react"; import { toast } from "sonner"; import type { SSHHost } from "@/types"; @@ -169,7 +170,7 @@ export function ConsoleTerminal({ const wsHost = baseUrl.replace(/^https?:\/\//, ""); return `${wsProtocol}${wsHost}/docker/console/`; })() - : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/docker/console/`; + : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}${getBasePath()}/docker/console/`; const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(token)}`; const ws = new WebSocket(wsUrl); diff --git a/src/ui/desktop/apps/features/file-manager/FileManager.tsx b/src/ui/desktop/apps/features/file-manager/FileManager.tsx index b25b7311..03d19c37 100644 --- a/src/ui/desktop/apps/features/file-manager/FileManager.tsx +++ b/src/ui/desktop/apps/features/file-manager/FileManager.tsx @@ -1,4 +1,10 @@ -import React, { useState, useEffect, useRef, useCallback } from "react"; +import React, { + useState, + useEffect, + useRef, + useCallback, + useMemo, +} from "react"; import { FileManagerGrid } from "./FileManagerGrid.tsx"; import { FileManagerSidebar } from "./FileManagerSidebar.tsx"; import { FileManagerContextMenu } from "./FileManagerContextMenu.tsx"; @@ -2161,8 +2167,21 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { localStorage.setItem("fileManagerViewMode", viewMode); }, [viewMode]); - const filteredFiles = files.filter((file) => - file.name.toLowerCase().includes(searchQuery.toLowerCase()), + const filteredFiles = useMemo( + () => + files + .filter((file) => + file.name.toLowerCase().includes(searchQuery.toLowerCase()), + ) + .sort((a, b) => { + if (a.type === "directory" && b.type !== "directory") return -1; + if (a.type !== "directory" && b.type === "directory") return 1; + return a.name.localeCompare(b.name, undefined, { + numeric: true, + sensitivity: "base", + }); + }), + [files, searchQuery], ); if (!currentHost) { diff --git a/src/ui/desktop/apps/features/terminal/Terminal.tsx b/src/ui/desktop/apps/features/terminal/Terminal.tsx index 47d4e562..bc91dfc1 100644 --- a/src/ui/desktop/apps/features/terminal/Terminal.tsx +++ b/src/ui/desktop/apps/features/terminal/Terminal.tsx @@ -12,6 +12,7 @@ import { ClipboardAddon } from "@xterm/addon-clipboard"; import { Unicode11Addon } from "@xterm/addon-unicode11"; import { WebLinksAddon } from "@xterm/addon-web-links"; import { useTranslation } from "react-i18next"; +import { getBasePath } from "@/lib/base-path"; import { getCookie, isElectron, @@ -23,6 +24,8 @@ import { import { TOTPDialog } from "@/ui/desktop/navigation/dialogs/TOTPDialog.tsx"; import { SSHAuthDialog } from "@/ui/desktop/navigation/dialogs/SSHAuthDialog.tsx"; import { WarpgateDialog } from "@/ui/desktop/navigation/dialogs/WarpgateDialog.tsx"; +import { OPKSSHDialog } from "@/ui/desktop/navigation/dialogs/OPKSSHDialog.tsx"; +import { HostKeyVerificationDialog } from "@/ui/desktop/navigation/dialogs/HostKeyVerificationDialog.tsx"; import { TERMINAL_THEMES, DEFAULT_TERMINAL_CONFIG, @@ -154,6 +157,25 @@ const TerminalInner = forwardRef( const [warpgateAuthUrl, setWarpgateAuthUrl] = useState(""); const [warpgateSecurityKey, setWarpgateSecurityKey] = useState(""); const warpgateTimeoutRef = useRef(null); + + const [opksshDialog, setOpksshDialog] = useState<{ + isOpen: boolean; + authUrl: string; + requestId: string; + stage: "chooser" | "waiting" | "authenticating" | "completed" | "error"; + error?: string; + } | null>(null); + const opksshTimeoutRef = useRef(null); + const opksshFailedRef = useRef(false); + const currentHostIdRef = useRef(null); + const currentHostConfigRef = useRef(null); + + const [hostKeyVerification, setHostKeyVerification] = useState<{ + isOpen: boolean; + scenario: "new" | "changed"; + data: any; + } | null>(null); + const isVisibleRef = useRef(false); const isFittingRef = useRef(false); const reconnectTimeoutRef = useRef(null); @@ -709,7 +731,7 @@ const TerminalInner = forwardRef( const wsHost = baseUrl.replace(/^https?:\/\//, ""); return `${wsProtocol}${wsHost}/ssh/websocket/`; })() - : `/ssh/websocket/`; + : `${getBasePath()}/ssh/websocket/`; if ( webSocketRef.current && @@ -775,6 +797,9 @@ const TerminalInner = forwardRef( } }, 35000); + currentHostIdRef.current = hostConfig.id; + currentHostConfigRef.current = hostConfig; + ws.send( JSON.stringify({ type: "connectToHost", @@ -899,6 +924,7 @@ const TerminalInner = forwardRef( updateConnectionError(errorMessage); setIsConnecting(false); } else if (msg.type === "connected") { + opksshFailedRef.current = false; setIsConnected(true); setIsConnecting(false); isConnectingRef.current = false; @@ -983,7 +1009,11 @@ const TerminalInner = forwardRef( terminal.clear(); } setIsConnecting(false); - if (onClose) { + if ( + onClose && + !connectionErrorRef.current && + !opksshFailedRef.current + ) { onClose(); } } else if (msg.type === "totp_required") { @@ -1038,6 +1068,124 @@ const TerminalInner = forwardRef( webSocketRef.current.close(); } }, 300000); + } else if (msg.type === "opkssh_auth_required") { + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + if (opksshFailedRef.current) { + setOpksshDialog(null); + if (opksshTimeoutRef.current) { + clearTimeout(opksshTimeoutRef.current); + opksshTimeoutRef.current = null; + } + updateConnectionError(t("terminal.opksshAuthFailed")); + addLog({ + type: "error", + stage: "auth", + message: t("terminal.opksshAuthFailed"), + }); + } else { + opksshFailedRef.current = true; + if (webSocketRef.current) { + webSocketRef.current.send( + JSON.stringify({ + type: "opkssh_start_auth", + data: { hostId: msg.hostId }, + }), + ); + } + } + } else if (msg.type === "opkssh_status") { + if (connectionErrorRef.current) return; + if (msg.stage === "chooser") { + setOpksshDialog({ + isOpen: true, + authUrl: msg.url || "", + requestId: msg.requestId || "", + stage: "chooser", + }); + if (opksshTimeoutRef.current) { + clearTimeout(opksshTimeoutRef.current); + } + opksshTimeoutRef.current = setTimeout(() => { + setOpksshDialog(null); + if (webSocketRef.current) { + webSocketRef.current.close(); + } + }, 300000); + } else { + setOpksshDialog((prev) => + prev ? { ...prev, stage: msg.stage } : null, + ); + } + } else if (msg.type === "opkssh_completed") { + if (opksshTimeoutRef.current) { + clearTimeout(opksshTimeoutRef.current); + opksshTimeoutRef.current = null; + } + setOpksshDialog(null); + if (webSocketRef.current && terminal) { + webSocketRef.current.send( + JSON.stringify({ + type: "opkssh_auth_completed", + data: { + hostId: currentHostIdRef.current, + cols: terminal.cols || 80, + rows: terminal.rows || 24, + hostConfig: currentHostConfigRef.current, + }, + }), + ); + } + } else if (msg.type === "opkssh_error") { + if (connectionErrorRef.current) return; + opksshFailedRef.current = true; + if (opksshDialog) { + setOpksshDialog((prev) => + prev ? { ...prev, stage: "error", error: msg.error } : null, + ); + } else { + setOpksshDialog({ + isOpen: true, + authUrl: "", + requestId: msg.requestId || "", + stage: "error", + error: msg.error, + }); + } + setIsConnecting(false); + } else if (msg.type === "opkssh_timeout") { + if (connectionErrorRef.current) return; + opksshFailedRef.current = true; + if (opksshDialog) { + setOpksshDialog((prev) => + prev + ? { + ...prev, + stage: "error", + error: t("terminal.opksshTimeout"), + } + : null, + ); + } else { + setOpksshDialog({ + isOpen: true, + authUrl: "", + requestId: msg.requestId || "", + stage: "error", + error: t("terminal.opksshTimeout"), + }); + } + setIsConnecting(false); + } else if (msg.type === "opkssh_config_error") { + setOpksshDialog({ + isOpen: true, + authUrl: "", + requestId: msg.requestId || "", + stage: "error", + error: msg.instructions || msg.error, + }); } else if (msg.type === "keyboard_interactive_available") { setKeyboardInteractiveDetected(true); setIsConnecting(false); @@ -1053,6 +1201,26 @@ const TerminalInner = forwardRef( clearTimeout(connectionTimeoutRef.current); connectionTimeoutRef.current = null; } + } else if (msg.type === "host_key_verification_required") { + setHostKeyVerification({ + isOpen: true, + scenario: "new", + data: msg.data, + }); + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + } else if (msg.type === "host_key_changed") { + setHostKeyVerification({ + isOpen: true, + scenario: "changed", + data: msg.data, + }); + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } } else if (msg.type === "connection_log") { if (msg.data) { addLog({ @@ -1063,8 +1231,8 @@ const TerminalInner = forwardRef( }); } } - } catch { - // Message parse errors are logged via backend + } catch (error) { + console.error("WebSocket message handler error:", error); } }); @@ -1399,61 +1567,23 @@ const TerminalInner = forwardRef( }; element?.addEventListener("contextmenu", handleContextMenu); - const handleMacKeyboard = (e: KeyboardEvent) => { - const isMacOS = - navigator.platform.toUpperCase().indexOf("MAC") >= 0 || - navigator.userAgent.toUpperCase().indexOf("MAC") >= 0; + const handleBackspaceMode = (e: KeyboardEvent) => { + if (e.key !== "Backspace") return; + if (e.ctrlKey || e.metaKey || e.altKey) return; + if (config.backspaceMode !== "control-h") return; - if ( - config.backspaceMode === "control-h" && - e.key === "Backspace" && - !e.ctrlKey && - !e.metaKey && - !e.altKey - ) { - e.preventDefault(); - e.stopPropagation(); - if (webSocketRef.current?.readyState === 1) { - webSocketRef.current.send( - JSON.stringify({ type: "input", data: "\x08" }), - ); - } - return false; - } - - if (!isMacOS) return; - - if (e.altKey && !e.metaKey && !e.ctrlKey) { - const keyMappings: { [key: string]: string } = { - "7": "|", - "2": "€", - "8": "[", - "9": "]", - l: "@", - L: "@", - Digit7: "|", - Digit2: "€", - Digit8: "[", - Digit9: "]", - KeyL: "@", - }; - - const char = keyMappings[e.key] || keyMappings[e.code]; - if (char) { - e.preventDefault(); - e.stopPropagation(); - - if (webSocketRef.current?.readyState === 1) { - webSocketRef.current.send( - JSON.stringify({ type: "input", data: char }), - ); - } - return false; - } + e.preventDefault(); + e.stopPropagation(); + + if (webSocketRef.current?.readyState === 1) { + webSocketRef.current.send( + JSON.stringify({ type: "input", data: "\x08" }), + ); } + return false; }; - element?.addEventListener("keydown", handleMacKeyboard, true); + element?.addEventListener("keydown", handleBackspaceMode, true); const resizeObserver = new ResizeObserver(() => { if (resizeTimeout.current) clearTimeout(resizeTimeout.current); @@ -1470,7 +1600,7 @@ const TerminalInner = forwardRef( isFittingRef.current = false; resizeObserver.disconnect(); element?.removeEventListener("contextmenu", handleContextMenu); - element?.removeEventListener("keydown", handleMacKeyboard, true); + element?.removeEventListener("keydown", handleBackspaceMode, true); if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); if (resizeTimeout.current) clearTimeout(resizeTimeout.current); }; @@ -1503,6 +1633,21 @@ const TerminalInner = forwardRef( return true; } + if ( + ((e.ctrlKey && !e.altKey && !e.metaKey) || + (e.metaKey && !e.ctrlKey && !e.altKey)) && + e.key.toLowerCase() === "v" + ) { + e.preventDefault(); + e.stopPropagation(); + readTextFromClipboard().then((pasteText) => { + if (pasteText && webSocketRef.current?.readyState === 1) { + terminal.paste(pasteText); + } + }); + return false; + } + if (e.ctrlKey && e.altKey && !e.metaKey && !e.shiftKey) { const key = e.key.toLowerCase(); const blockedKeys = ["w", "t", "n", "q"]; @@ -1725,10 +1870,13 @@ const TerminalInner = forwardRef( setIsConnecting(true); fitAddonRef.current?.fit(); - if (terminal.cols > 0 && terminal.rows > 0) { - scheduleNotify(terminal.cols, terminal.rows); - connectToHost(terminal.cols, terminal.rows); - } + requestAnimationFrame(() => { + fitAddonRef.current?.fit(); + if (terminal.cols > 0 && terminal.rows > 0) { + scheduleNotify(terminal.cols, terminal.rows); + connectToHost(terminal.cols, terminal.rows); + } + }); // Note: Using hostConfig.id instead of hostConfig object to prevent // unnecessary reconnections when host properties are updated. // Only reconnect when switching to a different host. @@ -1816,6 +1964,76 @@ const TerminalInner = forwardRef( backgroundColor={backgroundColor} /> + {opksshDialog?.isOpen && ( + { + if (webSocketRef.current) { + webSocketRef.current.send( + JSON.stringify({ + type: "opkssh_cancel", + data: { requestId: opksshDialog.requestId }, + }), + ); + } + setOpksshDialog(null); + if (opksshTimeoutRef.current) { + clearTimeout(opksshTimeoutRef.current); + opksshTimeoutRef.current = null; + } + }} + onOpenUrl={() => { + window.open(opksshDialog.authUrl, "_blank"); + if (webSocketRef.current) { + webSocketRef.current.send( + JSON.stringify({ + type: "opkssh_browser_opened", + data: { requestId: opksshDialog.requestId }, + }), + ); + } + }} + backgroundColor={backgroundColor} + /> + )} + + {hostKeyVerification?.isOpen && ( + { + if (webSocketRef.current) { + webSocketRef.current.send( + JSON.stringify({ + type: "host_key_verification_response", + data: { action: "accept" }, + }), + ); + } + setHostKeyVerification(null); + }} + onReject={() => { + if (webSocketRef.current) { + webSocketRef.current.send( + JSON.stringify({ + type: "host_key_verification_response", + data: { action: "reject" }, + }), + ); + } + setHostKeyVerification(null); + setIsConnecting(false); + updateConnectionError(t("terminal.hostKeyRejected")); + }} + backgroundColor={backgroundColor} + /> + )} + { const values = form.getValues(); - if (!values.name || !values.username) return false; + if (!values.name) return false; if (authTab === "password") { return !!(values.password && values.password.trim() !== ""); @@ -377,7 +379,7 @@ export function CredentialEditor({ setFormError(null); if (!data.name || data.name.trim() === "") { - data.name = data.username; + data.name = data.username || "Unnamed Credential"; } const submitData: CredentialData = { @@ -386,7 +388,7 @@ export function CredentialEditor({ folder: data.folder, tags: data.tags, authType: data.authType, - username: data.username, + username: data.username || undefined, keyType: data.keyType, }; @@ -525,6 +527,25 @@ export function CredentialEditor({ {formError} )} +
+ {onBack && ( + + )} +

+ {editingCredential + ? t("credentials.editCredential") + : t("credentials.addCredential")} +

+
(""); const [deployLoading, setDeployLoading] = useState(false); const [hostComboboxOpen, setHostComboboxOpen] = useState(false); + const [showCopyCommandDialog, setShowCopyCommandDialog] = useState(false); + const [copyCommandCredential, setCopyCommandCredential] = + useState(null); const dragCounter = useRef(0); useEffect(() => { @@ -151,6 +158,40 @@ export function CredentialsManager({ setShowDeployDialog(true); }; + const handleCopyCommand = (credential: Credential) => { + if (credential.authType !== "key") { + toast.error(t("credentials.keyBasedOnlyForDeployment")); + return; + } + if (!credential.publicKey) { + toast.error(t("credentials.publicKeyRequiredForDeployment")); + return; + } + setCopyCommandCredential(credential); + setShowCopyCommandDialog(true); + }; + + const getDeployCommand = (credential: Credential): string => { + const publicKey = credential.publicKey?.trim() || ""; + const comment = `${credential.name || credential.username}@Termix`; + const keyWithComment = publicKey.includes(" ") + ? `${publicKey.split(" ").slice(0, 2).join(" ")} ${comment}` + : `${publicKey} ${comment}`; + return `mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo "${keyWithComment}" >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`; + }; + + const copyDeployCommand = async () => { + if (!copyCommandCredential) return; + try { + await navigator.clipboard.writeText( + getDeployCommand(copyCommandCredential), + ); + toast.success(t("credentials.deployCommandCopied")); + } catch { + toast.error(t("credentials.failedToCopy")); + } + }; + const performDeploy = async () => { if (!deployingCredential || !selectedHostId) { toast.error(t("credentials.selectTargetHost")); @@ -420,7 +461,7 @@ export function CredentialsManager({ if (credentials.length === 0) { return (
-
+

{t("credentials.sshCredentials")} @@ -436,6 +477,23 @@ export function CredentialsManager({

+
+
+ + setSearchQuery(e.target.value)} + className="pl-10 h-9" + disabled + /> +
+ +
+
@@ -471,14 +529,20 @@ export function CredentialsManager({
-
- - setSearchQuery(e.target.value)} - className="pl-10" - /> +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 h-9" + /> +
+
@@ -675,24 +739,48 @@ export function CredentialsManager({ {credential.authType === "key" && ( - - - - - -

Deploy SSH key to host

-
-
+ <> + + + + + +

Deploy SSH key to host

+
+
+ + + + + +

+ {t( + "credentials.copyDeployCommand", + )} +

+
+
+ )} @@ -993,6 +1081,106 @@ export function CredentialsManager({
+ + + +
+
+
+
+
+ {t("credentials.copyDeployCommand")} +
+
+ {t("credentials.copyDeployCommandDescription")} +
+
+
+
+ +
+ {copyCommandCredential && ( + <> +
+

+ + {t("credentials.sourceCredential")} +

+
+
+
+ +
+
+
+ {t("common.name")} +
+
+ {copyCommandCredential.name || + copyCommandCredential.username} +
+
+
+
+
+ +
+
+
+ {t("credentials.keyType")} +
+
+ {copyCommandCredential.keyType || + t("credentials.sshKey")} +
+
+
+
+
+ +
+ +
+                      {getDeployCommand(copyCommandCredential)}
+                    
+
+ +
+
+ +

+ {t("credentials.manualDeployInfo")} +

+
+
+ +
+
+ + +
+
+ + )} +
+
+
+
); } diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx index 47b2e1be..08783405 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx @@ -16,7 +16,7 @@ import type { SSHHost, HostManagerProps } from "../../../types/index"; export function HostManager({ isTopbarOpen, - initialTab = "host_viewer", + initialTab = "hosts", hostConfig, _updateTimestamp, rightSidebarOpen = false, @@ -25,10 +25,18 @@ export function HostManager({ updateTab, }: HostManagerProps): React.ReactElement { const { t } = useTranslation(); - const [activeTab, setActiveTab] = useState(initialTab); + const [activeTab, setActiveTab] = useState( + initialTab === "host_viewer" || initialTab === "add_host" + ? "hosts" + : initialTab === "credentials" || initialTab === "add_credential" + ? "credentials" + : initialTab, + ); const [editingHost, setEditingHost] = useState( hostConfig || null, ); + const [isAddingHost, setIsAddingHost] = useState(false); + const [isAddingCredential, setIsAddingCredential] = useState(false); useEffect(() => {}, [editingHost]); @@ -41,53 +49,94 @@ export function HostManager({ const ignoreNextHostConfigChangeRef = useRef(false); const lastProcessedHostIdRef = useRef(undefined); + useEffect(() => { + const handleAddHostEvent = () => { + setActiveTab("hosts"); + setEditingHost(null); + setIsAddingHost(true); + setIsAddingCredential(false); + }; + + const handleAddCredentialEvent = () => { + setActiveTab("credentials"); + setEditingCredential(null); + setIsAddingCredential(true); + setIsAddingHost(false); + }; + + window.addEventListener("host-manager:add-host", handleAddHostEvent); + window.addEventListener( + "host-manager:add-credential", + handleAddCredentialEvent, + ); + + return () => { + window.removeEventListener("host-manager:add-host", handleAddHostEvent); + window.removeEventListener( + "host-manager:add-credential", + handleAddCredentialEvent, + ); + }; + }, []); + useEffect(() => { if (_updateTimestamp !== undefined) { - if (initialTab && initialTab !== activeTab) { - setActiveTab(initialTab); + const normalizedTab = + initialTab === "host_viewer" || initialTab === "add_host" + ? "hosts" + : initialTab === "credentials" || initialTab === "add_credential" + ? "credentials" + : initialTab; + + if (initialTab && normalizedTab !== activeTab) { + setActiveTab(normalizedTab); } - if (hostConfig && hostConfig.id !== editingHost?.id) { + if (hostConfig && hostConfig.id !== lastProcessedHostIdRef.current) { setEditingHost(hostConfig); + setIsAddingHost(false); lastProcessedHostIdRef.current = hostConfig.id; - } else if ( - !hostConfig && - editingHost && - editingHost.id !== lastProcessedHostIdRef.current - ) { + } else if (!hostConfig && editingHost) { setEditingHost(null); - } - - if (initialTab !== "add_credential" && editingCredential) { - setEditingCredential(null); + setIsAddingHost(false); } } else { if (initialTab) { - setActiveTab(initialTab); + const normalizedTab = + initialTab === "host_viewer" || initialTab === "add_host" + ? "hosts" + : initialTab === "credentials" || initialTab === "add_credential" + ? "credentials" + : initialTab; + setActiveTab(normalizedTab); } - if (hostConfig) { + if (hostConfig && hostConfig.id !== lastProcessedHostIdRef.current) { setEditingHost(hostConfig); + setIsAddingHost(false); lastProcessedHostIdRef.current = hostConfig.id; } } - }, [_updateTimestamp, initialTab, hostConfig?.id]); + }, [_updateTimestamp, initialTab, hostConfig]); const handleEditHost = (host: SSHHost) => { setEditingHost(host); - setActiveTab("add_host"); + setIsAddingHost(false); lastProcessedHostIdRef.current = host.id; + }; - if (updateTab && currentTabId !== undefined) { - updateTab(currentTabId, { initialTab: "add_host" }); - } + const handleAddHost = () => { + setEditingHost(null); + setIsAddingHost(true); + lastProcessedHostIdRef.current = undefined; }; const handleFormSubmit = () => { ignoreNextHostConfigChangeRef.current = true; + const savedHostId = editingHost?.id; setEditingHost(null); - setActiveTab("host_viewer"); + setIsAddingHost(false); setTimeout(() => { - lastProcessedHostIdRef.current = undefined; + lastProcessedHostIdRef.current = savedHostId; }, 500); }; @@ -97,37 +146,31 @@ export function HostManager({ username: string; }) => { setEditingCredential(credential); - setActiveTab("add_credential"); + setIsAddingCredential(false); + }; - if (updateTab && currentTabId !== undefined) { - updateTab(currentTabId, { initialTab: "add_credential" }); - } + const handleAddCredential = () => { + setEditingCredential(null); + setIsAddingCredential(true); }; const handleCredentialFormSubmit = () => { setEditingCredential(null); - setActiveTab("credentials"); + setIsAddingCredential(false); }; const handleTabChange = (value: string) => { - if (activeTab === "add_host" && value !== "add_host") { + if (activeTab !== value) { setEditingHost(null); + setEditingCredential(null); + setIsAddingHost(false); + setIsAddingCredential(false); lastProcessedHostIdRef.current = undefined; - // Clear hostConfig from tab data when leaving add_host tab if (updateTab && currentTabId !== undefined) { updateTab(currentTabId, { hostConfig: null }); } } - if (activeTab === "add_credential" && value !== "add_credential") { - setEditingCredential(null); - } - - // Clear editing state when switching TO add_host tab (to ensure fresh state) - if (value === "add_host" && activeTab !== "add_host") { - setEditingHost(null); - lastProcessedHostIdRef.current = undefined; - } setActiveTab(value); @@ -164,76 +207,66 @@ export function HostManager({ > - {t("hosts.hostViewer")} + {t("hosts.hosts")} - - {editingHost - ? editingHost.id - ? t("hosts.editHost") - : t("hosts.cloneHost") - : t("hosts.addHost")} - -
- {t("credentials.credentialsViewer")} - - - {editingCredential - ? t("credentials.editCredential") - : t("credentials.addCredential")} + {t("credentials.credentials")}
- - - - -
- + { + setEditingHost(null); + setIsAddingHost(false); + lastProcessedHostIdRef.current = undefined; + }} + /> +
+ ) : ( + -
+ )} -
- -
-
- - -
- -
+ {editingCredential !== null || isAddingCredential ? ( +
+ { + setEditingCredential(null); + setIsAddingCredential(false); + }} + /> +
+ ) : ( +
+ +
+ )}
diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx index 251a40ce..a39bd70c 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx @@ -122,6 +122,7 @@ import { Shield, Clock, UserCircle, + ArrowLeft, } from "lucide-react"; import { HostGeneralTab } from "./tabs/HostGeneralTab"; import { HostTerminalTab } from "./tabs/HostTerminalTab"; @@ -141,11 +142,13 @@ interface User { interface SSHManagerHostEditorProps { editingHost?: SSHHost | null; onFormSubmit?: (updatedHost?: SSHHost) => void; + onBack?: () => void; } export function HostManagerEditor({ editingHost, onFormSubmit, + onBack, }: SSHManagerHostEditorProps) { const { t } = useTranslation(); const { theme: appTheme } = useTheme(); @@ -165,7 +168,7 @@ export function HostManagerEditor({ const [proxyMode, setProxyMode] = useState<"single" | "chain">("single"); const [authTab, setAuthTab] = useState< - "password" | "key" | "credential" | "none" + "password" | "key" | "credential" | "none" | "opkssh" >("password"); const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">( "upload", @@ -269,7 +272,7 @@ export function HostManagerEditor({ folder: z.string().optional(), tags: z.array(z.string().min(1)).default([]), pin: z.boolean().default(false), - authType: z.enum(["password", "key", "credential", "none"]), + authType: z.enum(["password", "key", "credential", "none", "opkssh"]), credentialId: z.number().optional().nullable(), overrideCredentialUsername: z.boolean().optional(), password: z.string().optional(), @@ -438,6 +441,10 @@ export function HostManagerEditor({ return; } + if (data.authType === "opkssh") { + return; + } + if (data.authType === "password") { if ( !data.password || @@ -475,6 +482,14 @@ export function HostManagerEditor({ path: ["credentialId"], }); } + } else if (data.authType === "credential") { + if (!data.credentialId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t("hosts.credentialRequired"), + path: ["credentialId"], + }); + } } data.tunnelConnections.forEach((connection, index) => { @@ -543,7 +558,8 @@ export function HostManagerEditor({ const isFormValid = React.useMemo(() => { const values = form.getValues(); - if (!values.ip || !values.username) return false; + if (!values.ip || !values.username || values.username.trim() === "") + return false; if (authTab === "password") { return !!(values.password && values.password.trim() !== ""); @@ -553,6 +569,8 @@ export function HostManagerEditor({ return !!values.credentialId; } else if (authTab === "none") { return true; + } else if (authTab === "opkssh") { + return true; } return false; @@ -582,7 +600,7 @@ export function HostManagerEditor({ const selectedCredential = credentials.find( (c) => c.id === currentCredentialId, ); - if (selectedCredential) { + if (selectedCredential?.username) { form.setValue("username", selectedCredential.username, { shouldValidate: true, }); @@ -594,6 +612,12 @@ export function HostManagerEditor({ form.setValue("keyPassword", "", { shouldValidate: true }); form.setValue("keyType", "auto", { shouldValidate: true }); form.setValue("credentialId", null, { shouldValidate: true }); + } else if (authTab === "opkssh") { + form.setValue("password", "", { shouldValidate: true }); + form.setValue("key", null, { shouldValidate: true }); + form.setValue("keyPassword", "", { shouldValidate: true }); + form.setValue("keyType", "auto", { shouldValidate: true }); + form.setValue("credentialId", null, { shouldValidate: true }); } await form.trigger(); @@ -616,13 +640,20 @@ export function HostManagerEditor({ cleanedHost.password = undefined; } - const defaultAuthType = cleanedHost.credentialId - ? "credential" - : cleanedHost.key - ? "key" - : cleanedHost.password - ? "password" - : "none"; + const defaultAuthType = (cleanedHost.authType || + (cleanedHost.credentialId + ? "credential" + : cleanedHost.key + ? "key" + : cleanedHost.password + ? "password" + : "none")) as + | "password" + | "key" + | "credential" + | "none" + | "opkssh"; + setAuthTab(defaultAuthType); let parsedStatsConfig: StatsConfig = DEFAULT_STATS_CONFIG; @@ -647,7 +678,12 @@ export function HostManagerEditor({ folder: cleanedHost.folder || "", tags: Array.isArray(cleanedHost.tags) ? cleanedHost.tags : [], pin: Boolean(cleanedHost.pin), - authType: defaultAuthType as "password" | "key" | "credential" | "none", + authType: defaultAuthType as + | "password" + | "key" + | "credential" + | "none" + | "opkssh", credentialId: cleanedHost.credentialId, overrideCredentialUsername: Boolean( cleanedHost.overrideCredentialUsername, @@ -1131,6 +1167,27 @@ export function HostManagerEditor({ {formError} )} +
+ {onBack && ( + + )} +

+ {editingHost + ? editingHost.id + ? t("hosts.editHost") + : t("hosts.cloneHost") + : t("hosts.addHost")} +

+
-
+

{t("hosts.sshHosts")}

@@ -927,6 +931,23 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) { className="hidden" /> +

+
+ + setSearchQuery(e.target.value)} + className="pl-10 h-9" + disabled + /> +
+ +
+
@@ -1010,14 +1031,20 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) { className="hidden" /> -
- - setSearchQuery(e.target.value)} - className="pl-10" - /> +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 h-9" + /> +
+
diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx index e0b56584..23211829 100644 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx @@ -118,10 +118,15 @@ export function HostGeneralTab({ name="username" render={({ field }) => { const isCredentialAuth = authTab === "credential"; - const hasCredential = !!form.watch("credentialId"); - const overrideEnabled = !!form.watch("overrideCredentialUsername"); + const credentialId = form.watch("credentialId"); + const overrideEnabled = form.watch("overrideCredentialUsername"); + const selectedCredential = credentials.find( + (c) => c.id === credentialId, + ); const shouldDisable = - isCredentialAuth && hasCredential && !overrideEnabled; + isCredentialAuth && + selectedCredential?.username && + !overrideEnabled; return ( @@ -131,6 +136,17 @@ export function HostGeneralTab({ placeholder={t("placeholders.username")} disabled={shouldDisable} {...field} + onChange={(e) => { + field.onChange(e.target.value); + if ( + isCredentialAuth && + selectedCredential && + !selectedCredential.username && + e.target.value.trim() !== "" + ) { + form.setValue("overrideCredentialUsername", true); + } + }} onBlur={(e) => { field.onChange(e.target.value.trim()); field.onBlur(); @@ -319,7 +335,8 @@ export function HostGeneralTab({ | "password" | "key" | "credential" - | "none"; + | "none" + | "opkssh"; setAuthTab(newAuthType); form.setValue("authType", newAuthType); }} @@ -354,6 +371,13 @@ export function HostGeneralTab({ > {t("hosts.none")} + + {t("hosts.opkssh")} + { if ( credential && + credential.username && !form.getValues("overrideCredentialUsername") ) { form.setValue("username", credential.username); @@ -573,30 +598,36 @@ export function HostGeneralTab({ )} /> - {form.watch("credentialId") && ( - ( - -
- - {t("hosts.overrideCredentialUsername")} - - - {t("hosts.overrideCredentialUsernameDesc")} - -
- - - -
- )} - /> - )} + {form.watch("credentialId") && + (() => { + const selectedCredential = credentials.find( + (c) => c.id === form.watch("credentialId"), + ); + return selectedCredential?.username ? ( + ( + +
+ + {t("hosts.overrideCredentialUsername")} + + + {t("hosts.overrideCredentialUsernameDesc")} + +
+ + + +
+ )} + /> + ) : null; + })()}
@@ -608,6 +639,24 @@ export function HostGeneralTab({ + + + + {t("hosts.opksshAuthTitle")} +
{t("hosts.opksshAuthDescription")}
+ +
+
+
diff --git a/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx b/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx index dcaa6a27..556c07f9 100644 --- a/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx +++ b/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx @@ -709,7 +709,24 @@ export function SSHToolsSidebar({ ); }; - const handleExecute = (snippet: Snippet) => { + const handleExecute = async (snippet: Snippet) => { + const confirmEnabled = + localStorage.getItem("confirmSnippetExecution") === "true"; + + if (confirmEnabled) { + const confirmed = await confirmWithToast( + t("snippets.confirmExecution", { name: snippet.name }), + undefined, + "default", + t("common.cancel"), + { confirmOnEnter: true, duration: 8000 }, + ); + + if (!confirmed) { + return; + } + } + if (selectedSnippetTabIds.length > 0) { selectedSnippetTabIds.forEach((tabId) => { const tab = tabs.find((t: TabData) => t.id === tabId); diff --git a/src/ui/desktop/navigation/dialogs/HostKeyVerificationDialog.tsx b/src/ui/desktop/navigation/dialogs/HostKeyVerificationDialog.tsx new file mode 100644 index 00000000..69d06ccb --- /dev/null +++ b/src/ui/desktop/navigation/dialogs/HostKeyVerificationDialog.tsx @@ -0,0 +1,217 @@ +import React, { useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; +import { Shield, AlertTriangle, Copy, Check } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +interface HostKeyVerificationDialogProps { + isOpen: boolean; + scenario: "new" | "changed"; + ip: string; + port: number; + hostname?: string; + fingerprint: string; + oldFingerprint?: string; + keyType: string; + oldKeyType?: string; + algorithm: string; + onAccept: () => void; + onReject: () => void; + backgroundColor?: string; +} + +export function HostKeyVerificationDialog({ + isOpen, + scenario, + ip, + port, + hostname, + fingerprint, + oldFingerprint, + keyType, + oldKeyType, + algorithm, + onAccept, + onReject, + backgroundColor, +}: HostKeyVerificationDialogProps) { + const { t } = useTranslation(); + const [copiedFingerprint, setCopiedFingerprint] = useState(false); + const [copiedOldFingerprint, setCopiedOldFingerprint] = useState(false); + + if (!isOpen) return null; + + const copyToClipboard = (text: string, isOld: boolean = false) => { + navigator.clipboard.writeText(text); + if (isOld) { + setCopiedOldFingerprint(true); + setTimeout(() => setCopiedOldFingerprint(false), 2000); + } else { + setCopiedFingerprint(true); + setTimeout(() => setCopiedFingerprint(false), 2000); + } + }; + + const formatFingerprint = (fp: string) => { + return fp.match(/.{1,2}/g)?.join(":") || fp; + }; + + return ( +
+
+ + +
+ {scenario === "new" ? ( + + ) : ( + + )} + + {scenario === "new" + ? t("hostKey.verifyNewHost") + : t("hostKey.keyChangedWarning")} + +
+ + {hostname || ip}:{port} + +
+ + + {scenario === "new" ? ( + <> + + + {t("hostKey.firstConnectionTitle")} + + {t("hostKey.firstConnectionDescription")} + + + +
+
+ {t("hostKey.fingerprint")} ({algorithm.toUpperCase()}) +
+
+
+ {formatFingerprint(fingerprint)} +
+ +
+
+ + + + {t("hostKey.verifyInstructions")} + + + + ) : ( + <> + + + {t("hostKey.securityWarning")} + + {t("hostKey.keyChangedDescription")} + + + +
+
+
+ {t("hostKey.previousKey")} +
+
+
+ {formatFingerprint(oldFingerprint || "")} +
+ +
+
+ +
+
+ {t("hostKey.newFingerprint")} +
+
+
+ {formatFingerprint(fingerprint)} +
+ +
+
+
+ + )} + +
+ + +
+
+
+
+ ); +} diff --git a/src/ui/desktop/navigation/dialogs/OPKSSHDialog.tsx b/src/ui/desktop/navigation/dialogs/OPKSSHDialog.tsx new file mode 100644 index 00000000..87b02f98 --- /dev/null +++ b/src/ui/desktop/navigation/dialogs/OPKSSHDialog.tsx @@ -0,0 +1,138 @@ +import React, { useEffect } from "react"; +import { Button } from "@/components/ui/button.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { Label } from "@/components/ui/label.tsx"; +import { Shield, Copy, ExternalLink, Loader2, AlertCircle } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; + +interface OPKSSHDialogProps { + isOpen: boolean; + authUrl: string; + requestId: string; + stage: "chooser" | "waiting" | "authenticating" | "completed" | "error"; + error?: string; + onCancel: () => void; + onOpenUrl: () => void; + backgroundColor?: string; +} + +export function OPKSSHDialog({ + isOpen, + authUrl, + requestId, + stage, + error, + onCancel, + onOpenUrl, + backgroundColor, +}: OPKSSHDialogProps) { + const { t } = useTranslation(); + const hasOpenedRef = React.useRef(false); + + useEffect(() => {}, [isOpen, authUrl, stage, onOpenUrl]); + + if (!isOpen) return null; + + const handleCopyUrl = async () => { + try { + await navigator.clipboard.writeText(authUrl); + toast.success(t("common.copied")); + } catch (error) { + toast.error(t("common.copyFailed")); + } + }; + + return ( +
+
+
+
+ +

+ {t("terminal.opksshAuthRequired")} +

+
+ +
+ {stage === "chooser" && authUrl && ( + <> +

+ {t("terminal.opksshAuthDescription")} +

+
+ +
+ + +
+
+
+ + +
+ + )} + + {(stage === "waiting" || stage === "authenticating") && ( +
+ +

+ {stage === "waiting" + ? t("terminal.opksshWaitingForAuth") + : t("terminal.opksshAuthenticating")} +

+
+ )} + + {stage === "error" && error && ( + <> +
+ +
+

+ {t("common.error")} +

+

{error}

+
+
+
+ +
+ + )} +
+
+
+ ); +} diff --git a/src/ui/desktop/navigation/hosts/Host.tsx b/src/ui/desktop/navigation/hosts/Host.tsx index 42f013ff..ccbc7c6a 100644 --- a/src/ui/desktop/navigation/hosts/Host.tsx +++ b/src/ui/desktop/navigation/hosts/Host.tsx @@ -82,7 +82,6 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement { const shouldShowStatus = statsConfig.statusCheckEnabled !== false; const shouldShowMetrics = statsConfig.metricsEnabled !== false; - // Use shared status context instead of individual polling const serverStatus = useHostStatus(host.id, shouldShowStatus); const hasTunnelConnections = useMemo(() => { @@ -270,7 +269,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement { type: "ssh_manager", title: t("nav.hostManager"), hostConfig: host, - initialTab: "add_host", + initialTab: "hosts", }) } className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" diff --git a/src/ui/desktop/user/LanguageSwitcher.tsx b/src/ui/desktop/user/LanguageSwitcher.tsx index 0642cd8f..cad82edc 100644 --- a/src/ui/desktop/user/LanguageSwitcher.tsx +++ b/src/ui/desktop/user/LanguageSwitcher.tsx @@ -16,7 +16,8 @@ const languages = [ { code: "bn", name: "Bengali", nativeName: "বাংলা" }, { code: "bg", name: "Bulgarian", nativeName: "Български" }, { code: "ca", name: "Catalan", nativeName: "Català" }, - { code: "zh", name: "Chinese", nativeName: "中文" }, + { code: "zh-CN", name: "Chinese Simplified", nativeName: "简体中文" }, + { code: "zh-TW", name: "Chinese Traditional", nativeName: "繁体中文" }, { code: "cs", name: "Czech", nativeName: "Čeština" }, { code: "da", name: "Danish", nativeName: "Dansk" }, { code: "nl", name: "Dutch", nativeName: "Nederlands" }, @@ -34,15 +35,20 @@ const languages = [ { code: "no", name: "Norwegian", nativeName: "Norsk" }, { code: "pl", name: "Polish", nativeName: "Polski" }, { - code: "pt", + code: "pt-PT", name: "Portuguese", nativeName: "Português", }, + { + code: "pt-BR", + name: "Portuguese (Brazil)", + nativeName: "Português (Brasil)", + }, { code: "ro", name: "Romanian", nativeName: "Română" }, { code: "ru", name: "Russian", nativeName: "Русский" }, { code: "sr", name: "Serbian", nativeName: "Српски" }, - { code: "es", name: "Spanish", nativeName: "Español" }, - { code: "sv", name: "Swedish", nativeName: "Svenska" }, + { code: "es-ES", name: "Spanish", nativeName: "Español" }, + { code: "sv-SE", name: "Swedish", nativeName: "Svenska" }, { code: "th", name: "Thai", nativeName: "ไทย" }, { code: "tr", name: "Turkish", nativeName: "Türkçe" }, { code: "uk", name: "Ukrainian", nativeName: "Українська" }, diff --git a/src/ui/desktop/user/UserProfile.tsx b/src/ui/desktop/user/UserProfile.tsx index 1af0b3d7..2f52cef2 100644 --- a/src/ui/desktop/user/UserProfile.tsx +++ b/src/ui/desktop/user/UserProfile.tsx @@ -141,6 +141,11 @@ export function UserProfile({ const saved = localStorage.getItem("commandPaletteShortcutEnabled"); return saved !== null ? saved === "true" : true; }); + const [confirmSnippetExecution, setConfirmSnippetExecution] = + useState(() => { + const saved = localStorage.getItem("confirmSnippetExecution"); + return saved !== null ? saved === "true" : false; + }); const [userRoles, setUserRoles] = useState([]); useEffect(() => { @@ -230,6 +235,11 @@ export function UserProfile({ localStorage.setItem("commandPaletteShortcutEnabled", enabled.toString()); }; + const handleConfirmSnippetExecutionToggle = (enabled: boolean) => { + setConfirmSnippetExecution(enabled); + localStorage.setItem("confirmSnippetExecution", enabled.toString()); + }; + const handleDeleteAccount = async (e: React.FormEvent) => { e.preventDefault(); setDeleteLoading(true); @@ -643,6 +653,20 @@ export function UserProfile({ } />
+
+
+ +

+ {t("profile.confirmSnippetExecutionDesc")} +

+
+ +
diff --git a/src/ui/hooks/useCommandTracker.ts b/src/ui/hooks/useCommandTracker.ts index 069ac7d6..45f82506 100644 --- a/src/ui/hooks/useCommandTracker.ts +++ b/src/ui/hooks/useCommandTracker.ts @@ -41,7 +41,7 @@ export function useCommandTracker({ if ( (charCode >= 65 && charCode <= 90) || (charCode >= 97 && charCode <= 122) || - charCode === 126 // ~ + charCode === 126 ) { isInEscapeSequenceRef.current = false; } diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index ecfdf359..b91b442d 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -1,5 +1,6 @@ import axios, { AxiosError, type AxiosInstance } from "axios"; import { toast } from "sonner"; +import { getBasePath } from "@/lib/base-path"; import type { SSHHost, SSHHostData, @@ -49,7 +50,7 @@ export interface AccessRecord { roleDisplayName: string | null; grantedBy: string; grantedByUsername: string; - permissionLevel: "view"; // Only view permission is supported + permissionLevel: "view"; expiresAt: string | null; createdAt: string; } @@ -472,7 +473,9 @@ function createApiInstance( toast.warning("Session expired. Please log in again."); } - dbHealthMonitor.reportDatabaseError(error, wasAuthenticated); + if (wasAuthenticated) { + dbHealthMonitor.reportSessionExpired(); + } userWasAuthenticated = false; } @@ -671,7 +674,7 @@ function getApiUrl(path: string, defaultPort: number): string { const url = `${protocol}://${apiHost}:${sslPort}${path}`; return url; } else { - return path; + return getBasePath() + path; } } diff --git a/src/ui/mobile/apps/terminal/Terminal.tsx b/src/ui/mobile/apps/terminal/Terminal.tsx index 7cbb9200..08cfd428 100644 --- a/src/ui/mobile/apps/terminal/Terminal.tsx +++ b/src/ui/mobile/apps/terminal/Terminal.tsx @@ -4,6 +4,7 @@ import { useState, useImperativeHandle, forwardRef, + useCallback, } from "react"; import { useXTerm } from "react-xtermjs"; import { FitAddon } from "@xterm/addon-fit"; @@ -11,9 +12,24 @@ import { ClipboardAddon } from "@xterm/addon-clipboard"; import { Unicode11Addon } from "@xterm/addon-unicode11"; import { WebLinksAddon } from "@xterm/addon-web-links"; import { useTranslation } from "react-i18next"; -import { isElectron, getCookie } from "@/ui/main-axios.ts"; +import { isElectron, getCookie, getSnippets } from "@/ui/main-axios.ts"; +import { getBasePath } from "@/lib/base-path"; import { useTheme } from "@/components/theme-provider"; -import { TERMINAL_THEMES } from "@/constants/terminal-themes"; +import { + TERMINAL_THEMES, + DEFAULT_TERMINAL_CONFIG, + TERMINAL_FONTS, +} from "@/constants/terminal-themes.ts"; +import type { TerminalConfig } from "@/types"; +import { TOTPDialog } from "@/ui/desktop/navigation/dialogs/TOTPDialog.tsx"; +import { SSHAuthDialog } from "@/ui/desktop/navigation/dialogs/SSHAuthDialog.tsx"; +import { WarpgateDialog } from "@/ui/desktop/navigation/dialogs/WarpgateDialog.tsx"; +import { + ConnectionLogProvider, + useConnectionLog, +} from "@/ui/desktop/navigation/connection-log/ConnectionLogContext.tsx"; +import { ConnectionLog } from "@/ui/desktop/navigation/connection-log/ConnectionLog.tsx"; +import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; interface HostConfig { id?: number; @@ -26,6 +42,8 @@ interface HostConfig { keyType?: string; authType?: string; credentialId?: number; + terminalConfig?: TerminalConfig; + name?: string; [key: string]: unknown; } @@ -43,11 +61,32 @@ interface SSHTerminalProps { title?: string; } -export const Terminal = forwardRef( +const TerminalInner = forwardRef( function SSHTerminal({ hostConfig, isVisible }, ref) { const { t } = useTranslation(); const { instance: terminal, ref: xtermRef } = useXTerm(); const { theme: appTheme } = useTheme(); + const { addLog, isExpanded: isConnectionLogExpanded } = useConnectionLog(); + + const config = { ...DEFAULT_TERMINAL_CONFIG, ...hostConfig.terminalConfig }; + + const isDarkMode = + appTheme === "dark" || + (appTheme === "system" && + window.matchMedia("(prefers-color-scheme: dark)").matches); + + let themeColors; + if (config.theme === "termix") { + themeColors = isDarkMode + ? TERMINAL_THEMES.termixDark.colors + : TERMINAL_THEMES.termixLight.colors; + } else { + themeColors = + TERMINAL_THEMES[config.theme]?.colors || + TERMINAL_THEMES.termixDark.colors; + } + const backgroundColor = themeColors.background; + const fitAddonRef = useRef(null); const webSocketRef = useRef(null); const resizeTimeout = useRef(null); @@ -55,27 +94,56 @@ export const Terminal = forwardRef( const pingIntervalRef = useRef(null); const [visible, setVisible] = useState(false); const [isReady, setIsReady] = useState(false); - const [, setIsConnected] = useState(false); - const [, setIsConnecting] = useState(false); - const [, setConnectionError] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); + const [connectionError, setConnectionError] = useState(null); + const connectionErrorRef = useRef(null); + + const updateConnectionError = useCallback((error: string | null) => { + connectionErrorRef.current = error; + setConnectionError(error); + }, []); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [totpRequired, setTotpRequired] = useState(false); + const [totpPrompt, setTotpPrompt] = useState(""); + const [isPasswordPrompt, setIsPasswordPrompt] = useState(false); + const [showAuthDialog, setShowAuthDialog] = useState(false); + const [authDialogReason, setAuthDialogReason] = useState< + "no_keyboard" | "auth_failed" | "timeout" + >("no_keyboard"); + const [warpgateAuthRequired, setWarpgateAuthRequired] = useState(false); + const [warpgateAuthUrl, setWarpgateAuthUrl] = useState(""); + const [warpgateSecurityKey, setWarpgateSecurityKey] = useState(""); + const isVisibleRef = useRef(false); - const isConnectingRef = useRef(false); const isFittingRef = useRef(false); + const reconnectTimeoutRef = useRef(null); + const reconnectAttempts = useRef(0); + const maxReconnectAttempts = 3; + const isUnmountingRef = useRef(false); + const shouldNotReconnectRef = useRef(false); + const isReconnectingRef = useRef(false); + const isConnectingRef = useRef(false); + const connectionAttemptIdRef = useRef(0); + const totpTimeoutRef = useRef(null); + const connectionTimeoutRef = useRef(null); + const warpgateTimeoutRef = useRef(null); const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null); const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null); const notifyTimerRef = useRef(null); const DEBOUNCE_MS = 140; - const isDarkMode = - appTheme === "dark" || - (appTheme === "system" && - window.matchMedia("(prefers-color-scheme: dark)").matches); - const themeColors = { - background: isDarkMode ? "#0e0e10" : "#ffffff", - foreground: isDarkMode ? "#f7f7f7" : "#18181b", - }; + useEffect(() => { + isUnmountingRef.current = false; + shouldNotReconnectRef.current = false; + isReconnectingRef.current = false; + isConnectingRef.current = false; + reconnectAttempts.current = 0; + + return () => {}; + }, [hostConfig.id]); useEffect(() => { isVisibleRef.current = isVisible; @@ -161,14 +229,130 @@ export const Terminal = forwardRef( }, DEBOUNCE_MS); } + function handleTotpSubmit(code: string) { + if (webSocketRef.current && code) { + if (totpTimeoutRef.current) { + clearTimeout(totpTimeoutRef.current); + totpTimeoutRef.current = null; + } + webSocketRef.current.send( + JSON.stringify({ + type: isPasswordPrompt ? "password_response" : "totp_response", + data: { code }, + }), + ); + setTotpRequired(false); + setTotpPrompt(""); + setIsPasswordPrompt(false); + } + } + + function handleTotpCancel() { + if (totpTimeoutRef.current) { + clearTimeout(totpTimeoutRef.current); + totpTimeoutRef.current = null; + } + setTotpRequired(false); + setTotpPrompt(""); + webSocketRef.current?.close(); + } + + function handleWarpgateContinue() { + if (webSocketRef.current) { + if (warpgateTimeoutRef.current) { + clearTimeout(warpgateTimeoutRef.current); + warpgateTimeoutRef.current = null; + } + webSocketRef.current.send( + JSON.stringify({ + type: "warpgate_auth_continue", + data: {}, + }), + ); + setWarpgateAuthRequired(false); + setWarpgateAuthUrl(""); + setWarpgateSecurityKey(""); + } + } + + function handleWarpgateCancel() { + if (warpgateTimeoutRef.current) { + clearTimeout(warpgateTimeoutRef.current); + warpgateTimeoutRef.current = null; + } + setWarpgateAuthRequired(false); + setWarpgateAuthUrl(""); + setWarpgateSecurityKey(""); + webSocketRef.current?.close(); + } + + function handleWarpgateOpenUrl() { + if (warpgateAuthUrl) { + window.open(warpgateAuthUrl, "_blank", "noopener,noreferrer"); + } + } + + function handleAuthDialogSubmit(credentials: { + password?: string; + sshKey?: string; + keyPassword?: string; + }) { + if (webSocketRef.current && terminal) { + webSocketRef.current.send( + JSON.stringify({ + type: "reconnect_with_credentials", + data: { + cols: terminal.cols, + rows: terminal.rows, + password: credentials.password, + sshKey: credentials.sshKey, + keyPassword: credentials.keyPassword, + hostConfig: { + ...hostConfig, + password: credentials.password, + key: credentials.sshKey, + keyPassword: credentials.keyPassword, + }, + }, + }), + ); + setShowAuthDialog(false); + setIsConnecting(true); + } + } + + function handleAuthDialogCancel() { + setShowAuthDialog(false); + webSocketRef.current?.close(); + } + useImperativeHandle( ref, () => ({ disconnect: () => { + isUnmountingRef.current = true; + shouldNotReconnectRef.current = true; + isReconnectingRef.current = false; if (pingIntervalRef.current) { clearInterval(pingIntervalRef.current); pingIntervalRef.current = null; } + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; + } + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + if (totpTimeoutRef.current) { + clearTimeout(totpTimeoutRef.current); + totpTimeoutRef.current = null; + } + if (warpgateTimeoutRef.current) { + clearTimeout(warpgateTimeoutRef.current); + warpgateTimeoutRef.current = null; + } webSocketRef.current?.close(); }, fit: () => { @@ -198,12 +382,204 @@ export const Terminal = forwardRef( [terminal], ); + function attemptReconnection() { + if ( + isUnmountingRef.current || + shouldNotReconnectRef.current || + isReconnectingRef.current || + isConnectingRef.current || + wasDisconnectedBySSH.current || + reconnectTimeoutRef.current !== null + ) { + return; + } + + if (reconnectAttempts.current >= maxReconnectAttempts) { + updateConnectionError(t("terminal.maxReconnectAttemptsReached")); + setIsConnecting(false); + shouldNotReconnectRef.current = true; + addLog({ + type: "error", + stage: "connection", + message: t("terminal.maxReconnectAttemptsReached"), + }); + return; + } + + isReconnectingRef.current = true; + + if (terminal) { + terminal.clear(); + } + + reconnectAttempts.current++; + + addLog({ + type: "info", + stage: "connection", + message: t("terminal.reconnecting", { + attempt: reconnectAttempts.current, + max: maxReconnectAttempts, + }), + }); + + const delay = Math.min( + 2000 * Math.pow(2, reconnectAttempts.current - 1), + 8000, + ); + + reconnectTimeoutRef.current = setTimeout(() => { + reconnectTimeoutRef.current = null; + + if ( + isUnmountingRef.current || + shouldNotReconnectRef.current || + wasDisconnectedBySSH.current + ) { + isReconnectingRef.current = false; + return; + } + + if (reconnectAttempts.current > maxReconnectAttempts) { + isReconnectingRef.current = false; + return; + } + + const jwtToken = getCookie("jwt"); + if (!jwtToken || jwtToken.trim() === "") { + console.warn("Reconnection cancelled - no authentication token"); + isReconnectingRef.current = false; + updateConnectionError(t("terminal.authenticationRequired")); + setIsConnecting(false); + shouldNotReconnectRef.current = true; + addLog({ + type: "error", + stage: "auth", + message: t("terminal.authenticationRequired"), + }); + return; + } + + if (terminal && hostConfig) { + terminal.clear(); + const cols = terminal.cols; + const rows = terminal.rows; + connectToHost(cols, rows); + } + + isReconnectingRef.current = false; + }, delay); + } + + function connectToHost(cols: number, rows: number) { + if (isConnectingRef.current) { + return; + } + + isConnectingRef.current = true; + connectionAttemptIdRef.current++; + + if (!isReconnectingRef.current) { + reconnectAttempts.current = 0; + shouldNotReconnectRef.current = false; + } + + const isDev = + process.env.NODE_ENV === "development" && + (window.location.port === "3000" || + window.location.port === "5173" || + window.location.port === ""); + + const jwtToken = getCookie("jwt"); + + if (!jwtToken || jwtToken.trim() === "") { + console.error("No JWT token available for WebSocket connection"); + setIsConnected(false); + setIsConnecting(false); + updateConnectionError("Authentication required"); + isConnectingRef.current = false; + return; + } + + const baseWsUrl = isDev + ? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002` + : isElectron() + ? (() => { + const baseUrl = + (window as { configuredServerUrl?: string }) + .configuredServerUrl || "http://127.0.0.1:30001"; + const wsProtocol = baseUrl.startsWith("https://") + ? "wss://" + : "ws://"; + const wsHost = baseUrl.replace(/^https?:\/\//, ""); + return `${wsProtocol}${wsHost}/ssh/websocket/`; + })() + : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}${getBasePath()}/ssh/websocket/`; + + if ( + webSocketRef.current && + webSocketRef.current.readyState !== WebSocket.CLOSED + ) { + webSocketRef.current.close(); + } + + if (pingIntervalRef.current) { + clearInterval(pingIntervalRef.current); + pingIntervalRef.current = null; + } + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + + const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`; + + const ws = new WebSocket(wsUrl); + webSocketRef.current = ws; + wasDisconnectedBySSH.current = false; + updateConnectionError(null); + shouldNotReconnectRef.current = false; + isReconnectingRef.current = false; + setIsConnecting(true); + + setupWebSocketListeners(ws, cols, rows); + } + function setupWebSocketListeners( ws: WebSocket, cols: number, rows: number, ) { ws.addEventListener("open", () => { + connectionTimeoutRef.current = setTimeout(() => { + if ( + !isConnected && + !totpRequired && + !isPasswordPrompt && + !connectionErrorRef.current + ) { + if (terminal) { + terminal.clear(); + } + const timeoutMessage = t("terminal.connectionTimeout"); + updateConnectionError(timeoutMessage); + addLog({ + type: "error", + stage: "connection", + message: timeoutMessage, + }); + if (webSocketRef.current) { + webSocketRef.current.close(); + } + if (reconnectAttempts.current > 0) { + attemptReconnection(); + } else { + setIsConnecting(false); + shouldNotReconnectRef.current = true; + } + } + }, 35000); + ws.send( JSON.stringify({ type: "connectToHost", @@ -230,41 +606,317 @@ export const Terminal = forwardRef( } else { terminal.write(String(msg.data)); } - } else if (msg.type === "error") - terminal.writeln(`\r\n[${t("terminal.error")}] ${msg.message}`); - else if (msg.type === "connected") { + } else if (msg.type === "error") { + const errorMessage = msg.message || t("terminal.unknownError"); + + addLog({ + type: "error", + stage: "connection", + message: errorMessage, + }); + + if ( + errorMessage.toLowerCase().includes("connection") || + errorMessage.toLowerCase().includes("timeout") || + errorMessage.toLowerCase().includes("network") + ) { + updateConnectionError(errorMessage); + setIsConnected(false); + if (terminal) { + terminal.clear(); + } + setIsConnecting(false); + wasDisconnectedBySSH.current = false; + return; + } + + if ( + (errorMessage.toLowerCase().includes("auth") && + errorMessage.toLowerCase().includes("failed")) || + errorMessage.toLowerCase().includes("permission denied") || + (errorMessage.toLowerCase().includes("invalid") && + (errorMessage.toLowerCase().includes("password") || + errorMessage.toLowerCase().includes("key"))) || + errorMessage.toLowerCase().includes("incorrect password") + ) { + updateConnectionError(errorMessage); + setIsConnecting(false); + shouldNotReconnectRef.current = true; + if (webSocketRef.current) { + webSocketRef.current.close(); + } + return; + } + + updateConnectionError(errorMessage); + setIsConnecting(false); + } else if (msg.type === "connected") { + setIsConnected(true); + setIsConnecting(false); isConnectingRef.current = false; + updateConnectionError(null); + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + if (reconnectAttempts.current > 0) { + addLog({ + type: "success", + stage: "connection", + message: t("terminal.reconnected"), + }); + } else { + addLog({ + type: "success", + stage: "connection", + message: t("terminal.connected"), + }); + } + reconnectAttempts.current = 0; + isReconnectingRef.current = false; + + setTimeout(async () => { + const terminalConfig = { + ...DEFAULT_TERMINAL_CONFIG, + ...hostConfig.terminalConfig, + }; + + if ( + terminalConfig.environmentVariables && + terminalConfig.environmentVariables.length > 0 + ) { + for (const envVar of terminalConfig.environmentVariables) { + if (envVar.key && envVar.value && ws.readyState === 1) { + ws.send( + JSON.stringify({ + type: "input", + data: `export ${envVar.key}="${envVar.value}"\n`, + }), + ); + } + } + } + + if (terminalConfig.startupSnippetId) { + try { + const snippets = await getSnippets(); + const snippet = snippets.find( + (s: { id: number }) => + s.id === terminalConfig.startupSnippetId, + ); + if (snippet && ws.readyState === 1) { + ws.send( + JSON.stringify({ + type: "input", + data: snippet.content + "\n", + }), + ); + } + } catch (err) { + console.warn("Failed to execute startup snippet:", err); + } + } + + if (terminalConfig.autoMosh && ws.readyState === 1) { + ws.send( + JSON.stringify({ + type: "input", + data: terminalConfig.moshCommand + "\n", + }), + ); + } + }, 100); } else if (msg.type === "disconnected") { wasDisconnectedBySSH.current = true; isConnectingRef.current = false; - terminal.writeln( - `\r\n[${msg.message || t("terminal.disconnected")}]`, - ); + setIsConnected(false); + if (terminal) { + terminal.clear(); + } + setIsConnecting(false); + } else if (msg.type === "totp_required") { + setTotpRequired(true); + setTotpPrompt(msg.prompt || t("terminal.totpCodeLabel")); + setIsPasswordPrompt(false); + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + if (totpTimeoutRef.current) { + clearTimeout(totpTimeoutRef.current); + } + totpTimeoutRef.current = setTimeout(() => { + setTotpRequired(false); + if (webSocketRef.current) { + webSocketRef.current.close(); + } + }, 180000); + } else if (msg.type === "password_required") { + setTotpRequired(true); + setTotpPrompt(msg.prompt || t("common.password")); + setIsPasswordPrompt(true); + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + if (totpTimeoutRef.current) { + clearTimeout(totpTimeoutRef.current); + } + totpTimeoutRef.current = setTimeout(() => { + setTotpRequired(false); + if (webSocketRef.current) { + webSocketRef.current.close(); + } + }, 180000); + } else if (msg.type === "warpgate_auth_required") { + setWarpgateAuthRequired(true); + setWarpgateAuthUrl(msg.url || ""); + setWarpgateSecurityKey(msg.securityKey || "N/A"); + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + if (warpgateTimeoutRef.current) { + clearTimeout(warpgateTimeoutRef.current); + } + warpgateTimeoutRef.current = setTimeout(() => { + setWarpgateAuthRequired(false); + if (webSocketRef.current) { + webSocketRef.current.close(); + } + }, 300000); + } else if (msg.type === "keyboard_interactive_available") { + setIsConnecting(false); + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + } else if (msg.type === "auth_method_not_available") { + setAuthDialogReason("no_keyboard"); + setShowAuthDialog(true); + setIsConnecting(false); + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } + } else if (msg.type === "connection_log") { + if (msg.data) { + addLog({ + type: msg.data.level || "info", + stage: msg.data.stage || "auth", + message: msg.data.message, + details: msg.data.details, + }); + } } } catch (error) { console.error("Terminal operation failed:", error); } }); + const currentAttemptId = connectionAttemptIdRef.current; + ws.addEventListener("close", (event) => { - isConnectingRef.current = false; - - if (event.code === 1008) { - console.error("WebSocket authentication failed:", event.reason); - terminal.writeln(`\r\n[Authentication failed - please re-login]`); - - localStorage.removeItem("jwt"); + if (currentAttemptId !== connectionAttemptIdRef.current) { return; } - if (!wasDisconnectedBySSH.current) { - terminal.writeln(`\r\n[${t("terminal.connectionClosed")}]`); + setIsConnected(false); + isConnectingRef.current = false; + if (terminal) { + terminal.clear(); + } + + if (totpTimeoutRef.current) { + clearTimeout(totpTimeoutRef.current); + totpTimeoutRef.current = null; + } + + if (event.code === 1006) { + console.error( + "[WebSocket] Abnormal closure detected - possible HTTPS/proxy issue", + ); + addLog({ + type: "error", + stage: "connection", + message: t("terminal.websocketAbnormalClose"), + }); + updateConnectionError(t("terminal.websocketAbnormalClose")); + setIsConnecting(false); + shouldNotReconnectRef.current = true; + return; + } + + if (event.code === 1008) { + console.error("WebSocket authentication failed:", event.reason); + addLog({ + type: "error", + stage: "auth", + message: "Authentication failed - please re-login", + }); + updateConnectionError("Authentication failed - please re-login"); + setIsConnecting(false); + shouldNotReconnectRef.current = true; + + localStorage.removeItem("jwt"); + + setTimeout(() => { + window.location.reload(); + }, 1000); + + return; + } + + if ( + !isConnected && + event.wasClean && + (event.code === 1005 || event.code === 1000) + ) { + console.error("[WebSocket] Connection rejected by server"); + addLog({ + type: "error", + stage: "connection", + message: t("terminal.connectionRejected"), + }); + updateConnectionError(t("terminal.connectionRejected")); + setIsConnecting(false); + shouldNotReconnectRef.current = true; + return; + } + + const shouldAttemptReconnection = + !wasDisconnectedBySSH.current && + !isUnmountingRef.current && + !shouldNotReconnectRef.current && + !isConnectingRef.current; + + if (shouldAttemptReconnection) { + wasDisconnectedBySSH.current = false; + attemptReconnection(); + } else { + setIsConnecting(false); } }); - ws.addEventListener("error", () => { + ws.addEventListener("error", (event) => { + if (currentAttemptId !== connectionAttemptIdRef.current) { + return; + } + + console.error("[WebSocket] Error:", event); + + setIsConnected(false); isConnectingRef.current = false; - terminal.writeln(`\r\n[${t("terminal.connectionError")}]`); + updateConnectionError(t("terminal.websocketError")); + if (terminal) { + terminal.clear(); + } + setIsConnecting(false); + + if (totpTimeoutRef.current) { + clearTimeout(totpTimeoutRef.current); + totpTimeoutRef.current = null; + } }); } @@ -275,28 +927,54 @@ export const Terminal = forwardRef( return; } + const fontConfig = TERMINAL_FONTS.find( + (f) => f.value === config.fontFamily, + ); + const fontFamily = fontConfig?.fallback || TERMINAL_FONTS[0].fallback; + terminal.options = { - cursorBlink: false, - cursorStyle: "bar", - scrollback: 10000, - fontSize: 14, - fontFamily: - '"Caskaydia Cove Nerd Font Mono", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace', - theme: themeColors, + cursorBlink: config.cursorBlink, + cursorStyle: config.cursorStyle, + scrollback: config.scrollback, + fontSize: config.fontSize, + fontFamily, allowTransparency: true, convertEol: false, windowsMode: false, macOptionIsMeta: false, macOptionClickForcesSelection: false, - rightClickSelectsWord: false, - fastScrollModifier: "alt", - fastScrollSensitivity: 5, + rightClickSelectsWord: config.rightClickSelectsWord, + fastScrollModifier: config.fastScrollModifier, + fastScrollSensitivity: config.fastScrollSensitivity, allowProposedApi: true, - disableStdin: true, - cursorInactiveStyle: "bar", - minimumContrastRatio: 1, - letterSpacing: 0, - lineHeight: 1.2, + minimumContrastRatio: config.minimumContrastRatio, + letterSpacing: config.letterSpacing, + lineHeight: config.lineHeight, + bellStyle: config.bellStyle as "none" | "sound" | "visual" | "both", + theme: { + background: themeColors.background, + foreground: themeColors.foreground, + cursor: themeColors.cursor, + cursorAccent: themeColors.cursorAccent, + selectionBackground: themeColors.selectionBackground, + selectionForeground: themeColors.selectionForeground, + black: themeColors.black, + red: themeColors.red, + green: themeColors.green, + yellow: themeColors.yellow, + blue: themeColors.blue, + magenta: themeColors.magenta, + cyan: themeColors.cyan, + white: themeColors.white, + brightBlack: themeColors.brightBlack, + brightRed: themeColors.brightRed, + brightGreen: themeColors.brightGreen, + brightYellow: themeColors.brightYellow, + brightBlue: themeColors.brightBlue, + brightMagenta: themeColors.brightMagenta, + brightCyan: themeColors.brightCyan, + brightWhite: themeColors.brightWhite, + }, }; const fitAddon = new FitAddon(); @@ -314,16 +992,6 @@ export const Terminal = forwardRef( terminal.open(xtermRef.current); - const textarea = xtermRef.current.querySelector( - ".xterm-helper-textarea", - ) as HTMLTextAreaElement | null; - if (textarea) { - textarea.readOnly = true; - textarea.blur(); - } - - terminal.focus = () => {}; - const resizeObserver = new ResizeObserver(() => { if (resizeTimeout.current) clearTimeout(resizeTimeout.current); resizeTimeout.current = setTimeout(() => { @@ -352,7 +1020,7 @@ export const Terminal = forwardRef( if (!jwtToken || jwtToken.trim() === "") { setIsConnected(false); setIsConnecting(false); - setConnectionError("Authentication required"); + updateConnectionError("Authentication required"); setVisible(true); setIsReady(true); return; @@ -361,58 +1029,16 @@ export const Terminal = forwardRef( const cols = terminal.cols; const rows = terminal.rows; - const isDev = - process.env.NODE_ENV === "development" && - (window.location.port === "3000" || - window.location.port === "5173" || - window.location.port === ""); - - const baseWsUrl = isDev - ? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002` - : isElectron() - ? (() => { - const baseUrl = - (window as { configuredServerUrl?: string }) - .configuredServerUrl || "http://127.0.0.1:30001"; - const wsProtocol = baseUrl.startsWith("https://") - ? "wss://" - : "ws://"; - const wsHost = baseUrl.replace(/^https?:\/\//, ""); - return `${wsProtocol}${wsHost}/ssh/websocket/`; - })() - : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`; - if (isConnectingRef.current) { setVisible(true); setIsReady(true); return; } - isConnectingRef.current = true; - - if ( - webSocketRef.current && - webSocketRef.current.readyState !== WebSocket.CLOSED - ) { - webSocketRef.current.close(); + if (terminal.cols > 0 && terminal.rows > 0) { + connectToHost(cols, rows); } - if (pingIntervalRef.current) { - clearInterval(pingIntervalRef.current); - pingIntervalRef.current = null; - } - - const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`; - - setIsConnecting(true); - setConnectionError(null); - - const ws = new WebSocket(wsUrl); - webSocketRef.current = ws; - wasDisconnectedBySSH.current = false; - - setupWebSocketListeners(ws, cols, rows); - setVisible(true); setIsReady(true); }); @@ -433,6 +1059,27 @@ export const Terminal = forwardRef( }; }, [xtermRef, terminal, hostConfig, isAuthenticated, isDarkMode]); + useEffect(() => { + return () => { + isUnmountingRef.current = true; + shouldNotReconnectRef.current = true; + isReconnectingRef.current = false; + setIsConnecting(false); + if (reconnectTimeoutRef.current) + clearTimeout(reconnectTimeoutRef.current); + if (connectionTimeoutRef.current) + clearTimeout(connectionTimeoutRef.current); + if (totpTimeoutRef.current) clearTimeout(totpTimeoutRef.current); + if (warpgateTimeoutRef.current) + clearTimeout(warpgateTimeoutRef.current); + if (pingIntervalRef.current) { + clearInterval(pingIntervalRef.current); + pingIntervalRef.current = null; + } + webSocketRef.current?.close(); + }; + }, []); + useEffect(() => { if (!isVisible || !isReady || !fitAddonRef.current || !terminal) { return; @@ -445,12 +1092,74 @@ export const Terminal = forwardRef( return () => clearTimeout(fitTimeout); }, [isVisible, isReady, terminal]); + const hasConnectionError = !!connectionError; + return ( -
+
+
+ + + + + + + + + + +
+ ); + }, +); + +export const Terminal = forwardRef( + function Terminal(props, ref) { + return ( + + + ); }, ); diff --git a/vite.config.ts b/vite.config.ts index 63780858..7c3a4f3a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, - base: "./", + base: process.env.VITE_BASE_PATH || "./", build: { sourcemap: false, rollupOptions: {