Compare commits

...

5 Commits

Author SHA1 Message Date
Luke Gustafson 55789db3c5 New Crowdin updates (#618)
* New translations en.json (Russian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* 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 (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* 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 (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Indonesian)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Hindi)
2026-03-08 18:21:21 -05:00
LukeGus dcf5468acc chore: remove translations to be overwritten 2026-03-08 18:09:09 -05:00
Luke Gustafson 0ff03110c9 v1.11.2 (#613)
* feat: add robust OSC 52 clipboard support for tmux/SSH clipboard sync (#587)

Upgrade @xterm/addon-clipboard to v0.2.0 and add a custom
RobustClipboardProvider that handles browser Clipboard API focus
restrictions by deferring writes until the window regains focus.
Uses Electron native clipboard when available for reliable access
without browser API limitations.

Changes:
- Create src/lib/clipboard-provider.ts with write-only provider
- Update all terminal components (desktop, mobile, docker console)
- Add electronClipboard bridge in Electron preload
- Add clipboard permission handler in Electron main process

* chore: remove translations

* feat: add 5-panel and 6-panel split screen layouts (#584)

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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+<key> as an alternative that sends Ctrl+<key>.

- 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 <bugattiguy527@gmail.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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 <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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+<key> as an alternative that sends Ctrl+<key>.

- 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 <bugattiguy527@gmail.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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 <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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…

* feat: show terminal title in tab name (#579)

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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+<key> as an alternative that sends Ctrl+<key>.

- 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 <bugattiguy527@gmail.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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 <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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+<key> as an alternative that sends Ctrl+<key>.

- 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 <bugattiguy527@gmail.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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 <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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 dir…

* fix: allow OIDC JIT user creation when registration is disabled (#578)

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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+<key> as an alternative that sends Ctrl+<key>.

- 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 <bugattiguy527@gmail.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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 <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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+<key> as an alternative that sends Ctrl+<key>.

- 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 <bugattiguy527@gmail.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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 <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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 r…

* feat: add file manager sorting by name, date, and size (#582)

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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+<key> as an alternative that sends Ctrl+<key>.

- 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 <bugattiguy527@gmail.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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 <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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+<key> as an alternative that sends Ctrl+<key>.

- 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 <bugattiguy527@gmail.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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 <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* 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 <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* 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 …

* fix: unable to delete OIDC user due to missing table cleanup (#576)

deleteUserAndRelatedData was missing cleanup for networkTopology,
dashboardPreferences, and opksshTokens tables. When deleting an OIDC
user who had records in these tables, the final DELETE on users hit
SQLITE_CONSTRAINT_FOREIGNKEY. Add explicit deletes for all three
tables before the user row deletion.

* fix: use lightweight wget for Docker healthcheck (#558)

- Replace node -e healthcheck with wget (avoid spawning full Node.js process)
- Increase start-period from 30s to 60s for slower machines

* feat: add "Remember Me" option to login (#585)

When checked, browser sessions get 30-day expiry (same as desktop)
instead of the default 7-day. The flag is passed through the full
login chain including TOTP verification flow.

* feat: expand environment variables in file manager path bar (#583)

Resolve $VAR and ~ paths via SSH echo before navigating.
Add /ssh/resolvePath backend endpoint, resolveSSHPath API function,
and intercept in loadDirectory. Fix confirmEditingPath not to
prefix ~ or $ paths with /.

* fix: add Ctrl+Shift+C keyboard shortcut for terminal copy (#581)

Ctrl+Shift+C was not handled in the custom key event handler,
so users had no keyboard shortcut to copy selected text from
the terminal. Adds the shortcut alongside existing Cmd+C (macOS)
support, copying the current selection to clipboard.

* feat: add OIDC user allowlist for registration control (#580)

Add allowed_users field to OIDC config supporting comma-separated
patterns: exact emails (user@example.com), domain suffixes
(@example.com), or empty for unrestricted access. Only new user
registrations are checked; existing users and first-user setup
are unaffected.

* fix: OIDC redirect_uri incorrectly resolves to localhost (#577)

The OIDC authorize endpoint used the client Origin header to build
the redirect_uri. Desktop/mobile apps connecting to a remote server
send their local origin (e.g. http://localhost:5173), which got
force-rewritten to http://localhost:30001. This broke OIDC for all
non-browser clients.

Use req.protocol + req.get("Host") instead, which correctly resolves
to the server's own address. trust proxy is already enabled so this
works behind reverse proxies too.

* fix: keep terminal tab open on connection failure (#574)

When SSH connection fails before being established, the terminal tab
auto-closes immediately, leaving no error message visible to the user.
Added wasConnectedRef to track whether the connection was ever
established. Now only auto-closes tab on disconnect if it was previously
connected. If never connected, shows error message instead.

* fix: prevent restart loop when PUID/PGID is set to 0 (#571)

When PUID=0, usermod sets the node user's UID to 0 (root), then
gosu re-executes the script as node — but since node is now UID 0,
the id check passes again, causing an infinite loop.

Skip the gosu step when PUID=0 since the process is already root.

* feat: add README translations for 12 additional languages (#569)

Add translated README files for: Japanese, Korean, French, German,
Spanish, Portuguese (BR), Russian, Arabic, Hindi, Turkish,
Vietnamese, and Italian. Update language navigation bar in all
README files including existing English and Chinese versions.

* fix: allow OIDC users to export database without password (#575)

OIDC users have no password, but the export endpoint unconditionally
required password authentication. This reuses the same OIDC branch
pattern already implemented in the import endpoint: skip password
validation for OIDC users and authenticate via authenticateOIDCUser
instead. Frontend now also skips the password input for OIDC users.

* fix: SSH session leak — SFTP channel reuse + shared connection pool (#556)

- Cache SFTP channel per session in file-manager, reuse across all operations
- Remove all sftp.end() calls, channel lifecycle follows session
- Auto-rebuild SFTP channel on error/close events
- Extract SSHConnectionPool from server-stats into shared module
- Factory-based pool API: getConnection(key, factory) / withConnection()
- Migrate server-stats and tunnel killRemoteTunnelByMarker to shared pool
- Refactor tunnel kill from callback hell to async/await

Fixes: Termix-SSH/Support#485

* fix: handle DEC private mode sequences in syntax highlighter (#562)

Extend CSI parameter byte matching to include ?, >, =, ! per ECMA-48.
Previously sequences like \x1b[?1h and \x1b[?25l were not recognized
as ANSI segments and fell through to plain text processing.

* fix: catch disconnected client error in listFiles fallback (#564)

When SFTP times out and the fallback calls client.exec() on an
already-disconnected SSH client, the synchronous throw was not
caught, crashing the backend process.

* fix: forward Shift+Tab as backtab escape sequence (#563)

Explicitly handle Shift+Tab in the custom key event handler by sending
\x1b[Z (CSI Z) directly, with preventDefault to stop browser/WebView
focus navigation from consuming the keystroke.

* fix: disable font ligatures in terminal by default (#561)

Nerd Fonts ligatures cause != to render as ≠, <= as ≤, etc.
This is confusing for terminal use and breaks cursor positioning.

* fix: bypass ls alias in file manager fallback (#560)

- Add --color=never to command ls in SFTP fallback path
- Use /bin/ls absolute path in sudo fallback to avoid alias/wrapper

* feat: add overwrite option for JSON host import (#559)

- Add overwrite mode to bulk-import endpoint, matching by ip:port:username
- Import button now shows dropdown with "skip existing" and "overwrite existing" options
- Response includes created/updated/skipped/failed counts

* fix: status check tcpPing causing kex_exchange_identification flood in sshd logs (#557)

- Complete SSH identification exchange before closing socket in tcpPing
- Read server banner, send client identification string, then close gracefully
- Prevents sshd from logging kex_exchange_identification errors on every ping
- Increase default statusCheckInterval from 30s to 60s

* fix: add WebSocket protocol-level ping to prevent proxy timeouts (#572)

The existing application-level ping (JSON message via setInterval)
stops working when the browser tab is backgrounded due to timer
throttling. Reverse proxies like Cloudflare (100s) and Nginx Proxy
Manager (60s) then drop the idle WebSocket connection.

Add server-side ws.ping() every 30s for both terminal and docker
console WebSocket servers. Protocol-level pings are handled by
the browser's WebSocket implementation, unaffected by tab throttling.

* feat: embed backend server in Electron desktop app (#539)

* feat: embed backend server in Electron desktop app

- Fork backend process on app startup (production mode only)
- Store data in userData/server-data for desktop users
- Unpack dist/ and native modules (better-sqlite3, ssh2) from asar
- Resolve asar vs asar.unpacked paths for forked process
- Serve frontend static files from Express for iframe auth flow
- Add SPA fallback for non-API routes
- Graceful shutdown with SIGTERM + 5s force kill
- Add get-embedded-server-status IPC handler

* fix: improve electron embedded server reliability

- Extract duplicate fetch polyfill to shared httpFetch function
- Wait for backend ready before creating window (15s timeout)
- Use IPC message for graceful shutdown (SIGTERM doesn't work on Windows)
- Simplify SPA fallback to use Accept header instead of route blacklist
- Add IPC shutdown handler in backend starter

* feat: system tray minimize and runtime server switching

* fix: resolve all 274 ESLint errors in backend source files (#591)

- Remove unused imports and variables (110 no-unused-vars)
- Replace explicit any with proper types (80 no-explicit-any)
- Add comments to intentionally empty catch blocks (66 no-empty)
- Fix no-extra-boolean-cast, prefer-const, no-async-promise-executor
- No logic changes, lint-only cleanup

* fix: add missing clipboard shortcuts for terminal and docker console (#589)

Terminal and Docker Console were missing Ctrl+Shift+C (copy selection),
Ctrl+Insert (copy), and Shift+Insert (paste) shortcuts. Docker Console
also lacked Ctrl+V paste entirely. Added consistent clipboard key
handlers to both components.

* fix: resolve all ESLint errors and increase CI heap size (#588)

* fix: resolve all ESLint errors blocking CI

- Downgrade mass legacy violations to warnings (no-unused-vars,
  no-explicit-any, no-empty, no-unused-expressions)
- Turn off no-control-regex (terminal app uses control chars)
- Ignore dist/release/Mobile dirs from linting
- Auto-fix prefer-const, no-extra-boolean-cast
- Fix real bugs: async promise executor, rules-of-hooks,
  dupe-else-if, constant-binary-expression, case-declarations
- Run prettier --write on all files

* fix: increase Node.js heap size for CI build step

vite build OOMs on 2vCPU runner with default heap limit.
Set NODE_OPTIONS=--max-old-space-size=4096 to prevent it.

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* fix: reduce excessive disk writes from unconditional database persistence (#590)

The in-memory database was serialized, encrypted, and written to disk
every 15 seconds regardless of whether any data had changed. Combined
with the 2-second debounced save trigger on every modification, this
caused ~1GB/hour of disk writes even when idle.

Add a dirty flag so the periodic save only writes when data has actually
changed, and increase the safety-net interval from 15 seconds to 5
minutes. Triggered saves (on actual data modifications) continue to
fire after the existing 2-second debounce.

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>

* fix: opkssh 404 and browser issues

* fix: resolve backend TypeScript strict compilation errors (#592)

Fix 56 type errors under tsconfig.node.json caused by overly broad
unknown/Record<string, unknown> types from the ESLint cleanup.

- Add JumpHostConfig interface for jump host chain resolution
- Use proper types for resolvedCredentials, ConnectConfig, ProxyNode
- Fix setWindow call to pass all 4 required arguments
- Remove duplicate responseTimeout declarations in same scope
- Add missing ClientChannel import

* fix: clear Electron partition cookies on auth failure and logout (#599)

When the Docker container is recreated, a new JWT secret is generated and
all existing tokens become invalid. The 401 interceptor clears localStorage
and document.cookie, but the Electron persistent partition cookies
(partition: "persist:termix") are managed by Chromium and not accessible
via document.cookie. This leaves the stale token in place, causing an
infinite 401 retry loop and UI flickering.

Add a clear-session-cookies IPC handler in the main process that uses
session.cookies API to remove all partition cookies. Invoke it from the
401 interceptor and logoutUser function in the renderer.

* refactor: unify Drizzle schema property names to camelCase (#598)

* feat: add bulk host settings editing with multi-select UI

Add PATCH /ssh/bulk-update endpoint for partial updates on multiple hosts.
Frontend adds selection mode with checkboxes, folder-level select-all,
and a floating action bar for monitoring, features, folder, and pin operations.

* fix: correct Drizzle schema field name access for SSH credentials

The sshCredentials schema uses mixed naming: private_key and
key_password (snake_case) but authType and keyType (camelCase).
Multiple files accessed these fields with wrong names, causing
undefined values at runtime.

Fixed direct bugs:
- docker.ts: host.keyPassword -> host.key_password
- docker-console.ts: jumpHost.keyPassword -> jumpHost.key_password
- auth-manager.ts: cred.privateKey -> cred.private_key
- auth-manager.ts: cred.passphrase -> cred.key_password

Cleaned up redundant fallback patterns (e.g. credential.auth_type ||
credential.authType) across 8 files to use the correct property name
directly.

* refactor: unify Drizzle schema property names to camelCase

Rename 18 snake_case Drizzle schema properties to camelCase across
users, sshData, and sshCredentials tables. SQL column names unchanged,
no database migration needed.

This fixes the root cause of field name mismatch bugs where code
accessed Drizzle results using camelCase but schema used snake_case.

Updated all references across 21 files: routes, SSH modules, crypto
utilities, type definitions, and field encryption mappings.

* feat: add bulk host settings editing with multi-select UI (#596)

Add PATCH /ssh/bulk-update endpoint for partial updates on multiple hosts.
Frontend adds selection mode with checkboxes, folder-level select-all,
and a floating action bar for monitoring, features, folder, and pin operations.

* feat: add global default for status check and metrics intervals (#595)

Add global monitoring interval settings that apply to all hosts by default,
with per-host override capability. Hosts use the global default unless
explicitly configured with a custom interval.

* fix: shared credentials fail for OIDC users due to missing re-encryption (#593)

When an admin shares a host with an OIDC user who hasn't logged in yet,
a pending shared credential is created with needsReEncryption=true.
The OIDC callback login path was missing the call to
reEncryptPendingCredentialsForUser(), so the credential stayed pending
forever. Additionally, server-stats.ts accessed the shared credential
without a null check, causing a crash.

- Add reEncryptPendingCredentialsForUser() call to OIDC callback login
- Add null guard for getSharedCredentialForUser() in server-stats.ts

* feat: SSH session persistence across browser refresh (#594)

* feat: decouple SSH session lifecycle from WebSocket connections

SSH sessions now persist independently of browser tab state. When a
WebSocket disconnects (tab close/refresh), the SSH connection is
detached rather than destroyed, allowing reattachment within a
configurable idle timeout (default 30 min).

- Add TerminalSessionManager singleton with output buffering, idle
  timeout, health checks, and per-user session limits
- Refactor terminal.ts to route SSH state through session manager
- Frontend tracks session IDs in localStorage for reconnection
- Tabs persist to localStorage and restore on page reload
- Add GET/POST /terminal/session_settings API endpoints
- Clear session storage on logout

* fix: resolve session persistence bugs found during code review

- Fix detachWs timeout leak on double-detach (clear existing timeout first)
- Fix healthCheck mutating Map during iteration (collect IDs first)
- Capture sessionId at stream bind time to prevent cross-session data leakage
- Add session destruction in OPKSSH/auth error paths that previously leaked
- Add session destruction in shell init early-return paths
- Reset isConnecting flag in attachSession handler
- Use current terminal dimensions in sessionExpired handler (not stale closure)
- Fix nextTabId race condition with synchronous initialization from restored tabs
- Validate restored currentTab against actual restored tabs
- Add clearTermixSessionStorage to logout error path

* fix: correct raw SQL field name mismatches in crypto and credential routes (#601)

Fix camelCase/snake_case mismatches left after #598 schema refactor:

- credentials.ts: 5 remaining snake_case fields in Drizzle insert/update
- data-crypto.ts: migrateUserSensitiveFields() accessed raw SQL records
  with camelCase keys (keyPassword, privateKey, etc.) but SELECT * returns
  snake_case columns, causing undefined values and silent data loss
- lazy-field-encryption.ts: getSensitiveFieldsForTable() missing 4 ssh_data
  fields (sudoPassword, autostartPassword, autostartKey, autostartKeyPassword),
  migrateRecordSensitiveFields() now uses propertyToColumn() to resolve
  snake_case keys from raw SQL results

* feat: add per-host SSH keepalive configuration (#603)

* fix: correct raw SQL field name mismatches in crypto and credential routes

Fix camelCase/snake_case mismatches left after #598 schema refactor:

- credentials.ts: 5 remaining snake_case fields in Drizzle insert/update
- data-crypto.ts: migrateUserSensitiveFields() accessed raw SQL records
  with camelCase keys (keyPassword, privateKey, etc.) but SELECT * returns
  snake_case columns, causing undefined values and silent data loss
- lazy-field-encryption.ts: getSensitiveFieldsForTable() missing 4 ssh_data
  fields (sudoPassword, autostartPassword, autostartKey, autostartKeyPassword),
  migrateRecordSensitiveFields() now uses propertyToColumn() to resolve
  snake_case keys from raw SQL results

* feat: add per-host SSH keepalive configuration

MikroTik and similar devices ignore SSH-level keepalive requests,
causing ssh2 to disconnect after keepaliveCountMax unanswered pings
(default: 30s × 4 = 120s).

Add keepaliveInterval and keepaliveCountMax to terminalConfig,
allowing per-host override. Users can set keepaliveInterval to 0
to disable SSH keepalives and rely on TCP keepalive instead.

Backend reads the values from hostConfig.terminalConfig sent via
WebSocket. Frontend exposes the settings in the Advanced section
of the terminal configuration tab.

* fix: enable Electron desktop app to run standalone with embedded backend (#609)

* fix: enable Electron desktop app to run standalone with embedded backend

The Electron app already starts an embedded backend server via fork(),
but the frontend had no awareness of it. On first launch, users were
always prompted to enter a remote server URL even though a local backend
was already running on localhost.

- Add getEmbeddedServerStatus() to query the embedded backend via IPC
- Add embeddedMode flag in main-axios.ts, detected at initialization
- In embedded mode without a configured remote URL, getApiUrl() now
  routes each service to its own localhost port (30001, 30003, etc.)
  matching the dev-mode multi-port strategy
- Auth.tsx checkServerConfig() detects embedded backend and skips the
  server configuration form, falling through to the standard login UI

* fix: add explicit "Use Local Server" button and improve embedded detection

The silent auto-detection of the embedded backend can fail due to IPC
timing issues or backend startup failures. Add a visible "Use Local
Server" button on the Server Configuration page that:

- Probes http://localhost:30001/health to verify the backend is running
- Sets embeddedMode and reinitializes API instances with per-port routing
- Falls through to the standard login form (no iframe needed)

Also adds setEmbeddedMode() export so the UI can explicitly activate
embedded mode, and adds i18n keys for the new UI elements.

* fix: disable asar for embedded backend, improve tray and health probe

The forked backend process uses ESM imports and cannot resolve modules
from inside an asar archive (NODE_PATH is CJS-only). Disabling asar
ensures node_modules are plain files accessible to the backend.

- Disable asar packaging so forked backend can resolve ESM imports
- Remove node_modules exclusion from files config
- Fix system tray: use nativeImage with template on macOS, add error
  handling, only minimize-to-tray when tray exists
- Add retry mechanism for backend health probe (10 retries over ~30s)
  to handle slow backend startup
- Always show "Use Local Server" button in Electron mode regardless
  of backend running status
- Add file logging to userData for debugging GUI-launched app

* feat: unified proxy + jump host pipeline with HTTP CONNECT support (#608)

* fix: unify proxy and jump host connection paths

SOCKS5 proxy and jump host logic were mutually exclusive — the SOCKS5
branch returned early, so jump hosts were never reached when both were
configured. This affected terminal, file-manager, docker, and
server-stats modules.

Refactored all four modules to a unified pipeline:
- createJumpHostChain() accepts optional socks5Config parameter
- When both proxy and jump hosts are configured, the proxy socket is
  created to the first jump host internally, then used as transport
- Three-branch flow: jumpHosts (with optional proxy) → proxy-only → direct
- Enhanced error logging with hopIndex, totalHops, previousHop fields

* feat: add HTTP CONNECT proxy support with mixed chain

Extend ProxyNode.type to support 'http' alongside SOCKS4/5.

New proxy-helper.ts provides:
- createHttpConnectConnection(): HTTP CONNECT tunnel with Basic auth
  and optional existingSocket for chaining
- createMixedProxyChainConnection(): routes each hop to SOCKS or HTTP
  CONNECT based on node type; pure-SOCKS chains still use the optimized
  SocksClient.createConnectionChain path
- createProxyConnection(): unified entry point (backward-compatible alias
  for createSocks5Connection)
- testProxyConnectivity(): connect through proxy to test target, measure
  latency

socks5-helper.ts becomes a re-export shim — all existing import sites
continue to work unchanged.

* feat: add proxy connectivity test endpoint

New POST /ssh/db/proxy/test API accepts singleProxy, proxyChain, and
optional testTarget. Calls testProxyConnectivity() and returns
success/latencyMs or error.

Frontend testProxyConnection() function added to main-axios.ts.

* feat: add connection path visualization and HTTP CONNECT UI

- Proxy chain type selector now includes HTTP CONNECT option
- Test Connection button calls /ssh/db/proxy/test with loading state
- Connection path visualization shows the full route when proxy and/or
  jump hosts are configured: [You] → [Proxy] → [Jump Host] → [Target]
- New i18n keys: httpConnect, testProxy, testingProxy, proxyTestSuccess,
  proxyTestFailed, connectionPath

* fix: add Cache-Control no-store header to all API responses (#607)

Backend API responses had no Cache-Control headers, making them
vulnerable to caching by intermediate reverse proxies. This could
cause stale data in the UI when Termix is deployed behind certain
proxy configurations (e.g., SWAG, Pangolin).

Add Cache-Control: no-store middleware to all 6 Express apps to
prevent any proxy or browser from caching API responses.

* fix: preserve external reverse proxy X-Forwarded headers in internal Nginx (#606)

The internal Nginx proxy was overwriting X-Forwarded-Proto, X-Forwarded-Host,
and X-Forwarded-Port with local values ($scheme, $http_host, $server_port),
discarding headers set by the external reverse proxy. This caused
getRequestOrigin() to always return http:// with the internal port for
OpkSSH authentication URLs.

Add map directives to preserve original X-Forwarded-* headers from the
external proxy, falling back to local values when no external proxy is
present. Also remove duplicate header directives in the WebSocket location.

* fix: host update fails silently due to statsConfig double-serialization (#605)

- Fix statsConfig double JSON.stringify in both frontend (createSSHHost,
  updateSSHHost) and backend (POST/PUT handlers). Frontend was pre-
  stringifying statsConfig before sending, then backend stringified again,
  corrupting the data after multiple edits and eventually causing Zod
  validation failures on the edit form.
- Add missing sudoPassword field to createSSHHost and updateSSHHost submit
  data, which was being set in onSubmit but dropped during API call
  construction.
- Add toast notification in handleFormError so users get visible feedback
  when form validation fails instead of silent no-op.

* fix: Ctrl+C copies selection and clipboard error feedback (#604)

* fix: correct raw SQL field name mismatches in crypto and credential routes

Fix camelCase/snake_case mismatches left after #598 schema refactor:

- credentials.ts: 5 remaining snake_case fields in Drizzle insert/update
- data-crypto.ts: migrateUserSensitiveFields() accessed raw SQL records
  with camelCase keys (keyPassword, privateKey, etc.) but SELECT * returns
  snake_case columns, causing undefined values and silent data loss
- lazy-field-encryption.ts: getSensitiveFieldsForTable() missing 4 ssh_data
  fields (sudoPassword, autostartPassword, autostartKey, autostartKeyPassword),
  migrateRecordSensitiveFields() now uses propertyToColumn() to resolve
  snake_case keys from raw SQL results

* fix: Ctrl+C copies selection instead of sending SIGINT when text is selected

When terminal has an active text selection, Ctrl+C now copies the
selected text to clipboard and clears the selection. When nothing
is selected, Ctrl+C sends SIGINT as before. Ctrl+Shift+C continues
to work as a dedicated copy shortcut.

Also adds toast notifications when clipboard operations fail instead
of silently swallowing errors. Applied to both the main terminal
and Docker console terminal.

* fix: improve IPv6 connection handling with bracket stripping and ENETUNREACH hint (#602)

Strip square brackets from IPv6 addresses at all SSH connection entry
points (terminal, docker, docker-console, file-manager, server-stats,
tunnel) to handle addresses entered as [::1] format.

Add ENETUNREACH error hint in terminal.ts that detects IPv6 addresses
and suggests checking Docker IPv6 network configuration.

Also fix remaining snake_case field names in credentials.ts (same as #601).

* fix: backend catch error

* feat: improve selection UI

* fix: remove tab naming

* fix: squished ssh toolbar split ui buttons

* feat: improve persistent tabs, data not saving for users, various ui inconsistencies

* fix: tab context creating random context errors

* fix: improve terminal session logic (not perfect)s

* fix: terminal session logic creating errors and electron app not having UI to do local mode

* fix: desktop build error

* feat: update readme

* feat: update readme

* feat: update readme

* Update README.md (#612)

* fix: macos build failure and updated rest of the langs for readme

* feat: update readmes to fix spelling error

* feat: update readmes to fix spelling error

* fix: oidc failures, opkssh not wokring, added some qol to selecting

* fix: buyild erorr

* fix: buyild erorr

* fix: macos build error

* fix: opkssh, macos copy/paste, admin settings global default backend crashes, oidc redirect issues, updated translations

* chore: clean up files

* fix: oidc http/https redirect issue

* fix: build error

* feat: update json import with all new fields

---------

Co-authored-by: skyam25 <simonkyam@gmail.com>
Co-authored-by: ZacharyZcR <PayasoNorahC@protonmail.com>
Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>
Co-authored-by: Deepansh Khurana <deepanshkhurana@outlook.com>
Co-authored-by: Dylan Ysmal <Xenthys@users.noreply.github.com>
Co-authored-by: TomyJan <TomyJan6@gmail.com>
Co-authored-by: ywaf <52742690+ywaf@users.noreply.github.com>
2026-03-08 18:02:14 -05:00
Luke Gustafson 44049b8031 Add Cloudflare logo to README
Added Cloudflare logo to the README file.
2026-02-16 23:01:07 -06:00
LukeGus ffec17201d chore: update cask 2026-02-12 23:18:55 -06:00
154 changed files with 13916 additions and 4097 deletions
-1
View File
@@ -31,7 +31,6 @@ Thumbs.db
.gitignore
README.md
README-CN.md
CONTRIBUTING.md
LICENSE
-2
View File
@@ -319,7 +319,6 @@ jobs:
cd flatpak-build
flatpak-builder --repo=repo --force-clean --disable-rofiles-fuse build-dir com.karmaa.termix.yml
# Determine the architecture
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
FLATPAK_ARCH="x86_64"
@@ -329,7 +328,6 @@ jobs:
FLATPAK_ARCH="$ARCH"
fi
# Build bundle for the current architecture
flatpak build-bundle repo ../release/termix_linux_flatpak.flatpak com.karmaa.termix --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
- name: Create flatpakref file
+3
View File
@@ -8,6 +8,9 @@ jobs:
lint-and-build:
runs-on: blacksmith-2vcpu-ubuntu-2404
env:
NODE_OPTIONS: "--max-old-space-size=4096"
steps:
- name: Checkout code
uses: actions/checkout@v4
+2 -2
View File
@@ -1,6 +1,6 @@
cask "termix" do
version "1.11.0"
sha256 "c62ea2c3097f466229edcbd65a5ea49d898028a716a3023704c5f644d973c70d"
version "1.11.1"
sha256 "0551e2bd7a3a030ffb967627d04320c1456259dd26b7a992fc02ea0fd9940315"
url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg"
name "Termix"
-172
View File
@@ -1,172 +0,0 @@
# 仓库统计
<p align="center">
<a href="README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> 英文</a> |
<img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="./repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">2025年9月1日获得</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=./repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
如果你愿意,可以在这里支持这个项目!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# 概览
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=./public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix
提供 SSH 终端访问、SSH 隧道功能以及远程文件管理,还会陆续添加更多工具。Termix 是适用于所有平台的完美免费自托管 Termius 替代品。
# 功能
- **SSH 终端访问** - 功能齐全的终端,具有分屏支持(最多 4 个面板)和类似浏览器的选项卡系统。包括对自定义终端的支持,包括常见终端主题、字体和其他组件
- **SSH 隧道管理** - 创建和管理 SSH 隧道,具有自动重新连接和健康监控功能
- **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。无缝上传、下载、重命名、删除和移动文件
- **Docker 管理** - 启动、停止、暂停、删除容器。查看容器统计信息。使用 docker exec 终端控制容器。它不是用来替代 Portainer 或 Dockge,而是用于简单管理你的容器而不是创建它们。
- **SSH 主机管理器** - 保存、组织和管理您的 SSH 连接,支持标签和文件夹,并轻松保存可重用的登录信息,同时能够自动部署 SSH 密钥
- **服务器统计** - 在任何 SSH 服务器上查看 CPU、内存和磁盘使用情况以及网络、正常运行时间和系统信息
- **仪表板** - 在仪表板上一目了然地查看服务器信息
- **RBAC** - 创建角色并在用户/角色之间共享主机
- **用户认证** - 安全的用户管理,具有管理员控制以及 OIDC 和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDC/本地帐户链接在一起。
- **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://docs.termix.site/security)了解更多信息。
- **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据
- **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向
- **现代用户界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁的桌面/移动设备友好界面。可选择基于深色或浅色模式的用户界面。
- **语言** - 内置支持约 30 种语言(由 [Crowdin](https://docs.termix.site/translations) 管理)
- **平台支持** - 可作为 Web 应用程序、桌面应用程序(Windows、Linux 和 macOS)、PWA 以及适用于 iOS 和 Android 的专用移动/平板电脑应用程序。
- **SSH 工具** - 创建可重用的命令片段,单击即可执行。在多个打开的终端上同时运行一个命令。
- **命令历史** - 自动完成并查看以前运行的 SSH 命令
- **快速连接** - 无需保存连接数据即可连接到服务器
- **命令面板** - 双击左 Shift 键可快速使用键盘访问 SSH 连接
- **SSH 功能丰富** - 支持跳板机、Warpgate、基于 TOTP 的连接、SOCKS5、密码自动填充等。
- **网络图** - 自定义您的仪表板,根据您的 SSH 连接可视化您的家庭实验室,支持状态显示
# 计划功能
查看 [项目](https://github.com/orgs/Termix-SSH/projects/2) 了解所有计划功能。如果你想贡献代码,请参阅 [贡献指南](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)。
# 安装
支持的设备:
- 网站(任何平台上的任何现代浏览器,如 Chrome、Safari 和 Firefox)(包括 PWA 支持)
- Windowsx64/ia32
- 便携版
- MSI 安装程序
- Chocolatey 软件包管理器
- Linuxx64/ia32
- 便携版
- AUR
- AppImage
- Deb
- Flatpak
- macOSx64/ia32 on v12.0+
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOSv15.1+
- Apple App Store
- ISO
- Androidv7.0+
- Google Play 商店
- APK
访问 Termix [文档](https://docs.termix.site/install) 了解有关如何在所有平台上安装 Termix 的更多信息。或者,在此处查看示例 Docker Compose 文件:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# 赞助商
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
</p>
# 支持
如果你需要 Termix 的帮助或想要请求功能,请访问 [Issues](https://github.com/Termix-SSH/Support/issues) 页面,登录并点击 `New Issue`
请尽可能详细地描述你的问题,最好使用英语。你也可以加入 [Discord](https://discord.gg/jVQGdvHDrf) 服务器并访问支持
频道,但响应时间可能较长。
# 展示
[![](https://utfs.io/f/nGnSqDveMsqxqiKJF5EnObSopHatulx93N0E1KVsP7hvjMfF)](https://www.youtube.com/watch?v=j1_I1mkhUkE)
<p align="center">
<img src="./repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
<img src="./repo-images/Image 2.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="./repo-images/Image 3.png" width="400" alt="Termix Demo 3"/>
<img src="./repo-images/Image 4.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="./repo-images/Image 5.png" width="400" alt="Termix Demo 5"/>
<img src="./repo-images/Image 6.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="./repo-images/Image 7.png" width="400" alt="Termix Demo 7"/>
<img src="./repo-images/Image 8.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="./repo-images/Image 9.png" width="400" alt="Termix Demo 9"/>
<img src="./repo-images/Image 10.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="./repo-images/Image 11.png" width="400" alt="Termix Demo 11"/>
<img src="./repo-images/Image 12.png" width="400" alt="Termix Demo 12"/>
</p>
某些视频和图像可能已过时或可能无法完美展示功能。
# 许可证
根据 Apache License Version 2.0 发布。更多信息请参见 LICENSE。
+24 -7
View File
@@ -1,8 +1,20 @@
# Repo Stats
<p align="center">
<img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English |
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a>
<img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English ·
<a href="readme/README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="readme/README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="readme/README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="readme/README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="readme/README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="readme/README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="readme/README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="readme/README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="readme/README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="readme/README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="readme/README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="readme/README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="readme/README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -39,7 +51,7 @@ free and self-hosted alternative to Termius available for all platforms.
# Features
- **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) with a browser-like tab system. Includes support for customizing the terminal including common terminal themes, fonts, and other components
- **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) with a browser-like tab system. Includes support for customizing the terminal including common terminal themes, fonts, and other components.
- **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring and support for -l or -r connections
- **Remote File Manager** - Manage files directly on remote servers with support for viewing and editing code, images, audio, and video. Upload, download, rename, delete, and move files seamlessly with sudo support.
- **Docker Management** - Start, stop, pause, remove containers. View container stats. Control container using docker exec terminal. It was not made to replace Portainer or Dockge but rather to simply manage your containers compared to creating them.
@@ -47,19 +59,20 @@ free and self-hosted alternative to Termius available for all platforms.
- **Server Stats** - View CPU, memory, and disk usage along with network, uptime, system information, firewall, port monitor, on most Linux based servers
- **Dashboard** - View server information at a glance on your dashboard
- **RBAC** - Create roles and share hosts across users/roles
- **User Authentication** - Secure user management with admin controls and OIDC and 2FA (TOTP) support. View active user sessions across all platforms and revoke permissions. Link your OIDC/Local accounts together.
- **User Authentication** - Secure user management with admin controls and OIDC (with access control) and 2FA (TOTP) support. View active user sessions across all platforms and revoke permissions. Link your OIDC/Local accounts together.
- **Database Encryption** - Backend stored as encrypted SQLite database files. View [docs](https://docs.termix.site/security) for more.
- **Data Export/Import** - Export and import SSH hosts, credentials, and file manager data
- **Automatic SSL Setup** - Built-in SSL certificate generation and management with HTTPS redirects
- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn. Choose between dark or light mode based UI. Use URL routes to open any connection in full-screen.
- **Languages** - Built-in support ~30 languages (managed by [Crowdin](https://docs.termix.site/translations))
- **Platform Support** - Available as a web app, desktop application (Windows, Linux, and macOS), PWA, and dedicated mobile/tablet app for iOS and Android.
- **Platform Support** - Available as a web app, desktop application (Windows, Linux, and macOS, can be run standalone without Termix backend), PWA, and dedicated mobile/tablet app for iOS and Android.
- **SSH Tools** - Create reusable command snippets that execute with a single click. Run one command simultaneously across multiple open terminals.
- **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, 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
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# Planned Features
@@ -86,7 +99,7 @@ Supported Devices:
- Homebrew
- iOS/iPadOS (v15.1+)
- Apple App Store
- ISO
- IPA
- Android (v7.0+)
- Google Play Store
- APK
@@ -126,6 +139,10 @@ volumes:
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# Support
@@ -136,7 +153,7 @@ channel, however, response times may be longer.
# Screenshots
[![YouTube](./repo-images/YouTube.png)](https://youtu.be/8OYCPwS7ciA)
[![YouTube](./repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="./repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
+4 -5
View File
@@ -6,8 +6,7 @@ RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt
COPY package*.json ./
RUN rm -rf node_modules package-lock.json && \
npm install --ignore-scripts --force && \
RUN npm ci --ignore-scripts --force && \
npm cache clean --force
# Stage 2: Build frontend
@@ -51,7 +50,7 @@ ENV DATA_DIR=/app/data \
PORT=8080 \
NODE_ENV=production
RUN apt-get update && apt-get install -y nginx gettext-base openssl ca-certificates gosu && \
RUN apt-get update && apt-get install -y nginx gettext-base openssl ca-certificates gosu wget && \
update-ca-certificates && \
rm -rf /var/lib/apt/lists/* && \
mkdir -p /app/data /app/uploads /app/data/.opk /app/nginx /app/nginx/logs /app/nginx/cache /app/nginx/client_body && \
@@ -75,8 +74,8 @@ VOLUME ["/app/data"]
EXPOSE ${PORT} 30001 30002 30003 30004 30005 30006
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD node -e "require('http').get('http://localhost:30001/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD wget -q -O /dev/null http://localhost:30001/health || exit 1
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
+17 -8
View File
@@ -5,16 +5,21 @@ PUID=${PUID:-1000}
PGID=${PGID:-1000}
if [ "$(id -u)" = "0" ]; then
echo "Setting up user permissions (PUID: $PUID, PGID: $PGID)..."
if [ "$PUID" = "0" ]; then
echo "Running as root (PUID=0, PGID=$PGID)"
chown -R root:root /app/data /app/uploads /app/nginx 2>/dev/null || true
else
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
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
chown -R node:node /app/data /app/uploads /app/nginx 2>/dev/null || true
echo "User node is now UID: $PUID, GID: $PGID"
echo "User node is now UID: $PUID, GID: $PGID"
exec gosu node:node "$0" "$@"
exec gosu node:node "$0" "$@"
fi
fi
export PORT=${PORT:-8080}
@@ -52,8 +57,12 @@ else
ls -ld /app/data/.opk
fi
if [ ! -d "/app/opkssh" ]; then
echo "WARNING: OPKSSH binary directory not found at /app/opkssh"
OPKSSH_DIR="${DATA_DIR:-/app/data}/opkssh"
if [ ! -d "$OPKSSH_DIR" ]; then
echo "WARNING: OPKSSH binary directory not found at $OPKSSH_DIR"
echo "OPKSSH will be downloaded automatically on first use."
else
echo "OPKSSH binary directory found at $OPKSSH_DIR"
fi
if [ "$ENABLE_SSL" = "true" ]; then
+34 -13
View File
@@ -26,6 +26,21 @@ http {
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
map $http_x_forwarded_host $proxy_x_forwarded_host {
default $http_x_forwarded_host;
'' $http_host;
}
map $http_x_forwarded_port $proxy_x_forwarded_port {
default $http_x_forwarded_port;
'' $server_port;
}
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off;
@@ -75,7 +90,7 @@ http {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
}
location ~ ^/users(/.*)?$ {
@@ -84,7 +99,7 @@ http {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
}
location ~ ^/version(/.*)?$ {
@@ -217,22 +232,30 @@ http {
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-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_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;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_cache_bypass 1;
proxy_no_cache 1;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
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-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_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;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_cache_bypass 1;
proxy_no_cache 1;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
location /ssh/ {
@@ -251,15 +274,13 @@ 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_set_header X-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
+34 -12
View File
@@ -26,6 +26,21 @@ http {
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
map $http_x_forwarded_host $proxy_x_forwarded_host {
default $http_x_forwarded_host;
'' $http_host;
}
map $http_x_forwarded_port $proxy_x_forwarded_port {
default $http_x_forwarded_port;
'' $server_port;
}
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off;
@@ -64,7 +79,7 @@ http {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
}
location ~ ^/users(/.*)?$ {
@@ -73,7 +88,7 @@ http {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
}
location ~ ^/version(/.*)?$ {
@@ -206,22 +221,30 @@ http {
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-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_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;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_cache_bypass 1;
proxy_no_cache 1;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
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-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_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;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_cache_bypass 1;
proxy_no_cache 1;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
location /ssh/ {
@@ -240,14 +263,13 @@ 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 X-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
+11 -6
View File
@@ -5,24 +5,27 @@
"directories": {
"output": "release"
},
"asar": false,
"files": [
"dist/**/*",
"electron/**/*",
"public/**/*",
"!**/node_modules/**/*",
"!src/**/*",
"!*.md",
"!tsconfig*.json",
"!vite.config.ts",
"!eslint.config.js"
"!eslint.config.js",
"!node_modules/@napi-rs/canvas*/**/*",
"!node_modules/@rollup/rollup-darwin-*/**/*",
"!node_modules/@rollup/rollup-linux-*/**/*",
"!node_modules/@rollup/rollup-win32-*/**/*"
],
"asarUnpack": ["node_modules/node-fetch/**/*"],
"extraMetadata": {
"main": "electron/main.cjs"
},
"buildDependenciesFromSource": false,
"nodeGypRebuild": false,
"npmRebuild": true,
"npmRebuild": false,
"win": {
"target": [
{
@@ -103,7 +106,9 @@
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.inherit.plist",
"type": "distribution",
"minimumSystemVersion": "10.15"
"minimumSystemVersion": "10.15",
"mergeASARs": false,
"singleArchFiles": "node_modules/@tailwindcss/oxide-*/**/*"
},
"dmg": {
"artifactName": "termix_macos_${arch}_dmg.${ext}",
@@ -116,7 +121,7 @@
"entitlementsInherit": "build/entitlements.mas.inherit.plist",
"hardenedRuntime": false,
"gatekeeperAssess": false,
"asarUnpack": ["**/*.node"],
"asar": false,
"type": "distribution",
"category": "public.app-category.developer-tools",
"artifactName": "termix_macos_${arch}_mas.${ext}",
+349 -116
View File
@@ -5,10 +5,76 @@ const {
ipcMain,
dialog,
Menu,
Tray,
} = require("electron");
const path = require("path");
const fs = require("fs");
const os = require("os");
const https = require("https");
const http = require("http");
const { URL } = require("url");
const { fork } = require("child_process");
const logFile = path.join(app.getPath("userData"), "termix-main.log");
function logToFile(...args) {
const timestamp = new Date().toISOString();
const msg = args
.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a)))
.join(" ");
const line = `[${timestamp}] ${msg}\n`;
try {
fs.appendFileSync(logFile, line);
} catch {
// ignore
}
console.log(...args);
}
function httpFetch(url, options = {}) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === "https:";
const client = isHttps ? https : http;
const requestOptions = {
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 10000,
};
if (isHttps) {
requestOptions.rejectUnauthorized = false;
requestOptions.agent = new https.Agent({
rejectUnauthorized: false,
checkServerIdentity: () => undefined,
});
}
const req = client.request(url, requestOptions, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
text: () => Promise.resolve(data),
json: () => Promise.resolve(JSON.parse(data)),
});
});
});
req.on("error", reject);
req.on("timeout", () => {
req.destroy();
reject(new Error("Request timeout"));
});
if (options.body) {
req.write(options.body);
}
req.end();
});
}
if (process.platform === "linux") {
app.commandLine.appendSwitch("--ozone-platform-hint=auto");
@@ -22,10 +88,153 @@ app.commandLine.appendSwitch("--ignore-certificate-errors-spki-list");
app.commandLine.appendSwitch("--enable-features=NetworkService");
let mainWindow = null;
let backendProcess = null;
let tray = null;
let isQuitting = false;
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
const appRoot = isDev ? process.cwd() : path.join(__dirname, "..");
function getBackendEntryPath() {
if (isDev) {
return path.join(appRoot, "dist", "backend", "backend", "starter.js");
}
// In production, asar is disabled (asar: false in electron-builder.json)
// so backend is directly in appRoot/dist
return path.join(appRoot, "dist", "backend", "backend", "starter.js");
}
function getBackendDataDir() {
const userDataPath = app.getPath("userData");
const dataDir = path.join(userDataPath, "server-data");
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
return dataDir;
}
function startBackendServer() {
return new Promise((resolve) => {
const entryPath = getBackendEntryPath();
logToFile("isDev:", isDev, "appRoot:", appRoot);
logToFile("app.isPackaged:", app.isPackaged);
logToFile("process.env.NODE_ENV:", process.env.NODE_ENV);
if (!fs.existsSync(entryPath)) {
logToFile("Backend entry not found:", entryPath);
resolve(false);
return;
}
const dataDir = getBackendDataDir();
logToFile("Starting embedded backend server...");
logToFile("Backend entry:", entryPath);
logToFile("Data directory:", dataDir);
logToFile("Backend cwd:", appRoot);
// Verify all required paths exist
logToFile("Checking paths...");
logToFile(" entryPath exists:", fs.existsSync(entryPath));
logToFile(" dataDir exists:", fs.existsSync(dataDir));
logToFile(" appRoot exists:", fs.existsSync(appRoot));
// List contents of dist directory
const distPath = path.join(appRoot, "dist");
if (fs.existsSync(distPath)) {
logToFile(" dist directory contents:", fs.readdirSync(distPath));
const backendPath = path.join(distPath, "backend");
if (fs.existsSync(backendPath)) {
logToFile(" dist/backend contents:", fs.readdirSync(backendPath));
}
}
backendProcess = fork(entryPath, [], {
cwd: appRoot,
env: {
...process.env,
DATA_DIR: dataDir,
NODE_ENV: "production",
ELECTRON_EMBEDDED: "true",
PORT: "30001",
},
stdio: ["pipe", "pipe", "pipe", "ipc"],
});
logToFile("Backend process spawned, pid:", backendProcess.pid);
let resolved = false;
const readyTimeout = setTimeout(() => {
if (!resolved) {
resolved = true;
logToFile("Backend ready timeout (15s), proceeding anyway...");
resolve(true);
}
}, 15000);
backendProcess.stdout.on("data", (data) => {
const msg = data.toString().trim();
logToFile("[backend]", msg);
if (!resolved && msg.includes("started successfully")) {
resolved = true;
clearTimeout(readyTimeout);
logToFile("Backend ready signal received");
resolve(true);
}
});
backendProcess.stderr.on("data", (data) => {
logToFile("[backend:stderr]", data.toString().trim());
});
backendProcess.on("exit", (code, signal) => {
logToFile(`Backend process exited with code ${code}, signal ${signal}`);
backendProcess = null;
if (!resolved) {
resolved = true;
clearTimeout(readyTimeout);
resolve(false);
}
});
backendProcess.on("error", (err) => {
logToFile("Failed to start backend process:", err.message);
backendProcess = null;
if (!resolved) {
resolved = true;
clearTimeout(readyTimeout);
resolve(false);
}
});
});
}
function stopBackendServer() {
if (!backendProcess) return;
console.log("Stopping embedded backend server...");
// Use IPC for graceful shutdown (SIGTERM doesn't work on Windows)
try {
backendProcess.send({ type: "shutdown" });
} catch {
// IPC channel may already be closed
}
const forceKillTimeout = setTimeout(() => {
if (backendProcess) {
console.log("Force killing backend process...");
backendProcess.kill("SIGKILL");
backendProcess = null;
}
}, 5000);
backendProcess.on("exit", () => {
clearTimeout(forceKillTimeout);
backendProcess = null;
});
}
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
console.log("Another instance is already running, quitting...");
@@ -41,6 +250,64 @@ if (!gotTheLock) {
});
}
function createTray() {
try {
const { nativeImage } = require("electron");
let trayIcon;
if (process.platform === "darwin") {
// macOS: use 16x16 Template image for menu bar
const iconPath = path.join(appRoot, "public", "icons", "16x16.png");
trayIcon = nativeImage.createFromPath(iconPath);
trayIcon.setTemplateImage(true);
} else if (process.platform === "win32") {
trayIcon = path.join(appRoot, "public", "icon.ico");
} else {
trayIcon = path.join(appRoot, "public", "icons", "32x32.png");
}
tray = new Tray(trayIcon);
tray.setToolTip("Termix");
const contextMenu = Menu.buildFromTemplate([
{
label: "Show Window",
click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
},
},
{
label: "Quit",
click: () => {
isQuitting = true;
app.quit();
},
},
]);
tray.setContextMenu(contextMenu);
tray.on("click", () => {
if (mainWindow) {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
mainWindow.focus();
}
}
});
console.log("System tray created successfully");
} catch (err) {
console.error("Failed to create system tray:", err);
// Tray is non-critical; app still works without it
}
}
function createWindow() {
const appVersion = app.getVersion();
const electronVersion = process.versions.electron;
@@ -71,6 +338,19 @@ function createWindow() {
show: true,
});
mainWindow.webContents.session.setPermissionRequestHandler(
(webContents, permission, callback) => {
if (
permission === "clipboard-read" ||
permission === "clipboard-sanitized-write"
) {
callback(true);
return;
}
callback(true);
},
);
if (process.platform !== "darwin") {
mainWindow.setMenuBarVisibility(false);
}
@@ -183,6 +463,13 @@ function createWindow() {
console.log("Frontend loaded successfully");
});
mainWindow.on("close", (event) => {
if (!isQuitting && tray && !tray.isDestroyed()) {
event.preventDefault();
mainWindow.hide();
}
});
mainWindow.on("closed", () => {
mainWindow = null;
});
@@ -215,65 +502,7 @@ async function fetchGitHubAPI(endpoint, cacheKey) {
}
try {
let fetch;
try {
fetch = globalThis.fetch || require("node-fetch");
} catch (e) {
const https = require("https");
const http = require("http");
const { URL } = require("url");
fetch = (url, options = {}) => {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === "https:";
const client = isHttps ? https : http;
const requestOptions = {
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 10000,
};
if (isHttps) {
requestOptions.rejectUnauthorized = false;
requestOptions.agent = new https.Agent({
rejectUnauthorized: false,
secureProtocol: "TLSv1_2_method",
checkServerIdentity: () => undefined,
ciphers: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
honorCipherOrder: true,
});
}
const req = client.request(url, requestOptions, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
text: () => Promise.resolve(data),
json: () => Promise.resolve(JSON.parse(data)),
});
});
});
req.on("error", reject);
req.on("timeout", () => {
req.destroy();
reject(new Error("Request timeout"));
});
if (options.body) {
req.write(options.body);
}
req.end();
});
};
}
const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {
const response = await httpFetch(`${GITHUB_API_BASE}${endpoint}`, {
headers: {
Accept: "application/vnd.github+json",
"User-Agent": "TermixElectronUpdateChecker/1.0",
@@ -358,6 +587,14 @@ ipcMain.handle("get-platform", () => {
return process.platform;
});
ipcMain.handle("get-embedded-server-status", () => {
return {
running: backendProcess !== null && !backendProcess.killed,
embedded: !isDev,
dataDir: isDev ? null : getBackendDataDir(),
};
});
ipcMain.handle("get-server-config", () => {
try {
const userDataPath = app.getPath("userData");
@@ -433,67 +670,33 @@ ipcMain.handle("set-setting", (event, key, value) => {
}
});
ipcMain.handle("clear-session-cookies", async () => {
try {
const ses = mainWindow?.webContents?.session;
if (ses) {
const cookies = await ses.cookies.get({});
for (const cookie of cookies) {
const scheme = cookie.secure ? "https" : "http";
const domain = cookie.domain?.startsWith(".")
? cookie.domain.slice(1)
: cookie.domain || "localhost";
const url = `${scheme}://${domain}${cookie.path || "/"}`;
await ses.cookies.remove(url, cookie.name);
}
}
} catch (error) {
console.error("Failed to clear session cookies:", error);
}
});
ipcMain.handle("test-server-connection", async (event, serverUrl) => {
try {
const https = require("https");
const http = require("http");
const { URL } = require("url");
const fetch = (url, options = {}) => {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === "https:";
const client = isHttps ? https : http;
const requestOptions = {
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 10000,
};
if (isHttps) {
requestOptions.rejectUnauthorized = false;
requestOptions.agent = new https.Agent({
rejectUnauthorized: false,
secureProtocol: "TLSv1_2_method",
checkServerIdentity: () => undefined,
ciphers: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
honorCipherOrder: true,
});
}
const req = client.request(url, requestOptions, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
text: () => Promise.resolve(data),
json: () => Promise.resolve(JSON.parse(data)),
});
});
});
req.on("error", reject);
req.on("timeout", () => {
req.destroy();
reject(new Error("Request timeout"));
});
if (options.body) {
req.write(options.body);
}
req.end();
});
};
const normalizedServerUrl = serverUrl.replace(/\/$/, "");
const healthUrl = `${normalizedServerUrl}/health`;
try {
const response = await fetch(healthUrl, {
const response = await httpFetch(healthUrl, {
method: "GET",
timeout: 10000,
});
@@ -539,7 +742,7 @@ ipcMain.handle("test-server-connection", async (event, serverUrl) => {
try {
const versionUrl = `${normalizedServerUrl}/version`;
const response = await fetch(versionUrl, {
const response = await httpFetch(versionUrl, {
method: "GET",
timeout: 10000,
});
@@ -656,13 +859,38 @@ function createMenu() {
}
}
app.whenReady().then(() => {
app.whenReady().then(async () => {
logToFile("=== App ready ===");
logToFile(
"isDev:",
isDev,
"platform:",
process.platform,
"arch:",
process.arch,
);
createMenu();
// Start embedded backend server (skip in dev mode, backend runs separately via npm run dev:backend)
if (!isDev) {
const result = await startBackendServer();
logToFile("startBackendServer result:", result);
} else {
logToFile(
"Skipping embedded backend (isDev=true) - expecting separate dev:backend process",
);
}
createTray();
createWindow();
logToFile("=== Startup complete ===");
});
app.on("window-all-closed", () => {
app.quit();
// If tray exists, keep backend alive; otherwise quit normally
if (!tray || tray.isDestroyed()) {
app.quit();
}
});
app.on("activate", () => {
@@ -671,8 +899,13 @@ app.on("activate", () => {
}
});
app.on("before-quit", () => {
isQuitting = true;
});
app.on("will-quit", () => {
console.log("App will quit...");
stopBackendServer();
});
process.on("uncaughtException", (error) => {
+8
View File
@@ -1,4 +1,5 @@
const { contextBridge, ipcRenderer } = require("electron");
const { clipboard } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
@@ -10,7 +11,14 @@ contextBridge.exposeInMainWorld("electronAPI", {
getSetting: (key) => ipcRenderer.invoke("get-setting", key),
setSetting: (key, value) => ipcRenderer.invoke("set-setting", key, value),
clearSessionCookies: () => ipcRenderer.invoke("clear-session-cookies"),
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
});
contextBridge.exposeInMainWorld("electronClipboard", {
writeText: (text) => clipboard.writeText(text),
readText: () => clipboard.readText(),
});
window.IS_ELECTRON = true;
+9 -1
View File
@@ -6,7 +6,7 @@ import tseslint from "typescript-eslint";
import { globalIgnores } from "eslint/config";
export default tseslint.config([
globalIgnores(["dist"]),
globalIgnores(["dist", "release", "Mobile"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
@@ -19,5 +19,13 @@ export default tseslint.config([
ecmaVersion: 2020,
globals: globals.browser,
},
rules: {
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-expressions": "warn",
"no-empty": "warn",
"no-control-regex": "off",
"react-refresh/only-export-components": "warn",
},
},
]);
+58 -54
View File
@@ -1,12 +1,12 @@
{
"name": "termix",
"version": "1.11.1",
"version": "1.11.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "termix",
"version": "1.11.1",
"version": "1.11.2",
"dependencies": {
"@codemirror/autocomplete": "^6.18.7",
"@codemirror/commands": "^6.3.3",
@@ -43,7 +43,7 @@
"@uiw/codemirror-extensions-langs": "^4.24.1",
"@uiw/codemirror-theme-github": "^4.25.4",
"@uiw/react-codemirror": "^4.24.1",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-clipboard": "^0.2.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0",
@@ -107,6 +107,7 @@
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@electron/notarize": "^2.5.0",
"@electron/rebuild": "^3.7.2",
"@eslint/js": "^9.34.0",
"@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.19",
@@ -213,7 +214,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -499,7 +499,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz",
"integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@@ -548,7 +547,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
@@ -575,7 +573,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
@@ -603,7 +600,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
@@ -804,7 +800,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@@ -881,7 +876,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
@@ -903,7 +897,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
@@ -1231,7 +1224,6 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -1508,9 +1500,9 @@
}
},
"node_modules/@electron/rebuild": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.0.tgz",
"integrity": "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==",
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.2.tgz",
"integrity": "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1618,6 +1610,7 @@
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": {
"cross-dirname": "^0.1.0",
"debug": "^4.3.4",
@@ -1639,6 +1632,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -2692,8 +2686,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz",
"integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@lezer/cpp": {
"version": "1.1.3",
@@ -2733,7 +2726,6 @@
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@lezer/common": "^1.3.0"
}
@@ -2765,7 +2757,6 @@
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
@@ -2788,7 +2779,6 @@
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@lezer/common": "^1.0.0"
}
@@ -5236,7 +5226,6 @@
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/node": "*"
}
@@ -5400,7 +5389,6 @@
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz",
"integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
@@ -5523,7 +5511,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz",
"integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -5566,7 +5553,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -5577,7 +5563,6 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -5754,7 +5739,6 @@
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2",
@@ -6120,15 +6104,12 @@
}
},
"node_modules/@xterm/addon-clipboard": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.1.0.tgz",
"integrity": "sha512-zdoM7p53T5sv/HbRTyp4hY0kKmEQ3MZvAvEtiXqNIHc/JdpqwByCtsTaQF5DX2n4hYdXRPO4P/eOS0QEhX1nPw==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0.tgz",
"integrity": "sha512-Dl31BCtBhLaUEECUbEiVcCLvLBbaeGYdT7NofB8OJkGTD3MWgBsaLjXvfGAD4tQNHhm6mbKyYkR7XD8kiZsdNg==",
"license": "MIT",
"dependencies": {
"js-base64": "^3.7.5"
},
"peerDependencies": {
"@xterm/xterm": "^5.4.0"
}
},
"node_modules/@xterm/addon-fit": {
@@ -6162,8 +6143,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/7zip-bin": {
"version": "5.2.0",
@@ -6198,7 +6178,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6368,6 +6347,35 @@
"electron-builder-squirrel-windows": "26.0.12"
}
},
"node_modules/app-builder-lib/node_modules/@electron/rebuild": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.0.tgz",
"integrity": "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
"@malept/cross-spawn-promise": "^2.0.0",
"chalk": "^4.0.0",
"debug": "^4.1.1",
"detect-libc": "^2.0.1",
"fs-extra": "^10.0.0",
"got": "^11.7.0",
"node-abi": "^3.45.0",
"node-api-version": "^0.2.0",
"ora": "^5.1.0",
"read-binary-file-arch": "^1.0.6",
"semver": "^7.3.5",
"tar": "^6.0.5",
"yargs": "^17.0.1"
},
"bin": {
"electron-rebuild": "lib/cli.js"
},
"engines": {
"node": ">=12.13.0"
}
},
"node_modules/app-builder-lib/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -6626,7 +6634,6 @@
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
@@ -6769,7 +6776,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -7776,7 +7782,6 @@
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"env-paths": "^2.2.1",
"import-fresh": "^3.3.0",
@@ -7853,7 +7858,8 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true,
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/cross-spawn": {
"version": "7.0.6",
@@ -7886,7 +7892,6 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10"
}
@@ -8326,7 +8331,6 @@
"integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"app-builder-lib": "26.0.12",
"builder-util": "26.0.11",
@@ -8437,7 +8441,8 @@
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
"license": "(MPL-2.0 OR Apache-2.0)"
"license": "(MPL-2.0 OR Apache-2.0)",
"peer": true
},
"node_modules/dot-prop": {
"version": "5.3.0",
@@ -8812,6 +8817,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@electron/asar": "^3.2.1",
"debug": "^4.1.1",
@@ -8832,6 +8838,7 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
@@ -8847,6 +8854,7 @@
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"license": "MIT",
"peer": true,
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
@@ -8857,6 +8865,7 @@
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 4.0.0"
}
@@ -9118,7 +9127,6 @@
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -11136,7 +11144,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -12719,6 +12726,7 @@
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -14756,6 +14764,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"commander": "^9.4.0"
},
@@ -14773,6 +14782,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": "^12.20.0 || >=14"
}
@@ -15258,7 +15268,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -15281,7 +15290,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -15340,7 +15348,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
"integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -15488,7 +15495,6 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -15711,8 +15717,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -17197,6 +17202,7 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"mkdirp": "^0.5.1",
"rimraf": "~2.6.2"
@@ -17237,6 +17243,7 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
@@ -17251,6 +17258,7 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -17355,7 +17363,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -17561,7 +17568,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -17984,7 +17990,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -18076,7 +18081,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
+10 -8
View File
@@ -1,7 +1,7 @@
{
"name": "termix",
"private": true,
"version": "1.11.1",
"version": "1.11.2",
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
"author": "Karmaa",
"main": "electron/main.cjs",
@@ -22,12 +22,13 @@
"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 .\"",
"build:win-portable": "npm run build && electron-builder --win --dir",
"build:win-installer": "npm run build && electron-builder --win --publish=never",
"build:linux-portable": "npm run build && electron-builder --linux --dir",
"build:linux-appimage": "npm run build && electron-builder --linux AppImage",
"build:linux-targz": "npm run build && electron-builder --linux tar.gz",
"build:mac": "npm run build && electron-builder --mac --universal"
"electron:rebuild": "electron-rebuild -f -w better-sqlite3",
"build:win-portable": "npm run build && npm run electron:rebuild && electron-builder --win --dir",
"build:win-installer": "npm run build && npm run electron:rebuild && electron-builder --win --publish=never",
"build:linux-portable": "npm run build && npm run electron:rebuild && electron-builder --linux --dir",
"build:linux-appimage": "npm run build && npm run electron:rebuild && electron-builder --linux AppImage",
"build:linux-targz": "npm run build && npm run electron:rebuild && electron-builder --linux tar.gz",
"build:mac": "npm run build && npm run electron:rebuild && electron-builder --mac --universal"
},
"dependencies": {
"@codemirror/autocomplete": "^6.18.7",
@@ -65,7 +66,7 @@
"@uiw/codemirror-extensions-langs": "^4.24.1",
"@uiw/codemirror-theme-github": "^4.25.4",
"@uiw/react-codemirror": "^4.24.1",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-clipboard": "^0.2.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0",
@@ -129,6 +130,7 @@
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@electron/notarize": "^2.5.0",
"@electron/rebuild": "^3.7.2",
"@eslint/js": "^9.34.0",
"@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.19",
+7
View File
@@ -54,6 +54,13 @@ self.addEventListener("fetch", (event) => {
return;
}
if (
url.pathname.startsWith("/ssh/opkssh-chooser/") ||
url.pathname.startsWith("/ssh/opkssh-callback/")
) {
return;
}
if (url.origin !== self.location.origin) {
return;
}
+187
View File
@@ -0,0 +1,187 @@
# إحصائيات المستودع
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">تم تحقيقه في 1 سبتمبر 2025</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
إذا كنت ترغب في ذلك، يمكنك دعم المشروع هنا!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# نظرة عامة
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix هي منصة مفتوحة المصدر ومجانية للأبد وذاتية الاستضافة لإدارة الخوادم بشكل شامل. توفر حلاً متعدد المنصات لإدارة خوادمك وبنيتك التحتية من خلال واجهة واحدة وسهلة الاستخدام. يوفر Termix الوصول إلى طرفية SSH، وقدرات إنشاء أنفاق SSH، وإدارة الملفات عن بُعد، والعديد من الأدوات الأخرى. يُعد Termix البديل المثالي المجاني وذاتي الاستضافة لـ Termius المتاح لجميع المنصات.
# الميزات
- **الوصول إلى طرفية SSH** - طرفية كاملة الميزات مع دعم تقسيم الشاشة (حتى 4 لوحات) مع نظام علامات تبويب شبيه بالمتصفح. يتضمن دعم تخصيص الطرفية بما في ذلك السمات الشائعة والخطوط والمكونات الأخرى
- **إدارة أنفاق SSH** - إنشاء وإدارة أنفاق SSH مع إعادة الاتصال التلقائي ومراقبة الحالة ودعم اتصالات -l أو -r
- **مدير الملفات عن بُعد** - إدارة الملفات مباشرة على الخوادم البعيدة مع دعم عرض وتحرير الكود والصور والصوت والفيديو. رفع وتنزيل وإعادة تسمية وحذف ونقل الملفات بسلاسة مع دعم sudo
- **إدارة Docker** - تشغيل وإيقاف وتعليق وحذف الحاويات. عرض إحصائيات الحاويات. التحكم في الحاوية باستخدام طرفية docker exec. لم يُصمم ليحل محل Portainer أو Dockge بل لإدارة حاوياتك ببساطة مقارنة بإنشائها
- **مدير مضيفات SSH** - حفظ وتنظيم وإدارة اتصالات SSH الخاصة بك باستخدام العلامات والمجلدات، وحفظ بيانات تسجيل الدخول القابلة لإعادة الاستخدام بسهولة مع إمكانية أتمتة نشر مفاتيح SSH
- **إحصائيات الخادم** - عرض استخدام المعالج والذاكرة والقرص إلى جانب الشبكة ووقت التشغيل ومعلومات النظام وجدار الحماية ومراقب المنافذ على معظم الخوادم المبنية على Linux
- **لوحة التحكم** - عرض معلومات الخادم بنظرة واحدة على لوحة التحكم
- **RBAC** - إنشاء الأدوار ومشاركة المضيفات عبر المستخدمين/الأدوار
- **مصادقة المستخدمين** - إدارة آمنة للمستخدمين مع ضوابط إدارية ودعم OIDC و 2FA (TOTP). عرض جلسات المستخدمين النشطة عبر جميع المنصات وإلغاء الصلاحيات. ربط حسابات OIDC/المحلية معاً
- **تشفير قاعدة البيانات** - يُخزَّن الخادم الخلفي كملفات قاعدة بيانات SQLite مشفرة. اطلع على [الوثائق](https://docs.termix.site/security) لمزيد من المعلومات
- **تصدير/استيراد البيانات** - تصدير واستيراد مضيفات SSH وبيانات الاعتماد وبيانات مدير الملفات
- **إعداد SSL تلقائي** - إنشاء وإدارة شهادات SSL مدمجة مع إعادة التوجيه إلى HTTPS
- **واجهة مستخدم حديثة** - واجهة نظيفة متوافقة مع سطح المكتب والهاتف المحمول مبنية بـ React و Tailwind CSS و Shadcn. الاختيار بين الوضع الداكن أو الفاتح. استخدام مسارات URL لفتح أي اتصال في وضع ملء الشاشة
- **اللغات** - دعم مدمج لحوالي 30 لغة (تُدار بواسطة [Crowdin](https://docs.termix.site/translations))
- **دعم المنصات** - متاح كتطبيق ويب، وتطبيق سطح مكتب (Windows و Linux و macOS)، و PWA، وتطبيق مخصص للهاتف المحمول/الجهاز اللوحي لـ iOS و Android
- **أدوات SSH** - إنشاء مقتطفات أوامر قابلة لإعادة الاستخدام تُنفَّذ بنقرة واحدة. تشغيل أمر واحد في وقت واحد عبر عدة طرفيات مفتوحة
- **سجل الأوامر** - الإكمال التلقائي وعرض أوامر SSH التي تم تنفيذها سابقاً
- **الاتصال السريع** - الاتصال بخادم دون الحاجة إلى حفظ بيانات الاتصال
- **لوحة الأوامر** - اضغط مرتين على Shift الأيسر للوصول السريع إلى اتصالات SSH باستخدام لوحة المفاتيح
- **ميزات SSH الغنية** - دعم مضيفات القفز، Warpgate، الاتصالات المبنية على TOTP، SOCKS5، التحقق من مفتاح المضيف، الملء التلقائي لكلمة المرور، [OPKSSH](https://github.com/openpubkey/opkssh)، وغيرها
- **الرسم البياني للشبكة** - تخصيص لوحة التحكم لتصور مختبرك المنزلي بناءً على اتصالات SSH مع دعم الحالة
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# الميزات المخططة
راجع [المشاريع](https://github.com/orgs/Termix-SSH/projects/2) لعرض جميع الميزات المخططة. إذا كنت تتطلع للمساهمة، راجع [المساهمة](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
# التثبيت
الأجهزة المدعومة:
- الموقع الإلكتروني (أي متصفح حديث على أي منصة مثل Chrome و Safari و Firefox) (يتضمن دعم PWA)
- Windows (x64/ia32)
- نسخة محمولة
- مثبت MSI
- مدير حزم Chocolatey
- Linux (x64/ia32)
- نسخة محمولة
- AUR
- AppImage
- Deb
- Flatpak
- macOS (x64/ia32 على الإصدار 12.0+)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (الإصدار 15.1+)
- Apple App Store
- IPA
- Android (الإصدار 7.0+)
- Google Play Store
- APK
قم بزيارة [وثائق](https://docs.termix.site/install) Termix للحصول على مزيد من المعلومات حول كيفية تثبيت Termix على جميع المنصات. بخلاف ذلك، يمكنك الاطلاع على نموذج ملف Docker Compose هنا:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# الرعاة
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# الدعم
إذا كنت بحاجة إلى مساعدة أو ترغب في طلب ميزة لـ Termix، قم بزيارة صفحة [المشكلات](https://github.com/Termix-SSH/Support/issues)، وسجل الدخول، واضغط على `New Issue`.
يرجى أن تكون مفصلاً قدر الإمكان في مشكلتك، ويُفضَّل كتابتها باللغة الإنجليزية. يمكنك أيضاً الانضمام إلى خادم [Discord](https://discord.gg/jVQGdvHDrf) وزيارة قناة الدعم، ومع ذلك قد تكون أوقات الاستجابة أطول.
# لقطات الشاشة
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
قد تكون بعض مقاطع الفيديو والصور قديمة أو قد لا تعرض الميزات بشكل مثالي.
# الترخيص
موزع بموجب رخصة Apache License الإصدار 2.0. راجع ملف LICENSE لمزيد من المعلومات.
+189
View File
@@ -0,0 +1,189 @@
# 仓库统计
<p align="IPAenter">
<a href="../README.md"><img srIPA="https://flagIPAdn.IPAom/us.svg" alt="English" width="24" height="16"> English</a> ·
<img srIPA="https://flagIPAdn.IPAom/IPAn.svg" alt="中文" width="24" height="16"> 中文 ·
<a href="README-JA.md"><img srIPA="https://flagIPAdn.IPAom/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img srIPA="https://flagIPAdn.IPAom/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img srIPA="https://flagIPAdn.IPAom/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img srIPA="https://flagIPAdn.IPAom/de.svg" alt="DeutsIPAh" width="24" height="16"> DeutsIPAh</a> ·
<a href="README-ES.md"><img srIPA="https://flagIPAdn.IPAom/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img srIPA="https://flagIPAdn.IPAom/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img srIPA="https://flagIPAdn.IPAom/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img srIPA="https://flagIPAdn.IPAom/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img srIPA="https://flagIPAdn.IPAom/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img srIPA="https://flagIPAdn.IPAom/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img srIPA="https://flagIPAdn.IPAom/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img srIPA="https://flagIPAdn.IPAom/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://disIPAord.gg/jVQGdvHDrf"><img alt="DisIPAord" srIPA="https://img.shields.io/disIPAord/1347374268253470720"></a>
<p align="IPAenter">
<img srIPA="../repo-images/RepoOfTheDay.png" alt="Repo of the Day AIPAhievement" style="width: 300px; height: auto;">
<br>
<small style="IPAolor: #666;">2025年9月1日获得</small>
</p>
<br />
<p align="IPAenter">
<a href="https://github.IPAom/Termix-SSH/Termix">
<img alt="Termix Banner" srIPA=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
如果你愿意,可以在这里支持这个项目!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoIPAolor=white)](https://github.IPAom/sponsors/LukeGus)
# 概览
<p align="IPAenter">
<a href="https://github.IPAom/Termix-SSH/Termix">
<img alt="Termix Banner" srIPA=../publiIPA/iIPAon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix
提供 SSH 终端访问、SSH 隧道功能以及远程文件管理,还会陆续添加更多工具。Termix 是适用于所有平台的完美免费自托管 Termius 替代品。
# 功能
- **SSH 终端访问** - 功能齐全的终端,具有分屏支持(最多 4 个面板)和类似浏览器的选项卡系统。包括对自定义终端的支持,包括常见终端主题、字体和其他组件
- **SSH 隧道管理** - 创建和管理 SSH 隧道,具有自动重新连接和健康监控功能
- **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。无缝上传、下载、重命名、删除和移动文件
- **DoIPAker 管理** - 启动、停止、暂停、删除容器。查看容器统计信息。使用 doIPAker exeIPA 终端控制容器。它不是用来替代 Portainer 或 DoIPAkge,而是用于简单管理你的容器而不是创建它们。
- **SSH 主机管理器** - 保存、组织和管理您的 SSH 连接,支持标签和文件夹,并轻松保存可重用的登录信息,同时能够自动部署 SSH 密钥
- **服务器统计** - 在任何 SSH 服务器上查看 IPAPU、内存和磁盘使用情况以及网络、正常运行时间和系统信息
- **仪表板** - 在仪表板上一目了然地查看服务器信息
- **RBAIPA** - 创建角色并在用户/角色之间共享主机
- **用户认证** - 安全的用户管理,具有管理员控制以及 OIDIPA 和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDIPA/本地帐户链接在一起。
- **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://doIPAs.termix.site/seIPAurity)了解更多信息。
- **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据
- **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向
- **现代用户界面** - 使用 ReaIPAt、Tailwind IPASS 和 ShadIPAn 构建的简洁的桌面/移动设备友好界面。可选择基于深色或浅色模式的用户界面。
- **语言** - 内置支持约 30 种语言(由 [IPArowdin](https://doIPAs.termix.site/translations) 管理)
- **平台支持** - 可作为 Web 应用程序、桌面应用程序(Windows、Linux 和 maIPAOS)、PWA 以及适用于 iOS 和 Android 的专用移动/平板电脑应用程序。
- **SSH 工具** - 创建可重用的命令片段,单击即可执行。在多个打开的终端上同时运行一个命令。
- **命令历史** - 自动完成并查看以前运行的 SSH 命令
- **快速连接** - 无需保存连接数据即可连接到服务器
- **命令面板** - 双击左 Shift 键可快速使用键盘访问 SSH 连接
- **SSH 功能丰富** - 支持跳板机、Warpgate、基于 TOTP 的连接、SOIPAKS5、主机密钥验证、密码自动填充、[OPKSSH](https://github.IPAom/openpubkey/opkssh)等。
- **网络图** - 自定义您的仪表板,根据您的 SSH 连接可视化您的家庭实验室,支持状态显示
- **持久标签页** - 如果在用户配置文件中启用,SSH 会话和标签页在设备/刷新后保持打开状态
# 计划功能
查看 [项目](https://github.IPAom/orgs/Termix-SSH/projeIPAts/2) 了解所有计划功能。如果你想贡献代码,请参阅 [贡献指南](https://github.IPAom/Termix-SSH/Termix/blob/main/IPAONTRIBUTING.md)。
# 安装
支持的设备:
- 网站(任何平台上的任何现代浏览器,如 IPAhrome、Safari 和 Firefox)(包括 PWA 支持)
- Windowsx64/ia32
- 便携版
- MSI 安装程序
- IPAhoIPAolatey 软件包管理器
- Linuxx64/ia32
- 便携版
- AUR
- AppImage
- Deb
- Flatpak
- maIPAOSx64/ia32 on v12.0+
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOSv15.1+
- Apple App Store
- IPA
- Androidv7.0+
- Google Play 商店
- APK
访问 Termix [文档](https://doIPAs.termix.site/install) 了解有关如何在所有平台上安装 Termix 的更多信息。或者,在此处查看示例 DoIPAker IPAompose 文件:
```yaml
serviIPAes:
termix:
image: ghIPAr.io/lukegus/termix:latest
IPAontainer_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: loIPAal
```
# 赞助商
<p align="left">
<a href="https://www.digitaloIPAean.IPAom/">
<img srIPA="https://opensourIPAe.nyIPA3.IPAdn.digitaloIPAeanspaIPAes.IPAom/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOIPAean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://IPArowdin.IPAom/">
<img srIPA="https://support.IPArowdin.IPAom/assets/logos/IPAore-logo/svg/IPArowdin-IPAore-logo-IPADark.svg" height="50" alt="IPArowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blaIPAksmith.sh/">
<img srIPA="https://IPAdn.prod.website-files.IPAom/681bfb0IPA9a4601bIPA6e288eIPA4/683IPAa9e2IPA5186757092611b8_e8IPAb22127df4da0811IPA4120a523722d2_logo-baIPAksmith-wordmark-light.svg" height="50" alt="IPArowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.IPAloudflare.IPAom/">
<img srIPA="https://sirv.sirv.IPAom/website/sIPAreenshots/IPAloudflare/IPAloudflare-logo.png?w=300" height="50" alt="IPArowdin">
</a>
</p>
# 支持
如果你需要 Termix 的帮助或想要请求功能,请访问 [Issues](https://github.IPAom/Termix-SSH/Support/issues) 页面,登录并点击 `New Issue`
请尽可能详细地描述你的问题,最好使用英语。你也可以加入 [DisIPAord](https://disIPAord.gg/jVQGdvHDrf) 服务器并访问支持
频道,但响应时间可能较长。
# 展示
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfIPAK0NY)
<p align="IPAenter">
<img srIPA="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img srIPA="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="IPAenter">
<img srIPA="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img srIPA="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="IPAenter">
<img srIPA="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img srIPA="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="IPAenter">
<img srIPA="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img srIPA="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="IPAenter">
<img srIPA="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img srIPA="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="IPAenter">
<img srIPA="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img srIPA="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
某些视频和图像可能已过时或可能无法完美展示功能。
# 许可证
根据 ApaIPAhe LiIPAense Version 2.0 发布。更多信息请参见 LIIPAENSE。
+187
View File
@@ -0,0 +1,187 @@
# Repo-Statistiken
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">Erreicht am 1. September 2025</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Wenn Sie möchten, können Sie das Projekt hier unterstützen!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Überblick
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-Serververwaltungsplattform. Sie bietet eine plattformübergreifende Lösung zur Verwaltung Ihrer Server und Infrastruktur über eine einzige, intuitive Oberfläche. Termix bietet SSH-Terminalzugriff, SSH-Tunneling-Funktionen, Remote-Dateiverwaltung und viele weitere Werkzeuge. Termix ist die perfekte kostenlose und selbst gehostete Alternative zu Termius, verfügbar für alle Plattformen.
# Funktionen
- **SSH-Terminalzugriff** - Voll ausgestattetes Terminal mit Split-Screen-Unterstützung (bis zu 4 Panels) mit einem browserähnlichen Tab-System. Enthält Unterstützung für die Anpassung des Terminals einschließlich gängiger Terminal-Themes, Schriftarten und anderer Komponenten
- **SSH-Tunnelverwaltung** - Erstellen und verwalten Sie SSH-Tunnel mit automatischer Wiederverbindung und Gesundheitsüberwachung sowie Unterstützung für -l oder -r Verbindungen
- **Remote-Dateimanager** - Verwalten Sie Dateien direkt auf Remote-Servern mit Unterstützung für das Anzeigen und Bearbeiten von Code, Bildern, Audio und Video. Laden Sie Dateien hoch, herunter, benennen Sie sie um, löschen oder verschieben Sie sie nahtlos mit Sudo-Unterstützung.
- **Docker-Verwaltung** - Container starten, stoppen, pausieren, entfernen. Container-Statistiken anzeigen. Container über Docker-Exec-Terminal steuern. Es wurde nicht entwickelt, um Portainer oder Dockge zu ersetzen, sondern um Ihre Container einfach zu verwalten, anstatt sie zu erstellen.
- **SSH-Host-Manager** - Speichern, organisieren und verwalten Sie Ihre SSH-Verbindungen mit Tags und Ordnern und speichern Sie einfach wiederverwendbare Anmeldeinformationen mit der Möglichkeit, die Bereitstellung von SSH-Schlüsseln zu automatisieren
- **Serverstatistiken** - CPU-, Arbeitsspeicher- und Festplattenauslastung sowie Netzwerk, Betriebszeit, Systeminformationen, Firewall, Port-Monitor auf den meisten Linux-basierten Servern anzeigen
- **Dashboard** - Serverinformationen auf einen Blick auf Ihrem Dashboard anzeigen
- **RBAC** - Rollen erstellen und Hosts über Benutzer/Rollen teilen
- **Benutzerauthentifizierung** - Sichere Benutzerverwaltung mit Admin-Kontrollen und OIDC- sowie 2FA (TOTP)-Unterstützung. Aktive Benutzersitzungen über alle Plattformen anzeigen und Berechtigungen widerrufen. OIDC-/Lokale Konten miteinander verknüpfen.
- **Datenbankverschlüsselung** - Backend gespeichert als verschlüsselte SQLite-Datenbankdateien. Weitere Informationen in der [Dokumentation](https://docs.termix.site/security).
- **Datenexport/-import** - SSH-Hosts, Anmeldeinformationen und Dateimanager-Daten exportieren und importieren
- **Automatische SSL-Einrichtung** - Integrierte SSL-Zertifikatsgenerierung und -verwaltung mit HTTPS-Weiterleitungen
- **Moderne Benutzeroberfläche** - Saubere desktop-/mobilfreundliche Oberfläche, erstellt mit React, Tailwind CSS und Shadcn. Wählen Sie zwischen dunklem oder hellem Modus. Verwenden Sie URL-Routen, um jede Verbindung im Vollbildmodus zu öffnen.
- **Sprachen** - Integrierte Unterstützung für ~30 Sprachen (verwaltet über [Crowdin](https://docs.termix.site/translations))
- **Plattformunterstützung** - Verfügbar als Web-App, Desktop-Anwendung (Windows, Linux und macOS), PWA und dedizierte Mobil-/Tablet-App für iOS und Android.
- **SSH-Werkzeuge** - Erstellen Sie wiederverwendbare Befehlsvorlagen, die mit einem einzigen Klick ausgeführt werden. Führen Sie einen Befehl gleichzeitig in mehreren geöffneten Terminals aus.
- **Befehlsverlauf** - Autovervollständigung und Anzeige zuvor ausgeführter SSH-Befehle
- **Schnellverbindung** - Verbinden Sie sich mit einem Server, ohne die Verbindungsdaten speichern zu müssen
- **Befehlspalette** - Doppeltippen Sie die linke Umschalttaste, um schnell auf SSH-Verbindungen mit Ihrer Tastatur zuzugreifen
- **SSH-Funktionsreich** - Unterstützt Jump-Hosts, Warpgate, TOTP-basierte Verbindungen, SOCKS5, Host-Key-Verifizierung, automatisches Ausfüllen von Passwörtern, [OPKSSH](https://github.com/openpubkey/opkssh) usw.
- **Netzwerkgraph** - Passen Sie Ihr Dashboard an, um Ihr Homelab basierend auf Ihren SSH-Verbindungen mit Statusunterstützung zu visualisieren
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# Geplante Funktionen
Siehe [Projekte](https://github.com/orgs/Termix-SSH/projects/2) für alle geplanten Funktionen. Wenn Sie beitragen möchten, siehe [Mitwirken](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
# Installation
Unterstützte Geräte:
- Website (jeder moderne Browser auf jeder Plattform wie Chrome, Safari und Firefox) (einschließlich PWA-Unterstützung)
- Windows (x64/ia32)
- Portabel
- MSI-Installationsprogramm
- Chocolatey-Paketmanager
- Linux (x64/ia32)
- Portabel
- AUR
- AppImage
- Deb
- Flatpak
- macOS (x64/ia32 ab v12.0+)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (v15.1+)
- Apple App Store
- IPA
- Android (v7.0+)
- Google Play Store
- APK
Besuchen Sie die Termix-[Dokumentation](https://docs.termix.site/install) für weitere Informationen zur Installation von Termix auf allen Plattformen. Alternativ finden Sie hier eine Docker Compose-Beispieldatei:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# Sponsoren
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# Support
Wenn Sie Hilfe benötigen oder eine Funktion für Termix anfragen möchten, besuchen Sie die [Issues](https://github.com/Termix-SSH/Support/issues)-Seite, melden Sie sich an und klicken Sie auf `New Issue`.
Bitte beschreiben Sie Ihr Anliegen so detailliert wie möglich, vorzugsweise auf Englisch. Sie können auch dem [Discord](https://discord.gg/jVQGdvHDrf)-Server beitreten und den Support-Kanal besuchen, allerdings können die Antwortzeiten dort länger sein.
# Screenshots
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Einige Videos und Bilder können veraltet sein oder Funktionen möglicherweise nicht perfekt darstellen.
# Lizenz
Verteilt unter der Apache License Version 2.0. Siehe LICENSE für weitere Informationen.
+187
View File
@@ -0,0 +1,187 @@
# Estadísticas del Repositorio
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">Logrado el 1 de septiembre de 2025</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Si lo desea, puede apoyar el proyecto aquí.\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Descripción General
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix es una plataforma de gestión de servidores todo en uno, de código abierto, siempre gratuita y autoalojada. Proporciona una solución multiplataforma para gestionar sus servidores e infraestructura a través de una interfaz única e intuitiva. Termix ofrece acceso a terminal SSH, capacidades de túneles SSH, gestión remota de archivos y muchas otras herramientas. Termix es la alternativa perfecta, gratuita y autoalojada a Termius, disponible para todas las plataformas.
# Características
- **Acceso a Terminal SSH** - Terminal completo con soporte de pantalla dividida (hasta 4 paneles) con un sistema de pestañas similar al navegador. Incluye soporte para personalizar el terminal incluyendo temas comunes de terminal, fuentes y otros componentes
- **Gestión de Túneles SSH** - Cree y gestione túneles SSH con reconexión automática y monitoreo de estado, con soporte para conexiones -l o -r
- **Gestor Remoto de Archivos** - Gestione archivos directamente en servidores remotos con soporte para visualizar y editar código, imágenes, audio y video. Suba, descargue, renombre, elimine y mueva archivos sin problemas con soporte sudo.
- **Gestión de Docker** - Inicie, detenga, pause, elimine contenedores. Vea estadísticas de contenedores. Controle contenedores usando el terminal Docker Exec. No fue creado para reemplazar Portainer o Dockge, sino para simplemente gestionar sus contenedores en lugar de crearlos.
- **Gestor de Hosts SSH** - Guarde, organice y gestione sus conexiones SSH con etiquetas y carpetas, y guarde fácilmente información de inicio de sesión reutilizable con la capacidad de automatizar el despliegue de claves SSH
- **Estadísticas del Servidor** - Vea el uso de CPU, memoria y disco junto con red, tiempo de actividad, información del sistema, firewall, monitor de puertos en la mayoría de los servidores basados en Linux
- **Dashboard** - Vea la información del servidor de un vistazo en su dashboard
- **RBAC** - Cree roles y comparta hosts entre usuarios/roles
- **Autenticación de Usuarios** - Gestión segura de usuarios con controles de administrador y soporte para OIDC y 2FA (TOTP). Vea sesiones activas de usuarios en todas las plataformas y revoque permisos. Vincule sus cuentas OIDC/Locales entre sí.
- **Cifrado de Base de Datos** - Backend almacenado como archivos de base de datos SQLite cifrados. Consulte la [documentación](https://docs.termix.site/security) para más información.
- **Exportación/Importación de Datos** - Exporte e importe hosts SSH, credenciales y datos del gestor de archivos
- **Configuración Automática de SSL** - Generación y gestión integrada de certificados SSL con redirecciones HTTPS
- **Interfaz Moderna** - Interfaz limpia compatible con escritorio/móvil construida con React, Tailwind CSS y Shadcn. Elija entre modo oscuro o claro. Use rutas URL para abrir cualquier conexión en pantalla completa.
- **Idiomas** - Soporte integrado para ~30 idiomas (gestionado por [Crowdin](https://docs.termix.site/translations))
- **Soporte de Plataformas** - Disponible como aplicación web, aplicación de escritorio (Windows, Linux y macOS), PWA y aplicación dedicada para móviles/tablets en iOS y Android.
- **Herramientas SSH** - Cree fragmentos de comandos reutilizables que se ejecutan con un solo clic. Ejecute un comando simultáneamente en múltiples terminales abiertos.
- **Historial de Comandos** - Autocompletado y visualización de comandos SSH ejecutados anteriormente
- **Conexión Rápida** - Conéctese a un servidor sin necesidad de guardar los datos de conexión
- **Paleta de Comandos** - Pulse dos veces la tecla Shift izquierda para acceder rápidamente a las conexiones SSH con su teclado
- **SSH Rico en Funciones** - Soporta jump hosts, Warpgate, conexiones basadas en TOTP, SOCKS5, verificación de clave de host, autocompletado de contraseñas, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
- **Gráfico de Red** - Personalice su Dashboard para visualizar su homelab basado en sus conexiones SSH con soporte de estado
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# Características Planeadas
Consulte [Proyectos](https://github.com/orgs/Termix-SSH/projects/2) para todas las características planeadas. Si desea contribuir, consulte [Contribuir](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
# Instalación
Dispositivos soportados:
- Sitio web (cualquier navegador moderno en cualquier plataforma como Chrome, Safari y Firefox) (incluye soporte PWA)
- Windows (x64/ia32)
- Portable
- Instalador MSI
- Gestor de paquetes Chocolatey
- Linux (x64/ia32)
- Portable
- AUR
- AppImage
- Deb
- Flatpak
- macOS (x64/ia32 en v12.0+)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (v15.1+)
- Apple App Store
- IPA
- Android (v7.0+)
- Google Play Store
- APK
Visite la [documentación](https://docs.termix.site/install) de Termix para más información sobre cómo instalar Termix en todas las plataformas. De lo contrario, vea un archivo Docker Compose de ejemplo aquí:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# Patrocinadores
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# Soporte
Si necesita ayuda o desea solicitar una función para Termix, visite la página de [Issues](https://github.com/Termix-SSH/Support/issues), inicie sesión y pulse `New Issue`.
Por favor, sea lo más detallado posible en su reporte, preferiblemente escrito en inglés. También puede unirse al servidor de [Discord](https://discord.gg/jVQGdvHDrf) y visitar el canal de soporte, sin embargo, los tiempos de respuesta pueden ser más largos.
# Capturas de Pantalla
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Algunos videos e imágenes pueden estar desactualizados o no mostrar perfectamente las características.
# Licencia
Distribuido bajo la Licencia Apache Versión 2.0. Consulte LICENSE para más información.
+186
View File
@@ -0,0 +1,186 @@
# Statistiques du dépôt
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">Obtenu le 1er septembre 2025</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Si vous le souhaitez, vous pouvez soutenir le projet ici !\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Présentation
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix est une plateforme de gestion de serveurs tout-en-un, open source, à jamais gratuite et auto-hébergée. Elle fournit une solution multiplateforme pour gérer vos serveurs et votre infrastructure à travers une interface unique et intuitive. Termix offre un accès terminal SSH, des capacités de tunneling SSH, la gestion de fichiers à distance, et de nombreux autres outils. Termix est l'alternative parfaite, gratuite et auto-hébergée à Termius, disponible sur toutes les plateformes.
# Fonctionnalités
- **Accès terminal SSH** - Terminal complet avec support d'écran partagé (jusqu'à 4 panneaux) et un système d'onglets inspiré des navigateurs. Inclut la personnalisation du terminal avec des thèmes courants, des polices et d'autres composants
- **Gestion des tunnels SSH** - Créez et gérez des tunnels SSH avec reconnexion automatique et surveillance de l'état, avec support des connexions -l ou -r
- **Gestionnaire de fichiers distant** - Gérez les fichiers directement sur les serveurs distants avec support de la visualisation et de l'édition de code, images, audio et vidéo. Téléversez, téléchargez, renommez, supprimez et déplacez des fichiers de manière fluide avec support sudo
- **Gestion Docker** - Démarrez, arrêtez, mettez en pause, supprimez des conteneurs. Consultez les statistiques des conteneurs. Contrôlez les conteneurs via le terminal docker exec. Non conçu pour remplacer Portainer ou Dockge, mais plutôt pour gérer simplement vos conteneurs plutôt que de les créer
- **Gestionnaire d'hôtes SSH** - Enregistrez, organisez et gérez vos connexions SSH avec des tags et des dossiers, et sauvegardez facilement les informations de connexion réutilisables tout en automatisant le déploiement des clés SSH
- **Statistiques serveur** - Visualisez l'utilisation du CPU, de la mémoire et du disque ainsi que le réseau, le temps de fonctionnement, les informations système, le pare-feu et le moniteur de ports sur la plupart des serveurs Linux
- **Tableau de bord** - Consultez les informations de vos serveurs en un coup d'œil depuis votre tableau de bord
- **RBAC** - Créez des rôles et partagez des hôtes entre utilisateurs/rôles
- **Authentification des utilisateurs** - Gestion sécurisée des utilisateurs avec contrôles administrateur et support OIDC et 2FA (TOTP). Visualisez les sessions utilisateur actives sur toutes les plateformes et révoquez les permissions. Liez vos comptes OIDC/locaux ensemble
- **Chiffrement de la base de données** - Le backend est stocké sous forme de fichiers de base de données SQLite chiffrés. Consultez la [documentation](https://docs.termix.site/security) pour plus de détails
- **Export/Import de données** - Exportez et importez les hôtes SSH, les identifiants et les données du gestionnaire de fichiers
- **Configuration SSL automatique** - Génération et gestion intégrées de certificats SSL avec redirections HTTPS
- **Interface moderne** - Interface épurée compatible desktop/mobile construite avec React, Tailwind CSS et Shadcn. Choisissez entre un thème sombre ou clair. Utilisez les routes URL pour ouvrir n'importe quelle connexion en plein écran
- **Langues** - Support intégré d'environ 30 langues (géré par [Crowdin](https://docs.termix.site/translations))
- **Support multiplateforme** - Disponible en tant qu'application web, application de bureau (Windows, Linux et macOS), PWA, et application mobile/tablette dédiée pour iOS et Android
- **Outils SSH** - Créez des extraits de commandes réutilisables exécutables en un seul clic. Exécutez une commande simultanément sur plusieurs terminaux ouverts
- **Historique des commandes** - Auto-complétion et consultation des commandes SSH précédemment exécutées
- **Connexion rapide** - Connectez-vous à un serveur sans avoir à sauvegarder les données de connexion
- **Palette de commandes** - Appuyez deux fois sur Shift gauche pour accéder rapidement aux connexions SSH avec votre clavier
- **SSH riche en fonctionnalités** - Support des hôtes de rebond, Warpgate, connexions basées sur TOTP, SOCKS5, vérification des clés d'hôte, remplissage automatique des mots de passe, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
- **Graphe réseau** - Personnalisez votre tableau de bord pour visualiser votre homelab basé sur vos connexions SSH avec support des statuts
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# Fonctionnalités prévues
Consultez les [Projects](https://github.com/orgs/Termix-SSH/projects/2) pour toutes les fonctionnalités prévues. Si vous souhaitez contribuer, consultez [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
# Installation
Appareils supportés :
- Site web (tout navigateur moderne sur toute plateforme comme Chrome, Safari et Firefox) (support PWA inclus)
- Windows (x64/ia32)
- Portable
- Installateur MSI
- Gestionnaire de paquets Chocolatey
- Linux (x64/ia32)
- Portable
- AUR
- AppImage
- Deb
- Flatpak
- macOS (x64/ia32 sur v12.0+)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (v15.1+)
- Apple App Store
- IPA
- Android (v7.0+)
- Google Play Store
- APK
Visitez la [documentation](https://docs.termix.site/install) de Termix pour plus d'informations sur l'installation de Termix sur toutes les plateformes. Sinon, voici un exemple de fichier Docker Compose :
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# Sponsors
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# Support
Si vous avez besoin d'aide ou souhaitez demander une fonctionnalité pour Termix, visitez la page [Issues](https://github.com/Termix-SSH/Support/issues), connectez-vous et appuyez sur `New Issue`. Veuillez être aussi détaillé que possible dans votre issue, de préférence rédigée en anglais. Vous pouvez également rejoindre le serveur [Discord](https://discord.gg/jVQGdvHDrf) et visiter le canal de support, cependant les temps de réponse peuvent être plus longs.
# Captures d'écran
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Certaines vidéos et images peuvent être obsolètes ou ne pas présenter parfaitement les fonctionnalités.
# Licence
Distribué sous la licence Apache Version 2.0. Consultez LICENSE pour plus d'informations.
+187
View File
@@ -0,0 +1,187 @@
# रिपॉजिटरी आँकड़े
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">1 सितंबर, 2025 को प्राप्त</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
यदि आप चाहें, तो आप यहाँ प्रोजेक्ट को सपोर्ट कर सकते हैं!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# अवलोकन
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix एक ओपन-सोर्स, हमेशा के लिए मुफ़्त, सेल्फ-होस्टेड ऑल-इन-वन सर्वर प्रबंधन प्लेटफ़ॉर्म है। यह एक एकल, सहज इंटरफ़ेस के माध्यम से आपके सर्वर और बुनियादी ढाँचे के प्रबंधन के लिए एक मल्टी-प्लेटफ़ॉर्म समाधान प्रदान करता है। Termix SSH टर्मिनल एक्सेस, SSH टनलिंग क्षमताएँ, रिमोट फ़ाइल प्रबंधन, और कई अन्य उपकरण प्रदान करता है। Termix सभी प्लेटफ़ॉर्म पर उपलब्ध Termius का सही मुफ़्त और सेल्फ-होस्टेड विकल्प है।
# विशेषताएँ
- **SSH टर्मिनल एक्सेस** - ब्राउज़र जैसी टैब प्रणाली के साथ स्प्लिट-स्क्रीन सपोर्ट (4 पैनल तक) वाला पूर्ण-विशेषता वाला टर्मिनल। इसमें लोकप्रिय टर्मिनल थीम, फ़ॉन्ट और अन्य कंपोनेंट सहित टर्मिनल को कस्टमाइज़ करने का सपोर्ट शामिल है
- **SSH टनल प्रबंधन** - ऑटोमैटिक रीकनेक्शन और हेल्थ मॉनिटरिंग के साथ SSH टनल बनाएँ और प्रबंधित करें, -l या -r कनेक्शन के सपोर्ट के साथ
- **रिमोट फ़ाइल मैनेजर** - कोड, इमेज, ऑडियो और वीडियो देखने और संपादित करने के सपोर्ट के साथ रिमोट सर्वर पर सीधे फ़ाइलें प्रबंधित करें। sudo सपोर्ट के साथ फ़ाइलें अपलोड, डाउनलोड, रीनेम, डिलीट और मूव करें
- **Docker प्रबंधन** - कंटेनर शुरू, बंद, पॉज़, हटाएँ। कंटेनर स्टैट्स देखें। docker exec टर्मिनल का उपयोग करके कंटेनर को नियंत्रित करें। इसे Portainer या Dockge की जगह लेने के लिए नहीं बनाया गया बल्कि कंटेनर बनाने की तुलना में उन्हें सरलता से प्रबंधित करने के लिए बनाया गया है
- **SSH होस्ट मैनेजर** - टैग और फ़ोल्डर के साथ अपने SSH कनेक्शन सहेजें, व्यवस्थित करें और प्रबंधित करें, और SSH कुंजियों की तैनाती को स्वचालित करने की क्षमता के साथ पुन: उपयोग योग्य लॉगिन जानकारी आसानी से सहेजें
- **सर्वर आँकड़े** - अधिकांश Linux आधारित सर्वर पर नेटवर्क, अपटाइम, सिस्टम जानकारी, फ़ायरवॉल, पोर्ट मॉनिटर के साथ CPU, मेमोरी और डिस्क उपयोग देखें
- **डैशबोर्ड** - अपने डैशबोर्ड पर एक नज़र में सर्वर की जानकारी देखें
- **RBAC** - भूमिकाएँ बनाएँ और उपयोगकर्ताओं/भूमिकाओं में होस्ट साझा करें
- **उपयोगकर्ता प्रमाणीकरण** - व्यवस्थापक नियंत्रण और OIDC और 2FA (TOTP) सपोर्ट के साथ सुरक्षित उपयोगकर्ता प्रबंधन। सभी प्लेटफ़ॉर्म पर सक्रिय उपयोगकर्ता सत्र देखें और अनुमतियाँ रद्द करें। अपने OIDC/स्थानीय खातों को एक साथ जोड़ें
- **डेटाबेस एन्क्रिप्शन** - बैकएंड एन्क्रिप्टेड SQLite डेटाबेस फ़ाइलों के रूप में संग्रहीत। अधिक जानकारी के लिए [डॉक्स](https://docs.termix.site/security) देखें
- **डेटा एक्सपोर्ट/इम्पोर्ट** - SSH होस्ट, क्रेडेंशियल और फ़ाइल मैनेजर डेटा एक्सपोर्ट और इम्पोर्ट करें
- **स्वचालित SSL सेटअप** - HTTPS रीडायरेक्ट के साथ बिल्ट-इन SSL सर्टिफ़िकेट जनरेशन और प्रबंधन
- **आधुनिक UI** - React, Tailwind CSS, और Shadcn से बना साफ़ डेस्कटॉप/मोबाइल-फ़्रेंडली इंटरफ़ेस। डार्क या लाइट मोड UI के बीच चुनें। किसी भी कनेक्शन को फ़ुल-स्क्रीन में खोलने के लिए URL रूट का उपयोग करें
- **भाषाएँ** - लगभग 30 भाषाओं का बिल्ट-इन सपोर्ट ([Crowdin](https://docs.termix.site/translations) द्वारा प्रबंधित)
- **प्लेटफ़ॉर्म सपोर्ट** - वेब ऐप, डेस्कटॉप एप्लिकेशन (Windows, Linux, और macOS), PWA, और iOS और Android के लिए समर्पित मोबाइल/टैबलेट ऐप के रूप में उपलब्ध
- **SSH टूल्स** - एक क्लिक से निष्पादित होने वाले पुन: उपयोग योग्य कमांड स्निपेट बनाएँ। एक साथ कई खुले टर्मिनलों में एक कमांड चलाएँ
- **कमांड इतिहास** - पहले चलाए गए SSH कमांड का ऑटो-कम्प्लीट और दृश्य
- **क्विक कनेक्ट** - कनेक्शन डेटा सहेजे बिना सर्वर से कनेक्ट करें
- **कमांड पैलेट** - अपने कीबोर्ड से SSH कनेक्शन तक त्वरित पहुँच के लिए बाएँ Shift को दो बार टैप करें
- **SSH सुविधाओं से भरपूर** - जम्प होस्ट, Warpgate, TOTP आधारित कनेक्शन, SOCKS5, होस्ट की वेरिफ़िकेशन, पासवर्ड ऑटोफ़िल, [OPKSSH](https://github.com/openpubkey/opkssh) आदि का सपोर्ट
- **नेटवर्क ग्राफ़** - स्थिति सपोर्ट के साथ अपने SSH कनेक्शन के आधार पर अपने होमलैब को विज़ुअलाइज़ करने के लिए अपना डैशबोर्ड कस्टमाइज़ करें
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# नियोजित विशेषताएँ
सभी नियोजित विशेषताओं के लिए [प्रोजेक्ट्स](https://github.com/orgs/Termix-SSH/projects/2) देखें। यदि आप योगदान देना चाहते हैं, तो [योगदान](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) देखें।
# इंस्टॉलेशन
समर्थित डिवाइस:
- वेबसाइट (किसी भी प्लेटफ़ॉर्म पर कोई भी आधुनिक ब्राउज़र जैसे Chrome, Safari, और Firefox) (PWA सपोर्ट सहित)
- Windows (x64/ia32)
- पोर्टेबल
- MSI इंस्टॉलर
- Chocolatey पैकेज मैनेजर
- Linux (x64/ia32)
- पोर्टेबल
- AUR
- AppImage
- Deb
- Flatpak
- macOS (v12.0+ पर x64/ia32)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (v15.1+)
- Apple App Store
- IPA
- Android (v7.0+)
- Google Play Store
- APK
सभी प्लेटफ़ॉर्म पर Termix इंस्टॉल करने के बारे में अधिक जानकारी के लिए Termix [डॉक्स](https://docs.termix.site/install) पर जाएँ। अन्यथा, यहाँ एक नमूना Docker Compose फ़ाइल देखें:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# प्रायोजक
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# सहायता
यदि आपको सहायता चाहिए या Termix के लिए किसी विशेषता का अनुरोध करना चाहते हैं, तो [इश्यूज़](https://github.com/Termix-SSH/Support/issues) पेज पर जाएँ, लॉग इन करें, और `New Issue` दबाएँ।
कृपया अपने इश्यू में यथासंभव विस्तृत विवरण दें, अधिमानतः अंग्रेज़ी में लिखें। आप [Discord](https://discord.gg/jVQGdvHDrf) सर्वर में भी शामिल हो सकते हैं और सहायता चैनल पर जा सकते हैं, हालाँकि, प्रतिक्रिया समय अधिक हो सकता है।
# स्क्रीनशॉट
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
कुछ वीडियो और छवियाँ पुरानी हो सकती हैं या विशेषताओं को पूरी तरह से प्रदर्शित नहीं कर सकती हैं।
# लाइसेंस
Apache License Version 2.0 के तहत वितरित। अधिक जानकारी के लिए LICENSE देखें।
+187
View File
@@ -0,0 +1,187 @@
# Statistiche Repo
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">Ottenuto il 1 settembre 2025</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Se lo desideri, puoi supportare il progetto qui!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Panoramica
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix è una piattaforma di gestione server tutto-in-uno, open-source, per sempre gratuita e self-hosted. Fornisce una soluzione multipiattaforma per gestire i tuoi server e la tua infrastruttura attraverso un'unica interfaccia intuitiva. Termix offre accesso al terminale SSH, funzionalità di tunneling SSH, gestione remota dei file e molti altri strumenti. Termix è la perfetta alternativa gratuita e self-hosted a Termius, disponibile per tutte le piattaforme.
# Funzionalità
- **Accesso Terminale SSH** - Terminale completo con supporto schermo divIPA (fino a 4 pannelli) con un sistema di schede in stile browser. Include il supporto per la personalizzazione del terminale, inclusi temi, font e altri componenti comuni
- **Gestione Tunnel SSH** - Crea e gestisci tunnel SSH con riconnessione automatica e monitoraggio dello stato, con supporto per connessioni -l o -r
- **Gestore File Remoto** - Gestisci i file direttamente sui server remoti con supporto per la visualizzazione e la modifica di codice, immagini, audio e video. Carica, scarica, rinomina, elimina e sposta file senza problemi con supporto sudo.
- **Gestione Docker** - Avvia, ferma, metti in pausa, rimuovi container. Visualizza le statistiche dei container. Controlla i container tramite terminale docker exec. Non è stato creato per sostituire Portainer o Dockge, ma piuttosto per gestire semplicemente i tuoi container rispetto alla loro creazione.
- **Gestore Host SSH** - Salva, organizza e gestisci le tue connessioni SSH con tag e cartelle, salva facilmente le informazioni di accesso riutilizzabili e automatizza il deployment delle chiavi SSH
- **Statistiche Server** - Visualizza l'utilizzo di CPU, memoria e disco insieme a rete, uptime, informazioni di sistema, firewall, monitoraggio porte sulla maggior parte dei server basati su Linux
- **Dashboard** - Visualizza le informazioni del server a colpo d'occhio sulla tua dashboard
- **RBAC** - Crea ruoli e condividi host tra utenti/ruoli
- **Autenticazione Utente** - Gestione utenti sicura con controlli amministrativi e supporto OIDC e 2FA (TOTP). Visualizza le sessioni utente attive su tutte le piattaforme e revoca i permessi. Collega i tuoi account OIDC/Locali tra loro.
- **Crittografia Database** - Il backend è archiviato come file di database SQLite crittografati. Consulta la [documentazione](https://docs.termix.site/security) per maggiori informazioni.
- **Esportazione/Importazione Dati** - Esporta e importa host SSH, credenziali e dati del gestore file
- **Configurazione SSL Automatica** - Generazione e gestione integrata dei certificati SSL con reindirizzamenti HTTPS
- **Interfaccia Moderna** - Interfaccia pulita e responsive per desktop/mobile costruita con React, Tailwind CSS e Shadcn. Scegli tra modalità scura o chiara. Usa i percorsi URL per aprire qualsiasi connessione a schermo intero.
- **Lingue** - Supporto integrato per ~30 lingue (gestito da [Crowdin](https://docs.termix.site/translations))
- **Supporto Piattaforme** - Disponibile come app web, applicazione desktop (Windows, Linux e macOS), PWA e app dedicata per mobile/tablet su iOS e Android.
- **Strumenti SSH** - Crea snippet di comandi riutilizzabili che si eseguono con un singolo clic. Esegui un comando simultaneamente su più terminali aperti.
- **Cronologia Comandi** - Autocompletamento e visualizzazione dei comandi SSH eseguiti in precedenza
- **Connessione Rapida** - Connettiti a un server senza dover salvare i dati di connessione
- **Palette Comandi** - Premi due volte shift sinistro per accedere rapidamente alle connessioni SSH con la tastiera
- **SSH Ricco di Funzionalità** - Supporta jump host, Warpgate, connessioni basate su TOTP, SOCKS5, verifica chiave host, compilazione automatica password, [OPKSSH](https://github.com/openpubkey/opkssh), ecc.
- **Grafico di Rete** - Personalizza la tua Dashboard per visualizzare il tuo homelab basato sulle connessioni SSH con supporto dello stato
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# Funzionalità Pianificate
Consulta [Progetti](https://github.com/orgs/Termix-SSH/projects/2) per tutte le funzionalità pianificate. Se desideri contribuire, consulta [Contribuire](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
# Installazione
Dispositivi Supportati:
- Sito web (qualsiasi browser moderno su qualsiasi piattaforma come Chrome, Safari e Firefox) (include supporto PWA)
- Windows (x64/ia32)
- Portable
- MSI Installer
- Chocolatey Package Manager
- Linux (x64/ia32)
- Portable
- AUR
- AppImage
- Deb
- Flatpak
- macOS (x64/ia32 su v12.0+)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (v15.1+)
- Apple App Store
- IPA
- Android (v7.0+)
- Google Play Store
- APK
Visita la [Documentazione](https://docs.termix.site/install) di Termix per maggiori informazioni su come installare Termix su tutte le piattaforme. In alternativa, visualizza un file Docker Compose di esempio qui:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# Sponsor
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# Supporto
Se hai bIPAgno di aiuto o vuoi richiedere una funzionalità per Termix, visita la pagina [Segnalazioni](https://github.com/Termix-SSH/Support/issues), accedi e premi `New Issue`.
Per favore, sii il più dettagliato possibile nella tua segnalazione, preferibilmente scritta in inglese. Puoi anche unirti al server [Discord](https://discord.gg/jVQGdvHDrf) e visitare il canale di supporto, tuttavia i tempi di risposta potrebbero essere più lunghi.
# Screenshot
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Alcuni video e immagini potrebbero non essere aggiornati o potrebbero non mostrare perfettamente le funzionalità.
# Licenza
Distribuito sotto la Licenza Apache Versione 2.0. Consulta LICENSE per maggiori informazioni.
+186
View File
@@ -0,0 +1,186 @@
# リポジトリ統計
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語 ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">2025年9月1日に達成</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
プロジェクトを支援していただける方はこちらからどうぞ!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# 概要
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termixは、オープンソースで永久無料のセルフホスト型オールインワンサーバー管理プラットフォームです。単一の直感的なインターフェースを通じて、サーバーとインフラストラクチャを管理するマルチプラットフォームソリューションを提供します。Termixは、SSHターミナルアクセス、SSHトンネリング機能、リモートファイル管理、その他多くのツールを提供します。Termixは、すべてのプラットフォームで利用可能なTermiusの完全無料でセルフホスト可能な代替ソリューションです。
# 機能
- **SSHターミナルアクセス** - ブラウザ風タブシステムによる分割画面対応(最大4パネル)のフル機能ターミナル。一般的なターミナルテーマ、フォント、その他のコンポーネントを含むターミナルカスタマイズに対応
- **SSHトンネル管理** - 自動再接続とヘルスモニタリング機能を備えたSSHトンネルの作成・管理、-l または -r 接続に対応
- **リモートファイルマネージャー** - コード、画像、音声、動画の表示・編集に対応し、リモートサーバー上のファイルを直接管理。sudo対応でファイルのアップロード、ダウンロード、名前変更、削除、移動をシームレスに実行
- **Docker管理** - コンテナの起動、停止、一時停止、削除。コンテナの統計情報を表示。docker execターミナルでコンテナを操作。PortainerやDockgeの代替ではなく、コンテナの作成よりも簡易管理を目的としています
- **SSHホストマネージャー** - タグやフォルダでSSH接続を保存、整理、管理し、再利用可能なログイン情報を簡単に保存しながらSSHキーのデプロイを自動化
- **サーバー統計** - ほとんどのLinuxベースのサーバーで、CPU、メモリ、ディスク使用量、ネットワーク、アップタイム、システム情報、ファイアウォール、ポートモニターを表示
- **ダッシュボード** - ダッシュボードでサーバー情報を一目で確認
- **RBAC** - ロールを作成し、ユーザー/ロール間でホストを共有
- **ユーザー認証** - 管理者コントロールとOIDCおよび2FA(TOTP)対応による安全なユーザー管理。すべてのプラットフォームでアクティブなユーザーセッションを表示し、権限を取り消し可能。OIDC/ローカルアカウントの連携
- **データベース暗号化** - バックエンドは暗号化されたSQLiteデータベースファイルとして保存。詳細は[ドキュメント](https://docs.termix.site/security)をご覧ください
- **データのエクスポート/インポート** - SSHホスト、認証情報、ファイルマネージャーデータのエクスポートとインポート
- **自動SSL設定** - HTTPSリダイレクト付きの組み込みSSL証明書生成・管理
- **モダンUI** - React、Tailwind CSS、Shadcnで構築された、デスクトップ/モバイル対応のクリーンなインターフェース。ダーク/ライトモードの切り替え対応。URLルートで任意の接続をフルスクリーンで開くことが可能
- **多言語対応** - 約30言語の組み込みサポート([Crowdin](https://docs.termix.site/translations)で管理)
- **プラットフォーム対応** - Webアプリ、デスクトップアプリケーション(Windows、Linux、macOS)、PWA、iOS・Android専用モバイル/タブレットアプリとして利用可能
- **SSHツール** - ワンクリックで実行できる再利用可能なコマンドスニペットの作成。複数の開いているターミナルに対して同時にコマンドを実行
- **コマンド履歴** - 過去に実行したSSHコマンドの自動補完と表示
- **クイック接続** - 接続データを保存せずにサーバーに接続
- **コマンドパレット** - 左Shiftキーを2回押すことで、キーボードからSSH接続に素早くアクセス
- **SSH機能充実** - ジャンプホスト、Warpgate、TOTPベースの接続、SOCKS5、ホストキー検証、パスワード自動入力、[OPKSSH](https://github.com/openpubkey/opkssh)などに対応
- **ネットワークグラフ** - ダッシュボードをカスタマイズして、SSH接続に基づくホームラボのネットワークをステータス表示付きで可視化
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# 予定されている機能
すべての予定機能については[Projects](https://github.com/orgs/Termix-SSH/projects/2)をご覧ください。コントリビュートをご希望の方は[Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)をご覧ください。
# インストール
対応デバイス:
- Webサイト(Chrome、Safari、Firefoxなど、あらゆるプラットフォームのモダンブラウザ)(PWA対応)
- Windows (x64/ia32)
- ポータブル版
- MSIインストーラー
- Chocolateyパッケージマネージャー
- Linux (x64/ia32)
- ポータブル版
- AUR
- AppImage
- Deb
- Flatpak
- macOS (x64/ia32、v12.0以降)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (v15.1以降)
- Apple App Store
- IPA
- Android (v7.0以降)
- Google Play Store
- APK
すべてのプラットフォームへのTermixのインストール方法については、Termixの[ドキュメント](https://docs.termix.site/install)をご覧ください。以下はDocker Composeファイルのサンプルです:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# スポンサー
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# サポート
Termixに関するヘルプや機能リクエストが必要な場合は、[Issues](https://github.com/Termix-SSH/Support/issues)ページにアクセスし、ログインして`New Issue`を押してください。Issueはできるだけ詳細に記述し、英語での記述が望ましいです。また、[Discord](https://discord.gg/jVQGdvHDrf)サーバーに参加してサポートチャンネルを利用することもできますが、応答時間が長くなる場合があります。
# スクリーンショット
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
一部の動画や画像は古い場合や、機能を完全に紹介していない場合があります。
# ライセンス
Apache License Version 2.0のもとで配布されています。詳細はLICENSEをご覧ください。
+186
View File
@@ -0,0 +1,186 @@
# 리포지토리 통계
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어 ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">2025년 9월 1일 달성</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
프로젝트를 후원하고 싶으시다면 여기에서 지원해 주세요!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# 개요
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버 관리 플랫폼입니다. 단일 직관적인 인터페이스를 통해 서버와 인프라를 관리할 수 있는 멀티 플랫폼 솔루션을 제공합니다. Termix는 SSH 터미널 접속, SSH 터널링 기능, 원격 파일 관리 및 기타 다양한 도구를 제공합니다. Termix는 모든 플랫폼에서 사용 가능한 Termius의 완벽한 무료 셀프 호스팅 대안입니다.
# 기능
- **SSH 터미널 접속** - 브라우저 스타일 탭 시스템과 분할 화면 지원(최대 4개 패널)을 갖춘 완전한 기능의 터미널. 일반 터미널 테마, 글꼴 및 기타 구성 요소를 포함한 터미널 사용자 정의 지원
- **SSH 터널 관리** - 자동 재연결 및 상태 모니터링 기능을 갖춘 SSH 터널 생성 및 관리, -l 또는 -r 연결 지원
- **원격 파일 관리자** - 코드, 이미지, 오디오, 비디오의 보기 및 편집을 지원하여 원격 서버에서 파일을 직접 관리. sudo 지원으로 파일 업로드, 다운로드, 이름 변경, 삭제, 이동을 원활하게 수행
- **Docker 관리** - 컨테이너 시작, 중지, 일시 정지, 제거. 컨테이너 통계 보기. docker exec 터미널로 컨테이너 제어. Portainer나 Dockge를 대체하기 위한 것이 아니라 컨테이너 생성보다는 간편한 관리를 목적으로 합니다
- **SSH 호스트 관리자** - 태그와 폴더로 SSH 연결을 저장, 정리, 관리하고, 재사용 가능한 로그인 정보를 쉽게 저장하면서 SSH 키 배포를 자동화
- **서버 통계** - 대부분의 Linux 기반 서버에서 CPU, 메모리, 디스크 사용량과 함께 네트워크, 업타임, 시스템 정보, 방화벽, 포트 모니터를 표시
- **대시보드** - 대시보드에서 서버 정보를 한눈에 확인
- **RBAC** - 역할을 생성하고 사용자/역할 간에 호스트 공유
- **사용자 인증** - 관리자 제어와 OIDC 및 2FA(TOTP) 지원을 통한 안전한 사용자 관리. 모든 플랫폼에서 활성 사용자 세션을 보고 권한을 취소 가능. OIDC/로컬 계정 연동
- **데이터베이스 암호화** - 백엔드가 암호화된 SQLite 데이터베이스 파일로 저장됨. 자세한 내용은 [문서](https://docs.termix.site/security)를 참조하세요
- **데이터 내보내기/가져오기** - SSH 호스트, 자격 증명, 파일 관리자 데이터의 내보내기 및 가져오기
- **자동 SSL 설정** - HTTPS 리디렉션을 포함한 내장 SSL 인증서 생성 및 관리
- **모던 UI** - React, Tailwind CSS, Shadcn으로 구축된 깔끔한 데스크톱/모바일 친화적 인터페이스. 다크 또는 라이트 모드 기반 UI 선택. URL 라우트를 사용하여 모든 연결을 전체 화면으로 열기 가능
- **다국어 지원** - 약 30개 언어 내장 지원([Crowdin](https://docs.termix.site/translations)으로 관리)
- **플랫폼 지원** - 웹 앱, 데스크톱 애플리케이션(Windows, Linux, macOS), PWA, iOS 및 Android 전용 모바일/태블릿 앱으로 제공
- **SSH 도구** - 한 번의 클릭으로 실행 가능한 재사용 가능 명령어 스니펫 생성. 여러 열린 터미널에서 동시에 하나의 명령어 실행
- **명령어 기록** - 이전에 실행한 SSH 명령어의 자동 완성 및 조회
- **빠른 연결** - 연결 데이터를 저장하지 않고 서버에 접속
- **명령어 팔레트** - 왼쪽 Shift 키를 두 번 눌러 키보드로 SSH 연결에 빠르게 접근
- **풍부한 SSH 기능** - 점프 호스트, Warpgate, TOTP 기반 연결, SOCKS5, 호스트 키 검증, 비밀번호 자동 입력, [OPKSSH](https://github.com/openpubkey/opkssh) 등 지원
- **네트워크 그래프** - 대시보드를 사용자 정의하여 SSH 연결 기반의 홈랩 네트워크를 상태 표시와 함께 시각화
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# 계획된 기능
모든 계획된 기능은 [Projects](https://github.com/orgs/Termix-SSH/projects/2)를 참조하세요. 기여를 원하시면 [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)을 참조하세요.
# 설치
지원 기기:
- 웹사이트 (Chrome, Safari, Firefox 등 모든 플랫폼의 최신 브라우저) (PWA 지원 포함)
- Windows (x64/ia32)
- 포터블
- MSI 설치 프로그램
- Chocolatey 패키지 관리자
- Linux (x64/ia32)
- 포터블
- AUR
- AppImage
- Deb
- Flatpak
- macOS (x64/ia32, v12.0 이상)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (v15.1 이상)
- Apple App Store
- IPA
- Android (v7.0 이상)
- Google Play Store
- APK
모든 플랫폼에 Termix를 설치하는 방법에 대한 자세한 내용은 Termix [문서](https://docs.termix.site/install)를 방문하세요. 다음은 Docker Compose 파일 예시입니다:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# 스폰서
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# 지원
Termix에 대한 도움이 필요하거나 기능을 요청하려면 [Issues](https://github.com/Termix-SSH/Support/issues) 페이지를 방문하여 로그인하고 `New Issue`를 누르세요. 이슈는 가능한 한 상세하게 작성하고, 영어로 작성하는 것이 좋습니다. [Discord](https://discord.gg/jVQGdvHDrf) 서버에 참여하여 지원 채널을 이용할 수도 있지만, 응답 시간이 더 길 수 있습니다.
# 스크린샷
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
일부 비디오 및 이미지는 최신이 아니거나 기능을 완벽하게 보여주지 않을 수 있습니다.
# 라이선스
Apache License Version 2.0에 따라 배포됩니다. 자세한 내용은 LICENSE를 참조하세요.
+187
View File
@@ -0,0 +1,187 @@
# Estatísticas do Repositório
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">Conquistado em 1 de setembro de 2025</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Se desejar, você pode apoiar o projeto aqui!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Visão Geral
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código aberto, sempre gratuita e auto-hospedada. Ela fornece uma solução multiplataforma para gerenciar seus servidores e infraestrutura através de uma interface única e intuitiva. Termix oferece acesso a terminal SSH, capacidades de tunelamento SSH, gerenciamento remoto de arquivos e muitas outras ferramentas. Termix é a alternativa perfeita, gratuita e auto-hospedada ao Termius, disponível para todas as plataformas.
# Funcionalidades
- **Acesso ao Terminal SSH** - Terminal completo com suporte a tela dividida (até 4 painéis) com um sistema de abas similar ao navegador. Inclui suporte para personalização do terminal incluindo temas comuns de terminal, fontes e outros componentes
- **Gerenciamento de Túneis SSH** - Crie e gerencie túneis SSH com reconexão automática e monitoramento de saúde, com suporte para conexões -l ou -r
- **Gerenciador Remoto de Arquivos** - Gerencie arquivos diretamente em servidores remotos com suporte para visualizar e editar código, imagens, áudio e vídeo. Faça upload, download, renomeie, exclua e mova arquivos facilmente com suporte sudo.
- **Gerenciamento de Docker** - Inicie, pare, pause, remova contêineres. Visualize estatísticas de contêineres. Controle contêineres usando o terminal Docker Exec. Não foi feito para substituir Portainer ou Dockge, mas sim para simplesmente gerenciar seus contêineres em vez de criá-los.
- **Gerenciador de Hosts SSH** - Salve, organize e gerencie suas conexões SSH com tags e pastas, e salve facilmente informações de login reutilizáveis com a capacidade de automatizar a implantação de chaves SSH
- **Estatísticas do Servidor** - Visualize o uso de CPU, memória e disco junto com rede, tempo de atividade, informações do sistema, firewall, monitor de portas na maioria dos servidores baseados em Linux
- **Dashboard** - Visualize informações do servidor de relance no seu dashboard
- **RBAC** - Crie funções e compartilhe hosts entre usuários/funções
- **Autenticação de Usuários** - Gerenciamento seguro de usuários com controles de administrador e suporte para OIDC e 2FA (TOTP). Visualize sessões ativas de usuários em todas as plataformas e revogue permissões. Vincule suas contas OIDC/Locais entre si.
- **Criptografia de Banco de Dados** - Backend armazenado como arquivos de banco de dados SQLite criptografados. Consulte a [documentação](https://docs.termix.site/security) para mais informações.
- **Exportação/Importação de Dados** - Exporte e importe hosts SSH, credenciais e dados do gerenciador de arquivos
- **Configuração Automática de SSL** - Geração e gerenciamento integrado de certificados SSL com redirecionamentos HTTPS
- **Interface Moderna** - Interface limpa compatível com desktop/mobile construída com React, Tailwind CSS e Shadcn. Escolha entre modo escuro ou claro. Use rotas de URL para abrir qualquer conexão em tela cheia.
- **Idiomas** - Suporte integrado para ~30 idiomas (gerenciado pelo [Crowdin](https://docs.termix.site/translations))
- **Suporte a Plataformas** - Disponível como aplicação web, aplicação desktop (Windows, Linux e macOS), PWA e aplicativo dedicado para celular/tablet para iOS e Android.
- **Ferramentas SSH** - Crie trechos de comandos reutilizáveis que são executados com um único clique. Execute um comando simultaneamente em múltiplos terminais abertos.
- **Histórico de Comandos** - Autocompletar e visualizar comandos SSH executados anteriormente
- **Conexão Rápida** - Conecte-se a um servidor sem precisar salvar os dados de conexão
- **Paleta de Comandos** - Pressione duas vezes a tecla Shift esquerda para acessar rapidamente as conexões SSH com seu teclado
- **SSH Rico em Funcionalidades** - Suporta jump hosts, Warpgate, conexões baseadas em TOTP, SOCKS5, verificação de chave do host, preenchimento automático de senhas, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
- **Gráfico de Rede** - Personalize seu Dashboard para visualizar seu homelab baseado nas suas conexões SSH com suporte de status
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# Funcionalidades Planejadas
Consulte [Projetos](https://github.com/orgs/Termix-SSH/projects/2) para todas as funcionalidades planejadas. Se você deseja contribuir, consulte [Contribuir](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
# Instalação
Dispositivos suportados:
- Website (qualquer navegador moderno em qualquer plataforma como Chrome, Safari e Firefox) (inclui suporte PWA)
- Windows (x64/ia32)
- Portátil
- Instalador MSI
- Gerenciador de pacotes Chocolatey
- Linux (x64/ia32)
- Portátil
- AUR
- AppImage
- Deb
- Flatpak
- macOS (x64/ia32 em v12.0+)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (v15.1+)
- Apple App Store
- IPA
- Android (v7.0+)
- Google Play Store
- APK
Visite a [documentação](https://docs.termix.site/install) do Termix para mais informações sobre como instalar o Termix em todas as plataformas. Caso contrário, veja um arquivo Docker Compose de exemplo aqui:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# Patrocinadores
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# Suporte
Se você precisa de ajuda ou deseja solicitar uma funcionalidade para o Termix, visite a página de [Issues](https://github.com/Termix-SSH/Support/issues), faça login e clique em `New Issue`.
Por favor, seja o mais detalhado possível no seu relato, preferencialmente escrito em inglês. Você também pode entrar no servidor do [Discord](https://discord.gg/jVQGdvHDrf) e visitar o canal de suporte, porém, os tempos de resposta podem ser mais longos.
# Capturas de Tela
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Alguns vídeos e imagens podem estar desatualizados ou podem não mostrar perfeitamente as funcionalidades.
# Licença
Distribuído sob a Licença Apache Versão 2.0. Consulte LICENSE para mais informações.
+187
View File
@@ -0,0 +1,187 @@
# Статистика репозитория
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">Достигнуто 1 сентября 2025 года</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Если хотите, вы можете поддержать проект здесь!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Обзор
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix — это платформа для управления серверами с открытым исходным кодом, навсегда бесплатная и размещаемая на собственном сервере. Она предоставляет мультиплатформенное решение для управления вашими серверами и инфраструктурой через единый интуитивно понятный интерфейс. Termix предлагает доступ к SSH-терминалу, возможности SSH-туннелирования, удалённое управление файлами и множество других инструментов. Termix — это идеальная бесплатная альтернатива Termius с возможностью размещения на собственном сервере, доступная для всех платформ.
# Возможности
- **Доступ к SSH-терминалу** — Полнофункциональный терминал с поддержкой разделения экрана (до 4 панелей) и системой вкладок, как в браузере. Включает поддержку настройки терминала, включая популярные темы, шрифты и другие компоненты
- **Управление SSH-туннелями** — Создание и управление SSH-туннелями с автоматическим переподключением и мониторингом состояния, с поддержкой соединений -l и -r
- **Удалённый файловый менеджер** — Управление файлами непосредственно на удалённых серверах с поддержкой просмотра и редактирования кода, изображений, аудио и видео. Загрузка, скачивание, переименование, удаление и перемещение файлов с поддержкой sudo
- **Управление Docker** — Запуск, остановка, приостановка, удаление контейнеров. Просмотр статистики контейнеров. Управление контейнером через терминал docker exec. Не предназначен для замены Portainer или Dockge, а скорее для простого управления контейнерами по сравнению с их созданием
- **Менеджер SSH-хостов** — Сохранение, организация и управление SSH-подключениями с помощью тегов и папок, с возможностью сохранения данных для повторного входа и автоматизации развёртывания SSH-ключей
- **Статистика сервера** — Просмотр использования CPU, памяти и диска, а также сети, времени работы, информации о системе, файрвола и монитора портов на большинстве серверов на базе Linux
- **Панель управления** — Просмотр информации о сервере на панели управления одним взглядом
- **RBAC** — Создание ролей и предоставление общего доступа к хостам для пользователей/ролей
- **Аутентификация пользователей** — Безопасное управление пользователями с административным контролем и поддержкой OIDC и 2FA (TOTP). Просмотр активных сессий пользователей на всех платформах и отзыв прав доступа. Связывание аккаунтов OIDC/локальных аккаунтов
- **Шифрование базы данных** — Бэкенд хранится в виде зашифрованных файлов базы данных SQLite. Подробнее в [документации](https://docs.termix.site/security)
- **Экспорт/импорт данных** — Экспорт и импорт SSH-хостов, учётных данных и данных файлового менеджера
- **Автоматическая настройка SSL** — Встроенная генерация и управление SSL-сертификатами с перенаправлением на HTTPS
- **Современный интерфейс** — Чистый интерфейс для десктопа и мобильных устройств, построенный на React, Tailwind CSS и Shadcn. Выбор между тёмной и светлой темой. Использование URL-маршрутов для открытия любого подключения в полноэкранном режиме
- **Языки** — Встроенная поддержка ~30 языков (управляется через [Crowdin](https://docs.termix.site/translations))
- **Поддержка платформ** — Доступен как веб-приложение, настольное приложение (Windows, Linux и macOS), PWA и специализированное мобильное/планшетное приложение для iOS и Android
- **Инструменты SSH** — Создание переиспользуемых фрагментов команд, выполняемых одним нажатием. Запуск одной команды одновременно в нескольких открытых терминалах
- **История команд** — Автодополнение и просмотр ранее выполненных SSH-команд
- **Быстрое подключение** — Подключение к серверу без необходимости сохранения данных подключения
- **Командная палитра** — Двойное нажатие левого Shift для быстрого доступа к SSH-подключениям с клавиатуры
- **Богатый функционал SSH** — Поддержка jump-хостов, Warpgate, подключений на основе TOTP, SOCKS5, верификации ключей хоста, автозаполнения паролей, [OPKSSH](https://github.com/openpubkey/opkssh) и др.
- **Сетевой граф** — Настройте панель управления для визуализации вашей домашней лаборатории на основе SSH-подключений с поддержкой статусов
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# Запланированные функции
Смотрите [Проекты](https://github.com/orgs/Termix-SSH/projects/2) для просмотра всех запланированных функций. Если вы хотите внести вклад, смотрите [Участие в разработке](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
# Установка
Поддерживаемые устройства:
- Веб-сайт (любой современный браузер на любой платформе, включая Chrome, Safari и Firefox) (включая поддержку PWA)
- Windows (x64/ia32)
- Портативная версия
- Установщик MSI
- Менеджер пакетов Chocolatey
- Linux (x64/ia32)
- Портативная версия
- AUR
- AppImage
- Deb
- Flatpak
- macOS (x64/ia32, версия 12.0+)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (версия 15.1+)
- Apple App Store
- IPA
- Android (версия 7.0+)
- Google Play Store
- APK
Посетите [документацию](https://docs.termix.site/install) Termix для получения дополнительной информации об установке Termix на всех платформах. Также вы можете ознакомиться с примером файла Docker Compose здесь:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# Спонсоры
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# Поддержка
Если вам нужна помощь или вы хотите запросить новую функцию для Termix, посетите страницу [Проблемы](https://github.com/Termix-SSH/Support/issues), войдите в систему и нажмите `New Issue`.
Пожалуйста, опишите вашу проблему как можно подробнее, предпочтительно на английском языке. Вы также можете присоединиться к серверу [Discord](https://discord.gg/jVQGdvHDrf) и обратиться в канал поддержки, однако время ответа может быть дольше.
# Скриншоты
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Некоторые видео и изображения могут быть устаревшими или не полностью отражать функциональность.
# Лицензия
Распространяется по лицензии Apache License Version 2.0. Подробнее см. в файле LICENSE.
+187
View File
@@ -0,0 +1,187 @@
# Repo İstatistikleri
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">1 Eylül 2025'te kazanıldı</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Projeyi desteklemek isterseniz, buradan destek olabilirsiniz!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Genel Bakış
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındırabileceğiniz hepsi bir arada sunucu yönetim platformudur. Sunucularınızı ve altyapınızı tek bir sezgisel arayüz üzerinden yönetmek için çok platformlu bir çözüm sunar. Termix, SSH terminal erişimi, SSH tünelleme yetenekleri, uzak dosya yönetimi ve daha birçok araç sağlar. Termix, tüm platformlarda kullanılabilen Termius'un mükemmel ücretsiz ve kendi barındırmalı alternatifidir.
# Özellikler
- **SSH Terminal Erişimi** - Tarayıcı benzeri sekme sistemiyle bölünmüş ekran desteğine sahip (4 panele kadar) tam özellikli terminal. Yaygın terminal temaları, yazı tipleri ve diğer bileşenler dahil olmak üzere terminal özelleştirme desteği içerir
- **SSH Tünel Yönetimi** - Otomatik yeniden bağlanma ve sağlık izleme ile SSH tünelleri oluşturun ve yönetin, -l veya -r bağlantıları desteğiyle
- **Uzak Dosya Yöneticisi** - Uzak sunuculardaki dosyaları doğrudan yönetin; kod, görüntü, ses ve video görüntüleme ve düzenleme desteğiyle. Sudo desteğiyle dosyaları sorunsuzca yükleyin, indirin, yeniden adlandırın, silin ve taşıyın.
- **Docker Yönetimi** - Konteynerleri başlatın, durdurun, duraklatın, kaldırın. Konteyner istatistiklerini görüntüleyin. Docker exec terminali kullanarak konteyneri kontrol edin. Portainer veya Dockge'nin yerini almak için değil, konteynerlerinizi oluşturmak yerine basitçe yönetmek için tasarlanmıştır.
- **SSH Ana Bilgisayar Yöneticisi** - SSH bağlantılarınızı etiketler ve klasörlerle kaydedin, düzenleyin ve yönetin; yeniden kullanılabilir giriş bilgilerini kolayca kaydedin ve SSH anahtarlarının dağıtımını otomatikleştirin
- **Sunucu İstatistikleri** - Çoğu Linux tabanlı sunucularda CPU, bellek ve disk kullanımını ağ, çalışma süresi, sistem bilgisi, güvenlik duvarı, port izleme ile birlikte görüntüleyin
- **Kontrol Paneli** - Kontrol panelinizde sunucu bilgilerini bir bakışta görüntüleyin
- **RBAC** - Roller oluşturun ve ana bilgisayarları kullanıcılar/roller arasında paylaşın
- **Kullanıcı Kimlik Doğrulama** - Yönetici kontrolleri, OIDC ve 2FA (TOTP) desteğiyle güvenli kullanıcı yönetimi. Tüm platformlardaki aktif kullanıcı oturumlarını görüntüleyin ve izinleri iptal edin. OIDC/Yerel hesaplarınızı birbirine bağlayın.
- **Veritabanı Şifreleme** - Arka uç, şifrelenmiş SQLite veritabanı dosyaları olarak depolanır. Daha fazla bilgi için [belgelere](https://docs.termix.site/security) bakın.
- **Veri Dışa/İçe Aktarma** - SSH ana bilgisayarlarını, kimlik bilgilerini ve dosya yöneticisi verilerini dışa ve içe aktarın
- **Otomatik SSL Kurulumu** - HTTPS yönlendirmeleriyle yerleşik SSL sertifika oluşturma ve yönetimi
- **Modern Arayüz** - React, Tailwind CSS ve Shadcn ile oluşturulmuş temiz masaüstü/mobil uyumlu arayüz. Karanlık veya açık tema arasında seçim yapın. Herhangi bir bağlantıyı tam ekranda açmak için URL yollarını kullanın.
- **Diller** - ~30 dil için yerleşik destek ([Crowdin](https://docs.termix.site/translations) tarafından yönetilir)
- **Platform Desteği** - Web uygulaması, masaüstü uygulaması (Windows, Linux ve macOS), PWA ve iOS ile Android için özel mobil/tablet uygulaması olarak kullanılabilir.
- **SSH Araçları** - Tek tıklamayla çalıştırılan yeniden kullanılabilir komut parçacıkları oluşturun. Birden fazla açık terminalde aynı anda tek bir komut çalıştırın.
- **Komut Geçmişi** - Daha önce çalıştırılan SSH komutlarını otomatik tamamlayın ve görüntüleyin
- **Hızlı Bağlantı** - Bağlantı verilerini kaydetmeden bir sunucuya bağlanın
- **Komut Paleti** - Sol shift tuşuna iki kez basarak SSH bağlantılarına klavyenizle hızlıca erişin
- **SSH Zengin Özellikler** - Atlama ana bilgisayarları, Warpgate, TOTP tabanlı bağlantılar, SOCKS5, ana bilgisayar anahtar doğrulama, otomatik şifre doldurma, [OPKSSH](https://github.com/openpubkey/opkssh) vb. destekler.
- **Ağ Grafiği** - Kontrol panelinizi, SSH bağlantılarınıza dayalı olarak ev laboratuvarınızı durum desteğiyle görselleştirmek için özelleştirin
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# Planlanan Özellikler
Tüm planlanan özellikler için [Projeler](https://github.com/orgs/Termix-SSH/projects/2) sayfasına bakın. Katkıda bulunmak istiyorsanız, [Katkıda Bulunma](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) sayfasına bakın.
# Kurulum
Desteklenen Cihazlar:
- Web sitesi (Chrome, Safari ve Firefox gibi herhangi bir platformda herhangi bir modern tarayıcı) (PWA desteği dahil)
- Windows (x64/ia32)
- Taşınabilir
- MSI Yükleyici
- Chocolatey Paket Yöneticisi
- Linux (x64/ia32)
- Taşınabilir
- AUR
- AppImage
- Deb
- Flatpak
- macOS (v12.0+ üzerinde x64/ia32)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (v15.1+)
- Apple App Store
- IPA
- Android (v7.0+)
- Google Play Store
- APK
Termix'i tüm platformlara nasıl kuracağınız hakkında daha fazla bilgi için Termix [Belgelerine](https://docs.termix.site/install) bakın. Aksi takdirde, örnek bir Docker Compose dosyasını burada görüntüleyin:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# Sponsorlar
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# Destek
Termix ile ilgili yardıma ihtiyacınız varsa veya bir özellik talep etmek istiyorsanız, [Sorunlar](https://github.com/Termix-SSH/Support/issues) sayfasını ziyaret edin, giriş yapın ve `New Issue` butonuna basın.
Lütfen sorununuzu mümkün olduğunca ayrıntılı yazın, tercihen İngilizce olarak. Ayrıca [Discord](https://discord.gg/jVQGdvHDrf) sunucusuna katılabilir ve destek kanalını ziyaret edebilirsiniz, ancak yanıt süreleri daha uzun olabilir.
# Ekran Görüntüleri
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Bazı videolar ve görseller güncel olmayabilir veya özellikleri tam olarak yansıtmayabilir.
# Lisans
Apache Lisansı Sürüm 2.0 altında dağıtılmaktadır. Daha fazla bilgi için LICENSE dosyasına bakın.
+187
View File
@@ -0,0 +1,187 @@
# Thống Kê Repo
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<a href="README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="color: #666;">Đạt được vào ngày 1 tháng 9 năm 2025</small>
</p>
<br />
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
Nếu bạn muốn, bạn có thể hỗ trợ dự án tại đây!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Tổng Quan
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồn mở, miễn phí vĩnh viễn, tự lưu trữ. Nó cung cấp giải pháp đa nền tảng để quản lý máy chủ và cơ sở hạ tầng của bạn thông qua một giao diện trực quan duy nhất. Termix cung cấp quyền truy cập terminal SSH, khả năng tạo đường hầm SSH, quản lý tệp từ xa và nhiều công cụ khác. Termix là giải pháp thay thế miễn phí và tự lưu trữ hoàn hảo cho Termius, khả dụng trên tất cả các nền tảng.
# Tính Năng
- **Truy Cập Terminal SSH** - Terminal đầy đủ tính năng với hỗ trợ chia màn hình (lên đến 4 bảng) với hệ thống tab kiểu trình duyệt. Bao gồm hỗ trợ tùy chỉnh terminal bao gồm các chủ đề terminal phổ biến, phông chữ và các thành phần khác
- **Quản Lý Đường Hầm SSH** - Tạo và quản lý đường hầm SSH với tự động kết nối lại và giám sát sức khỏe, hỗ trợ kết nối -l hoặc -r
- **Trình Quản Lý Tệp Từ Xa** - Quản lý tệp trực tiếp trên máy chủ từ xa với hỗ trợ xem và chỉnh sửa mã, hình ảnh, âm thanh và video. Tải lên, tải xuống, đổi tên, xóa và di chuyển tệp liền mạch với hỗ trợ sudo.
- **Quản Lý Docker** - Khởi động, dừng, tạm dừng, xóa container. Xem thống kê container. Điều khiển container bằng terminal docker exec. Không được tạo ra để thay thế Portainer hay Dockge mà đơn giản là để quản lý container của bạn thay vì tạo mới chúng.
- **Trình Quản Lý Máy Chủ SSH** - Lưu, sắp xếp và quản lý các kết nối SSH của bạn với thẻ và thư mục, dễ dàng lưu thông tin đăng nhập có thể tái sử dụng đồng thời có thể tự động hóa việc triển khai khóa SSH
- **Thống Kê Máy Chủ** - Xem mức sử dụng CPU, bộ nhớ và ổ đĩa cùng với mạng, thời gian hoạt động, thông tin hệ thống, tường lửa, giám sát cổng trên hầu hết các máy chủ chạy Linux
- **Bảng Điều Khiển** - Xem thông tin máy chủ trong nháy mắt trên bảng điều khiển của bạn
- **RBAC** - Tạo vai trò và chia sẻ máy chủ giữa người dùng/vai trò
- **Xác Thực Người Dùng** - Quản lý người dùng an toàn với quyền quản trị và hỗ trợ OIDC và 2FA (TOTP). Xem phiên hoạt động của người dùng trên tất cả các nền tảng và thu hồi quyền. Liên kết tài khoản OIDC/Nội bộ của bạn với nhau.
- **Mã Hóa Cơ Sở Dữ Liệu** - Backend được lưu trữ dưới dạng tệp cơ sở dữ liệu SQLite được mã hóa. Xem [tài liệu](https://docs.termix.site/security) để biết thêm.
- **Xuất/Nhập Dữ Liệu** - Xuất và nhập máy chủ SSH, thông tin xác thực và dữ liệu trình quản lý tệp
- **Thiết Lập SSL Tự Động** - Tạo và quản lý chứng chỉ SSL tích hợp với chuyển hướng HTTPS
- **Giao Diện Hiện Đại** - Giao diện sạch sẽ, thân thiện với máy tính/di động được xây dựng bằng React, Tailwind CSS và Shadcn. Chọn giữa giao diện chế độ tối hoặc sáng. Sử dụng đường dẫn URL để mở bất kỳ kết nối nào ở chế độ toàn màn hình.
- **Ngôn Ngữ** - Hỗ trợ tích hợp ~30 ngôn ngữ (được quản lý bởi [Crowdin](https://docs.termix.site/translations))
- **Hỗ Trợ Nền Tảng** - Khả dụng dưới dạng ứng dụng web, ứng dụng máy tính (Windows, Linux và macOS), PWA và ứng dụng di động/máy tính bảng chuyên dụng cho iOS và Android.
- **Công Cụ SSH** - Tạo đoạn lệnh có thể tái sử dụng, thực thi chỉ với một cú nhấp chuột. Chạy một lệnh đồng thời trên nhiều terminal đang mở.
- **Lịch Sử Lệnh** - Tự động hoàn thành và xem các lệnh SSH đã chạy trước đó
- **Kết Nối Nhanh** - Kết nối đến máy chủ mà không cần lưu dữ liệu kết nối
- **Bảng Lệnh** - Nhấn đúp phím shift trái để truy cập nhanh các kết nối SSH bằng bàn phím
- **SSH Giàu Tính Năng** - Hỗ trợ jump host, Warpgate, kết nối dựa trên TOTP, SOCKS5, xác minh khóa máy chủ, tự động điền mật khẩu, [OPKSSH](https://github.com/openpubkey/opkssh), v.v.
- **Biểu Đồ Mạng** - Tùy chỉnh Bảng Điều Khiển để trực quan hóa homelab của bạn dựa trên các kết nối SSH với hỗ trợ trạng thái
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
# Tính Năng Dự Kiến
Xem [Dự Án](https://github.com/orgs/Termix-SSH/projects/2) để biết tất cả các tính năng dự kiến. Nếu bạn muốn đóng góp, xem [Đóng Góp](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).
# Cài Đặt
Thiết Bị Được Hỗ Trợ:
- Trang web (bất kỳ trình duyệt hiện đại nào trên bất kỳ nền tảng nào như Chrome, Safari và Firefox) (bao gồm hỗ trợ PWA)
- Windows (x64/ia32)
- Portable
- MSI Installer
- Chocolatey Package Manager
- Linux (x64/ia32)
- Portable
- AUR
- AppImage
- Deb
- Flatpak
- macOS (x64/ia32 trên v12.0+)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOS (v15.1+)
- Apple App Store
- IPA
- Android (v7.0+)
- Google Play Store
- APK
Truy cập [Tài Liệu](https://docs.termix.site/install) Termix để biết thêm thông tin về cách cài đặt Termix trên tất cả các nền tảng. Ngoài ra, xem tệp Docker Compose mẫu tại đây:
```yaml
services:
termix:
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix-data:/app/data
environment:
PORT: "8080"
volumes:
termix-data:
driver: local
```
# Nhà Tài Trợ
<p align="left">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
</a>
</p>
# Hỗ Trợ
Nếu bạn cần trợ giúp hoặc muốn yêu cầu tính năng với Termix, hãy truy cập trang [Vấn Đề](https://github.com/Termix-SSH/Support/issues), đăng nhập và nhấn `New Issue`.
Vui lòng mô tả vấn đề càng chi tiết càng tốt, ưu tiên viết bằng tiếng Anh. Bạn cũng có thể tham gia máy chủ [Discord](https://discord.gg/jVQGdvHDrf) và truy cập kênh hỗ trợ, tuy nhiên thời gian phản hồi có thể lâu hơn.
# Ảnh Chụp Màn Hình
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
Một số video và hình ảnh có thể đã lỗi thời hoặc không thể hiện chính xác hoàn toàn các tính năng.
# Giấy Phép
Được phân phối theo Giấy Phép Apache Phiên Bản 2.0. Xem LICENSE để biết thêm thông tin.
Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

+6 -5
View File
@@ -8,7 +8,7 @@ import {
hostAccess,
dashboardPreferences,
} from "./database/db/schema.js";
import { eq, and, desc, or, sql } from "drizzle-orm";
import { eq, and, desc, sql } from "drizzle-orm";
import { dashboardLogger } from "./utils/logger.js";
import { SimpleDBOps } from "./utils/simple-db-ops.js";
import { AuthManager } from "./utils/auth-manager.js";
@@ -55,6 +55,10 @@ app.use(
);
app.use(cookieParser());
app.use(express.json({ limit: "1mb" }));
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "no-store");
next();
});
app.use(authManager.createAuthMiddleware());
@@ -85,9 +89,6 @@ 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);
@@ -296,7 +297,7 @@ app.post("/activity/log", async (req, res) => {
if (allActivities.length > 100) {
const toDelete = allActivities.slice(100);
for (const activity of toDelete) {
for (let i = 0; i < toDelete.length; i++) {
await SimpleDBOps.delete(recentActivity, "recent_activity", userId);
}
}
+104 -51
View File
@@ -45,6 +45,10 @@ import type {
} from "../../types/index.js";
import { getDb, DatabaseSaveTrigger } from "./db/index.js";
import Database from "better-sqlite3";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
@@ -203,6 +207,10 @@ app.use(bodyParser.json({ limit: "1gb" }));
app.use(bodyParser.urlencoded({ limit: "1gb", extended: true }));
app.use(bodyParser.raw({ limit: "5gb", type: "application/octet-stream" }));
app.use(cookieParser());
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "no-store");
next();
});
/**
* @openapi
@@ -595,21 +603,41 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body;
if (!password) {
return res.status(400).json({
error: "Password required for export",
code: "PASSWORD_REQUIRED",
});
}
const deviceInfo = parseUserAgent(req);
const unlocked = await authManager.authenticateUser(
userId,
password,
deviceInfo.type,
);
if (!unlocked) {
return res.status(401).json({ error: "Invalid password" });
const user = await getDb().select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0) {
return res.status(404).json({ error: "User not found" });
}
const isOidcUser = !!user[0].isOidc;
if (!isOidcUser) {
if (!password) {
return res.status(400).json({
error: "Password required for export",
code: "PASSWORD_REQUIRED",
});
}
const unlocked = await authManager.authenticateUser(
userId,
password,
deviceInfo.type,
);
if (!unlocked) {
return res.status(401).json({ error: "Invalid password" });
}
} else if (!DataCrypto.getUserDataKey(userId)) {
const oidcUnlocked = await authManager.authenticateOIDCUser(
userId,
deviceInfo.type,
);
if (!oidcUnlocked) {
return res.status(403).json({
error: "Failed to unlock user data with SSO credentials",
});
}
}
apiLogger.info("Exporting user data as SQLite", {
@@ -622,11 +650,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
throw new Error("User data not unlocked");
}
const user = await getDb().select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0) {
throw new Error(`User not found: ${userId}`);
}
const tempDir =
process.env.NODE_ENV === "production"
? path.join(process.env.DATA_DIR || "./db/data", ".temp", "exports")
@@ -805,20 +828,20 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
userRecord.id,
userRecord.username,
"[EXPORTED_USER_NO_PASSWORD]",
userRecord.is_admin ? 1 : 0,
userRecord.is_oidc ? 1 : 0,
userRecord.oidc_identifier || null,
userRecord.client_id || null,
userRecord.client_secret || null,
userRecord.issuer_url || null,
userRecord.authorization_url || null,
userRecord.token_url || null,
userRecord.identifier_path || null,
userRecord.name_path || null,
userRecord.isAdmin ? 1 : 0,
userRecord.isOidc ? 1 : 0,
userRecord.oidcIdentifier || null,
userRecord.clientId || null,
userRecord.clientSecret || null,
userRecord.issuerUrl || null,
userRecord.authorizationUrl || null,
userRecord.tokenUrl || null,
userRecord.identifierPath || null,
userRecord.namePath || null,
userRecord.scopes || null,
userRecord.totp_secret || null,
userRecord.totp_enabled ? 1 : 0,
userRecord.totp_backup_codes || null,
userRecord.totpSecret || null,
userRecord.totpEnabled ? 1 : 0,
userRecord.totpBackupCodes || null,
);
const sshHosts = await getDb()
@@ -851,31 +874,31 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.forceKeyboardInteractive || null,
decrypted.password || null,
decrypted.key || null,
decrypted.key_password || null,
decrypted.keyPassword || null,
decrypted.keyType || null,
decrypted.sudoPassword || null,
decrypted.autostartPassword || null,
decrypted.autostartKey || null,
decrypted.autostartKeyPassword || null,
decrypted.credentialId || null,
Boolean(decrypted.overrideCredentialUsername) ? 1 : 0,
Boolean(decrypted.enableTerminal) ? 1 : 0,
Boolean(decrypted.enableTunnel) ? 1 : 0,
decrypted.overrideCredentialUsername ? 1 : 0,
decrypted.enableTerminal ? 1 : 0,
decrypted.enableTunnel ? 1 : 0,
decrypted.tunnelConnections || null,
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.enableFileManager ? 1 : 0,
decrypted.enableDocker ? 1 : 0,
decrypted.showTerminalInSidebar ? 1 : 0,
decrypted.showFileManagerInSidebar ? 1 : 0,
decrypted.showTunnelInSidebar ? 1 : 0,
decrypted.showDockerInSidebar ? 1 : 0,
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.useSocks5 ? 1 : 0,
decrypted.socks5Host || null,
decrypted.socks5Port || null,
decrypted.socks5Username || null,
@@ -913,9 +936,9 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.username,
decrypted.password || null,
decrypted.key || null,
decrypted.private_key || null,
decrypted.public_key || null,
decrypted.key_password || null,
decrypted.privateKey || null,
decrypted.publicKey || null,
decrypted.keyPassword || null,
decrypted.keyType || null,
decrypted.detectedKeyType || null,
decrypted.usageCount || 0,
@@ -1135,7 +1158,7 @@ app.post(
return res.status(404).json({ error: "User not found" });
}
const isOidcUser = !!userRecords[0].is_oidc;
const isOidcUser = !!userRecords[0].isOidc;
if (!isOidcUser) {
if (!password) {
@@ -1506,7 +1529,7 @@ app.post(
.select()
.from(users)
.where(eq(users.id, userId));
if (targetUser.length > 0 && targetUser[0].is_admin) {
if (targetUser.length > 0 && targetUser[0].isAdmin) {
try {
const importedSettings = importDb
.prepare("SELECT * FROM settings")
@@ -1744,7 +1767,33 @@ app.use("/terminal", terminalRoutes);
app.use("/network-topology", networkTopologyRoutes);
app.use("/rbac", rbacRoutes);
const frontendDistPaths = [
path.join(__dirname, "../../../dist"),
path.join(__dirname, "../../dist"),
path.join(process.cwd(), "dist"),
];
const frontendDist = frontendDistPaths.find((p) =>
fs.existsSync(path.join(p, "index.html")),
);
if (frontendDist) {
databaseLogger.info(`Serving frontend from: ${frontendDist}`, {
operation: "static_files",
});
app.use(express.static(frontendDist));
app.use((req, res, next) => {
if (req.method === "GET" && req.accepts("html")) {
res.sendFile(path.join(frontendDist, "index.html"));
} else {
next();
}
});
}
app.use(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(
err: unknown,
req: express.Request,
@@ -1819,13 +1868,17 @@ app.get(
if (status.hasUnencryptedDb) {
try {
unencryptedSize = fs.statSync(dbPath).size;
} catch (error) {}
} catch {
// expected - file may not exist
}
}
if (status.hasEncryptedDb) {
try {
encryptedSize = fs.statSync(encryptedDbPath).size;
} catch (error) {}
} catch {
// expected - file may not exist
}
}
res.json({
+60 -17
View File
@@ -37,10 +37,11 @@ async function initializeDatabaseAsync(): Promise<void> {
memoryDatabase = new Database(decryptedBuffer);
try {
const sessionCount = memoryDatabase
memoryDatabase
.prepare("SELECT COUNT(*) as count FROM sessions")
.get() as { count: number };
} catch (countError) {
} catch {
// expected - sessions table may not exist yet
}
} else {
const migration = new DatabaseMigration(dataDir);
@@ -182,6 +183,18 @@ async function initializeCompleteDatabase(): Promise<void> {
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS trusted_devices (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
device_fingerprint TEXT NOT NULL,
device_type TEXT NOT NULL,
device_info TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT NOT NULL,
last_used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS ssh_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
@@ -750,6 +763,33 @@ const migrateSchema = () => {
}
}
try {
sqlite
.prepare("SELECT id FROM trusted_devices LIMIT 1")
.get();
} catch {
try {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS trusted_devices (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
device_fingerprint TEXT NOT NULL,
device_type TEXT NOT NULL,
device_info TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT NOT NULL,
last_used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
`);
} catch (createError) {
databaseLogger.warn("Failed to create trusted_devices table", {
operation: "schema_migration",
error: createError,
});
}
}
try {
sqlite
.prepare("SELECT id FROM network_topology LIMIT 1")
@@ -1033,23 +1073,15 @@ const migrateSchema = () => {
try {
const validSystemRoles = ['admin', 'user'];
const unwantedRoleNames = ['superAdmin', 'powerUser', 'readonly', 'member'];
let deletedCount = 0;
const deleteByName = sqlite.prepare("DELETE FROM roles WHERE name = ?");
for (const roleName of unwantedRoleNames) {
const result = deleteByName.run(roleName);
if (result.changes > 0) {
deletedCount += result.changes;
}
deleteByName.run(roleName);
}
const deleteOldSystemRole = sqlite.prepare("DELETE FROM roles WHERE name = ? AND is_system = 1");
for (const role of existingRoles) {
if (role.is_system === 1 && !validSystemRoles.includes(role.name) && !unwantedRoleNames.includes(role.name)) {
const result = deleteOldSystemRole.run(role.name);
if (result.changes > 0) {
deletedCount += result.changes;
}
deleteOldSystemRole.run(role.name);
}
}
} catch (cleanupError) {
@@ -1107,7 +1139,7 @@ const migrateSchema = () => {
for (const admin of adminUsers) {
try {
insertUserRole.run(admin.id, adminRole.id);
} catch (error) {
} catch {
// Ignore duplicate errors
}
}
@@ -1122,7 +1154,7 @@ const migrateSchema = () => {
for (const user of normalUsers) {
try {
insertUserRole.run(user.id, userRole.id);
} catch (error) {
} catch {
// Ignore duplicate errors
}
}
@@ -1156,10 +1188,11 @@ async function saveMemoryDatabaseToFile() {
}
try {
const sessionCount = memoryDatabase
memoryDatabase
.prepare("SELECT COUNT(*) as count FROM sessions")
.get() as { count: number };
} catch (countError) {
} catch {
// expected - sessions table may not exist yet
}
if (enableFileEncryption) {
@@ -1170,6 +1203,8 @@ async function saveMemoryDatabaseToFile() {
} else {
fs.writeFileSync(dbPath, buffer);
}
DatabaseSaveTrigger.markClean();
} catch (error) {
databaseLogger.error("Failed to save in-memory database", error, {
operation: "memory_db_save_failed",
@@ -1185,7 +1220,11 @@ async function handlePostInitFileEncryption() {
if (memoryDatabase) {
await saveMemoryDatabaseToFile();
setInterval(saveMemoryDatabaseToFile, 15 * 1000);
setInterval(() => {
if (DatabaseSaveTrigger.isDirty) {
saveMemoryDatabaseToFile();
}
}, 5 * 60 * 1000);
DatabaseSaveTrigger.initialize(saveMemoryDatabaseToFile);
}
@@ -1254,15 +1293,18 @@ async function cleanupDatabase() {
try {
fs.unlinkSync(path.join(tempDir, file));
} catch {
// expected - file cleanup best effort
}
}
try {
fs.rmdirSync(tempDir);
} catch {
// expected - dir cleanup best effort
}
}
} catch {
// expected - temp dir cleanup best effort
}
}
@@ -1271,6 +1313,7 @@ process.on("exit", () => {
try {
sqlite.close();
} catch {
// expected - database may already be closed
}
}
});
+35 -18
View File
@@ -4,25 +4,25 @@ import { sql } from "drizzle-orm";
export const users = sqliteTable("users", {
id: text("id").primaryKey(),
username: text("username").notNull(),
password_hash: text("password_hash").notNull(),
is_admin: integer("is_admin", { mode: "boolean" }).notNull().default(false),
passwordHash: text("password_hash").notNull(),
isAdmin: integer("is_admin", { mode: "boolean" }).notNull().default(false),
is_oidc: integer("is_oidc", { mode: "boolean" }).notNull().default(false),
oidc_identifier: text("oidc_identifier"),
client_id: text("client_id"),
client_secret: text("client_secret"),
issuer_url: text("issuer_url"),
authorization_url: text("authorization_url"),
token_url: text("token_url"),
identifier_path: text("identifier_path"),
name_path: text("name_path"),
isOidc: integer("is_oidc", { mode: "boolean" }).notNull().default(false),
oidcIdentifier: text("oidc_identifier"),
clientId: text("client_id"),
clientSecret: text("client_secret"),
issuerUrl: text("issuer_url"),
authorizationUrl: text("authorization_url"),
tokenUrl: text("token_url"),
identifierPath: text("identifier_path"),
namePath: text("name_path"),
scopes: text().default("openid email profile"),
totp_secret: text("totp_secret"),
totp_enabled: integer("totp_enabled", { mode: "boolean" })
totpSecret: text("totp_secret"),
totpEnabled: integer("totp_enabled", { mode: "boolean" })
.notNull()
.default(false),
totp_backup_codes: text("totp_backup_codes"),
totpBackupCodes: text("totp_backup_codes"),
});
export const settings = sqliteTable("settings", {
@@ -47,6 +47,23 @@ export const sessions = sqliteTable("sessions", {
.default(sql`CURRENT_TIMESTAMP`),
});
export const trustedDevices = sqliteTable("trusted_devices", {
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
deviceFingerprint: text("device_fingerprint").notNull(),
deviceType: text("device_type").notNull(),
deviceInfo: text("device_info").notNull(),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
expiresAt: text("expires_at").notNull(),
lastUsedAt: text("last_used_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
export const sshData = sqliteTable("ssh_data", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: text("user_id")
@@ -64,7 +81,7 @@ export const sshData = sqliteTable("ssh_data", {
password: text("password"),
key: text("key", { length: 8192 }),
key_password: text("key_password"),
keyPassword: text("key_password"),
keyType: text("key_type"),
sudoPassword: text("sudo_password"),
@@ -202,9 +219,9 @@ export const sshCredentials = sqliteTable("ssh_credentials", {
username: text("username"),
password: text("password"),
key: text("key", { length: 16384 }),
private_key: text("private_key", { length: 16384 }),
public_key: text("public_key", { length: 4096 }),
key_password: text("key_password"),
privateKey: text("private_key", { length: 16384 }),
publicKey: text("public_key", { length: 4096 }),
keyPassword: text("key_password"),
keyType: text("key_type"),
detectedKeyType: text("detected_key_type"),
+36 -37
View File
@@ -225,9 +225,9 @@ router.post(
username: username?.trim() || null,
password: plainPassword,
key: plainKey,
private_key: keyInfo?.privateKey || plainKey,
public_key: keyInfo?.publicKey || null,
key_password: plainKeyPassword,
privateKey: keyInfo?.privateKey || plainKey,
publicKey: keyInfo?.publicKey || null,
keyPassword: plainKeyPassword,
keyType: keyType || null,
detectedKeyType: keyInfo?.keyType || null,
usageCount: 0,
@@ -434,14 +434,14 @@ router.get(
if (credential.key) {
output.key = credential.key;
}
if (credential.private_key) {
output.privateKey = credential.private_key;
if (credential.privateKey) {
output.privateKey = credential.privateKey;
}
if (credential.public_key) {
output.publicKey = credential.public_key;
if (credential.publicKey) {
output.publicKey = credential.publicKey;
}
if (credential.key_password) {
output.keyPassword = credential.key_password;
if (credential.keyPassword) {
output.keyPassword = credential.keyPassword;
}
res.json(output);
@@ -564,13 +564,13 @@ router.put(
error: `Invalid SSH key: ${keyInfo.error}`,
});
}
updateFields.private_key = keyInfo.privateKey;
updateFields.public_key = keyInfo.publicKey;
updateFields.privateKey = keyInfo.privateKey;
updateFields.publicKey = keyInfo.publicKey;
updateFields.detectedKeyType = keyInfo.keyType;
}
}
if (updateData.keyPassword !== undefined) {
updateFields.key_password = updateData.keyPassword || null;
updateFields.keyPassword = updateData.keyPassword || null;
}
if (Object.keys(updateFields).length === 0) {
@@ -606,15 +606,15 @@ router.put(
userId,
);
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance();
await sharedCredManager.updateSharedCredentialsForOriginal(
parseInt(id),
userId,
);
const credential = updated[0];
authLogger.success("SSH credential updated", {
operation: "credential_update_success",
userId,
@@ -706,7 +706,7 @@ router.delete(
credentialId: null,
password: null,
key: null,
key_password: null,
keyPassword: null,
authType: "password",
})
.where(
@@ -737,8 +737,9 @@ router.delete(
}
}
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance();
await sharedCredManager.deleteSharedCredentialsForOriginal(parseInt(id));
@@ -751,7 +752,6 @@ router.delete(
),
);
const credential = credentialToDelete[0];
authLogger.success("SSH credential deleted", {
operation: "credential_delete_success",
userId,
@@ -841,10 +841,10 @@ router.post(
.set({
credentialId: parseInt(credentialId),
username: (credential.username as string) || "",
authType: (credential.auth_type || credential.authType) as string,
authType: credential.authType as string,
password: null,
key: null,
key_password: null,
keyPassword: null,
keyType: null,
updatedAt: new Date().toISOString(),
})
@@ -954,15 +954,15 @@ function formatCredentialOutput(
? credential.tags.split(",").filter(Boolean)
: []
: [],
authType: credential.authType || credential.auth_type,
authType: credential.authType,
username: credential.username || null,
publicKey: credential.public_key || credential.publicKey,
keyType: credential.key_type || credential.keyType,
detectedKeyType: credential.detected_key_type || credential.detectedKeyType,
usageCount: credential.usage_count || credential.usageCount || 0,
lastUsed: credential.last_used || credential.lastUsed,
createdAt: credential.created_at || credential.createdAt,
updatedAt: credential.updated_at || credential.updatedAt,
publicKey: credential.publicKey,
keyType: credential.keyType,
detectedKeyType: credential.detectedKeyType,
usageCount: credential.usageCount || 0,
lastUsed: credential.lastUsed,
createdAt: credential.createdAt,
updatedAt: credential.updatedAt,
};
}
@@ -1508,7 +1508,7 @@ async function deploySSHKeyToHost(
hostConfig: Record<string, unknown>,
credData: CredentialBackend,
): Promise<{ success: boolean; message?: string; error?: string }> {
const publicKey = credData.public_key as string;
const publicKey = credData.publicKey as string;
return new Promise((resolve) => {
const conn = new Client();
@@ -1934,7 +1934,7 @@ router.post(
});
}
const publicKey = credData.public_key;
const publicKey = credData.publicKey;
if (!publicKey) {
return res.status(400).json({
success: false,
@@ -1990,15 +1990,14 @@ router.post(
if (hostCredential && hostCredential.length > 0) {
const cred = hostCredential[0];
hostConfig.authType = cred.auth_type || cred.authType;
hostConfig.authType = cred.authType;
hostConfig.username = cred.username;
if ((cred.auth_type || cred.authType) === "password") {
if (cred.authType === "password") {
hostConfig.password = cred.password;
} else if ((cred.auth_type || cred.authType) === "key") {
hostConfig.privateKey =
cred.private_key || cred.privateKey || cred.key;
hostConfig.keyPassword = cred.key_password || cred.keyPassword;
} else if (cred.authType === "key") {
hostConfig.privateKey = cred.privateKey || cred.key;
hostConfig.keyPassword = cred.keyPassword;
}
} else {
return res.status(400).json({
+11 -12
View File
@@ -7,11 +7,10 @@ import {
users,
roles,
userRoles,
auditLogs,
sharedCredentials,
} from "../db/schema.js";
import { eq, and, desc, sql, or, isNull, gte } from "drizzle-orm";
import type { Request, Response } from "express";
import type { Response } from "express";
import { databaseLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import { PermissionManager } from "../../utils/permission-manager.js";
@@ -202,8 +201,9 @@ router.post(
.delete(sharedCredentials)
.where(eq(sharedCredentials.hostAccessId, existing[0].id));
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance();
if (targetType === "user") {
await sharedCredManager.createSharedCredentialForUser(
@@ -244,8 +244,9 @@ router.post(
expiresAt,
});
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance();
if (targetType === "user") {
@@ -857,10 +858,7 @@ router.delete(
permissionManager.invalidateUserPermissionCache(userId);
}
const deletedHostAccess = await db
.delete(hostAccess)
.where(eq(hostAccess.roleId, roleId))
.returning({ id: hostAccess.id });
await db.delete(hostAccess).where(eq(hostAccess.roleId, roleId));
await db.delete(roles).where(eq(roles.id, roleId));
@@ -983,8 +981,9 @@ router.post(
.innerJoin(sshData, eq(hostAccess.hostId, sshData.id))
.where(eq(hostAccess.roleId, roleId));
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance();
for (const { host_access, ssh_data } of hostsSharedWithRole) {
+5 -5
View File
@@ -655,7 +655,7 @@ router.post(
let password = host.password;
let privateKey = host.key;
let passphrase = host.key_password;
let passphrase = host.keyPassword;
let authType = host.authType;
if (host.credentialId) {
@@ -675,12 +675,12 @@ router.post(
if (credResult.length > 0) {
const cred = credResult[0];
authType = (cred.auth_type || cred.authType || authType) as string;
authType = (cred.authType || authType) as string;
password = (cred.password || undefined) as string | undefined;
privateKey = (cred.private_key || cred.key || undefined) as
privateKey = (cred.privateKey || cred.key || undefined) as
| string
| undefined;
passphrase = (cred.key_password || undefined) as string | undefined;
passphrase = (cred.keyPassword || undefined) as string | undefined;
}
}
@@ -731,7 +731,7 @@ router.post(
reject(err);
});
const config: any = {
const config: Record<string, unknown> = {
host: host.ip,
port: host.port,
username: host.username,
+474 -83
View File
@@ -251,7 +251,7 @@ router.get("/db/host/internal/all", async (req: Request, res: Response) => {
username: host.username,
password: host.autostartPassword || host.password,
key: host.autostartKey || host.key,
keyPassword: host.autostartKeyPassword || host.key_password,
keyPassword: host.autostartKeyPassword || host.keyPassword,
autostartPassword: host.autostartPassword,
autostartKey: host.autostartKey,
autostartKeyPassword: host.autostartKeyPassword,
@@ -426,8 +426,16 @@ router.post(
showDockerInSidebar: showDockerInSidebar ? 1 : 0,
showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0,
defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
statsConfig: statsConfig
? typeof statsConfig === "string"
? statsConfig
: JSON.stringify(statsConfig)
: null,
terminalConfig: terminalConfig
? typeof terminalConfig === "string"
? terminalConfig
: JSON.stringify(terminalConfig)
: null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
notes: notes || null,
sudoPassword: sudoPassword || null,
@@ -444,7 +452,7 @@ router.post(
if (effectiveAuthType === "password") {
sshDataObj.password = password || null;
sshDataObj.key = null;
sshDataObj.key_password = null;
sshDataObj.keyPassword = null;
sshDataObj.keyType = null;
} else if (effectiveAuthType === "key") {
if (key && typeof key === "string") {
@@ -481,13 +489,13 @@ router.post(
}
sshDataObj.key = key || null;
sshDataObj.key_password = keyPassword || null;
sshDataObj.keyPassword = keyPassword || null;
sshDataObj.keyType = keyType;
sshDataObj.password = null;
} else {
sshDataObj.password = null;
sshDataObj.key = null;
sshDataObj.key_password = null;
sshDataObj.keyPassword = null;
sshDataObj.keyType = null;
}
@@ -682,16 +690,10 @@ router.post(
const cred = credentials[0];
resolvedPassword = cred.password as string | undefined;
resolvedKey = (cred.private_key || cred.privateKey || cred.key) as
| string
| undefined;
resolvedKeyPassword = (cred.key_password || cred.keyPassword) as
| string
| undefined;
resolvedKeyType = (cred.key_type || cred.keyType) as string | undefined;
resolvedAuthType = (cred.auth_type || cred.authType) as
| string
| undefined;
resolvedKey = cred.privateKey as string | undefined;
resolvedKeyPassword = cred.keyPassword as string | undefined;
resolvedKeyType = cred.keyType as string | undefined;
resolvedAuthType = cred.authType as string | undefined;
if (!overrideCredentialUsername) {
resolvedUsername = cred.username as string;
@@ -909,8 +911,16 @@ router.put(
showDockerInSidebar: showDockerInSidebar ? 1 : 0,
showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0,
defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
statsConfig: statsConfig
? typeof statsConfig === "string"
? statsConfig
: JSON.stringify(statsConfig)
: null,
terminalConfig: terminalConfig
? typeof terminalConfig === "string"
? terminalConfig
: JSON.stringify(terminalConfig)
: null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
notes: notes || null,
sudoPassword: sudoPassword || null,
@@ -929,7 +939,7 @@ router.put(
sshDataObj.password = password;
}
sshDataObj.key = null;
sshDataObj.key_password = null;
sshDataObj.keyPassword = null;
sshDataObj.keyType = null;
} else if (effectiveAuthType === "key") {
if (key && typeof key === "string") {
@@ -969,7 +979,7 @@ router.put(
sshDataObj.key = key;
}
if (keyPassword !== undefined) {
sshDataObj.key_password = keyPassword || null;
sshDataObj.keyPassword = keyPassword || null;
}
if (keyType) {
sshDataObj.keyType = keyType;
@@ -978,7 +988,7 @@ router.put(
} else {
sshDataObj.password = null;
sshDataObj.key = null;
sshDataObj.key_password = null;
sshDataObj.keyPassword = null;
sshDataObj.keyType = null;
}
@@ -1055,10 +1065,9 @@ router.put(
hostRecord[0].credentialId !== null &&
sshDataObj.credentialId === null
) {
const revokedShares = await db
await db
.delete(hostAccess)
.where(eq(hostAccess.hostId, Number(hostId)))
.returning({ id: hostAccess.id, userId: hostAccess.userId });
.where(eq(hostAccess.hostId, Number(hostId)));
}
}
@@ -1189,7 +1198,7 @@ router.get(
authType: sshData.authType,
password: sshData.password,
key: sshData.key,
keyPassword: sshData.key_password,
keyPassword: sshData.keyPassword,
keyType: sshData.keyType,
enableTerminal: sshData.enableTerminal,
enableTunnel: sshData.enableTunnel,
@@ -1264,7 +1273,7 @@ router.get(
const ownHosts = rawData.filter((row) => row.userId === userId);
const sharedHosts = rawData.filter((row) => row.userId !== userId);
let decryptedOwnHosts: any[] = [];
let decryptedOwnHosts: Record<string, unknown>[] = [];
try {
decryptedOwnHosts = await SimpleDBOps.select(
Promise.resolve(ownHosts),
@@ -1447,24 +1456,53 @@ router.get(
authType: resolvedHost.authType,
password: resolvedHost.password || null,
key: resolvedHost.key || null,
keyPassword: resolvedHost.key_password || null,
keyPassword: resolvedHost.keyPassword || null,
keyType: resolvedHost.keyType || null,
credentialId: resolvedHost.credentialId || null,
overrideCredentialUsername: !!resolvedHost.overrideCredentialUsername,
folder: resolvedHost.folder,
tags:
typeof resolvedHost.tags === "string"
? resolvedHost.tags.split(",").filter(Boolean)
: resolvedHost.tags || [],
pin: !!resolvedHost.pin,
notes: resolvedHost.notes || null,
enableTerminal: !!resolvedHost.enableTerminal,
enableTunnel: !!resolvedHost.enableTunnel,
enableFileManager: !!resolvedHost.enableFileManager,
enableDocker: !!resolvedHost.enableDocker,
showTerminalInSidebar: !!resolvedHost.showTerminalInSidebar,
showFileManagerInSidebar: !!resolvedHost.showFileManagerInSidebar,
showTunnelInSidebar: !!resolvedHost.showTunnelInSidebar,
showDockerInSidebar: !!resolvedHost.showDockerInSidebar,
showServerStatsInSidebar: !!resolvedHost.showServerStatsInSidebar,
defaultPath: resolvedHost.defaultPath,
sudoPassword: resolvedHost.sudoPassword || null,
tunnelConnections: resolvedHost.tunnelConnections
? JSON.parse(resolvedHost.tunnelConnections as string)
: [],
jumpHosts: resolvedHost.jumpHosts
? JSON.parse(resolvedHost.jumpHosts as string)
: null,
quickActions: resolvedHost.quickActions
? JSON.parse(resolvedHost.quickActions as string)
: null,
statsConfig: resolvedHost.statsConfig
? JSON.parse(resolvedHost.statsConfig as string)
: null,
terminalConfig: resolvedHost.terminalConfig
? JSON.parse(resolvedHost.terminalConfig as string)
: null,
forceKeyboardInteractive:
resolvedHost.forceKeyboardInteractive === "true",
useSocks5: !!resolvedHost.useSocks5,
socks5Host: resolvedHost.socks5Host || null,
socks5Port: resolvedHost.socks5Port || null,
socks5Username: resolvedHost.socks5Username || null,
socks5Password: resolvedHost.socks5Password || null,
socks5ProxyChain: resolvedHost.socks5ProxyChain
? JSON.parse(resolvedHost.socks5ProxyChain as string)
: [],
: null,
};
sshLogger.success("Host exported with decrypted credentials", {
@@ -1583,7 +1621,6 @@ router.delete(
.delete(sshData)
.where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId)));
const host = hostToDelete[0];
databaseLogger.success("SSH host deleted", {
operation: "host_delete_success",
userId,
@@ -2417,8 +2454,8 @@ async function resolveHostCredentials(
...host,
password: credential.password,
key: credential.key,
keyPassword: credential.key_password || credential.keyPassword,
keyType: credential.key_type || credential.keyType,
keyPassword: credential.keyPassword,
keyType: credential.keyType,
};
if (!host.overrideCredentialUsername) {
@@ -2429,14 +2466,7 @@ async function resolveHostCredentials(
}
}
const result = { ...host };
if (host.key_password !== undefined) {
if (result.keyPassword === undefined) {
result.keyPassword = host.key_password;
}
delete result.key_password;
}
return result;
return { ...host };
} catch (error) {
sshLogger.warn(
`Failed to resolve credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
@@ -2846,12 +2876,142 @@ router.delete(
* 400:
* description: Invalid request body.
*/
/**
* @swagger
* /ssh/bulk-update:
* patch:
* summary: Bulk update partial fields on multiple SSH hosts
* tags: [SSH]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* hostIds:
* type: array
* items:
* type: number
* updates:
* type: object
* responses:
* 200:
* description: Bulk update completed.
* 400:
* description: Invalid request body.
*/
router.patch(
"/bulk-update",
authenticateJWT,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { hostIds, updates } = req.body;
if (!Array.isArray(hostIds) || hostIds.length === 0) {
return res
.status(400)
.json({ error: "hostIds array is required and must not be empty" });
}
if (hostIds.length > 1000) {
return res
.status(400)
.json({ error: "Maximum 1000 hosts allowed per bulk update" });
}
if (
!updates ||
typeof updates !== "object" ||
Object.keys(updates).length === 0
) {
return res.status(400).json({
error: "updates object is required and must contain at least one field",
});
}
try {
const ownedHosts = await db
.select({ id: sshData.id, statsConfig: sshData.statsConfig })
.from(sshData)
.where(and(inArray(sshData.id, hostIds), eq(sshData.userId, userId)));
const ownedIds = ownedHosts.map((h) => h.id);
const unauthorizedIds = hostIds.filter(
(id: number) => !ownedIds.includes(id),
);
if (ownedIds.length === 0) {
return res.status(404).json({ error: "No matching hosts found" });
}
const errors: string[] = [];
if (unauthorizedIds.length > 0) {
errors.push(`${unauthorizedIds.length} host(s) not found or not owned`);
}
const simpleUpdates: Record<string, unknown> = {};
if (typeof updates.pin === "boolean") simpleUpdates.pin = updates.pin;
if (typeof updates.folder === "string")
simpleUpdates.folder = updates.folder || null;
if (typeof updates.enableTerminal === "boolean")
simpleUpdates.enableTerminal = updates.enableTerminal;
if (typeof updates.enableTunnel === "boolean")
simpleUpdates.enableTunnel = updates.enableTunnel;
if (typeof updates.enableFileManager === "boolean")
simpleUpdates.enableFileManager = updates.enableFileManager;
if (typeof updates.enableDocker === "boolean")
simpleUpdates.enableDocker = updates.enableDocker;
if (Object.keys(simpleUpdates).length > 0) {
await db
.update(sshData)
.set(simpleUpdates)
.where(
and(inArray(sshData.id, ownedIds), eq(sshData.userId, userId)),
);
}
if (updates.statsConfig && typeof updates.statsConfig === "object") {
for (const host of ownedHosts) {
try {
const existing = host.statsConfig
? JSON.parse(host.statsConfig as string)
: {};
const merged = { ...existing, ...updates.statsConfig };
await db
.update(sshData)
.set({ statsConfig: JSON.stringify(merged) })
.where(and(eq(sshData.id, host.id), eq(sshData.userId, userId)));
} catch (e) {
errors.push(`Failed to update statsConfig for host ${host.id}`);
}
}
}
DatabaseSaveTrigger.triggerSave("bulk_update");
return res.json({
updated: ownedIds.length,
failed: unauthorizedIds.length,
errors,
});
} catch (error) {
sshLogger.error("Failed to bulk update hosts:", error);
return res.status(500).json({ error: "Failed to bulk update hosts" });
}
},
);
router.post(
"/bulk-import",
authenticateJWT,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { hosts } = req.body;
const { hosts, overwrite } = req.body;
if (!Array.isArray(hosts) || hosts.length === 0) {
return res
@@ -2867,10 +3027,30 @@ router.post(
const results = {
success: 0,
updated: 0,
skipped: 0,
failed: 0,
errors: [] as string[],
};
let existingHostMap: Map<string, { id: number }> | undefined;
if (overwrite) {
try {
const allHosts = await SimpleDBOps.select<Record<string, unknown>>(
db.select().from(sshData).where(eq(sshData.userId, userId)),
"ssh_data",
userId,
);
existingHostMap = new Map();
for (const h of allHosts) {
const key = `${h.ip}:${h.port}:${h.username}`;
existingHostMap.set(key, { id: h.id as number });
}
} catch {
existingHostMap = undefined;
}
}
for (let i = 0; i < hosts.length; i++) {
const hostData = hosts[i];
@@ -2940,9 +3120,7 @@ router.post(
hostData.authType === "credential" ? hostData.credentialId : null,
key: hostData.authType === "key" ? hostData.key : null,
keyPassword:
hostData.authType === "key"
? hostData.keyPassword || hostData.key_password || null
: null,
hostData.authType === "key" ? hostData.keyPassword || null : null,
keyType:
hostData.authType === "key" ? hostData.keyType || "auto" : null,
pin: hostData.pin || false,
@@ -2950,7 +3128,13 @@ router.post(
enableTunnel: hostData.enableTunnel !== false,
enableFileManager: hostData.enableFileManager !== false,
enableDocker: hostData.enableDocker || false,
showTerminalInSidebar: hostData.showTerminalInSidebar ? 1 : 0,
showFileManagerInSidebar: hostData.showFileManagerInSidebar ? 1 : 0,
showTunnelInSidebar: hostData.showTunnelInSidebar ? 1 : 0,
showDockerInSidebar: hostData.showDockerInSidebar ? 1 : 0,
showServerStatsInSidebar: hostData.showServerStatsInSidebar ? 1 : 0,
defaultPath: hostData.defaultPath || "/",
sudoPassword: hostData.sudoPassword || null,
tunnelConnections: hostData.tunnelConnections
? JSON.stringify(hostData.tunnelConnections)
: "[]",
@@ -2981,12 +3165,26 @@ router.post(
overrideCredentialUsername: hostData.overrideCredentialUsername
? 1
: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
await SimpleDBOps.insert(sshData, "ssh_data", sshDataObj, userId);
results.success++;
const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
const existing = existingHostMap?.get(lookupKey);
if (existing) {
await SimpleDBOps.update(
sshData,
"ssh_data",
eq(sshData.id, existing.id),
sshDataObj,
userId,
);
results.updated++;
} else {
sshDataObj.createdAt = new Date().toISOString();
await SimpleDBOps.insert(sshData, "ssh_data", sshDataObj, userId);
results.success++;
}
} catch (error) {
results.failed++;
results.errors.push(
@@ -2996,8 +3194,10 @@ router.post(
}
res.json({
message: `Import completed: ${results.success} successful, ${results.failed} failed`,
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.failed} failed`,
success: results.success,
updated: results.updated,
skipped: results.skipped,
failed: results.failed,
errors: results.errors,
});
@@ -3130,7 +3330,7 @@ router.post(
...tunnel,
endpointPassword: decryptedEndpoint.password || null,
endpointKey: decryptedEndpoint.key || null,
endpointKeyPassword: decryptedEndpoint.key_password || null,
endpointKeyPassword: decryptedEndpoint.keyPassword || null,
endpointAuthType: endpointHost.authType,
};
}
@@ -3153,7 +3353,7 @@ router.post(
.set({
autostartPassword: decryptedConfig.password || null,
autostartKey: decryptedConfig.key || null,
autostartKeyPassword: decryptedConfig.key_password || null,
autostartKeyPassword: decryptedConfig.keyPassword || null,
tunnelConnections: updatedTunnelConnections,
})
.where(eq(sshData.id, sshConfigId));
@@ -3466,9 +3666,17 @@ function rewriteOPKSSHHtml(
): 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}/`);
const attrPatterns = ["action", "href", "src"];
for (const attr of attrPatterns) {
html = html.replace(
new RegExp(`${attr}="(/[^"]*)`, "g"),
`${attr}="${basePath}$1`,
);
html = html.replace(
new RegExp(`${attr}='(/[^']*)`, "g"),
`${attr}='${basePath}$1`,
);
}
html = html.replace(
/href=["']?http:\/\/localhost:\d+\/([^"'\s]*)/g,
@@ -3518,11 +3726,37 @@ function rewriteOPKSSHHtml(
const baseTag = `<base href="${basePath}/">`;
if (html.includes("<base")) {
sshLogger.info("Replacing existing base tag", {
operation: "opkssh_html_rewrite_base_tag",
requestId,
basePath,
});
html = html.replace(/<base[^>]*>/i, baseTag);
} else {
} else if (html.includes("<head>")) {
sshLogger.info("Inserting base tag into head", {
operation: "opkssh_html_rewrite_base_tag_insert",
requestId,
basePath,
});
html = html.replace(/<head>/i, `<head>${baseTag}`);
} else {
sshLogger.warn("No <head> tag found, wrapping HTML", {
operation: "opkssh_html_rewrite_no_head",
requestId,
htmlLength: html.length,
htmlPreview: html.substring(0, 200),
});
html = `<!DOCTYPE html><html><head>${baseTag}</head><body>${html}</body></html>`;
}
sshLogger.info("HTML rewrite complete", {
operation: "opkssh_html_rewrite_complete",
requestId,
routePrefix,
hasBaseTag: html.includes("<base href="),
staticAssetCount: (html.match(/\/static\//g) || []).length,
});
return html;
}
@@ -3555,11 +3789,29 @@ router.use(
? req.params.requestId[0]
: req.params.requestId;
const fullPath = req.originalUrl || req.url;
const pathAfterRequestIdTemp =
fullPath.split(`/ssh/opkssh-chooser/${requestId}`)[1] || "";
sshLogger.info("OPKSSH chooser proxy request", {
operation: "opkssh_chooser_proxy_request",
requestId,
url: req.url,
originalUrl: req.originalUrl,
fullPath,
pathAfterRequestId: pathAfterRequestIdTemp,
method: req.method,
});
try {
const { getActiveAuthSession } = await import("../../ssh/opkssh-auth.js");
const session = getActiveAuthSession(requestId);
if (!session) {
sshLogger.error("Session not found for chooser request", {
operation: "opkssh_chooser_session_not_found",
requestId,
});
res.status(404).send(`
<!DOCTYPE html>
<html>
@@ -3682,6 +3934,14 @@ router.use(
const targetUrl = `http://localhost:${session.localPort}${targetPath}`;
sshLogger.info("Proxying to OPKSSH chooser", {
operation: "opkssh_chooser_proxy_request_to_opkssh",
requestId,
targetUrl,
localPort: session.localPort,
targetPath,
});
const response = await axios({
method: req.method,
url: targetUrl,
@@ -3696,6 +3956,15 @@ router.use(
responseType: "arraybuffer",
});
sshLogger.info("OPKSSH chooser response received", {
operation: "opkssh_chooser_proxy_response",
requestId,
statusCode: response.status,
contentType: response.headers["content-type"],
contentLength: response.headers["content-length"],
hasLocation: !!response.headers.location,
});
Object.entries(response.headers).forEach(([key, value]) => {
if (key.toLowerCase() === "transfer-encoding") {
return;
@@ -3821,57 +4090,103 @@ router.use(
*/
router.get("/opkssh-callback", async (req: Request, res: Response) => {
try {
const { getActiveSessionsAll } = await import("../../ssh/opkssh-auth.js");
sshLogger.info("OAuth callback received", {
operation: "opkssh_static_callback_received",
url: req.url,
originalUrl: req.originalUrl,
query: req.query,
headers: {
host: req.headers.host,
"x-forwarded-proto": req.headers["x-forwarded-proto"],
"x-forwarded-host": req.headers["x-forwarded-host"],
"x-forwarded-port": req.headers["x-forwarded-port"],
},
});
const allSessions = getActiveSessionsAll();
const session = allSessions[allSessions.length - 1];
const { getUserIdFromRequest, getActiveSessionsForUser } =
await import("../../ssh/opkssh-auth.js");
if (!session) {
const userId = await getUserIdFromRequest({
cookies: req.cookies,
headers: req.headers as Record<string, string | undefined>,
});
sshLogger.info("User ID resolved", {
operation: "opkssh_callback_user_lookup",
userId: userId || "null",
hasCookies: !!req.cookies?.jwt,
cookieKeys: Object.keys(req.cookies || {}),
});
if (!userId) {
sshLogger.error("No userId from callback request", {
operation: "opkssh_callback_unauthorized",
cookies: Object.keys(req.cookies || {}),
headers: Object.keys(req.headers),
});
res.status(401).send("Unauthorized - no valid session");
return;
}
const userSessions = getActiveSessionsForUser(userId);
sshLogger.info("Active sessions for user", {
operation: "opkssh_callback_session_lookup",
userId,
sessionCount: userSessions.length,
sessions: userSessions.map((s) => ({
requestId: s.requestId,
status: s.status,
hasCallbackPort: !!s.callbackPort,
callbackPort: s.callbackPort,
hasLocalPort: !!s.localPort,
localPort: s.localPort,
})),
});
if (userSessions.length === 0) {
sshLogger.error("No active sessions for callback", {
operation: "opkssh_callback_no_sessions",
userId,
});
res.status(404).send("No active authentication session found");
return;
}
const session = userSessions[userSessions.length - 1];
if (!session.callbackPort) {
sshLogger.error("Session callback port not ready", {
operation: "opkssh_callback_port_not_ready",
userId,
requestId: session.requestId,
sessionStatus: session.status,
hasLocalPort: !!session.localPort,
});
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}`;
const redirectUrl = `/ssh/opkssh-callback/${session.requestId}/login-callback${queryString}`;
sshLogger.info("Proxying OAuth callback to OPKSSH", {
operation: "opkssh_static_callback",
sshLogger.info("Redirecting OAuth callback to dynamic route", {
operation: "opkssh_static_callback_redirect",
userId,
requestId: session.requestId,
callbackPort: session.callbackPort,
targetUrl,
queryParams: Object.keys(req.query),
redirectUrl,
});
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);
res.redirect(302, redirectUrl);
} catch (error) {
sshLogger.error("Error handling OPKSSH static callback", error, {
operation: "opkssh_static_callback_error",
url: req.url,
originalUrl: req.originalUrl,
});
res.status(500).send("Authentication callback failed");
}
@@ -4139,4 +4454,80 @@ router.use(
},
);
/**
* @openapi
* /db/proxy/test:
* post:
* summary: Test proxy connectivity
* description: Tests connectivity through a proxy configuration to a target host.
* tags:
* - SSH
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* singleProxy:
* type: object
* properties:
* host:
* type: string
* port:
* type: number
* type:
* type: string
* username:
* type: string
* password:
* type: string
* proxyChain:
* type: array
* items:
* type: object
* testTarget:
* type: object
* properties:
* host:
* type: string
* port:
* type: number
* responses:
* 200:
* description: Test result
* 500:
* description: Proxy connection failed
*/
router.post(
"/db/proxy/test",
authenticateJWT,
requireDataAccess,
async (req: AuthenticatedRequest, res: Response) => {
try {
const { singleProxy, proxyChain, testTarget } = req.body;
const { testProxyConnectivity } =
await import("../../utils/proxy-helper.js");
const result = await testProxyConnectivity({
singleProxy,
proxyChain,
testTarget,
});
res.json(result);
} catch (error) {
sshLogger.error("Proxy connectivity test failed", error, {
operation: "proxy_test",
userId: req.userId,
});
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
});
}
},
);
export default router;
+114
View File
@@ -285,4 +285,118 @@ router.delete(
},
);
/**
* @openapi
* /terminal/session_settings:
* get:
* summary: Get session persistence settings
* description: Returns the session timeout and persistence enabled flag.
* tags:
* - Terminal
* responses:
* 200:
* description: Session settings.
* 500:
* description: Failed to fetch settings.
*/
router.get(
"/session_settings",
authenticateJWT,
async (_req: Request, res: Response) => {
try {
const timeoutRow = db.$client
.prepare(
"SELECT value FROM settings WHERE key = 'terminal_session_timeout_minutes'",
)
.get() as { value: string } | undefined;
const enabledRow = db.$client
.prepare(
"SELECT value FROM settings WHERE key = 'terminal_session_persistence_enabled'",
)
.get() as { value: string } | undefined;
res.json({
timeoutMinutes: timeoutRow ? parseInt(timeoutRow.value, 10) : 30,
enabled: enabledRow ? enabledRow.value === "true" : true,
});
} catch (err) {
authLogger.error("Failed to fetch session settings", err);
res.status(500).json({
error: err instanceof Error ? err.message : "Failed to fetch settings",
});
}
},
);
/**
* @openapi
* /terminal/session_settings:
* post:
* summary: Update session persistence settings
* description: Saves session timeout and persistence enabled flag.
* tags:
* - Terminal
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* timeoutMinutes:
* type: integer
* enabled:
* type: boolean
* responses:
* 200:
* description: Settings saved successfully.
* 400:
* description: Invalid parameters.
* 500:
* description: Failed to save settings.
*/
router.post(
"/session_settings",
authenticateJWT,
async (req: Request, res: Response) => {
const { timeoutMinutes, enabled } = req.body;
if (
timeoutMinutes !== undefined &&
(typeof timeoutMinutes !== "number" ||
timeoutMinutes < 1 ||
timeoutMinutes > 1440)
) {
return res
.status(400)
.json({ error: "timeoutMinutes must be between 1 and 1440" });
}
try {
if (timeoutMinutes !== undefined) {
db.$client
.prepare(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('terminal_session_timeout_minutes', ?)",
)
.run(String(timeoutMinutes));
}
if (enabled !== undefined) {
db.$client
.prepare(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('terminal_session_persistence_enabled', ?)",
)
.run(String(enabled));
}
res.json({ success: true });
} catch (err) {
authLogger.error("Failed to save session settings", err);
res.status(500).json({
error: err instanceof Error ? err.message : "Failed to save settings",
});
}
},
);
export default router;
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -99,5 +99,4 @@ main() {
setup_ssl_certificates
}
# Run main function
main "$@"
main "$@"
+4 -10
View File
@@ -1,12 +1,9 @@
import { Client } from "ssh2";
import type { WebSocket } from "ws";
import { sshLogger, authLogger } from "../utils/logger.js";
import { getDb } from "../database/db/index.js";
import { sshCredentials, sshData } from "../database/db/schema.js";
import { sshCredentials } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { UserCrypto } from "../utils/user-crypto.js";
interface ResolvedCredentials {
username: string;
password?: string;
@@ -44,9 +41,6 @@ interface AuthContext {
totpAttempts: number;
}
const userCrypto = UserCrypto.getInstance();
const MAX_TOTP_ATTEMPTS = 3;
export class SSHAuthManager {
public context: AuthContext;
@@ -85,7 +79,7 @@ export class SSHAuthManager {
key: cred.privateKey
? Buffer.from(cred.privateKey as string)
: undefined,
keyPassword: (cred.passphrase as string) || undefined,
keyPassword: (cred.keyPassword as string) || undefined,
authType: (cred.authType as string) || "none",
};
}
@@ -163,7 +157,7 @@ export class SSHAuthManager {
);
if (urlMatch) {
this.context.keyboardInteractiveFinish = (responses: string[]) => {
this.context.keyboardInteractiveFinish = () => {
finish([""]);
};
@@ -358,7 +352,7 @@ export class SSHAuthManager {
stage: string,
level: string,
message: string,
details?: Record<string, any>,
details?: Record<string, unknown>,
): void {
this.context.ws.send(
JSON.stringify({
+54 -38
View File
@@ -13,7 +13,7 @@ const sshLogger = systemLogger;
interface SSHSession {
client: SSHClient;
stream: any;
stream: import("ssh2").ClientChannel | null;
isConnected: boolean;
containerId?: string;
shell?: string;
@@ -42,7 +42,7 @@ const wss = new WebSocketServer({
}
return true;
} catch (error) {
} catch {
return false;
}
},
@@ -92,7 +92,7 @@ async function detectShell(
}
async function createJumpHostChain(
jumpHosts: any[],
jumpHosts: Array<{ hostId: number }>,
userId: string,
): Promise<SSHClient | null> {
if (!jumpHosts || jumpHosts.length === 0) {
@@ -129,7 +129,12 @@ async function createJumpHostChain(
}
}
let resolvedCredentials: any = {
let resolvedCredentials: {
password?: string;
sshKey?: string;
keyPassword?: string;
authType?: string;
} = {
password: jumpHost.password,
sshKey: jumpHost.key,
keyPassword: jumpHost.keyPassword,
@@ -154,19 +159,18 @@ async function createJumpHostChain(
if (credentials.length > 0) {
const credential = credentials[0];
resolvedCredentials = {
password: credential.password,
sshKey:
credential.private_key || credential.privateKey || credential.key,
keyPassword: credential.key_password || credential.keyPassword,
authType: credential.auth_type || credential.authType,
password: credential.password as string | undefined,
sshKey: credential.privateKey as string | undefined,
keyPassword: credential.keyPassword as string | undefined,
authType: credential.authType as string | undefined,
};
}
}
const client = new SSHClient();
const config: any = {
host: jumpHost.ip,
const config: Record<string, unknown> = {
host: jumpHost.ip?.replace(/^\[|\]$/g, "") || jumpHost.ip,
port: jumpHost.port || 22,
username: jumpHost.username,
tryKeyboard: true,
@@ -225,7 +229,7 @@ async function createJumpHostChain(
}
wss.on("connection", async (ws: WebSocket, req) => {
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
const sessionId = `docker-console-${Date.now()}-${Math.random()}`;
sshLogger.info("Docker console WebSocket connected", {
operation: "docker_console_connect",
@@ -235,6 +239,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
let sshSession: SSHSession | null = null;
const wsPingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 30000);
ws.on("message", async (data) => {
try {
const message = JSON.parse(data.toString());
@@ -286,7 +296,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
try {
let resolvedCredentials: any = {
let resolvedCredentials: {
password?: string;
sshKey?: string;
keyPassword?: string;
authType?: string;
} = {
password: hostConfig.password,
sshKey: hostConfig.key,
keyPassword: hostConfig.keyPassword,
@@ -311,22 +326,18 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (credentials.length > 0) {
const credential = credentials[0];
resolvedCredentials = {
password: credential.password,
sshKey:
credential.private_key ||
credential.privateKey ||
credential.key,
keyPassword:
credential.key_password || credential.keyPassword,
authType: credential.auth_type || credential.authType,
password: credential.password as string | undefined,
sshKey: credential.privateKey as string | undefined,
keyPassword: credential.keyPassword as string | undefined,
authType: credential.authType as string | undefined,
};
}
}
const client = new SSHClient();
const config: any = {
host: hostConfig.ip,
const config: Record<string, unknown> = {
host: hostConfig.ip?.replace(/^\[|\]$/g, "") || hostConfig.ip,
port: hostConfig.port || 22,
username: hostConfig.username,
tryKeyboard: true,
@@ -362,18 +373,20 @@ wss.on("connection", async (ws: WebSocket, req) => {
userId,
);
if (jumpClient) {
const stream = await new Promise<any>((resolve, reject) => {
jumpClient.forwardOut(
"127.0.0.1",
0,
hostConfig.ip,
hostConfig.port || 22,
(err, stream) => {
if (err) return reject(err);
resolve(stream);
},
);
});
const stream = await new Promise<import("ssh2").ClientChannel>(
(resolve, reject) => {
jumpClient.forwardOut(
"127.0.0.1",
0,
hostConfig.ip,
hostConfig.port || 22,
(err, stream) => {
if (err) return reject(err);
resolve(stream);
},
);
},
);
config.sock = stream;
}
}
@@ -496,7 +509,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
});
stream.stderr.on("data", (data: Buffer) => {});
stream.stderr.on("data", () => {
// stderr output ignored
});
stream.on("close", () => {
if (ws.readyState === WebSocket.OPEN) {
@@ -556,7 +571,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
case "resize": {
if (sshSession && sshSession.stream) {
const { cols, rows } = message.data;
sshSession.stream.setWindow(rows, cols);
sshSession.stream.setWindow(rows, cols, rows, cols);
}
break;
}
@@ -608,6 +623,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
});
ws.on("close", () => {
clearInterval(wsPingInterval);
sshLogger.info("Docker console disconnected", {
operation: "docker_console_disconnect",
sessionId,
@@ -641,7 +657,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
});
process.on("SIGTERM", () => {
activeSessions.forEach((session, sessionId) => {
activeSessions.forEach((session) => {
if (session.stream) {
session.stream.end();
}
+267 -172
View File
@@ -3,15 +3,17 @@ import cors from "cors";
import cookieParser from "cookie-parser";
import axios from "axios";
import { Client as SSHClient } from "ssh2";
import type { ClientChannel } from "ssh2";
import { getDb } from "../database/db/index.js";
import { sshData, sshCredentials } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { logger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js";
import { createSocks5Connection } from "../utils/socks5-helper.js";
import type { AuthenticatedRequest, SSHHost } from "../../types/index.js";
import {
createSocks5Connection,
type SOCKS5Config,
} from "../utils/socks5-helper.js";
import type { SSHHost, ProxyNode } from "../../types/index.js";
import type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
@@ -21,7 +23,7 @@ function createConnectionLog(
type: "info" | "success" | "warning" | "error",
stage: ConnectionStage,
message: string,
details?: Record<string, any>,
details?: Record<string, unknown>,
): Omit<LogEntry, "id" | "timestamp"> {
return {
type,
@@ -43,7 +45,7 @@ interface SSHSession {
interface PendingTOTPSession {
client: SSHClient;
finish: (responses: string[]) => void;
config: any;
config: Record<string, unknown>;
createdAt: number;
sessionId: string;
hostId?: number;
@@ -70,7 +72,9 @@ setInterval(() => {
if (now - session.createdAt > 180000) {
try {
session.client.end();
} catch {}
} catch {
// expected
}
delete pendingTOTPSessions[sessionId];
}
});
@@ -94,7 +98,9 @@ function cleanupSession(sessionId: string) {
try {
session.client.end();
} catch (error) {}
} catch {
// expected
}
clearTimeout(session.timeout);
delete sshSessions[sessionId];
}
@@ -111,10 +117,24 @@ function scheduleSessionCleanup(sessionId: string) {
}
}
interface JumpHostConfig {
id: number;
ip: string;
port: number;
username: string;
password?: string;
key?: string;
keyPassword?: string;
keyType?: string;
authType?: string;
credentialId?: number;
[key: string]: unknown;
}
async function resolveJumpHost(
hostId: number,
userId: string,
): Promise<any | null> {
): Promise<JumpHostConfig | null> {
try {
const hosts = await SimpleDBOps.select(
getDb()
@@ -150,17 +170,16 @@ async function resolveJumpHost(
const credential = credentials[0];
return {
...host,
password: credential.password,
key:
credential.private_key || credential.privateKey || credential.key,
keyPassword: credential.key_password || credential.keyPassword,
keyType: credential.key_type || credential.keyType,
authType: credential.auth_type || credential.authType,
};
password: credential.password as string | undefined,
key: credential.privateKey as string | undefined,
keyPassword: credential.keyPassword as string | undefined,
keyType: credential.keyType as string | undefined,
authType: credential.authType as string | undefined,
} as JumpHostConfig;
}
}
return host;
return host as JumpHostConfig;
} catch (error) {
sshLogger.error("Failed to resolve jump host", error, {
operation: "resolve_jump_host",
@@ -174,6 +193,7 @@ async function resolveJumpHost(
async function createJumpHostChain(
jumpHosts: Array<{ hostId: number }>,
userId: string,
socks5Config?: SOCKS5Config | null,
): Promise<SSHClient | null> {
if (!jumpHosts || jumpHosts.length === 0) {
return null;
@@ -187,19 +207,33 @@ async function createJumpHostChain(
jumpHosts.map((jh) => resolveJumpHost(jh.hostId, userId)),
);
const totalHops = jumpHostConfigs.length;
for (let i = 0; i < jumpHostConfigs.length; i++) {
if (!jumpHostConfigs[i]) {
sshLogger.error(`Jump host ${i + 1} not found`, undefined, {
operation: "jump_host_chain",
hostId: jumpHosts[i].hostId,
hopIndex: i,
totalHops,
});
clients.forEach((c) => c.end());
return null;
}
}
let proxySocket: import("net").Socket | null = null;
if (socks5Config?.useSocks5) {
const firstHop = jumpHostConfigs[0]!;
proxySocket = await createSocks5Connection(
firstHop.ip,
firstHop.port || 22,
socks5Config,
);
}
for (let i = 0; i < jumpHostConfigs.length; i++) {
const jumpHostConfig = jumpHostConfigs[i];
const jumpHostConfig = jumpHostConfigs[i]!;
const jumpClient = new SSHClient();
clients.push(jumpClient);
@@ -225,16 +259,29 @@ async function createJumpHostChain(
jumpClient.on("error", (err) => {
clearTimeout(timeout);
sshLogger.error(`Jump host ${i + 1} connection failed`, err, {
operation: "jump_host_connect",
hostId: jumpHostConfig.id,
ip: jumpHostConfig.ip,
});
sshLogger.error(
`Jump host ${i + 1}/${totalHops} connection failed`,
err,
{
operation: "jump_host_connect",
hostId: jumpHostConfig.id,
ip: jumpHostConfig.ip,
hopIndex: i,
totalHops,
previousHop:
i > 0
? jumpHostConfigs[i - 1]?.ip
: proxySocket
? "proxy"
: "direct",
usedProxySocket: i === 0 && !!proxySocket,
},
);
resolve(false);
});
const connectConfig: any = {
host: jumpHostConfig.ip,
const connectConfig: Record<string, unknown> = {
host: jumpHostConfig.ip?.replace(/^\[|\]$/g, "") || jumpHostConfig.ip,
port: jumpHostConfig.port || 22,
username: jumpHostConfig.username,
tryKeyboard: true,
@@ -271,6 +318,9 @@ async function createJumpHostChain(
jumpClient.connect(connectConfig);
},
);
} else if (proxySocket) {
connectConfig.sock = proxySocket;
jumpClient.connect(connectConfig);
} else {
jumpClient.connect(connectConfig);
}
@@ -411,6 +461,10 @@ app.use(
app.use(cookieParser());
app.use(express.json({ limit: "100mb" }));
app.use(express.urlencoded({ limit: "100mb", extended: true }));
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "no-store");
next();
});
const authManager = AuthManager.getInstance();
app.use(authManager.createAuthMiddleware());
@@ -450,7 +504,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
userProvidedPassword,
userProvidedSshKey,
userProvidedKeyPassword,
forceKeyboardInteractive,
useSocks5,
socks5Host,
socks5Port,
@@ -458,7 +511,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
socks5Password,
socks5ProxyChain,
} = req.body;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
const connectionLogs: Array<Omit<LogEntry, "id" | "timestamp">> = [];
@@ -533,8 +586,9 @@ app.post("/docker/ssh/connect", async (req, res) => {
const host = hosts[0] as unknown as SSHHost;
if (host.userId !== userId) {
const { PermissionManager } =
await import("../utils/permission-manager.js");
const { PermissionManager } = await import(
"../utils/permission-manager.js"
);
const permissionManager = PermissionManager.getInstance();
const accessInfo = await permissionManager.canAccessHost(
userId,
@@ -605,11 +659,18 @@ app.post("/docker/ssh/connect", async (req, res) => {
if (pendingTOTPSessions[sessionId]) {
try {
pendingTOTPSessions[sessionId].client.end();
} catch {}
} catch {
// expected
}
delete pendingTOTPSessions[sessionId];
}
let resolvedCredentials: any = {
let resolvedCredentials: {
password?: string;
sshKey?: string;
keyPassword?: string;
authType?: string;
} = {
password: host.password,
sshKey: host.key,
keyPassword: host.keyPassword,
@@ -632,8 +693,9 @@ app.post("/docker/ssh/connect", async (req, res) => {
if (userId !== ownerId) {
try {
const { SharedCredentialManager } =
await import("../utils/shared-credential-manager.js");
const { SharedCredentialManager } = await import(
"../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance();
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
host.id,
@@ -673,11 +735,10 @@ app.post("/docker/ssh/connect", async (req, res) => {
if (credentials.length > 0) {
const credential = credentials[0];
resolvedCredentials = {
password: credential.password,
sshKey:
credential.private_key || credential.privateKey || credential.key,
keyPassword: credential.key_password || credential.keyPassword,
authType: credential.auth_type || credential.authType,
password: credential.password as string | undefined,
sshKey: credential.privateKey as string | undefined,
keyPassword: credential.keyPassword as string | undefined,
authType: credential.authType as string | undefined,
};
}
}
@@ -685,8 +746,8 @@ app.post("/docker/ssh/connect", async (req, res) => {
const client = new SSHClient();
const config: any = {
host: host.ip,
const config: Record<string, unknown> = {
host: host.ip?.replace(/^\[|\]$/g, "") || host.ip,
port: host.port || 22,
username: host.username,
tryKeyboard: true,
@@ -706,6 +767,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
};
if (resolvedCredentials.authType === "none") {
// no credentials needed
} else if (resolvedCredentials.authType === "password") {
if (resolvedCredentials.password) {
config.password = resolvedCredentials.password;
@@ -871,8 +933,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
}
let responseSent = false;
let keyboardInteractiveResponded = false;
connectionLogs.push(
createConnectionLog("info", "dns", `Resolving DNS for ${host.ip}`),
);
@@ -1089,8 +1149,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
if (responseSent) return;
responseSent = true;
keyboardInteractiveResponded = true;
pendingTOTPSessions[sessionId] = {
client,
finish,
@@ -1158,8 +1216,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
return;
}
keyboardInteractiveResponded = true;
pendingTOTPSessions[sessionId] = {
client,
finish,
@@ -1244,8 +1300,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
return;
}
keyboardInteractiveResponded = true;
pendingTOTPSessions[sessionId] = {
client,
finish,
@@ -1283,121 +1337,160 @@ app.post("/docker/ssh/connect", async (req, res) => {
},
);
if (
const proxyConfig: SOCKS5Config | null =
useSocks5 &&
(socks5Host || (socks5ProxyChain && (socks5ProxyChain as any).length > 0))
) {
try {
connectionLogs.push(
createConnectionLog("info", "proxy", "Connecting via SOCKS5 proxy"),
);
const socks5Socket = await createSocks5Connection(
host.ip,
host.port || 22,
{
(socks5Host ||
(socks5ProxyChain && (socks5ProxyChain as ProxyNode[]).length > 0))
? {
useSocks5,
socks5Host,
socks5Port,
socks5Username,
socks5Password,
socks5ProxyChain: socks5ProxyChain as any,
},
socks5ProxyChain: socks5ProxyChain as ProxyNode[],
}
: null;
const hasJumpHosts = host.jumpHosts && host.jumpHosts.length > 0;
if (hasJumpHosts) {
try {
if (proxyConfig) {
connectionLogs.push(
createConnectionLog(
"info",
"proxy",
"Connecting via proxy + jump hosts",
),
);
}
connectionLogs.push(
createConnectionLog(
"info",
"jump",
`Connecting via ${host.jumpHosts!.length} jump host(s)`,
),
);
const jumpClient = await createJumpHostChain(
host.jumpHosts as Array<{ hostId: number }>,
userId,
proxyConfig,
);
if (socks5Socket) {
config.sock = socks5Socket;
client.connect(config);
return;
if (!jumpClient) {
connectionLogs.push(
createConnectionLog(
"error",
"jump",
"Failed to establish jump host chain",
),
);
return res.status(500).json({
error: "Failed to establish jump host chain",
connectionLogs,
});
}
} catch (socks5Error) {
sshLogger.error("SOCKS5 connection failed", socks5Error, {
operation: "docker_socks5_connect",
jumpClient.forwardOut(
"127.0.0.1",
0,
host.ip,
host.port || 22,
(err, stream) => {
if (err) {
sshLogger.error("Failed to forward through jump host", err, {
operation: "docker_jump_forward",
sessionId,
hostId,
});
connectionLogs.push(
createConnectionLog(
"error",
"jump",
`Failed to forward through jump host: ${err.message}`,
),
);
jumpClient.end();
if (!responseSent) {
responseSent = true;
return res.status(500).json({
error: "Failed to forward through jump host: " + err.message,
connectionLogs,
});
}
return;
}
config.sock = stream;
client.connect(config);
},
);
} catch (jumpError) {
sshLogger.error("Jump host connection failed", jumpError, {
operation: "docker_jump_connect",
sessionId,
hostId,
proxyHost: socks5Host,
proxyPort: socks5Port || 1080,
});
connectionLogs.push(
createConnectionLog(
"error",
"proxy",
`SOCKS5 proxy connection failed: ${socks5Error instanceof Error ? socks5Error.message : "Unknown error"}`,
"jump",
`Jump host connection failed: ${jumpError instanceof Error ? jumpError.message : "Unknown error"}`,
),
);
if (!responseSent) {
responseSent = true;
return res.status(500).json({
error:
"SOCKS5 proxy connection failed: " +
(socks5Error instanceof Error
? socks5Error.message
"Jump host connection failed: " +
(jumpError instanceof Error
? jumpError.message
: "Unknown error"),
connectionLogs,
});
}
return;
}
} else if (host.jumpHosts && host.jumpHosts.length > 0) {
} else if (proxyConfig) {
connectionLogs.push(
createConnectionLog(
"info",
"jump",
`Connecting via ${host.jumpHosts.length} jump host(s)`,
),
createConnectionLog("info", "proxy", "Connecting via proxy"),
);
const jumpClient = await createJumpHostChain(
host.jumpHosts as Array<{ hostId: number }>,
userId,
);
if (!jumpClient) {
try {
const proxySocket = await createSocks5Connection(
host.ip,
host.port || 22,
proxyConfig,
);
if (proxySocket) {
config.sock = proxySocket;
}
client.connect(config);
} catch (proxyError) {
sshLogger.error("Proxy connection failed", proxyError, {
operation: "docker_proxy_connect",
sessionId,
hostId,
});
connectionLogs.push(
createConnectionLog(
"error",
"jump",
"Failed to establish jump host chain",
"proxy",
`Proxy connection failed: ${proxyError instanceof Error ? proxyError.message : "Unknown error"}`,
),
);
return res.status(500).json({
error: "Failed to establish jump host chain",
connectionLogs,
});
if (!responseSent) {
responseSent = true;
return res.status(500).json({
error:
"Proxy connection failed: " +
(proxyError instanceof Error
? proxyError.message
: "Unknown error"),
connectionLogs,
});
}
return;
}
jumpClient.forwardOut(
"127.0.0.1",
0,
host.ip,
host.port || 22,
(err, stream) => {
if (err) {
sshLogger.error("Failed to forward through jump host", err, {
operation: "docker_jump_forward",
sessionId,
hostId,
});
connectionLogs.push(
createConnectionLog(
"error",
"jump",
`Failed to forward through jump host: ${err.message}`,
),
);
jumpClient.end();
if (!responseSent) {
responseSent = true;
return res.status(500).json({
error: "Failed to forward through jump host: " + err.message,
connectionLogs,
});
}
return;
}
config.sock = stream;
client.connect(config);
},
);
} else {
client.connect(config);
}
@@ -1489,7 +1582,7 @@ app.post("/docker/ssh/disconnect", async (req, res) => {
*/
app.post("/docker/ssh/connect-totp", async (req, res) => {
const { sessionId, totpCode } = req.body;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
sshLogger.error("TOTP verification rejected: no authenticated user", {
@@ -1521,7 +1614,9 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
delete pendingTOTPSessions[sessionId];
try {
session.client.end();
} catch {}
} catch {
// expected
}
sshLogger.warn("TOTP session timeout before code submission", {
operation: "docker_totp_verify",
sessionId,
@@ -1544,7 +1639,19 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
});
let responseSent = false;
let responseTimeout: NodeJS.Timeout;
const responseTimeout = setTimeout(() => {
if (!responseSent) {
responseSent = true;
delete pendingTOTPSessions[sessionId];
sshLogger.warn("TOTP verification timeout", {
operation: "docker_totp_verify",
sessionId,
userId,
});
res.status(408).json({ error: "TOTP verification timeout" });
}
}, 60000);
session.client.once("ready", () => {
if (responseSent) return;
@@ -1633,19 +1740,6 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
res.status(401).json({ status: "error", message: "Invalid TOTP code" });
});
responseTimeout = setTimeout(() => {
if (!responseSent) {
responseSent = true;
delete pendingTOTPSessions[sessionId];
sshLogger.warn("TOTP verification timeout", {
operation: "docker_totp_verify",
sessionId,
userId,
});
res.status(408).json({ error: "TOTP verification timeout" });
}
}, 60000);
session.finish(responses);
});
@@ -1679,7 +1773,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
*/
app.post("/docker/ssh/connect-warpgate", async (req, res) => {
const { sessionId } = req.body;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
sshLogger.error("Warpgate verification rejected: no authenticated user", {
@@ -1715,7 +1809,9 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
delete pendingTOTPSessions[sessionId];
try {
session.client.end();
} catch {}
} catch {
// expected
}
sshLogger.warn("Warpgate session timeout before completion", {
operation: "docker_warpgate_verify",
sessionId,
@@ -1728,7 +1824,19 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
}
let responseSent = false;
let responseTimeout: NodeJS.Timeout;
const responseTimeout = setTimeout(() => {
if (!responseSent) {
responseSent = true;
delete pendingTOTPSessions[sessionId];
sshLogger.warn("Warpgate verification timeout", {
operation: "docker_warpgate_verify",
sessionId,
userId,
});
res.status(408).json({ error: "Warpgate verification timeout" });
}
}, 60000);
session.client.once("ready", () => {
if (responseSent) return;
@@ -1819,19 +1927,6 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
.json({ status: "error", message: "Warpgate authentication failed" });
});
responseTimeout = setTimeout(() => {
if (!responseSent) {
responseSent = true;
delete pendingTOTPSessions[sessionId];
sshLogger.warn("Warpgate verification timeout", {
operation: "docker_warpgate_verify",
sessionId,
userId,
});
res.status(408).json({ error: "Warpgate verification timeout" });
}
}, 60000);
session.finish([""]);
});
@@ -1941,7 +2036,7 @@ app.get("/docker/ssh/status", async (req, res) => {
*/
app.get("/docker/validate/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
@@ -2020,7 +2115,7 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
code: "DOCKER_ERROR",
});
}
} catch (installError) {
} catch {
session.activeOperations--;
return res.json({
available: false,
@@ -2073,7 +2168,7 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
app.get("/docker/containers/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const all = req.query.all !== "false";
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
@@ -2115,7 +2210,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
.map((line) => {
try {
return JSON.parse(line);
} catch (e) {
} catch {
sshLogger.warn("Failed to parse container line", {
operation: "parse_container",
line,
@@ -2174,7 +2269,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
*/
app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
const { sessionId, containerId } = req.params;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
@@ -2269,7 +2364,7 @@ app.post(
"/docker/containers/:sessionId/:containerId/start",
async (req, res) => {
const { sessionId, containerId } = req.params;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
@@ -2369,7 +2464,7 @@ app.post(
"/docker/containers/:sessionId/:containerId/stop",
async (req, res) => {
const { sessionId, containerId } = req.params;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
@@ -2469,7 +2564,7 @@ app.post(
"/docker/containers/:sessionId/:containerId/restart",
async (req, res) => {
const { sessionId, containerId } = req.params;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
@@ -2569,7 +2664,7 @@ app.post(
"/docker/containers/:sessionId/:containerId/pause",
async (req, res) => {
const { sessionId, containerId } = req.params;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
@@ -2669,7 +2764,7 @@ app.post(
"/docker/containers/:sessionId/:containerId/unpause",
async (req, res) => {
const { sessionId, containerId } = req.params;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
@@ -2774,7 +2869,7 @@ app.delete(
async (req, res) => {
const { sessionId, containerId } = req.params;
const force = req.query.force === "true";
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
@@ -2902,7 +2997,7 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => {
const timestamps = req.query.timestamps === "true";
const since = req.query.since as string;
const until = req.query.until as string;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
@@ -3011,7 +3106,7 @@ app.get(
"/docker/containers/:sessionId/:containerId/stats",
async (req, res) => {
const { sessionId, containerId } = req.params;
const userId = (req as any).userId;
const userId = (req as unknown as { userId: string }).userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
File diff suppressed because it is too large Load Diff
-1
View File
@@ -3,7 +3,6 @@ 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";
+15 -7
View File
@@ -51,7 +51,9 @@ const cleanupInProgress = new Set<string>();
export function getRequestOrigin(req: IncomingMessage): string {
const protoHeader =
req.headers["x-forwarded-proto"] ||
((req.socket as any).encrypted ? "https" : "http");
((req.socket as unknown as { encrypted?: boolean }).encrypted
? "https"
: "http");
const proto =
typeof protoHeader === "string"
? protoHeader.split(",")[0].trim()
@@ -157,7 +159,7 @@ async function checkOPKConfigExists(): Promise<{
return {
exists: false,
configPath,
error: `OPKSSH configuration is missing 'redirect_uris' field.`,
error: `OPKSSH configuration is missing 'redirect_uris' field. This field must contain the Termix callback URL that you registered with your OAuth provider (e.g., http://localhost:8080/ssh/opkssh-callback for Docker). The static callback route will internally redirect to the dynamic route for proper URL rewriting.`,
};
}
@@ -235,11 +237,11 @@ export async function startOPKSSHAuth(
try {
const binaryPath = OPKSSHBinaryManager.getBinaryPath();
const configPath = getOPKConfigPath();
const configDir = path.dirname(configPath);
const args = [
"login",
"--print-key",
"--disable-browser-open",
`--config-path=${configPath}`,
`--remote-redirect-uri=${remoteRedirectUri}`,
];
@@ -284,7 +286,10 @@ export async function startOPKSSHAuth(
opksshProcess.stderr?.on("data", async (data) => {
const stderr = data.toString();
if (stderr.includes("Opening browser to")) {
if (
stderr.includes("Opening browser to") ||
stderr.includes("Open your browser to:")
) {
handleOPKSSHOutput(requestId, stderr);
}
@@ -371,7 +376,7 @@ function handleOPKSSHOutput(requestId: string, output: string): void {
session.stdoutBuffer += output;
const chooserUrlMatch = session.stdoutBuffer.match(
/Opening browser to http:\/\/localhost:(\d+)\/chooser/,
/(?:Opening browser to|Open your browser to:)\s*http:\/\/localhost:(\d+)\/chooser/,
);
if (chooserUrlMatch && session.status === "starting") {
const actualPort = parseInt(chooserUrlMatch[1], 10);
@@ -784,7 +789,10 @@ export function getActiveSessionsAll(): OPKSSHAuthSession[] {
return Array.from(activeAuthSessions.values());
}
export async function getUserIdFromRequest(req: any): Promise<string | null> {
export async function getUserIdFromRequest(req: {
cookies?: Record<string, string>;
headers: Record<string, string | undefined>;
}): Promise<string | null> {
try {
const { AuthManager } = await import("../utils/auth-manager.js");
const authManager = AuthManager.getInstance();
@@ -797,7 +805,7 @@ export async function getUserIdFromRequest(req: any): Promise<string | null> {
const decoded = await authManager.verifyJWTToken(token);
return decoded?.userId || null;
} catch (error) {
} catch {
return null;
}
}
File diff suppressed because it is too large Load Diff
+225
View File
@@ -0,0 +1,225 @@
import { Client } from "ssh2";
import { sshLogger } from "../utils/logger.js";
interface PooledConnection {
client: Client;
lastUsed: number;
inUse: boolean;
hostKey: string;
}
class SSHConnectionPool {
private connections = new Map<string, PooledConnection[]>();
private maxConnectionsPerHost = 3;
private cleanupInterval: NodeJS.Timeout;
constructor() {
this.cleanupInterval = setInterval(
() => {
this.cleanup();
},
2 * 60 * 1000,
);
}
private isConnectionHealthy(client: Client): boolean {
try {
const sock = (
client as unknown as {
_sock?: { destroyed?: boolean; writable?: boolean };
}
)._sock;
if (sock && (sock.destroyed || !sock.writable)) {
return false;
}
return true;
} catch {
return false;
}
}
async getConnection(
key: string,
factory: () => Promise<Client>,
): Promise<Client> {
let connections = this.connections.get(key) || [];
const available = connections.find((conn) => !conn.inUse);
if (available) {
if (!this.isConnectionHealthy(available.client)) {
sshLogger.warn("Removing unhealthy connection from pool", {
operation: "pool_remove_dead",
hostKey: key,
});
try {
available.client.end();
} catch {
// expected
}
connections = connections.filter((c) => c !== available);
this.connections.set(key, connections);
} else {
available.inUse = true;
available.lastUsed = Date.now();
return available.client;
}
}
if (connections.length < this.maxConnectionsPerHost) {
const client = await factory();
const pooled: PooledConnection = {
client,
lastUsed: Date.now(),
inUse: true,
hostKey: key,
};
connections.push(pooled);
this.connections.set(key, connections);
client.on("end", () => {
this.removeConnection(key, client);
});
client.on("close", () => {
this.removeConnection(key, client);
});
return client;
}
return new Promise((resolve) => {
const checkAvailable = () => {
const conns = this.connections.get(key) || [];
const avail = conns.find((conn) => !conn.inUse);
if (avail) {
if (!this.isConnectionHealthy(avail.client)) {
try {
avail.client.end();
} catch {
// expected
}
const filtered = conns.filter((c) => c !== avail);
this.connections.set(key, filtered);
factory().then((client) => {
const pooled: PooledConnection = {
client,
lastUsed: Date.now(),
inUse: true,
hostKey: key,
};
filtered.push(pooled);
this.connections.set(key, filtered);
client.on("end", () => this.removeConnection(key, client));
client.on("close", () => this.removeConnection(key, client));
resolve(client);
});
} else {
avail.inUse = true;
avail.lastUsed = Date.now();
resolve(avail.client);
}
} else {
setTimeout(checkAvailable, 100);
}
};
checkAvailable();
});
}
releaseConnection(key: string, client: Client): void {
const connections = this.connections.get(key) || [];
const pooled = connections.find((conn) => conn.client === client);
if (pooled) {
pooled.inUse = false;
pooled.lastUsed = Date.now();
}
}
private removeConnection(key: string, client: Client): void {
const connections = this.connections.get(key);
if (!connections) return;
const filtered = connections.filter((c) => c.client !== client);
if (filtered.length === 0) {
this.connections.delete(key);
} else {
this.connections.set(key, filtered);
}
}
clearKeyConnections(key: string): void {
const connections = this.connections.get(key) || [];
for (const conn of connections) {
try {
conn.client.end();
} catch {
// expected
}
}
this.connections.delete(key);
}
private cleanup(): void {
const now = Date.now();
const maxAge = 10 * 60 * 1000;
for (const [hostKey, connections] of this.connections.entries()) {
const activeConnections = connections.filter((conn) => {
if (!conn.inUse && now - conn.lastUsed > maxAge) {
try {
conn.client.end();
} catch {
// expected
}
return false;
}
if (!this.isConnectionHealthy(conn.client)) {
try {
conn.client.end();
} catch {
// expected
}
return false;
}
return true;
});
if (activeConnections.length === 0) {
this.connections.delete(hostKey);
} else {
this.connections.set(hostKey, activeConnections);
}
}
}
clearAllConnections(): void {
for (const connections of this.connections.values()) {
for (const conn of connections) {
try {
conn.client.end();
} catch {
// expected
}
}
}
this.connections.clear();
}
destroy(): void {
clearInterval(this.cleanupInterval);
this.clearAllConnections();
}
}
export const connectionPool = new SSHConnectionPool();
export async function withConnection<T>(
key: string,
factory: () => Promise<Client>,
fn: (client: Client) => Promise<T>,
): Promise<T> {
const client = await connectionPool.getConnection(key, factory);
try {
return await fn(client);
} finally {
connectionPool.releaseConnection(key, client);
}
}
+489
View File
@@ -0,0 +1,489 @@
import { type Client, type ClientChannel } from "ssh2";
import { WebSocket } from "ws";
import { sshLogger } from "../utils/logger.js";
import { getDb } from "../database/db/index.js";
const MAX_BUFFER_BYTES = 512 * 1024;
const DEFAULT_TIMEOUT_MINUTES = 30;
const HEALTH_CHECK_INTERVAL_MS = 60_000;
const MAX_SESSIONS_PER_USER = 10;
export interface TerminalSession {
id: string;
userId: string;
hostId: number;
hostName: string;
tabInstanceId?: string;
attachedTabInstanceId?: string;
sshConn: Client | null;
sshStream: ClientChannel | null;
jumpClient: Client | null;
opksshTempFiles: { keyPath: string; certPath: string } | null;
cols: number;
rows: number;
isConnected: boolean;
createdAt: number;
attachedWs: WebSocket | null;
lastDetachedAt: number | null;
detachTimeout: NodeJS.Timeout | null;
outputBuffer: string[];
outputBufferBytes: number;
}
class TerminalSessionManager {
private static instance: TerminalSessionManager;
private sessions = new Map<string, TerminalSession>();
private healthCheckTimer: NodeJS.Timeout | null = null;
private constructor() {
this.healthCheckTimer = setInterval(
() => this.healthCheck(),
HEALTH_CHECK_INTERVAL_MS,
);
}
static getInstance(): TerminalSessionManager {
if (!TerminalSessionManager.instance) {
TerminalSessionManager.instance = new TerminalSessionManager();
}
return TerminalSessionManager.instance;
}
createSession(
userId: string,
hostId: number,
hostName: string,
cols: number,
rows: number,
tabInstanceId?: string,
): string {
const userSessions = this.getUserSessions(userId);
if (userSessions.length >= MAX_SESSIONS_PER_USER) {
const detached = userSessions
.filter((s) => s.attachedWs === null)
.sort(
(a, b) =>
(a.lastDetachedAt ?? a.createdAt) -
(b.lastDetachedAt ?? b.createdAt),
);
if (detached.length > 0) {
this.destroySession(detached[0].id);
}
}
if (tabInstanceId) {
const tabSessions = userSessions.filter(
(s) => s.tabInstanceId === tabInstanceId,
);
if (tabSessions.length > 0) {
sshLogger.warn("Tab instance already has session, destroying old", {
operation: "session_tab_duplicate_cleanup",
existingSessionId: tabSessions[0].id,
tabInstanceId,
});
this.destroySession(tabSessions[0].id);
}
}
const id = crypto.randomUUID();
const session: TerminalSession = {
id,
userId,
hostId,
hostName,
tabInstanceId,
sshConn: null,
sshStream: null,
jumpClient: null,
opksshTempFiles: null,
cols,
rows,
isConnected: false,
createdAt: Date.now(),
attachedWs: null,
lastDetachedAt: null,
detachTimeout: null,
outputBuffer: [],
outputBufferBytes: 0,
};
this.sessions.set(id, session);
sshLogger.info("Terminal session created", {
operation: "session_created",
sessionId: id,
userId,
hostId,
});
return id;
}
getSession(sessionId: string | null): TerminalSession | null {
if (!sessionId) return null;
return this.sessions.get(sessionId) ?? null;
}
setSSHState(
sessionId: string,
conn: Client,
stream: ClientChannel,
jumpClient?: Client | null,
opksshTempFiles?: { keyPath: string; certPath: string } | null,
): void {
const session = this.sessions.get(sessionId);
if (!session) return;
session.sshConn = conn;
session.sshStream = stream;
session.jumpClient = jumpClient ?? null;
session.opksshTempFiles = opksshTempFiles ?? null;
session.isConnected = true;
}
attachWs(
sessionId: string,
userId: string,
ws: WebSocket,
tabInstanceId?: string,
): TerminalSession | null {
const session = this.sessions.get(sessionId);
if (!session) {
sshLogger.warn("Session not found for attachment", {
operation: "session_attach_not_found",
sessionId,
userId,
});
return null;
}
if (session.userId !== userId) {
sshLogger.warn("Session userId mismatch", {
operation: "session_attach_user_mismatch",
sessionId,
expectedUserId: session.userId,
providedUserId: userId,
});
return null;
}
if (!session.isConnected) {
sshLogger.warn("Session not connected", {
operation: "session_attach_not_connected",
sessionId,
userId,
createdAt: session.createdAt,
elapsed: Date.now() - session.createdAt,
});
return null;
}
const isDetached =
!session.attachedWs || session.attachedWs.readyState !== WebSocket.OPEN;
const isOriginalTab = session.tabInstanceId === tabInstanceId;
if (
!isDetached &&
!isOriginalTab &&
session.tabInstanceId &&
tabInstanceId
) {
sshLogger.warn("Session actively attached to different tab instance", {
operation: "session_attach_instance_conflict",
sessionId,
sessionInstanceId: session.tabInstanceId,
providedInstanceId: tabInstanceId,
});
try {
ws.send(
JSON.stringify({
type: "sessionExpired",
sessionId,
message: "Session belongs to a different tab instance",
}),
);
} catch {
/* ignore */
}
return null;
}
if (
session.tabInstanceId &&
tabInstanceId &&
session.tabInstanceId !== tabInstanceId
) {
sshLogger.info(
"Session attached to different tab instance (split-screen)",
{
operation: "session_attach_split_screen",
originalInstanceId: session.tabInstanceId,
newInstanceId: tabInstanceId,
sessionId,
},
);
}
if (session.attachedWs && session.attachedWs !== ws) {
try {
session.attachedWs.send(
JSON.stringify({
type: "sessionTakenOver",
sessionId,
message: "Session was attached from another tab",
}),
);
} catch {
/* ignore */
}
session.attachedWs = null;
}
if (session.detachTimeout) {
clearTimeout(session.detachTimeout);
session.detachTimeout = null;
}
session.attachedWs = ws;
session.attachedTabInstanceId = tabInstanceId;
session.lastDetachedAt = null;
sshLogger.info("WebSocket attached to session", {
operation: "session_attach",
sessionId,
userId,
tabInstanceId,
});
return session;
}
detachWs(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (!session) return;
if (session.detachTimeout) {
clearTimeout(session.detachTimeout);
session.detachTimeout = null;
}
session.attachedWs = null;
session.lastDetachedAt = Date.now();
const timeoutMs = this.getTimeoutMs();
session.detachTimeout = setTimeout(() => {
sshLogger.info("Session idle timeout expired", {
operation: "session_idle_timeout",
sessionId,
userId: session.userId,
});
this.destroySession(sessionId);
}, timeoutMs);
sshLogger.info("WebSocket detached from session", {
operation: "session_detach",
sessionId,
userId: session.userId,
timeoutMinutes: timeoutMs / 60_000,
});
}
destroySession(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (!session) return;
if (session.detachTimeout) {
clearTimeout(session.detachTimeout);
session.detachTimeout = null;
}
if (session.sshStream) {
try {
session.sshStream.end();
} catch {
/* ignore */
}
session.sshStream = null;
}
if (session.sshConn) {
try {
session.sshConn.end();
} catch {
/* ignore */
}
session.sshConn = null;
}
if (session.jumpClient) {
try {
session.jumpClient.end();
} catch {
/* ignore */
}
session.jumpClient = null;
}
if (session.opksshTempFiles) {
const tempFiles = session.opksshTempFiles;
session.opksshTempFiles = null;
this.cleanupOpksshFiles(tempFiles);
}
session.isConnected = false;
session.outputBuffer = [];
session.outputBufferBytes = 0;
this.sessions.delete(sessionId);
sshLogger.info("Terminal session destroyed", {
operation: "session_destroyed",
sessionId,
userId: session.userId,
hostId: session.hostId,
});
}
getUserSessions(userId: string): TerminalSession[] {
const result: TerminalSession[] = [];
for (const session of this.sessions.values()) {
if (session.userId === userId) {
result.push(session);
}
}
return result;
}
bufferOutput(sessionId: string, data: string): void {
const session = this.sessions.get(sessionId);
if (!session) return;
session.outputBuffer.push(data);
session.outputBufferBytes += data.length;
while (
session.outputBufferBytes > MAX_BUFFER_BYTES &&
session.outputBuffer.length > 0
) {
const removed = session.outputBuffer.shift();
if (removed) session.outputBufferBytes -= removed.length;
}
}
flushBuffer(session: TerminalSession): string | null {
if (session.outputBuffer.length === 0) return null;
const data = session.outputBuffer.join("");
session.outputBuffer = [];
session.outputBufferBytes = 0;
return data;
}
getBuffer(session: TerminalSession): string | null {
if (session.outputBuffer.length === 0) return null;
return session.outputBuffer.join("");
}
private getTimeoutMs(): number {
try {
const db = getDb();
const row = db.$client
.prepare(
"SELECT value FROM settings WHERE key = 'terminal_session_timeout_minutes'",
)
.get() as { value: string } | undefined;
if (row) {
const minutes = parseInt(row.value, 10);
if (!isNaN(minutes) && minutes > 0) {
return minutes * 60_000;
}
}
} catch {
// DB not available, use default
}
return DEFAULT_TIMEOUT_MINUTES * 60_000;
}
private healthCheck(): void {
const toDestroy: string[] = [];
const now = Date.now();
const GRACE_PERIOD_MS = 10_000;
for (const [id, session] of this.sessions) {
if (!session.isConnected) continue;
if (
session.attachedWs &&
session.attachedWs.readyState === WebSocket.OPEN
) {
continue;
}
if (session.sshStream?.destroyed) {
const detachedDuration = session.lastDetachedAt
? now - session.lastDetachedAt
: 0;
if (detachedDuration > GRACE_PERIOD_MS) {
sshLogger.info(
"SSH stream destroyed during detach window, cleaning up",
{
operation: "session_health_check_stream_destroyed",
sessionId: id,
userId: session.userId,
detachedFor: detachedDuration,
},
);
toDestroy.push(id);
}
}
if (!session.sshConn) {
toDestroy.push(id);
}
}
for (const id of toDestroy) {
this.destroySession(id);
}
}
private async cleanupOpksshFiles(tempFiles: {
keyPath: string;
certPath: string;
}): Promise<void> {
try {
const { promises: fs } = await import("fs");
const results = await Promise.allSettled([
fs.unlink(tempFiles.keyPath),
fs.unlink(tempFiles.certPath),
]);
results.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",
});
}
});
} catch (error) {
sshLogger.error("Failed to cleanup OPKSSH temp files", {
operation: "opkssh_temp_cleanup_error",
error,
});
}
}
destroyAll(): void {
for (const id of [...this.sessions.keys()]) {
this.destroySession(id);
}
if (this.healthCheckTimer) {
clearInterval(this.healthCheckTimer);
this.healthCheckTimer = null;
}
}
}
export const sessionManager = TerminalSessionManager.getInstance();
File diff suppressed because it is too large Load Diff
+205 -234
View File
@@ -6,7 +6,7 @@ import { ChildProcess } from "child_process";
import axios from "axios";
import { getDb } from "../database/db/index.js";
import { sshCredentials } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { eq } from "drizzle-orm";
import type {
SSHHost,
TunnelConfig,
@@ -16,13 +16,14 @@ import type {
AuthenticatedRequest,
} from "../../types/index.js";
import { CONNECTION_STATES } from "../../types/index.js";
import { tunnelLogger, sshLogger } from "../utils/logger.js";
import { tunnelLogger } from "../utils/logger.js";
import { SystemCrypto } from "../utils/system-crypto.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { DataCrypto } from "../utils/data-crypto.js";
import { createSocks5Connection } from "../utils/socks5-helper.js";
import { AuthManager } from "../utils/auth-manager.js";
import { PermissionManager } from "../utils/permission-manager.js";
import { withConnection } from "./ssh-connection-pool.js";
const app = express();
app.use(
@@ -61,6 +62,10 @@ app.use(
);
app.use(cookieParser());
app.use(express.json());
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "no-store");
next();
});
const authManager = AuthManager.getInstance();
const permissionManager = PermissionManager.getInstance();
@@ -603,8 +608,9 @@ async function connectSSHTunnel(
tunnelConfig.requestingUserId &&
tunnelConfig.requestingUserId !== tunnelConfig.sourceUserId
) {
const { SharedCredentialManager } =
await import("../utils/shared-credential-manager.js");
const { SharedCredentialManager } = await import(
"../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance();
if (tunnelConfig.sourceHostId) {
@@ -655,16 +661,10 @@ async function connectSSHTunnel(
const credential = credentials[0];
resolvedSourceCredentials = {
password: credential.password as string | undefined,
sshKey: (credential.private_key ||
credential.privateKey ||
credential.key) as string | undefined,
keyPassword: (credential.key_password ||
credential.keyPassword) as string | undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type ||
credential.authType) as string,
sshKey: credential.privateKey as string | undefined,
keyPassword: credential.keyPassword as string | undefined,
keyType: credential.keyType as string | undefined,
authMethod: credential.authType as string,
};
}
}
@@ -746,16 +746,10 @@ async function connectSSHTunnel(
const credential = credentials[0];
resolvedEndpointCredentials = {
password: credential.password as string | undefined,
sshKey: (credential.private_key ||
credential.privateKey ||
credential.key) as string | undefined,
keyPassword: (credential.key_password || credential.keyPassword) as
| string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type || credential.authType) as string,
sshKey: credential.privateKey as string | undefined,
keyPassword: credential.keyPassword as string | undefined,
keyType: credential.keyType as string | undefined,
authMethod: credential.authType as string,
};
} else {
tunnelLogger.warn("No endpoint credentials found in database", {
@@ -802,7 +796,9 @@ async function connectSSHTunnel(
try {
conn.end();
} catch (error) {}
} catch {
// expected
}
activeTunnels.delete(tunnelName);
@@ -996,7 +992,9 @@ async function connectSSHTunnel(
const verification = tunnelVerifications.get(tunnelName);
if (verification?.timeout) clearTimeout(verification.timeout);
verification?.conn.end();
} catch (error) {}
} catch {
// expected
}
tunnelVerifications.delete(tunnelName);
}
@@ -1110,7 +1108,8 @@ async function connectSSHTunnel(
});
const connOptions: Record<string, unknown> = {
host: tunnelConfig.sourceIP,
host:
tunnelConfig.sourceIP?.replace(/^\[|\]$/g, "") || tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
tryKeyboard: true,
@@ -1347,16 +1346,10 @@ async function killRemoteTunnelByMarker(
const credential = credentials[0];
resolvedSourceCredentials = {
password: credential.password as string | undefined,
sshKey: (credential.private_key ||
credential.privateKey ||
credential.key) as string | undefined,
keyPassword: (credential.key_password || credential.keyPassword) as
| string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type || credential.authType) as string,
sshKey: credential.privateKey as string | undefined,
keyPassword: credential.keyPassword as string | undefined,
keyType: credential.keyType as string | undefined,
authMethod: credential.authType as string,
};
}
}
@@ -1369,50 +1362,6 @@ async function killRemoteTunnelByMarker(
}
}
const conn = new Client();
const connOptions: Record<string, unknown> = {
host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 15000,
algorithms: {
kex: [
"diffie-hellman-group14-sha256",
"diffie-hellman-group14-sha1",
"diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group-exchange-sha1",
"ecdh-sha2-nistp256",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp521",
],
cipher: [
"aes128-ctr",
"aes192-ctr",
"aes256-ctr",
"aes128-gcm@openssh.com",
"aes256-gcm@openssh.com",
"aes128-cbc",
"aes192-cbc",
"aes256-cbc",
"3des-cbc",
],
hmac: [
"hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256",
"hmac-sha2-512",
"hmac-sha1",
"hmac-md5",
],
compress: ["none", "zlib@openssh.com", "zlib"],
},
};
if (
resolvedSourceCredentials.authMethod === "key" &&
resolvedSourceCredentials.sshKey
@@ -1424,151 +1373,83 @@ async function killRemoteTunnelByMarker(
callback(new Error("Invalid SSH key format"));
return;
}
const cleanKey = resolvedSourceCredentials.sshKey
.trim()
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
connOptions.privateKey = Buffer.from(cleanKey, "utf8");
if (resolvedSourceCredentials.keyPassword) {
connOptions.passphrase = resolvedSourceCredentials.keyPassword;
}
if (
resolvedSourceCredentials.keyType &&
resolvedSourceCredentials.keyType !== "auto"
) {
connOptions.privateKeyType = resolvedSourceCredentials.keyType;
}
} else {
connOptions.password = resolvedSourceCredentials.password;
}
conn.on("ready", () => {
const tunnelType = tunnelConfig.tunnelType || "remote";
const tunnelFlag = tunnelType === "local" ? "-L" : "-R";
const checkCmd = `ps aux | grep -E '(${tunnelMarker}|ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}:.*:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}|sshpass.*ssh.*${tunnelFlag})' | grep -v grep`;
const poolKey = `tunnel:${tunnelConfig.sourceUserId}:${tunnelConfig.sourceIP}:${tunnelConfig.sourceSSHPort}:${tunnelConfig.sourceUsername}`;
conn.exec(checkCmd, (_err, stream) => {
let foundProcesses = false;
const factory = async (): Promise<Client> => {
const connOptions: Record<string, unknown> = {
host:
tunnelConfig.sourceIP?.replace(/^\[|\]$/g, "") || tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 15000,
algorithms: {
kex: [
"diffie-hellman-group14-sha256",
"diffie-hellman-group14-sha1",
"diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group-exchange-sha1",
"ecdh-sha2-nistp256",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp521",
],
cipher: [
"aes128-ctr",
"aes192-ctr",
"aes256-ctr",
"aes128-gcm@openssh.com",
"aes256-gcm@openssh.com",
"aes128-cbc",
"aes192-cbc",
"aes256-cbc",
"3des-cbc",
],
hmac: [
"hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256",
"hmac-sha2-512",
"hmac-sha1",
"hmac-md5",
],
compress: ["none", "zlib@openssh.com", "zlib"],
},
};
stream.on("data", (data) => {
const output = data.toString().trim();
if (output) {
foundProcesses = true;
}
});
if (
resolvedSourceCredentials.authMethod === "key" &&
resolvedSourceCredentials.sshKey
) {
const cleanKey = resolvedSourceCredentials.sshKey
.trim()
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
connOptions.privateKey = Buffer.from(cleanKey, "utf8");
if (resolvedSourceCredentials.keyPassword) {
connOptions.passphrase = resolvedSourceCredentials.keyPassword;
}
if (
resolvedSourceCredentials.keyType &&
resolvedSourceCredentials.keyType !== "auto"
) {
connOptions.privateKeyType = resolvedSourceCredentials.keyType;
}
} else {
connOptions.password = resolvedSourceCredentials.password;
}
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}'`,
`sleep 1 && pkill -f 'ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}:.*:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}'`,
`sleep 1 && pkill -f 'sshpass.*ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}'`,
`sleep 2 && pkill -9 -f '${tunnelMarker}'`,
];
let commandIndex = 0;
function executeNextKillCommand() {
if (commandIndex >= killCmds.length) {
conn.exec(checkCmd, (_err, verifyStream) => {
let stillRunning = false;
verifyStream.on("data", (data) => {
const output = data.toString().trim();
if (output) {
stillRunning = true;
tunnelLogger.warn(
`Processes still running after cleanup for '${tunnelName}': ${output}`,
);
}
});
verifyStream.on("close", () => {
if (stillRunning) {
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();
});
});
return;
}
const killCmd = killCmds[commandIndex];
conn.exec(killCmd, (err, stream) => {
if (err) {
tunnelLogger.warn(
`Kill command ${commandIndex + 1} failed for '${tunnelName}': ${err.message}`,
);
}
stream.on("close", () => {
commandIndex++;
executeNextKillCommand();
});
stream.on("data", () => {});
stream.stderr.on("data", (data) => {
const output = data.toString().trim();
if (output && !output.includes("debug1")) {
tunnelLogger.warn(
`Kill command ${commandIndex + 1} stderr for '${tunnelName}': ${output}`,
);
}
});
});
}
executeNextKillCommand();
});
});
});
conn.on("error", (err) => {
tunnelLogger.error(
`Failed to connect to source host for killing tunnel '${tunnelName}': ${err.message}`,
);
callback(err);
});
if (
tunnelConfig.useSocks5 &&
(tunnelConfig.socks5Host ||
(tunnelConfig.socks5ProxyChain &&
tunnelConfig.socks5ProxyChain.length > 0))
) {
(async () => {
if (
tunnelConfig.useSocks5 &&
(tunnelConfig.socks5Host ||
(tunnelConfig.socks5ProxyChain &&
tunnelConfig.socks5ProxyChain.length > 0))
) {
try {
const socks5Socket = await createSocks5Connection(
tunnelConfig.sourceIP,
@@ -1585,9 +1466,8 @@ async function killRemoteTunnelByMarker(
if (socks5Socket) {
connOptions.sock = socks5Socket;
conn.connect(connOptions);
} else {
callback(new Error("Failed to create SOCKS5 connection"));
throw new Error("Failed to create SOCKS5 connection");
}
} catch (socks5Error) {
tunnelLogger.error(
@@ -1600,18 +1480,109 @@ async function killRemoteTunnelByMarker(
proxyPort: tunnelConfig.socks5Port || 1080,
},
);
callback(
new Error(
"SOCKS5 proxy connection failed: " +
(socks5Error instanceof Error
? socks5Error.message
: "Unknown error"),
),
throw new Error(
"SOCKS5 proxy connection failed: " +
(socks5Error instanceof Error
? socks5Error.message
: "Unknown error"),
);
}
})();
} else {
conn.connect(connOptions);
}
return new Promise<Client>((resolve, reject) => {
const conn = new Client();
conn.on("ready", () => resolve(conn));
conn.on("error", (err) => reject(err));
conn.connect(connOptions);
});
};
const execCommand = (client: Client, cmd: string): Promise<string> =>
new Promise((resolve, reject) => {
client.exec(cmd, (err, stream) => {
if (err) {
reject(err);
return;
}
let output = "";
stream.on("data", (data: Buffer) => {
output += data.toString();
});
stream.stderr.on("data", (data: Buffer) => {
const stderr = data.toString().trim();
if (stderr && !stderr.includes("debug1")) {
tunnelLogger.warn(
`Kill command stderr for '${tunnelName}': ${stderr}`,
);
}
});
stream.on("close", () => resolve(output.trim()));
});
});
try {
await withConnection(poolKey, factory, async (client) => {
const tunnelType = tunnelConfig.tunnelType || "remote";
const tunnelFlag = tunnelType === "local" ? "-L" : "-R";
const checkCmd = `ps aux | grep -E '(${tunnelMarker}|ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}:.*:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}|sshpass.*ssh.*${tunnelFlag})' | grep -v grep`;
const checkOutput = await execCommand(client, checkCmd);
if (!checkOutput) {
tunnelLogger.warn("Remote tunnel process not found", {
operation: "tunnel_remote_not_found",
userId: tunnelConfig.sourceUserId,
hostId: tunnelConfig.sourceHostId,
tunnelName,
marker: tunnelMarker,
});
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}'`,
`sleep 1 && pkill -f 'ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}:.*:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}'`,
`sleep 1 && pkill -f 'sshpass.*ssh.*${tunnelFlag}.*${tunnelConfig.endpointPort}'`,
`sleep 2 && pkill -9 -f '${tunnelMarker}'`,
];
for (const killCmd of killCmds) {
try {
await execCommand(client, killCmd);
} catch (err) {
tunnelLogger.warn(
`Kill command failed for '${tunnelName}': ${(err as Error).message}`,
);
}
}
const verifyOutput = await execCommand(client, checkCmd);
if (verifyOutput) {
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,
});
}
});
callback();
} catch (err) {
tunnelLogger.error(
`Failed to connect to source host for killing tunnel '${tunnelName}': ${(err as Error).message}`,
);
callback(err as Error);
}
}
@@ -1750,7 +1721,7 @@ app.post(
if (pendingTunnelOperations.has(tunnelName)) {
try {
await pendingTunnelOperations.get(tunnelName);
} catch (error) {
} catch {
tunnelLogger.warn(`Previous tunnel operation failed`, { tunnelName });
}
}
+4 -4
View File
@@ -1,4 +1,4 @@
import type { Client } from "ssh2";
import type { Client, ClientChannel } from "ssh2";
export function execCommand(
client: Client,
@@ -11,7 +11,7 @@ export function execCommand(
}> {
return new Promise((resolve, reject) => {
let settled = false;
let stream: any = null;
let stream: ClientChannel | null = null;
const timeout = setTimeout(() => {
if (!settled) {
@@ -30,8 +30,8 @@ export function execCommand(
stream.stderr.removeAllListeners();
}
stream.destroy();
} catch (error) {
// Ignore cleanup errors
} catch {
// expected - cleanup errors ignored
}
}
};
+1 -1
View File
@@ -77,7 +77,7 @@ export async function collectCpuMetrics(client: Client): Promise<{
const coresNum = Number((coresOut.stdout || "").trim());
cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null;
} catch (e) {
} catch {
cpuPercent = null;
cores = null;
loadTriplet = null;
+1 -1
View File
@@ -51,7 +51,7 @@ export async function collectDiskMetrics(client: Client): Promise<{
);
}
}
} catch (e) {
} catch {
diskPercent = null;
usedHuman = null;
totalHuman = null;
@@ -1,6 +1,5 @@
import type { Client } from "ssh2";
import { execCommand } from "./common-utils.js";
import { statsLogger } from "../../utils/logger.js";
export interface LoginRecord {
user: string;
@@ -53,7 +52,7 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
parsedTime = isNaN(date.getTime())
? new Date().toISOString()
: date.toISOString();
} catch (e) {
} catch {
parsedTime = new Date().toISOString();
}
@@ -70,7 +69,9 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
}
}
}
} catch (e) {}
} catch {
// expected
}
try {
const failedOut = await execCommand(
@@ -111,7 +112,7 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
parsedTime = isNaN(date.getTime())
? new Date().toISOString()
: date.toISOString();
} catch (e) {
} catch {
parsedTime = new Date().toISOString();
}
@@ -126,7 +127,9 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
}
}
}
} catch (e) {}
} catch {
// expected
}
return {
recentLogins: recentLogins.slice(0, 10),
+1 -1
View File
@@ -27,7 +27,7 @@ export async function collectMemoryMetrics(client: Client): Promise<{
usedGiB = kibToGiB(usedKb);
totalGiB = kibToGiB(totalKb);
}
} catch (e) {
} catch {
memPercent = null;
usedGiB = null;
totalGiB = null;
+3 -2
View File
@@ -1,6 +1,5 @@
import type { Client } from "ssh2";
import { execCommand } from "./common-utils.js";
import { statsLogger } from "../../utils/logger.js";
export async function collectNetworkMetrics(client: Client): Promise<{
interfaces: Array<{
@@ -68,7 +67,9 @@ export async function collectNetworkMetrics(client: Client): Promise<{
txBytes: null,
});
}
} catch (e) {}
} catch {
// expected
}
return { interfaces };
}
@@ -1,6 +1,5 @@
import type { Client } from "ssh2";
import { execCommand } from "./common-utils.js";
import { statsLogger } from "../../utils/logger.js";
export async function collectProcessesMetrics(client: Client): Promise<{
total: number | null;
@@ -54,7 +53,9 @@ export async function collectProcessesMetrics(client: Client): Promise<{
const runningCount2 = Number(runningCount.stdout.trim());
runningProcesses = Number.isFinite(runningCount2) ? runningCount2 : null;
} catch (e) {}
} catch {
// expected
}
return {
total: totalProcesses,
+2 -3
View File
@@ -1,6 +1,5 @@
import type { Client } from "ssh2";
import { execCommand } from "./common-utils.js";
import { statsLogger } from "../../utils/logger.js";
export async function collectSystemMetrics(client: Client): Promise<{
hostname: string | null;
@@ -22,8 +21,8 @@ export async function collectSystemMetrics(client: Client): Promise<{
hostname = hostnameOut.stdout.trim() || null;
kernel = kernelOut.stdout.trim() || null;
os = osOut.stdout.trim() || null;
} catch (e) {
// No error log
} catch {
// expected
}
return {
+3 -2
View File
@@ -1,6 +1,5 @@
import type { Client } from "ssh2";
import { execCommand } from "./common-utils.js";
import { statsLogger } from "../../utils/logger.js";
export async function collectUptimeMetrics(client: Client): Promise<{
seconds: number | null;
@@ -21,7 +20,9 @@ export async function collectUptimeMetrics(client: Client): Promise<{
uptimeFormatted = `${days}d ${hours}h ${minutes}m`;
}
}
} catch (e) {}
} catch {
// expected
}
return {
seconds: uptimeSeconds,
+16 -3
View File
@@ -22,7 +22,9 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
if (persistentConfig.parsed) {
Object.assign(process.env, persistentConfig.parsed);
}
} catch (error) {}
} catch {
// expected - env file may not exist
}
systemLogger.info("Termix backend initialization started", {
operation: "backend_init_start",
@@ -110,8 +112,9 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
await authManager.initialize();
DataCrypto.initialize();
const { OPKSSHBinaryManager } =
await import("./utils/opkssh-binary-manager.js");
const { OPKSSHBinaryManager } = await import(
"./utils/opkssh-binary-manager.js"
);
try {
await OPKSSHBinaryManager.ensureBinary();
} catch (error) {
@@ -163,6 +166,16 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
process.exit(0);
});
process.on("message", (msg: { type?: string }) => {
if (msg?.type === "shutdown") {
systemLogger.info(
"Received IPC shutdown, initiating graceful shutdown...",
{ operation: "shutdown" },
);
process.exit(0);
}
});
process.on("uncaughtException", (error) => {
systemLogger.error("Uncaught exception occurred", error, {
operation: "error_handling",
+161 -30
View File
@@ -5,7 +5,7 @@ import { DataCrypto } from "./data-crypto.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";
import { sessions, trustedDevices } from "../database/db/schema.js";
import { eq, and, sql } from "drizzle-orm";
import { nanoid } from "nanoid";
import type { DeviceType } from "./user-agent-parser.js";
@@ -99,7 +99,7 @@ class AuthManager {
const sessionDurationMs =
deviceType === "desktop" || deviceType === "mobile"
? 30 * 24 * 60 * 60 * 1000
: 7 * 24 * 60 * 60 * 1000;
: 2 * 60 * 60 * 1000;
const authenticated = await this.userCrypto.authenticateOIDCUser(
userId,
@@ -121,7 +121,7 @@ class AuthManager {
const sessionDurationMs =
deviceType === "desktop" || deviceType === "mobile"
? 30 * 24 * 60 * 60 * 1000
: 7 * 24 * 60 * 60 * 1000;
: 2 * 60 * 60 * 1000;
const authenticated = await this.userCrypto.authenticateUser(
userId,
@@ -154,8 +154,9 @@ class AuthManager {
return;
}
const { getSqlite, saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
const { getSqlite, saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const sqlite = getSqlite();
@@ -170,8 +171,9 @@ class AuthManager {
}
try {
const { CredentialSystemEncryptionMigration } =
await import("./credential-system-encryption-migration.js");
const { CredentialSystemEncryptionMigration } = await import(
"./credential-system-encryption-migration.js"
);
const credMigration = new CredentialSystemEncryptionMigration();
const credResult = await credMigration.migrateUserCredentials(userId);
@@ -199,6 +201,7 @@ class AuthManager {
options: {
expiresIn?: string;
pendingTOTP?: boolean;
rememberMe?: boolean;
deviceType?: DeviceType;
deviceInfo?: string;
} = {},
@@ -207,13 +210,13 @@ class AuthManager {
let expiresIn = options.expiresIn;
if (!expiresIn && !options.pendingTOTP) {
if (options.deviceType === "desktop" || options.deviceType === "mobile") {
if (options.rememberMe) {
expiresIn = "30d";
} else {
expiresIn = "7d";
expiresIn = "2h";
}
} else if (!expiresIn) {
expiresIn = "7d";
expiresIn = "2h";
}
const payload: JWTPayload = { userId };
@@ -247,8 +250,9 @@ class AuthManager {
});
try {
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
await saveMemoryDatabaseToFile();
} catch (saveError) {
databaseLogger.error(
@@ -276,7 +280,7 @@ class AuthManager {
private parseExpiresIn(expiresIn: string): number {
const match = expiresIn.match(/^(\d+)([smhd])$/);
if (!match) return 7 * 24 * 60 * 60 * 1000;
if (!match) return 2 * 60 * 60 * 1000;
const value = parseInt(match[1]);
const unit = match[2];
@@ -291,7 +295,7 @@ class AuthManager {
case "d":
return value * 24 * 60 * 60 * 1000;
default:
return 7 * 24 * 60 * 60 * 1000;
return 2 * 60 * 60 * 1000;
}
}
@@ -340,9 +344,15 @@ class AuthManager {
}
}
invalidateJWTToken(token: string): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
invalidateJWTToken(_token: string): void {
// expected - no-op, JWT tokens are stateless
}
invalidateUserTokens(userId: string): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
invalidateUserTokens(_userId: string): void {
// expected - no-op, handled by session management
}
async revokeSession(sessionId: string): Promise<boolean> {
try {
@@ -354,8 +364,9 @@ class AuthManager {
await db.delete(sessions).where(eq(sessions.id, sessionId));
try {
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
await saveMemoryDatabaseToFile();
} catch (saveError) {
databaseLogger.error(
@@ -412,8 +423,9 @@ class AuthManager {
}
try {
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
await saveMemoryDatabaseToFile();
} catch (saveError) {
databaseLogger.error(
@@ -454,8 +466,9 @@ class AuthManager {
.where(sql`${sessions.expiresAt} < datetime('now')`);
try {
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
await saveMemoryDatabaseToFile();
} catch (saveError) {
databaseLogger.error(
@@ -488,7 +501,7 @@ class AuthManager {
}
}
async getAllSessions(): Promise<any[]> {
async getAllSessions(): Promise<Record<string, unknown>[]> {
try {
const allSessions = await db.select().from(sessions);
return allSessions;
@@ -500,7 +513,7 @@ class AuthManager {
}
}
async getUserSessions(userId: string): Promise<any[]> {
async getUserSessions(userId: string): Promise<Record<string, unknown>[]> {
try {
const userSessions = await db
.select()
@@ -518,7 +531,7 @@ class AuthManager {
getSecureCookieOptions(
req: RequestWithHeaders,
maxAge: number = 7 * 24 * 60 * 60 * 1000,
maxAge: number = 2 * 60 * 60 * 1000,
) {
return {
httpOnly: false,
@@ -600,8 +613,9 @@ class AuthManager {
.where(eq(sessions.id, payload.sessionId))
.then(async () => {
try {
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
await saveMemoryDatabaseToFile();
const remainingSessions = await db
@@ -711,7 +725,7 @@ class AuthManager {
.from(users)
.where(eq(users.id, payload.userId));
if (!user || user.length === 0 || !user[0].is_admin) {
if (!user || user.length === 0 || !user[0].isAdmin) {
databaseLogger.warn(
"Non-admin user attempted to access admin endpoint",
{
@@ -745,8 +759,9 @@ class AuthManager {
await db.delete(sessions).where(eq(sessions.id, sessionId));
try {
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
await saveMemoryDatabaseToFile();
} catch (saveError) {
databaseLogger.error(
@@ -768,6 +783,7 @@ class AuthManager {
if (remainingSessions.length === 0) {
this.userCrypto.logoutUser(userId);
} else {
// expected - other sessions still active, keep user crypto state
}
} catch (error) {
databaseLogger.error("Failed to delete session on logout", error, {
@@ -810,6 +826,121 @@ class AuthManager {
newPassword,
);
}
/**
* Check if device is trusted for TOTP bypass
*/
async isTrustedDevice(
userId: string,
deviceFingerprint: string,
): Promise<boolean> {
try {
const device = await db
.select()
.from(trustedDevices)
.where(
and(
eq(trustedDevices.userId, userId),
eq(trustedDevices.deviceFingerprint, deviceFingerprint),
),
)
.limit(1);
if (!device || device.length === 0) {
return false;
}
const now = new Date();
const expiresAt = new Date(device[0].expiresAt);
if (now > expiresAt) {
await this.removeTrustedDevice(userId, deviceFingerprint);
return false;
}
await db
.update(trustedDevices)
.set({ lastUsedAt: now.toISOString() })
.where(
and(
eq(trustedDevices.userId, userId),
eq(trustedDevices.deviceFingerprint, deviceFingerprint),
),
);
return true;
} catch (error) {
authLogger.error("Failed to check trusted device", { userId, error });
return false;
}
}
/**
* Add device to trusted list for TOTP bypass
*/
async addTrustedDevice(
userId: string,
deviceFingerprint: string,
deviceType: string,
deviceInfo: string,
): Promise<void> {
const now = new Date();
const expiresAt = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
const existingDevice = await db
.select()
.from(trustedDevices)
.where(
and(
eq(trustedDevices.userId, userId),
eq(trustedDevices.deviceFingerprint, deviceFingerprint),
),
)
.limit(1);
if (existingDevice && existingDevice.length > 0) {
await db
.update(trustedDevices)
.set({
expiresAt: expiresAt.toISOString(),
lastUsedAt: now.toISOString(),
})
.where(
and(
eq(trustedDevices.userId, userId),
eq(trustedDevices.deviceFingerprint, deviceFingerprint),
),
);
} else {
await db.insert(trustedDevices).values({
id: nanoid(),
userId,
deviceFingerprint,
deviceType,
deviceInfo,
createdAt: now.toISOString(),
expiresAt: expiresAt.toISOString(),
lastUsedAt: now.toISOString(),
});
}
}
/**
* Remove trusted device
*/
async removeTrustedDevice(
userId: string,
deviceFingerprint: string,
): Promise<void> {
await db
.delete(trustedDevices)
.where(
and(
eq(trustedDevices.userId, userId),
eq(trustedDevices.deviceFingerprint, deviceFingerprint),
),
);
}
}
export { AuthManager, type AuthenticationResult, type JWTPayload };
+3 -1
View File
@@ -233,7 +233,9 @@ IP.3 = 0.0.0.0
let envContent = "";
try {
envContent = await fs.readFile(this.ENV_FILE, "utf8");
} catch (error) {}
} catch {
// expected - env file may not exist yet
}
let updatedContent = envContent;
let hasChanges = false;
@@ -59,9 +59,9 @@ export class CredentialSystemEncryptionMigration {
)
: null;
const plainKeyPassword = cred.key_password
const plainKeyPassword = cred.keyPassword
? FieldCrypto.decryptField(
cred.key_password,
cred.keyPassword,
userDEK,
cred.id.toString(),
"key_password",
+34 -37
View File
@@ -131,12 +131,12 @@ class DataCrypto {
db.prepare(updateQuery).run(
updatedRecord.password || null,
updatedRecord.key || null,
updatedRecord.key_password || updatedRecord.keyPassword || null,
updatedRecord.keyType || null,
updatedRecord.autostartPassword || null,
updatedRecord.autostartKey || null,
updatedRecord.autostartKeyPassword || null,
updatedRecord.sudoPassword || null,
updatedRecord.key_password || null,
updatedRecord.key_type || null,
updatedRecord.autostart_password || null,
updatedRecord.autostart_key || null,
updatedRecord.autostart_key_password || null,
updatedRecord.sudo_password || null,
record.id,
);
@@ -171,10 +171,10 @@ class DataCrypto {
db.prepare(updateQuery).run(
updatedRecord.password || null,
updatedRecord.key || null,
updatedRecord.key_password || updatedRecord.keyPassword || null,
updatedRecord.private_key || updatedRecord.privateKey || null,
updatedRecord.public_key || updatedRecord.publicKey || null,
updatedRecord.keyType || null,
updatedRecord.key_password || null,
updatedRecord.private_key || null,
updatedRecord.public_key || null,
updatedRecord.key_type || null,
record.id,
);
@@ -207,14 +207,10 @@ class DataCrypto {
WHERE id = ?
`;
db.prepare(updateQuery).run(
updatedRecord.totp_secret || updatedRecord.totpSecret || null,
updatedRecord.totp_backup_codes ||
updatedRecord.totpBackupCodes ||
null,
updatedRecord.client_secret || updatedRecord.clientSecret || null,
updatedRecord.oidc_identifier ||
updatedRecord.oidcIdentifier ||
null,
updatedRecord.totp_secret || null,
updatedRecord.totp_backup_codes || null,
updatedRecord.client_secret || null,
updatedRecord.oidc_identifier || null,
userId,
);
@@ -267,46 +263,41 @@ class DataCrypto {
"password",
"key",
"key_password",
"keyPassword",
"keyType",
"autostartPassword",
"autostartKey",
"autostartKeyPassword",
"sudo_password",
"autostart_password",
"autostart_key",
"autostart_key_password",
],
},
{
table: "ssh_credentials",
fields: [
"password",
"private_key",
"privateKey",
"key_password",
"keyPassword",
"key",
"private_key",
"public_key",
"publicKey",
"keyType",
"key_password",
],
},
{
table: "users",
fields: [
"client_secret",
"clientSecret",
"totp_secret",
"totpSecret",
"totp_backup_codes",
"totpBackupCodes",
"oidc_identifier",
"oidcIdentifier",
],
},
];
for (const { table, fields } of tablesToReencrypt) {
try {
const selectQuery =
table === "users"
? `SELECT * FROM ${table} WHERE id = ?`
: `SELECT * FROM ${table} WHERE user_id = ?`;
const records = db
.prepare(`SELECT * FROM ${table} WHERE user_id = ?`)
.prepare(selectQuery)
.all(userId) as DatabaseRecord[];
for (const record of records) {
@@ -359,7 +350,13 @@ class DataCrypto {
(field) => updatedRecord[field] !== record[field],
);
if (updateFields.length > 0) {
const updateQuery = `UPDATE ${table} SET ${updateFields.map((f) => `${f} = ?`).join(", ")}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
const setClause = updateFields
.map((f) => `${f} = ?`)
.join(", ");
const updateQuery =
table === "users"
? `UPDATE ${table} SET ${setClause} WHERE id = ?`
: `UPDATE ${table} SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
const updateValues = updateFields.map(
(field) => updatedRecord[field],
);
@@ -507,9 +504,9 @@ class DataCrypto {
);
}
if (record.key_password && typeof record.key_password === "string") {
if (record.keyPassword && typeof record.keyPassword === "string") {
systemEncrypted.systemKeyPassword = FieldCrypto.encryptField(
record.key_password as string,
record.keyPassword as string,
systemKey,
recordId as string,
"key_password",
@@ -327,7 +327,9 @@ class DatabaseFileEncryption {
fs.accessSync(envPath, fs.constants.R_OK);
envFileReadable = true;
}
} catch (error) {}
} catch {
// expected - env file access check may fail
}
databaseLogger.error(
"Database decryption authentication failed - possible causes: wrong DATABASE_KEY, corrupted files, or interrupted write",
@@ -581,7 +583,9 @@ class DatabaseFileEncryption {
try {
fs.accessSync(envPath, fs.constants.R_OK);
result.environment.envFileReadable = true;
} catch (error) {}
} catch {
// expected - env file access check may fail
}
}
if (
@@ -5,12 +5,21 @@ export class DatabaseSaveTrigger {
private static isInitialized = false;
private static pendingSave = false;
private static saveTimeout: NodeJS.Timeout | null = null;
private static _dirty = false;
static initialize(saveFunction: () => Promise<void>): void {
this.saveFunction = saveFunction;
this.isInitialized = true;
}
static get isDirty(): boolean {
return this._dirty;
}
static markClean(): void {
this._dirty = false;
}
static async triggerSave(
reason: string = "data_modification",
): Promise<void> {
@@ -22,6 +31,8 @@ export class DatabaseSaveTrigger {
return;
}
this._dirty = true;
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
}
@@ -35,6 +46,7 @@ export class DatabaseSaveTrigger {
try {
await this.saveFunction!();
this._dirty = false;
} catch (error) {
databaseLogger.error("Database save failed", error, {
operation: "db_save_trigger_failed",
+2 -16
View File
@@ -16,43 +16,29 @@ class FieldCrypto {
private static readonly ENCRYPTED_FIELDS = {
users: new Set([
"password_hash",
"passwordHash",
"client_secret",
"clientSecret",
"totp_secret",
"totpSecret",
"totp_backup_codes",
"totpBackupCodes",
"oidc_identifier",
"oidcIdentifier",
]),
ssh_data: new Set([
"password",
"key",
"key_password",
"keyPassword",
"sudoPassword",
"autostartPassword",
"autostartKey",
"autostartKeyPassword",
"sudoPassword",
]),
ssh_credentials: new Set([
"password",
"private_key",
"privateKey",
"key_password",
"keyPassword",
"key",
"public_key",
"publicKey",
]),
opkssh_tokens: new Set([
"ssh_cert",
"sshCert",
"private_key",
"privateKey",
]),
opkssh_tokens: new Set(["sshCert", "privateKey"]),
};
static encryptField(
+56 -22
View File
@@ -82,19 +82,21 @@ export class LazyFieldEncryption {
legacyFieldName,
);
return decrypted;
} catch (error) {}
} catch {
// expected - legacy decryption may fail, try other methods
}
}
const sensitiveFields = [
"totp_secret",
"totp_backup_codes",
"totpSecret",
"totpBackupCodes",
"password",
"key",
"key_password",
"private_key",
"public_key",
"client_secret",
"oidc_identifier",
"keyPassword",
"privateKey",
"publicKey",
"clientSecret",
"oidcIdentifier",
];
if (sensitiveFields.includes(fieldName)) {
@@ -174,7 +176,9 @@ export class LazyFieldEncryption {
wasPlaintext: false,
wasLegacyEncryption: true,
};
} catch (error) {}
} catch {
// expected - re-encryption may fail, return original
}
}
return {
encrypted: fieldValue,
@@ -200,7 +204,8 @@ export class LazyFieldEncryption {
let needsUpdate = false;
for (const fieldName of sensitiveFields) {
const fieldValue = record[fieldName];
const column = this.propertyToColumn(fieldName);
const fieldValue = record[column] ?? record[fieldName];
if (fieldValue) {
try {
@@ -213,7 +218,7 @@ export class LazyFieldEncryption {
);
if (wasPlaintext || wasLegacyEncryption) {
updatedRecord[fieldName] = encrypted;
updatedRecord[column] = encrypted;
migratedFields.push(fieldName);
needsUpdate = true;
}
@@ -230,22 +235,48 @@ export class LazyFieldEncryption {
return { updatedRecord, migratedFields, needsUpdate };
}
private static readonly PROPERTY_TO_COLUMN: Record<string, string> = {
keyPassword: "key_password",
privateKey: "private_key",
publicKey: "public_key",
sudoPassword: "sudo_password",
autostartPassword: "autostart_password",
autostartKey: "autostart_key",
autostartKeyPassword: "autostart_key_password",
totpSecret: "totp_secret",
totpBackupCodes: "totp_backup_codes",
clientSecret: "client_secret",
oidcIdentifier: "oidc_identifier",
};
static getSensitiveFieldsForTable(tableName: string): string[] {
const sensitiveFieldsMap: Record<string, string[]> = {
ssh_data: ["password", "key", "key_password"],
ssh_data: [
"password",
"key",
"keyPassword",
"sudoPassword",
"autostartPassword",
"autostartKey",
"autostartKeyPassword",
],
ssh_credentials: [
"password",
"key",
"key_password",
"private_key",
"public_key",
"keyPassword",
"privateKey",
"publicKey",
],
users: ["totp_secret", "totp_backup_codes"],
users: ["totpSecret", "totpBackupCodes"],
};
return sensitiveFieldsMap[tableName] || [];
}
static propertyToColumn(propertyName: string): string {
return this.PROPERTY_TO_COLUMN[propertyName] || propertyName;
}
static fieldNeedsMigration(
fieldValue: string,
userKEK: Buffer,
@@ -310,10 +341,11 @@ export class LazyFieldEncryption {
const hostPlaintextFields: string[] = [];
for (const field of sensitiveFields) {
const column = this.propertyToColumn(field);
if (
host[field] &&
host[column] &&
this.fieldNeedsMigration(
host[field] as string,
host[column] as string,
userKEK,
host.id.toString(),
field,
@@ -344,10 +376,11 @@ export class LazyFieldEncryption {
const credentialPlaintextFields: string[] = [];
for (const field of sensitiveFields) {
const column = this.propertyToColumn(field);
if (
credential[field] &&
credential[column] &&
this.fieldNeedsMigration(
credential[field] as string,
credential[column] as string,
userKEK,
credential.id.toString(),
field,
@@ -373,9 +406,10 @@ export class LazyFieldEncryption {
const userPlaintextFields: string[] = [];
for (const field of sensitiveFields) {
const column = this.propertyToColumn(field);
if (
user[field] &&
this.fieldNeedsMigration(user[field], userKEK, userId, field)
user[column] &&
this.fieldNeedsMigration(user[column], userKEK, userId, field)
) {
userPlaintextFields.push(field);
needsMigration = true;
+5 -5
View File
@@ -66,15 +66,14 @@ class PermissionManager {
private async cleanupExpiredAccess(): Promise<void> {
try {
const now = new Date().toISOString();
const result = await db
await db
.delete(hostAccess)
.where(
and(
sql`${hostAccess.expiresAt} IS NOT NULL`,
sql`${hostAccess.expiresAt} <= ${now}`,
),
)
.returning({ id: hostAccess.id });
);
} catch (error) {
databaseLogger.error("Failed to cleanup expired host access", error, {
operation: "host_access_cleanup_failed",
@@ -280,7 +279,7 @@ class PermissionManager {
async isAdmin(userId: string): Promise<boolean> {
try {
const user = await db
.select({ isAdmin: users.is_admin })
.select({ isAdmin: users.isAdmin })
.from(users)
.where(eq(users.id, userId))
.limit(1);
@@ -383,7 +382,8 @@ class PermissionManager {
});
}
(req as any).hostAccessInfo = accessInfo;
(req as unknown as { hostAccessInfo: HostAccessInfo }).hostAccessInfo =
accessInfo;
next();
};
+1 -3
View File
@@ -18,9 +18,7 @@ export function getProxyAgent(targetUrl?: string): Agent | undefined {
const trimmed = entry.trim().toLowerCase();
if (!trimmed) continue;
const normalized = trimmed
.replace(/^\*\./, "")
.replace(/^\./, "");
const normalized = trimmed.replace(/^\*\./, "").replace(/^\./, "");
if (hostname === normalized || hostname.endsWith(`.${normalized}`)) {
return undefined;
+340
View File
@@ -0,0 +1,340 @@
import { SocksClient } from "socks";
import type { SocksClientOptions } from "socks";
import net from "net";
import { sshLogger } from "./logger.js";
import type { ProxyNode } from "../../types/index.js";
export interface SOCKS5Config {
useSocks5?: boolean;
socks5Host?: string;
socks5Port?: number;
socks5Username?: string;
socks5Password?: string;
socks5ProxyChain?: ProxyNode[];
}
export async function createProxyConnection(
targetHost: string,
targetPort: number,
socks5Config: SOCKS5Config,
): Promise<net.Socket | null> {
if (!socks5Config.useSocks5) {
return null;
}
if (
socks5Config.socks5ProxyChain &&
socks5Config.socks5ProxyChain.length > 0
) {
return createMixedProxyChainConnection(
targetHost,
targetPort,
socks5Config.socks5ProxyChain,
);
}
if (socks5Config.socks5Host) {
return createSingleProxyConnection(targetHost, targetPort, socks5Config);
}
return null;
}
export const createSocks5Connection = createProxyConnection;
async function createSingleProxyConnection(
targetHost: string,
targetPort: number,
socks5Config: SOCKS5Config,
): Promise<net.Socket> {
const socksOptions: SocksClientOptions = {
proxy: {
host: socks5Config.socks5Host!,
port: socks5Config.socks5Port || 1080,
type: 5,
userId: socks5Config.socks5Username,
password: socks5Config.socks5Password,
},
command: "connect",
destination: {
host: targetHost,
port: targetPort,
},
};
try {
const info = await SocksClient.createConnection(socksOptions);
return info.socket;
} catch (error) {
sshLogger.error("SOCKS5 connection failed", error, {
operation: "socks5_connect_failed",
proxyHost: socks5Config.socks5Host,
proxyPort: socks5Config.socks5Port || 1080,
targetHost,
targetPort,
errorMessage: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
}
export async function createHttpConnectConnection(
targetHost: string,
targetPort: number,
proxyHost: string,
proxyPort: number,
username?: string,
password?: string,
existingSocket?: net.Socket,
): Promise<net.Socket> {
return new Promise<net.Socket>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(
new Error(
`HTTP CONNECT proxy timeout connecting to ${proxyHost}:${proxyPort}`,
),
);
}, 15000);
function sendConnect(socket: net.Socket) {
let connectReq = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n`;
if (username && password) {
const credentials = Buffer.from(`${username}:${password}`).toString(
"base64",
);
connectReq += `Proxy-Authorization: Basic ${credentials}\r\n`;
}
connectReq += "\r\n";
let responseBuffer = "";
function onData(chunk: Buffer) {
responseBuffer += chunk.toString("utf8");
const headerEnd = responseBuffer.indexOf("\r\n\r\n");
if (headerEnd === -1) return;
clearTimeout(timeout);
socket.removeListener("data", onData);
socket.removeListener("error", onError);
const statusLine = responseBuffer.slice(
0,
responseBuffer.indexOf("\r\n"),
);
const statusCode = parseInt(statusLine.split(" ")[1], 10);
if (statusCode === 200) {
resolve(socket);
} else {
socket.destroy();
reject(
new Error(
`HTTP CONNECT proxy returned ${statusCode}: ${statusLine}`,
),
);
}
}
function onError(err: Error) {
clearTimeout(timeout);
reject(new Error(`HTTP CONNECT proxy error: ${err.message}`));
}
socket.on("data", onData);
socket.on("error", onError);
socket.write(connectReq);
}
if (existingSocket) {
sendConnect(existingSocket);
} else {
const socket = net.connect(proxyPort, proxyHost, () => {
sendConnect(socket);
});
socket.on("error", (err) => {
clearTimeout(timeout);
reject(new Error(`HTTP CONNECT proxy TCP error: ${err.message}`));
});
}
});
}
export async function createMixedProxyChainConnection(
targetHost: string,
targetPort: number,
proxyChain: ProxyNode[],
): Promise<net.Socket> {
if (proxyChain.length === 0) {
throw new Error("Proxy chain is empty");
}
const hasMixedTypes = proxyChain.some((p) => p.type === "http");
if (!hasMixedTypes) {
return createPureSocksChainConnection(targetHost, targetPort, proxyChain);
}
return createHopByHopConnection(targetHost, targetPort, proxyChain);
}
async function createPureSocksChainConnection(
targetHost: string,
targetPort: number,
proxyChain: ProxyNode[],
): Promise<net.Socket> {
try {
const info = await SocksClient.createConnectionChain({
proxies: proxyChain.map((p) => ({
host: p.host,
port: p.port,
type: p.type as 4 | 5,
userId: p.username,
password: p.password,
timeout: 10000,
})),
command: "connect",
destination: {
host: targetHost,
port: targetPort,
},
});
return info.socket;
} catch (error) {
sshLogger.error("SOCKS proxy chain connection failed", error, {
operation: "socks5_chain_connect_failed",
chainLength: proxyChain.length,
targetHost,
targetPort,
errorMessage: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
}
async function createHopByHopConnection(
targetHost: string,
targetPort: number,
proxyChain: ProxyNode[],
): Promise<net.Socket> {
let currentSocket: net.Socket | null = null;
try {
for (let i = 0; i < proxyChain.length; i++) {
const node = proxyChain[i];
const isLast = i === proxyChain.length - 1;
const nextTarget = isLast
? { host: targetHost, port: targetPort }
: { host: proxyChain[i + 1].host, port: proxyChain[i + 1].port };
if (node.type === "http") {
currentSocket = await createHttpConnectConnection(
nextTarget.host,
nextTarget.port,
node.host,
node.port,
node.username,
node.password,
currentSocket ?? undefined,
);
} else {
const socksOptions: SocksClientOptions = {
proxy: {
host: node.host,
port: node.port,
type: node.type as 4 | 5,
userId: node.username,
password: node.password,
},
command: "connect",
destination: nextTarget,
};
if (currentSocket) {
socksOptions.existing_socket = currentSocket;
}
const info = await SocksClient.createConnection(socksOptions);
currentSocket = info.socket;
}
}
if (!currentSocket) {
throw new Error("Proxy chain produced no socket");
}
return currentSocket;
} catch (error) {
if (currentSocket) {
currentSocket.destroy();
}
sshLogger.error("Mixed proxy chain connection failed", error, {
operation: "mixed_chain_connect_failed",
chainLength: proxyChain.length,
targetHost,
targetPort,
errorMessage: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
}
export async function testProxyConnectivity(options: {
singleProxy?: {
host: string;
port: number;
type?: 4 | 5 | "http";
username?: string;
password?: string;
};
proxyChain?: ProxyNode[];
testTarget?: { host: string; port: number };
}): Promise<{ success: boolean; latencyMs: number }> {
const target = options.testTarget ?? { host: "google.com", port: 443 };
const start = Date.now();
let socket: net.Socket | null = null;
try {
if (options.proxyChain && options.proxyChain.length > 0) {
socket = await createMixedProxyChainConnection(
target.host,
target.port,
options.proxyChain,
);
} else if (options.singleProxy) {
const proxy = options.singleProxy;
if (proxy.type === "http") {
socket = await createHttpConnectConnection(
target.host,
target.port,
proxy.host,
proxy.port,
proxy.username,
proxy.password,
);
} else {
const socksOptions: SocksClientOptions = {
proxy: {
host: proxy.host,
port: proxy.port,
type: (proxy.type as 4 | 5) || 5,
userId: proxy.username,
password: proxy.password,
},
command: "connect",
destination: target,
timeout: 10000,
};
const info = await SocksClient.createConnection(socksOptions);
socket = info.socket;
}
} else {
throw new Error("No proxy configuration provided");
}
const latencyMs = Date.now() - start;
socket.destroy();
return { success: true, latencyMs };
} catch (error) {
if (socket) socket.destroy();
throw error;
}
}
@@ -3,7 +3,6 @@ import {
sharedCredentials,
sshCredentials,
hostAccess,
users,
userRoles,
sshData,
} from "../database/db/schema.js";
@@ -293,10 +292,9 @@ class SharedCredentialManager {
credentialId: number,
): Promise<void> {
try {
const result = await db
await db
.delete(sharedCredentials)
.where(eq(sharedCredentials.originalCredentialId, credentialId))
.returning({ id: sharedCredentials.id });
.where(eq(sharedCredentials.originalCredentialId, credentialId));
} catch (error) {
databaseLogger.error("Failed to delete shared credentials", error, {
operation: "delete_shared_credentials",
@@ -364,9 +362,9 @@ class SharedCredentialManager {
key: cred.key
? this.decryptField(cred.key, ownerDEK, credentialId, "key")
: undefined,
keyPassword: cred.key_password
keyPassword: cred.keyPassword
? this.decryptField(
cred.key_password,
cred.keyPassword,
ownerDEK,
credentialId,
"key_password",
@@ -534,7 +532,7 @@ class SharedCredentialManager {
recordId.toString(),
fieldName,
);
} catch (error) {
} catch {
databaseLogger.warn("Field decryption failed, returning as-is", {
operation: "decrypt_field",
fieldName,
+6 -5
View File
@@ -1,6 +1,7 @@
import { getDb, DatabaseSaveTrigger } from "../database/db/index.js";
import { DataCrypto } from "./data-crypto.js";
import type { SQLiteTable } from "drizzle-orm/sqlite-core";
import type { SQL } from "drizzle-orm";
type TableName =
| "users"
@@ -11,7 +12,7 @@ type TableName =
class SimpleDBOps {
static async insert<T extends Record<string, unknown>>(
table: SQLiteTable<any>,
table: SQLiteTable,
tableName: TableName,
data: T,
userId: string,
@@ -109,7 +110,7 @@ class SimpleDBOps {
}
static async update<T extends Record<string, unknown>>(
table: SQLiteTable<any>,
table: SQLiteTable,
tableName: TableName,
where: unknown,
data: Partial<T>,
@@ -141,7 +142,7 @@ class SimpleDBOps {
const result = await getDb()
.update(table)
.set(encryptedData)
.where(where as any)
.where(where as SQL | undefined)
.returning();
DatabaseSaveTrigger.triggerSave(`update_${tableName}`);
@@ -157,13 +158,13 @@ class SimpleDBOps {
}
static async delete(
table: SQLiteTable<any>,
table: SQLiteTable,
tableName: TableName,
where: unknown,
): Promise<unknown[]> {
const result = await getDb()
.delete(table)
.where(where as any)
.where(where as SQL | undefined)
.returning();
DatabaseSaveTrigger.triggerSave(`delete_${tableName}`);
+8 -117
View File
@@ -1,117 +1,8 @@
import { SocksClient } from "socks";
import type { SocksClientOptions } from "socks";
import net from "net";
import { sshLogger } from "./logger.js";
import type { ProxyNode } from "../../types/index.js";
export interface SOCKS5Config {
useSocks5?: boolean;
socks5Host?: string;
socks5Port?: number;
socks5Username?: string;
socks5Password?: string;
socks5ProxyChain?: ProxyNode[];
}
export async function createSocks5Connection(
targetHost: string,
targetPort: number,
socks5Config: SOCKS5Config,
): Promise<net.Socket | null> {
if (!socks5Config.useSocks5) {
return null;
}
if (
socks5Config.socks5ProxyChain &&
socks5Config.socks5ProxyChain.length > 0
) {
return createProxyChainConnection(
targetHost,
targetPort,
socks5Config.socks5ProxyChain,
);
}
if (socks5Config.socks5Host) {
return createSingleProxyConnection(targetHost, targetPort, socks5Config);
}
return null;
}
async function createSingleProxyConnection(
targetHost: string,
targetPort: number,
socks5Config: SOCKS5Config,
): Promise<net.Socket> {
const socksOptions: SocksClientOptions = {
proxy: {
host: socks5Config.socks5Host!,
port: socks5Config.socks5Port || 1080,
type: 5,
userId: socks5Config.socks5Username,
password: socks5Config.socks5Password,
},
command: "connect",
destination: {
host: targetHost,
port: targetPort,
},
};
try {
const info = await SocksClient.createConnection(socksOptions);
return info.socket;
} catch (error) {
sshLogger.error("SOCKS5 connection failed", error, {
operation: "socks5_connect_failed",
proxyHost: socks5Config.socks5Host,
proxyPort: socks5Config.socks5Port || 1080,
targetHost,
targetPort,
errorMessage: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
}
async function createProxyChainConnection(
targetHost: string,
targetPort: number,
proxyChain: ProxyNode[],
): Promise<net.Socket> {
if (proxyChain.length === 0) {
throw new Error("Proxy chain is empty");
}
const chainPath = proxyChain.map((p) => `${p.host}:${p.port}`).join(" → ");
try {
const info = await SocksClient.createConnectionChain({
proxies: proxyChain.map((p) => ({
host: p.host,
port: p.port,
type: p.type,
userId: p.username,
password: p.password,
timeout: 10000,
})),
command: "connect",
destination: {
host: targetHost,
port: targetPort,
},
});
return info.socket;
} catch (error) {
sshLogger.error("SOCKS proxy chain connection failed", error, {
operation: "socks5_chain_connect_failed",
chainLength: proxyChain.length,
targetHost,
targetPort,
errorMessage: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
}
export {
createSocks5Connection,
createProxyConnection,
createHttpConnectConnection,
createMixedProxyChainConnection,
testProxyConnectivity,
} from "./proxy-helper.js";
export type { SOCKS5Config } from "./proxy-helper.js";
+12 -5
View File
@@ -1,5 +1,4 @@
import ssh2Pkg from "ssh2";
import { sshLogger } from "./logger.js";
const ssh2Utils = ssh2Pkg.utils;
function detectKeyTypeFromContent(keyContent: string): string {
@@ -85,7 +84,9 @@ function detectKeyTypeFromContent(keyContent: string): string {
} else if (decodedString.includes("1.3.101.112")) {
return "ssh-ed25519";
}
} catch (error) {}
} catch {
// expected - base64 decode may fail for some key formats
}
if (content.length < 800) {
return "ssh-ed25519";
@@ -141,7 +142,9 @@ function detectPublicKeyTypeFromContent(publicKeyContent: string): string {
} else if (decodedString.includes("1.3.101.112")) {
return "ssh-ed25519";
}
} catch (error) {}
} catch {
// expected - base64 decode may fail for some key formats
}
if (content.length < 400) {
return "ssh-ed25519";
@@ -243,7 +246,9 @@ export function parseSSHKey(
useSSH2 = true;
}
} catch (error) {}
} catch {
// expected - ssh2 key parsing may fail
}
}
if (!useSSH2) {
@@ -269,7 +274,9 @@ export function parseSSHKey(
success: true,
};
}
} catch (error) {}
} catch {
// expected - fallback key type detection may fail
}
return {
privateKey: privateKeyData,
+13 -17
View File
@@ -52,7 +52,9 @@ class SystemCrypto {
},
);
}
} catch (fileError) {}
} catch {
// expected - env file may not exist
}
await this.generateAndGuideUser();
} catch (error) {
@@ -78,12 +80,6 @@ class SystemCrypto {
const envKey = process.env.DATABASE_KEY;
if (envKey && envKey.length >= 64) {
this.databaseKey = Buffer.from(envKey, "hex");
const keyFingerprint = crypto
.createHash("sha256")
.update(this.databaseKey)
.digest("hex")
.substring(0, 16);
return;
}
@@ -93,17 +89,13 @@ class SystemCrypto {
if (dbKeyMatch && dbKeyMatch[1] && dbKeyMatch[1].length >= 64) {
this.databaseKey = Buffer.from(dbKeyMatch[1], "hex");
process.env.DATABASE_KEY = dbKeyMatch[1];
const keyFingerprint = crypto
.createHash("sha256")
.update(this.databaseKey)
.digest("hex")
.substring(0, 16);
return;
} else {
// expected - key not found or invalid length in env file
}
} catch (fileError) {}
} catch {
// expected - env file may not exist
}
await this.generateAndGuideDatabaseKey();
} catch (error) {
@@ -141,7 +133,9 @@ class SystemCrypto {
process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1];
return;
}
} catch (error) {}
} catch {
// expected - env file may not exist
}
await this.generateAndGuideInternalAuthToken();
} catch (error) {
@@ -178,7 +172,9 @@ class SystemCrypto {
process.env.CREDENTIAL_SHARING_KEY = csKeyMatch[1];
return;
}
} catch (fileError) {}
} catch {
// expected - env file may not exist
}
await this.generateAndGuideCredentialSharingKey();
} catch (error) {
+20
View File
@@ -1,4 +1,5 @@
import type { Request } from "express";
import crypto from "crypto";
export type DeviceType = "web" | "desktop" | "mobile";
@@ -237,3 +238,22 @@ function parseMacVersion(userAgent: string): string {
}
return "macOS";
}
/**
* Generate a stable device fingerprint based on device type, browser, and OS.
* Ignores minor version numbers to handle browser auto-updates.
*/
export function generateDeviceFingerprint(deviceInfo: DeviceInfo): string {
let fingerprintString = "";
if (deviceInfo.type === "desktop") {
fingerprintString = `${deviceInfo.type}|${deviceInfo.browser}|${deviceInfo.os}`;
} else if (deviceInfo.type === "mobile") {
fingerprintString = `${deviceInfo.type}|${deviceInfo.browser}|${deviceInfo.os}`;
} else {
const browserMajor = deviceInfo.version.split(".")[0];
fingerprintString = `${deviceInfo.type}|${deviceInfo.browser} ${browserMajor}|${deviceInfo.os}`;
}
return crypto.createHash("sha256").update(fingerprintString).digest("hex");
}
+9 -6
View File
@@ -303,8 +303,9 @@ 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);
@@ -340,8 +341,9 @@ 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);
@@ -416,8 +418,9 @@ 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, {
+5 -4
View File
@@ -194,7 +194,7 @@ class UserDataImport {
continue;
}
const newHostData: any = {
const newHostData: Record<string, unknown> = {
...host,
userId: targetUserId,
updatedAt: new Date().toISOString(),
@@ -204,7 +204,7 @@ class UserDataImport {
newHostData.createdAt = new Date().toISOString();
}
let processedHostData: any = newHostData;
let processedHostData: Record<string, unknown> = newHostData;
if (options.userDataKey) {
processedHostData = DataCrypto.encryptRecord(
"ssh_data",
@@ -275,7 +275,7 @@ class UserDataImport {
continue;
}
const newCredentialData: any = {
const newCredentialData: Record<string, unknown> = {
...credential,
userId: targetUserId,
updatedAt: new Date().toISOString(),
@@ -287,7 +287,8 @@ class UserDataImport {
newCredentialData.createdAt = new Date().toISOString();
}
let processedCredentialData: any = newCredentialData;
let processedCredentialData: Record<string, unknown> =
newCredentialData;
if (options.userDataKey) {
processedCredentialData = DataCrypto.encryptRecord(
"ssh_credentials",
+2
View File
@@ -762,6 +762,8 @@ export const DEFAULT_TERMINAL_CONFIG = {
autoMosh: false,
moshCommand: "mosh-server new -s -l LANG=en_US.UTF-8",
sudoPasswordAutoFill: false,
keepaliveInterval: undefined as number | undefined,
keepaliveCountMax: undefined as number | undefined,
};
export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG;
+47
View File
@@ -0,0 +1,47 @@
import type {
IClipboardProvider,
ClipboardSelectionType,
} from "@xterm/addon-clipboard";
export class RobustClipboardProvider implements IClipboardProvider {
private pendingWrite: string | null = null;
private readonly focusHandler: () => void;
constructor() {
this.focusHandler = () => {
if (this.pendingWrite !== null) {
const text = this.pendingWrite;
this.pendingWrite = null;
navigator.clipboard.writeText(text).catch(() => {
this.pendingWrite = text;
});
}
};
window.addEventListener("focus", this.focusHandler);
}
dispose(): void {
window.removeEventListener("focus", this.focusHandler);
this.pendingWrite = null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
readText(selection: ClipboardSelectionType): string {
return "";
}
async writeText(
selection: ClipboardSelectionType,
text: string,
): Promise<void> {
try {
if (window.electronClipboard) {
window.electronClipboard.writeText(text);
return;
}
await navigator.clipboard.writeText(text);
} catch {
this.pendingWrite = text;
}
}
}
+3 -3
View File
@@ -113,14 +113,14 @@ const PATTERNS: HighlightPattern[] = [
function hasExistingAnsiCodes(text: string): boolean {
const ansiCount = (
text.match(
/\x1b[\[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nq-uy=><~]/g,
/\x1b[[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nq-uy=><~]/g,
) || []
).length;
return ansiCount > MAX_ANSI_CODES;
}
function hasIncompleteAnsiSequence(text: string): boolean {
return /\x1b(?:\[(?:[0-9;]*)?)?$/.test(text);
return /\x1b(?:\[(?:[0-9;?>=!]*)?)?$/.test(text);
}
interface TextSegment {
@@ -130,7 +130,7 @@ interface TextSegment {
function parseAnsiSegments(text: string): TextSegment[] {
const segments: TextSegment[] = [];
const ansiRegex = /\x1b(?:[@-Z\\-_]|\[[0-9;]*[@-~])/g;
const ansiRegex = /\x1b(?:[@-Z\\-_]|\[[0-9;?>=!]*[@-~])/g;
let lastIndex = 0;
let match;
+86 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "2-Way",
"threeSplit": "3-Way",
"fourSplit": "4-Way",
"fiveSplit": "5-Way",
"sixSplit": "6-Way",
"availableTabs": "Available Tabs",
"dragTabsHint": "Drag tabs to the layout cells below to assign them",
"layout": "Split Screen Layout",
@@ -372,7 +374,11 @@
"warning": "Warning",
"notValidatedWarning": "URL not validated - ensure it's correct",
"changeServer": "Change Server",
"mustIncludeProtocol": "Server URL must start with http:// or https://"
"mustIncludeProtocol": "Server URL must start with http:// or https://",
"useEmbedded": "Use Local Server",
"embeddedDesc": "Run Termix with the built-in local server (no remote server needed)",
"embeddedConnecting": "Connecting to local server...",
"embeddedNotReady": "Local server is not ready yet. Please wait a moment and try again."
},
"versionCheck": {
"error": "Version Check Error",
@@ -471,6 +477,7 @@
"confirm": "Confirm",
"yes": "Yes",
"no": "No",
"or": "OR",
"ok": "OK",
"enabled": "Enabled",
"disabled": "Disabled",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "User {{username}} deleted successfully",
"failedToDeleteUser": "Failed to delete user",
"overrideUserInfoUrl": "Override User Info URL (not required)",
"allowedUsers": "Allowed Users",
"allowedUsersDescription": "Comma-separated list of allowed email patterns. Use exact emails (user@example.com), domain suffixes (@example.com), or leave empty to allow all users.",
"failedToFetchSessions": "Failed to fetch sessions",
"sessionRevokedSuccessfully": "Session revoked successfully",
"failedToRevokeSession": "Failed to revoke session",
@@ -794,6 +803,13 @@
"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",
"monitoringDefaults": "Monitoring Defaults",
"monitoringDefaultsDesc": "Global default intervals for status checks and metrics collection. Per-host settings override these values.",
"globalStatusCheckInterval": "Default Status Check Interval",
"globalMetricsInterval": "Default Metrics Collection Interval",
"globalSettingsSaved": "Global monitoring settings saved",
"failedToSaveGlobalSettings": "Failed to save global monitoring settings",
"failedToLoadGlobalSettings": "Failed to load global monitoring settings",
"sessionManagement": "Session Management",
"loadingSessions": "Loading sessions...",
"noActiveSessions": "No active sessions found.",
@@ -849,6 +865,11 @@
"noHostsInJson": "No hosts found in JSON file",
"maxHostsAllowed": "Maximum 100 hosts allowed per import",
"importCompleted": "Import completed: {{success}} successful, {{failed}} failed",
"importCreated": "created",
"importUpdated": "updated",
"importFailedCount": "failed",
"importSkipExisting": "Import (skip existing)",
"importOverwriteExisting": "Import (overwrite existing)",
"importFailed": "Import failed",
"importError": "Import error",
"failedToImportJson": "Failed to import JSON file",
@@ -1027,6 +1048,9 @@
"statusCheckEnabledDesc": "Check if the server is online or offline",
"statusCheckInterval": "Status Check Interval",
"statusCheckIntervalDesc": "How often to check if host is online (5s - 1h)",
"useGlobalStatusInterval": "Use global default",
"useGlobalMetricsInterval": "Use global default",
"usingGlobalDefault": "Using global default ({{value}}s)",
"metricsEnabled": "Enable Metrics Monitoring",
"metricsEnabledDesc": "Collect CPU, RAM, disk, and other system statistics",
"metricsInterval": "Metrics Collection Interval",
@@ -1184,8 +1208,20 @@
"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)",
"keepaliveSettings": "SSH Keepalive",
"keepaliveSettingsDesc": "Configure SSH-level keepalive behavior. Set interval to 0 to disable SSH keepalives (useful for MikroTik and other devices that ignore keepalive requests).",
"keepaliveInterval": "Keepalive Interval (ms)",
"keepaliveIntervalDesc": "How often to send SSH keepalive pings. Default: 30000 (30s). Set to 0 to disable.",
"keepaliveCountMax": "Keepalive Max Failures",
"keepaliveCountMaxDesc": "Disconnect after this many unanswered keepalives. Default: 3.",
"socks4": "SOCKS4",
"socks5": "SOCKS5",
"httpConnect": "HTTP CONNECT",
"testProxy": "Test Connection",
"testingProxy": "Testing...",
"proxyTestSuccess": "Proxy connection successful ({{latency}}ms)",
"proxyTestFailed": "Proxy test failed: {{error}}",
"connectionPath": "Connection Path",
"executeSnippetOnConnect": "Execute a snippet when the terminal connects",
"autoMosh": "Auto-MOSH",
"autoMoshDesc": "Automatically run MOSH command on connect",
@@ -1296,7 +1332,35 @@
"showTimestamps": "Show Timestamps",
"autoRefresh": "Auto Refresh",
"filterLogsPlaceholder": "Filter logs...",
"noLogsAvailable": "No logs available"
"noLogsAvailable": "No logs available",
"selectMode": "Select",
"exitSelectMode": "Cancel",
"selectedCount": "{{count}} selected",
"bulkMonitoring": "Monitoring",
"bulkFeatures": "Features",
"bulkMoveFolder": "Move to Folder",
"bulkPin": "Pin",
"bulkUnpin": "Unpin",
"enableAllFeatures": "Enable All Features",
"disableAllFeatures": "Disable All Features",
"enableStatusCheck": "Enable Status Check",
"disableStatusCheck": "Disable Status Check",
"enableMetrics": "Enable Metrics",
"disableMetrics": "Disable Metrics",
"bulkEnableTerminal": "Enable Terminal",
"bulkDisableTerminal": "Disable Terminal",
"bulkEnableTunnel": "Enable Tunnel",
"bulkDisableTunnel": "Disable Tunnel",
"bulkEnableFileManager": "Enable File Manager",
"bulkDisableFileManager": "Disable File Manager",
"bulkEnableDocker": "Enable Docker",
"bulkDisableDocker": "Disable Docker",
"bulkUpdateSuccess": "Updated {{count}} host(s) successfully",
"bulkUpdateFailed": "Bulk update failed",
"selectAll": "Select all",
"deselectAll": "Deselect all",
"useGlobalStatusDefault": "Use Global Default (Status)",
"useGlobalMetricsDefault": "Use Global Default (Metrics)"
},
"terminal": {
"title": "Split Screen",
@@ -1304,6 +1368,8 @@
"twoSplit": "2-Split",
"threeSplit": "3-Split",
"fourSplit": "4-Split",
"fiveSplit": "5-Split",
"sixSplit": "6-Split",
"availableTabs": "Available Tabs",
"dragTabsHint": "Drag tabs into the grid below to position them",
"layout": "Layout",
@@ -1339,6 +1405,8 @@
"connectionClosed": "Connection closed",
"connectionError": "Connection error: {{message}}",
"connected": "Connected",
"clipboardWriteFailed": "Failed to copy to clipboard. Make sure the page is served over HTTPS or localhost.",
"clipboardReadFailed": "Failed to read from clipboard. Make sure clipboard permissions are granted.",
"sshConnected": "SSH connection established",
"authError": "Authentication failed: {{message}}",
"unknownError": "Unknown error occurred",
@@ -1393,7 +1461,9 @@
"totpTimeout": "TOTP verification timeout. Please reconnect.",
"passwordTimeout": "Password verification timeout. Please reconnect.",
"connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.",
"hostKeyRejected": "SSH host key verification rejected. Connection cancelled."
"hostKeyRejected": "SSH host key verification rejected. Connection cancelled.",
"sessionTakenOver": "Session was opened in another tab. Reconnecting...",
"sessionAttachTimeout": "Session attachment timed out. Creating new connection..."
},
"fileManager": {
"title": "File Manager",
@@ -1718,7 +1788,13 @@
"write": "Write",
"execute": "Execute",
"permissionsChangedSuccessfully": "Permissions changed successfully",
"failedToChangePermissions": "Failed to change permissions"
"failedToChangePermissions": "Failed to change permissions",
"name": "Name",
"sortByName": "Name",
"sortByDate": "Date Modified",
"sortBySize": "Size",
"ascending": "Ascending",
"descending": "Descending"
},
"tunnel": {
"noTunnelsConfigured": "No Tunnels Configured",
@@ -1897,7 +1973,7 @@
"loginButton": "Login",
"registerButton": "Register",
"forgotPassword": "Forgot Password?",
"rememberMe": "Remember Me",
"rememberMe": "Remember Device for 30 Days (includes TOTP)",
"noAccount": "Don't have an account?",
"hasAccount": "Already have an account?",
"loginSuccess": "Login successful",
@@ -2062,6 +2138,7 @@
"processing": "Processing...",
"pleaseWait": "Please wait...",
"registrationDisabled": "New account registration is currently disabled by an admin. Please log in or contact an administrator.",
"userNotAllowed": "Your account is not authorized to register. Please contact an administrator.",
"databaseConnected": "Database connected successfully",
"databaseConnectionFailed": "Failed to connect to the database server",
"checkServerConnection": "Please check your server connection and try again",
@@ -2117,7 +2194,9 @@
"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"
"enableCommandPaletteShortcutDesc": "Double-tap left Shift to open the Command Palette for quick access to hosts",
"enableTerminalSessionPersistence": "Persistent Terminal Sessions",
"enableTerminalSessionPersistenceDesc": "Maintain SSH connections when switching tabs or closing the browser (may be unstable)"
},
"user": {
"failedToLoadVersionInfo": "Failed to load version information"
@@ -2153,6 +2232,7 @@
"usernameField": "name",
"scopes": "openid email profile",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"allowedUsers": "user@example.com, @company.com",
"enterUsername": "Enter username to make admin",
"searchHosts": "Search hosts by name, username, IP, folder, tags...",
"enterPassword": "Enter your password",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "2-rigting",
"threeSplit": "3-rigting",
"fourSplit": "4-rigting",
"fiveSplit": "5-rigting",
"sixSplit": "6-rigting",
"availableTabs": "Beskikbare oortjies",
"dragTabsHint": "Sleep oortjies na die uitlegselle hieronder om hulle toe te ken",
"layout": "Gesplete skermuitleg",
@@ -372,7 +374,11 @@
"warning": "Waarskuwing",
"notValidatedWarning": "URL nie gevalideer nie - maak seker dit is korrek",
"changeServer": "Verander bediener",
"mustIncludeProtocol": "Bediener-URL moet met http:// of https:// begin"
"mustIncludeProtocol": "Bediener-URL moet met http:// of https:// begin",
"useEmbedded": "Gebruik plaaslike bediener",
"embeddedDesc": "Begin Termix met die ingeboude plaaslike bediener (geen afstandbediener nodig nie)",
"embeddedConnecting": "Verbind met plaaslike bediener...",
"embeddedNotReady": "Die plaaslike bediener is nog nie gereed nie. Wag asseblief 'n oomblik en probeer weer."
},
"versionCheck": {
"error": "Weergawekontrolefout",
@@ -471,6 +477,7 @@
"confirm": "Bevestig",
"yes": "Ja",
"no": "Nee",
"or": "OF",
"ok": "Goed",
"enabled": "Geaktiveer",
"disabled": "Gestremd",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "Gebruiker {{username}} suksesvol verwyder",
"failedToDeleteUser": "Kon nie gebruiker verwyder nie",
"overrideUserInfoUrl": "Oorskryf Gebruikersinligting-URL (nie vereis nie)",
"allowedUsers": "Toegelate Gebruikers",
"allowedUsersDescription": "Komma-geskeide lys van toegelate e-pospatrone. Gebruik presiese e-posadresse (gebruiker@voorbeeld.com), domeinagtervoegsels (@voorbeeld.com), of los leeg om alle gebruikers toe te laat.",
"failedToFetchSessions": "Kon nie sessies haal nie",
"sessionRevokedSuccessfully": "Sessie suksesvol herroep",
"failedToRevokeSession": "Kon nie sessie herroep nie",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "KRITIES: Wagwoordaanmelding is gedeaktiveer. As jy OIDC herstel of verkeerd konfigureer, sal jy alle toegang tot Termix verloor en jou instansie blokkeer. Gaan slegs voort as jy absoluut seker is.",
"confirmDisableOIDCWarning": "WAARSKUWING: Jy is op die punt om OIDC te deaktiveer terwyl wagwoordaanmelding ook gedeaktiveer is. Dit sal jou Termix-instansie blokkeer en jy sal alle toegang verloor. Is jy absoluut seker jy wil voortgaan?",
"failedToUpdatePasswordLoginStatus": "Kon nie wagwoord-aanmeldstatus opdateer nie",
"monitoringDefaults": "Monitering van Standaardwaardes",
"monitoringDefaultsDesc": "Globale standaardintervalle vir statuskontroles en metrieke-insameling. Instellings per gasheer ignoreer hierdie waardes.",
"globalStatusCheckInterval": "Standaardstatuskontrole-interval",
"globalMetricsInterval": "Standaard Metrieke Versamelingsinterval",
"globalSettingsSaved": "Globale moniteringsinstellings gestoor",
"failedToSaveGlobalSettings": "Kon nie globale moniteringsinstellings stoor nie",
"failedToLoadGlobalSettings": "Kon nie globale moniteringsinstellings laai nie",
"loadingSessions": "Laai sessies...",
"noActiveSessions": "Geen aktiewe sessies gevind nie.",
"device": "Toestel",
@@ -841,6 +857,11 @@
"noHostsInJson": "Geen gashere in JSON-lêer gevind nie",
"maxHostsAllowed": "Maksimum 100 gashere toegelaat per invoer",
"importCompleted": "Invoer voltooi: {{success}} suksesvol, {{failed}} misluk",
"importCreated": "geskep",
"importUpdated": "opgedateer",
"importFailedCount": "misluk",
"importSkipExisting": "Invoer (slaan bestaande oor)",
"importOverwriteExisting": "Invoer (oorskryf bestaande)",
"importFailed": "Invoer het misluk",
"importError": "Invoerfout",
"failedToImportJson": "Kon nie JSON-lêer invoer nie",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "Kontroleer of die bediener aanlyn of vanlyn is",
"statusCheckInterval": "Statuskontrole-interval",
"statusCheckIntervalDesc": "Hoe gereeld om te kyk of die gasheer aanlyn is (5s - 1u)",
"useGlobalStatusInterval": "Gebruik globale verstekwaarde",
"useGlobalMetricsInterval": "Gebruik globale verstekwaarde",
"usingGlobalDefault": "Gebruik globale verstekwaarde ({{value}}s)",
"metricsEnabled": "Aktiveer Metrieke Monitering",
"metricsEnabledDesc": "Versamel SVE-, RAM-, skyf- en ander stelselstatistieke",
"metricsInterval": "Metrieke-insamelingsinterval",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "Bied outomaties aan om SSH-wagwoord in te voeg wanneer sudo vir wagwoord vra",
"sudoPassword": "Sudo-wagwoord",
"sudoPasswordDesc": "Opsionele wagwoord vir sudo-opdragte (nuttig met sleutelverifikasie)",
"keepaliveSettings": "SSH Keepalive",
"keepaliveSettingsDesc": "Konfigureer SSH-vlak keepalive-gedrag. Stel interval op 0 om SSH keepalives te deaktiveer (nuttig vir MikroTik en ander toestelle wat keepalive-versoeke ignoreer).",
"keepaliveInterval": "Lewendige interval (ms)",
"keepaliveIntervalDesc": "Hoe gereeld om SSH keepalive pings te stuur. Standaard: 30000 (30s). Stel na 0 om te deaktiveer.",
"keepaliveCountMax": "Hou Maksimum Mislukkings",
"keepaliveCountMaxDesc": "Ontkoppel na soveel onbeantwoorde lewende boodskappe. Standaard: 3.",
"socks4": "SOKKIES4",
"socks5": "SOKKIES5",
"httpConnect": "HTTP-VERBINDING",
"testProxy": "Toetsverbinding",
"testingProxy": "Toetsing...",
"proxyTestSuccess": "Proxy-verbinding suksesvol ({{latency}}ms)",
"proxyTestFailed": "Proxy-toets het misluk: {{error}}",
"connectionPath": "Verbindingspad",
"executeSnippetOnConnect": "Voer 'n brokkie uit wanneer die terminaal verbind",
"autoMosh": "Outomatiese MOSH",
"autoMoshDesc": "Voer outomaties die MOSH-opdrag uit tydens verbinding",
@@ -1284,7 +1320,35 @@
"showTimestamps": "Wys Tydstempels",
"autoRefresh": "Outomatiese Herlaai",
"filterLogsPlaceholder": "Filtreer logboeke...",
"noLogsAvailable": "Geen logboeke beskikbaar nie"
"noLogsAvailable": "Geen logboeke beskikbaar nie",
"selectMode": "Kies",
"exitSelectMode": "Kanselleer",
"selectedCount": "{{count}} gekies",
"bulkMonitoring": "Monitering",
"bulkFeatures": "Kenmerke",
"bulkMoveFolder": "Skuif na vouer",
"bulkPin": "Speld vas",
"bulkUnpin": "Ontspeld",
"enableAllFeatures": "Aktiveer alle funksies",
"disableAllFeatures": "Deaktiveer alle funksies",
"enableStatusCheck": "Aktiveer Statuskontrole",
"disableStatusCheck": "Deaktiveer Statuskontrole",
"enableMetrics": "Aktiveer Metrieke",
"disableMetrics": "Deaktiveer Metrieke",
"bulkEnableTerminal": "Aktiveer Terminaal",
"bulkDisableTerminal": "Deaktiveer terminaal",
"bulkEnableTunnel": "Aktiveer Tonnel",
"bulkDisableTunnel": "Deaktiveer Tonnel",
"bulkEnableFileManager": "Aktiveer Lêerbestuurder",
"bulkDisableFileManager": "Deaktiveer Lêerbestuurder",
"bulkEnableDocker": "Aktiveer Docker",
"bulkDisableDocker": "Deaktiveer Docker",
"bulkUpdateSuccess": "{{count}} gasheer(s) suksesvol opgedateer",
"bulkUpdateFailed": "Grootmaatopdatering het misluk",
"selectAll": "Kies alles",
"deselectAll": "Deselekteer alles",
"useGlobalStatusDefault": "Gebruik globale verstekwaarde (status)",
"useGlobalMetricsDefault": "Gebruik globale verstekwaarde (metrieke)"
},
"terminal": {
"title": "Terminaal",
@@ -1306,6 +1370,8 @@
"connectionClosed": "Verbinding gesluit",
"connectionError": "Verbindingsfout: {{message}}",
"connected": "Verbonde",
"clipboardWriteFailed": "Kon nie na knipbord kopieer nie. Maak seker dat die bladsy oor HTTPS of localhost bedien word.",
"clipboardReadFailed": "Kon nie vanaf knipbord lees nie. Maak seker dat knipbordtoestemmings toegestaan is.",
"sshConnected": "SSH-verbinding gevestig",
"authError": "Verifikasie het misluk: {{message}}",
"unknownError": "Onbekende fout het voorgekom",
@@ -1360,7 +1426,9 @@
"totpTimeout": "TOTP-verifikasie het uitgeloop. Koppel asseblief weer aan.",
"passwordTimeout": "Wagwoordverifikasie het uitgeloop. Koppel asseblief weer aan.",
"connectionRejected": "Verbinding deur bediener verwerp. Kontroleer asseblief u verifikasie en netwerkkonfigurasie.",
"hostKeyRejected": "SSH-gasheersleutelverifikasie verwerp. Verbinding gekanselleer."
"hostKeyRejected": "SSH-gasheersleutelverifikasie verwerp. Verbinding gekanselleer.",
"sessionTakenOver": "Sessie is in 'n ander oortjie oopgemaak. Herverbind tans...",
"sessionAttachTimeout": "Sessie-aanhegsel het uitgetel. Skep nuwe verbinding..."
},
"fileManager": {
"title": "Lêerbestuurder",
@@ -1670,7 +1738,13 @@
"write": "Skryf",
"execute": "Voer uit",
"permissionsChangedSuccessfully": "Toestemmings suksesvol verander",
"failedToChangePermissions": "Kon nie toestemmings verander nie"
"failedToChangePermissions": "Kon nie toestemmings verander nie",
"name": "Naam",
"sortByName": "Naam",
"sortByDate": "Datum Gewysig",
"sortBySize": "Grootte",
"ascending": "Stygend",
"descending": "Dalend"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "Aanmeld",
"registerButton": "Registreer",
"forgotPassword": "Wagwoord vergeet?",
"rememberMe": "Onthou My",
"rememberMe": "Onthou toestel vir 30 dae (sluit TOTP in)",
"noAccount": "Het jy nie 'n rekening nie?",
"hasAccount": "Het jy reeds 'n rekening?",
"loginSuccess": "Aanmelding suksesvol",
@@ -2023,6 +2097,7 @@
"processing": "Verwerk...",
"pleaseWait": "Wag asseblief...",
"registrationDisabled": "Nuwe rekeningregistrasie is tans deur 'n administrateur gedeaktiveer. Meld asseblief aan of kontak 'n administrateur.",
"userNotAllowed": "Jou rekening is nie gemagtig om te registreer nie. Kontak asseblief 'n administrateur.",
"databaseConnected": "Databasis suksesvol gekoppel",
"databaseConnectionFailed": "Kon nie aan die databasisbediener koppel nie",
"checkServerConnection": "Kontroleer asseblief u bedienerverbinding en probeer weer",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "Kies die kleurtema vir die toepassing",
"terminalSyntaxHighlightingDesc": "Merk outomaties opdragte, paaie, IP's en logvlakke in terminaaluitvoer",
"enableCommandPaletteShortcut": "Aktiveer die opdragpalet-kortpad",
"enableCommandPaletteShortcutDesc": "Dubbeltik links Shift om die opdragpalet oop te maak vir vinnige toegang tot gashere"
"enableCommandPaletteShortcutDesc": "Dubbeltik links Shift om die opdragpalet oop te maak vir vinnige toegang tot gashere",
"enableTerminalSessionPersistence": "Aanhoudende terminale sessies",
"enableTerminalSessionPersistenceDesc": "Handhaaf SSH-verbindings wanneer jy oortjies wissel of die blaaier toemaak (dit kan onstabiel wees)."
},
"user": {
"failedToLoadVersionInfo": "Kon nie weergawe-inligting laai nie"
@@ -2114,6 +2191,7 @@
"usernameField": "naam",
"scopes": "openid e-posprofiel",
"userinfoUrl": "https://jou-verskaffer.com/aansoek/o/gebruikersinfo/",
"allowedUsers": "gebruiker@voorbeeld.com, @maatskappy.com",
"enterUsername": "Voer gebruikersnaam in om admin te word",
"searchHosts": "Soek gashere volgens naam, gebruikersnaam, IP, vouer, etikette...",
"enterPassword": "Voer jou wagwoord in",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "طريقتان",
"threeSplit": "3-طريق",
"fourSplit": "4-طريق",
"fiveSplit": "5-الطريق",
"sixSplit": "6-طريق",
"availableTabs": "علامات التبويب المتاحة",
"dragTabsHint": "اسحب علامات التبويب إلى خلايا التخطيط أدناه لتعيينها",
"layout": "تقسيم تخطيط الشاشة",
@@ -372,7 +374,11 @@
"warning": "تحذير",
"notValidatedWarning": "لم يتم التحقق من صحة الرابط - تأكد من صحته",
"changeServer": "تغيير الخادم",
"mustIncludeProtocol": "عنوان URL للخادم يجب أن يبدأ مع http:// أو https://"
"mustIncludeProtocol": "عنوان URL للخادم يجب أن يبدأ مع http:// أو https://",
"useEmbedded": "استخدام الخادم المحلي",
"embeddedDesc": "تشغيل ترميكس مع الخادم المحلي المدمج (لا حاجة للخادم البعيد)",
"embeddedConnecting": "جاري الاتصال بالخادم المحلي...",
"embeddedNotReady": "الخادم المحلي غير جاهز بعد. الرجاء الانتظار لحظة وحاول مرة أخرى."
},
"versionCheck": {
"error": "خطأ في التحقق من الإصدار",
@@ -471,6 +477,7 @@
"confirm": "تأكيد",
"yes": "نعم",
"no": "لا",
"or": "أو",
"ok": "حسناً",
"enabled": "تمكين",
"disabled": "معطل",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "تم حذف المستخدم {{username}} بنجاح",
"failedToDeleteUser": "فشل في حذف المستخدم",
"overrideUserInfoUrl": "تجاوز رابط معلومات المستخدم (غير مطلوب)",
"allowedUsers": "المستخدمين المسموح بهم",
"allowedUsersDescription": "قائمة مفصولة بفواصل بأنماط البريد الإلكتروني المسموح بها. استخدم رسائل البريد الإلكتروني بالضبط (user@example.com)، المجال لاحقة (@example.com)، أو اترك فارغًا للسماح لجميع المستخدمين.",
"failedToFetchSessions": "فشل في جلب الجلسات",
"sessionRevokedSuccessfully": "تم إلغاء الجلسة بنجاح",
"failedToRevokeSession": "فشل في إلغاء الجلسة",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "إنشاء: تم تعطيل تسجيل الدخول لكلمة المرور. إذا قمت بإعادة تعيين OIDC أو إساءة إعداده، فستفقد جميع الصلاحيات إلى Termix وستلغي مثالك. المضي قدما فقط إذا كنت متأكدا تماما.",
"confirmDisableOIDCWarning": "تحذير: أنت على وشك تعطيل OIDC بينما يتم تعطيل تسجيل الدخول لكلمة المرور أيضا. سيؤدي هذا إلى طوب مثيل الـ Termix الخاص بك وسوف تفقد كل الوصول. هل أنت متأكد تماما من أنك تريد المتابعة؟",
"failedToUpdatePasswordLoginStatus": "فشل تحديث حالة تسجيل الدخول لكلمة المرور",
"monitoringDefaults": "مراقبة الافتراضات",
"monitoringDefaultsDesc": "الفترات الافتراضية العالمية للتحقق من الحالة وجمع المقاييس. تتجاوز إعدادات كل مضيف هذه القيم.",
"globalStatusCheckInterval": "الفاصل الزمني الافتراضي للتحقق من الحالة",
"globalMetricsInterval": "الفاصل الزمني الافتراضي لمجموعة القياسات",
"globalSettingsSaved": "تم حفظ إعدادات المراقبة العالمية",
"failedToSaveGlobalSettings": "فشل في حفظ إعدادات المراقبة العالمية",
"failedToLoadGlobalSettings": "فشل تحميل إعدادات المراقبة العالمية",
"loadingSessions": "جاري تحميل الجلسات...",
"noActiveSessions": "لم يتم العثور على جلسات نشطة.",
"device": "الجهاز",
@@ -841,6 +857,11 @@
"noHostsInJson": "لم يتم العثور على مضيف في ملف JSON",
"maxHostsAllowed": "الحد الأقصى المسموح به 100 مضيف لكل استيراد",
"importCompleted": "اكتمل الاستيراد: {{success}} ناجح, {{failed}} فشل",
"importCreated": "أنشئ",
"importUpdated": "تم التحديث",
"importFailedCount": "فشل",
"importSkipExisting": "استيراد (تخطي قائم)",
"importOverwriteExisting": "الاستيراد (الكتابة فوق القائمة)",
"importFailed": "فشل الاستيراد",
"importError": "خطأ في الاستيراد",
"failedToImportJson": "فشل استيراد ملف JSON",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "تحقق مما إذا كان الخادم متصلاً أو غير متصل",
"statusCheckInterval": "فترة التحقق من الحالة",
"statusCheckIntervalDesc": "كم عدد المرات للتحقق مما إذا كان المضيف متصل بالإنترنت (5s - 1ساعة)",
"useGlobalStatusInterval": "استخدام الافتراضي العام",
"useGlobalMetricsInterval": "استخدام الافتراضي العام",
"usingGlobalDefault": "استخدام الافتراضي العالمي ({{value}})",
"metricsEnabled": "تمكين مراقبة القياسات",
"metricsEnabledDesc": "جمع إحصائيات المعالجة، ذاكرة الوصول العشوائي، القرص وغيرها من إحصائيات النظام",
"metricsInterval": "الفاصل الزمني لمجموعة القياسات",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "عرض تلقائي لإدراج كلمة مرور SSH عندما يطلب sudo لكلمة المرور",
"sudoPassword": "كلمة مرور سودو",
"sudoPasswordDesc": "كلمة المرور الاختيارية لأوامر sudo (مفيدة مع مصادقة المفتاح)",
"keepaliveSettings": "حفظ SSH",
"keepaliveSettingsDesc": "تهيئة السلوك على مستوى SSHH. تعيين الفاصل الزمني إلى 0 لتعطيل الحفاظ على SSH (مفيد لMikroTik والأجهزة الأخرى التي تتجاهل طلبات البقاء على قيد الحياة).",
"keepaliveInterval": "الفاصل الزمني للحفاظ على (مللي ثانية)",
"keepaliveIntervalDesc": "كم مرة إرسال صفحات SSH الاحتفاظ بالحياة. الافتراضي: 30000 (30). تعيين إلى 0 لتعطيل.",
"keepaliveCountMax": "الحفاظ على الحد الأقصى للفشل",
"keepaliveCountMaxDesc": "قطع الاتصال بعد هذا العديد من الابقاء بدون إجابة. الافتراضي: 3.",
"socks4": "SOCKS4",
"socks5": "SOCKS5",
"httpConnect": "اتصال HTTP",
"testProxy": "اختبار الاتصال",
"testingProxy": "اختبار...",
"proxyTestSuccess": "تم بنجاح اتصال البروكسي ({{latency}}ms)",
"proxyTestFailed": "فشل اختبار البروكسي: {{error}}",
"connectionPath": "مسار الاتصال",
"executeSnippetOnConnect": "تنفيذ كتلة الكود عندما تتصل المحطة الطرفية",
"autoMosh": "وضع تلقائي",
"autoMoshDesc": "تشغيل أمر MOSH تلقائياً عند الاتصال",
@@ -1284,7 +1320,35 @@
"showTimestamps": "إظهار الطوابع الزمنية",
"autoRefresh": "تحديث تلقائي",
"filterLogsPlaceholder": "تصفية السجلات...",
"noLogsAvailable": "لا توجد سجلات متاحة"
"noLogsAvailable": "لا توجد سجلات متاحة",
"selectMode": "حدد",
"exitSelectMode": "إلغاء",
"selectedCount": "تم اختيار {{count}}",
"bulkMonitoring": "الرصد",
"bulkFeatures": "الميزات",
"bulkMoveFolder": "نقل إلى مجلد",
"bulkPin": "تثبيت",
"bulkUnpin": "Unpin",
"enableAllFeatures": "تمكين جميع الميزات",
"disableAllFeatures": "تعطيل جميع الميزات",
"enableStatusCheck": "تمكين التحقق من الحالة",
"disableStatusCheck": "تعطيل التحقق من الحالة",
"enableMetrics": "تمكين القياسات",
"disableMetrics": "تعطيل القياسات",
"bulkEnableTerminal": "تمكين المحطة الطرفية",
"bulkDisableTerminal": "تعطيل المحطة الطرفية",
"bulkEnableTunnel": "تمكين النفق",
"bulkDisableTunnel": "تعطيل النفق",
"bulkEnableFileManager": "تمكين مدير الملفات",
"bulkDisableFileManager": "تعطيل مدير الملفات",
"bulkEnableDocker": "تمكين Docker",
"bulkDisableDocker": "تعطيل المرفأ",
"bulkUpdateSuccess": "تم تحديث مضيف {{count}} بنجاح",
"bulkUpdateFailed": "فشل التحديث الشامل",
"selectAll": "حدد الكل",
"deselectAll": "إلغاء تحديد الكل",
"useGlobalStatusDefault": "استخدام الافتراضي العام (الحالة)",
"useGlobalMetricsDefault": "استخدام الافتراضي العام (القياسات)"
},
"terminal": {
"title": "المحطة",
@@ -1306,6 +1370,8 @@
"connectionClosed": "تم إغلاق الاتصال",
"connectionError": "خطأ في الاتصال: {{message}}",
"connected": "متصل",
"clipboardWriteFailed": "فشل النسخ إلى الحافظة. تأكد من خدمة الصفحة عبر HTTPS أو localhost.",
"clipboardReadFailed": "فشل في القراءة من الحافظة. تأكد من منح أذونات الحافظة.",
"sshConnected": "تم إنشاء اتصال SSH",
"authError": "فشل المصادقة: {{message}}",
"unknownError": "حدث خطأ غير معروف",
@@ -1360,7 +1426,9 @@
"totpTimeout": "انتهت مهلة التحقق من TOTP. الرجاء إعادة الاتصال.",
"passwordTimeout": "انتهت مهلة التحقق من كلمة المرور. الرجاء إعادة الاتصال.",
"connectionRejected": "تم رفض الاتصال بواسطة الخادم. الرجاء التحقق من المصادقة الخاصة بك وتكوين الشبكة.",
"hostKeyRejected": "تم رفض التحقق من مفتاح المضيف SSH. تم إلغاء الاتصال."
"hostKeyRejected": "تم رفض التحقق من مفتاح المضيف SSH. تم إلغاء الاتصال.",
"sessionTakenOver": "تم فتح الجلسة في علامة تبويب أخرى. إعادة الاتصال...",
"sessionAttachTimeout": "انتهت مهلة مرفق الجلسة. إنشاء اتصال جديد..."
},
"fileManager": {
"title": "مدير الملفات",
@@ -1670,7 +1738,13 @@
"write": "كتابة",
"execute": "تنفيذ",
"permissionsChangedSuccessfully": "تم تغيير الأذونات بنجاح",
"failedToChangePermissions": "فشل في تغيير الأذونات"
"failedToChangePermissions": "فشل في تغيير الأذونات",
"name": "الاسم",
"sortByName": "الاسم",
"sortByDate": "تاريخ التعديل",
"sortBySize": "الحجم",
"ascending": "تصاعدي",
"descending": "تنازلي"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "تسجيل الدخول",
"registerButton": "تسجيل",
"forgotPassword": "نسيت كلمة المرور؟",
"rememberMe": "تذكر لي",
"rememberMe": "تذكر الجهاز لمدة 30 يوما (يشمل TOTP)",
"noAccount": "ليس لديك حساب؟",
"hasAccount": "لديك حساب بالفعل؟",
"loginSuccess": "تم تسجيل الدخول بنجاح",
@@ -2023,6 +2097,7 @@
"processing": "معالجة...",
"pleaseWait": "الرجاء الانتظار...",
"registrationDisabled": "تسجيل الحساب الجديد معطل حاليًا من قبل المسؤول. الرجاء تسجيل الدخول أو الاتصال بالمسؤول.",
"userNotAllowed": "حسابك غير مصرح له بالتسجيل. الرجاء الاتصال بالمسؤول.",
"databaseConnected": "تم توصيل قاعدة البيانات بنجاح",
"databaseConnectionFailed": "فشل الاتصال بخادم قاعدة البيانات",
"checkServerConnection": "الرجاء التحقق من اتصال الخادم الخاص بك وحاول مرة أخرى",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "حدد سمة اللون للتطبيق",
"terminalSyntaxHighlightingDesc": "تسليط الضوء تلقائياً على الأوامر والمسارات وIP ومستويات السجل في المخرجات الطرفية",
"enableCommandPaletteShortcut": "تمكين اختصار قفل لوحة الأوامر",
"enableCommandPaletteShortcutDesc": "انقر نقرًا مزدوجًا على نوبات اليسار لفتح لوحة القيادة للوصول السريع إلى المضيفين"
"enableCommandPaletteShortcutDesc": "انقر نقرًا مزدوجًا على نوبات اليسار لفتح لوحة القيادة للوصول السريع إلى المضيفين",
"enableTerminalSessionPersistence": "الدورات الطرفية المستمرة",
"enableTerminalSessionPersistenceDesc": "الحفاظ على اتصالات SSH عند تبديل علامات التبويب أو إغلاق المتصفح (قد يكون غير مستقر)"
},
"user": {
"failedToLoadVersionInfo": "فشل تحميل معلومات الإصدار"
@@ -2114,6 +2191,7 @@
"usernameField": "اسم",
"scopes": "ملف البريد الإلكتروني المفتوح",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"allowedUsers": "user@example.com, @company.com",
"enterUsername": "أدخل اسم المستخدم لجعل المشرف",
"searchHosts": "البحث عن المضيفين حسب الاسم، اسم المستخدم، IP ، المجلد، العلامات...",
"enterPassword": "أدخل كلمة المرور",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "2-посочно",
"threeSplit": "3-посочен",
"fourSplit": "4-посочен",
"fiveSplit": "5-посочен",
"sixSplit": "6-посочен",
"availableTabs": "Налични раздели",
"dragTabsHint": "Плъзнете раздели към клетките на оформлението по-долу, за да ги присвоите",
"layout": "Разделен екран",
@@ -372,7 +374,11 @@
"warning": "Предупреждение",
"notValidatedWarning": "URL адресът не е валидиран - уверете се, че е правилен",
"changeServer": "Промяна на сървъра",
"mustIncludeProtocol": "URL адресът на сървъра трябва да започва с http:// или https://"
"mustIncludeProtocol": "URL адресът на сървъра трябва да започва с http:// или https://",
"useEmbedded": "Използване на локален сървър",
"embeddedDesc": "Стартирайте Termix с вградения локален сървър (не е необходим отдалечен сървър)",
"embeddedConnecting": "Свързване с локален сървър...",
"embeddedNotReady": "Локалният сървър все още не е готов. Моля, изчакайте малко и опитайте отново."
},
"versionCheck": {
"error": "Грешка при проверка на версията",
@@ -471,6 +477,7 @@
"confirm": "Потвърди",
"yes": "Да",
"no": "Не",
"or": "ИЛИ",
"ok": "Добре",
"enabled": "Активирано",
"disabled": "За хора с увреждания",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "Потребител {{username}} е изтрит успешно",
"failedToDeleteUser": "Изтриването на потребителя не бе успешно",
"overrideUserInfoUrl": "Замяна на URL адреса с информация за потребителя (не е задължително)",
"allowedUsers": "Разрешени потребители",
"allowedUsersDescription": "Списък с разрешени имейл шаблони, разделени със запетаи. Използвайте точни имейли (user@example.com), суфикси на домейн (@example.com) или оставете празно, за да разрешите всички потребители.",
"failedToFetchSessions": "Извличането на сесиите не бе успешно",
"sessionRevokedSuccessfully": "Сесията е успешно отменена",
"failedToRevokeSession": "Неуспешно отменяне на сесията",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "КРИТИЧНО: Входът с парола е деактивиран. Ако нулирате или конфигурирате неправилно OIDC, ще загубите целия достъп до Termix и ще блокирате вашия екземпляр. Продължете само ако сте абсолютно сигурни.",
"confirmDisableOIDCWarning": "ПРЕДУПРЕЖДЕНИЕ: На път сте да деактивирате OIDC, докато входът с парола също е деактивиран. Това ще блокира вашия Termix екземпляр и ще загубите всякакъв достъп. Абсолютно ли сте сигурни, че искате да продължите?",
"failedToUpdatePasswordLoginStatus": "Актуализирането на състоянието на вход с парола не бе успешно",
"monitoringDefaults": "Мониторинг на настройки по подразбиране",
"monitoringDefaultsDesc": "Глобални интервали по подразбиране за проверки на състоянието и събиране на показатели. Настройките за всеки хост отменят тези стойности.",
"globalStatusCheckInterval": "Интервал за проверка на състоянието по подразбиране",
"globalMetricsInterval": "Интервал за събиране на показатели по подразбиране",
"globalSettingsSaved": "Глобалните настройки за наблюдение са запазени",
"failedToSaveGlobalSettings": "Запазването на настройките за глобално наблюдение не бе успешно",
"failedToLoadGlobalSettings": "Зареждането на настройките за глобално наблюдение не бе успешно",
"loadingSessions": "Зареждане на сесиите...",
"noActiveSessions": "Няма намерени активни сесии.",
"device": "Устройство",
@@ -841,6 +857,11 @@
"noHostsInJson": "Не са намерени хостове във JSON файла",
"maxHostsAllowed": "Максимално 100 хоста са разрешени на импортиране",
"importCompleted": "Импортирането завършено: {{success}} успешно, {{failed}} неуспешно",
"importCreated": "създаден",
"importUpdated": "актуализиран",
"importFailedCount": "неуспешен",
"importSkipExisting": "Импортиране (пропускане на съществуващото)",
"importOverwriteExisting": "Импортиране (презаписване на съществуващо)",
"importFailed": "Импортирането не бе успешно",
"importError": "Грешка при импортиране",
"failedToImportJson": "Импортирането на JSON файл не бе успешно",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "Проверете дали сървърът е онлайн или офлайн",
"statusCheckInterval": "Интервал за проверка на състоянието",
"statusCheckIntervalDesc": "Колко често да проверявам дали хостът е онлайн (5 секунди - 1 час)",
"useGlobalStatusInterval": "Използване на глобално по подразбиране",
"useGlobalMetricsInterval": "Използване на глобално по подразбиране",
"usingGlobalDefault": "Използване на глобалното по подразбиране ({{value}}s)",
"metricsEnabled": "Активиране на мониторинг на показатели",
"metricsEnabledDesc": "Събиране на статистически данни за процесора, RAM, диска и други системни данни",
"metricsInterval": "Интервал на събиране на показатели",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "Автоматично предлагане за въвеждане на SSH парола, когато sudo поиска парола",
"sudoPassword": "Парола за Sudo",
"sudoPasswordDesc": "Незадължителна парола за sudo команди (полезно при удостоверяване с ключ)",
"keepaliveSettings": "SSH поддържане на активност",
"keepaliveSettingsDesc": "Конфигурирайте поведението за keepalive на ниво SSH. Задайте интервал на 0, за да деактивирате SSH keepalive (полезно за MikroTik и други устройства, които игнорират заявките за keepalive).",
"keepaliveInterval": "Интервал на поддържане на активността (ms)",
"keepaliveIntervalDesc": "Колко често да се изпращат SSH keepalive ping-ове. По подразбиране: 30000 (30 секунди). Задайте на 0, за да деактивирате.",
"keepaliveCountMax": "Максимални неуспехи при поддържане на активността",
"keepaliveCountMaxDesc": "Прекъсване след толкова много неотговорени keepalive съобщения. По подразбиране: 3.",
"socks4": "ЧОРАПИ4",
"socks5": "ЧОРАПИ5",
"httpConnect": "HTTP ВРЪЗКА",
"testProxy": "Тест на връзката",
"testingProxy": "Тестване...",
"proxyTestSuccess": "Прокси връзката е успешна ({{latency}}мс)",
"proxyTestFailed": "Тестът на прокси сървъра не бе успешен: {{error}}",
"connectionPath": "Път на свързване",
"executeSnippetOnConnect": "Изпълнете фрагмент, когато терминалът се свърже",
"autoMosh": "Auto-MOSH",
"autoMoshDesc": "Автоматично изпълнение на MOSH команда при свързване",
@@ -1284,7 +1320,35 @@
"showTimestamps": "Показване на времеви отпечатъци",
"autoRefresh": "Автоматично опресняване",
"filterLogsPlaceholder": "Филтриране на лог файлове...",
"noLogsAvailable": "Няма налични лог файлове"
"noLogsAvailable": "Няма налични лог файлове",
"selectMode": "Изберете",
"exitSelectMode": "Отказ",
"selectedCount": "{{count}} избрано",
"bulkMonitoring": "Мониторинг",
"bulkFeatures": "Характеристики",
"bulkMoveFolder": "Преместване в папка",
"bulkPin": "Закачи",
"bulkUnpin": "Откачи",
"enableAllFeatures": "Активиране на всички функции",
"disableAllFeatures": "Деактивиране на всички функции",
"enableStatusCheck": "Активиране на проверка на състоянието",
"disableStatusCheck": "Деактивиране на проверката на състоянието",
"enableMetrics": "Активиране на показатели",
"disableMetrics": "Деактивиране на показателите",
"bulkEnableTerminal": "Активиране на терминала",
"bulkDisableTerminal": "Деактивиране на терминала",
"bulkEnableTunnel": "Активиране на тунел",
"bulkDisableTunnel": "Деактивиране на тунела",
"bulkEnableFileManager": "Активиране на файловия мениджър",
"bulkDisableFileManager": "Деактивиране на файловия мениджър",
"bulkEnableDocker": "Активиране на Докер",
"bulkDisableDocker": "Деактивиране на Докер",
"bulkUpdateSuccess": "{{count}} хост(ове) са актуализирани успешно",
"bulkUpdateFailed": "Груповата актуализация не бе успешна",
"selectAll": "Избери всички",
"deselectAll": "Премахване на избора от всички",
"useGlobalStatusDefault": "Използване на глобално по подразбиране (състояние)",
"useGlobalMetricsDefault": "Използване на глобални настройки по подразбиране (метрики)"
},
"terminal": {
"title": "Терминал",
@@ -1306,6 +1370,8 @@
"connectionClosed": "Връзката е затворена",
"connectionError": "Грешка при свързване: {{message}}",
"connected": "Свързан",
"clipboardWriteFailed": "Копирането в клипборда не бе успешно. Уверете се, че страницата се предоставя през HTTPS или localhost.",
"clipboardReadFailed": "Четенето от клипборда не бе успешно. Уверете се, че са предоставени разрешения за достъп до клипборда.",
"sshConnected": "SSH връзка е установена",
"authError": "Удостоверяването не бе успешно: {{message}}",
"unknownError": "Възникна неизвестна грешка",
@@ -1360,7 +1426,9 @@
"totpTimeout": "Времето за изчакване за проверка на TOTP изтече. Моля, свържете се отново.",
"passwordTimeout": "Времето за изчакване за потвърждение на паролата изтече. Моля, свържете се отново.",
"connectionRejected": "Връзката е отхвърлена от сървъра. Моля, проверете удостоверяването и мрежовата си конфигурация.",
"hostKeyRejected": "Проверката на SSH ключа за хост е отхвърлена. Връзката е прекратена."
"hostKeyRejected": "Проверката на SSH ключа за хост е отхвърлена. Връзката е прекратена.",
"sessionTakenOver": "Сесията беше отворена в друг раздел. Повторно свързване...",
"sessionAttachTimeout": "Времето за изчакване на прикачения файл към сесията изтече. Създаване на нова връзка..."
},
"fileManager": {
"title": "Файлов мениджър",
@@ -1670,7 +1738,13 @@
"write": "Пишете",
"execute": "Изпълнение",
"permissionsChangedSuccessfully": "Разрешенията са променени успешно",
"failedToChangePermissions": "Промяната на разрешенията не бе успешна"
"failedToChangePermissions": "Промяната на разрешенията не бе успешна",
"name": "Име",
"sortByName": "Име",
"sortByDate": "Дата на промяна",
"sortBySize": "Размер",
"ascending": "Възходящ",
"descending": "Низходящо"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "Вход",
"registerButton": "Регистрация",
"forgotPassword": "Забравена парола?",
"rememberMe": "Запомни ме",
"rememberMe": "Запомни устройството за 30 дни (включва TOTP)",
"noAccount": "Нямате акаунт?",
"hasAccount": "Вече имате акаунт?",
"loginSuccess": "Влизането е успешно",
@@ -2023,6 +2097,7 @@
"processing": "Обработва се...",
"pleaseWait": "Моля, изчакайте...",
"registrationDisabled": "Регистрацията на нов акаунт в момента е деактивирана от администратор. Моля, влезте или се свържете с администратор.",
"userNotAllowed": "Вашият акаунт не е оторизиран за регистрация. Моля, свържете се с администратор.",
"databaseConnected": "Базата данни е свързана успешно",
"databaseConnectionFailed": "Неуспешно свързване със сървъра на базата данни",
"checkServerConnection": "Моля, проверете връзката със сървъра си и опитайте отново",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "Изберете цветовата тема за приложението",
"terminalSyntaxHighlightingDesc": "Автоматично маркиране на команди, пътища, IP адреси и нива на лог файлове в изхода на терминала",
"enableCommandPaletteShortcut": "Активиране на пряк път за палитра с команди",
"enableCommandPaletteShortcutDesc": "Докоснете двукратно левия Shift, за да отворите палитрата с команди за бърз достъп до хостове"
"enableCommandPaletteShortcutDesc": "Докоснете двукратно левия Shift, за да отворите палитрата с команди за бърз достъп до хостове",
"enableTerminalSessionPersistence": "Постоянни терминални сесии",
"enableTerminalSessionPersistenceDesc": "Поддържане на SSH връзки при превключване на раздели или затваряне на браузъра (може да е нестабилно)"
},
"user": {
"failedToLoadVersionInfo": "Зареждането на информация за версията не бе успешно"
@@ -2114,6 +2191,7 @@
"usernameField": "име",
"scopes": "имейл профил с отворен ID",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"allowedUsers": "потребител@пример.com, @компания.com",
"enterUsername": "Въведете потребителско име, за да го направите администратор",
"searchHosts": "Търсене на хостове по име, потребителско име, IP адрес, папка, тагове...",
"enterPassword": "Въведете паролата си",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "2-উপায়",
"threeSplit": "3-উপায়",
"fourSplit": "-উপায়",
"fiveSplit": "৫-উপায়",
"sixSplit": "৬-উপায়",
"availableTabs": "উপলব্ধ ট্যাবগুলি",
"dragTabsHint": "ট্যাবগুলিকে নিচের লেআউট সেলগুলিতে টেনে আনুন যাতে সেগুলি অ্যাসাইন করা যায়।",
"layout": "স্প্লিট স্ক্রিন লেআউট",
@@ -372,7 +374,11 @@
"warning": "সতর্কতা",
"notValidatedWarning": "URL যাচাই করা হয়নি - নিশ্চিত করুন যে এটি সঠিক",
"changeServer": "সার্ভার পরিবর্তন করুন",
"mustIncludeProtocol": "সার্ভার URL টি http:// অথবা https:// দিয়ে শুরু হতে হবে"
"mustIncludeProtocol": "সার্ভার URL টি http:// অথবা https:// দিয়ে শুরু হতে হবে",
"useEmbedded": "স্থানীয় সার্ভার ব্যবহার করুন",
"embeddedDesc": "অন্তর্নির্মিত স্থানীয় সার্ভার দিয়ে টার্মিক্স চালান (কোনও দূরবর্তী সার্ভারের প্রয়োজন নেই)",
"embeddedConnecting": "স্থানীয় সার্ভারের সাথে সংযোগ স্থাপন করা হচ্ছে...",
"embeddedNotReady": "স্থানীয় সার্ভার এখনও প্রস্তুত নয়। দয়া করে কিছুক্ষণ অপেক্ষা করুন এবং আবার চেষ্টা করুন।"
},
"versionCheck": {
"error": "সংস্করণ পরীক্ষা ত্রুটি",
@@ -471,6 +477,7 @@
"confirm": "নিশ্চিত করুন",
"yes": "হাঁ",
"no": "না",
"or": "অথবা",
"ok": "ঠিক আছে",
"enabled": "সক্ষম করা হয়েছে",
"disabled": "অক্ষম",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "ব্যবহারকারী {{username}} সফলভাবে মুছে ফেলা হয়েছে",
"failedToDeleteUser": "ব্যবহারকারী মুছে ফেলা যায়নি",
"overrideUserInfoUrl": "ব্যবহারকারীর তথ্য URL ওভাররাইড করুন (প্রয়োজনীয় নয়)",
"allowedUsers": "অনুমোদিত ব্যবহারকারীরা",
"allowedUsersDescription": "অনুমোদিত ইমেল প্যাটার্নের কমা দ্বারা পৃথক তালিকা। সঠিক ইমেল (user@example.com), ডোমেন প্রত্যয় (@example.com) ব্যবহার করুন, অথবা সমস্ত ব্যবহারকারীকে অনুমতি দিতে খালি রাখুন।",
"failedToFetchSessions": "সেশনগুলি আনা যায়নি",
"sessionRevokedSuccessfully": "সেশন সফলভাবে প্রত্যাহার করা হয়েছে",
"failedToRevokeSession": "সেশন প্রত্যাহার করতে ব্যর্থ হয়েছে",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "গুরুত্বপূর্ণ: পাসওয়ার্ড লগইন অক্ষম করা আছে। আপনি যদি OIDC রিসেট করেন বা ভুলভাবে কনফিগার করেন, তাহলে আপনি Termix-এর সমস্ত অ্যাক্সেস হারাবেন এবং আপনার ইনস্ট্যান্স ব্রিক হয়ে যাবে। আপনি যদি সম্পূর্ণরূপে নিশ্চিত হন তবেই এগিয়ে যান।",
"confirmDisableOIDCWarning": "সতর্কতা: আপনি OIDC নিষ্ক্রিয় করতে চলেছেন, যখন পাসওয়ার্ড লগইনও নিষ্ক্রিয় থাকবে। এটি আপনার Termix ইন্সট্যান্সকে ব্রিক করবে এবং আপনি সমস্ত অ্যাক্সেস হারাবেন। আপনি কি নিশ্চিত যে আপনি এগিয়ে যেতে চান?",
"failedToUpdatePasswordLoginStatus": "পাসওয়ার্ড লগইন স্ট্যাটাস আপডেট করা যায়নি",
"monitoringDefaults": "ডিফল্ট পর্যবেক্ষণ",
"monitoringDefaultsDesc": "স্থিতি পরীক্ষা এবং মেট্রিক্স সংগ্রহের জন্য বিশ্বব্যাপী ডিফল্ট ব্যবধান। প্রতি-হোস্ট সেটিংস এই মানগুলিকে ওভাররাইড করে।",
"globalStatusCheckInterval": "ডিফল্ট স্থিতি পরীক্ষা ব্যবধান",
"globalMetricsInterval": "ডিফল্ট মেট্রিক্স সংগ্রহের ব্যবধান",
"globalSettingsSaved": "বিশ্বব্যাপী পর্যবেক্ষণ সেটিংস সংরক্ষণ করা হয়েছে",
"failedToSaveGlobalSettings": "বিশ্বব্যাপী পর্যবেক্ষণ সেটিংস সংরক্ষণ করতে ব্যর্থ হয়েছে",
"failedToLoadGlobalSettings": "বিশ্বব্যাপী পর্যবেক্ষণ সেটিংস লোড করা যায়নি",
"loadingSessions": "সেশন লোড হচ্ছে...",
"noActiveSessions": "কোন সক্রিয় সেশন পাওয়া যায়নি।",
"device": "যন্ত্র",
@@ -841,6 +857,11 @@
"noHostsInJson": "JSON ফাইলে কোনও হোস্ট পাওয়া যায়নি",
"maxHostsAllowed": "প্রতি আমদানিতে সর্বোচ্চ ১০০টি হোস্ট অনুমোদিত",
"importCompleted": "আমদানি সম্পন্ন হয়েছে: {{success}} সফল, {{failed}} ব্যর্থ",
"importCreated": "তৈরি",
"importUpdated": "আপডেট করা হয়েছে",
"importFailedCount": "ব্যর্থ",
"importSkipExisting": "আমদানি করুন (বিদ্যমান এড়িয়ে যান)",
"importOverwriteExisting": "আমদানি করুন (বিদ্যমানগুলিকে ওভাররাইট করুন)",
"importFailed": "আমদানি ব্যর্থ হয়েছে",
"importError": "আমদানি ত্রুটি",
"failedToImportJson": "JSON ফাইল আমদানি করা যায়নি",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "সার্ভারটি অনলাইন নাকি অফলাইন তা পরীক্ষা করুন।",
"statusCheckInterval": "স্থিতি পরীক্ষা ব্যবধান",
"statusCheckIntervalDesc": "হোস্ট অনলাইন আছে কিনা তা কতবার পরীক্ষা করতে হবে (৫ সেকেন্ড - ১ ঘন্টা)",
"useGlobalStatusInterval": "গ্লোবাল ডিফল্ট ব্যবহার করুন",
"useGlobalMetricsInterval": "গ্লোবাল ডিফল্ট ব্যবহার করুন",
"usingGlobalDefault": "গ্লোবাল ডিফল্ট ব্যবহার করে ({{value}}s)",
"metricsEnabled": "মেট্রিক্স মনিটরিং সক্ষম করুন",
"metricsEnabledDesc": "CPU, RAM, ডিস্ক এবং অন্যান্য সিস্টেম পরিসংখ্যান সংগ্রহ করুন",
"metricsInterval": "মেট্রিক্স সংগ্রহের ব্যবধান",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "sudo পাসওয়ার্ডের জন্য অনুরোধ করলে স্বয়ংক্রিয়ভাবে SSH পাসওয়ার্ড সন্নিবেশ করার প্রস্তাব দেয়",
"sudoPassword": "সুডো পাসওয়ার্ড",
"sudoPasswordDesc": "sudo কমান্ডের জন্য ঐচ্ছিক পাসওয়ার্ড (কী প্রমাণীকরণের সাথে কার্যকর)",
"keepaliveSettings": "SSH Keepalive সম্পর্কে",
"keepaliveSettingsDesc": "SSH-স্তরের keepalive আচরণ কনফিগার করুন। SSH keepalives নিষ্ক্রিয় করতে ব্যবধান 0 তে সেট করুন (MikroTik এবং অন্যান্য ডিভাইসের জন্য দরকারী যা keepalive অনুরোধ উপেক্ষা করে)।",
"keepaliveInterval": "কিপলাইভ ব্যবধান (মিলিসেকেন্ড)",
"keepaliveIntervalDesc": "কত ঘন ঘন SSH keepalive পিং পাঠাতে হবে। ডিফল্ট: 30000 (30s)। অক্ষম করতে 0 তে সেট করুন।",
"keepaliveCountMax": "Keepalive সর্বোচ্চ ব্যর্থতা",
"keepaliveCountMaxDesc": "এই অনেক উত্তর না দেওয়া কিপলাইভের পরে সংযোগ বিচ্ছিন্ন করুন। ডিফল্ট: 3।",
"socks4": "মোজা ",
"socks5": "মোজা৫",
"httpConnect": "HTTP সংযোগ",
"testProxy": "সংযোগ পরীক্ষা করুন",
"testingProxy": "পরীক্ষা করা হচ্ছে...",
"proxyTestSuccess": "প্রক্সি সংযোগ সফল হয়েছে ({{latency}}মিলিসেকেন্ড)",
"proxyTestFailed": "প্রক্সি পরীক্ষা ব্যর্থ হয়েছে: {{error}}",
"connectionPath": "সংযোগ পথ",
"executeSnippetOnConnect": "টার্মিনাল সংযোগ করলে একটি স্নিপেট কার্যকর করুন",
"autoMosh": "অটো-মোশ",
"autoMoshDesc": "কানেক্টে স্বয়ংক্রিয়ভাবে MOSH কমান্ড চালান",
@@ -1284,7 +1320,35 @@
"showTimestamps": "টাইমস্ট্যাম্প দেখান",
"autoRefresh": "অটো রিফ্রেশ",
"filterLogsPlaceholder": "ফিল্টার লগ...",
"noLogsAvailable": "কোনও লগ উপলব্ধ নেই"
"noLogsAvailable": "কোনও লগ উপলব্ধ নেই",
"selectMode": "নির্বাচন করুন",
"exitSelectMode": "বাতিল করুন",
"selectedCount": "{{count}} নির্বাচিত",
"bulkMonitoring": "পর্যবেক্ষণ",
"bulkFeatures": "ফিচার",
"bulkMoveFolder": "ফোল্ডারে সরান",
"bulkPin": "পিন",
"bulkUnpin": "আনপিন করুন",
"enableAllFeatures": "সকল বৈশিষ্ট্য সক্ষম করুন",
"disableAllFeatures": "সমস্ত বৈশিষ্ট্য অক্ষম করুন",
"enableStatusCheck": "স্থিতি পরীক্ষা সক্ষম করুন",
"disableStatusCheck": "স্থিতি পরীক্ষা অক্ষম করুন",
"enableMetrics": "মেট্রিক্স সক্ষম করুন",
"disableMetrics": "মেট্রিক্স অক্ষম করুন",
"bulkEnableTerminal": "টার্মিনাল সক্ষম করুন",
"bulkDisableTerminal": "টার্মিনাল অক্ষম করুন",
"bulkEnableTunnel": "টানেল সক্ষম করুন",
"bulkDisableTunnel": "টানেল অক্ষম করুন",
"bulkEnableFileManager": "ফাইল ম্যানেজার সক্ষম করুন",
"bulkDisableFileManager": "ফাইল ম্যানেজার অক্ষম করুন",
"bulkEnableDocker": "ডকার সক্ষম করুন",
"bulkDisableDocker": "ডকার অক্ষম করুন",
"bulkUpdateSuccess": "{{count}} হোস্ট(গুলি) সফলভাবে আপডেট করা হয়েছে",
"bulkUpdateFailed": "বাল্ক আপডেট ব্যর্থ হয়েছে",
"selectAll": "সবগুলো নির্বাচন করুন",
"deselectAll": "সবগুলো অনির্বাচন করুন",
"useGlobalStatusDefault": "গ্লোবাল ডিফল্ট (স্থিতি) ব্যবহার করুন",
"useGlobalMetricsDefault": "গ্লোবাল ডিফল্ট (মেট্রিক্স) ব্যবহার করুন"
},
"terminal": {
"title": "টার্মিনাল",
@@ -1306,6 +1370,8 @@
"connectionClosed": "সংযোগ বন্ধ আছে",
"connectionError": "সংযোগ ত্রুটি: {{message}}",
"connected": "সংযুক্ত",
"clipboardWriteFailed": "ক্লিপবোর্ডে কপি করা যায়নি। নিশ্চিত করুন যে পৃষ্ঠাটি HTTPS অথবা localhost এর মাধ্যমে পরিবেশিত হচ্ছে।",
"clipboardReadFailed": "ক্লিপবোর্ড থেকে পড়া যায়নি। ক্লিপবোর্ডের অনুমতি মঞ্জুর করা হয়েছে কিনা তা নিশ্চিত করুন।",
"sshConnected": "SSH সংযোগ স্থাপন করা হয়েছে",
"authError": "প্রমাণীকরণ ব্যর্থ হয়েছে: {{message}}",
"unknownError": "অজানা ত্রুটি ঘটেছে",
@@ -1360,7 +1426,9 @@
"totpTimeout": "TOTP যাচাইকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।",
"passwordTimeout": "পাসওয়ার্ড যাচাইকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।",
"connectionRejected": "সার্ভার সংযোগ প্রত্যাখ্যান করেছে। অনুগ্রহ করে আপনার প্রমাণীকরণ এবং নেটওয়ার্ক কনফিগারেশন পরীক্ষা করুন।",
"hostKeyRejected": "SSH হোস্ট কী যাচাইকরণ প্রত্যাখ্যাত হয়েছে। সংযোগ বাতিল করা হয়েছে।"
"hostKeyRejected": "SSH হোস্ট কী যাচাইকরণ প্রত্যাখ্যাত হয়েছে। সংযোগ বাতিল করা হয়েছে।",
"sessionTakenOver": "অন্য ট্যাবে সেশন খোলা হয়েছে। পুনরায় সংযোগ করা হচ্ছে...",
"sessionAttachTimeout": "সেশন সংযুক্তির সময় শেষ। নতুন সংযোগ তৈরি করা হচ্ছে..."
},
"fileManager": {
"title": "ফাইল ম্যানেজার",
@@ -1670,7 +1738,13 @@
"write": "লিখুন",
"execute": "সম্পাদন করুন",
"permissionsChangedSuccessfully": "অনুমতিগুলি সফলভাবে পরিবর্তন করা হয়েছে",
"failedToChangePermissions": "অনুমতি পরিবর্তন করা যায়নি"
"failedToChangePermissions": "অনুমতি পরিবর্তন করা যায়নি",
"name": "নাম",
"sortByName": "নাম",
"sortByDate": "তারিখ পরিবর্তন করা হয়েছে",
"sortBySize": "আকার",
"ascending": "আরোহী",
"descending": "অবরোহী"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "লগইন",
"registerButton": "নিবন্ধন",
"forgotPassword": "পাসওয়ার্ড ভুলে গেছেন?",
"rememberMe": "আমাকে মনে রেখো",
"rememberMe": "ডিভাইসটি ৩০ দিনের জন্য মনে রাখবেন (TOTP সহ)",
"noAccount": "কোন অ্যাকাউন্ট নেই?",
"hasAccount": "ইতিমধ্যে একটি অ্যাকাউন্ট আছে?",
"loginSuccess": "লগইন সফল হয়েছে",
@@ -2023,6 +2097,7 @@
"processing": "প্রক্রিয়াজাতকরণ...",
"pleaseWait": "অনুগ্রহ করে অপেক্ষা করুন...",
"registrationDisabled": "একজন প্রশাসক বর্তমানে নতুন অ্যাকাউন্ট নিবন্ধন বন্ধ করে রেখেছেন। অনুগ্রহ করে লগ ইন করুন অথবা একজন প্রশাসকের সাথে যোগাযোগ করুন।",
"userNotAllowed": "আপনার অ্যাকাউন্টটি নিবন্ধনের জন্য অনুমোদিত নয়। অনুগ্রহ করে একজন প্রশাসকের সাথে যোগাযোগ করুন।",
"databaseConnected": "ডাটাবেস সফলভাবে সংযুক্ত হয়েছে",
"databaseConnectionFailed": "ডাটাবেস সার্ভারের সাথে সংযোগ করতে ব্যর্থ হয়েছে।",
"checkServerConnection": "আপনার সার্ভার সংযোগ পরীক্ষা করে আবার চেষ্টা করুন।",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "অ্যাপ্লিকেশনের জন্য রঙের থিম নির্বাচন করুন",
"terminalSyntaxHighlightingDesc": "টার্মিনাল আউটপুটে স্বয়ংক্রিয়ভাবে কমান্ড, পাথ, আইপি এবং লগ লেভেল হাইলাইট করুন",
"enableCommandPaletteShortcut": "কমান্ড প্যালেট শর্টকাট সক্ষম করুন",
"enableCommandPaletteShortcutDesc": "হোস্টগুলিতে দ্রুত অ্যাক্সেসের জন্য কমান্ড প্যালেট খুলতে বাম দিকে Shift-এ দুবার ট্যাপ করুন।"
"enableCommandPaletteShortcutDesc": "হোস্টগুলিতে দ্রুত অ্যাক্সেসের জন্য কমান্ড প্যালেট খুলতে বাম দিকে Shift-এ দুবার ট্যাপ করুন।",
"enableTerminalSessionPersistence": "স্থায়ী টার্মিনাল সেশন",
"enableTerminalSessionPersistenceDesc": "ট্যাব স্যুইচ করার সময় বা ব্রাউজার বন্ধ করার সময় SSH সংযোগ বজায় রাখুন (অস্থির হতে পারে)"
},
"user": {
"failedToLoadVersionInfo": "সংস্করণের তথ্য লোড করা যায়নি"
@@ -2114,6 +2191,7 @@
"usernameField": "নাম",
"scopes": "ওপেনআইডি ইমেল প্রোফাইল",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"allowedUsers": "user@example.com, @company.com",
"enterUsername": "অ্যাডমিন করতে ব্যবহারকারীর নাম লিখুন",
"searchHosts": "নাম, ব্যবহারকারীর নাম, আইপি, ফোল্ডার, ট্যাগ দ্বারা হোস্ট অনুসন্ধান করুন...",
"enterPassword": "আপনার পাসওয়ার্ড লিখুন",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "Bidireccional",
"threeSplit": "3 vies",
"fourSplit": "4 vies",
"fiveSplit": "5 vies",
"sixSplit": "6 vies",
"availableTabs": "Pestanyes disponibles",
"dragTabsHint": "Arrossegueu pestanyes a les cel·les de disseny següents per assignar-les",
"layout": "Disseny de pantalla dividida",
@@ -372,7 +374,11 @@
"warning": "Avís",
"notValidatedWarning": "URL no validada: assegureu-vos que sigui correcta",
"changeServer": "Canviar servidor",
"mustIncludeProtocol": "L'URL del servidor ha de començar per http:// o https://"
"mustIncludeProtocol": "L'URL del servidor ha de començar per http:// o https://",
"useEmbedded": "Utilitza el servidor local",
"embeddedDesc": "Executeu Termix amb el servidor local integrat (no cal cap servidor remot)",
"embeddedConnecting": "Connectant al servidor local...",
"embeddedNotReady": "El servidor local encara no està a punt. Espereu un moment i torneu-ho a intentar."
},
"versionCheck": {
"error": "Error de comprovació de versió",
@@ -471,6 +477,7 @@
"confirm": "Confirma",
"yes": "Sí",
"no": "No",
"or": "O",
"ok": "D'acord",
"enabled": "Activat",
"disabled": "Discapacitat",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "L'usuari {{username}} s'ha eliminat correctament",
"failedToDeleteUser": "No s'ha pogut suprimir l'usuari",
"overrideUserInfoUrl": "Substitueix l'URL d'informació de l'usuari (no obligatori)",
"allowedUsers": "Usuaris permesos",
"allowedUsersDescription": "Llista de patrons de correu electrònic permesos separats per comes. Utilitzeu correus electrònics exactes (usuari@exemple.com), sufixos de domini (@exemple.com) o deixeu-ho en blanc per permetre tots els usuaris.",
"failedToFetchSessions": "No s'han pogut recuperar les sessions",
"sessionRevokedSuccessfully": "La sessió s'ha revocat correctament",
"failedToRevokeSession": "No s'ha pogut revocar la sessió",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "CRÍTIC: L'inici de sessió amb contrasenya està desactivat. Si restableixes o configures incorrectament l'OIDC, perdràs tot l'accés a Termix i bloquejaràs la teva instància. Només continua si n'estàs absolutament segur.",
"confirmDisableOIDCWarning": "AVÍS: Esteu a punt de desactivar OIDC mentre l'inici de sessió amb contrasenya també està desactivat. Això bloquejarà la vostra instància de Termix i perdreu tot l'accés. Esteu completament segur que voleu continuar?",
"failedToUpdatePasswordLoginStatus": "No s'ha pogut actualitzar l'estat d'inici de sessió de la contrasenya",
"monitoringDefaults": "Monitorització dels valors predeterminats",
"monitoringDefaultsDesc": "Intervals globals predeterminats per a les comprovacions d'estat i la recopilació de mètriques. La configuració per host substitueix aquests valors.",
"globalStatusCheckInterval": "Interval de comprovació d'estat per defecte",
"globalMetricsInterval": "Interval de recopilació de mètriques per defecte",
"globalSettingsSaved": "S'ha desat la configuració de monitorització global",
"failedToSaveGlobalSettings": "No s'ha pogut desar la configuració de monitorització global",
"failedToLoadGlobalSettings": "No s'ha pogut carregar la configuració de monitorització global",
"loadingSessions": "S'estan carregant les sessions...",
"noActiveSessions": "No s'han trobat sessions actives.",
"device": "Dispositiu",
@@ -841,6 +857,11 @@
"noHostsInJson": "No s'han trobat hosts al fitxer JSON",
"maxHostsAllowed": "Màxim de 100 amfitrions permesos per importació",
"importCompleted": "Importació completada: {{success}} correcta, {{failed}} fallida",
"importCreated": "creat",
"importUpdated": "actualitzat",
"importFailedCount": "fallat",
"importSkipExisting": "Importa (omet els existents)",
"importOverwriteExisting": "Importa (sobreescriu l'existent)",
"importFailed": "La importació ha fallat",
"importError": "Error d'importació",
"failedToImportJson": "No s'ha pogut importar el fitxer JSON",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "Comprova si el servidor està en línia o fora de línia",
"statusCheckInterval": "Interval de comprovació d'estat",
"statusCheckIntervalDesc": "Amb quina freqüència cal comprovar si l'amfitrió està en línia (5 segons - 1 hora)",
"useGlobalStatusInterval": "Utilitza el valor predeterminat global",
"useGlobalMetricsInterval": "Utilitza el valor predeterminat global",
"usingGlobalDefault": "Utilitzant el valor per defecte global ({{value}}s)",
"metricsEnabled": "Activa la supervisió de mètriques",
"metricsEnabledDesc": "Recopila estadístiques de CPU, RAM, disc i altres estadístiques del sistema",
"metricsInterval": "Interval de recopilació de mètriques",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "Ofereix automàticament la inserció d'una contrasenya SSH quan sudo demani la contrasenya",
"sudoPassword": "Contrasenya de Sudo",
"sudoPasswordDesc": "Contrasenya opcional per a les ordres sudo (útil amb l'autenticació de clau)",
"keepaliveSettings": "SSH Keeping Alive",
"keepaliveSettingsDesc": "Configura el comportament de keepalive a nivell SSH. Estableix l'interval a 0 per desactivar els keepalive SSH (útil per a MikroTik i altres dispositius que ignoren les sol·licituds de keepalive).",
"keepaliveInterval": "Interval de manteniment (ms)",
"keepaliveIntervalDesc": "Freqüència d'enviament de pings de keepalive SSH. Per defecte: 30000 (30 s). Establiu-ho a 0 per desactivar-ho.",
"keepaliveCountMax": "Errors màxims de KeepAlive",
"keepaliveCountMaxDesc": "Desconnecta després d'aquest nombre de keepalives sense resposta. Per defecte: 3.",
"socks4": "MITJONS4",
"socks5": "MITJONS5",
"httpConnect": "CONNEXIÓ HTTP",
"testProxy": "Prova de connexió",
"testingProxy": "Proves...",
"proxyTestSuccess": "Connexió de proxy correcta ({{latency}}ms)",
"proxyTestFailed": "La prova del servidor intermediari ha fallat: {{error}}",
"connectionPath": "Ruta de connexió",
"executeSnippetOnConnect": "Executa un fragment quan el terminal es connecti",
"autoMosh": "Auto-MOSH",
"autoMoshDesc": "Executa automàticament l'ordre MOSH en connectar-se",
@@ -1284,7 +1320,35 @@
"showTimestamps": "Mostra les marques de temps",
"autoRefresh": "Actualització automàtica",
"filterLogsPlaceholder": "Filtra els registres...",
"noLogsAvailable": "No hi ha registres disponibles"
"noLogsAvailable": "No hi ha registres disponibles",
"selectMode": "Selecciona",
"exitSelectMode": "Cancel·la",
"selectedCount": "{{count}} seleccionat",
"bulkMonitoring": "Monitorització",
"bulkFeatures": "Característiques",
"bulkMoveFolder": "Mou a la carpeta",
"bulkPin": "Fixar",
"bulkUnpin": "Desfixar",
"enableAllFeatures": "Activa totes les funcions",
"disableAllFeatures": "Desactiva totes les funcions",
"enableStatusCheck": "Activa la comprovació d'estat",
"disableStatusCheck": "Desactiva la comprovació d'estat",
"enableMetrics": "Activa les mètriques",
"disableMetrics": "Desactiva les mètriques",
"bulkEnableTerminal": "Habilita el terminal",
"bulkDisableTerminal": "Desactiva el terminal",
"bulkEnableTunnel": "Habilita el túnel",
"bulkDisableTunnel": "Desactiva el túnel",
"bulkEnableFileManager": "Habilita el gestor de fitxers",
"bulkDisableFileManager": "Desactiva el gestor de fitxers",
"bulkEnableDocker": "Activa Docker",
"bulkDisableDocker": "Desactiva Docker",
"bulkUpdateSuccess": "S'han actualitzat correctament els hosts {{count}}",
"bulkUpdateFailed": "L'actualització massiva ha fallat",
"selectAll": "Selecciona-ho tot",
"deselectAll": "Desselecciona-ho tot",
"useGlobalStatusDefault": "Utilitza el valor per defecte global (estat)",
"useGlobalMetricsDefault": "Utilitza el valor predeterminat global (mètriques)"
},
"terminal": {
"title": "Terminal",
@@ -1306,6 +1370,8 @@
"connectionClosed": "Connexió tancada",
"connectionError": "Error de connexió: {{message}}",
"connected": "Connectat",
"clipboardWriteFailed": "No s'ha pogut copiar al porta-retalls. Assegureu-vos que la pàgina es serveix per HTTPS o localhost.",
"clipboardReadFailed": "No s'ha pogut llegir del porta-retalls. Assegureu-vos que els permisos del porta-retalls estiguin concedits.",
"sshConnected": "Connexió SSH establerta",
"authError": "L'autenticació ha fallat: {{message}}",
"unknownError": "S'ha produït un error desconegut",
@@ -1360,7 +1426,9 @@
"totpTimeout": "Temps d'espera de verificació TOTP superat. Si us plau, torneu a connectar-vos.",
"passwordTimeout": "Temps d'espera de verificació de la contrasenya. Si us plau, torneu a connectar-vos.",
"connectionRejected": "Connexió rebutjada pel servidor. Si us plau, comproveu l'autenticació i la configuració de xarxa.",
"hostKeyRejected": "Verificació de la clau de l'amfitrió SSH rebutjada. Connexió cancel·lada."
"hostKeyRejected": "Verificació de la clau de l'amfitrió SSH rebutjada. Connexió cancel·lada.",
"sessionTakenOver": "La sessió s'ha obert en una altra pestanya. S'està tornant a connectar...",
"sessionAttachTimeout": "S'ha esgotat el temps d'espera per al fitxer adjunt de la sessió. S'està creant una connexió nova..."
},
"fileManager": {
"title": "Gestor de fitxers",
@@ -1670,7 +1738,13 @@
"write": "Escriure",
"execute": "Executar",
"permissionsChangedSuccessfully": "Els permisos s'han canviat correctament",
"failedToChangePermissions": "No s'han pogut canviar els permisos"
"failedToChangePermissions": "No s'han pogut canviar els permisos",
"name": "Nom",
"sortByName": "Nom",
"sortByDate": "Data de modificació",
"sortBySize": "Mida",
"ascending": "Ascendent",
"descending": "Descendent"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "Inicia la sessió",
"registerButton": "Registra't",
"forgotPassword": "Has oblidat la contrasenya?",
"rememberMe": "Recorda'm",
"rememberMe": "Recorda el dispositiu durant 30 dies (inclou TOTP)",
"noAccount": "No tens un compte?",
"hasAccount": "Ja tens un compte?",
"loginSuccess": "Inici de sessió correcte",
@@ -2023,6 +2097,7 @@
"processing": "Processant...",
"pleaseWait": "Si us plau, espereu...",
"registrationDisabled": "El registre de comptes nous està desactivat actualment per un administrador. Si us plau, inicia la sessió o contacta amb un administrador.",
"userNotAllowed": "El vostre compte no està autoritzat per registrar-se. Poseu-vos en contacte amb un administrador.",
"databaseConnected": "La base de dades s'ha connectat correctament",
"databaseConnectionFailed": "No s'ha pogut connectar al servidor de la base de dades",
"checkServerConnection": "Si us plau, comproveu la connexió del servidor i torneu-ho a intentar.",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "Seleccioneu el tema de color per a l'aplicació",
"terminalSyntaxHighlightingDesc": "Ressaltar automàticament les ordres, les rutes, les IP i els nivells de registre a la sortida del terminal",
"enableCommandPaletteShortcut": "Activa la drecera de la paleta d'ordres",
"enableCommandPaletteShortcutDesc": "Toqueu dues vegades la tecla Majúscules esquerra per obrir la paleta d'ordres i accedir ràpidament als amfitrions."
"enableCommandPaletteShortcutDesc": "Toqueu dues vegades la tecla Majúscules esquerra per obrir la paleta d'ordres i accedir ràpidament als amfitrions.",
"enableTerminalSessionPersistence": "Sessions terminals persistents",
"enableTerminalSessionPersistenceDesc": "Mantingueu les connexions SSH en canviar de pestanya o tancar el navegador (pot ser inestable)"
},
"user": {
"failedToLoadVersionInfo": "No s'ha pogut carregar la informació de la versió"
@@ -2114,6 +2191,7 @@
"usernameField": "nom",
"scopes": "perfil de correu electrònic d'OpenID",
"userinfoUrl": "https://el-teu-proveïdor.com/aplicació/o/info-usuari/",
"allowedUsers": "usuari@exemple.com, @empresa.com",
"enterUsername": "Introdueix el nom d'usuari per fer-lo administrador",
"searchHosts": "Cerca hosts per nom, nom d'usuari, IP, carpeta, etiquetes...",
"enterPassword": "Introdueix la teva contrasenya",
+218 -140
View File
@@ -1,20 +1,20 @@
{
"credentials": {
"credentials": "Pověření",
"credentials": "Přihlašovací údaje",
"credentialsViewer": "Prohlížeč přihlašovacích údajů",
"manageYourSSHCredentials": "Bezpečná správa přihlašovacích údajů SSH",
"addCredential": "Přidat pověření",
"createCredential": "Vytvořit pověření",
"editCredential": "Upravit pověření",
"viewCredential": "Zobrazit pověření",
"duplicateCredential": "Duplikovat pověření",
"deleteCredential": "Odstranit pověření",
"updateCredential": "Aktualizovat pověření",
"credentialName": "Název pověření",
"credentialDescription": "L 343, 22.12.2009, s. 1).",
"manageYourSSHCredentials": "Spravujte své přihlašovací údaje SSH bezpečně",
"addCredential": "Přidat přihlašovací údaje",
"createCredential": "Vytvořit přihlašovací údaje",
"editCredential": "Upravit přihlašovací údaje",
"viewCredential": "Zobrazit přihlašovací údaje",
"duplicateCredential": "Duplikovat přihlašovací údaje",
"deleteCredential": "Odstranit přihlašovací údaje",
"updateCredential": "Aktualizovat přihlašovací údaje",
"credentialName": "Název přihlašovacích údajů",
"credentialDescription": "Popis",
"username": "Uživatelské jméno",
"searchCredentials": "Hledat přihlašovací údaje...",
"selectFolder": "Vybrat složku",
"searchCredentials": "Hledat přihlašovací údaje",
"selectFolder": "Vyberte složku",
"selectAuthType": "Vyberte typ ověření",
"allFolders": "Všechny složky",
"allAuthTypes": "Všechny typy ověření",
@@ -23,38 +23,38 @@
"keyBased": "Na základě klíče",
"passwordBased": "Na základě hesla",
"folders": "Složky",
"noCredentialsMatchFilters": "Žádné přihlašovací údaje neodpovídají vašim filtrům",
"noCredentialsYet": "Dosud nebyla vytvořena žádná pověření",
"createFirstCredential": "Vytvořte své první pověření",
"noCredentialsMatchFilters": "Vašim filtrům neodpovídají žádné přihlašovací údaje",
"noCredentialsYet": "Zatím nebyly vytvořeny žádné přihlašovací údaje",
"createFirstCredential": "Vytvořte své první přihlašovací údaje",
"failedToFetchCredentials": "Nepodařilo se načíst přihlašovací údaje",
"credentialDeletedSuccessfully": "Pověření bylo úspěšně odstraněno",
"failedToDeleteCredential": "Nepodařilo se odstranit pověření",
"confirmDeleteCredential": "Jste si jisti, že chcete odstranit pověření \"{{name}}\"?",
"credentialCreatedSuccessfully": "Pověření bylo úspěšně vytvořeno",
"credentialUpdatedSuccessfully": "Útvar byl úspěšně aktualizován",
"failedToSaveCredential": "Nepodařilo se uložit pověření",
"failedToFetchCredentialDetails": "Nepodařilo se načíst detaily pověření",
"failedToFetchHostsUsing": "Nepodařilo se načíst hostitele pomocí tohoto pověření",
"loadingCredentials": "Načítání pověření...",
"credentialDeletedSuccessfully": "Přihlašovací údaje byly úspěšně odstraněny",
"failedToDeleteCredential": "Nepodařilo se odstranit přihlašovací údaje",
"confirmDeleteCredential": "Opravdu chcete odstranit přihlašovací údaje „{{name}}?",
"credentialCreatedSuccessfully": "Přihlašovací údaje byly úspěšně vytvořeny",
"credentialUpdatedSuccessfully": "Přihlašovací údaje byly úspěšně aktualizovány",
"failedToSaveCredential": "Nepodařilo se uložit přihlašovací údaje",
"failedToFetchCredentialDetails": "Nepodařilo se načíst podrobnosti o přihlašovacích údajích",
"failedToFetchHostsUsing": "Nepodařilo se načíst hostitele pomocí těchto přihlašovacích údajů",
"loadingCredentials": "Načítání přihlašovacích údajů…",
"retry": "Opakovat",
"noCredentials": "Žádné přihlašovací údaje",
"noCredentialsMessage": "Zatím jste nepřidali žádné přihlašovací údaje. Chcete-li začít, klikněte na \"Přidat přihlašovací údaje\".",
"sshCredentials": "SSH pověření",
"credentialsCount": "{{count}} pověření",
"refresh": "Aktualizovat",
"passwordRequired": "Heslo je vyžadováno",
"sshKeyRequired": "SSH klíč je povinný",
"credentialAddedSuccessfully": "Pověření \"{{name}}\" bylo úspěšně přidáno",
"savingCredential": "Ukládání přihlašovacích údajů...",
"updatingCredential": "Aktualizace pověření...",
"general": "Obecná ustanovení",
"description": "L 343, 22.12.2009, s. 1).",
"noCredentialsMessage": "Zatím jste nepřidali žádné přihlašovací údaje. Začněte kliknutím na Přidat přihlašovací údaje.",
"sshCredentials": "Přihlašovací údaje SSH",
"credentialsCount": "{{count}} přihlašovacích údajů",
"refresh": "Obnovit",
"passwordRequired": "Je vyžadováno heslo",
"sshKeyRequired": "Je vyžadován klíč SSH",
"credentialAddedSuccessfully": "Přihlašovací údaje „{{name}} byly úspěšně přidány",
"savingCredential": "Ukládání přihlašovacích údajů",
"updatingCredential": "Aktualizace přihlašovacích údajů…",
"general": "Obecné",
"description": "Popis",
"folder": "Složka",
"tags": "Štítky",
"addTagsSpaceToAdd": "Přidat štítky (stiskněte mezeru, chcete-li přidat)",
"addTagsSpaceToAdd": "Přidat štítky (pro přidání stiskněte mezerník)",
"password": "Heslo",
"key": "Klíč",
"sshPrivateKey": "SSH soukromý klíč",
"sshPrivateKey": "Soukromý klíč SSH",
"upload": "Nahrát",
"updateKey": "Aktualizovat klíč",
"keyPassword": "Heslo klíče",
@@ -66,104 +66,104 @@
"authentication": "Ověření",
"organization": "Organizace",
"basicInformation": "Základní informace",
"basicInformationDescription": "Zadejte základní informace pro toto pověření",
"basicInformationDescription": "Zadejte základní informace pro tyto přihlašovací údaje",
"authenticationMethod": "Metoda ověřování",
"authenticationMethodDescription": "Vyberte si, jak chcete ověřit pomocí SSH serverů",
"organizationDescription": "Uspořádat vaše přihlašovací údaje se složkami a štítky",
"enterCredentialName": "Zadejte název pověření",
"authenticationMethodDescription": "Zvolte, jak se chcete ověřovat u serverů SSH",
"organizationDescription": "Uspořádejte si přihlašovací údaje pomocí složek a štítků",
"enterCredentialName": "Zadejte název pro přihlašovací údaje",
"enterCredentialDescription": "Zadejte popis (volitelné)",
"enterUsername": "Zadejte uživatelské jméno",
"nameIsRequired": "Je vyžadován název pověření",
"usernameIsRequired": "Uživatelské jméno je povinné",
"nameIsRequired": "Je vyžadován název přihlašovach údajů",
"usernameIsRequired": "Je vyžadováno uživatelské jméno",
"authenticationType": "Typ ověření",
"passwordAuthDescription": "Použít ověření hesla",
"sshKeyAuthDescription": "Použít ověření klíče SSH",
"passwordIsRequired": "Heslo je vyžadováno",
"sshKeyIsRequired": "SSH klíč je povinný",
"passwordAuthDescription": "Použít ověřování heslem",
"sshKeyAuthDescription": "Použít ověřování klíčem SSH",
"passwordIsRequired": "Je vyžadováno heslo",
"sshKeyIsRequired": "Je vyžadován klíč SSH",
"sshKeyType": "Typ klíče SSH",
"privateKey": "Soukromý klíč",
"enterPassword": "Zadejte heslo",
"enterPrivateKey": "Zadejte soukromý klíč",
"keyPassphrase": "Heslo klíče",
"enterKeyPassphrase": "Zadejte heslo klíče (volitelné)",
"keyPassphraseOptional": "Volitelné: ponechte prázdné, pokud váš klíč nemá žádné přístupové heslo",
"leaveEmptyToKeepCurrent": "Ponechte prázdné pro zachování aktuální hodnoty",
"uploadKeyFile": "Nahrát klíčový soubor",
"generateKeyPairButton": "Generovat párování klíče",
"generateKeyPair": "Generovat párování klíče",
"generateKeyPairDescription": "Vygenerujte novou dvojici klíčů SSH. Pokud chcete klíč chránit pomocí přístupové fráze, zadejte jej nejprve do pole Klíčové heslo.",
"deploySSHKey": "Nasadit SSH klíč",
"keyPassphraseOptional": "Volitelné: pokud váš klíč nemá heslo, ponechte prázdné",
"leaveEmptyToKeepCurrent": "Pro zachování aktuální hodnoty ponechte prázdné",
"uploadKeyFile": "Nahrát soubor klíče",
"generateKeyPairButton": "Generovat pár klíčů",
"generateKeyPair": "Generovat pár klíčů",
"generateKeyPairDescription": "Vygenerujte nový pár klíčů SSH. Pokud chcete klíč chránit heslem, zadejte ho nejprve do pole Heslo klíče níže.",
"deploySSHKey": "Nasadit klíč SSH",
"deploySSHKeyDescription": "Nasadit veřejný klíč na cílový server",
"sourceCredential": "Zdrojové autority",
"targetHost": "Hostitel cíle",
"sourceCredential": "Zdrojové přihlašovací údaje",
"targetHost": "Cílový hostitel",
"deploymentProcess": "Proces nasazení",
"deploymentProcessDescription": "Toto bezpečně přidá veřejný klíč do souboru ~/.ssh/authorized_keys bez přepsání existujících klíčů. Operace je vratná.",
"chooseHostToDeploy": "Vyberte hostitele pro nasazení...",
"deploying": "Nasazování...",
"deploymentProcessDescription": "Toto bezpečně přidá veřejný klíč do souboru ~/.ssh/authorized_keys cílového hostitele bez přepsání existujících klíčů. Operace je vratná.",
"chooseHostToDeploy": "Zvolte hostitele pro nasazení",
"deploying": "Nasazování",
"copyDeployCommand": "Kopírovat příkaz pro nasazení",
"copyDeployCommandDescription": "Zkopírujte příkaz shell do manuálního přidání veřejného klíče do souboru autorized_keys. Užitečné, pokud je heslo vypnuto.",
"copyDeployCommandDescription": "Zkopírujte příkaz shellu pro ruční přidání veřejného klíče do souboru authorized_keys hostitele. Užitečné, když je zakázáno ověřování heslem.",
"deployCommandCopied": "Příkaz nasazení zkopírován do schránky",
"manualDeployInfo": "Vložte tento příkaz do terminálu cílového hostitele (např. Proxmox konzola, IPMI nebo fyzický přístup) pro přidání SSH klíče.",
"manualDeployInfo": "Vložte tento příkaz do terminálu cílového hostitele (např. konzole Proxmox, IPMI nebo fyzického přístupu) pro přidání klíče SSH.",
"name": "Název",
"noHostsAvailable": "Nejsou k dispozici žádní hostitelé",
"noHostsMatchSearch": "Žádné hostitele neodpovídají vašemu hledání",
"sshKeyGenerationNotImplemented": "Vytváření SSH klíče přichází brzy",
"connectionTestingNotImplemented": "Testovací funkce připojení přichází brzy",
"noHostsAvailable": "Nejsou dostupní žádní hostitelé",
"noHostsMatchSearch": "Vašemu hledání neodpovídají žádní hostitelé",
"sshKeyGenerationNotImplemented": "Funkce generování klíčů SSH bude brzy dostupná",
"connectionTestingNotImplemented": "Funkce testování připojení bude brzy dostupná",
"testConnection": "Testovat připojení",
"selectOrCreateFolder": "Vyberte nebo vytvořte složku",
"noFolder": "Žádná složka",
"orCreateNewFolder": "Nebo vytvořit novou složku",
"orCreateNewFolder": "Nebo vytvořte novou složku",
"addTag": "Přidat štítek",
"saving": "Ukládání...",
"credentialId": "ID pověření",
"saving": "Ukládání",
"credentialId": "ID přihlašovacích údajů",
"overview": "Přehled",
"security": "Zabezpečení",
"usage": "Využití",
"securityDetails": "Detaily zabezpečení",
"securityDetailsDescription": "Zobrazit šifrované přihlašovací informace",
"credentialSecured": "Zabezpečení důvěryhodnosti",
"securityDetails": "Podrobnosti zabezpečení",
"securityDetailsDescription": "Zobrazit zašifrované informace o přihlašovacích údajích",
"credentialSecured": "Zabezpečené přihlašovací údaje",
"credentialSecuredDescription": "Všechna citlivá data jsou šifrována pomocí AES-256",
"passwordAuthentication": "Ověření hesla",
"keyAuthentication": "Ověření klíče",
"passwordAuthentication": "Ověřování heslem",
"keyAuthentication": "Ověřování klíčem",
"securityReminder": "Upozornění zabezpečení",
"securityReminderText": "Nikdy nesdílejte Vaše přihlašovací údaje. Všechna data jsou v odpočinku šifrována.",
"hostsUsingCredential": "Hostitelé používající tento klíč",
"noHostsUsingCredential": "Žádné hostitele v současné době používají toto pověření",
"securityReminderText": "Nikdy nesdílejte své přihlašovací údaje. Všechna data jsou v klidovém stavu šifrována.",
"hostsUsingCredential": "Hostitelé používající tyto přihlašovací údaje",
"noHostsUsingCredential": "Tyto přihlašovací údaje nepoužívají aktuálně žádní hostitelé",
"timesUsed": "Četnost použití",
"lastUsed": "Naposledy použito",
"connectedHosts": "Připojení hostitelé",
"created": "Vytvořeno",
"lastModified": "Poslední změna",
"lastModified": "Naposledy upraveno",
"usageStatistics": "Statistiky využití",
"copiedToClipboard": "{{field}} zkopírován do schránky",
"copiedToClipboard": "{{field}} zkopírováno do schránky",
"failedToCopy": "Kopírování do schránky se nezdařilo",
"sshKey": "SSH klíč",
"createCredentialDescription": "Vytvořit nové SSH pověření pro bezpečný přístup",
"editCredentialDescription": "Aktualizovat důvěryhodné informace",
"sshKey": "Klíč SSH",
"createCredentialDescription": "Vytvořit nové přihlašovací údaje SSH pro bezpečný přístup",
"editCredentialDescription": "Aktualizovat informace o přihlašovacích údajích",
"listView": "Seznam",
"folderView": "Složky",
"unknownCredential": "Neznámý",
"confirmRemoveFromFolder": "Opravdu chcete odstranit \"{{name}}\" ze složky \"{{folder}}\"? pověření bude přesunuto do \"Uncategorized\".",
"removedFromFolder": "Pověření \"{{name}}\" bylo úspěšně odstraněno ze složky",
"failedToRemoveFromFolder": "Nepodařilo se odstranit pověření ze složky",
"folderRenamed": "Složka \"{{oldName}}\" byla úspěšně přejmenována na \"{{newName}}\"",
"failedToRenameFolder": "Složku se nepodařilo přejmenovat",
"movedToFolder": "Pověření \"{{name}}\" bylo úspěšně přesunuto do \"{{folder}}\"",
"failedToMoveToFolder": "Nepodařilo se přesunout pověření do složky",
"confirmRemoveFromFolder": "Opravdu chcete odebrat „{{name}} ze složky {{folder}}“? Přihlašovací údaje budou přesunuty do složky „Nezařazené“.",
"removedFromFolder": "Přihlašovací údaje „{{name}} byly úspěšně odebrány ze složky",
"failedToRemoveFromFolder": "Nepodařilo se odebrat přihlašovací údaje ze složky",
"folderRenamed": "Složka {{oldName}} byla úspěšně přejmenována na {{newName}}",
"failedToRenameFolder": "Nepodařilo se přejmenovat složku",
"movedToFolder": "Pověření {{name}} bylo úspěšně přesunuto do {{folder}}",
"failedToMoveToFolder": "Nepodařilo se přesunout přihlašovací údaje do složky",
"sshPublicKey": "Veřejný klíč SSH",
"publicKeyNote": "Veřejný klíč je volitelný, ale doporučuje se pro ověření klíče",
"publicKeyUploaded": "Veřejný klíč byl nahrán",
"uploadPublicKey": "Nahrát veřejný klíč",
"uploadPrivateKeyFile": "Nahrát soubor soukromého klíče",
"uploadPublicKeyFile": "Nahrát soubor s veřejným klíčem",
"uploadPublicKeyFile": "Nahrát soubor veřejného klíče",
"privateKeyRequiredForGeneration": "Pro generování veřejného klíče je vyžadován soukromý klíč",
"failedToGeneratePublicKey": "Nepodařilo se vygenerovat veřejný klíč",
"generatePublicKey": "Generovat ze soukromého klíče",
"publicKeyGeneratedSuccessfully": "Veřejný klíč byl úspěšně vygenerován",
"detectedKeyType": "Zjištěný typ klíče",
"detectingKeyType": "zjišťuji...",
"optional": "Nepovinné",
"generateKeyPairNew": "Generovat nové páry",
"detectingKeyType": "zjišťování…",
"optional": "Volitelné",
"generateKeyPairNew": "Generovat nový pár klíčů",
"generateEd25519": "Generovat Ed25519",
"generateECDSA": "Generovat ECDSA",
"generateRSA": "Generovat RSA",
@@ -173,20 +173,20 @@
"keyTypeDsa": "DSA (SSH)",
"keyTypeRsaSha256": "RSA-SHA2-256",
"keyTypeRsaSha512": "RSA-SHA2-512",
"keyPairGeneratedSuccessfully": "{{keyType}} klíče byly úspěšně generovány",
"keyPairGeneratedSuccessfully": "Pár klíčů {{keyType}} byl úspěšně vygenerován",
"failedToGenerateKeyPair": "Nepodařilo se vygenerovat pár klíčů",
"generateKeyPairNote": "Generovat nový pár SSH klíčů přímo. Tím se nahradí všechny existující klíče ve formuláři.",
"generateKeyPairNote": "Generovat nový pár klíčů SSH přímo. Tím se nahradí všechny existující klíče ve formuláři.",
"invalidKey": "Neplatný klíč",
"detectionError": "Chyba detekce",
"removing": "Odstranění:",
"clickToEditCredential": "Kliknutím upravíte pověření",
"detectionError": "Chyba zjišťování",
"removing": "Odebírání:",
"clickToEditCredential": "Kliknutím upravíte přihlašovací údaje",
"dragToMoveBetweenFolders": "Přetažením přesunout mezi složkami",
"keyBasedOnlyForDeployment": "Lze použít pouze přístupové údaje založené na klíči SSH",
"publicKeyRequiredForDeployment": "Veřejný klíč je potřebpro nasazení",
"keyBasedOnlyForDeployment": "Lze nasadit pouze přihlašovací údaje založené na klíčích SSH",
"publicKeyRequiredForDeployment": "Pro nasazení je vyžadován veřejklíč",
"selectTargetHost": "Vyberte prosím cílového hostitele",
"keyDeployedSuccessfully": "SSH klíč byl úspěšně nasazen",
"deploymentFailed": "Nasazení selhalo",
"failedToDeployKey": "Nepodařilo se použít SSH klíč",
"keyDeployedSuccessfully": "Klíč SSH byl úspěšně nasazen",
"deploymentFailed": "Nasazení se nezdařilo",
"failedToDeployKey": "Nepodařilo se nasadit klíč SSH",
"clickToRenameFolder": "Klikněte pro přejmenování složky",
"renameFolder": "Přejmenovat složku",
"idLabel": "ID:"
@@ -195,16 +195,16 @@
"title": "Rychlé připojení",
"description": "Připojit přímo k relaci terminálu nebo správce souborů bez uložení konfigurace hostitele",
"ipAddress": "IP adresa nebo název hostitele",
"port": "Přístav",
"port": "Port",
"username": "Uživatelské jméno",
"password": "Heslo",
"key": "SSH soukromý klíč",
"keyPassword": "Klíčové heslo (volitelné)",
"key": "Soukromý klíč SSH",
"keyPassword": "Heslo klíče (volitelné)",
"keyType": "Typ klíče",
"uploadFile": "Nahrát soubor",
"pasteKey": "Vložit klíč",
"credential": "Pověření",
"overrideUsername": "Přepsat pověření uživatelské jméno",
"credential": "Přihlašovací údaj",
"overrideUsername": "Přepsat uživatelské jméno pro přihlašovací údaje",
"overrideUsernameDesc": "Použít jiné uživatelské jméno, než jaké je uloženo v pověření",
"connectTerminal": "Připojit k terminálu",
"connectFileManager": "Připojit ke správci souborů",
@@ -238,7 +238,7 @@
"commandsWillBeSent": "Příkazy budou odeslány do vybraných terminálů {{count}}.",
"settings": "Nastavení",
"enableRightClickCopyPaste": "Povolit kopírování/vkládání kliknutím pravým tlačítkem",
"shareIdeas": "Máte nápady na to, co by mělo přijít dále pro ssh nástroje? Sdílejte je na",
"shareIdeas": "Máte nápady, co by mělo být dalším vývojem nástrojů SSH? Sdílejte je na",
"scripts": {
"inputPlaceholder": "Např. systémové příkazy, Docker skripty"
}
@@ -331,6 +331,8 @@
"twoSplit": "2-Way",
"threeSplit": "3-Way",
"fourSplit": "4 cesty",
"fiveSplit": "5-Way",
"sixSplit": "6-Way",
"availableTabs": "Dostupné panely",
"dragTabsHint": "Přetáhněte záložky do polí níže uvedeného rozvržení pro jejich přiřazení",
"layout": "Rozdělit rozložení obrazovky",
@@ -372,7 +374,11 @@
"warning": "Varování",
"notValidatedWarning": "URL není ověřena - ujistěte se, že je to správné",
"changeServer": "Změnit server",
"mustIncludeProtocol": "Adresa URL serveru musí začínat http:// nebo https:////"
"mustIncludeProtocol": "Adresa URL serveru musí začínat http:// nebo https:////",
"useEmbedded": "Použít místní server",
"embeddedDesc": "Spustit Termix s vestavěným lokálním serverem (nepotřebujete vzdálený server)",
"embeddedConnecting": "Připojování k lokálnímu serveru...",
"embeddedNotReady": "Místní server není ještě připravený. Počkejte prosím chvíli a zkuste to znovu."
},
"versionCheck": {
"error": "Chyba kontroly verze",
@@ -471,6 +477,7 @@
"confirm": "Potvrdit",
"yes": "Ano",
"no": "Ne",
"or": "NEBO",
"ok": "OK",
"enabled": "Povoleno",
"disabled": "Zakázáno",
@@ -520,13 +527,13 @@
"serverStats": "Statistiky serveru",
"admin": "Admin",
"userProfile": "Profil uživatele",
"tools": "Nástroje a nářadí",
"tools": "Nástroje",
"snippets": "Výstřižky bloků",
"newTab": "New Tab",
"splitScreen": "Rozdělit obrazovku",
"closeTab": "Zavřít kartu",
"sshManager": "SSH správce",
"hostManager": "Manažer serveru",
"sshManager": "Správce SSH",
"hostManager": "Správce hostitelů",
"cannotSplitTab": "Nelze rozdělit tuto kartu",
"tabNavigation": "Tab Navigation",
"hostTabTitle": "{{username}}@{{ip}}:{{port}}",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "Uživatel {{username}} byl úspěšně smazán",
"failedToDeleteUser": "Odstranění uživatele se nezdařilo",
"overrideUserInfoUrl": "Přepsat URL informace o uživateli (nepovinné)",
"allowedUsers": "Povolení uživatelé",
"allowedUsersDescription": "Seznam povolených e-mailových vzorů oddělených čárkou. Použijte přesné e-maily (user@example.com), přípony domény (@example.com), nebo nechte prázdné pro povolení všem uživatelům.",
"failedToFetchSessions": "Nepodařilo se načíst relace",
"sessionRevokedSuccessfully": "Relace byla úspěšně zrušena",
"failedToRevokeSession": "Nepodařilo se zrušit relaci",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "CRITICAL: Přihlášení k heslu je zakázáno. Pokud resetujete nebo špatně nastavíte OIDC, ztratíte přístup k Termix a blokovat vaši instanci. Pokračujte pouze v případě, že jste naprosto jistí.",
"confirmDisableOIDCWarning": "VAROVÁNÍ: Chystáte se zakázat OIDC, zatímco je také zakázáno přihlášení heslem. Tohle blokuje vaši instanci Termix a ztratíte veškerý přístup. Jste si jisti, že chcete pokračovat?",
"failedToUpdatePasswordLoginStatus": "Nepodařilo se aktualizovat stav přihlášení k heslu",
"monitoringDefaults": "Výchozí nastavení sledování",
"monitoringDefaultsDesc": "Globální výchozí intervaly pro kontrolu stavu a sběr metrik. Nastavení hostitele tyto hodnoty přepsá.",
"globalStatusCheckInterval": "Interval kontroly stavu",
"globalMetricsInterval": "Výchozí interval sběru metrik",
"globalSettingsSaved": "Globální nastavení monitorování uloženo",
"failedToSaveGlobalSettings": "Nepodařilo se uložit globální nastavení monitorování",
"failedToLoadGlobalSettings": "Nepodařilo se načíst globální nastavení monitorování",
"loadingSessions": "Načítání relací...",
"noActiveSessions": "Nebyly nalezeny žádné aktivní relace.",
"device": "Zařízení",
@@ -814,7 +830,7 @@
"assignNewRole": "Přiřadit novou roli"
},
"hosts": {
"title": "Manažer serveru",
"title": "Správce hostitelů",
"hosts": "Hostitelé",
"sshHosts": "SSH hostitelé",
"noHosts": "Žádné SSH hostitele",
@@ -829,7 +845,7 @@
"importing": "Importování...",
"importJsonTitle": "Importovat SSH hostitele z JSON",
"importJsonDesc": "Nahrajte soubor JSON pro hromadný import více SSH hostitelů (max. 100).",
"downloadSample": "Stáhnout ukázku",
"downloadSample": "Stáhnout vzorek",
"formatGuide": "Průvodce formátováním",
"exportCredentialWarning": "Varování: Hostitel \"{{name}}\" používá ověření důvěryhodnosti. Exportovaný soubor nebude obsahovat přihlašovací údaje a bude muset být po importu ručně přenakonfigurován. Chcete pokračovat?",
"exportSensitiveDataWarning": "Varování: Hostitel \"{{name}}\" obsahuje citlivé ověřovací údaje (heslo/SSH klíč). Exportovaný soubor bude obsahovat tato data do prostého textu. Udržujte soubor bezpečný a po jeho použití. Chcete pokračovat?",
@@ -841,13 +857,18 @@
"noHostsInJson": "Nebyly nalezeny žádné hostitele v souboru JSON",
"maxHostsAllowed": "Maximálně 100 hostitelů na import",
"importCompleted": "Import dokončen: {{success}} úspěšný, {{failed}} selhalo",
"importCreated": "vytvořeno",
"importUpdated": "aktualizováno",
"importFailedCount": "selhalo",
"importSkipExisting": "Import (existující přeskočení)",
"importOverwriteExisting": "Import (přepsat)",
"importFailed": "Import se nezdařil",
"importError": "Chyba importu",
"failedToImportJson": "Nepodařilo se importovat soubor JSON",
"connectionDetails": "Detaily připojení",
"organization": "Organizace",
"ipAddress": "IP adresa nebo název hostitele",
"port": "Přístav",
"port": "Port",
"name": "Název",
"username": "Uživatelské jméno",
"folder": "Složka",
@@ -885,7 +906,7 @@
"remove": "Odebrat",
"sourcePort": "Zdrojový port",
"sourcePortDesc": " (Odběr se týká podrobností o aktuálním připojení v obecné kartě)",
"endpointPort": "Přístav koncového bodu",
"endpointPort": "Port koncového bodu",
"endpointSshConfig": "Konfigurace koncového bodu SSH",
"tunnelForwardDescription": "Tento tunel přeposílá provoz z portu {{sourcePort}} na zdrojovém počítači (aktuální údaje o připojení obecně) do portu {{endpointPort}} na koncovém počítači.",
"maxRetries": "Maximální počet opakování",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "Zkontrolujte, zda je server online nebo offline",
"statusCheckInterval": "Interval kontroly stavu",
"statusCheckIntervalDesc": "Jak často kontrolovat, zda je hostitel online (5s - 1h)",
"useGlobalStatusInterval": "Použít globální výchozí nastavení",
"useGlobalMetricsInterval": "Použít globální výchozí nastavení",
"usingGlobalDefault": "Použití globálního výchozího ({{value}}s)",
"metricsEnabled": "Povolit sledování metriky",
"metricsEnabledDesc": "Sbírat CPU, RAM, disky a další systémové statistiky",
"metricsInterval": "Interval sběru metrik",
@@ -1025,7 +1049,7 @@
"intervalMinutes": "minuty",
"intervalValidation": "Monitorovací intervaly musí být mezi 5 sekundami a 1 hodinou (3600 sekund)",
"monitoringDisabled": "Monitorování serveru je pro tohoto hostitele zakázáno",
"enableMonitoring": "Povolit monitorování v Host ManagerZáložka Statistiky",
"enableMonitoring": "Povolit monitorování ve Správci hostitelůzáložka Statistiky",
"monitoringDisabledBadge": "Monitorování vypnuto",
"statusMonitoring": "Stav",
"metricsMonitoring": "Metrika",
@@ -1113,7 +1137,7 @@
"enableSocks5": "Povolit SOCKS5 Proxy",
"enableSocks5Description": "Použít SOCKS5 proxy pro toto SSH připojení",
"socks5Host": "Proxy server",
"socks5Port": "Port proxy serveru",
"socks5Port": "Port serveru proxy",
"socks5Username": "Proxy uživatelské jméno",
"socks5Password": "Proxy heslo",
"socks5UsernameOptional": "Volitelné: ponechte prázdné, pokud proxy nevyžaduje ověření",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "Automaticky nabídne vložení hesla SSH při žádání o heslo",
"sudoPassword": "Sudo heslo",
"sudoPasswordDesc": "Volitelné heslo pro sudo příkazy (užitečné s ověřením klíče)",
"keepaliveSettings": "SSH uchová",
"keepaliveSettingsDesc": "Konfigurace udržovacího chování SSH-úrovně. Nastavte interval 0 pro vypnutí SSH zachování (užitečné pro MikroTik a další zařízení, která ignorují udržující požadavky).",
"keepaliveInterval": "Interval Keepalive (ms)",
"keepaliveIntervalDesc": "Jak často odesílat SSH udržující pingy. Výchozí: 30000 (30s). Nastavte na 0 pro vypnutí.",
"keepaliveCountMax": "Ponechat maximální počet selhání",
"keepaliveCountMaxDesc": "Odpojte se po tomto počtu nezodpovězených držení. Výchozí: 3.",
"socks4": "SOCKS4",
"socks5": "SOCKS5",
"httpConnect": "HTTP PŘIPOJIT",
"testProxy": "Testovat připojení",
"testingProxy": "Testování...",
"proxyTestSuccess": "Proxy připojení úspěšné ({{latency}}ms)",
"proxyTestFailed": "Proxy test selhal: {{error}}",
"connectionPath": "Cesta k připojení",
"executeSnippetOnConnect": "Spustit snippet, když se terminál připojí",
"autoMosh": "Automatické MOSH",
"autoMoshDesc": "Automaticky spustit MOSH příkaz při připojení",
@@ -1237,7 +1273,7 @@
"failedToFetchStats": "Nepodařilo se načíst statistiky",
"noContainersFound": "Nebyly nalezeny žádné kontejnery",
"noContainersFoundHint": "Začněte vytvořením kontejnerů na vašem serveru",
"searchPlaceholder": "Hledat podle jména, obrázku, nebo ID...",
"searchPlaceholder": "Hledat podle jména, obrázku, nebo ID",
"filterByStatusPlaceholder": "Filtrovat podle stavu",
"allContainersCount": "Vše ({{count}})",
"statusCount": "{{status}} ({{count}})",
@@ -1256,7 +1292,7 @@
"failedToRemoveContainer": "Odstranění kontejneru selhalo: {{error}}",
"image": "Obrázek:",
"idLabel": "ID:",
"ports": "Přístavy:",
"ports": "Porty:",
"noPorts": "Nic",
"created": "Vytvořeno:",
"start": "Začít",
@@ -1284,7 +1320,35 @@
"showTimestamps": "Zobrazit časová razítka",
"autoRefresh": "Automatické obnovení",
"filterLogsPlaceholder": "Filtrovat logy...",
"noLogsAvailable": "Žádné logy nejsou k dispozici"
"noLogsAvailable": "Žádné logy nejsou k dispozici",
"selectMode": "Vybrat",
"exitSelectMode": "Zrušit",
"selectedCount": "{{count}} vybrán",
"bulkMonitoring": "Monitorování",
"bulkFeatures": "Vlastnosti",
"bulkMoveFolder": "Přesunout do složky",
"bulkPin": "Připnout",
"bulkUnpin": "Unpin",
"enableAllFeatures": "Povolit všechny funkce",
"disableAllFeatures": "Zakázat všechny funkce",
"enableStatusCheck": "Povolit kontrolu stavu",
"disableStatusCheck": "Zakázat kontrolu stavu",
"enableMetrics": "Povolit metriku",
"disableMetrics": "Zakázat metriku",
"bulkEnableTerminal": "Povolit terminál",
"bulkDisableTerminal": "Zakázat terminál",
"bulkEnableTunnel": "Povolit tunel",
"bulkDisableTunnel": "Zakázat tunel",
"bulkEnableFileManager": "Povolit správce souborů",
"bulkDisableFileManager": "Zakázat správce souborů",
"bulkEnableDocker": "Povolit Docker",
"bulkDisableDocker": "Zakázat Docker",
"bulkUpdateSuccess": "Úspěšně aktualizováno {{count}} hostitele",
"bulkUpdateFailed": "Hromadná aktualizace selhala",
"selectAll": "Vybrat vše",
"deselectAll": "Zrušit výběr všech",
"useGlobalStatusDefault": "Použít globální výchozí (stav)",
"useGlobalMetricsDefault": "Použít globální výchozí (Metrics)"
},
"terminal": {
"title": "Terminál",
@@ -1306,6 +1370,8 @@
"connectionClosed": "Připojení bylo uzavřeno",
"connectionError": "Chyba připojení: {{message}}",
"connected": "Připojeno",
"clipboardWriteFailed": "Kopírování do schránky se nezdařilo. Ujistěte se, že stránka je vedena přes HTTPS nebo localhost.",
"clipboardReadFailed": "Nepodařilo se přečíst ze schránky. Ujistěte se, že máte oprávnění na schránku.",
"sshConnected": "SSH připojení navázáno",
"authError": "Ověření se nezdařilo: {{message}}",
"unknownError": "Došlo k neznámé chybě",
@@ -1360,7 +1426,9 @@
"totpTimeout": "Časový limit ověřování TOTP. Připojte se znovu.",
"passwordTimeout": "Časový limit ověření hesla vypršel. Připojte se znovu.",
"connectionRejected": "Připojení odmítnuto serverem. Zkontrolujte prosím ověření a nastavení sítě.",
"hostKeyRejected": "Ověření SSH hostitele bylo zamítnuto. Připojení bylo zrušeno."
"hostKeyRejected": "Ověření SSH hostitele bylo zamítnuto. Připojení bylo zrušeno.",
"sessionTakenOver": "Relace byla otevřena v jiné kartě. Znovu připojuji...",
"sessionAttachTimeout": "Vypršel časový limit přílohy relace. Vytváření nového připojení..."
},
"fileManager": {
"title": "Správce souborů",
@@ -1502,7 +1570,7 @@
"cannotReadFile": "Soubor nelze přečíst",
"noSshSessionId": "Žádné SSH ID relace není k dispozici",
"noFilePath": "Není dostupná cesta k souboru",
"noCurrentHost": "Aktuální hostitel není k dispozici",
"noCurrentHost": "Aktuálně není dostupný žádný hostitel",
"fileSavedSuccessfully": "Soubor byl úspěšně uložen",
"saveTimeout": "Vypršel časový limit uložení. Soubor mohl být úspěšně uložen, ale dokončení operace trvalo příliš dlouho. Potvrzte protokol Dockeru.",
"failedToSaveFile": "Nepodařilo se uložit soubor",
@@ -1670,7 +1738,13 @@
"write": "Napsat",
"execute": "Spustit",
"permissionsChangedSuccessfully": "Oprávnění úspěšně změněna",
"failedToChangePermissions": "Změna oprávnění se nezdařila"
"failedToChangePermissions": "Změna oprávnění se nezdařila",
"name": "Název",
"sortByName": "Název",
"sortByDate": "Datum úpravy",
"sortBySize": "Velikost",
"ascending": "Vzestupně",
"descending": "sestupně"
},
"tunnel": {
"log": {
@@ -1707,7 +1781,7 @@
"connect": "Připojit",
"disconnect": "Odpojit",
"cancel": "Zrušit",
"port": "Přístav",
"port": "Port",
"attempt": "Pokusit se {{current}} o {{max}}",
"nextRetryIn": "Další opakování za {{seconds}} sekund",
"checkDockerLogs": "Zkontroluj své dokovací protokoly z důvodu chyby, připojte se k",
@@ -1786,7 +1860,7 @@
"totpInvalidCode": "Neplatný ověřovací kód",
"totpCancelled": "Metrika byla zrušena",
"authenticationFailed": "Ověření se nezdařilo",
"noneAuthNotSupported": "Statistiky serveru nepodporují 'none' typ autentizace.",
"noneAuthNotSupported": "Statistiky serveru nepodporují typ ověřování 'žádné'.",
"load": "Načíst",
"editLayout": "Upravit rozvržení",
"cancelEdit": "Zrušit",
@@ -1820,13 +1894,13 @@
"quickActionFailed": "{{name}} selhal",
"quickActionError": "Provedení {{name}} se nezdařilo",
"ports": {
"title": "Přístavy poslech",
"title": "Naslouchající porty",
"protocol": "Protocol",
"port": "Přístav",
"port": "Port",
"address": "Adresa",
"state": "Stát",
"process": "Proces",
"noData": "Žádná data z portů pro poslech"
"noData": "Žádná data naslouchajících portů"
},
"firewall": {
"title": "Firewall",
@@ -1839,7 +1913,7 @@
"noData": "Nejsou k dispozici žádná data firewallu",
"action": "Akce",
"protocol": "Proto",
"port": "Přístav",
"port": "Port",
"source": "Zdroj",
"accept": "PŘIJMOUT",
"drop": "DROP",
@@ -1858,7 +1932,7 @@
"loginButton": "Přihlásit se",
"registerButton": "Registrovat se",
"forgotPassword": "Zapomněli jste heslo?",
"rememberMe": "Pamatovat si mě",
"rememberMe": "Zapamatovat zařízení na 30 dní (včetně TOTP)",
"noAccount": "Nemáte účet?",
"hasAccount": "Již máte účet?",
"loginSuccess": "Přihlášení bylo úspěšné",
@@ -2023,6 +2097,7 @@
"processing": "Zpracovávám...",
"pleaseWait": "Počkejte prosím...",
"registrationDisabled": "Registrace nového účtu je v současné době zakázána administrátorem. Přihlaste se nebo kontaktujte správce.",
"userNotAllowed": "Váš účet nemá oprávnění k registraci. Obraťte se na správce.",
"databaseConnected": "Databáze byla úspěšně připojena",
"databaseConnectionFailed": "Nepodařilo se připojit k databázovému serveru",
"checkServerConnection": "Zkontrolujte připojení k serveru a zkuste to znovu",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "Vyberte barevný motiv pro aplikaci",
"terminalSyntaxHighlightingDesc": "Automaticky zvýraznit příkazy, cesty, IP a logovací úrovně v terminálu",
"enableCommandPaletteShortcut": "Povolit zástupce pro Palety",
"enableCommandPaletteShortcutDesc": "Dvojitým poklepáním vlevo otevřít Příkazovou paletu pro rychlý přístup k hostům"
"enableCommandPaletteShortcutDesc": "Dvojitým poklepáním vlevo otevřít Příkazovou paletu pro rychlý přístup k hostům",
"enableTerminalSessionPersistence": "Trvalé relace terminálů",
"enableTerminalSessionPersistenceDesc": "Udržovat SSH připojení při přepínání karet nebo zavření prohlížeče (může být nestabilní)"
},
"user": {
"failedToLoadVersionInfo": "Nepodařilo se načíst informace o verzi"
@@ -2114,11 +2191,12 @@
"usernameField": "jméno",
"scopes": "openid e-mailový profil",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"allowedUsers": "user@example.com, @company.com",
"enterUsername": "Zadejte uživatelské jméno pro administrátora",
"searchHosts": "Hledat hostitele podle jména, uživatelského jména, IP složky, tagů...",
"searchHosts": "Hledat hostitele podle názvu, uživatelského jména, IP, složky, štítků…",
"enterPassword": "Zadejte své heslo",
"totpCode": "6-digit TOTP code",
"searchHostsAny": "Hledat hostitele (try: tag:prod, user:root, ip:192.168)...",
"searchHostsAny": "Hledat hostitele (zkuste: tag:prod, user:root, ip:192.168)",
"confirmPassword": "Pro potvrzení zadejte své heslo",
"typeHere": "Napište zde",
"fileName": "Zadejte název souboru (např. příklad.txt)",
@@ -2179,7 +2257,7 @@
"clickToSelectFile": "Kliknutím vyberte soubor",
"noSshHosts": "Žádné SSH hostitele",
"sshHosts": "SSH hostitelé",
"importSshHosts": "Importovat SSH hostitele z JSON",
"importSshHosts": "Importovat hostitele SSH z JSON",
"clientId": "ID klienta",
"clientSecret": "Tajný klíč klienta",
"error": "Chyba",
@@ -2374,7 +2452,7 @@
"sourceDifferentFromTarget": "Zdroj a cíl musí být jiný",
"exportJSON": "Exportovat JSON",
"importJSON": "Importovat JSON",
"searchHost": "Hledat v hostech...",
"searchHost": "Hledat hostitele…",
"noHostFound": "Nenalezeny žádné hostitele",
"searchGroup": "Hledat skupiny...",
"noGroupFound": "Nenalezeny žádné skupiny",
@@ -2549,7 +2627,7 @@
"viewDesc": "Kvůli šifrovacímu systému Termix dojde později k dalším úrovním oprávnění"
},
"commandPalette": {
"searchPlaceholder": "Hledat hostitele nebo rychlé akce...",
"searchPlaceholder": "Hledat hostitele nebo rychlé akce",
"recentActivity": "Nedávná aktivita",
"navigation": "Navigation",
"addHost": "Přidat hostitele",
@@ -2569,7 +2647,7 @@
"press": "Tisk",
"toToggle": "přepnout",
"close": "Zavřít",
"hostManager": "Manažer serveru",
"hostManager": "Správce hostitelů",
"pressToToggle": "Dvakrát stiskněte levé tlačítko Shift pro otevření palety příkazu"
},
"docker": {
@@ -2598,7 +2676,7 @@
"failedToRemoveContainer": "Odstranění kontejneru {{name}} se nezdařilo",
"image": "Obrázek",
"idLabel": "ID",
"ports": "Přístavy",
"ports": "Porty",
"noPorts": "Žádné porty",
"created": "Vytvořeno",
"start": "Začít",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "2-vejs",
"threeSplit": "3-vejs",
"fourSplit": "4-vejs",
"fiveSplit": "5-vejs",
"sixSplit": "6-vejs",
"availableTabs": "Tilgængelige Faneblade",
"dragTabsHint": "Træk faneblade til layoutcellerne nedenfor for at tildele dem",
"layout": "Opdel Skærm Layout",
@@ -372,7 +374,11 @@
"warning": "Advarsel",
"notValidatedWarning": "URL ikke valideret - sørg for at det er korrekt",
"changeServer": "Skift Server",
"mustIncludeProtocol": "Server URL skal starte med http:// eller https://"
"mustIncludeProtocol": "Server URL skal starte med http:// eller https://",
"useEmbedded": "Brug Lokal Server",
"embeddedDesc": "Kør Termix med den indbyggede lokale server (ingen fjernserver er nødvendig)",
"embeddedConnecting": "Forbinder til lokal server...",
"embeddedNotReady": "Lokal server er ikke klar endnu. Vent et øjeblik og prøv igen."
},
"versionCheck": {
"error": "Fejl Ved Versionskontrol",
@@ -471,6 +477,7 @@
"confirm": "Bekræft",
"yes": "Ja",
"no": "Nej",
"or": "ELLER",
"ok": "Ok",
"enabled": "Aktiveret",
"disabled": "Deaktiveret",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "Bruger {{username}} slettet",
"failedToDeleteUser": "Kunne ikke slette bruger",
"overrideUserInfoUrl": "Tilsidesæt Bruger Info URL (ikke påkrævet)",
"allowedUsers": "Tilladt Brugere",
"allowedUsersDescription": "Kommaadskilt liste over tilladte e-mail mønstre. Brug nøjagtige e-mails (user@example.com), domæne suffikser (@example.com), eller efterlad tom for at tillade alle brugere.",
"failedToFetchSessions": "Kunne ikke hente sessioner",
"sessionRevokedSuccessfully": "Session tilbagekaldt succesfuldt",
"failedToRevokeSession": "Mislykkedes at tilbagekalde session",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "CRITISK adgangskodelogin er deaktiveret. Hvis du nulstiller eller fejlkonfigurerer OIDC, mister du al adgang til Termix og tester din instans. Fortsæt kun, hvis du er helt sikker.",
"confirmDisableOIDCWarning": "ADVARSEL: Du er ved at deaktivere OIDC, mens adgangskodelogin også er deaktiveret. Dette vil ødelægge din Termix-instans, og du vil miste al adgang. Er du helt sikker på, at du vil fortsætte?",
"failedToUpdatePasswordLoginStatus": "Kunne ikke opdatere adgangskode login status",
"monitoringDefaults": "Overvågning Af Standarder",
"monitoringDefaultsDesc": "Globale standardintervaller for statuskontrol og måling samling. Per-host indstillinger tilsidesætter disse værdier.",
"globalStatusCheckInterval": "Standard Status Check Interval",
"globalMetricsInterval": "Standard Metrics Collection Interval",
"globalSettingsSaved": "Globale overvågningsindstillinger gemt",
"failedToSaveGlobalSettings": "Kunne ikke gemme globale overvågningsindstillinger",
"failedToLoadGlobalSettings": "Kunne ikke indlæse globale overvågningsindstillinger",
"loadingSessions": "Indlæser sessioner...",
"noActiveSessions": "Ingen aktive sessioner fundet.",
"device": "Enhed",
@@ -841,6 +857,11 @@
"noHostsInJson": "Ingen værter fundet i JSON- filen",
"maxHostsAllowed": "Maksimum 100 værter tilladt pr. import",
"importCompleted": "Import fuldført: {{success}} vellykket, {{failed}} mislykkedes",
"importCreated": "oprettet",
"importUpdated": "opdateret",
"importFailedCount": "mislykkedes",
"importSkipExisting": "Import (spring over eksisterende)",
"importOverwriteExisting": "Import (overskriv eksisterende)",
"importFailed": "Import mislykkedes",
"importError": "Import fejl",
"failedToImportJson": "Kunne ikke importere JSON- fil",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "Tjek om serveren er online eller offline",
"statusCheckInterval": "Status Tjek Interval",
"statusCheckIntervalDesc": "Hvor ofte til at kontrollere, om værten er online (5s - 1h)",
"useGlobalStatusInterval": "Brug global standard",
"useGlobalMetricsInterval": "Brug global standard",
"usingGlobalDefault": "Bruger global standard ({{value}}s)",
"metricsEnabled": "Aktiver Måleovervågning",
"metricsEnabledDesc": "Indsaml CPU, RAM, disk, og andre systemstatistikker",
"metricsInterval": "Interval For Metrikelsamling",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "Tilbyd automatisk at indsætte SSH-adgangskode, når sudo beder om adgangskode",
"sudoPassword": "Sudo Adgangskode",
"sudoPasswordDesc": "Valgfri adgangskode til sudo kommandoer (nyttigt med nøgle godkendelse)",
"keepaliveSettings": "SSH Holde",
"keepaliveSettingsDesc": "Indstil SSH-niveau keepalive adfærd. Sæt interval til 0 for at deaktivere SSH keepalives (nyttigt for MikroTik og andre enheder, der ignorerer keepalive anmodninger).",
"keepaliveInterval": "Keepalive Interval (ms)",
"keepaliveIntervalDesc": "Hvor ofte skal SSH keepalive pings. Standard: 30000 (30s). Sæt til 0 for at deaktivere.",
"keepaliveCountMax": "Keepalive Max Fejl",
"keepaliveCountMaxDesc": "Afbryd efter dette mange ubesvarede keepalives. Standard: 3.",
"socks4": "SOCKS4",
"socks5": "SOCKS5",
"httpConnect": "HTTP CONNECT",
"testProxy": "Test Forbindelse",
"testingProxy": "Test...",
"proxyTestSuccess": "Proxyforbindelse vellykket ({{latency}}ms)",
"proxyTestFailed": "Proxytest fejlede: {{error}}",
"connectionPath": "Sti Til Forbindelse",
"executeSnippetOnConnect": "Udfør en snippet når terminalen tilsluttes",
"autoMosh": "Auto-MOSH",
"autoMoshDesc": "Kør automatisk MOSH kommando ved tilslutning",
@@ -1284,7 +1320,35 @@
"showTimestamps": "Vis Tidsstempler",
"autoRefresh": "Opdater Automatisk",
"filterLogsPlaceholder": "Filtrer logger...",
"noLogsAvailable": "Ingen logs tilgængelige"
"noLogsAvailable": "Ingen logs tilgængelige",
"selectMode": "Vælg",
"exitSelectMode": "Annuller",
"selectedCount": "{{count}} valgt",
"bulkMonitoring": "Overvågning",
"bulkFeatures": "Funktioner",
"bulkMoveFolder": "Flyt til mappe",
"bulkPin": "Fastgør",
"bulkUnpin": "Unpin",
"enableAllFeatures": "Aktiver Alle Funktioner",
"disableAllFeatures": "Deaktivér Alle Funktioner",
"enableStatusCheck": "Aktiver Status Tjek",
"disableStatusCheck": "Deaktivér Statuscheck",
"enableMetrics": "Aktiver Metrics",
"disableMetrics": "Deaktivér Metrics",
"bulkEnableTerminal": "Aktiver Terminal",
"bulkDisableTerminal": "Deaktivér Terminal",
"bulkEnableTunnel": "Aktiver Tunnel",
"bulkDisableTunnel": "Deaktivér Tunnel",
"bulkEnableFileManager": "Aktiver Filhåndtering",
"bulkDisableFileManager": "Deaktivér Filhåndtering",
"bulkEnableDocker": "Aktiver Docker",
"bulkDisableDocker": "Deaktivér Docker",
"bulkUpdateSuccess": "Opdateret {{count}} vært(er) succesfuldt",
"bulkUpdateFailed": "Masse opdatering mislykkedes",
"selectAll": "Vælg alle",
"deselectAll": "Fravælg alle",
"useGlobalStatusDefault": "Brug Global Standard (Status)",
"useGlobalMetricsDefault": "Brug Global Standard (Metrics)"
},
"terminal": {
"title": "Terminal",
@@ -1306,6 +1370,8 @@
"connectionClosed": "Forbindelse lukket",
"connectionError": "Forbindelsesfejl: {{message}}",
"connected": "Forbundet",
"clipboardWriteFailed": "Kunne ikke kopiere til udklipsholderen. Sørg for, at siden er serveret over HTTPS eller localhost.",
"clipboardReadFailed": "Kunne ikke læse fra udklipsholderen. Sørg for, at udklipsholdertilladelser er tildelt.",
"sshConnected": "SSH-forbindelse oprettet",
"authError": "Godkendelse mislykkedes: {{message}}",
"unknownError": "Ukendt fejl opstod",
@@ -1360,7 +1426,9 @@
"totpTimeout": "TOTP bekræftelses-timeout. Genforbind.",
"passwordTimeout": "Adgangskodens bekræftelses-timeout. Opret venligst ny forbindelse.",
"connectionRejected": "Forbindelse afvist af serveren. Tjek venligst din godkendelse og netværkskonfiguration.",
"hostKeyRejected": "SSH-værtnøglebekræftelse afvist. Forbindelse annulleret."
"hostKeyRejected": "SSH-værtnøglebekræftelse afvist. Forbindelse annulleret.",
"sessionTakenOver": "Session blev åbnet i et andet faneblad. Genjusterer...",
"sessionAttachTimeout": "Session vedhæftet fil timeout. Oprettelse af ny forbindelse..."
},
"fileManager": {
"title": "Filhåndtering",
@@ -1670,7 +1738,13 @@
"write": "Skriv",
"execute": "Udfør",
"permissionsChangedSuccessfully": "Tilladelser ændret",
"failedToChangePermissions": "Mislykkedes at ændre tilladelser"
"failedToChangePermissions": "Mislykkedes at ændre tilladelser",
"name": "Navn",
"sortByName": "Navn",
"sortByDate": "Ændret Dato",
"sortBySize": "Størrelse",
"ascending": "Stigende",
"descending": "Faldende"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "Login",
"registerButton": "Registrer",
"forgotPassword": "Glemt Adgangskode?",
"rememberMe": "Husk Mig",
"rememberMe": "Husk enheden for 30 dage (inkluderer TOTP)",
"noAccount": "Har du ikke en konto?",
"hasAccount": "Har du allerede en konto?",
"loginSuccess": "Login lykkedes",
@@ -2023,6 +2097,7 @@
"processing": "Behandler...",
"pleaseWait": "Vent venligst...",
"registrationDisabled": "Ny konto registrering er i øjeblikket deaktiveret af en administrator. Log ind eller kontakt en administrator.",
"userNotAllowed": "Din konto er ikke autoriseret til at registrere. Kontakt venligst en administrator.",
"databaseConnected": "Database forbundet succesfuldt",
"databaseConnectionFailed": "Kunne ikke forbinde til databaseserveren",
"checkServerConnection": "Tjek venligst din server forbindelse og prøv igen",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "Vælg farvetemaet for programmet",
"terminalSyntaxHighlightingDesc": "Fremhæv automatisk kommandoer, stier, IP'er og logniveauer i terminaloutput",
"enableCommandPaletteShortcut": "Aktivér Kommandopalettegenvej",
"enableCommandPaletteShortcutDesc": "Dobbelttryk på venstre Skift for at åbne kommandopaletten for hurtig adgang til værter"
"enableCommandPaletteShortcutDesc": "Dobbelttryk på venstre Skift for at åbne kommandopaletten for hurtig adgang til værter",
"enableTerminalSessionPersistence": "Vedvarende Terminalsessioner",
"enableTerminalSessionPersistenceDesc": "Oprethold SSH-forbindelser, når du skifter faner eller lukker browseren (kan være ustabil)"
},
"user": {
"failedToLoadVersionInfo": "Kunne ikke indlæse versionsinformation"
@@ -2114,6 +2191,7 @@
"usernameField": "navn",
"scopes": "openid e-mail profil",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"allowedUsers": "user@example.com, @company.com",
"enterUsername": "Indtast brugernavn at lave admin",
"searchHosts": "Søg hosts ved navn, brugernavn, IP, mappe, tags...",
"enterPassword": "Indtast din adgangskode",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "2-Weg",
"threeSplit": "3-Weg",
"fourSplit": "4-Weg",
"fiveSplit": "5-Weg",
"sixSplit": "6-Weg",
"availableTabs": "Verfügbare Tabs",
"dragTabsHint": "Ziehen Sie Tabs in die Layoutzellen unten, um sie zuzuordnen",
"layout": "Bildschirmlayout teilen",
@@ -372,7 +374,11 @@
"warning": "Warnung",
"notValidatedWarning": "URL nicht validiert - stellen Sie sicher, dass sie korrekt ist",
"changeServer": "Server ändern",
"mustIncludeProtocol": "Server-URL muss mit http:// oder https:// beginnen"
"mustIncludeProtocol": "Server-URL muss mit http:// oder https:// beginnen",
"useEmbedded": "Lokalen Server verwenden",
"embeddedDesc": "Starte Termix mit dem eingebauten lokalen Server (kein entfernter Server benötigt)",
"embeddedConnecting": "Verbindung zum lokalen Server...",
"embeddedNotReady": "Lokaler Server ist noch nicht bereit. Bitte warten Sie einen Moment und versuchen Sie es erneut."
},
"versionCheck": {
"error": "Versionsüberprüfungsfehler",
@@ -471,6 +477,7 @@
"confirm": "Bestätigen",
"yes": "Ja",
"no": "Nein",
"or": "ODER",
"ok": "Ok",
"enabled": "Aktiviert",
"disabled": "Deaktiviert",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "Benutzer {{username}} erfolgreich gelöscht",
"failedToDeleteUser": "Fehler beim Löschen des Benutzers",
"overrideUserInfoUrl": "Benutzer-Info URL überschreiben (nicht erforderlich)",
"allowedUsers": "Erlaubte Benutzer",
"allowedUsersDescription": "Kommaseparierte Liste von erlaubten E-Mail-Mustern. Verwenden Sie exakte E-Mails (user@example.com), Domain-Suffixes (@example.com) oder lassen Sie leer, um alle Benutzer zu erlauben.",
"failedToFetchSessions": "Fehler beim Abrufen der Sitzungen",
"sessionRevokedSuccessfully": "Sitzung erfolgreich widerrufen",
"failedToRevokeSession": "Sitzung konnte nicht widerrufen werden",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "CRITICAL: Passwort-Login ist deaktiviert. Wenn Sie OIDC zurücksetzen oder falsch konfigurieren, verlieren Sie alle Zugriffe auf Termix und Brick Ihre Instanz. Fahren Sie nur fort, wenn Sie absolut sicher sind.",
"confirmDisableOIDCWarning": "WARNUNG: Sie sind dabei, OIDC zu deaktivieren, während die Passwort-Anmeldung ebenfalls deaktiviert ist. Dies wird deine Termix-Instanz bricken und du wirst alle Zugriffe verlieren. Bist du dir sicher, dass du fortfahren möchtest?",
"failedToUpdatePasswordLoginStatus": "Fehler beim Aktualisieren des Passwort-Login-Status",
"monitoringDefaults": "Standard überwachen",
"monitoringDefaultsDesc": "Globale Standardintervalle für Statusüberprüfungen und Metriksammlung. Per-Host-Einstellungen überschreiben diese Werte.",
"globalStatusCheckInterval": "Standard Statusüberprüfungsintervall",
"globalMetricsInterval": "Standard Metrik-Sammlungsintervall",
"globalSettingsSaved": "Globale Überwachungseinstellungen gespeichert",
"failedToSaveGlobalSettings": "Fehler beim Speichern der globalen Überwachungseinstellungen",
"failedToLoadGlobalSettings": "Fehler beim Laden der globalen Überwachungseinstellungen",
"loadingSessions": "Lade Sitzungen...",
"noActiveSessions": "Keine aktiven Sitzungen gefunden.",
"device": "Gerät",
@@ -841,6 +857,11 @@
"noHostsInJson": "Keine Hosts in JSON-Datei gefunden",
"maxHostsAllowed": "Maximal 100 Hosts pro Import erlaubt",
"importCompleted": "Import abgeschlossen: {{success}} erfolgreich, {{failed}} fehlgeschlagen",
"importCreated": "erstellt",
"importUpdated": "aktualisiert",
"importFailedCount": "fehlgeschlagen",
"importSkipExisting": "Import (Bestehende überspringen)",
"importOverwriteExisting": "Import (Überschreiben vorhanden)",
"importFailed": "Import fehlgeschlagen",
"importError": "Importfehler",
"failedToImportJson": "Import der JSON-Datei fehlgeschlagen",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "Prüfen, ob der Server online oder offline ist",
"statusCheckInterval": "Statusüberprüfungsintervall",
"statusCheckIntervalDesc": "Wie oft wird überprüft, ob der Host online ist (5s - 1h)",
"useGlobalStatusInterval": "Globale Standard verwenden",
"useGlobalMetricsInterval": "Globale Standard verwenden",
"usingGlobalDefault": "Globale Standardeinstellung verwenden ({{value}}s)",
"metricsEnabled": "Metrik-Überwachung aktivieren",
"metricsEnabledDesc": "Erfassen Sie CPU, RAM, Festplatte und andere Systemstatistiken",
"metricsInterval": "Metrik-Sammlungsintervall",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "SH Passwort automatisch einfügen, wenn sudo nach Passwort gefragt wird",
"sudoPassword": "Sudo Passwort",
"sudoPasswordDesc": "Optionales Passwort für sudo-Befehle (nützlich bei der Schlüsselauthentifizierung)",
"keepaliveSettings": "SSH lebendig",
"keepaliveSettingsDesc": "Konfigurieren Sie das Verhalten auf SSH-Niveau. Setzen Sie das Intervall auf 0, um SSH keepalives zu deaktivieren (nützlich für MikroTik und andere Geräte, die keepalive Requests ignorieren).",
"keepaliveInterval": "Keepalive-Intervall (ms)",
"keepaliveIntervalDesc": "Wie oft man SSH keepalive Pings sendet. Standard: 30000 (30s). Auf 0 setzen, um zu deaktivieren.",
"keepaliveCountMax": "Keepalive Max Fehler",
"keepaliveCountMaxDesc": "Trenne die Verbindung nach diesen vielen unbeantworteten keepalives. Standard: 3.",
"socks4": "SOCKS4",
"socks5": "SOCKS5",
"httpConnect": "HTTP-CONNECT",
"testProxy": "Verbindung testen",
"testingProxy": "Testen...",
"proxyTestSuccess": "Proxy-Verbindung erfolgreich ({{latency}}ms)",
"proxyTestFailed": "Proxy-Test fehlgeschlagen: {{error}}",
"connectionPath": "Verbindungspfad",
"executeSnippetOnConnect": "Snippet beim Verbinden des Terminals ausfhren",
"autoMosh": "Auto-MOSH",
"autoMoshDesc": "MOSH Befehl automatisch beim Verbinden ausführen",
@@ -1284,7 +1320,35 @@
"showTimestamps": "Zeitstempel anzeigen",
"autoRefresh": "Auto-Aktualisierung",
"filterLogsPlaceholder": "Protokolle filtern...",
"noLogsAvailable": "Keine Logs verfügbar"
"noLogsAvailable": "Keine Logs verfügbar",
"selectMode": "Auswählen",
"exitSelectMode": "Abbrechen",
"selectedCount": "{{count}} ausgewählt",
"bulkMonitoring": "Überwachung",
"bulkFeatures": "Eigenschaften",
"bulkMoveFolder": "In Ordner verschieben",
"bulkPin": "Pin",
"bulkUnpin": "Unpin",
"enableAllFeatures": "Alle Funktionen aktivieren",
"disableAllFeatures": "Alle Funktionen deaktivieren",
"enableStatusCheck": "Statusprüfung aktivieren",
"disableStatusCheck": "Statusprüfung deaktivieren",
"enableMetrics": "Metriken aktivieren",
"disableMetrics": "Metriken deaktivieren",
"bulkEnableTerminal": "Terminal aktivieren",
"bulkDisableTerminal": "Terminal deaktivieren",
"bulkEnableTunnel": "Tunnel aktivieren",
"bulkDisableTunnel": "Tunnel deaktivieren",
"bulkEnableFileManager": "Dateimanager aktivieren",
"bulkDisableFileManager": "Dateimanager deaktivieren",
"bulkEnableDocker": "Docker aktivieren",
"bulkDisableDocker": "Docker deaktivieren",
"bulkUpdateSuccess": "{{count}} Host(s) erfolgreich aktualisiert",
"bulkUpdateFailed": "Massenaktualisierung fehlgeschlagen",
"selectAll": "Alles auswählen",
"deselectAll": "Alle abwählen",
"useGlobalStatusDefault": "Globale Standardeinstellung verwenden (Status)",
"useGlobalMetricsDefault": "Globale Standardeinstellung (Metriks) verwenden"
},
"terminal": {
"title": "Terminal",
@@ -1306,6 +1370,8 @@
"connectionClosed": "Verbindung geschlossen",
"connectionError": "Verbindungsfehler: {{message}}",
"connected": "Verbunden",
"clipboardWriteFailed": "Fehler beim Kopieren in die Zwischenablage. Stellen Sie sicher, dass die Seite über HTTPS oder localhost ausgeliefert wird.",
"clipboardReadFailed": "Lesen aus der Zwischenablage fehlgeschlagen. Stellen Sie sicher, dass die Zwischenablage Berechtigungen gewährt werden.",
"sshConnected": "SSH-Verbindung hergestellt",
"authError": "Authentifizierung fehlgeschlagen: {{message}}",
"unknownError": "Unbekannter Fehler aufgetreten",
@@ -1360,7 +1426,9 @@
"totpTimeout": "TOTP-Verifizierungs-Timeout. Bitte erneut verbinden.",
"passwordTimeout": "Timeout zur Passwortüberprüfung. Bitte erneut verbinden.",
"connectionRejected": "Verbindung vom Server abgelehnt. Bitte überprüfen Sie Ihre Authentifizierung und Netzwerkkonfiguration.",
"hostKeyRejected": "Überprüfung des SSH-Host-Schlüssels abgelehnt. Verbindung abgebrochen."
"hostKeyRejected": "Überprüfung des SSH-Host-Schlüssels abgelehnt. Verbindung abgebrochen.",
"sessionTakenOver": "Sitzung wurde in einem anderen Tab geöffnet. Wiederverbinden...",
"sessionAttachTimeout": "Zeitüberschreitung beim Anhang. Neue Verbindung wird erstellt..."
},
"fileManager": {
"title": "Datei-Manager",
@@ -1670,7 +1738,13 @@
"write": "Schreiben",
"execute": "Ausführen",
"permissionsChangedSuccessfully": "Berechtigungen erfolgreich geändert",
"failedToChangePermissions": "Fehler beim Ändern der Berechtigungen"
"failedToChangePermissions": "Fehler beim Ändern der Berechtigungen",
"name": "Name",
"sortByName": "Name",
"sortByDate": "Änderungsdatum",
"sortBySize": "Größe",
"ascending": "Aufsteigend",
"descending": "Absteigend"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "Anmelden",
"registerButton": "Registrieren",
"forgotPassword": "Passwort vergessen?",
"rememberMe": "Erinnere mich",
"rememberMe": "Gerät für 30 Tage merken (inklusive TOTP)",
"noAccount": "Sie haben noch kein Konto?",
"hasAccount": "Haben Sie bereits ein Konto?",
"loginSuccess": "Anmeldung erfolgreich",
@@ -2023,6 +2097,7 @@
"processing": "Verarbeitung...",
"pleaseWait": "Bitte warten...",
"registrationDisabled": "Die Registrierung eines neuen Kontos ist derzeit von einem Administrator deaktiviert. Bitte melden Sie sich an oder kontaktieren Sie einen Administrator.",
"userNotAllowed": "Ihr Konto ist nicht berechtigt, sich zu registrieren. Bitte kontaktieren Sie einen Administrator.",
"databaseConnected": "Datenbank erfolgreich verbunden",
"databaseConnectionFailed": "Verbindung zum Datenbankserver fehlgeschlagen",
"checkServerConnection": "Bitte überprüfe deine Serververbindung und versuche es erneut",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "Wählen Sie das Farbschema für die Anwendung",
"terminalSyntaxHighlightingDesc": "Hervorheben von Befehlen, Pfaden, IPs und Log-Ebenen in Terminal-Ausgabe",
"enableCommandPaletteShortcut": "Tastenkürzel für Kommandopalette aktivieren",
"enableCommandPaletteShortcutDesc": "Doppeltippen Sie nach links, um die Befehlspalette zu öffnen, um schnell auf Hosts zuzugreifen"
"enableCommandPaletteShortcutDesc": "Doppeltippen Sie nach links, um die Befehlspalette zu öffnen, um schnell auf Hosts zuzugreifen",
"enableTerminalSessionPersistence": "Dauerhafte Terminalsitzungen",
"enableTerminalSessionPersistenceDesc": "SSH-Verbindungen beim Wechseln von Tabs oder Schließen des Browsers beibehalten (kann instabil sein)"
},
"user": {
"failedToLoadVersionInfo": "Fehler beim Laden der Versionsinformationen"
@@ -2114,6 +2191,7 @@
"usernameField": "name",
"scopes": "openid E-Mail-Profil",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"allowedUsers": "user@beispiel.com, @firma.com",
"enterUsername": "Benutzernamen eingeben um Admin zu machen",
"searchHosts": "Hosts nach Namen, Benutzernamen, IP, Ordner, Tags suchen...",
"enterPassword": "Geben Sie Ihr Passwort ein",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "2-Τρόπος",
"threeSplit": "3-Τρόπος",
"fourSplit": "4-Τρόπος",
"fiveSplit": "5-Τρόπος",
"sixSplit": "6-Τρόπος",
"availableTabs": "Διαθέσιμες Καρτέλες",
"dragTabsHint": "Σύρετε τις καρτέλες στα παρακάτω κελιά διάταξης για να τις αντιστοιχίσετε",
"layout": "Διαίρεση Διάταξης Οθόνης",
@@ -372,7 +374,11 @@
"warning": "Προειδοποίηση",
"notValidatedWarning": "Το URL δεν επικυρώθηκε - βεβαιωθείτε ότι είναι σωστό",
"changeServer": "Αλλαγή Διακομιστή",
"mustIncludeProtocol": "Η διεύθυνση URL του διακομιστή πρέπει να ξεκινά με http:// ή http://"
"mustIncludeProtocol": "Η διεύθυνση URL του διακομιστή πρέπει να ξεκινά με http:// ή http://",
"useEmbedded": "Χρήση Τοπικού Διακομιστή",
"embeddedDesc": "Εκτέλεση Termix με τον ενσωματωμένο τοπικό διακομιστή (δεν απαιτείται απομακρυσμένος διακομιστής)",
"embeddedConnecting": "Σύνδεση στον τοπικό διακομιστή...",
"embeddedNotReady": "Ο τοπικός διακομιστής δεν είναι έτοιμος ακόμα. Παρακαλώ περιμένετε λίγο και προσπαθήστε ξανά."
},
"versionCheck": {
"error": "Σφάλμα Ελέγχου Έκδοσης",
@@ -471,6 +477,7 @@
"confirm": "Επιβεβαίωση",
"yes": "Ναι",
"no": "Όχι",
"or": "Ή",
"ok": "Εντάξει",
"enabled": "Ενεργοποιημένο",
"disabled": "Απενεργοποιημένο",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "Ο χρήστης {{username}} διαγράφηκε επιτυχώς",
"failedToDeleteUser": "Αποτυχία διαγραφής χρήστη",
"overrideUserInfoUrl": "Παράκαμψη URL Πληροφοριών Χρήστη (δεν απαιτείται)",
"allowedUsers": "Επιτρεπόμενοι Χρήστες",
"allowedUsersDescription": "Λίστα επιτρεπόμενων προτύπων ηλεκτρονικού ταχυδρομείου χωρισμένη με κόμμα. Χρησιμοποιήστε ακριβή μηνύματα ηλεκτρονικού ταχυδρομείου (user@example.com), επίθεμα τομέα (@example.com) ή αφήστε κενό για να επιτρέψετε σε όλους τους χρήστες.",
"failedToFetchSessions": "Αποτυχία ανάκτησης συνεδριών",
"sessionRevokedSuccessfully": "Η συνεδρία ανακλήθηκε με επιτυχία",
"failedToRevokeSession": "Αποτυχία ανάκλησης συνεδρίας",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "CRITICAL: Η σύνδεση με τον κωδικό πρόσβασης είναι απενεργοποιημένη. Αν επαναφέρετε ή ρυθμίσετε εσφαλμένα το OIDC, θα χάσετε όλη την πρόσβαση στο Termix και θα βγάλετε την παρουσία σας. Να προχωρήσετε μόνο αν είστε απολύτως σίγουροι.",
"confirmDisableOIDCWarning": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Πρόκειται να απενεργοποιήσετε το OIDC ενώ ο κωδικός πρόσβασης είναι επίσης απενεργοποιημένος. Αυτό θα τούβλο Termix παράδειγμα σας και θα χάσετε όλη την πρόσβαση. Είστε απολύτως σίγουροι ότι θέλετε να προχωρήσετε?",
"failedToUpdatePasswordLoginStatus": "Αποτυχία ενημέρωσης κατάστασης σύνδεσης κωδικού πρόσβασης",
"monitoringDefaults": "Προεπιλογές Παρακολούθησης",
"monitoringDefaultsDesc": "Καθολικά προεπιλεγμένα διαστήματα για τους ελέγχους κατάστασης και τη συλλογή μετρήσεων. Οι ρυθμίσεις Per-host υπερισχύουν αυτών των τιμών.",
"globalStatusCheckInterval": "Προεπιλεγμένο Διάστημα Ελέγχου Κατάστασης",
"globalMetricsInterval": "Προεπιλεγμένο Διάστημα Συλλογής Μετρήσεων",
"globalSettingsSaved": "Οι καθολικές ρυθμίσεις παρακολούθησης αποθηκεύτηκαν",
"failedToSaveGlobalSettings": "Αποτυχία αποθήκευσης καθολικών ρυθμίσεων παρακολούθησης",
"failedToLoadGlobalSettings": "Αποτυχία φόρτωσης καθολικών ρυθμίσεων παρακολούθησης",
"loadingSessions": "Φόρτωση συνεδριών...",
"noActiveSessions": "Δεν βρέθηκαν ενεργές συνεδρίες.",
"device": "Συσκευή",
@@ -841,6 +857,11 @@
"noHostsInJson": "Δεν βρέθηκαν υπολογιστές στο αρχείο JSON",
"maxHostsAllowed": "Μέγιστο επιτρεπόμενο 100 hosts ανά εισαγωγή",
"importCompleted": "Η εισαγωγή ολοκληρώθηκε: {{success}} επιτυχής, {{failed}} απέτυχε",
"importCreated": "δημιουργήθηκε",
"importUpdated": "ενημερώθηκε",
"importFailedCount": "απέτυχε",
"importSkipExisting": "Εισαγωγή (παράλειψη υπάρχει)",
"importOverwriteExisting": "Εισαγωγή (αντικατάσταση υπάρχουν)",
"importFailed": "Η εισαγωγή απέτυχε",
"importError": "Σφάλμα εισαγωγής",
"failedToImportJson": "Αποτυχία εισαγωγής αρχείου JSON",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "Ελέγξτε αν ο διακομιστής είναι συνδεδεμένος ή εκτός σύνδεσης",
"statusCheckInterval": "Διάστημα Ελέγχου Κατάστασης",
"statusCheckIntervalDesc": "Πόσο συχνά πρέπει να ελέγξετε αν ο οικοδεσπότης είναι σε απευθείας σύνδεση (5s - 1h)",
"useGlobalStatusInterval": "Χρήση καθολικής προεπιλογής",
"useGlobalMetricsInterval": "Χρήση καθολικής προεπιλογής",
"usingGlobalDefault": "Χρήση καθολικής προεπιλογής ({{value}}s)",
"metricsEnabled": "Ενεργοποίηση Παρακολούθησης Μετρήσεων",
"metricsEnabledDesc": "Συλλογή CPU, RAM, δίσκου και άλλων στατιστικών συστημάτων",
"metricsInterval": "Διάστημα Συλλογής Μετρήσεων",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "Αυτόματη προσφορά για εισαγωγή κωδικού πρόσβασης SSH όταν το σχετικό αίτημα για κωδικό πρόσβασης",
"sudoPassword": "Κωδικός Πρόσβασης Sudo",
"sudoPasswordDesc": "Προαιρετικός κωδικός πρόσβασης για εντολές sudo (χρήσιμο με έλεγχο ταυτότητας κλειδιών)",
"keepaliveSettings": "SSH Keepalive",
"keepaliveSettingsDesc": "Ρυθμίστε το χρονικό διάστημα σε 0 για να απενεργοποιήσετε τις κάρτες SSH (χρήσιμο για το MikroTik και άλλες συσκευές που αγνοούν τα αιτήματα keepalive).",
"keepaliveInterval": "Διάστημα Διατροφής (ms)",
"keepaliveIntervalDesc": "Πόσο συχνά στέλνει SSH keepalive pings. Προεπιλογή: 30000 (30s). Ρυθμίστε σε 0 για απενεργοποίηση.",
"keepaliveCountMax": "Μέγιστες Αποτυχίες",
"keepaliveCountMaxDesc": "Αποσύνδεση μετά από τόσα πολλά αναπάντητα κρατά. Προεπιλογή: 3.",
"socks4": "SOCKS4",
"socks5": "SOCKS5",
"httpConnect": "ΣΥΝΔΕΣΗ HTTP",
"testProxy": "Δοκιμή Σύνδεσης",
"testingProxy": "Δοκιμή...",
"proxyTestSuccess": "Επιτυχής σύνδεση διακομιστή μεσολάβησης ({{latency}}ms)",
"proxyTestFailed": "Η δοκιμή διακομιστή μεσολάβησης απέτυχε: {{error}}",
"connectionPath": "Διαδρομή Σύνδεσης",
"executeSnippetOnConnect": "Εκτέλεση ενός αποσπάσματος όταν συνδεθεί το τερματικό",
"autoMosh": "Αυτόματο-MOSH",
"autoMoshDesc": "Αυτόματη εκτέλεση της εντολής MOSH κατά τη σύνδεση",
@@ -1284,7 +1320,35 @@
"showTimestamps": "Εμφάνιση Χρονοσημάνσεων",
"autoRefresh": "Αυτόματη Ανανέωση",
"filterLogsPlaceholder": "Φιλτράρισμα αρχείων καταγραφής...",
"noLogsAvailable": "Δεν υπάρχουν διαθέσιμα αρχεία καταγραφής"
"noLogsAvailable": "Δεν υπάρχουν διαθέσιμα αρχεία καταγραφής",
"selectMode": "Επιλογή",
"exitSelectMode": "Ακύρωση",
"selectedCount": "επιλεγμένο {{count}}",
"bulkMonitoring": "Παρακολούθηση",
"bulkFeatures": "Χαρακτηριστικά",
"bulkMoveFolder": "Μετακίνηση στο φάκελο",
"bulkPin": "Καρφίτσα",
"bulkUnpin": "Unpin",
"enableAllFeatures": "Ενεργοποίηση Όλων Των Χαρακτηριστικών",
"disableAllFeatures": "Απενεργοποίηση Όλων Των Χαρακτηριστικών",
"enableStatusCheck": "Ενεργοποίηση Ελέγχου Κατάστασης",
"disableStatusCheck": "Απενεργοποίηση Ελέγχου Κατάστασης",
"enableMetrics": "Ενεργοποίηση Μετρήσεων",
"disableMetrics": "Απενεργοποίηση Μετρήσεων",
"bulkEnableTerminal": "Ενεργοποίηση Τερματικού",
"bulkDisableTerminal": "Απενεργοποίηση Τερματικού",
"bulkEnableTunnel": "Ενεργοποίηση Tunnel",
"bulkDisableTunnel": "Απενεργοποίηση Του Tunnel",
"bulkEnableFileManager": "Ενεργοποίηση Διαχειριστή Αρχείων",
"bulkDisableFileManager": "Απενεργοποίηση Διαχείρισης Αρχείων",
"bulkEnableDocker": "Ενεργοποίηση Docker",
"bulkDisableDocker": "Απενεργοποίηση Προσάρτησης",
"bulkUpdateSuccess": "Ενημερώθηκαν με επιτυχία οι {{count}} κεντρικοί υπολογιστές",
"bulkUpdateFailed": "Η μαζική ενημέρωση απέτυχε",
"selectAll": "Επιλογή όλων",
"deselectAll": "Αποεπιλογή όλων",
"useGlobalStatusDefault": "Χρήση Καθολικής Προεπιλογής (Κατάσταση)",
"useGlobalMetricsDefault": "Χρήση Καθολικής Προεπιλογής (Μετρήσεις)"
},
"terminal": {
"title": "Τερματικό",
@@ -1306,6 +1370,8 @@
"connectionClosed": "Η σύνδεση έκλεισε",
"connectionError": "Σφάλμα σύνδεσης: {{message}}",
"connected": "Συνδεδεμένο",
"clipboardWriteFailed": "Αποτυχία αντιγραφής στο πρόχειρο. Βεβαιωθείτε ότι η σελίδα εξυπηρετείται μέσω HTTPS ή localhost.",
"clipboardReadFailed": "Αποτυχία ανάγνωσης από το πρόχειρο. Βεβαιωθείτε ότι έχουν χορηγηθεί δικαιώματα πρόχειρου.",
"sshConnected": "Η σύνδεση SSH δημιουργήθηκε",
"authError": "Η πιστοποίηση απέτυχε: {{message}}",
"unknownError": "Προέκυψε άγνωστο σφάλμα",
@@ -1360,7 +1426,9 @@
"totpTimeout": "Λήξη χρονικού ορίου επαλήθευσης TOTP. Επανασύνδεση.",
"passwordTimeout": "Λήξη χρονικού ορίου επαλήθευσης κωδικού πρόσβασης. Επανασύνδεση.",
"connectionRejected": "Η σύνδεση απορρίφθηκε από το διακομιστή. Παρακαλώ ελέγξτε τον έλεγχο ταυτότητας και τις ρυθμίσεις δικτύου.",
"hostKeyRejected": "Η επαλήθευση SSH κεντρικού υπολογιστή απορρίφθηκε. Η σύνδεση ακυρώθηκε."
"hostKeyRejected": "Η επαλήθευση SSH κεντρικού υπολογιστή απορρίφθηκε. Η σύνδεση ακυρώθηκε.",
"sessionTakenOver": "Η συνεδρία άνοιξε σε άλλη καρτέλα. Επανασύνδεση...",
"sessionAttachTimeout": "Το συνημμένο περιόδου σύνδεσης έληξε. Δημιουργία νέας σύνδεσης..."
},
"fileManager": {
"title": "Διαχειριστής Αρχείων",
@@ -1670,7 +1738,13 @@
"write": "Εγγραφή",
"execute": "Εκτέλεση",
"permissionsChangedSuccessfully": "Τα δικαιώματα άλλαξαν επιτυχώς",
"failedToChangePermissions": "Αποτυχία αλλαγής δικαιωμάτων"
"failedToChangePermissions": "Αποτυχία αλλαγής δικαιωμάτων",
"name": "Όνομα",
"sortByName": "Όνομα",
"sortByDate": "Ημερομηνία Τροποποίησης",
"sortBySize": "Μέγεθος",
"ascending": "Αύξουσα",
"descending": "Φθίνουσα"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "Είσοδος",
"registerButton": "Εγγραφή",
"forgotPassword": "Ξεχάσατε Τον Κωδικό Πρόσβασης?",
"rememberMe": "Να Με Θυμάσαι",
"rememberMe": "Απομνημόνευση συσκευής για 30 ημέρες (περιλαμβάνει TOTP)",
"noAccount": "Δεν έχετε λογαριασμό?",
"hasAccount": "Έχετε ήδη λογαριασμό?",
"loginSuccess": "Επιτυχής σύνδεση",
@@ -2023,6 +2097,7 @@
"processing": "Επεξεργασία...",
"pleaseWait": "Παρακαλώ περιμένετε...",
"registrationDisabled": "Νέα εγγραφή λογαριασμού απενεργοποιείται από έναν διαχειριστή. Παρακαλούμε συνδεθείτε ή επικοινωνήστε με το διαχειριστή.",
"userNotAllowed": "Ο λογαριασμός σας δεν είναι εξουσιοδοτημένος να εγγραφεί. Επικοινωνήστε με το διαχειριστή.",
"databaseConnected": "Η βάση δεδομένων συνδέθηκε επιτυχώς",
"databaseConnectionFailed": "Αποτυχία σύνδεσης με το διακομιστή βάσης δεδομένων",
"checkServerConnection": "Παρακαλώ ελέγξτε τη σύνδεση του διακομιστή σας και προσπαθήστε ξανά",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "Επιλέξτε το χρώμα θέματος για την εφαρμογή",
"terminalSyntaxHighlightingDesc": "Αυτόματη επισήμανση εντολών, μονοπατιών, IP και επιπέδων καταγραφής στην έξοδο τερματικού",
"enableCommandPaletteShortcut": "Ενεργοποίηση Συντόμευσης Παλέτας Εντολών",
"enableCommandPaletteShortcutDesc": "Διπλό χτύπημα αριστερά Shift για άνοιγμα της παλέτας εντολών για γρήγορη πρόσβαση στους υπολογιστές"
"enableCommandPaletteShortcutDesc": "Διπλό χτύπημα αριστερά Shift για άνοιγμα της παλέτας εντολών για γρήγορη πρόσβαση στους υπολογιστές",
"enableTerminalSessionPersistence": "Επίμονες Συνεδρίες Τερματικού",
"enableTerminalSessionPersistenceDesc": "Διατήρηση συνδέσεων SSH κατά την εναλλαγή καρτελών ή το κλείσιμο του προγράμματος περιήγησης (μπορεί να είναι ασταθές)"
},
"user": {
"failedToLoadVersionInfo": "Αποτυχία φόρτωσης πληροφοριών έκδοσης"
@@ -2114,6 +2191,7 @@
"usernameField": "όνομα",
"scopes": "άνοιγμα προφίλ email",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"allowedUsers": "user@example.com, @company.com",
"enterUsername": "Εισάγετε όνομα χρήστη για δημιουργία διαχειριστή",
"searchHosts": "Αναζήτηση υπολογιστών με όνομα, όνομα χρήστη, IP, φάκελο, ετικέτες...",
"enterPassword": "Εισάγετε τον κωδικό σας",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "2-Vía",
"threeSplit": "3-Vía",
"fourSplit": "4-Vía",
"fiveSplit": "5-Vía",
"sixSplit": "6-Vía",
"availableTabs": "Pestañas disponibles",
"dragTabsHint": "Arrastra las pestañas a las celdas de diseño de abajo para asignarlas",
"layout": "Dividir diseño de pantalla",
@@ -372,7 +374,11 @@
"warning": "Advertencia",
"notValidatedWarning": "URL no validada - asegúrese de que es correcta",
"changeServer": "Cambiar servidor",
"mustIncludeProtocol": "La URL del servidor debe comenzar con http:// o https://"
"mustIncludeProtocol": "La URL del servidor debe comenzar con http:// o https://",
"useEmbedded": "Usar servidor local",
"embeddedDesc": "Ejecutar Termix con el servidor local integrado (no se necesita un servidor remoto)",
"embeddedConnecting": "Conectando con el servidor local...",
"embeddedNotReady": "El servidor local aún no está listo. Por favor, espere un momento y vuelva a intentarlo."
},
"versionCheck": {
"error": "Error de comprobación de versión",
@@ -471,6 +477,7 @@
"confirm": "Confirmar",
"yes": "Sí",
"no": "Nu",
"or": "O",
"ok": "Ok",
"enabled": "Activado",
"disabled": "Deshabilitado",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "Usuario {{username}} eliminado correctamente",
"failedToDeleteUser": "Error al eliminar el usuario",
"overrideUserInfoUrl": "Reemplazar URL de información de usuario (no requerido)",
"allowedUsers": "Usuarios Permitidos",
"allowedUsersDescription": "Lista separada por comas de patrones de correo electrónico permitidos. Utilice correos electrónicos exactos (user@example.com), sufijos de dominio (@example.com), o deje en blanco para permitir a todos los usuarios.",
"failedToFetchSessions": "Error al obtener las sesiones",
"sessionRevokedSuccessfully": "Sesión revocada con éxito",
"failedToRevokeSession": "Error al revocar la sesión",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "CRITICAL: El inicio de sesión de la contraseña está desactivado. Si restableces o configuras OIDC, perderás todo el acceso a Termix y bloquearás tu instancia. Sólo proceda si usted está absolutamente seguro.",
"confirmDisableOIDCWarning": "ATENCIÓN: Está a punto de desactivar OIDC mientras que el inicio de sesión de la contraseña también está desactivado. Esto bloqueará su instancia de Termix y perderá todo el acceso. ¿Está seguro de que desea continuar?",
"failedToUpdatePasswordLoginStatus": "Error al actualizar el estado de acceso a la contraseña",
"monitoringDefaults": "Monitoreo por defecto",
"monitoringDefaultsDesc": "Intervalos globales por defecto para comprobaciones de estado y recolección de métricas. Por configuración de host anulan estos valores.",
"globalStatusCheckInterval": "Intervalo de verificación de estado por defecto",
"globalMetricsInterval": "Intervalo por defecto de la colección de métricas",
"globalSettingsSaved": "Ajustes globales de monitoreo guardados",
"failedToSaveGlobalSettings": "Error al guardar la configuración global de monitoreo",
"failedToLoadGlobalSettings": "Error al cargar la configuración global de monitoreo",
"loadingSessions": "Cargando sesiones...",
"noActiveSessions": "No se encontraron sesiones activas.",
"device": "Dispositivo",
@@ -841,6 +857,11 @@
"noHostsInJson": "No hay hosts en el archivo JSON",
"maxHostsAllowed": "Máximo 100 hosts permitidos por importación",
"importCompleted": "Importación completada: {{success}} exitoso, {{failed}} falló",
"importCreated": "creado",
"importUpdated": "actualizado",
"importFailedCount": "falló",
"importSkipExisting": "Importar (saltar existente)",
"importOverwriteExisting": "Importar (sobrescribir existente)",
"importFailed": "Error al importar",
"importError": "Error al importar",
"failedToImportJson": "Error al importar el archivo JSON",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "Comprobar si el servidor está conectado o fuera de línea",
"statusCheckInterval": "Intervalo de verificación de estado",
"statusCheckIntervalDesc": "Con qué frecuencia comprobar si el host está en línea (5s - 1h)",
"useGlobalStatusInterval": "Usar global por defecto",
"useGlobalMetricsInterval": "Usar global por defecto",
"usingGlobalDefault": "Uso global por defecto ({{value}}s)",
"metricsEnabled": "Habilitar monitoreo de métricas",
"metricsEnabledDesc": "Recoge CPU, RAM, disco y otras estadísticas del sistema",
"metricsInterval": "Intervalo de recolección de métricas",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "Ofrecer automáticamente para insertar la contraseña SSH cuando sudo solicita la contraseña",
"sudoPassword": "Contraseña Sudo",
"sudoPasswordDesc": "Contraseña opcional para comandos sudo (útil con autenticación de clave)",
"keepaliveSettings": "SSH Keepalive",
"keepaliveSettingsDesc": "Configurar el comportamiento keepalive a nivel SSH. Establecer el intervalo a 0 para desactivar keepalives SSSH (útil para MikroTik y otros dispositivos que ignoran las solicitudes keepalive).",
"keepaliveInterval": "Intervalo Keepalive (ms)",
"keepaliveIntervalDesc": "Con qué frecuencia enviar pings de SSH keepalive. Por defecto: 30000 (30s). Establecer en 0 para desactivar.",
"keepaliveCountMax": "Fallos máximos guardados",
"keepaliveCountMaxDesc": "Desconecte después de esta cantidad de keepalives sin respuesta. Por defecto: 3.",
"socks4": "SOCKS4",
"socks5": "SOCKS5",
"httpConnect": "CONECTO HTTP",
"testProxy": "Probar conexión",
"testingProxy": "Probando...",
"proxyTestSuccess": "Conexión proxy correcta ({{latency}}ms)",
"proxyTestFailed": "Prueba de proxy fallida: {{error}}",
"connectionPath": "Ruta de conexión",
"executeSnippetOnConnect": "Ejecutar un fragmento cuando el terminal se conecta",
"autoMosh": "Auto-MOSH",
"autoMoshDesc": "Ejecutar automáticamente el comando MOSH al conectar",
@@ -1284,7 +1320,35 @@
"showTimestamps": "Mostrar marcas de tiempo",
"autoRefresh": "Actualización automática",
"filterLogsPlaceholder": "Filtrar registros...",
"noLogsAvailable": "No hay registros disponibles"
"noLogsAvailable": "No hay registros disponibles",
"selectMode": "Seleccionar",
"exitSelectMode": "Cancelar",
"selectedCount": "{{count}} seleccionado",
"bulkMonitoring": "Monitoreo",
"bulkFeatures": "Características",
"bulkMoveFolder": "Mover a la carpeta",
"bulkPin": "Fijar",
"bulkUnpin": "Unpin",
"enableAllFeatures": "Habilitar todas las características",
"disableAllFeatures": "Desactivar todas las características",
"enableStatusCheck": "Activar verificación de estado",
"disableStatusCheck": "Desactivar verificación de estado",
"enableMetrics": "Activar métricas",
"disableMetrics": "Desactivar métricas",
"bulkEnableTerminal": "Activar Terminal",
"bulkDisableTerminal": "Desactivar Terminal",
"bulkEnableTunnel": "Activar túnel",
"bulkDisableTunnel": "Desactivar túnel",
"bulkEnableFileManager": "Habilitar gestor de archivos",
"bulkDisableFileManager": "Desactivar gestor de archivos",
"bulkEnableDocker": "Activar Docker",
"bulkDisableDocker": "Desactivar Docker",
"bulkUpdateSuccess": "Actualizado {{count}} host(s) correctamente",
"bulkUpdateFailed": "Actualización masiva fallida",
"selectAll": "Seleccionar todo",
"deselectAll": "Deseleccionar todo",
"useGlobalStatusDefault": "Usar predeterminado global (Estado)",
"useGlobalMetricsDefault": "Usar predeterminado global (métricas)"
},
"terminal": {
"title": "Terminal",
@@ -1306,6 +1370,8 @@
"connectionClosed": "Conexión cerrada",
"connectionError": "Error de conexión: {{message}}",
"connected": "Conectado",
"clipboardWriteFailed": "No se pudo copiar al portapapeles. Asegúrese de que la página se sirve sobre HTTPS o localhost.",
"clipboardReadFailed": "Error al leer desde el portapapeles. Asegúrate de que los permisos del portapapeles estén garantizados.",
"sshConnected": "Conexión SSH establecida",
"authError": "Autenticación fallida: {{message}}",
"unknownError": "Ocurrió un error desconocido",
@@ -1360,7 +1426,9 @@
"totpTimeout": "Tiempo de verificación TOTP. Por favor, vuelva a conectar.",
"passwordTimeout": "Tiempo de verificación de contraseña. Por favor, vuelva a conectar.",
"connectionRejected": "Conexión rechazada por el servidor. Por favor, compruebe su autenticación y configuración de red.",
"hostKeyRejected": "Verificación de clave del host SSH rechazada. Conexión cancelada."
"hostKeyRejected": "Verificación de clave del host SSH rechazada. Conexión cancelada.",
"sessionTakenOver": "La sesión fue abierta en otra pestaña. Volviendo a conectar...",
"sessionAttachTimeout": "Se ha agotado el tiempo de espera del archivo adjunto. Creando una nueva conexión..."
},
"fileManager": {
"title": "Gestor de archivos",
@@ -1670,7 +1738,13 @@
"write": "Escribir",
"execute": "Ejecutar",
"permissionsChangedSuccessfully": "Permisos modificados con éxito",
"failedToChangePermissions": "Error al cambiar los permisos"
"failedToChangePermissions": "Error al cambiar los permisos",
"name": "Nombre",
"sortByName": "Nombre",
"sortByDate": "Fecha Modificada",
"sortBySize": "Tamaño",
"ascending": "Ascendente",
"descending": "Descendente"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "Ingresar",
"registerButton": "Registrarse",
"forgotPassword": "¿Olvidó la contraseña?",
"rememberMe": "Recordarme",
"rememberMe": "Recordar dispositivo durante 30 días (incluye TOTP)",
"noAccount": "¿No tienes una cuenta?",
"hasAccount": "¿Ya tienes una cuenta?",
"loginSuccess": "Inicio de sesión correcto",
@@ -2023,6 +2097,7 @@
"processing": "Procesando...",
"pleaseWait": "Por favor espere...",
"registrationDisabled": "El registro de nueva cuenta está actualmente desactivado por un administrador. Por favor, inicie sesión o póngase en contacto con un administrador.",
"userNotAllowed": "Tu cuenta no está autorizada a registrarse. Por favor, contacta con un administrador.",
"databaseConnected": "Base de datos conectada con éxito",
"databaseConnectionFailed": "Error al conectar al servidor de base de datos",
"checkServerConnection": "Por favor, comprueba la conexión de tu servidor e inténtalo de nuevo",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "Seleccione el tema de color para la aplicación",
"terminalSyntaxHighlightingDesc": "Destacar automáticamente comandos, rutas, direcciones IP y niveles de registro en la salida del terminal",
"enableCommandPaletteShortcut": "Habilitar acceso directo a la paleta de comandos",
"enableCommandPaletteShortcutDesc": "Toque dos veces a la izquierda para abrir la paleta de comandos para acceder rápidamente a los hosts"
"enableCommandPaletteShortcutDesc": "Toque dos veces a la izquierda para abrir la paleta de comandos para acceder rápidamente a los hosts",
"enableTerminalSessionPersistence": "Sesiones de Terminal persistentes",
"enableTerminalSessionPersistenceDesc": "Mantener conexiones SSH al cambiar pestañas o cerrar el navegador (puede ser inestable)"
},
"user": {
"failedToLoadVersionInfo": "Error al cargar la información de la versión"
@@ -2114,6 +2191,7 @@
"usernameField": "nombre",
"scopes": "perfil de correo abierto",
"userinfoUrl": "https://su-provider.com/application/o/userinfo/",
"allowedUsers": "usuario@ejemplo.com, @company.com",
"enterUsername": "Introducir nombre de usuario para hacer administrador",
"searchHosts": "Buscar hosts por nombre, nombre de usuario, IP, carpeta, etiquetas...",
"enterPassword": "Introduzca su contraseña",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "2-suuntainen",
"threeSplit": "3-tie",
"fourSplit": "4-suuntainen",
"fiveSplit": "5-suuntainen",
"sixSplit": "6-tapainen",
"availableTabs": "Saatavilla Olevat Välilehdet",
"dragTabsHint": "Vedä välilehdet alla oleviin asettelusoluihin määrittääksesi ne",
"layout": "Jaettu Ruudun Asettelu",
@@ -372,7 +374,11 @@
"warning": "Varoitus",
"notValidatedWarning": "URL-osoitetta ei ole vahvistettu - varmista, että se on oikein",
"changeServer": "Vaihda Palvelinta",
"mustIncludeProtocol": "Palvelimen URL-osoitteen on alettava http:// tai http://"
"mustIncludeProtocol": "Palvelimen URL-osoitteen on alettava http:// tai http://",
"useEmbedded": "Käytä Paikallista Palvelinta",
"embeddedDesc": "Suorita Termix sisäänrakennetulla paikallisella palvelimella (etäpalvelinta ei tarvita)",
"embeddedConnecting": "Yhdistetään paikalliseen palvelimeen...",
"embeddedNotReady": "Paikallinen palvelin ei ole vielä valmis. Odota hetki ja yritä uudelleen."
},
"versionCheck": {
"error": "Version Tarkistus Virhe",
@@ -471,6 +477,7 @@
"confirm": "Vahvista",
"yes": "Kyllä",
"no": "Ei",
"or": "TAI",
"ok": "Ok",
"enabled": "Käytössä",
"disabled": "Pois Käytöstä",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "Käyttäjä {{username}} poistettu onnistuneesti",
"failedToDeleteUser": "Käyttäjän poistaminen epäonnistui",
"overrideUserInfoUrl": "Ohita käyttäjätietojen URL-osoite (ei vaadita)",
"allowedUsers": "Sallitut Käyttäjät",
"allowedUsersDescription": "Pilkuilla erotettu lista sallituista sähköpostimalleista. Käytä tarkkaa sähköpostia (user@example.com), verkkotunnuksen loppuliitteitä (@example.com), tai jätä tyhjäksi salliaksesi kaikkien käyttäjien.",
"failedToFetchSessions": "Istuntojen noutaminen epäonnistui",
"sessionRevokedSuccessfully": "Istunto kumottu onnistuneesti",
"failedToRevokeSession": "Istunnon peruuttaminen epäonnistui",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "CRITICAL: Salasanan kirjautuminen on poistettu käytöstä. Jos nollaat tai määrität väärin OIDC, menetät kaikki pääsyn Termix ja tiili instanssi. Jatka vain, jos olet ehdottoman varma.",
"confirmDisableOIDCWarning": "VAROITUS: Olet ottamassa OIDC:n pois käytöstä samalla kun kirjautuminen on myös pois käytöstä. Tämä tiili Termix-instanssi ja menetät kaikki käyttöoikeudet. Oletko varma, että haluat jatkaa?",
"failedToUpdatePasswordLoginStatus": "Salasanan sisäänkirjautumisen tilan päivittäminen epäonnistui",
"monitoringDefaults": "Oletusten Seuranta",
"monitoringDefaultsDesc": "Yleiset oletusvälit tilatarkastuksiin ja mittausten keräämiseen. Isäntä-asetukset ohittavat nämä arvot.",
"globalStatusCheckInterval": "Oletus Tilan Tarkistuksen Aikaväli",
"globalMetricsInterval": "Metrics Kokoelman Oletusväli",
"globalSettingsSaved": "Yleiset seurantaasetukset tallennettu",
"failedToSaveGlobalSettings": "Yleisten seuranta-asetusten tallentaminen epäonnistui",
"failedToLoadGlobalSettings": "Globaalien valvontaasetusten lataaminen epäonnistui",
"loadingSessions": "Ladataan istuntoja...",
"noActiveSessions": "Aktiivisia istuntoja ei löytynyt.",
"device": "Laite",
@@ -841,6 +857,11 @@
"noHostsInJson": "JSON-tiedostossa ei löytynyt isäntiä",
"maxHostsAllowed": "Tuonnissa sallittu enintään 100 isäntää",
"importCompleted": "Tuonti valmis: {{success}} onnistui, {{failed}} epäonnistui",
"importCreated": "luotu",
"importUpdated": "päivitetty",
"importFailedCount": "epäonnistui",
"importSkipExisting": "Tuo (ohita olemassa)",
"importOverwriteExisting": "Tuo (korvaa olemassaoleva)",
"importFailed": "Tuonti epäonnistui",
"importError": "Tuontivirhe",
"failedToImportJson": "JSON-tiedoston tuonti epäonnistui",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "Tarkista, onko palvelin online-tilassa vai offline-tilassa",
"statusCheckInterval": "Tilan tarkistusväli",
"statusCheckIntervalDesc": "Kuinka usein tarkistaa, onko isäntä verkossa (5 s - 1 h)",
"useGlobalStatusInterval": "Käytä yleistä oletusta",
"useGlobalMetricsInterval": "Käytä yleistä oletusta",
"usingGlobalDefault": "Käytetään yleistä oletusta ({{value}}s)",
"metricsEnabled": "Ota käyttöön mittareiden seuranta",
"metricsEnabledDesc": "Kerää prosessorin, RAMin, levyn ja muiden järjestelmätilastojen tiedot",
"metricsInterval": "Mittarien keräysväli",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "Tarjoaa automaattisesti SSH-salasanan syöttämistä, kun sudo pyytää salasanaa",
"sudoPassword": "Sudo-salasana",
"sudoPasswordDesc": "Valinnainen salasana sudo-komennoille (hyödyllinen avaintodennuksen kanssa)",
"keepaliveSettings": "SSH Keepalive",
"keepaliveSettingsDesc": "Määritä SSH-tason keepalive käyttäytyminen. Aseta aikaväli 0 poistaaksesi SSH keepalives (hyödyllinen MikroTik ja muut laitteet, jotka jättävät huomiotta keepalive pyynnöt).",
"keepaliveInterval": "Keepalive -väli (ms)",
"keepaliveIntervalDesc": "Miten usein lähettää SSH keepalive pings. Oletus: 30000 (30s). Aseta 0 poistaa käytöstä.",
"keepaliveCountMax": "Keepalive Max Epäonnistumiset",
"keepaliveCountMaxDesc": "Katkaise yhteys tämän monen vastaamattoman avaimen jälkeen. Oletus: 3.",
"socks4": "SOCKS4",
"socks5": "SUKAT5",
"httpConnect": "HTTP YHDISTÄ",
"testProxy": "Testaa Yhteys",
"testingProxy": "Testataan...",
"proxyTestSuccess": "Välityspalvelin yhteys onnistui ({{latency}}ms)",
"proxyTestFailed": "Välityspalvelimen testi epäonnistui: {{error}}",
"connectionPath": "Yhteyden Polku",
"executeSnippetOnConnect": "Suorita koodinpätkä, kun päätelaite muodostaa yhteyden",
"autoMosh": "Auto-MOSH",
"autoMoshDesc": "Suorita MOSH-komento automaattisesti yhteyden muodostamisen yhteydessä",
@@ -1284,7 +1320,35 @@
"showTimestamps": "Näytä aikaleimat",
"autoRefresh": "Automaattinen päivitys",
"filterLogsPlaceholder": "Suodata lokit...",
"noLogsAvailable": "Ei lokeja saatavilla"
"noLogsAvailable": "Ei lokeja saatavilla",
"selectMode": "Valitse",
"exitSelectMode": "Peruuta",
"selectedCount": "{{count}} valittu",
"bulkMonitoring": "Seuranta",
"bulkFeatures": "Ominaisuudet",
"bulkMoveFolder": "Siirrä kansioon",
"bulkPin": "Kiinnitä",
"bulkUnpin": "Unpin",
"enableAllFeatures": "Ota Käyttöön Kaikki Ominaisuudet",
"disableAllFeatures": "Poista Kaikki Ominaisuudet Käytöstä",
"enableStatusCheck": "Ota Tilan Tarkistus Käyttöön",
"disableStatusCheck": "Poista Tilan Tarkistus Käytöstä",
"enableMetrics": "Ota Metrics Käyttöön",
"disableMetrics": "Poista Metrics Käytöstä",
"bulkEnableTerminal": "Ota Pääte Käyttöön",
"bulkDisableTerminal": "Poista Pääte Käytöstä",
"bulkEnableTunnel": "Ota Tunneli Käyttöön",
"bulkDisableTunnel": "Poista Tunneli Käytöstä",
"bulkEnableFileManager": "Ota Tiedostonhallinta Käyttöön",
"bulkDisableFileManager": "Poista Tiedostonhallinta Käytöstä",
"bulkEnableDocker": "Ota Docker Käyttöön",
"bulkDisableDocker": "Poista Docker Käytöstä",
"bulkUpdateSuccess": "Päivitetty {{count}} isäntä onnistuneesti",
"bulkUpdateFailed": "Massapäivitys epäonnistui",
"selectAll": "Valitse kaikki",
"deselectAll": "Poista kaikki valinnat",
"useGlobalStatusDefault": "Käytä Globaalia Oletusta (Tila)",
"useGlobalMetricsDefault": "Käytä Globaaleja Oletuksia (Metrics)"
},
"terminal": {
"title": "Pääte",
@@ -1306,6 +1370,8 @@
"connectionClosed": "Yhteys suljettu",
"connectionError": "Yhteysvirhe: {{message}}",
"connected": "Yhdistetty",
"clipboardWriteFailed": "Kopiointi leikepöydälle epäonnistui. Varmista, että sivu on tarjolla HTTPS:n tai localhostin kautta.",
"clipboardReadFailed": "Leikepöydältä ei voitu lukea. Varmista, että leikepöydän oikeudet on myönnetty.",
"sshConnected": "SSH-yhteys muodostettu",
"authError": "Todennus epäonnistui: {{message}}",
"unknownError": "Tuntematon virhe tapahtui",
@@ -1360,7 +1426,9 @@
"totpTimeout": "TOTP vahvistuksen aikakatkaisu. Yhdistä uudelleen.",
"passwordTimeout": "Salasanan varmennuksen aikakatkaisu. Yhdistä uudelleen.",
"connectionRejected": "Palvelin hylkäsi yhteyden. Tarkista todennus ja verkon asetukset.",
"hostKeyRejected": "SSH isäntäavaimen vahvistus hylätty. Yhteys peruutettu."
"hostKeyRejected": "SSH isäntäavaimen vahvistus hylätty. Yhteys peruutettu.",
"sessionTakenOver": "Istunto avattiin toisessa välilehdessä. Yhdistetään uudelleen...",
"sessionAttachTimeout": "Istunnon liite aikakatkaistiin. Uuden yhteyden luonti..."
},
"fileManager": {
"title": "Tiedostojen Hallinta",
@@ -1670,7 +1738,13 @@
"write": "Kirjoittaa",
"execute": "Suorittaa",
"permissionsChangedSuccessfully": "Käyttöoikeudet muutettu onnistuneesti",
"failedToChangePermissions": "Käyttöoikeuksien muuttaminen epäonnistui"
"failedToChangePermissions": "Käyttöoikeuksien muuttaminen epäonnistui",
"name": "Nimi",
"sortByName": "Nimi",
"sortByDate": "Muokkaus Päivämäärä",
"sortBySize": "Koko",
"ascending": "Nouseva",
"descending": "Laskeva"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "Kirjaudu",
"registerButton": "Rekisteröidy",
"forgotPassword": "Unohditko salasanasi?",
"rememberMe": "Muista minut",
"rememberMe": "Muista laite 30 päivän ajan (sisältää TOTP)",
"noAccount": "Eikö sinulla ole tiliä?",
"hasAccount": "Onko sinulla jo tili?",
"loginSuccess": "Kirjautuminen onnistui",
@@ -2023,6 +2097,7 @@
"processing": "Käsitellään...",
"pleaseWait": "Odota hetki...",
"registrationDisabled": "Ylläpitäjä on tällä hetkellä poistanut käytöstä uuden tilin rekisteröinnin. Kirjaudu sisään tai ota yhteyttä ylläpitäjään.",
"userNotAllowed": "Tililläsi ei ole oikeutta rekisteröityä. Ota yhteyttä järjestelmänvalvojaan.",
"databaseConnected": "Tietokanta yhdistetty onnistuneesti",
"databaseConnectionFailed": "Yhteyden muodostaminen tietokantapalvelimeen epäonnistui",
"checkServerConnection": "Tarkista palvelinyhteytesi ja yritä uudelleen",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "Valitse sovelluksen väriteema",
"terminalSyntaxHighlightingDesc": "Korosta automaattisesti komennot, polut, IP-osoitteet ja lokitasot päätetulosteessa",
"enableCommandPaletteShortcut": "Ota Käyttöön Komentopaletin Pikakuvake",
"enableCommandPaletteShortcutDesc": "Kaksoisnapauta vasen Shift avataksesi komentopaletin saadaksesi nopean pääsyn isäntiin"
"enableCommandPaletteShortcutDesc": "Kaksoisnapauta vasen Shift avataksesi komentopaletin saadaksesi nopean pääsyn isäntiin",
"enableTerminalSessionPersistence": "Pysyvät Pääteistunnot",
"enableTerminalSessionPersistenceDesc": "Säilytä SSH yhteydet kun välilehtiä vaihdetaan tai selain suljetaan (voi olla epävakaa)"
},
"user": {
"failedToLoadVersionInfo": "Versiotietojen lataaminen epäonnistui"
@@ -2114,6 +2191,7 @@
"usernameField": "nimi",
"scopes": "openid-sähköpostiprofiili",
"userinfoUrl": "https://palveluntarjoajasi.com/application/o/userinfo/",
"allowedUsers": "user@example.com, @company.com",
"enterUsername": "Syötä käyttäjänimi tehdäksesi järjestelmänvalvojan",
"searchHosts": "Hae isäntiä nimen, käyttäjätunnuksen, IP-osoitteen, kansion, tunnisteiden... perusteella...",
"enterPassword": "Anna salasanasi",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "2-Voie",
"threeSplit": "3-Voie",
"fourSplit": "4 Voies",
"fiveSplit": "5 Voies",
"sixSplit": "6-Voie",
"availableTabs": "Onglets disponibles",
"dragTabsHint": "Faites glisser les onglets vers les cellules de mise en page ci-dessous pour les assigner",
"layout": "Diviser la disposition de l'écran",
@@ -372,7 +374,11 @@
"warning": "Avertissement",
"notValidatedWarning": "URL non validée - assurez-vous que c'est correct",
"changeServer": "Changer de serveur",
"mustIncludeProtocol": "L'URL du serveur doit commencer par http:// ou https://"
"mustIncludeProtocol": "L'URL du serveur doit commencer par http:// ou https://",
"useEmbedded": "Utiliser le serveur local",
"embeddedDesc": "Exécuter Termix avec le serveur local intégré (aucun serveur distant nécessaire)",
"embeddedConnecting": "Connexion au serveur local...",
"embeddedNotReady": "Le serveur local n'est pas encore prêt. Veuillez patienter un instant et réessayer."
},
"versionCheck": {
"error": "Erreur de vérification de version",
@@ -471,6 +477,7 @@
"confirm": "Valider",
"yes": "Oui",
"no": "Non",
"or": "OU",
"ok": "Ok",
"enabled": "Activé",
"disabled": "Désactivé",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "Utilisateur {{username}} supprimé avec succès",
"failedToDeleteUser": "Échec de la suppression de l'utilisateur",
"overrideUserInfoUrl": "Remplacer l'URL des informations de l'utilisateur (pas obligatoire)",
"allowedUsers": "Utilisateurs autorisés",
"allowedUsersDescription": "Liste de modèles d'e-mails autorisés séparés par des virgules. Utilisez des e-mails exacts (utilisateur@exemple.com), des suffixes de domaine (@exemple.com), ou laissez vide pour autoriser tous les utilisateurs.",
"failedToFetchSessions": "Impossible de récupérer les sessions",
"sessionRevokedSuccessfully": "Session révoquée avec succès",
"failedToRevokeSession": "Échec de la révocation de la session",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "CRITIQUE : La connexion par mot de passe est désactivée. Si vous réinitialisez ou déconfigurez OIDC, vous perdrez tous les accès à Termix et brickez votre instance. Ne procédez que si vous êtes absolument certain.",
"confirmDisableOIDCWarning": "AVERTISSEMENT : Vous êtes sur le point de désactiver OIDC alors que la connexion au mot de passe est également désactivée. Cela va briquer votre instance Termix et vous perdrez tous les accès. Êtes-vous absolument sûr de vouloir continuer ?",
"failedToUpdatePasswordLoginStatus": "Impossible de mettre à jour le statut de connexion du mot de passe",
"monitoringDefaults": "Surveillance par défaut",
"monitoringDefaultsDesc": "Intervalles par défaut pour la vérification des statuts et la collecte des métriques. Les paramètres par hôte remplacent ces valeurs.",
"globalStatusCheckInterval": "Intervalle de vérification du statut par défaut",
"globalMetricsInterval": "Intervalle de collecte des métriques par défaut",
"globalSettingsSaved": "Paramètres de surveillance globale enregistrés",
"failedToSaveGlobalSettings": "Impossible d'enregistrer les paramètres de surveillance globale",
"failedToLoadGlobalSettings": "Impossible de charger les paramètres de surveillance globale",
"loadingSessions": "Chargement des sessions...",
"noActiveSessions": "Aucune session active trouvée.",
"device": "Appareil",
@@ -841,6 +857,11 @@
"noHostsInJson": "Aucun hôte trouvé dans le fichier JSON",
"maxHostsAllowed": "Maximum 100 hôtes autorisés par import",
"importCompleted": "Importation terminée : {{success}} réussie, {{failed}} a échoué",
"importCreated": "créé",
"importUpdated": "Mis à jour",
"importFailedCount": "a échoué",
"importSkipExisting": "Importer (sauter le existant)",
"importOverwriteExisting": "Importer (écraser les données existantes)",
"importFailed": "Importation échouée",
"importError": "Erreur d'importation",
"failedToImportJson": "Impossible d'importer le fichier JSON",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "Vérifier si le serveur est en ligne ou hors ligne",
"statusCheckInterval": "Intervalle de vérification de l'état",
"statusCheckIntervalDesc": "À quelle fréquence vérifier si l'hôte est en ligne (5s - 1h)",
"useGlobalStatusInterval": "Utiliser la valeur par défaut globale",
"useGlobalMetricsInterval": "Utiliser la valeur par défaut globale",
"usingGlobalDefault": "Utiliser la valeur globale par défaut ({{value}}s)",
"metricsEnabled": "Activer la surveillance des métriques",
"metricsEnabledDesc": "Collecter des statistiques CPU, RAM, disque et autres systèmes",
"metricsInterval": "Intervalle de collecte des métriques",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "Proposer automatiquement d'insérer le mot de passe SSH lorsque sudo demande le mot de passe",
"sudoPassword": "Mot de passe Sudo",
"sudoPasswordDesc": "Mot de passe optionnel pour les commandes sudo (utile avec l'authentification de la clé)",
"keepaliveSettings": "Keepalive SSH",
"keepaliveSettingsDesc": "Configurer le comportement de maintien de niveau SSH. Réglez l'intervalle à 0 pour désactiver les entretiens SSH (utile pour MikroTik et d'autres appareils qui ignorent les requêtes de maintenance).",
"keepaliveInterval": "Intervalle de Keepalive (ms)",
"keepaliveIntervalDesc": "À quelle fréquence envoyer des pings de keepalive SSH. Par défaut: 30000 (30s). Mettre à 0 pour désactiver.",
"keepaliveCountMax": "Échec de Keepalive Max",
"keepaliveCountMaxDesc": "Déconnectez après ce nombre de gardiens non répondus. Par défaut: 3.",
"socks4": "SOCKS4",
"socks5": "SOCKS5",
"httpConnect": "CONNECT HTTP",
"testProxy": "Tester la connexion",
"testingProxy": "Test en cours...",
"proxyTestSuccess": "Connexion au proxy réussie ({{latency}}ms)",
"proxyTestFailed": "Le test du proxy a échoué : {{error}}",
"connectionPath": "Chemin de connexion",
"executeSnippetOnConnect": "Exécuter un snippet lorsque le terminal se connecte",
"autoMosh": "MOSH automatique",
"autoMoshDesc": "Lancer automatiquement la commande MOSH à la connexion",
@@ -1284,7 +1320,35 @@
"showTimestamps": "Afficher les horodatages",
"autoRefresh": "Rafraîchissement automatique",
"filterLogsPlaceholder": "Filtrer les journaux...",
"noLogsAvailable": "Aucun journal disponible"
"noLogsAvailable": "Aucun journal disponible",
"selectMode": "Sélectionner",
"exitSelectMode": "Abandonner",
"selectedCount": "{{count}} sélectionné",
"bulkMonitoring": "Surveillance",
"bulkFeatures": "Fonctionnalités",
"bulkMoveFolder": "Déplacer vers le dossier",
"bulkPin": "Épingler",
"bulkUnpin": "Unpin",
"enableAllFeatures": "Activer toutes les fonctionnalités",
"disableAllFeatures": "Désactiver toutes les fonctionnalités",
"enableStatusCheck": "Activer la vérification de l'état",
"disableStatusCheck": "Désactiver la vérification de l'état",
"enableMetrics": "Activer les métriques",
"disableMetrics": "Désactiver les métriques",
"bulkEnableTerminal": "Activer le Terminal",
"bulkDisableTerminal": "Désactiver le terminal",
"bulkEnableTunnel": "Activer le tunnel",
"bulkDisableTunnel": "Désactiver le tunnel",
"bulkEnableFileManager": "Activer le gestionnaire de fichiers",
"bulkDisableFileManager": "Désactiver le gestionnaire de fichiers",
"bulkEnableDocker": "Activer Docker",
"bulkDisableDocker": "Désactiver Docker",
"bulkUpdateSuccess": "Hôte(s) {{count}} mis à jour avec succès",
"bulkUpdateFailed": "La mise à jour en bloc a échoué",
"selectAll": "Tout sélectionner",
"deselectAll": "Désélectionner tout",
"useGlobalStatusDefault": "Utiliser la valeur globale par défaut (Status)",
"useGlobalMetricsDefault": "Utiliser la valeur globale par défaut (métriques)"
},
"terminal": {
"title": "Terminal",
@@ -1306,6 +1370,8 @@
"connectionClosed": "Connexion fermée",
"connectionError": "Erreur de connexion : {{message}}",
"connected": "Connecté",
"clipboardWriteFailed": "Échec de la copie dans le presse-papiers. Assurez-vous que la page est servie via HTTPS ou localhost.",
"clipboardReadFailed": "Échec de lecture du presse-papiers. Assurez-vous que les permissions du presse-papiers sont accordées.",
"sshConnected": "Connexion SSH établie",
"authError": "Échec de l'authentification : {{message}}",
"unknownError": "Une erreur inconnue s'est produite",
@@ -1360,7 +1426,9 @@
"totpTimeout": "Délai de vérification TOTP. Veuillez vous reconnecter.",
"passwordTimeout": "Délai de vérification du mot de passe. Veuillez vous reconnecter.",
"connectionRejected": "Connexion refusée par le serveur. Veuillez vérifier votre authentification et la configuration du réseau.",
"hostKeyRejected": "La vérification de la clé d'hôte SSH a été rejetée. La connexion a été annulée."
"hostKeyRejected": "La vérification de la clé d'hôte SSH a été rejetée. La connexion a été annulée.",
"sessionTakenOver": "La session a été ouverte dans un autre onglet. Reconnexion...",
"sessionAttachTimeout": "La session a expiré. Création d'une nouvelle connexion..."
},
"fileManager": {
"title": "Gestionnaire de fichiers",
@@ -1670,7 +1738,13 @@
"write": "Écrire",
"execute": "Exécuter",
"permissionsChangedSuccessfully": "Permissions modifiées avec succès",
"failedToChangePermissions": "Impossible de modifier les autorisations"
"failedToChangePermissions": "Impossible de modifier les autorisations",
"name": "Nom",
"sortByName": "Nom",
"sortByDate": "Date de modification",
"sortBySize": "Taille",
"ascending": "Ascendant",
"descending": "Descendant"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "Se connecter",
"registerButton": "Inscription",
"forgotPassword": "Mot de passe oublié ?",
"rememberMe": "Se souvenir de moi",
"rememberMe": "Se souvenir de l'appareil pendant 30 jours (comprend TOTP)",
"noAccount": "Vous n'avez pas de compte ?",
"hasAccount": "Vous avez déjà un compte ?",
"loginSuccess": "Connexion réussie",
@@ -2023,6 +2097,7 @@
"processing": "Traitement en cours...",
"pleaseWait": "Veuillez patienter...",
"registrationDisabled": "L'enregistrement d'un nouveau compte est actuellement désactivé par un administrateur. Veuillez vous connecter ou contacter un administrateur.",
"userNotAllowed": "Votre compte n'est pas autorisé à vous inscrire. Veuillez contacter un administrateur.",
"databaseConnected": "La base de données est connectée avec succès",
"databaseConnectionFailed": "Impossible de se connecter au serveur de base de données",
"checkServerConnection": "Veuillez vérifier votre connexion au serveur et réessayer",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "Sélectionnez le thème de couleur pour l'application",
"terminalSyntaxHighlightingDesc": "Mettre automatiquement en surbrillance les commandes, chemins, IPs et niveaux de log dans la sortie du terminal",
"enableCommandPaletteShortcut": "Activer le raccourci de la palette de commandes",
"enableCommandPaletteShortcutDesc": "Tapotez deux fois vers la gauche pour ouvrir la palette de commandes pour un accès rapide aux hôtes"
"enableCommandPaletteShortcutDesc": "Tapotez deux fois vers la gauche pour ouvrir la palette de commandes pour un accès rapide aux hôtes",
"enableTerminalSessionPersistence": "Sessions terminales persistantes",
"enableTerminalSessionPersistenceDesc": "Maintenir les connexions SSH lors du changement d'onglets ou de la fermeture du navigateur (peut être instable)"
},
"user": {
"failedToLoadVersionInfo": "Impossible de charger les informations de version"
@@ -2114,6 +2191,7 @@
"usernameField": "Nom",
"scopes": "Profil e-mail openid",
"userinfoUrl": "https://votre-fournisseur.com/application/o/userinfo/",
"allowedUsers": "utilisateur@exemple.com, @entreprise.com",
"enterUsername": "Entrez le nom d'utilisateur pour devenir administrateur",
"searchHosts": "Rechercher les hôtes par nom, nom d'utilisateur, IP, répertoire, tags...",
"enterPassword": "Entrez votre mot de passe",
+84 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "דו-כיווני",
"threeSplit": "3-כיווני",
"fourSplit": "4-כיווני",
"fiveSplit": "5-כיווני",
"sixSplit": "6-כיווני",
"availableTabs": "כרטיסיות זמינות",
"dragTabsHint": "גררו טאבים לתאי הפריסה למטה כדי להקצות אותם",
"layout": "פריסת מסך מפוצל",
@@ -372,7 +374,11 @@
"warning": "אַזהָרָה",
"notValidatedWarning": "כתובת האתר לא אומתה - ודא שהיא נכונה",
"changeServer": "שנה שרת",
"mustIncludeProtocol": "כתובת השרת חייבת להתחיל ב-http:// או https://"
"mustIncludeProtocol": "כתובת השרת חייבת להתחיל ב-http:// או https://",
"useEmbedded": "השתמש בשרת מקומי",
"embeddedDesc": "הפעל את Termix עם השרת המקומי המובנה (אין צורך בשרת מרוחק)",
"embeddedConnecting": "מתחבר לשרת מקומי...",
"embeddedNotReady": "השרת המקומי עדיין לא מוכן. אנא המתן רגע ונסה שוב."
},
"versionCheck": {
"error": "שגיאת בדיקת גרסה",
@@ -471,6 +477,7 @@
"confirm": "לְאַשֵׁר",
"yes": "כֵּן",
"no": "לֹא",
"or": "אוֹ",
"ok": "בְּסֵדֶר",
"enabled": "מופעל",
"disabled": "נָכֶה",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "משתמש {{username}} נמחק בהצלחה",
"failedToDeleteUser": "מחיקת המשתמש נכשלה",
"overrideUserInfoUrl": "עקיפת כתובת URL של פרטי משתמש (לא נדרש)",
"allowedUsers": "משתמשים מורשים",
"allowedUsersDescription": "רשימה מופרדת בפסיקים של דפוסי דוא\"ל מותרים. השתמש בכתובות דוא\"ל מדויקות (user@example.com), סיומות דומיין (@example.com), או השאר ריק כדי לאפשר לכל המשתמשים.",
"failedToFetchSessions": "נכשלה אחזור הסשנים",
"sessionRevokedSuccessfully": "ההפעלה בוטלה בהצלחה",
"failedToRevokeSession": "ביטול ההפעלה נכשל",
@@ -792,6 +801,13 @@
"oidcRequiredWarning": "קריטי: כניסה באמצעות סיסמה מושבתת. אם תאפסו או תגדירו שגוי את ה-OIDC, תאבדו את כל הגישה ל-Termix ותחסמו את המופע שלכם. המשיכו רק אם אתם בטוחים לחלוטין.",
"confirmDisableOIDCWarning": "אזהרה: אתה עומד להשבית את OIDC כאשר גם כניסה באמצעות סיסמה מושבתת. פעולה זו תגרום לבלום את מופע ה-Termix שלך ותאבד את כל הגישה. האם אתה בטוח לחלוטין שברצונך להמשיך?",
"failedToUpdatePasswordLoginStatus": "נכשל עדכון סטטוס כניסה באמצעות סיסמה",
"monitoringDefaults": "ניטור ברירות מחדל",
"monitoringDefaultsDesc": "מרווחי ברירת מחדל גלובליים לבדיקות סטטוס ואיסוף מדדים. הגדרות לכל מארח גוברות על ערכים אלה.",
"globalStatusCheckInterval": "מרווח ברירת מחדל לבדיקת סטטוס",
"globalMetricsInterval": "מרווח ברירת מחדל לאיסוף מדדים",
"globalSettingsSaved": "הגדרות ניטור גלובליות נשמרו",
"failedToSaveGlobalSettings": "נכשלה שמירת הגדרות הניטור הגלובליות",
"failedToLoadGlobalSettings": "נכשלה טעינת הגדרות הניטור הגלובליות",
"loadingSessions": "טוען סשנים...",
"noActiveSessions": "לא נמצאו סשנים פעילים.",
"device": "הֶתקֵן",
@@ -841,6 +857,11 @@
"noHostsInJson": "לא נמצאו מארחים בקובץ JSON",
"maxHostsAllowed": "מקסימום 100 מארחים מותרים לכל ייבוא",
"importCompleted": "ייבוא הושלם: {{success}} הצליח, {{failed}} נכשל",
"importCreated": "נוצר",
"importUpdated": "מְעוּדכָּן",
"importFailedCount": "נִכשָׁל",
"importSkipExisting": "ייבוא (דלג על קיים)",
"importOverwriteExisting": "ייבוא (החלפת קבצים קיימים)",
"importFailed": "הייבוא נכשל",
"importError": "שגיאת ייבוא",
"failedToImportJson": "ייבוא קובץ JSON נכשל",
@@ -1017,6 +1038,9 @@
"statusCheckEnabledDesc": "בדוק אם השרת מחובר או לא מחובר",
"statusCheckInterval": "מרווח זמן לבדיקת סטטוס",
"statusCheckIntervalDesc": "באיזו תדירות לבדוק אם המארח מחובר (5 שניות - שעה)",
"useGlobalStatusInterval": "השתמש בברירת מחדל גלובלית",
"useGlobalMetricsInterval": "השתמש בברירת מחדל גלובלית",
"usingGlobalDefault": "שימוש בברירת מחדל גלובלית ({{value}}s)",
"metricsEnabled": "הפעל ניטור מדדים",
"metricsEnabledDesc": "איסוף סטטיסטיקות של מעבד, זיכרון RAM, דיסק ונתונים סטטיסטיים אחרים של המערכת",
"metricsInterval": "מרווח איסוף מדדים",
@@ -1174,8 +1198,20 @@
"sudoPasswordAutoFillDesc": "הצעה אוטומטית להזין סיסמת SSH כאשר sudo מבקש סיסמה",
"sudoPassword": "סיסמת סודו",
"sudoPasswordDesc": "סיסמה אופציונלית לפקודות sudo (שימושית עם אימות מפתח)",
"keepaliveSettings": "SSH Keepalive",
"keepaliveSettingsDesc": "הגדר התנהגות keepalive ברמת SSH. הגדר את המרווח ל-0 כדי להשבית keepalives של SSH (שימושי עבור MikroTik ומכשירים אחרים שמתעלמים מבקשות keepalive).",
"keepaliveInterval": "מרווח זמן שמירה (מילישניות)",
"keepaliveIntervalDesc": "באיזו תדירות לשלוח פינגים של SSH keepalive. ברירת מחדל: 30000 (30 שניות). הגדר ל-0 כדי להשבית.",
"keepaliveCountMax": "מקסימום כשלים בשמירה על חיים",
"keepaliveCountMaxDesc": "התנתק לאחר מספר זה של הודעות שמירה ללא מענה. ברירת מחדל: 3.",
"socks4": "גרביים4",
"socks5": "גרביים5",
"httpConnect": "חיבור HTTP",
"testProxy": "בדיקת חיבור",
"testingProxy": "בּוֹחֵן...",
"proxyTestSuccess": "חיבור פרוקסי הצליח ({{latency}}אלפיות שנייה)",
"proxyTestFailed": "בדיקת פרוקסי נכשלה: {{error}}",
"connectionPath": "נתיב החיבור",
"executeSnippetOnConnect": "בצע קטע קוד כאשר הטרמינל מתחבר",
"autoMosh": "אוטו-MOSH",
"autoMoshDesc": "הפעלת פקודת MOSH באופן אוטומטי בעת חיבור",
@@ -1284,7 +1320,35 @@
"showTimestamps": "הצג חותמות זמן",
"autoRefresh": "רענון אוטומטי",
"filterLogsPlaceholder": "סנן יומני רישום...",
"noLogsAvailable": "אין יומנים זמינים"
"noLogsAvailable": "אין יומנים זמינים",
"selectMode": "לִבחוֹר",
"exitSelectMode": "לְבַטֵל",
"selectedCount": "{{count}} נבחר",
"bulkMonitoring": "ניטור",
"bulkFeatures": "תכונות",
"bulkMoveFolder": "העבר לתיקייה",
"bulkPin": "פִּין",
"bulkUnpin": "לְהוֹצִיא סִיכָּה",
"enableAllFeatures": "הפעל את כל התכונות",
"disableAllFeatures": "השבת את כל התכונות",
"enableStatusCheck": "הפעל בדיקת סטטוס",
"disableStatusCheck": "השבתת בדיקת סטטוס",
"enableMetrics": "הפעל מדדים",
"disableMetrics": "השבת מדדים",
"bulkEnableTerminal": "הפעלת טרמינל",
"bulkDisableTerminal": "השבתת הטרמינל",
"bulkEnableTunnel": "הפעלת מנהרה",
"bulkDisableTunnel": "השבתת מנהרה",
"bulkEnableFileManager": "הפעל את מנהל הקבצים",
"bulkDisableFileManager": "השבתת מנהל הקבצים",
"bulkEnableDocker": "הפעל את Docker",
"bulkDisableDocker": "השבת את Docker",
"bulkUpdateSuccess": "מארח(ים) {{count}} עודכן/ו בהצלחה",
"bulkUpdateFailed": "עדכון בכמות גדולה נכשל",
"selectAll": "בחר הכל",
"deselectAll": "בטל את הבחירה של הכל",
"useGlobalStatusDefault": "השתמש בברירת מחדל גלובלית (סטטוס)",
"useGlobalMetricsDefault": "השתמש בברירת מחדל גלובלית (מדדים)"
},
"terminal": {
"title": "מָסוֹף",
@@ -1306,6 +1370,8 @@
"connectionClosed": "החיבור נסגר",
"connectionError": "שגיאת חיבור: {{message}}",
"connected": "מְחוּבָּר",
"clipboardWriteFailed": "ההעתקה ללוח נכשלה. ודא שהדף מוגש דרך HTTPS או localhost.",
"clipboardReadFailed": "הקריאה מהלוח נכשלה. ודא שהרשאות הלוח ניתנות.",
"sshConnected": "נוצר חיבור SSH",
"authError": "האימות נכשל: {{message}}",
"unknownError": "אירעה שגיאה לא ידועה",
@@ -1360,7 +1426,9 @@
"totpTimeout": "פסק זמן לאימות TOTP. אנא התחבר מחדש.",
"passwordTimeout": "פסק זמן לאימות סיסמה. אנא התחבר מחדש.",
"connectionRejected": "החיבור נדחה על ידי השרת. אנא בדוק את האימות ותצורת הרשת שלך.",
"hostKeyRejected": "אימות מפתח מארח SSH נדחה. החיבור בוטל."
"hostKeyRejected": "אימות מפתח מארח SSH נדחה. החיבור בוטל.",
"sessionTakenOver": "הסשן נפתח בכרטיסייה אחרת. מתחבר מחדש...",
"sessionAttachTimeout": "תם הזמן שהוקצב לקובץ המצורף לסשן. יצירת חיבור חדש..."
},
"fileManager": {
"title": "מנהל הקבצים",
@@ -1670,7 +1738,13 @@
"write": "לִכתוֹב",
"execute": "לְבַצֵעַ",
"permissionsChangedSuccessfully": "ההרשאות שונו בהצלחה",
"failedToChangePermissions": "שינוי ההרשאות נכשל"
"failedToChangePermissions": "שינוי ההרשאות נכשל",
"name": "שֵׁם",
"sortByName": "שֵׁם",
"sortByDate": "תאריך שינוי",
"sortBySize": "גוֹדֶל",
"ascending": "עוֹלֶה",
"descending": "יורד"
},
"tunnel": {
"log": {
@@ -1858,7 +1932,7 @@
"loginButton": "כְּנִיסָה לַמַעֲרֶכֶת",
"registerButton": "לִרְשׁוֹם",
"forgotPassword": "שכחת סיסמה?",
"rememberMe": "זכור אותי",
"rememberMe": "זכור את המכשיר למשך 30 יום (כולל TOTP)",
"noAccount": "אין לך חשבון?",
"hasAccount": "כבר יש לך חשבון?",
"loginSuccess": "הכניסה הצליחה",
@@ -2023,6 +2097,7 @@
"processing": "עיבוד...",
"pleaseWait": "אנא המתן...",
"registrationDisabled": "רישום חשבון חדש מושבת כעת על ידי מנהל. אנא התחבר או צור קשר עם מנהל.",
"userNotAllowed": "החשבון שלך אינו מורשה להירשם. אנא צור קשר עם מנהל מערכת.",
"databaseConnected": "מסד הנתונים התחבר בהצלחה",
"databaseConnectionFailed": "נכשלה ההתחברות לשרת מסד הנתונים",
"checkServerConnection": "אנא בדוק את חיבור השרת שלך ונסה שוב",
@@ -2078,7 +2153,9 @@
"appearanceDesc": "בחר את ערכת הצבעים עבור האפליקציה",
"terminalSyntaxHighlightingDesc": "סמן אוטומטית פקודות, נתיבים, כתובות IP ורמות יומן בפלט הטרמינל",
"enableCommandPaletteShortcut": "הפעל קיצור דרך של לוח הפקודות",
"enableCommandPaletteShortcutDesc": "הקש פעמיים על Shift שמאלי כדי לפתוח את לוח הפקודות לגישה מהירה למארחים"
"enableCommandPaletteShortcutDesc": "הקש פעמיים על Shift שמאלי כדי לפתוח את לוח הפקודות לגישה מהירה למארחים",
"enableTerminalSessionPersistence": "הפעלות טרמינל מתמשכות",
"enableTerminalSessionPersistenceDesc": "שמרו על חיבורי SSH בעת מעבר בין כרטיסיות או סגירת הדפדפן (ייתכן שזה לא יציב)"
},
"user": {
"failedToLoadVersionInfo": "טעינת פרטי הגרסה נכשלה"
@@ -2114,6 +2191,7 @@
"usernameField": "שֵׁם",
"scopes": "פרופיל דוא\"ל openid",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"allowedUsers": "משתמש@example.com, @company.com",
"enterUsername": "הזן שם משתמש כדי להפוך למנהל",
"searchHosts": "חפש מארחים לפי שם, שם משתמש, כתובת IP, תיקייה, תגיות...",
"enterPassword": "הזן את הסיסמה שלך",

Some files were not shown because too many files have changed in this diff Show More