* 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>
This commit is contained in:
Luke Gustafson
2026-03-08 18:02:14 -05:00
committed by GitHub
parent 44049b8031
commit 0ff03110c9
153 changed files with 10978 additions and 5141 deletions
-1
View File
@@ -31,7 +31,6 @@ Thumbs.db
.gitignore .gitignore
README.md README.md
README-CN.md
CONTRIBUTING.md CONTRIBUTING.md
LICENSE LICENSE
-2
View File
@@ -319,7 +319,6 @@ jobs:
cd flatpak-build cd flatpak-build
flatpak-builder --repo=repo --force-clean --disable-rofiles-fuse build-dir com.karmaa.termix.yml flatpak-builder --repo=repo --force-clean --disable-rofiles-fuse build-dir com.karmaa.termix.yml
# Determine the architecture
ARCH=$(uname -m) ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then if [ "$ARCH" = "x86_64" ]; then
FLATPAK_ARCH="x86_64" FLATPAK_ARCH="x86_64"
@@ -329,7 +328,6 @@ jobs:
FLATPAK_ARCH="$ARCH" FLATPAK_ARCH="$ARCH"
fi 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 flatpak build-bundle repo ../release/termix_linux_flatpak.flatpak com.karmaa.termix --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
- name: Create flatpakref file - name: Create flatpakref file
+3
View File
@@ -8,6 +8,9 @@ jobs:
lint-and-build: lint-and-build:
runs-on: blacksmith-2vcpu-ubuntu-2404 runs-on: blacksmith-2vcpu-ubuntu-2404
env:
NODE_OPTIONS: "--max-old-space-size=4096"
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
-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。
+20 -7
View File
@@ -1,8 +1,20 @@
# Repo Stats # Repo Stats
<p align="center"> <p align="center">
<img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English | <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> <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> </p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) ![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 # 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 - **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. - **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. - **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 - **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 - **Dashboard** - View server information at a glance on your dashboard
- **RBAC** - Create roles and share hosts across users/roles - **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. - **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 - **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 - **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. - **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)) - **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. - **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 - **Command History** - Auto-complete and view previously ran SSH commands
- **Quick Connect** - Connect to a server without having to save the connection data - **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 - **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. - **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 - **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 # Planned Features
@@ -86,7 +99,7 @@ Supported Devices:
- Homebrew - Homebrew
- iOS/iPadOS (v15.1+) - iOS/iPadOS (v15.1+)
- Apple App Store - Apple App Store
- ISO - IPA
- Android (v7.0+) - Android (v7.0+)
- Google Play Store - Google Play Store
- APK - APK
@@ -140,7 +153,7 @@ channel, however, response times may be longer.
# Screenshots # Screenshots
[![YouTube](./repo-images/YouTube.png)](https://youtu.be/8OYCPwS7ciA) [![YouTube](./repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
<p align="center"> <p align="center">
<img src="./repo-images/Image 1.png" width="400" alt="Termix Demo 1"/> <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 ./ COPY package*.json ./
RUN rm -rf node_modules package-lock.json && \ RUN npm ci --ignore-scripts --force && \
npm install --ignore-scripts --force && \
npm cache clean --force npm cache clean --force
# Stage 2: Build frontend # Stage 2: Build frontend
@@ -51,7 +50,7 @@ ENV DATA_DIR=/app/data \
PORT=8080 \ PORT=8080 \
NODE_ENV=production 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 && \ update-ca-certificates && \
rm -rf /var/lib/apt/lists/* && \ 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 && \ 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 EXPOSE ${PORT} 30001 30002 30003 30004 30005 30006
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --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))" CMD wget -q -O /dev/null http://localhost:30001/health || exit 1
COPY docker/entrypoint.sh /entrypoint.sh COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
+17 -8
View File
@@ -5,16 +5,21 @@ PUID=${PUID:-1000}
PGID=${PGID:-1000} PGID=${PGID:-1000}
if [ "$(id -u)" = "0" ]; then 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 groupmod -o -g "$PGID" node 2>/dev/null || true
usermod -o -u "$PUID" 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 fi
export PORT=${PORT:-8080} export PORT=${PORT:-8080}
@@ -52,8 +57,12 @@ else
ls -ld /app/data/.opk ls -ld /app/data/.opk
fi fi
if [ ! -d "/app/opkssh" ]; then OPKSSH_DIR="${DATA_DIR:-/app/data}/opkssh"
echo "WARNING: OPKSSH binary directory not found at /app/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 fi
if [ "$ENABLE_SSL" = "true" ]; then if [ "$ENABLE_SSL" = "true" ]; then
+34 -13
View File
@@ -26,6 +26,21 @@ http {
set_real_ip_from 127.0.0.1; set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For; 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_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_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off; ssl_prefer_server_ciphers off;
@@ -75,7 +90,7 @@ http {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 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(/.*)?$ { location ~ ^/users(/.*)?$ {
@@ -84,7 +99,7 @@ http {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 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(/.*)?$ { location ~ ^/version(/.*)?$ {
@@ -217,22 +232,30 @@ http {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args; proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 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(/.*)?$ { location ~ ^/ssh/opkssh-callback(/.*)?$ {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args; proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 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/ { location /ssh/ {
@@ -251,15 +274,13 @@ http {
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade; proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s; proxy_read_timeout 86400s;
proxy_send_timeout 86400s; proxy_send_timeout 86400s;
+34 -12
View File
@@ -26,6 +26,21 @@ http {
set_real_ip_from 127.0.0.1; set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For; 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_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_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off; ssl_prefer_server_ciphers off;
@@ -64,7 +79,7 @@ http {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 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(/.*)?$ { location ~ ^/users(/.*)?$ {
@@ -73,7 +88,7 @@ http {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 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(/.*)?$ { location ~ ^/version(/.*)?$ {
@@ -206,22 +221,30 @@ http {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args; proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 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(/.*)?$ { location ~ ^/ssh/opkssh-callback(/.*)?$ {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args; proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 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/ { location /ssh/ {
@@ -240,14 +263,13 @@ http {
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_cache_bypass $http_upgrade; proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s; proxy_read_timeout 86400s;
proxy_send_timeout 86400s; proxy_send_timeout 86400s;
+11 -6
View File
@@ -5,24 +5,27 @@
"directories": { "directories": {
"output": "release" "output": "release"
}, },
"asar": false,
"files": [ "files": [
"dist/**/*", "dist/**/*",
"electron/**/*", "electron/**/*",
"public/**/*", "public/**/*",
"!**/node_modules/**/*",
"!src/**/*", "!src/**/*",
"!*.md", "!*.md",
"!tsconfig*.json", "!tsconfig*.json",
"!vite.config.ts", "!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": { "extraMetadata": {
"main": "electron/main.cjs" "main": "electron/main.cjs"
}, },
"buildDependenciesFromSource": false, "buildDependenciesFromSource": false,
"nodeGypRebuild": false, "nodeGypRebuild": false,
"npmRebuild": true, "npmRebuild": false,
"win": { "win": {
"target": [ "target": [
{ {
@@ -103,7 +106,9 @@
"entitlements": "build/entitlements.mac.plist", "entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.inherit.plist", "entitlementsInherit": "build/entitlements.mac.inherit.plist",
"type": "distribution", "type": "distribution",
"minimumSystemVersion": "10.15" "minimumSystemVersion": "10.15",
"mergeASARs": false,
"singleArchFiles": "node_modules/@tailwindcss/oxide-*/**/*"
}, },
"dmg": { "dmg": {
"artifactName": "termix_macos_${arch}_dmg.${ext}", "artifactName": "termix_macos_${arch}_dmg.${ext}",
@@ -116,7 +121,7 @@
"entitlementsInherit": "build/entitlements.mas.inherit.plist", "entitlementsInherit": "build/entitlements.mas.inherit.plist",
"hardenedRuntime": false, "hardenedRuntime": false,
"gatekeeperAssess": false, "gatekeeperAssess": false,
"asarUnpack": ["**/*.node"], "asar": false,
"type": "distribution", "type": "distribution",
"category": "public.app-category.developer-tools", "category": "public.app-category.developer-tools",
"artifactName": "termix_macos_${arch}_mas.${ext}", "artifactName": "termix_macos_${arch}_mas.${ext}",
+349 -116
View File
@@ -5,10 +5,76 @@ const {
ipcMain, ipcMain,
dialog, dialog,
Menu, Menu,
Tray,
} = require("electron"); } = require("electron");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const os = require("os"); 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") { if (process.platform === "linux") {
app.commandLine.appendSwitch("--ozone-platform-hint=auto"); 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"); app.commandLine.appendSwitch("--enable-features=NetworkService");
let mainWindow = null; let mainWindow = null;
let backendProcess = null;
let tray = null;
let isQuitting = false;
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged; const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
const appRoot = isDev ? process.cwd() : path.join(__dirname, ".."); 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(); const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) { if (!gotTheLock) {
console.log("Another instance is already running, quitting..."); 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() { function createWindow() {
const appVersion = app.getVersion(); const appVersion = app.getVersion();
const electronVersion = process.versions.electron; const electronVersion = process.versions.electron;
@@ -71,6 +338,19 @@ function createWindow() {
show: true, 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") { if (process.platform !== "darwin") {
mainWindow.setMenuBarVisibility(false); mainWindow.setMenuBarVisibility(false);
} }
@@ -183,6 +463,13 @@ function createWindow() {
console.log("Frontend loaded successfully"); console.log("Frontend loaded successfully");
}); });
mainWindow.on("close", (event) => {
if (!isQuitting && tray && !tray.isDestroyed()) {
event.preventDefault();
mainWindow.hide();
}
});
mainWindow.on("closed", () => { mainWindow.on("closed", () => {
mainWindow = null; mainWindow = null;
}); });
@@ -215,65 +502,7 @@ async function fetchGitHubAPI(endpoint, cacheKey) {
} }
try { try {
let fetch; const response = await httpFetch(`${GITHUB_API_BASE}${endpoint}`, {
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}`, {
headers: { headers: {
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
"User-Agent": "TermixElectronUpdateChecker/1.0", "User-Agent": "TermixElectronUpdateChecker/1.0",
@@ -358,6 +587,14 @@ ipcMain.handle("get-platform", () => {
return process.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", () => { ipcMain.handle("get-server-config", () => {
try { try {
const userDataPath = app.getPath("userData"); 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) => { ipcMain.handle("test-server-connection", async (event, serverUrl) => {
try { 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 normalizedServerUrl = serverUrl.replace(/\/$/, "");
const healthUrl = `${normalizedServerUrl}/health`; const healthUrl = `${normalizedServerUrl}/health`;
try { try {
const response = await fetch(healthUrl, { const response = await httpFetch(healthUrl, {
method: "GET", method: "GET",
timeout: 10000, timeout: 10000,
}); });
@@ -539,7 +742,7 @@ ipcMain.handle("test-server-connection", async (event, serverUrl) => {
try { try {
const versionUrl = `${normalizedServerUrl}/version`; const versionUrl = `${normalizedServerUrl}/version`;
const response = await fetch(versionUrl, { const response = await httpFetch(versionUrl, {
method: "GET", method: "GET",
timeout: 10000, 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(); 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(); createWindow();
logToFile("=== Startup complete ===");
}); });
app.on("window-all-closed", () => { 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", () => { app.on("activate", () => {
@@ -671,8 +899,13 @@ app.on("activate", () => {
} }
}); });
app.on("before-quit", () => {
isQuitting = true;
});
app.on("will-quit", () => { app.on("will-quit", () => {
console.log("App will quit..."); console.log("App will quit...");
stopBackendServer();
}); });
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
+8
View File
@@ -1,4 +1,5 @@
const { contextBridge, ipcRenderer } = require("electron"); const { contextBridge, ipcRenderer } = require("electron");
const { clipboard } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", { contextBridge.exposeInMainWorld("electronAPI", {
getAppVersion: () => ipcRenderer.invoke("get-app-version"), getAppVersion: () => ipcRenderer.invoke("get-app-version"),
@@ -10,7 +11,14 @@ contextBridge.exposeInMainWorld("electronAPI", {
getSetting: (key) => ipcRenderer.invoke("get-setting", key), getSetting: (key) => ipcRenderer.invoke("get-setting", key),
setSetting: (key, value) => ipcRenderer.invoke("set-setting", key, value), setSetting: (key, value) => ipcRenderer.invoke("set-setting", key, value),
clearSessionCookies: () => ipcRenderer.invoke("clear-session-cookies"),
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
}); });
contextBridge.exposeInMainWorld("electronClipboard", {
writeText: (text) => clipboard.writeText(text),
readText: () => clipboard.readText(),
});
window.IS_ELECTRON = true; window.IS_ELECTRON = true;
+9 -1
View File
@@ -6,7 +6,7 @@ import tseslint from "typescript-eslint";
import { globalIgnores } from "eslint/config"; import { globalIgnores } from "eslint/config";
export default tseslint.config([ export default tseslint.config([
globalIgnores(["dist"]), globalIgnores(["dist", "release", "Mobile"]),
{ {
files: ["**/*.{ts,tsx}"], files: ["**/*.{ts,tsx}"],
extends: [ extends: [
@@ -19,5 +19,13 @@ export default tseslint.config([
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, 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", "name": "termix",
"version": "1.11.1", "version": "1.11.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "termix", "name": "termix",
"version": "1.11.1", "version": "1.11.2",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.18.7", "@codemirror/autocomplete": "^6.18.7",
"@codemirror/commands": "^6.3.3", "@codemirror/commands": "^6.3.3",
@@ -43,7 +43,7 @@
"@uiw/codemirror-extensions-langs": "^4.24.1", "@uiw/codemirror-extensions-langs": "^4.24.1",
"@uiw/codemirror-theme-github": "^4.25.4", "@uiw/codemirror-theme-github": "^4.25.4",
"@uiw/react-codemirror": "^4.24.1", "@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-fit": "^0.10.0",
"@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "^0.11.0",
@@ -107,6 +107,7 @@
"@commitlint/cli": "^20.1.0", "@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0", "@commitlint/config-conventional": "^20.0.0",
"@electron/notarize": "^2.5.0", "@electron/notarize": "^2.5.0",
"@electron/rebuild": "^3.7.2",
"@eslint/js": "^9.34.0", "@eslint/js": "^9.34.0",
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
@@ -213,7 +214,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@@ -499,7 +499,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz",
"integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==", "integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^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", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^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", "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^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", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0", "@codemirror/language": "^6.6.0",
@@ -804,7 +800,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0", "@codemirror/view": "^6.23.0",
@@ -881,7 +876,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@marijn/find-cluster-break": "^1.0.0" "@marijn/find-cluster-break": "^1.0.0"
} }
@@ -903,7 +897,6 @@
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.5.0", "@codemirror/state": "^6.5.0",
"crelt": "^1.0.6", "crelt": "^1.0.6",
@@ -1231,7 +1224,6 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -1508,9 +1500,9 @@
} }
}, },
"node_modules/@electron/rebuild": { "node_modules/@electron/rebuild": {
"version": "3.7.0", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.0.tgz", "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.2.tgz",
"integrity": "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==", "integrity": "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1618,6 +1610,7 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"cross-dirname": "^0.1.0", "cross-dirname": "^0.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -1639,6 +1632,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
@@ -2692,8 +2686,7 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz",
"integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==", "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@lezer/cpp": { "node_modules/@lezer/cpp": {
"version": "1.1.3", "version": "1.1.3",
@@ -2733,7 +2726,6 @@
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@lezer/common": "^1.3.0" "@lezer/common": "^1.3.0"
} }
@@ -2765,7 +2757,6 @@
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3", "@lezer/highlight": "^1.1.3",
@@ -2788,7 +2779,6 @@
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
@@ -5236,7 +5226,6 @@
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
@@ -5400,7 +5389,6 @@
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz",
"integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/body-parser": "*", "@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0", "@types/express-serve-static-core": "^5.0.0",
@@ -5523,7 +5511,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz",
"integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -5566,7 +5553,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -5577,7 +5563,6 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@@ -5754,7 +5739,6 @@
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2", "@typescript-eslint/types": "8.46.2",
@@ -6120,15 +6104,12 @@
} }
}, },
"node_modules/@xterm/addon-clipboard": { "node_modules/@xterm/addon-clipboard": {
"version": "0.1.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.1.0.tgz", "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0.tgz",
"integrity": "sha512-zdoM7p53T5sv/HbRTyp4hY0kKmEQ3MZvAvEtiXqNIHc/JdpqwByCtsTaQF5DX2n4hYdXRPO4P/eOS0QEhX1nPw==", "integrity": "sha512-Dl31BCtBhLaUEECUbEiVcCLvLBbaeGYdT7NofB8OJkGTD3MWgBsaLjXvfGAD4tQNHhm6mbKyYkR7XD8kiZsdNg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"js-base64": "^3.7.5" "js-base64": "^3.7.5"
},
"peerDependencies": {
"@xterm/xterm": "^5.4.0"
} }
}, },
"node_modules/@xterm/addon-fit": { "node_modules/@xterm/addon-fit": {
@@ -6162,8 +6143,7 @@
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/7zip-bin": { "node_modules/7zip-bin": {
"version": "5.2.0", "version": "5.2.0",
@@ -6198,7 +6178,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -6368,6 +6347,35 @@
"electron-builder-squirrel-windows": "26.0.12" "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": { "node_modules/app-builder-lib/node_modules/dotenv": {
"version": "16.6.1", "version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -6626,7 +6634,6 @@
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"bindings": "^1.5.0", "bindings": "^1.5.0",
"prebuild-install": "^7.1.1" "prebuild-install": "^7.1.1"
@@ -6769,7 +6776,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.19", "baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751", "caniuse-lite": "^1.0.30001751",
@@ -7776,7 +7782,6 @@
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"env-paths": "^2.2.1", "env-paths": "^2.2.1",
"import-fresh": "^3.3.0", "import-fresh": "^3.3.0",
@@ -7853,7 +7858,8 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true,
"peer": true
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
@@ -7886,7 +7892,6 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
} }
@@ -8326,7 +8331,6 @@
"integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"app-builder-lib": "26.0.12", "app-builder-lib": "26.0.12",
"builder-util": "26.0.11", "builder-util": "26.0.11",
@@ -8437,7 +8441,8 @@
"version": "3.1.7", "version": "3.1.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", "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": { "node_modules/dot-prop": {
"version": "5.3.0", "version": "5.3.0",
@@ -8812,6 +8817,7 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.1", "@electron/asar": "^3.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -8832,6 +8838,7 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0", "jsonfile": "^4.0.0",
@@ -8847,6 +8854,7 @@
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"optionalDependencies": { "optionalDependencies": {
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
@@ -8857,6 +8865,7 @@
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 4.0.0" "node": ">= 4.0.0"
} }
@@ -9118,7 +9127,6 @@
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -11136,7 +11144,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.6" "@babel/runtime": "^7.27.6"
}, },
@@ -12719,6 +12726,7 @@
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"marked": "bin/marked.js" "marked": "bin/marked.js"
}, },
@@ -14756,6 +14764,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"commander": "^9.4.0" "commander": "^9.4.0"
}, },
@@ -14773,6 +14782,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
@@ -15258,7 +15268,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -15281,7 +15290,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -15340,7 +15348,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
"integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -15488,7 +15495,6 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@@ -15711,8 +15717,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
@@ -17197,6 +17202,7 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"rimraf": "~2.6.2" "rimraf": "~2.6.2"
@@ -17237,6 +17243,7 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"minimist": "^1.2.6" "minimist": "^1.2.6"
}, },
@@ -17251,6 +17258,7 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported", "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^7.1.3"
}, },
@@ -17355,7 +17363,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -17561,7 +17568,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -17984,7 +17990,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -18076,7 +18081,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
+10 -8
View File
@@ -1,7 +1,7 @@
{ {
"name": "termix", "name": "termix",
"private": true, "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", "description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
"author": "Karmaa", "author": "Karmaa",
"main": "electron/main.cjs", "main": "electron/main.cjs",
@@ -22,12 +22,13 @@
"generate:openapi": "tsc -p tsconfig.node.json && node ./dist/backend/backend/swagger.js", "generate:openapi": "tsc -p tsconfig.node.json && node ./dist/backend/backend/swagger.js",
"preview": "vite preview", "preview": "vite preview",
"electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && electron .\"", "electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && electron .\"",
"build:win-portable": "npm run build && electron-builder --win --dir", "electron:rebuild": "electron-rebuild -f -w better-sqlite3",
"build:win-installer": "npm run build && electron-builder --win --publish=never", "build:win-portable": "npm run build && npm run electron:rebuild && electron-builder --win --dir",
"build:linux-portable": "npm run build && electron-builder --linux --dir", "build:win-installer": "npm run build && npm run electron:rebuild && electron-builder --win --publish=never",
"build:linux-appimage": "npm run build && electron-builder --linux AppImage", "build:linux-portable": "npm run build && npm run electron:rebuild && electron-builder --linux --dir",
"build:linux-targz": "npm run build && electron-builder --linux tar.gz", "build:linux-appimage": "npm run build && npm run electron:rebuild && electron-builder --linux AppImage",
"build:mac": "npm run build && electron-builder --mac --universal" "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": { "dependencies": {
"@codemirror/autocomplete": "^6.18.7", "@codemirror/autocomplete": "^6.18.7",
@@ -65,7 +66,7 @@
"@uiw/codemirror-extensions-langs": "^4.24.1", "@uiw/codemirror-extensions-langs": "^4.24.1",
"@uiw/codemirror-theme-github": "^4.25.4", "@uiw/codemirror-theme-github": "^4.25.4",
"@uiw/react-codemirror": "^4.24.1", "@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-fit": "^0.10.0",
"@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "^0.11.0",
@@ -129,6 +130,7 @@
"@commitlint/cli": "^20.1.0", "@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0", "@commitlint/config-conventional": "^20.0.0",
"@electron/notarize": "^2.5.0", "@electron/notarize": "^2.5.0",
"@electron/rebuild": "^3.7.2",
"@eslint/js": "^9.34.0", "@eslint/js": "^9.34.0",
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
+7
View File
@@ -54,6 +54,13 @@ self.addEventListener("fetch", (event) => {
return; return;
} }
if (
url.pathname.startsWith("/ssh/opkssh-chooser/") ||
url.pathname.startsWith("/ssh/opkssh-callback/")
) {
return;
}
if (url.origin !== self.location.origin) { if (url.origin !== self.location.origin) {
return; 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, hostAccess,
dashboardPreferences, dashboardPreferences,
} from "./database/db/schema.js"; } 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 { dashboardLogger } from "./utils/logger.js";
import { SimpleDBOps } from "./utils/simple-db-ops.js"; import { SimpleDBOps } from "./utils/simple-db-ops.js";
import { AuthManager } from "./utils/auth-manager.js"; import { AuthManager } from "./utils/auth-manager.js";
@@ -55,6 +55,10 @@ app.use(
); );
app.use(cookieParser()); app.use(cookieParser());
app.use(express.json({ limit: "1mb" })); app.use(express.json({ limit: "1mb" }));
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "no-store");
next();
});
app.use(authManager.createAuthMiddleware()); app.use(authManager.createAuthMiddleware());
@@ -85,9 +89,6 @@ app.use(authManager.createAuthMiddleware());
*/ */
app.get("/uptime", async (req, res) => { app.get("/uptime", async (req, res) => {
try { try {
const startTime = Date.now();
const userId = (req as AuthenticatedRequest).userId;
const uptimeMs = Date.now() - serverStartTime; const uptimeMs = Date.now() - serverStartTime;
const uptimeSeconds = Math.floor(uptimeMs / 1000); const uptimeSeconds = Math.floor(uptimeMs / 1000);
const days = Math.floor(uptimeSeconds / 86400); const days = Math.floor(uptimeSeconds / 86400);
@@ -296,7 +297,7 @@ app.post("/activity/log", async (req, res) => {
if (allActivities.length > 100) { if (allActivities.length > 100) {
const toDelete = allActivities.slice(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); await SimpleDBOps.delete(recentActivity, "recent_activity", userId);
} }
} }
+104 -51
View File
@@ -45,6 +45,10 @@ import type {
} from "../../types/index.js"; } from "../../types/index.js";
import { getDb, DatabaseSaveTrigger } from "./db/index.js"; import { getDb, DatabaseSaveTrigger } from "./db/index.js";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express(); const app = express();
@@ -203,6 +207,10 @@ app.use(bodyParser.json({ limit: "1gb" }));
app.use(bodyParser.urlencoded({ limit: "1gb", extended: true })); app.use(bodyParser.urlencoded({ limit: "1gb", extended: true }));
app.use(bodyParser.raw({ limit: "5gb", type: "application/octet-stream" })); app.use(bodyParser.raw({ limit: "5gb", type: "application/octet-stream" }));
app.use(cookieParser()); app.use(cookieParser());
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "no-store");
next();
});
/** /**
* @openapi * @openapi
@@ -595,21 +603,41 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
try { try {
const userId = (req as AuthenticatedRequest).userId; const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body; const { password } = req.body;
if (!password) {
return res.status(400).json({
error: "Password required for export",
code: "PASSWORD_REQUIRED",
});
}
const deviceInfo = parseUserAgent(req); const deviceInfo = parseUserAgent(req);
const unlocked = await authManager.authenticateUser(
userId, const user = await getDb().select().from(users).where(eq(users.id, userId));
password, if (!user || user.length === 0) {
deviceInfo.type, return res.status(404).json({ error: "User not found" });
); }
if (!unlocked) {
return res.status(401).json({ error: "Invalid password" }); 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", { 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"); 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 = const tempDir =
process.env.NODE_ENV === "production" process.env.NODE_ENV === "production"
? path.join(process.env.DATA_DIR || "./db/data", ".temp", "exports") ? 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.id,
userRecord.username, userRecord.username,
"[EXPORTED_USER_NO_PASSWORD]", "[EXPORTED_USER_NO_PASSWORD]",
userRecord.is_admin ? 1 : 0, userRecord.isAdmin ? 1 : 0,
userRecord.is_oidc ? 1 : 0, userRecord.isOidc ? 1 : 0,
userRecord.oidc_identifier || null, userRecord.oidcIdentifier || null,
userRecord.client_id || null, userRecord.clientId || null,
userRecord.client_secret || null, userRecord.clientSecret || null,
userRecord.issuer_url || null, userRecord.issuerUrl || null,
userRecord.authorization_url || null, userRecord.authorizationUrl || null,
userRecord.token_url || null, userRecord.tokenUrl || null,
userRecord.identifier_path || null, userRecord.identifierPath || null,
userRecord.name_path || null, userRecord.namePath || null,
userRecord.scopes || null, userRecord.scopes || null,
userRecord.totp_secret || null, userRecord.totpSecret || null,
userRecord.totp_enabled ? 1 : 0, userRecord.totpEnabled ? 1 : 0,
userRecord.totp_backup_codes || null, userRecord.totpBackupCodes || null,
); );
const sshHosts = await getDb() const sshHosts = await getDb()
@@ -851,31 +874,31 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.forceKeyboardInteractive || null, decrypted.forceKeyboardInteractive || null,
decrypted.password || null, decrypted.password || null,
decrypted.key || null, decrypted.key || null,
decrypted.key_password || null, decrypted.keyPassword || null,
decrypted.keyType || null, decrypted.keyType || null,
decrypted.sudoPassword || null, decrypted.sudoPassword || null,
decrypted.autostartPassword || null, decrypted.autostartPassword || null,
decrypted.autostartKey || null, decrypted.autostartKey || null,
decrypted.autostartKeyPassword || null, decrypted.autostartKeyPassword || null,
decrypted.credentialId || null, decrypted.credentialId || null,
Boolean(decrypted.overrideCredentialUsername) ? 1 : 0, decrypted.overrideCredentialUsername ? 1 : 0,
Boolean(decrypted.enableTerminal) ? 1 : 0, decrypted.enableTerminal ? 1 : 0,
Boolean(decrypted.enableTunnel) ? 1 : 0, decrypted.enableTunnel ? 1 : 0,
decrypted.tunnelConnections || null, decrypted.tunnelConnections || null,
decrypted.jumpHosts || null, decrypted.jumpHosts || null,
Boolean(decrypted.enableFileManager) ? 1 : 0, decrypted.enableFileManager ? 1 : 0,
Boolean(decrypted.enableDocker) ? 1 : 0, decrypted.enableDocker ? 1 : 0,
Boolean(decrypted.showTerminalInSidebar) ? 1 : 0, decrypted.showTerminalInSidebar ? 1 : 0,
Boolean(decrypted.showFileManagerInSidebar) ? 1 : 0, decrypted.showFileManagerInSidebar ? 1 : 0,
Boolean(decrypted.showTunnelInSidebar) ? 1 : 0, decrypted.showTunnelInSidebar ? 1 : 0,
Boolean(decrypted.showDockerInSidebar) ? 1 : 0, decrypted.showDockerInSidebar ? 1 : 0,
Boolean(decrypted.showServerStatsInSidebar) ? 1 : 0, decrypted.showServerStatsInSidebar ? 1 : 0,
decrypted.defaultPath || null, decrypted.defaultPath || null,
decrypted.statsConfig || null, decrypted.statsConfig || null,
decrypted.terminalConfig || null, decrypted.terminalConfig || null,
decrypted.quickActions || null, decrypted.quickActions || null,
decrypted.notes || null, decrypted.notes || null,
Boolean(decrypted.useSocks5) ? 1 : 0, decrypted.useSocks5 ? 1 : 0,
decrypted.socks5Host || null, decrypted.socks5Host || null,
decrypted.socks5Port || null, decrypted.socks5Port || null,
decrypted.socks5Username || null, decrypted.socks5Username || null,
@@ -913,9 +936,9 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.username, decrypted.username,
decrypted.password || null, decrypted.password || null,
decrypted.key || null, decrypted.key || null,
decrypted.private_key || null, decrypted.privateKey || null,
decrypted.public_key || null, decrypted.publicKey || null,
decrypted.key_password || null, decrypted.keyPassword || null,
decrypted.keyType || null, decrypted.keyType || null,
decrypted.detectedKeyType || null, decrypted.detectedKeyType || null,
decrypted.usageCount || 0, decrypted.usageCount || 0,
@@ -1135,7 +1158,7 @@ app.post(
return res.status(404).json({ error: "User not found" }); return res.status(404).json({ error: "User not found" });
} }
const isOidcUser = !!userRecords[0].is_oidc; const isOidcUser = !!userRecords[0].isOidc;
if (!isOidcUser) { if (!isOidcUser) {
if (!password) { if (!password) {
@@ -1506,7 +1529,7 @@ app.post(
.select() .select()
.from(users) .from(users)
.where(eq(users.id, userId)); .where(eq(users.id, userId));
if (targetUser.length > 0 && targetUser[0].is_admin) { if (targetUser.length > 0 && targetUser[0].isAdmin) {
try { try {
const importedSettings = importDb const importedSettings = importDb
.prepare("SELECT * FROM settings") .prepare("SELECT * FROM settings")
@@ -1744,7 +1767,33 @@ app.use("/terminal", terminalRoutes);
app.use("/network-topology", networkTopologyRoutes); app.use("/network-topology", networkTopologyRoutes);
app.use("/rbac", rbacRoutes); 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( app.use(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
( (
err: unknown, err: unknown,
req: express.Request, req: express.Request,
@@ -1819,13 +1868,17 @@ app.get(
if (status.hasUnencryptedDb) { if (status.hasUnencryptedDb) {
try { try {
unencryptedSize = fs.statSync(dbPath).size; unencryptedSize = fs.statSync(dbPath).size;
} catch (error) {} } catch {
// expected - file may not exist
}
} }
if (status.hasEncryptedDb) { if (status.hasEncryptedDb) {
try { try {
encryptedSize = fs.statSync(encryptedDbPath).size; encryptedSize = fs.statSync(encryptedDbPath).size;
} catch (error) {} } catch {
// expected - file may not exist
}
} }
res.json({ res.json({
+60 -17
View File
@@ -37,10 +37,11 @@ async function initializeDatabaseAsync(): Promise<void> {
memoryDatabase = new Database(decryptedBuffer); memoryDatabase = new Database(decryptedBuffer);
try { try {
const sessionCount = memoryDatabase memoryDatabase
.prepare("SELECT COUNT(*) as count FROM sessions") .prepare("SELECT COUNT(*) as count FROM sessions")
.get() as { count: number }; .get() as { count: number };
} catch (countError) { } catch {
// expected - sessions table may not exist yet
} }
} else { } else {
const migration = new DatabaseMigration(dataDir); const migration = new DatabaseMigration(dataDir);
@@ -182,6 +183,18 @@ async function initializeCompleteDatabase(): Promise<void> {
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE 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 ( CREATE TABLE IF NOT EXISTS ssh_data (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL, 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 { try {
sqlite sqlite
.prepare("SELECT id FROM network_topology LIMIT 1") .prepare("SELECT id FROM network_topology LIMIT 1")
@@ -1033,23 +1073,15 @@ const migrateSchema = () => {
try { try {
const validSystemRoles = ['admin', 'user']; const validSystemRoles = ['admin', 'user'];
const unwantedRoleNames = ['superAdmin', 'powerUser', 'readonly', 'member']; const unwantedRoleNames = ['superAdmin', 'powerUser', 'readonly', 'member'];
let deletedCount = 0;
const deleteByName = sqlite.prepare("DELETE FROM roles WHERE name = ?"); const deleteByName = sqlite.prepare("DELETE FROM roles WHERE name = ?");
for (const roleName of unwantedRoleNames) { for (const roleName of unwantedRoleNames) {
const result = deleteByName.run(roleName); deleteByName.run(roleName);
if (result.changes > 0) {
deletedCount += result.changes;
}
} }
const deleteOldSystemRole = sqlite.prepare("DELETE FROM roles WHERE name = ? AND is_system = 1"); const deleteOldSystemRole = sqlite.prepare("DELETE FROM roles WHERE name = ? AND is_system = 1");
for (const role of existingRoles) { for (const role of existingRoles) {
if (role.is_system === 1 && !validSystemRoles.includes(role.name) && !unwantedRoleNames.includes(role.name)) { if (role.is_system === 1 && !validSystemRoles.includes(role.name) && !unwantedRoleNames.includes(role.name)) {
const result = deleteOldSystemRole.run(role.name); deleteOldSystemRole.run(role.name);
if (result.changes > 0) {
deletedCount += result.changes;
}
} }
} }
} catch (cleanupError) { } catch (cleanupError) {
@@ -1107,7 +1139,7 @@ const migrateSchema = () => {
for (const admin of adminUsers) { for (const admin of adminUsers) {
try { try {
insertUserRole.run(admin.id, adminRole.id); insertUserRole.run(admin.id, adminRole.id);
} catch (error) { } catch {
// Ignore duplicate errors // Ignore duplicate errors
} }
} }
@@ -1122,7 +1154,7 @@ const migrateSchema = () => {
for (const user of normalUsers) { for (const user of normalUsers) {
try { try {
insertUserRole.run(user.id, userRole.id); insertUserRole.run(user.id, userRole.id);
} catch (error) { } catch {
// Ignore duplicate errors // Ignore duplicate errors
} }
} }
@@ -1156,10 +1188,11 @@ async function saveMemoryDatabaseToFile() {
} }
try { try {
const sessionCount = memoryDatabase memoryDatabase
.prepare("SELECT COUNT(*) as count FROM sessions") .prepare("SELECT COUNT(*) as count FROM sessions")
.get() as { count: number }; .get() as { count: number };
} catch (countError) { } catch {
// expected - sessions table may not exist yet
} }
if (enableFileEncryption) { if (enableFileEncryption) {
@@ -1170,6 +1203,8 @@ async function saveMemoryDatabaseToFile() {
} else { } else {
fs.writeFileSync(dbPath, buffer); fs.writeFileSync(dbPath, buffer);
} }
DatabaseSaveTrigger.markClean();
} catch (error) { } catch (error) {
databaseLogger.error("Failed to save in-memory database", error, { databaseLogger.error("Failed to save in-memory database", error, {
operation: "memory_db_save_failed", operation: "memory_db_save_failed",
@@ -1185,7 +1220,11 @@ async function handlePostInitFileEncryption() {
if (memoryDatabase) { if (memoryDatabase) {
await saveMemoryDatabaseToFile(); await saveMemoryDatabaseToFile();
setInterval(saveMemoryDatabaseToFile, 15 * 1000); setInterval(() => {
if (DatabaseSaveTrigger.isDirty) {
saveMemoryDatabaseToFile();
}
}, 5 * 60 * 1000);
DatabaseSaveTrigger.initialize(saveMemoryDatabaseToFile); DatabaseSaveTrigger.initialize(saveMemoryDatabaseToFile);
} }
@@ -1254,15 +1293,18 @@ async function cleanupDatabase() {
try { try {
fs.unlinkSync(path.join(tempDir, file)); fs.unlinkSync(path.join(tempDir, file));
} catch { } catch {
// expected - file cleanup best effort
} }
} }
try { try {
fs.rmdirSync(tempDir); fs.rmdirSync(tempDir);
} catch { } catch {
// expected - dir cleanup best effort
} }
} }
} catch { } catch {
// expected - temp dir cleanup best effort
} }
} }
@@ -1271,6 +1313,7 @@ process.on("exit", () => {
try { try {
sqlite.close(); sqlite.close();
} catch { } 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", { export const users = sqliteTable("users", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
username: text("username").notNull(), username: text("username").notNull(),
password_hash: text("password_hash").notNull(), passwordHash: text("password_hash").notNull(),
is_admin: integer("is_admin", { mode: "boolean" }).notNull().default(false), isAdmin: integer("is_admin", { mode: "boolean" }).notNull().default(false),
is_oidc: integer("is_oidc", { mode: "boolean" }).notNull().default(false), isOidc: integer("is_oidc", { mode: "boolean" }).notNull().default(false),
oidc_identifier: text("oidc_identifier"), oidcIdentifier: text("oidc_identifier"),
client_id: text("client_id"), clientId: text("client_id"),
client_secret: text("client_secret"), clientSecret: text("client_secret"),
issuer_url: text("issuer_url"), issuerUrl: text("issuer_url"),
authorization_url: text("authorization_url"), authorizationUrl: text("authorization_url"),
token_url: text("token_url"), tokenUrl: text("token_url"),
identifier_path: text("identifier_path"), identifierPath: text("identifier_path"),
name_path: text("name_path"), namePath: text("name_path"),
scopes: text().default("openid email profile"), scopes: text().default("openid email profile"),
totp_secret: text("totp_secret"), totpSecret: text("totp_secret"),
totp_enabled: integer("totp_enabled", { mode: "boolean" }) totpEnabled: integer("totp_enabled", { mode: "boolean" })
.notNull() .notNull()
.default(false), .default(false),
totp_backup_codes: text("totp_backup_codes"), totpBackupCodes: text("totp_backup_codes"),
}); });
export const settings = sqliteTable("settings", { export const settings = sqliteTable("settings", {
@@ -47,6 +47,23 @@ export const sessions = sqliteTable("sessions", {
.default(sql`CURRENT_TIMESTAMP`), .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", { export const sshData = sqliteTable("ssh_data", {
id: integer("id").primaryKey({ autoIncrement: true }), id: integer("id").primaryKey({ autoIncrement: true }),
userId: text("user_id") userId: text("user_id")
@@ -64,7 +81,7 @@ export const sshData = sqliteTable("ssh_data", {
password: text("password"), password: text("password"),
key: text("key", { length: 8192 }), key: text("key", { length: 8192 }),
key_password: text("key_password"), keyPassword: text("key_password"),
keyType: text("key_type"), keyType: text("key_type"),
sudoPassword: text("sudo_password"), sudoPassword: text("sudo_password"),
@@ -202,9 +219,9 @@ export const sshCredentials = sqliteTable("ssh_credentials", {
username: text("username"), username: text("username"),
password: text("password"), password: text("password"),
key: text("key", { length: 16384 }), key: text("key", { length: 16384 }),
private_key: text("private_key", { length: 16384 }), privateKey: text("private_key", { length: 16384 }),
public_key: text("public_key", { length: 4096 }), publicKey: text("public_key", { length: 4096 }),
key_password: text("key_password"), keyPassword: text("key_password"),
keyType: text("key_type"), keyType: text("key_type"),
detectedKeyType: text("detected_key_type"), detectedKeyType: text("detected_key_type"),
+36 -37
View File
@@ -225,9 +225,9 @@ router.post(
username: username?.trim() || null, username: username?.trim() || null,
password: plainPassword, password: plainPassword,
key: plainKey, key: plainKey,
private_key: keyInfo?.privateKey || plainKey, privateKey: keyInfo?.privateKey || plainKey,
public_key: keyInfo?.publicKey || null, publicKey: keyInfo?.publicKey || null,
key_password: plainKeyPassword, keyPassword: plainKeyPassword,
keyType: keyType || null, keyType: keyType || null,
detectedKeyType: keyInfo?.keyType || null, detectedKeyType: keyInfo?.keyType || null,
usageCount: 0, usageCount: 0,
@@ -434,14 +434,14 @@ router.get(
if (credential.key) { if (credential.key) {
output.key = credential.key; output.key = credential.key;
} }
if (credential.private_key) { if (credential.privateKey) {
output.privateKey = credential.private_key; output.privateKey = credential.privateKey;
} }
if (credential.public_key) { if (credential.publicKey) {
output.publicKey = credential.public_key; output.publicKey = credential.publicKey;
} }
if (credential.key_password) { if (credential.keyPassword) {
output.keyPassword = credential.key_password; output.keyPassword = credential.keyPassword;
} }
res.json(output); res.json(output);
@@ -564,13 +564,13 @@ router.put(
error: `Invalid SSH key: ${keyInfo.error}`, error: `Invalid SSH key: ${keyInfo.error}`,
}); });
} }
updateFields.private_key = keyInfo.privateKey; updateFields.privateKey = keyInfo.privateKey;
updateFields.public_key = keyInfo.publicKey; updateFields.publicKey = keyInfo.publicKey;
updateFields.detectedKeyType = keyInfo.keyType; updateFields.detectedKeyType = keyInfo.keyType;
} }
} }
if (updateData.keyPassword !== undefined) { if (updateData.keyPassword !== undefined) {
updateFields.key_password = updateData.keyPassword || null; updateFields.keyPassword = updateData.keyPassword || null;
} }
if (Object.keys(updateFields).length === 0) { if (Object.keys(updateFields).length === 0) {
@@ -606,15 +606,15 @@ router.put(
userId, userId,
); );
const { SharedCredentialManager } = const { SharedCredentialManager } = await import(
await import("../../utils/shared-credential-manager.js"); "../../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance(); const sharedCredManager = SharedCredentialManager.getInstance();
await sharedCredManager.updateSharedCredentialsForOriginal( await sharedCredManager.updateSharedCredentialsForOriginal(
parseInt(id), parseInt(id),
userId, userId,
); );
const credential = updated[0];
authLogger.success("SSH credential updated", { authLogger.success("SSH credential updated", {
operation: "credential_update_success", operation: "credential_update_success",
userId, userId,
@@ -706,7 +706,7 @@ router.delete(
credentialId: null, credentialId: null,
password: null, password: null,
key: null, key: null,
key_password: null, keyPassword: null,
authType: "password", authType: "password",
}) })
.where( .where(
@@ -737,8 +737,9 @@ router.delete(
} }
} }
const { SharedCredentialManager } = const { SharedCredentialManager } = await import(
await import("../../utils/shared-credential-manager.js"); "../../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance(); const sharedCredManager = SharedCredentialManager.getInstance();
await sharedCredManager.deleteSharedCredentialsForOriginal(parseInt(id)); await sharedCredManager.deleteSharedCredentialsForOriginal(parseInt(id));
@@ -751,7 +752,6 @@ router.delete(
), ),
); );
const credential = credentialToDelete[0];
authLogger.success("SSH credential deleted", { authLogger.success("SSH credential deleted", {
operation: "credential_delete_success", operation: "credential_delete_success",
userId, userId,
@@ -841,10 +841,10 @@ router.post(
.set({ .set({
credentialId: parseInt(credentialId), credentialId: parseInt(credentialId),
username: (credential.username as string) || "", username: (credential.username as string) || "",
authType: (credential.auth_type || credential.authType) as string, authType: credential.authType as string,
password: null, password: null,
key: null, key: null,
key_password: null, keyPassword: null,
keyType: null, keyType: null,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}) })
@@ -954,15 +954,15 @@ function formatCredentialOutput(
? credential.tags.split(",").filter(Boolean) ? credential.tags.split(",").filter(Boolean)
: [] : []
: [], : [],
authType: credential.authType || credential.auth_type, authType: credential.authType,
username: credential.username || null, username: credential.username || null,
publicKey: credential.public_key || credential.publicKey, publicKey: credential.publicKey,
keyType: credential.key_type || credential.keyType, keyType: credential.keyType,
detectedKeyType: credential.detected_key_type || credential.detectedKeyType, detectedKeyType: credential.detectedKeyType,
usageCount: credential.usage_count || credential.usageCount || 0, usageCount: credential.usageCount || 0,
lastUsed: credential.last_used || credential.lastUsed, lastUsed: credential.lastUsed,
createdAt: credential.created_at || credential.createdAt, createdAt: credential.createdAt,
updatedAt: credential.updated_at || credential.updatedAt, updatedAt: credential.updatedAt,
}; };
} }
@@ -1508,7 +1508,7 @@ async function deploySSHKeyToHost(
hostConfig: Record<string, unknown>, hostConfig: Record<string, unknown>,
credData: CredentialBackend, credData: CredentialBackend,
): Promise<{ success: boolean; message?: string; error?: string }> { ): Promise<{ success: boolean; message?: string; error?: string }> {
const publicKey = credData.public_key as string; const publicKey = credData.publicKey as string;
return new Promise((resolve) => { return new Promise((resolve) => {
const conn = new Client(); const conn = new Client();
@@ -1934,7 +1934,7 @@ router.post(
}); });
} }
const publicKey = credData.public_key; const publicKey = credData.publicKey;
if (!publicKey) { if (!publicKey) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
@@ -1990,15 +1990,14 @@ router.post(
if (hostCredential && hostCredential.length > 0) { if (hostCredential && hostCredential.length > 0) {
const cred = hostCredential[0]; const cred = hostCredential[0];
hostConfig.authType = cred.auth_type || cred.authType; hostConfig.authType = cred.authType;
hostConfig.username = cred.username; hostConfig.username = cred.username;
if ((cred.auth_type || cred.authType) === "password") { if (cred.authType === "password") {
hostConfig.password = cred.password; hostConfig.password = cred.password;
} else if ((cred.auth_type || cred.authType) === "key") { } else if (cred.authType === "key") {
hostConfig.privateKey = hostConfig.privateKey = cred.privateKey || cred.key;
cred.private_key || cred.privateKey || cred.key; hostConfig.keyPassword = cred.keyPassword;
hostConfig.keyPassword = cred.key_password || cred.keyPassword;
} }
} else { } else {
return res.status(400).json({ return res.status(400).json({
+11 -12
View File
@@ -7,11 +7,10 @@ import {
users, users,
roles, roles,
userRoles, userRoles,
auditLogs,
sharedCredentials, sharedCredentials,
} from "../db/schema.js"; } from "../db/schema.js";
import { eq, and, desc, sql, or, isNull, gte } from "drizzle-orm"; 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 { databaseLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js"; import { AuthManager } from "../../utils/auth-manager.js";
import { PermissionManager } from "../../utils/permission-manager.js"; import { PermissionManager } from "../../utils/permission-manager.js";
@@ -202,8 +201,9 @@ router.post(
.delete(sharedCredentials) .delete(sharedCredentials)
.where(eq(sharedCredentials.hostAccessId, existing[0].id)); .where(eq(sharedCredentials.hostAccessId, existing[0].id));
const { SharedCredentialManager } = const { SharedCredentialManager } = await import(
await import("../../utils/shared-credential-manager.js"); "../../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance(); const sharedCredManager = SharedCredentialManager.getInstance();
if (targetType === "user") { if (targetType === "user") {
await sharedCredManager.createSharedCredentialForUser( await sharedCredManager.createSharedCredentialForUser(
@@ -244,8 +244,9 @@ router.post(
expiresAt, expiresAt,
}); });
const { SharedCredentialManager } = const { SharedCredentialManager } = await import(
await import("../../utils/shared-credential-manager.js"); "../../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance(); const sharedCredManager = SharedCredentialManager.getInstance();
if (targetType === "user") { if (targetType === "user") {
@@ -857,10 +858,7 @@ router.delete(
permissionManager.invalidateUserPermissionCache(userId); permissionManager.invalidateUserPermissionCache(userId);
} }
const deletedHostAccess = await db await db.delete(hostAccess).where(eq(hostAccess.roleId, roleId));
.delete(hostAccess)
.where(eq(hostAccess.roleId, roleId))
.returning({ id: hostAccess.id });
await db.delete(roles).where(eq(roles.id, roleId)); await db.delete(roles).where(eq(roles.id, roleId));
@@ -983,8 +981,9 @@ router.post(
.innerJoin(sshData, eq(hostAccess.hostId, sshData.id)) .innerJoin(sshData, eq(hostAccess.hostId, sshData.id))
.where(eq(hostAccess.roleId, roleId)); .where(eq(hostAccess.roleId, roleId));
const { SharedCredentialManager } = const { SharedCredentialManager } = await import(
await import("../../utils/shared-credential-manager.js"); "../../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance(); const sharedCredManager = SharedCredentialManager.getInstance();
for (const { host_access, ssh_data } of hostsSharedWithRole) { for (const { host_access, ssh_data } of hostsSharedWithRole) {
+5 -5
View File
@@ -655,7 +655,7 @@ router.post(
let password = host.password; let password = host.password;
let privateKey = host.key; let privateKey = host.key;
let passphrase = host.key_password; let passphrase = host.keyPassword;
let authType = host.authType; let authType = host.authType;
if (host.credentialId) { if (host.credentialId) {
@@ -675,12 +675,12 @@ router.post(
if (credResult.length > 0) { if (credResult.length > 0) {
const cred = credResult[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; password = (cred.password || undefined) as string | undefined;
privateKey = (cred.private_key || cred.key || undefined) as privateKey = (cred.privateKey || cred.key || undefined) as
| string | string
| undefined; | undefined;
passphrase = (cred.key_password || undefined) as string | undefined; passphrase = (cred.keyPassword || undefined) as string | undefined;
} }
} }
@@ -731,7 +731,7 @@ router.post(
reject(err); reject(err);
}); });
const config: any = { const config: Record<string, unknown> = {
host: host.ip, host: host.ip,
port: host.port, port: host.port,
username: host.username, 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, username: host.username,
password: host.autostartPassword || host.password, password: host.autostartPassword || host.password,
key: host.autostartKey || host.key, key: host.autostartKey || host.key,
keyPassword: host.autostartKeyPassword || host.key_password, keyPassword: host.autostartKeyPassword || host.keyPassword,
autostartPassword: host.autostartPassword, autostartPassword: host.autostartPassword,
autostartKey: host.autostartKey, autostartKey: host.autostartKey,
autostartKeyPassword: host.autostartKeyPassword, autostartKeyPassword: host.autostartKeyPassword,
@@ -426,8 +426,16 @@ router.post(
showDockerInSidebar: showDockerInSidebar ? 1 : 0, showDockerInSidebar: showDockerInSidebar ? 1 : 0,
showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0, showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0,
defaultPath: defaultPath || null, defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null, statsConfig: statsConfig
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null, ? typeof statsConfig === "string"
? statsConfig
: JSON.stringify(statsConfig)
: null,
terminalConfig: terminalConfig
? typeof terminalConfig === "string"
? terminalConfig
: JSON.stringify(terminalConfig)
: null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false", forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
notes: notes || null, notes: notes || null,
sudoPassword: sudoPassword || null, sudoPassword: sudoPassword || null,
@@ -444,7 +452,7 @@ router.post(
if (effectiveAuthType === "password") { if (effectiveAuthType === "password") {
sshDataObj.password = password || null; sshDataObj.password = password || null;
sshDataObj.key = null; sshDataObj.key = null;
sshDataObj.key_password = null; sshDataObj.keyPassword = null;
sshDataObj.keyType = null; sshDataObj.keyType = null;
} else if (effectiveAuthType === "key") { } else if (effectiveAuthType === "key") {
if (key && typeof key === "string") { if (key && typeof key === "string") {
@@ -481,13 +489,13 @@ router.post(
} }
sshDataObj.key = key || null; sshDataObj.key = key || null;
sshDataObj.key_password = keyPassword || null; sshDataObj.keyPassword = keyPassword || null;
sshDataObj.keyType = keyType; sshDataObj.keyType = keyType;
sshDataObj.password = null; sshDataObj.password = null;
} else { } else {
sshDataObj.password = null; sshDataObj.password = null;
sshDataObj.key = null; sshDataObj.key = null;
sshDataObj.key_password = null; sshDataObj.keyPassword = null;
sshDataObj.keyType = null; sshDataObj.keyType = null;
} }
@@ -682,16 +690,10 @@ router.post(
const cred = credentials[0]; const cred = credentials[0];
resolvedPassword = cred.password as string | undefined; resolvedPassword = cred.password as string | undefined;
resolvedKey = (cred.private_key || cred.privateKey || cred.key) as resolvedKey = cred.privateKey as string | undefined;
| string resolvedKeyPassword = cred.keyPassword as string | undefined;
| undefined; resolvedKeyType = cred.keyType as string | undefined;
resolvedKeyPassword = (cred.key_password || cred.keyPassword) as resolvedAuthType = cred.authType as string | undefined;
| string
| undefined;
resolvedKeyType = (cred.key_type || cred.keyType) as string | undefined;
resolvedAuthType = (cred.auth_type || cred.authType) as
| string
| undefined;
if (!overrideCredentialUsername) { if (!overrideCredentialUsername) {
resolvedUsername = cred.username as string; resolvedUsername = cred.username as string;
@@ -909,8 +911,16 @@ router.put(
showDockerInSidebar: showDockerInSidebar ? 1 : 0, showDockerInSidebar: showDockerInSidebar ? 1 : 0,
showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0, showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0,
defaultPath: defaultPath || null, defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null, statsConfig: statsConfig
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null, ? typeof statsConfig === "string"
? statsConfig
: JSON.stringify(statsConfig)
: null,
terminalConfig: terminalConfig
? typeof terminalConfig === "string"
? terminalConfig
: JSON.stringify(terminalConfig)
: null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false", forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
notes: notes || null, notes: notes || null,
sudoPassword: sudoPassword || null, sudoPassword: sudoPassword || null,
@@ -929,7 +939,7 @@ router.put(
sshDataObj.password = password; sshDataObj.password = password;
} }
sshDataObj.key = null; sshDataObj.key = null;
sshDataObj.key_password = null; sshDataObj.keyPassword = null;
sshDataObj.keyType = null; sshDataObj.keyType = null;
} else if (effectiveAuthType === "key") { } else if (effectiveAuthType === "key") {
if (key && typeof key === "string") { if (key && typeof key === "string") {
@@ -969,7 +979,7 @@ router.put(
sshDataObj.key = key; sshDataObj.key = key;
} }
if (keyPassword !== undefined) { if (keyPassword !== undefined) {
sshDataObj.key_password = keyPassword || null; sshDataObj.keyPassword = keyPassword || null;
} }
if (keyType) { if (keyType) {
sshDataObj.keyType = keyType; sshDataObj.keyType = keyType;
@@ -978,7 +988,7 @@ router.put(
} else { } else {
sshDataObj.password = null; sshDataObj.password = null;
sshDataObj.key = null; sshDataObj.key = null;
sshDataObj.key_password = null; sshDataObj.keyPassword = null;
sshDataObj.keyType = null; sshDataObj.keyType = null;
} }
@@ -1055,10 +1065,9 @@ router.put(
hostRecord[0].credentialId !== null && hostRecord[0].credentialId !== null &&
sshDataObj.credentialId === null sshDataObj.credentialId === null
) { ) {
const revokedShares = await db await db
.delete(hostAccess) .delete(hostAccess)
.where(eq(hostAccess.hostId, Number(hostId))) .where(eq(hostAccess.hostId, Number(hostId)));
.returning({ id: hostAccess.id, userId: hostAccess.userId });
} }
} }
@@ -1189,7 +1198,7 @@ router.get(
authType: sshData.authType, authType: sshData.authType,
password: sshData.password, password: sshData.password,
key: sshData.key, key: sshData.key,
keyPassword: sshData.key_password, keyPassword: sshData.keyPassword,
keyType: sshData.keyType, keyType: sshData.keyType,
enableTerminal: sshData.enableTerminal, enableTerminal: sshData.enableTerminal,
enableTunnel: sshData.enableTunnel, enableTunnel: sshData.enableTunnel,
@@ -1264,7 +1273,7 @@ router.get(
const ownHosts = rawData.filter((row) => row.userId === userId); const ownHosts = rawData.filter((row) => row.userId === userId);
const sharedHosts = rawData.filter((row) => row.userId !== userId); const sharedHosts = rawData.filter((row) => row.userId !== userId);
let decryptedOwnHosts: any[] = []; let decryptedOwnHosts: Record<string, unknown>[] = [];
try { try {
decryptedOwnHosts = await SimpleDBOps.select( decryptedOwnHosts = await SimpleDBOps.select(
Promise.resolve(ownHosts), Promise.resolve(ownHosts),
@@ -1447,24 +1456,53 @@ router.get(
authType: resolvedHost.authType, authType: resolvedHost.authType,
password: resolvedHost.password || null, password: resolvedHost.password || null,
key: resolvedHost.key || null, key: resolvedHost.key || null,
keyPassword: resolvedHost.key_password || null, keyPassword: resolvedHost.keyPassword || null,
keyType: resolvedHost.keyType || null, keyType: resolvedHost.keyType || null,
credentialId: resolvedHost.credentialId || null,
overrideCredentialUsername: !!resolvedHost.overrideCredentialUsername,
folder: resolvedHost.folder, folder: resolvedHost.folder,
tags: tags:
typeof resolvedHost.tags === "string" typeof resolvedHost.tags === "string"
? resolvedHost.tags.split(",").filter(Boolean) ? resolvedHost.tags.split(",").filter(Boolean)
: resolvedHost.tags || [], : resolvedHost.tags || [],
pin: !!resolvedHost.pin, pin: !!resolvedHost.pin,
notes: resolvedHost.notes || null,
enableTerminal: !!resolvedHost.enableTerminal, enableTerminal: !!resolvedHost.enableTerminal,
enableTunnel: !!resolvedHost.enableTunnel, enableTunnel: !!resolvedHost.enableTunnel,
enableFileManager: !!resolvedHost.enableFileManager, enableFileManager: !!resolvedHost.enableFileManager,
enableDocker: !!resolvedHost.enableDocker,
showTerminalInSidebar: !!resolvedHost.showTerminalInSidebar,
showFileManagerInSidebar: !!resolvedHost.showFileManagerInSidebar,
showTunnelInSidebar: !!resolvedHost.showTunnelInSidebar,
showDockerInSidebar: !!resolvedHost.showDockerInSidebar,
showServerStatsInSidebar: !!resolvedHost.showServerStatsInSidebar,
defaultPath: resolvedHost.defaultPath, defaultPath: resolvedHost.defaultPath,
sudoPassword: resolvedHost.sudoPassword || null,
tunnelConnections: resolvedHost.tunnelConnections tunnelConnections: resolvedHost.tunnelConnections
? JSON.parse(resolvedHost.tunnelConnections as string) ? 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 socks5ProxyChain: resolvedHost.socks5ProxyChain
? JSON.parse(resolvedHost.socks5ProxyChain as string) ? JSON.parse(resolvedHost.socks5ProxyChain as string)
: [], : null,
}; };
sshLogger.success("Host exported with decrypted credentials", { sshLogger.success("Host exported with decrypted credentials", {
@@ -1583,7 +1621,6 @@ router.delete(
.delete(sshData) .delete(sshData)
.where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId))); .where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId)));
const host = hostToDelete[0];
databaseLogger.success("SSH host deleted", { databaseLogger.success("SSH host deleted", {
operation: "host_delete_success", operation: "host_delete_success",
userId, userId,
@@ -2417,8 +2454,8 @@ async function resolveHostCredentials(
...host, ...host,
password: credential.password, password: credential.password,
key: credential.key, key: credential.key,
keyPassword: credential.key_password || credential.keyPassword, keyPassword: credential.keyPassword,
keyType: credential.key_type || credential.keyType, keyType: credential.keyType,
}; };
if (!host.overrideCredentialUsername) { if (!host.overrideCredentialUsername) {
@@ -2429,14 +2466,7 @@ async function resolveHostCredentials(
} }
} }
const result = { ...host }; return { ...host };
if (host.key_password !== undefined) {
if (result.keyPassword === undefined) {
result.keyPassword = host.key_password;
}
delete result.key_password;
}
return result;
} catch (error) { } catch (error) {
sshLogger.warn( sshLogger.warn(
`Failed to resolve credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`, `Failed to resolve credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
@@ -2846,12 +2876,142 @@ router.delete(
* 400: * 400:
* description: Invalid request body. * 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( router.post(
"/bulk-import", "/bulk-import",
authenticateJWT, authenticateJWT,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId; const userId = (req as AuthenticatedRequest).userId;
const { hosts } = req.body; const { hosts, overwrite } = req.body;
if (!Array.isArray(hosts) || hosts.length === 0) { if (!Array.isArray(hosts) || hosts.length === 0) {
return res return res
@@ -2867,10 +3027,30 @@ router.post(
const results = { const results = {
success: 0, success: 0,
updated: 0,
skipped: 0,
failed: 0, failed: 0,
errors: [] as string[], 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++) { for (let i = 0; i < hosts.length; i++) {
const hostData = hosts[i]; const hostData = hosts[i];
@@ -2940,9 +3120,7 @@ router.post(
hostData.authType === "credential" ? hostData.credentialId : null, hostData.authType === "credential" ? hostData.credentialId : null,
key: hostData.authType === "key" ? hostData.key : null, key: hostData.authType === "key" ? hostData.key : null,
keyPassword: keyPassword:
hostData.authType === "key" hostData.authType === "key" ? hostData.keyPassword || null : null,
? hostData.keyPassword || hostData.key_password || null
: null,
keyType: keyType:
hostData.authType === "key" ? hostData.keyType || "auto" : null, hostData.authType === "key" ? hostData.keyType || "auto" : null,
pin: hostData.pin || false, pin: hostData.pin || false,
@@ -2950,7 +3128,13 @@ router.post(
enableTunnel: hostData.enableTunnel !== false, enableTunnel: hostData.enableTunnel !== false,
enableFileManager: hostData.enableFileManager !== false, enableFileManager: hostData.enableFileManager !== false,
enableDocker: hostData.enableDocker || 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 || "/", defaultPath: hostData.defaultPath || "/",
sudoPassword: hostData.sudoPassword || null,
tunnelConnections: hostData.tunnelConnections tunnelConnections: hostData.tunnelConnections
? JSON.stringify(hostData.tunnelConnections) ? JSON.stringify(hostData.tunnelConnections)
: "[]", : "[]",
@@ -2981,12 +3165,26 @@ router.post(
overrideCredentialUsername: hostData.overrideCredentialUsername overrideCredentialUsername: hostData.overrideCredentialUsername
? 1 ? 1
: 0, : 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}; };
await SimpleDBOps.insert(sshData, "ssh_data", sshDataObj, userId); const lookupKey = `${hostData.ip}:${hostData.port}:${hostData.username}`;
results.success++; 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) { } catch (error) {
results.failed++; results.failed++;
results.errors.push( results.errors.push(
@@ -2996,8 +3194,10 @@ router.post(
} }
res.json({ 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, success: results.success,
updated: results.updated,
skipped: results.skipped,
failed: results.failed, failed: results.failed,
errors: results.errors, errors: results.errors,
}); });
@@ -3130,7 +3330,7 @@ router.post(
...tunnel, ...tunnel,
endpointPassword: decryptedEndpoint.password || null, endpointPassword: decryptedEndpoint.password || null,
endpointKey: decryptedEndpoint.key || null, endpointKey: decryptedEndpoint.key || null,
endpointKeyPassword: decryptedEndpoint.key_password || null, endpointKeyPassword: decryptedEndpoint.keyPassword || null,
endpointAuthType: endpointHost.authType, endpointAuthType: endpointHost.authType,
}; };
} }
@@ -3153,7 +3353,7 @@ router.post(
.set({ .set({
autostartPassword: decryptedConfig.password || null, autostartPassword: decryptedConfig.password || null,
autostartKey: decryptedConfig.key || null, autostartKey: decryptedConfig.key || null,
autostartKeyPassword: decryptedConfig.key_password || null, autostartKeyPassword: decryptedConfig.keyPassword || null,
tunnelConnections: updatedTunnelConnections, tunnelConnections: updatedTunnelConnections,
}) })
.where(eq(sshData.id, sshConfigId)); .where(eq(sshData.id, sshConfigId));
@@ -3466,9 +3666,17 @@ function rewriteOPKSSHHtml(
): string { ): string {
const basePath = `/ssh/${routePrefix}/${requestId}`; const basePath = `/ssh/${routePrefix}/${requestId}`;
html = html.replace(/action=["']?\//g, `action="${basePath}/`); const attrPatterns = ["action", "href", "src"];
html = html.replace(/href=["']?\//g, `href="${basePath}/`); for (const attr of attrPatterns) {
html = html.replace(/src=["']?\//g, `src="${basePath}/`); html = html.replace(
new RegExp(`${attr}="(/[^"]*)`, "g"),
`${attr}="${basePath}$1`,
);
html = html.replace(
new RegExp(`${attr}='(/[^']*)`, "g"),
`${attr}='${basePath}$1`,
);
}
html = html.replace( html = html.replace(
/href=["']?http:\/\/localhost:\d+\/([^"'\s]*)/g, /href=["']?http:\/\/localhost:\d+\/([^"'\s]*)/g,
@@ -3518,11 +3726,37 @@ function rewriteOPKSSHHtml(
const baseTag = `<base href="${basePath}/">`; const baseTag = `<base href="${basePath}/">`;
if (html.includes("<base")) { if (html.includes("<base")) {
sshLogger.info("Replacing existing base tag", {
operation: "opkssh_html_rewrite_base_tag",
requestId,
basePath,
});
html = html.replace(/<base[^>]*>/i, baseTag); 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}`); 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; return html;
} }
@@ -3555,11 +3789,29 @@ router.use(
? req.params.requestId[0] ? req.params.requestId[0]
: req.params.requestId; : 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 { try {
const { getActiveAuthSession } = await import("../../ssh/opkssh-auth.js"); const { getActiveAuthSession } = await import("../../ssh/opkssh-auth.js");
const session = getActiveAuthSession(requestId); const session = getActiveAuthSession(requestId);
if (!session) { if (!session) {
sshLogger.error("Session not found for chooser request", {
operation: "opkssh_chooser_session_not_found",
requestId,
});
res.status(404).send(` res.status(404).send(`
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@@ -3682,6 +3934,14 @@ router.use(
const targetUrl = `http://localhost:${session.localPort}${targetPath}`; 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({ const response = await axios({
method: req.method, method: req.method,
url: targetUrl, url: targetUrl,
@@ -3696,6 +3956,15 @@ router.use(
responseType: "arraybuffer", 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]) => { Object.entries(response.headers).forEach(([key, value]) => {
if (key.toLowerCase() === "transfer-encoding") { if (key.toLowerCase() === "transfer-encoding") {
return; return;
@@ -3821,57 +4090,103 @@ router.use(
*/ */
router.get("/opkssh-callback", async (req: Request, res: Response) => { router.get("/opkssh-callback", async (req: Request, res: Response) => {
try { 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 { getUserIdFromRequest, getActiveSessionsForUser } =
const session = allSessions[allSessions.length - 1]; 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"); res.status(404).send("No active authentication session found");
return; return;
} }
const session = userSessions[userSessions.length - 1];
if (!session.callbackPort) { 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"); res.status(503).send("OPKSSH callback listener not ready yet");
return; return;
} }
const axios = (await import("axios")).default;
const queryString = req.url.includes("?") const queryString = req.url.includes("?")
? req.url.substring(req.url.indexOf("?")) ? 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", { sshLogger.info("Redirecting OAuth callback to dynamic route", {
operation: "opkssh_static_callback", operation: "opkssh_static_callback_redirect",
userId,
requestId: session.requestId,
callbackPort: session.callbackPort, callbackPort: session.callbackPort,
targetUrl, queryParams: Object.keys(req.query),
redirectUrl,
}); });
const response = await axios({ res.redirect(302, redirectUrl);
method: req.method,
url: targetUrl,
headers: {
...req.headers,
host: `localhost:${session.callbackPort}`,
},
data: req.body,
timeout: 10000,
validateStatus: () => true,
maxRedirects: 0,
responseType: "arraybuffer",
});
Object.entries(response.headers).forEach(([key, value]) => {
if (key.toLowerCase() !== "transfer-encoding") {
res.setHeader(key, value as string);
}
});
res.status(response.status).send(response.data);
} catch (error) { } catch (error) {
sshLogger.error("Error handling OPKSSH static callback", error, { sshLogger.error("Error handling OPKSSH static callback", error, {
operation: "opkssh_static_callback_error", operation: "opkssh_static_callback_error",
url: req.url,
originalUrl: req.originalUrl,
}); });
res.status(500).send("Authentication callback failed"); 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; 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; 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 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 type { WebSocket } from "ws";
import { sshLogger, authLogger } from "../utils/logger.js"; import { sshLogger, authLogger } from "../utils/logger.js";
import { getDb } from "../database/db/index.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 { eq, and } from "drizzle-orm";
import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { UserCrypto } from "../utils/user-crypto.js";
interface ResolvedCredentials { interface ResolvedCredentials {
username: string; username: string;
password?: string; password?: string;
@@ -44,9 +41,6 @@ interface AuthContext {
totpAttempts: number; totpAttempts: number;
} }
const userCrypto = UserCrypto.getInstance();
const MAX_TOTP_ATTEMPTS = 3;
export class SSHAuthManager { export class SSHAuthManager {
public context: AuthContext; public context: AuthContext;
@@ -85,7 +79,7 @@ export class SSHAuthManager {
key: cred.privateKey key: cred.privateKey
? Buffer.from(cred.privateKey as string) ? Buffer.from(cred.privateKey as string)
: undefined, : undefined,
keyPassword: (cred.passphrase as string) || undefined, keyPassword: (cred.keyPassword as string) || undefined,
authType: (cred.authType as string) || "none", authType: (cred.authType as string) || "none",
}; };
} }
@@ -163,7 +157,7 @@ export class SSHAuthManager {
); );
if (urlMatch) { if (urlMatch) {
this.context.keyboardInteractiveFinish = (responses: string[]) => { this.context.keyboardInteractiveFinish = () => {
finish([""]); finish([""]);
}; };
@@ -358,7 +352,7 @@ export class SSHAuthManager {
stage: string, stage: string,
level: string, level: string,
message: string, message: string,
details?: Record<string, any>, details?: Record<string, unknown>,
): void { ): void {
this.context.ws.send( this.context.ws.send(
JSON.stringify({ JSON.stringify({
+54 -38
View File
@@ -13,7 +13,7 @@ const sshLogger = systemLogger;
interface SSHSession { interface SSHSession {
client: SSHClient; client: SSHClient;
stream: any; stream: import("ssh2").ClientChannel | null;
isConnected: boolean; isConnected: boolean;
containerId?: string; containerId?: string;
shell?: string; shell?: string;
@@ -42,7 +42,7 @@ const wss = new WebSocketServer({
} }
return true; return true;
} catch (error) { } catch {
return false; return false;
} }
}, },
@@ -92,7 +92,7 @@ async function detectShell(
} }
async function createJumpHostChain( async function createJumpHostChain(
jumpHosts: any[], jumpHosts: Array<{ hostId: number }>,
userId: string, userId: string,
): Promise<SSHClient | null> { ): Promise<SSHClient | null> {
if (!jumpHosts || jumpHosts.length === 0) { 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, password: jumpHost.password,
sshKey: jumpHost.key, sshKey: jumpHost.key,
keyPassword: jumpHost.keyPassword, keyPassword: jumpHost.keyPassword,
@@ -154,19 +159,18 @@ async function createJumpHostChain(
if (credentials.length > 0) { if (credentials.length > 0) {
const credential = credentials[0]; const credential = credentials[0];
resolvedCredentials = { resolvedCredentials = {
password: credential.password, password: credential.password as string | undefined,
sshKey: sshKey: credential.privateKey as string | undefined,
credential.private_key || credential.privateKey || credential.key, keyPassword: credential.keyPassword as string | undefined,
keyPassword: credential.key_password || credential.keyPassword, authType: credential.authType as string | undefined,
authType: credential.auth_type || credential.authType,
}; };
} }
} }
const client = new SSHClient(); const client = new SSHClient();
const config: any = { const config: Record<string, unknown> = {
host: jumpHost.ip, host: jumpHost.ip?.replace(/^\[|\]$/g, "") || jumpHost.ip,
port: jumpHost.port || 22, port: jumpHost.port || 22,
username: jumpHost.username, username: jumpHost.username,
tryKeyboard: true, tryKeyboard: true,
@@ -225,7 +229,7 @@ async function createJumpHostChain(
} }
wss.on("connection", async (ws: WebSocket, req) => { 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()}`; const sessionId = `docker-console-${Date.now()}-${Math.random()}`;
sshLogger.info("Docker console WebSocket connected", { sshLogger.info("Docker console WebSocket connected", {
operation: "docker_console_connect", operation: "docker_console_connect",
@@ -235,6 +239,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
let sshSession: SSHSession | null = null; let sshSession: SSHSession | null = null;
const wsPingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 30000);
ws.on("message", async (data) => { ws.on("message", async (data) => {
try { try {
const message = JSON.parse(data.toString()); const message = JSON.parse(data.toString());
@@ -286,7 +296,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
} }
try { try {
let resolvedCredentials: any = { let resolvedCredentials: {
password?: string;
sshKey?: string;
keyPassword?: string;
authType?: string;
} = {
password: hostConfig.password, password: hostConfig.password,
sshKey: hostConfig.key, sshKey: hostConfig.key,
keyPassword: hostConfig.keyPassword, keyPassword: hostConfig.keyPassword,
@@ -311,22 +326,18 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (credentials.length > 0) { if (credentials.length > 0) {
const credential = credentials[0]; const credential = credentials[0];
resolvedCredentials = { resolvedCredentials = {
password: credential.password, password: credential.password as string | undefined,
sshKey: sshKey: credential.privateKey as string | undefined,
credential.private_key || keyPassword: credential.keyPassword as string | undefined,
credential.privateKey || authType: credential.authType as string | undefined,
credential.key,
keyPassword:
credential.key_password || credential.keyPassword,
authType: credential.auth_type || credential.authType,
}; };
} }
} }
const client = new SSHClient(); const client = new SSHClient();
const config: any = { const config: Record<string, unknown> = {
host: hostConfig.ip, host: hostConfig.ip?.replace(/^\[|\]$/g, "") || hostConfig.ip,
port: hostConfig.port || 22, port: hostConfig.port || 22,
username: hostConfig.username, username: hostConfig.username,
tryKeyboard: true, tryKeyboard: true,
@@ -362,18 +373,20 @@ wss.on("connection", async (ws: WebSocket, req) => {
userId, userId,
); );
if (jumpClient) { if (jumpClient) {
const stream = await new Promise<any>((resolve, reject) => { const stream = await new Promise<import("ssh2").ClientChannel>(
jumpClient.forwardOut( (resolve, reject) => {
"127.0.0.1", jumpClient.forwardOut(
0, "127.0.0.1",
hostConfig.ip, 0,
hostConfig.port || 22, hostConfig.ip,
(err, stream) => { hostConfig.port || 22,
if (err) return reject(err); (err, stream) => {
resolve(stream); if (err) return reject(err);
}, resolve(stream);
); },
}); );
},
);
config.sock = 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", () => { stream.on("close", () => {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
@@ -556,7 +571,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
case "resize": { case "resize": {
if (sshSession && sshSession.stream) { if (sshSession && sshSession.stream) {
const { cols, rows } = message.data; const { cols, rows } = message.data;
sshSession.stream.setWindow(rows, cols); sshSession.stream.setWindow(rows, cols, rows, cols);
} }
break; break;
} }
@@ -608,6 +623,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
}); });
ws.on("close", () => { ws.on("close", () => {
clearInterval(wsPingInterval);
sshLogger.info("Docker console disconnected", { sshLogger.info("Docker console disconnected", {
operation: "docker_console_disconnect", operation: "docker_console_disconnect",
sessionId, sessionId,
@@ -641,7 +657,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
}); });
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
activeSessions.forEach((session, sessionId) => { activeSessions.forEach((session) => {
if (session.stream) { if (session.stream) {
session.stream.end(); session.stream.end();
} }
+267 -172
View File
@@ -3,15 +3,17 @@ import cors from "cors";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import axios from "axios"; import axios from "axios";
import { Client as SSHClient } from "ssh2"; import { Client as SSHClient } from "ssh2";
import type { ClientChannel } from "ssh2";
import { getDb } from "../database/db/index.js"; import { getDb } from "../database/db/index.js";
import { sshData, sshCredentials } from "../database/db/schema.js"; import { sshData, sshCredentials } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { logger } from "../utils/logger.js"; import { logger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js"; import { AuthManager } from "../utils/auth-manager.js";
import { createSocks5Connection } from "../utils/socks5-helper.js"; import {
import type { AuthenticatedRequest, SSHHost } from "../../types/index.js"; 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 type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
import { SSHHostKeyVerifier } from "./host-key-verifier.js"; import { SSHHostKeyVerifier } from "./host-key-verifier.js";
@@ -21,7 +23,7 @@ function createConnectionLog(
type: "info" | "success" | "warning" | "error", type: "info" | "success" | "warning" | "error",
stage: ConnectionStage, stage: ConnectionStage,
message: string, message: string,
details?: Record<string, any>, details?: Record<string, unknown>,
): Omit<LogEntry, "id" | "timestamp"> { ): Omit<LogEntry, "id" | "timestamp"> {
return { return {
type, type,
@@ -43,7 +45,7 @@ interface SSHSession {
interface PendingTOTPSession { interface PendingTOTPSession {
client: SSHClient; client: SSHClient;
finish: (responses: string[]) => void; finish: (responses: string[]) => void;
config: any; config: Record<string, unknown>;
createdAt: number; createdAt: number;
sessionId: string; sessionId: string;
hostId?: number; hostId?: number;
@@ -70,7 +72,9 @@ setInterval(() => {
if (now - session.createdAt > 180000) { if (now - session.createdAt > 180000) {
try { try {
session.client.end(); session.client.end();
} catch {} } catch {
// expected
}
delete pendingTOTPSessions[sessionId]; delete pendingTOTPSessions[sessionId];
} }
}); });
@@ -94,7 +98,9 @@ function cleanupSession(sessionId: string) {
try { try {
session.client.end(); session.client.end();
} catch (error) {} } catch {
// expected
}
clearTimeout(session.timeout); clearTimeout(session.timeout);
delete sshSessions[sessionId]; 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( async function resolveJumpHost(
hostId: number, hostId: number,
userId: string, userId: string,
): Promise<any | null> { ): Promise<JumpHostConfig | null> {
try { try {
const hosts = await SimpleDBOps.select( const hosts = await SimpleDBOps.select(
getDb() getDb()
@@ -150,17 +170,16 @@ async function resolveJumpHost(
const credential = credentials[0]; const credential = credentials[0];
return { return {
...host, ...host,
password: credential.password, password: credential.password as string | undefined,
key: key: credential.privateKey as string | undefined,
credential.private_key || credential.privateKey || credential.key, keyPassword: credential.keyPassword as string | undefined,
keyPassword: credential.key_password || credential.keyPassword, keyType: credential.keyType as string | undefined,
keyType: credential.key_type || credential.keyType, authType: credential.authType as string | undefined,
authType: credential.auth_type || credential.authType, } as JumpHostConfig;
};
} }
} }
return host; return host as JumpHostConfig;
} catch (error) { } catch (error) {
sshLogger.error("Failed to resolve jump host", error, { sshLogger.error("Failed to resolve jump host", error, {
operation: "resolve_jump_host", operation: "resolve_jump_host",
@@ -174,6 +193,7 @@ async function resolveJumpHost(
async function createJumpHostChain( async function createJumpHostChain(
jumpHosts: Array<{ hostId: number }>, jumpHosts: Array<{ hostId: number }>,
userId: string, userId: string,
socks5Config?: SOCKS5Config | null,
): Promise<SSHClient | null> { ): Promise<SSHClient | null> {
if (!jumpHosts || jumpHosts.length === 0) { if (!jumpHosts || jumpHosts.length === 0) {
return null; return null;
@@ -187,19 +207,33 @@ async function createJumpHostChain(
jumpHosts.map((jh) => resolveJumpHost(jh.hostId, userId)), jumpHosts.map((jh) => resolveJumpHost(jh.hostId, userId)),
); );
const totalHops = jumpHostConfigs.length;
for (let i = 0; i < jumpHostConfigs.length; i++) { for (let i = 0; i < jumpHostConfigs.length; i++) {
if (!jumpHostConfigs[i]) { if (!jumpHostConfigs[i]) {
sshLogger.error(`Jump host ${i + 1} not found`, undefined, { sshLogger.error(`Jump host ${i + 1} not found`, undefined, {
operation: "jump_host_chain", operation: "jump_host_chain",
hostId: jumpHosts[i].hostId, hostId: jumpHosts[i].hostId,
hopIndex: i,
totalHops,
}); });
clients.forEach((c) => c.end()); clients.forEach((c) => c.end());
return null; 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++) { for (let i = 0; i < jumpHostConfigs.length; i++) {
const jumpHostConfig = jumpHostConfigs[i]; const jumpHostConfig = jumpHostConfigs[i]!;
const jumpClient = new SSHClient(); const jumpClient = new SSHClient();
clients.push(jumpClient); clients.push(jumpClient);
@@ -225,16 +259,29 @@ async function createJumpHostChain(
jumpClient.on("error", (err) => { jumpClient.on("error", (err) => {
clearTimeout(timeout); clearTimeout(timeout);
sshLogger.error(`Jump host ${i + 1} connection failed`, err, { sshLogger.error(
operation: "jump_host_connect", `Jump host ${i + 1}/${totalHops} connection failed`,
hostId: jumpHostConfig.id, err,
ip: jumpHostConfig.ip, {
}); 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); resolve(false);
}); });
const connectConfig: any = { const connectConfig: Record<string, unknown> = {
host: jumpHostConfig.ip, host: jumpHostConfig.ip?.replace(/^\[|\]$/g, "") || jumpHostConfig.ip,
port: jumpHostConfig.port || 22, port: jumpHostConfig.port || 22,
username: jumpHostConfig.username, username: jumpHostConfig.username,
tryKeyboard: true, tryKeyboard: true,
@@ -271,6 +318,9 @@ async function createJumpHostChain(
jumpClient.connect(connectConfig); jumpClient.connect(connectConfig);
}, },
); );
} else if (proxySocket) {
connectConfig.sock = proxySocket;
jumpClient.connect(connectConfig);
} else { } else {
jumpClient.connect(connectConfig); jumpClient.connect(connectConfig);
} }
@@ -411,6 +461,10 @@ app.use(
app.use(cookieParser()); app.use(cookieParser());
app.use(express.json({ limit: "100mb" })); app.use(express.json({ limit: "100mb" }));
app.use(express.urlencoded({ limit: "100mb", extended: true })); app.use(express.urlencoded({ limit: "100mb", extended: true }));
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "no-store");
next();
});
const authManager = AuthManager.getInstance(); const authManager = AuthManager.getInstance();
app.use(authManager.createAuthMiddleware()); app.use(authManager.createAuthMiddleware());
@@ -450,7 +504,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
userProvidedPassword, userProvidedPassword,
userProvidedSshKey, userProvidedSshKey,
userProvidedKeyPassword, userProvidedKeyPassword,
forceKeyboardInteractive,
useSocks5, useSocks5,
socks5Host, socks5Host,
socks5Port, socks5Port,
@@ -458,7 +511,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
socks5Password, socks5Password,
socks5ProxyChain, socks5ProxyChain,
} = req.body; } = req.body;
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
const connectionLogs: Array<Omit<LogEntry, "id" | "timestamp">> = []; 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; const host = hosts[0] as unknown as SSHHost;
if (host.userId !== userId) { if (host.userId !== userId) {
const { PermissionManager } = const { PermissionManager } = await import(
await import("../utils/permission-manager.js"); "../utils/permission-manager.js"
);
const permissionManager = PermissionManager.getInstance(); const permissionManager = PermissionManager.getInstance();
const accessInfo = await permissionManager.canAccessHost( const accessInfo = await permissionManager.canAccessHost(
userId, userId,
@@ -605,11 +659,18 @@ app.post("/docker/ssh/connect", async (req, res) => {
if (pendingTOTPSessions[sessionId]) { if (pendingTOTPSessions[sessionId]) {
try { try {
pendingTOTPSessions[sessionId].client.end(); pendingTOTPSessions[sessionId].client.end();
} catch {} } catch {
// expected
}
delete pendingTOTPSessions[sessionId]; delete pendingTOTPSessions[sessionId];
} }
let resolvedCredentials: any = { let resolvedCredentials: {
password?: string;
sshKey?: string;
keyPassword?: string;
authType?: string;
} = {
password: host.password, password: host.password,
sshKey: host.key, sshKey: host.key,
keyPassword: host.keyPassword, keyPassword: host.keyPassword,
@@ -632,8 +693,9 @@ app.post("/docker/ssh/connect", async (req, res) => {
if (userId !== ownerId) { if (userId !== ownerId) {
try { try {
const { SharedCredentialManager } = const { SharedCredentialManager } = await import(
await import("../utils/shared-credential-manager.js"); "../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance(); const sharedCredManager = SharedCredentialManager.getInstance();
const sharedCred = await sharedCredManager.getSharedCredentialForUser( const sharedCred = await sharedCredManager.getSharedCredentialForUser(
host.id, host.id,
@@ -673,11 +735,10 @@ app.post("/docker/ssh/connect", async (req, res) => {
if (credentials.length > 0) { if (credentials.length > 0) {
const credential = credentials[0]; const credential = credentials[0];
resolvedCredentials = { resolvedCredentials = {
password: credential.password, password: credential.password as string | undefined,
sshKey: sshKey: credential.privateKey as string | undefined,
credential.private_key || credential.privateKey || credential.key, keyPassword: credential.keyPassword as string | undefined,
keyPassword: credential.key_password || credential.keyPassword, authType: credential.authType as string | undefined,
authType: credential.auth_type || credential.authType,
}; };
} }
} }
@@ -685,8 +746,8 @@ app.post("/docker/ssh/connect", async (req, res) => {
const client = new SSHClient(); const client = new SSHClient();
const config: any = { const config: Record<string, unknown> = {
host: host.ip, host: host.ip?.replace(/^\[|\]$/g, "") || host.ip,
port: host.port || 22, port: host.port || 22,
username: host.username, username: host.username,
tryKeyboard: true, tryKeyboard: true,
@@ -706,6 +767,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
}; };
if (resolvedCredentials.authType === "none") { if (resolvedCredentials.authType === "none") {
// no credentials needed
} else if (resolvedCredentials.authType === "password") { } else if (resolvedCredentials.authType === "password") {
if (resolvedCredentials.password) { if (resolvedCredentials.password) {
config.password = resolvedCredentials.password; config.password = resolvedCredentials.password;
@@ -871,8 +933,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
} }
let responseSent = false; let responseSent = false;
let keyboardInteractiveResponded = false;
connectionLogs.push( connectionLogs.push(
createConnectionLog("info", "dns", `Resolving DNS for ${host.ip}`), createConnectionLog("info", "dns", `Resolving DNS for ${host.ip}`),
); );
@@ -1089,8 +1149,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
if (responseSent) return; if (responseSent) return;
responseSent = true; responseSent = true;
keyboardInteractiveResponded = true;
pendingTOTPSessions[sessionId] = { pendingTOTPSessions[sessionId] = {
client, client,
finish, finish,
@@ -1158,8 +1216,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
return; return;
} }
keyboardInteractiveResponded = true;
pendingTOTPSessions[sessionId] = { pendingTOTPSessions[sessionId] = {
client, client,
finish, finish,
@@ -1244,8 +1300,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
return; return;
} }
keyboardInteractiveResponded = true;
pendingTOTPSessions[sessionId] = { pendingTOTPSessions[sessionId] = {
client, client,
finish, finish,
@@ -1283,121 +1337,160 @@ app.post("/docker/ssh/connect", async (req, res) => {
}, },
); );
if ( const proxyConfig: SOCKS5Config | null =
useSocks5 && useSocks5 &&
(socks5Host || (socks5ProxyChain && (socks5ProxyChain as any).length > 0)) (socks5Host ||
) { (socks5ProxyChain && (socks5ProxyChain as ProxyNode[]).length > 0))
try { ? {
connectionLogs.push(
createConnectionLog("info", "proxy", "Connecting via SOCKS5 proxy"),
);
const socks5Socket = await createSocks5Connection(
host.ip,
host.port || 22,
{
useSocks5, useSocks5,
socks5Host, socks5Host,
socks5Port, socks5Port,
socks5Username, socks5Username,
socks5Password, 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) { if (!jumpClient) {
config.sock = socks5Socket; connectionLogs.push(
client.connect(config); createConnectionLog(
return; "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, { jumpClient.forwardOut(
operation: "docker_socks5_connect", "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, sessionId,
hostId, hostId,
proxyHost: socks5Host,
proxyPort: socks5Port || 1080,
}); });
connectionLogs.push( connectionLogs.push(
createConnectionLog( createConnectionLog(
"error", "error",
"proxy", "jump",
`SOCKS5 proxy connection failed: ${socks5Error instanceof Error ? socks5Error.message : "Unknown error"}`, `Jump host connection failed: ${jumpError instanceof Error ? jumpError.message : "Unknown error"}`,
), ),
); );
if (!responseSent) { if (!responseSent) {
responseSent = true; responseSent = true;
return res.status(500).json({ return res.status(500).json({
error: error:
"SOCKS5 proxy connection failed: " + "Jump host connection failed: " +
(socks5Error instanceof Error (jumpError instanceof Error
? socks5Error.message ? jumpError.message
: "Unknown error"), : "Unknown error"),
connectionLogs, connectionLogs,
}); });
} }
return; return;
} }
} else if (host.jumpHosts && host.jumpHosts.length > 0) { } else if (proxyConfig) {
connectionLogs.push( connectionLogs.push(
createConnectionLog( createConnectionLog("info", "proxy", "Connecting via proxy"),
"info",
"jump",
`Connecting via ${host.jumpHosts.length} jump host(s)`,
),
); );
const jumpClient = await createJumpHostChain( try {
host.jumpHosts as Array<{ hostId: number }>, const proxySocket = await createSocks5Connection(
userId, host.ip,
); host.port || 22,
proxyConfig,
if (!jumpClient) { );
if (proxySocket) {
config.sock = proxySocket;
}
client.connect(config);
} catch (proxyError) {
sshLogger.error("Proxy connection failed", proxyError, {
operation: "docker_proxy_connect",
sessionId,
hostId,
});
connectionLogs.push( connectionLogs.push(
createConnectionLog( createConnectionLog(
"error", "error",
"jump", "proxy",
"Failed to establish jump host chain", `Proxy connection failed: ${proxyError instanceof Error ? proxyError.message : "Unknown error"}`,
), ),
); );
return res.status(500).json({ if (!responseSent) {
error: "Failed to establish jump host chain", responseSent = true;
connectionLogs, 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 { } else {
client.connect(config); client.connect(config);
} }
@@ -1489,7 +1582,7 @@ app.post("/docker/ssh/disconnect", async (req, res) => {
*/ */
app.post("/docker/ssh/connect-totp", async (req, res) => { app.post("/docker/ssh/connect-totp", async (req, res) => {
const { sessionId, totpCode } = req.body; const { sessionId, totpCode } = req.body;
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
sshLogger.error("TOTP verification rejected: no authenticated user", { sshLogger.error("TOTP verification rejected: no authenticated user", {
@@ -1521,7 +1614,9 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
delete pendingTOTPSessions[sessionId]; delete pendingTOTPSessions[sessionId];
try { try {
session.client.end(); session.client.end();
} catch {} } catch {
// expected
}
sshLogger.warn("TOTP session timeout before code submission", { sshLogger.warn("TOTP session timeout before code submission", {
operation: "docker_totp_verify", operation: "docker_totp_verify",
sessionId, sessionId,
@@ -1544,7 +1639,19 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
}); });
let responseSent = false; 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", () => { session.client.once("ready", () => {
if (responseSent) return; 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" }); 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); 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) => { app.post("/docker/ssh/connect-warpgate", async (req, res) => {
const { sessionId } = req.body; const { sessionId } = req.body;
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
sshLogger.error("Warpgate verification rejected: no authenticated user", { sshLogger.error("Warpgate verification rejected: no authenticated user", {
@@ -1715,7 +1809,9 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
delete pendingTOTPSessions[sessionId]; delete pendingTOTPSessions[sessionId];
try { try {
session.client.end(); session.client.end();
} catch {} } catch {
// expected
}
sshLogger.warn("Warpgate session timeout before completion", { sshLogger.warn("Warpgate session timeout before completion", {
operation: "docker_warpgate_verify", operation: "docker_warpgate_verify",
sessionId, sessionId,
@@ -1728,7 +1824,19 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
} }
let responseSent = false; 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", () => { session.client.once("ready", () => {
if (responseSent) return; if (responseSent) return;
@@ -1819,19 +1927,6 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
.json({ status: "error", message: "Warpgate authentication failed" }); .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([""]); session.finish([""]);
}); });
@@ -1941,7 +2036,7 @@ app.get("/docker/ssh/status", async (req, res) => {
*/ */
app.get("/docker/validate/:sessionId", async (req, res) => { app.get("/docker/validate/:sessionId", async (req, res) => {
const { sessionId } = req.params; const { sessionId } = req.params;
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); return res.status(401).json({ error: "Authentication required" });
@@ -2020,7 +2115,7 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
code: "DOCKER_ERROR", code: "DOCKER_ERROR",
}); });
} }
} catch (installError) { } catch {
session.activeOperations--; session.activeOperations--;
return res.json({ return res.json({
available: false, available: false,
@@ -2073,7 +2168,7 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
app.get("/docker/containers/:sessionId", async (req, res) => { app.get("/docker/containers/:sessionId", async (req, res) => {
const { sessionId } = req.params; const { sessionId } = req.params;
const all = req.query.all !== "false"; const all = req.query.all !== "false";
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); return res.status(401).json({ error: "Authentication required" });
@@ -2115,7 +2210,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
.map((line) => { .map((line) => {
try { try {
return JSON.parse(line); return JSON.parse(line);
} catch (e) { } catch {
sshLogger.warn("Failed to parse container line", { sshLogger.warn("Failed to parse container line", {
operation: "parse_container", operation: "parse_container",
line, line,
@@ -2174,7 +2269,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
*/ */
app.get("/docker/containers/:sessionId/:containerId", async (req, res) => { app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
const { sessionId, containerId } = req.params; const { sessionId, containerId } = req.params;
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); return res.status(401).json({ error: "Authentication required" });
@@ -2269,7 +2364,7 @@ app.post(
"/docker/containers/:sessionId/:containerId/start", "/docker/containers/:sessionId/:containerId/start",
async (req, res) => { async (req, res) => {
const { sessionId, containerId } = req.params; const { sessionId, containerId } = req.params;
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); return res.status(401).json({ error: "Authentication required" });
@@ -2369,7 +2464,7 @@ app.post(
"/docker/containers/:sessionId/:containerId/stop", "/docker/containers/:sessionId/:containerId/stop",
async (req, res) => { async (req, res) => {
const { sessionId, containerId } = req.params; const { sessionId, containerId } = req.params;
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); return res.status(401).json({ error: "Authentication required" });
@@ -2469,7 +2564,7 @@ app.post(
"/docker/containers/:sessionId/:containerId/restart", "/docker/containers/:sessionId/:containerId/restart",
async (req, res) => { async (req, res) => {
const { sessionId, containerId } = req.params; const { sessionId, containerId } = req.params;
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); return res.status(401).json({ error: "Authentication required" });
@@ -2569,7 +2664,7 @@ app.post(
"/docker/containers/:sessionId/:containerId/pause", "/docker/containers/:sessionId/:containerId/pause",
async (req, res) => { async (req, res) => {
const { sessionId, containerId } = req.params; const { sessionId, containerId } = req.params;
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); return res.status(401).json({ error: "Authentication required" });
@@ -2669,7 +2764,7 @@ app.post(
"/docker/containers/:sessionId/:containerId/unpause", "/docker/containers/:sessionId/:containerId/unpause",
async (req, res) => { async (req, res) => {
const { sessionId, containerId } = req.params; const { sessionId, containerId } = req.params;
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); return res.status(401).json({ error: "Authentication required" });
@@ -2774,7 +2869,7 @@ app.delete(
async (req, res) => { async (req, res) => {
const { sessionId, containerId } = req.params; const { sessionId, containerId } = req.params;
const force = req.query.force === "true"; const force = req.query.force === "true";
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); 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 timestamps = req.query.timestamps === "true";
const since = req.query.since as string; const since = req.query.since as string;
const until = req.query.until 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) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); return res.status(401).json({ error: "Authentication required" });
@@ -3011,7 +3106,7 @@ app.get(
"/docker/containers/:sessionId/:containerId/stats", "/docker/containers/:sessionId/:containerId/stats",
async (req, res) => { async (req, res) => {
const { sessionId, containerId } = req.params; const { sessionId, containerId } = req.params;
const userId = (req as any).userId; const userId = (req as unknown as { userId: string }).userId;
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Authentication required" }); 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 { sshData } from "../database/db/schema.js";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { sshLogger } from "../utils/logger.js"; import { sshLogger } from "../utils/logger.js";
import crypto from "crypto";
interface HostKeyVerificationData { interface HostKeyVerificationData {
scenario: "new" | "changed"; scenario: "new" | "changed";
+15 -7
View File
@@ -51,7 +51,9 @@ const cleanupInProgress = new Set<string>();
export function getRequestOrigin(req: IncomingMessage): string { export function getRequestOrigin(req: IncomingMessage): string {
const protoHeader = const protoHeader =
req.headers["x-forwarded-proto"] || req.headers["x-forwarded-proto"] ||
((req.socket as any).encrypted ? "https" : "http"); ((req.socket as unknown as { encrypted?: boolean }).encrypted
? "https"
: "http");
const proto = const proto =
typeof protoHeader === "string" typeof protoHeader === "string"
? protoHeader.split(",")[0].trim() ? protoHeader.split(",")[0].trim()
@@ -157,7 +159,7 @@ async function checkOPKConfigExists(): Promise<{
return { return {
exists: false, exists: false,
configPath, 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 { try {
const binaryPath = OPKSSHBinaryManager.getBinaryPath(); const binaryPath = OPKSSHBinaryManager.getBinaryPath();
const configPath = getOPKConfigPath(); const configPath = getOPKConfigPath();
const configDir = path.dirname(configPath);
const args = [ const args = [
"login", "login",
"--print-key", "--print-key",
"--disable-browser-open",
`--config-path=${configPath}`, `--config-path=${configPath}`,
`--remote-redirect-uri=${remoteRedirectUri}`, `--remote-redirect-uri=${remoteRedirectUri}`,
]; ];
@@ -284,7 +286,10 @@ export async function startOPKSSHAuth(
opksshProcess.stderr?.on("data", async (data) => { opksshProcess.stderr?.on("data", async (data) => {
const stderr = data.toString(); 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); handleOPKSSHOutput(requestId, stderr);
} }
@@ -371,7 +376,7 @@ function handleOPKSSHOutput(requestId: string, output: string): void {
session.stdoutBuffer += output; session.stdoutBuffer += output;
const chooserUrlMatch = session.stdoutBuffer.match( 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") { if (chooserUrlMatch && session.status === "starting") {
const actualPort = parseInt(chooserUrlMatch[1], 10); const actualPort = parseInt(chooserUrlMatch[1], 10);
@@ -784,7 +789,10 @@ export function getActiveSessionsAll(): OPKSSHAuthSession[] {
return Array.from(activeAuthSessions.values()); 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 { try {
const { AuthManager } = await import("../utils/auth-manager.js"); const { AuthManager } = await import("../utils/auth-manager.js");
const authManager = AuthManager.getInstance(); const authManager = AuthManager.getInstance();
@@ -797,7 +805,7 @@ export async function getUserIdFromRequest(req: any): Promise<string | null> {
const decoded = await authManager.verifyJWTToken(token); const decoded = await authManager.verifyJWTToken(token);
return decoded?.userId || null; return decoded?.userId || null;
} catch (error) { } catch {
return null; 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 axios from "axios";
import { getDb } from "../database/db/index.js"; import { getDb } from "../database/db/index.js";
import { sshCredentials } from "../database/db/schema.js"; import { sshCredentials } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { import type {
SSHHost, SSHHost,
TunnelConfig, TunnelConfig,
@@ -16,13 +16,14 @@ import type {
AuthenticatedRequest, AuthenticatedRequest,
} from "../../types/index.js"; } from "../../types/index.js";
import { CONNECTION_STATES } 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 { SystemCrypto } from "../utils/system-crypto.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { DataCrypto } from "../utils/data-crypto.js"; import { DataCrypto } from "../utils/data-crypto.js";
import { createSocks5Connection } from "../utils/socks5-helper.js"; import { createSocks5Connection } from "../utils/socks5-helper.js";
import { AuthManager } from "../utils/auth-manager.js"; import { AuthManager } from "../utils/auth-manager.js";
import { PermissionManager } from "../utils/permission-manager.js"; import { PermissionManager } from "../utils/permission-manager.js";
import { withConnection } from "./ssh-connection-pool.js";
const app = express(); const app = express();
app.use( app.use(
@@ -61,6 +62,10 @@ app.use(
); );
app.use(cookieParser()); app.use(cookieParser());
app.use(express.json()); app.use(express.json());
app.use((_req, res, next) => {
res.setHeader("Cache-Control", "no-store");
next();
});
const authManager = AuthManager.getInstance(); const authManager = AuthManager.getInstance();
const permissionManager = PermissionManager.getInstance(); const permissionManager = PermissionManager.getInstance();
@@ -603,8 +608,9 @@ async function connectSSHTunnel(
tunnelConfig.requestingUserId && tunnelConfig.requestingUserId &&
tunnelConfig.requestingUserId !== tunnelConfig.sourceUserId tunnelConfig.requestingUserId !== tunnelConfig.sourceUserId
) { ) {
const { SharedCredentialManager } = const { SharedCredentialManager } = await import(
await import("../utils/shared-credential-manager.js"); "../utils/shared-credential-manager.js"
);
const sharedCredManager = SharedCredentialManager.getInstance(); const sharedCredManager = SharedCredentialManager.getInstance();
if (tunnelConfig.sourceHostId) { if (tunnelConfig.sourceHostId) {
@@ -655,16 +661,10 @@ async function connectSSHTunnel(
const credential = credentials[0]; const credential = credentials[0];
resolvedSourceCredentials = { resolvedSourceCredentials = {
password: credential.password as string | undefined, password: credential.password as string | undefined,
sshKey: (credential.private_key || sshKey: credential.privateKey as string | undefined,
credential.privateKey || keyPassword: credential.keyPassword as string | undefined,
credential.key) as string | undefined, keyType: credential.keyType as string | undefined,
keyPassword: (credential.key_password || authMethod: credential.authType as string,
credential.keyPassword) as string | undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type ||
credential.authType) as string,
}; };
} }
} }
@@ -746,16 +746,10 @@ async function connectSSHTunnel(
const credential = credentials[0]; const credential = credentials[0];
resolvedEndpointCredentials = { resolvedEndpointCredentials = {
password: credential.password as string | undefined, password: credential.password as string | undefined,
sshKey: (credential.private_key || sshKey: credential.privateKey as string | undefined,
credential.privateKey || keyPassword: credential.keyPassword as string | undefined,
credential.key) as string | undefined, keyType: credential.keyType as string | undefined,
keyPassword: (credential.key_password || credential.keyPassword) as authMethod: credential.authType as string,
| string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type || credential.authType) as string,
}; };
} else { } else {
tunnelLogger.warn("No endpoint credentials found in database", { tunnelLogger.warn("No endpoint credentials found in database", {
@@ -802,7 +796,9 @@ async function connectSSHTunnel(
try { try {
conn.end(); conn.end();
} catch (error) {} } catch {
// expected
}
activeTunnels.delete(tunnelName); activeTunnels.delete(tunnelName);
@@ -996,7 +992,9 @@ async function connectSSHTunnel(
const verification = tunnelVerifications.get(tunnelName); const verification = tunnelVerifications.get(tunnelName);
if (verification?.timeout) clearTimeout(verification.timeout); if (verification?.timeout) clearTimeout(verification.timeout);
verification?.conn.end(); verification?.conn.end();
} catch (error) {} } catch {
// expected
}
tunnelVerifications.delete(tunnelName); tunnelVerifications.delete(tunnelName);
} }
@@ -1110,7 +1108,8 @@ async function connectSSHTunnel(
}); });
const connOptions: Record<string, unknown> = { const connOptions: Record<string, unknown> = {
host: tunnelConfig.sourceIP, host:
tunnelConfig.sourceIP?.replace(/^\[|\]$/g, "") || tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort, port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername, username: tunnelConfig.sourceUsername,
tryKeyboard: true, tryKeyboard: true,
@@ -1347,16 +1346,10 @@ async function killRemoteTunnelByMarker(
const credential = credentials[0]; const credential = credentials[0];
resolvedSourceCredentials = { resolvedSourceCredentials = {
password: credential.password as string | undefined, password: credential.password as string | undefined,
sshKey: (credential.private_key || sshKey: credential.privateKey as string | undefined,
credential.privateKey || keyPassword: credential.keyPassword as string | undefined,
credential.key) as string | undefined, keyType: credential.keyType as string | undefined,
keyPassword: (credential.key_password || credential.keyPassword) as authMethod: credential.authType as string,
| string
| undefined,
keyType: (credential.key_type || credential.keyType) as
| string
| undefined,
authMethod: (credential.auth_type || 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 ( if (
resolvedSourceCredentials.authMethod === "key" && resolvedSourceCredentials.authMethod === "key" &&
resolvedSourceCredentials.sshKey resolvedSourceCredentials.sshKey
@@ -1424,151 +1373,83 @@ async function killRemoteTunnelByMarker(
callback(new Error("Invalid SSH key format")); callback(new Error("Invalid SSH key format"));
return; 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 poolKey = `tunnel:${tunnelConfig.sourceUserId}:${tunnelConfig.sourceIP}:${tunnelConfig.sourceSSHPort}:${tunnelConfig.sourceUsername}`;
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`;
conn.exec(checkCmd, (_err, stream) => { const factory = async (): Promise<Client> => {
let foundProcesses = false; 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) => { if (
const output = data.toString().trim(); resolvedSourceCredentials.authMethod === "key" &&
if (output) { resolvedSourceCredentials.sshKey
foundProcesses = true; ) {
} 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 (
if (!foundProcesses) { tunnelConfig.useSocks5 &&
tunnelLogger.warn("Remote tunnel process not found", { (tunnelConfig.socks5Host ||
operation: "tunnel_remote_not_found", (tunnelConfig.socks5ProxyChain &&
userId: tunnelConfig.sourceUserId, tunnelConfig.socks5ProxyChain.length > 0))
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 () => {
try { try {
const socks5Socket = await createSocks5Connection( const socks5Socket = await createSocks5Connection(
tunnelConfig.sourceIP, tunnelConfig.sourceIP,
@@ -1585,9 +1466,8 @@ async function killRemoteTunnelByMarker(
if (socks5Socket) { if (socks5Socket) {
connOptions.sock = socks5Socket; connOptions.sock = socks5Socket;
conn.connect(connOptions);
} else { } else {
callback(new Error("Failed to create SOCKS5 connection")); throw new Error("Failed to create SOCKS5 connection");
} }
} catch (socks5Error) { } catch (socks5Error) {
tunnelLogger.error( tunnelLogger.error(
@@ -1600,18 +1480,109 @@ async function killRemoteTunnelByMarker(
proxyPort: tunnelConfig.socks5Port || 1080, proxyPort: tunnelConfig.socks5Port || 1080,
}, },
); );
callback( throw new Error(
new Error( "SOCKS5 proxy connection failed: " +
"SOCKS5 proxy connection failed: " + (socks5Error instanceof Error
(socks5Error instanceof Error ? socks5Error.message
? socks5Error.message : "Unknown error"),
: "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)) { if (pendingTunnelOperations.has(tunnelName)) {
try { try {
await pendingTunnelOperations.get(tunnelName); await pendingTunnelOperations.get(tunnelName);
} catch (error) { } catch {
tunnelLogger.warn(`Previous tunnel operation failed`, { tunnelName }); 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( export function execCommand(
client: Client, client: Client,
@@ -11,7 +11,7 @@ export function execCommand(
}> { }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let settled = false; let settled = false;
let stream: any = null; let stream: ClientChannel | null = null;
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (!settled) { if (!settled) {
@@ -30,8 +30,8 @@ export function execCommand(
stream.stderr.removeAllListeners(); stream.stderr.removeAllListeners();
} }
stream.destroy(); stream.destroy();
} catch (error) { } catch {
// Ignore cleanup errors // 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()); const coresNum = Number((coresOut.stdout || "").trim());
cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null; cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null;
} catch (e) { } catch {
cpuPercent = null; cpuPercent = null;
cores = null; cores = null;
loadTriplet = null; loadTriplet = null;
+1 -1
View File
@@ -51,7 +51,7 @@ export async function collectDiskMetrics(client: Client): Promise<{
); );
} }
} }
} catch (e) { } catch {
diskPercent = null; diskPercent = null;
usedHuman = null; usedHuman = null;
totalHuman = null; totalHuman = null;
@@ -1,6 +1,5 @@
import type { Client } from "ssh2"; import type { Client } from "ssh2";
import { execCommand } from "./common-utils.js"; import { execCommand } from "./common-utils.js";
import { statsLogger } from "../../utils/logger.js";
export interface LoginRecord { export interface LoginRecord {
user: string; user: string;
@@ -53,7 +52,7 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
parsedTime = isNaN(date.getTime()) parsedTime = isNaN(date.getTime())
? new Date().toISOString() ? new Date().toISOString()
: date.toISOString(); : date.toISOString();
} catch (e) { } catch {
parsedTime = new Date().toISOString(); parsedTime = new Date().toISOString();
} }
@@ -70,7 +69,9 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
} }
} }
} }
} catch (e) {} } catch {
// expected
}
try { try {
const failedOut = await execCommand( const failedOut = await execCommand(
@@ -111,7 +112,7 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
parsedTime = isNaN(date.getTime()) parsedTime = isNaN(date.getTime())
? new Date().toISOString() ? new Date().toISOString()
: date.toISOString(); : date.toISOString();
} catch (e) { } catch {
parsedTime = new Date().toISOString(); parsedTime = new Date().toISOString();
} }
@@ -126,7 +127,9 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
} }
} }
} }
} catch (e) {} } catch {
// expected
}
return { return {
recentLogins: recentLogins.slice(0, 10), recentLogins: recentLogins.slice(0, 10),
+1 -1
View File
@@ -27,7 +27,7 @@ export async function collectMemoryMetrics(client: Client): Promise<{
usedGiB = kibToGiB(usedKb); usedGiB = kibToGiB(usedKb);
totalGiB = kibToGiB(totalKb); totalGiB = kibToGiB(totalKb);
} }
} catch (e) { } catch {
memPercent = null; memPercent = null;
usedGiB = null; usedGiB = null;
totalGiB = null; totalGiB = null;
+3 -2
View File
@@ -1,6 +1,5 @@
import type { Client } from "ssh2"; import type { Client } from "ssh2";
import { execCommand } from "./common-utils.js"; import { execCommand } from "./common-utils.js";
import { statsLogger } from "../../utils/logger.js";
export async function collectNetworkMetrics(client: Client): Promise<{ export async function collectNetworkMetrics(client: Client): Promise<{
interfaces: Array<{ interfaces: Array<{
@@ -68,7 +67,9 @@ export async function collectNetworkMetrics(client: Client): Promise<{
txBytes: null, txBytes: null,
}); });
} }
} catch (e) {} } catch {
// expected
}
return { interfaces }; return { interfaces };
} }
@@ -1,6 +1,5 @@
import type { Client } from "ssh2"; import type { Client } from "ssh2";
import { execCommand } from "./common-utils.js"; import { execCommand } from "./common-utils.js";
import { statsLogger } from "../../utils/logger.js";
export async function collectProcessesMetrics(client: Client): Promise<{ export async function collectProcessesMetrics(client: Client): Promise<{
total: number | null; total: number | null;
@@ -54,7 +53,9 @@ export async function collectProcessesMetrics(client: Client): Promise<{
const runningCount2 = Number(runningCount.stdout.trim()); const runningCount2 = Number(runningCount.stdout.trim());
runningProcesses = Number.isFinite(runningCount2) ? runningCount2 : null; runningProcesses = Number.isFinite(runningCount2) ? runningCount2 : null;
} catch (e) {} } catch {
// expected
}
return { return {
total: totalProcesses, total: totalProcesses,
+2 -3
View File
@@ -1,6 +1,5 @@
import type { Client } from "ssh2"; import type { Client } from "ssh2";
import { execCommand } from "./common-utils.js"; import { execCommand } from "./common-utils.js";
import { statsLogger } from "../../utils/logger.js";
export async function collectSystemMetrics(client: Client): Promise<{ export async function collectSystemMetrics(client: Client): Promise<{
hostname: string | null; hostname: string | null;
@@ -22,8 +21,8 @@ export async function collectSystemMetrics(client: Client): Promise<{
hostname = hostnameOut.stdout.trim() || null; hostname = hostnameOut.stdout.trim() || null;
kernel = kernelOut.stdout.trim() || null; kernel = kernelOut.stdout.trim() || null;
os = osOut.stdout.trim() || null; os = osOut.stdout.trim() || null;
} catch (e) { } catch {
// No error log // expected
} }
return { return {
+3 -2
View File
@@ -1,6 +1,5 @@
import type { Client } from "ssh2"; import type { Client } from "ssh2";
import { execCommand } from "./common-utils.js"; import { execCommand } from "./common-utils.js";
import { statsLogger } from "../../utils/logger.js";
export async function collectUptimeMetrics(client: Client): Promise<{ export async function collectUptimeMetrics(client: Client): Promise<{
seconds: number | null; seconds: number | null;
@@ -21,7 +20,9 @@ export async function collectUptimeMetrics(client: Client): Promise<{
uptimeFormatted = `${days}d ${hours}h ${minutes}m`; uptimeFormatted = `${days}d ${hours}h ${minutes}m`;
} }
} }
} catch (e) {} } catch {
// expected
}
return { return {
seconds: uptimeSeconds, seconds: uptimeSeconds,
+16 -3
View File
@@ -22,7 +22,9 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
if (persistentConfig.parsed) { if (persistentConfig.parsed) {
Object.assign(process.env, persistentConfig.parsed); Object.assign(process.env, persistentConfig.parsed);
} }
} catch (error) {} } catch {
// expected - env file may not exist
}
systemLogger.info("Termix backend initialization started", { systemLogger.info("Termix backend initialization started", {
operation: "backend_init_start", operation: "backend_init_start",
@@ -110,8 +112,9 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
await authManager.initialize(); await authManager.initialize();
DataCrypto.initialize(); DataCrypto.initialize();
const { OPKSSHBinaryManager } = const { OPKSSHBinaryManager } = await import(
await import("./utils/opkssh-binary-manager.js"); "./utils/opkssh-binary-manager.js"
);
try { try {
await OPKSSHBinaryManager.ensureBinary(); await OPKSSHBinaryManager.ensureBinary();
} catch (error) { } catch (error) {
@@ -163,6 +166,16 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
process.exit(0); 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) => { process.on("uncaughtException", (error) => {
systemLogger.error("Uncaught exception occurred", error, { systemLogger.error("Uncaught exception occurred", error, {
operation: "error_handling", operation: "error_handling",
+161 -30
View File
@@ -5,7 +5,7 @@ import { DataCrypto } from "./data-crypto.js";
import { databaseLogger, authLogger } from "./logger.js"; import { databaseLogger, authLogger } from "./logger.js";
import type { Request, Response, NextFunction } from "express"; import type { Request, Response, NextFunction } from "express";
import { db } from "../database/db/index.js"; 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 { eq, and, sql } from "drizzle-orm";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import type { DeviceType } from "./user-agent-parser.js"; import type { DeviceType } from "./user-agent-parser.js";
@@ -99,7 +99,7 @@ class AuthManager {
const sessionDurationMs = const sessionDurationMs =
deviceType === "desktop" || deviceType === "mobile" deviceType === "desktop" || deviceType === "mobile"
? 30 * 24 * 60 * 60 * 1000 ? 30 * 24 * 60 * 60 * 1000
: 7 * 24 * 60 * 60 * 1000; : 2 * 60 * 60 * 1000;
const authenticated = await this.userCrypto.authenticateOIDCUser( const authenticated = await this.userCrypto.authenticateOIDCUser(
userId, userId,
@@ -121,7 +121,7 @@ class AuthManager {
const sessionDurationMs = const sessionDurationMs =
deviceType === "desktop" || deviceType === "mobile" deviceType === "desktop" || deviceType === "mobile"
? 30 * 24 * 60 * 60 * 1000 ? 30 * 24 * 60 * 60 * 1000
: 7 * 24 * 60 * 60 * 1000; : 2 * 60 * 60 * 1000;
const authenticated = await this.userCrypto.authenticateUser( const authenticated = await this.userCrypto.authenticateUser(
userId, userId,
@@ -154,8 +154,9 @@ class AuthManager {
return; return;
} }
const { getSqlite, saveMemoryDatabaseToFile } = const { getSqlite, saveMemoryDatabaseToFile } = await import(
await import("../database/db/index.js"); "../database/db/index.js"
);
const sqlite = getSqlite(); const sqlite = getSqlite();
@@ -170,8 +171,9 @@ class AuthManager {
} }
try { try {
const { CredentialSystemEncryptionMigration } = const { CredentialSystemEncryptionMigration } = await import(
await import("./credential-system-encryption-migration.js"); "./credential-system-encryption-migration.js"
);
const credMigration = new CredentialSystemEncryptionMigration(); const credMigration = new CredentialSystemEncryptionMigration();
const credResult = await credMigration.migrateUserCredentials(userId); const credResult = await credMigration.migrateUserCredentials(userId);
@@ -199,6 +201,7 @@ class AuthManager {
options: { options: {
expiresIn?: string; expiresIn?: string;
pendingTOTP?: boolean; pendingTOTP?: boolean;
rememberMe?: boolean;
deviceType?: DeviceType; deviceType?: DeviceType;
deviceInfo?: string; deviceInfo?: string;
} = {}, } = {},
@@ -207,13 +210,13 @@ class AuthManager {
let expiresIn = options.expiresIn; let expiresIn = options.expiresIn;
if (!expiresIn && !options.pendingTOTP) { if (!expiresIn && !options.pendingTOTP) {
if (options.deviceType === "desktop" || options.deviceType === "mobile") { if (options.rememberMe) {
expiresIn = "30d"; expiresIn = "30d";
} else { } else {
expiresIn = "7d"; expiresIn = "2h";
} }
} else if (!expiresIn) { } else if (!expiresIn) {
expiresIn = "7d"; expiresIn = "2h";
} }
const payload: JWTPayload = { userId }; const payload: JWTPayload = { userId };
@@ -247,8 +250,9 @@ class AuthManager {
}); });
try { try {
const { saveMemoryDatabaseToFile } = const { saveMemoryDatabaseToFile } = await import(
await import("../database/db/index.js"); "../database/db/index.js"
);
await saveMemoryDatabaseToFile(); await saveMemoryDatabaseToFile();
} catch (saveError) { } catch (saveError) {
databaseLogger.error( databaseLogger.error(
@@ -276,7 +280,7 @@ class AuthManager {
private parseExpiresIn(expiresIn: string): number { private parseExpiresIn(expiresIn: string): number {
const match = expiresIn.match(/^(\d+)([smhd])$/); 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 value = parseInt(match[1]);
const unit = match[2]; const unit = match[2];
@@ -291,7 +295,7 @@ class AuthManager {
case "d": case "d":
return value * 24 * 60 * 60 * 1000; return value * 24 * 60 * 60 * 1000;
default: 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> { async revokeSession(sessionId: string): Promise<boolean> {
try { try {
@@ -354,8 +364,9 @@ class AuthManager {
await db.delete(sessions).where(eq(sessions.id, sessionId)); await db.delete(sessions).where(eq(sessions.id, sessionId));
try { try {
const { saveMemoryDatabaseToFile } = const { saveMemoryDatabaseToFile } = await import(
await import("../database/db/index.js"); "../database/db/index.js"
);
await saveMemoryDatabaseToFile(); await saveMemoryDatabaseToFile();
} catch (saveError) { } catch (saveError) {
databaseLogger.error( databaseLogger.error(
@@ -412,8 +423,9 @@ class AuthManager {
} }
try { try {
const { saveMemoryDatabaseToFile } = const { saveMemoryDatabaseToFile } = await import(
await import("../database/db/index.js"); "../database/db/index.js"
);
await saveMemoryDatabaseToFile(); await saveMemoryDatabaseToFile();
} catch (saveError) { } catch (saveError) {
databaseLogger.error( databaseLogger.error(
@@ -454,8 +466,9 @@ class AuthManager {
.where(sql`${sessions.expiresAt} < datetime('now')`); .where(sql`${sessions.expiresAt} < datetime('now')`);
try { try {
const { saveMemoryDatabaseToFile } = const { saveMemoryDatabaseToFile } = await import(
await import("../database/db/index.js"); "../database/db/index.js"
);
await saveMemoryDatabaseToFile(); await saveMemoryDatabaseToFile();
} catch (saveError) { } catch (saveError) {
databaseLogger.error( databaseLogger.error(
@@ -488,7 +501,7 @@ class AuthManager {
} }
} }
async getAllSessions(): Promise<any[]> { async getAllSessions(): Promise<Record<string, unknown>[]> {
try { try {
const allSessions = await db.select().from(sessions); const allSessions = await db.select().from(sessions);
return allSessions; return allSessions;
@@ -500,7 +513,7 @@ class AuthManager {
} }
} }
async getUserSessions(userId: string): Promise<any[]> { async getUserSessions(userId: string): Promise<Record<string, unknown>[]> {
try { try {
const userSessions = await db const userSessions = await db
.select() .select()
@@ -518,7 +531,7 @@ class AuthManager {
getSecureCookieOptions( getSecureCookieOptions(
req: RequestWithHeaders, req: RequestWithHeaders,
maxAge: number = 7 * 24 * 60 * 60 * 1000, maxAge: number = 2 * 60 * 60 * 1000,
) { ) {
return { return {
httpOnly: false, httpOnly: false,
@@ -600,8 +613,9 @@ class AuthManager {
.where(eq(sessions.id, payload.sessionId)) .where(eq(sessions.id, payload.sessionId))
.then(async () => { .then(async () => {
try { try {
const { saveMemoryDatabaseToFile } = const { saveMemoryDatabaseToFile } = await import(
await import("../database/db/index.js"); "../database/db/index.js"
);
await saveMemoryDatabaseToFile(); await saveMemoryDatabaseToFile();
const remainingSessions = await db const remainingSessions = await db
@@ -711,7 +725,7 @@ class AuthManager {
.from(users) .from(users)
.where(eq(users.id, payload.userId)); .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( databaseLogger.warn(
"Non-admin user attempted to access admin endpoint", "Non-admin user attempted to access admin endpoint",
{ {
@@ -745,8 +759,9 @@ class AuthManager {
await db.delete(sessions).where(eq(sessions.id, sessionId)); await db.delete(sessions).where(eq(sessions.id, sessionId));
try { try {
const { saveMemoryDatabaseToFile } = const { saveMemoryDatabaseToFile } = await import(
await import("../database/db/index.js"); "../database/db/index.js"
);
await saveMemoryDatabaseToFile(); await saveMemoryDatabaseToFile();
} catch (saveError) { } catch (saveError) {
databaseLogger.error( databaseLogger.error(
@@ -768,6 +783,7 @@ class AuthManager {
if (remainingSessions.length === 0) { if (remainingSessions.length === 0) {
this.userCrypto.logoutUser(userId); this.userCrypto.logoutUser(userId);
} else { } else {
// expected - other sessions still active, keep user crypto state
} }
} catch (error) { } catch (error) {
databaseLogger.error("Failed to delete session on logout", error, { databaseLogger.error("Failed to delete session on logout", error, {
@@ -810,6 +826,121 @@ class AuthManager {
newPassword, 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 }; export { AuthManager, type AuthenticationResult, type JWTPayload };
+3 -1
View File
@@ -233,7 +233,9 @@ IP.3 = 0.0.0.0
let envContent = ""; let envContent = "";
try { try {
envContent = await fs.readFile(this.ENV_FILE, "utf8"); envContent = await fs.readFile(this.ENV_FILE, "utf8");
} catch (error) {} } catch {
// expected - env file may not exist yet
}
let updatedContent = envContent; let updatedContent = envContent;
let hasChanges = false; let hasChanges = false;
@@ -59,9 +59,9 @@ export class CredentialSystemEncryptionMigration {
) )
: null; : null;
const plainKeyPassword = cred.key_password const plainKeyPassword = cred.keyPassword
? FieldCrypto.decryptField( ? FieldCrypto.decryptField(
cred.key_password, cred.keyPassword,
userDEK, userDEK,
cred.id.toString(), cred.id.toString(),
"key_password", "key_password",
+34 -37
View File
@@ -131,12 +131,12 @@ class DataCrypto {
db.prepare(updateQuery).run( db.prepare(updateQuery).run(
updatedRecord.password || null, updatedRecord.password || null,
updatedRecord.key || null, updatedRecord.key || null,
updatedRecord.key_password || updatedRecord.keyPassword || null, updatedRecord.key_password || null,
updatedRecord.keyType || null, updatedRecord.key_type || null,
updatedRecord.autostartPassword || null, updatedRecord.autostart_password || null,
updatedRecord.autostartKey || null, updatedRecord.autostart_key || null,
updatedRecord.autostartKeyPassword || null, updatedRecord.autostart_key_password || null,
updatedRecord.sudoPassword || null, updatedRecord.sudo_password || null,
record.id, record.id,
); );
@@ -171,10 +171,10 @@ class DataCrypto {
db.prepare(updateQuery).run( db.prepare(updateQuery).run(
updatedRecord.password || null, updatedRecord.password || null,
updatedRecord.key || null, updatedRecord.key || null,
updatedRecord.key_password || updatedRecord.keyPassword || null, updatedRecord.key_password || null,
updatedRecord.private_key || updatedRecord.privateKey || null, updatedRecord.private_key || null,
updatedRecord.public_key || updatedRecord.publicKey || null, updatedRecord.public_key || null,
updatedRecord.keyType || null, updatedRecord.key_type || null,
record.id, record.id,
); );
@@ -207,14 +207,10 @@ class DataCrypto {
WHERE id = ? WHERE id = ?
`; `;
db.prepare(updateQuery).run( db.prepare(updateQuery).run(
updatedRecord.totp_secret || updatedRecord.totpSecret || null, updatedRecord.totp_secret || null,
updatedRecord.totp_backup_codes || updatedRecord.totp_backup_codes || null,
updatedRecord.totpBackupCodes || updatedRecord.client_secret || null,
null, updatedRecord.oidc_identifier || null,
updatedRecord.client_secret || updatedRecord.clientSecret || null,
updatedRecord.oidc_identifier ||
updatedRecord.oidcIdentifier ||
null,
userId, userId,
); );
@@ -267,46 +263,41 @@ class DataCrypto {
"password", "password",
"key", "key",
"key_password", "key_password",
"keyPassword", "sudo_password",
"keyType", "autostart_password",
"autostartPassword", "autostart_key",
"autostartKey", "autostart_key_password",
"autostartKeyPassword",
], ],
}, },
{ {
table: "ssh_credentials", table: "ssh_credentials",
fields: [ fields: [
"password", "password",
"private_key",
"privateKey",
"key_password",
"keyPassword",
"key", "key",
"private_key",
"public_key", "public_key",
"publicKey", "key_password",
"keyType",
], ],
}, },
{ {
table: "users", table: "users",
fields: [ fields: [
"client_secret", "client_secret",
"clientSecret",
"totp_secret", "totp_secret",
"totpSecret",
"totp_backup_codes", "totp_backup_codes",
"totpBackupCodes",
"oidc_identifier", "oidc_identifier",
"oidcIdentifier",
], ],
}, },
]; ];
for (const { table, fields } of tablesToReencrypt) { for (const { table, fields } of tablesToReencrypt) {
try { try {
const selectQuery =
table === "users"
? `SELECT * FROM ${table} WHERE id = ?`
: `SELECT * FROM ${table} WHERE user_id = ?`;
const records = db const records = db
.prepare(`SELECT * FROM ${table} WHERE user_id = ?`) .prepare(selectQuery)
.all(userId) as DatabaseRecord[]; .all(userId) as DatabaseRecord[];
for (const record of records) { for (const record of records) {
@@ -359,7 +350,13 @@ class DataCrypto {
(field) => updatedRecord[field] !== record[field], (field) => updatedRecord[field] !== record[field],
); );
if (updateFields.length > 0) { 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( const updateValues = updateFields.map(
(field) => updatedRecord[field], (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( systemEncrypted.systemKeyPassword = FieldCrypto.encryptField(
record.key_password as string, record.keyPassword as string,
systemKey, systemKey,
recordId as string, recordId as string,
"key_password", "key_password",
@@ -327,7 +327,9 @@ class DatabaseFileEncryption {
fs.accessSync(envPath, fs.constants.R_OK); fs.accessSync(envPath, fs.constants.R_OK);
envFileReadable = true; envFileReadable = true;
} }
} catch (error) {} } catch {
// expected - env file access check may fail
}
databaseLogger.error( databaseLogger.error(
"Database decryption authentication failed - possible causes: wrong DATABASE_KEY, corrupted files, or interrupted write", "Database decryption authentication failed - possible causes: wrong DATABASE_KEY, corrupted files, or interrupted write",
@@ -581,7 +583,9 @@ class DatabaseFileEncryption {
try { try {
fs.accessSync(envPath, fs.constants.R_OK); fs.accessSync(envPath, fs.constants.R_OK);
result.environment.envFileReadable = true; result.environment.envFileReadable = true;
} catch (error) {} } catch {
// expected - env file access check may fail
}
} }
if ( if (
@@ -5,12 +5,21 @@ export class DatabaseSaveTrigger {
private static isInitialized = false; private static isInitialized = false;
private static pendingSave = false; private static pendingSave = false;
private static saveTimeout: NodeJS.Timeout | null = null; private static saveTimeout: NodeJS.Timeout | null = null;
private static _dirty = false;
static initialize(saveFunction: () => Promise<void>): void { static initialize(saveFunction: () => Promise<void>): void {
this.saveFunction = saveFunction; this.saveFunction = saveFunction;
this.isInitialized = true; this.isInitialized = true;
} }
static get isDirty(): boolean {
return this._dirty;
}
static markClean(): void {
this._dirty = false;
}
static async triggerSave( static async triggerSave(
reason: string = "data_modification", reason: string = "data_modification",
): Promise<void> { ): Promise<void> {
@@ -22,6 +31,8 @@ export class DatabaseSaveTrigger {
return; return;
} }
this._dirty = true;
if (this.saveTimeout) { if (this.saveTimeout) {
clearTimeout(this.saveTimeout); clearTimeout(this.saveTimeout);
} }
@@ -35,6 +46,7 @@ export class DatabaseSaveTrigger {
try { try {
await this.saveFunction!(); await this.saveFunction!();
this._dirty = false;
} catch (error) { } catch (error) {
databaseLogger.error("Database save failed", error, { databaseLogger.error("Database save failed", error, {
operation: "db_save_trigger_failed", operation: "db_save_trigger_failed",
+2 -16
View File
@@ -16,43 +16,29 @@ class FieldCrypto {
private static readonly ENCRYPTED_FIELDS = { private static readonly ENCRYPTED_FIELDS = {
users: new Set([ users: new Set([
"password_hash",
"passwordHash", "passwordHash",
"client_secret",
"clientSecret", "clientSecret",
"totp_secret",
"totpSecret", "totpSecret",
"totp_backup_codes",
"totpBackupCodes", "totpBackupCodes",
"oidc_identifier",
"oidcIdentifier", "oidcIdentifier",
]), ]),
ssh_data: new Set([ ssh_data: new Set([
"password", "password",
"key", "key",
"key_password",
"keyPassword", "keyPassword",
"sudoPassword",
"autostartPassword", "autostartPassword",
"autostartKey", "autostartKey",
"autostartKeyPassword", "autostartKeyPassword",
"sudoPassword",
]), ]),
ssh_credentials: new Set([ ssh_credentials: new Set([
"password", "password",
"private_key",
"privateKey", "privateKey",
"key_password",
"keyPassword", "keyPassword",
"key", "key",
"public_key",
"publicKey", "publicKey",
]), ]),
opkssh_tokens: new Set([ opkssh_tokens: new Set(["sshCert", "privateKey"]),
"ssh_cert",
"sshCert",
"private_key",
"privateKey",
]),
}; };
static encryptField( static encryptField(
+56 -22
View File
@@ -82,19 +82,21 @@ export class LazyFieldEncryption {
legacyFieldName, legacyFieldName,
); );
return decrypted; return decrypted;
} catch (error) {} } catch {
// expected - legacy decryption may fail, try other methods
}
} }
const sensitiveFields = [ const sensitiveFields = [
"totp_secret", "totpSecret",
"totp_backup_codes", "totpBackupCodes",
"password", "password",
"key", "key",
"key_password", "keyPassword",
"private_key", "privateKey",
"public_key", "publicKey",
"client_secret", "clientSecret",
"oidc_identifier", "oidcIdentifier",
]; ];
if (sensitiveFields.includes(fieldName)) { if (sensitiveFields.includes(fieldName)) {
@@ -174,7 +176,9 @@ export class LazyFieldEncryption {
wasPlaintext: false, wasPlaintext: false,
wasLegacyEncryption: true, wasLegacyEncryption: true,
}; };
} catch (error) {} } catch {
// expected - re-encryption may fail, return original
}
} }
return { return {
encrypted: fieldValue, encrypted: fieldValue,
@@ -200,7 +204,8 @@ export class LazyFieldEncryption {
let needsUpdate = false; let needsUpdate = false;
for (const fieldName of sensitiveFields) { for (const fieldName of sensitiveFields) {
const fieldValue = record[fieldName]; const column = this.propertyToColumn(fieldName);
const fieldValue = record[column] ?? record[fieldName];
if (fieldValue) { if (fieldValue) {
try { try {
@@ -213,7 +218,7 @@ export class LazyFieldEncryption {
); );
if (wasPlaintext || wasLegacyEncryption) { if (wasPlaintext || wasLegacyEncryption) {
updatedRecord[fieldName] = encrypted; updatedRecord[column] = encrypted;
migratedFields.push(fieldName); migratedFields.push(fieldName);
needsUpdate = true; needsUpdate = true;
} }
@@ -230,22 +235,48 @@ export class LazyFieldEncryption {
return { updatedRecord, migratedFields, needsUpdate }; 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[] { static getSensitiveFieldsForTable(tableName: string): string[] {
const sensitiveFieldsMap: Record<string, string[]> = { const sensitiveFieldsMap: Record<string, string[]> = {
ssh_data: ["password", "key", "key_password"], ssh_data: [
"password",
"key",
"keyPassword",
"sudoPassword",
"autostartPassword",
"autostartKey",
"autostartKeyPassword",
],
ssh_credentials: [ ssh_credentials: [
"password", "password",
"key", "key",
"key_password", "keyPassword",
"private_key", "privateKey",
"public_key", "publicKey",
], ],
users: ["totp_secret", "totp_backup_codes"], users: ["totpSecret", "totpBackupCodes"],
}; };
return sensitiveFieldsMap[tableName] || []; return sensitiveFieldsMap[tableName] || [];
} }
static propertyToColumn(propertyName: string): string {
return this.PROPERTY_TO_COLUMN[propertyName] || propertyName;
}
static fieldNeedsMigration( static fieldNeedsMigration(
fieldValue: string, fieldValue: string,
userKEK: Buffer, userKEK: Buffer,
@@ -310,10 +341,11 @@ export class LazyFieldEncryption {
const hostPlaintextFields: string[] = []; const hostPlaintextFields: string[] = [];
for (const field of sensitiveFields) { for (const field of sensitiveFields) {
const column = this.propertyToColumn(field);
if ( if (
host[field] && host[column] &&
this.fieldNeedsMigration( this.fieldNeedsMigration(
host[field] as string, host[column] as string,
userKEK, userKEK,
host.id.toString(), host.id.toString(),
field, field,
@@ -344,10 +376,11 @@ export class LazyFieldEncryption {
const credentialPlaintextFields: string[] = []; const credentialPlaintextFields: string[] = [];
for (const field of sensitiveFields) { for (const field of sensitiveFields) {
const column = this.propertyToColumn(field);
if ( if (
credential[field] && credential[column] &&
this.fieldNeedsMigration( this.fieldNeedsMigration(
credential[field] as string, credential[column] as string,
userKEK, userKEK,
credential.id.toString(), credential.id.toString(),
field, field,
@@ -373,9 +406,10 @@ export class LazyFieldEncryption {
const userPlaintextFields: string[] = []; const userPlaintextFields: string[] = [];
for (const field of sensitiveFields) { for (const field of sensitiveFields) {
const column = this.propertyToColumn(field);
if ( if (
user[field] && user[column] &&
this.fieldNeedsMigration(user[field], userKEK, userId, field) this.fieldNeedsMigration(user[column], userKEK, userId, field)
) { ) {
userPlaintextFields.push(field); userPlaintextFields.push(field);
needsMigration = true; needsMigration = true;
+5 -5
View File
@@ -66,15 +66,14 @@ class PermissionManager {
private async cleanupExpiredAccess(): Promise<void> { private async cleanupExpiredAccess(): Promise<void> {
try { try {
const now = new Date().toISOString(); const now = new Date().toISOString();
const result = await db await db
.delete(hostAccess) .delete(hostAccess)
.where( .where(
and( and(
sql`${hostAccess.expiresAt} IS NOT NULL`, sql`${hostAccess.expiresAt} IS NOT NULL`,
sql`${hostAccess.expiresAt} <= ${now}`, sql`${hostAccess.expiresAt} <= ${now}`,
), ),
) );
.returning({ id: hostAccess.id });
} catch (error) { } catch (error) {
databaseLogger.error("Failed to cleanup expired host access", error, { databaseLogger.error("Failed to cleanup expired host access", error, {
operation: "host_access_cleanup_failed", operation: "host_access_cleanup_failed",
@@ -280,7 +279,7 @@ class PermissionManager {
async isAdmin(userId: string): Promise<boolean> { async isAdmin(userId: string): Promise<boolean> {
try { try {
const user = await db const user = await db
.select({ isAdmin: users.is_admin }) .select({ isAdmin: users.isAdmin })
.from(users) .from(users)
.where(eq(users.id, userId)) .where(eq(users.id, userId))
.limit(1); .limit(1);
@@ -383,7 +382,8 @@ class PermissionManager {
}); });
} }
(req as any).hostAccessInfo = accessInfo; (req as unknown as { hostAccessInfo: HostAccessInfo }).hostAccessInfo =
accessInfo;
next(); next();
}; };
+1 -3
View File
@@ -18,9 +18,7 @@ export function getProxyAgent(targetUrl?: string): Agent | undefined {
const trimmed = entry.trim().toLowerCase(); const trimmed = entry.trim().toLowerCase();
if (!trimmed) continue; if (!trimmed) continue;
const normalized = trimmed const normalized = trimmed.replace(/^\*\./, "").replace(/^\./, "");
.replace(/^\*\./, "")
.replace(/^\./, "");
if (hostname === normalized || hostname.endsWith(`.${normalized}`)) { if (hostname === normalized || hostname.endsWith(`.${normalized}`)) {
return undefined; 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, sharedCredentials,
sshCredentials, sshCredentials,
hostAccess, hostAccess,
users,
userRoles, userRoles,
sshData, sshData,
} from "../database/db/schema.js"; } from "../database/db/schema.js";
@@ -293,10 +292,9 @@ class SharedCredentialManager {
credentialId: number, credentialId: number,
): Promise<void> { ): Promise<void> {
try { try {
const result = await db await db
.delete(sharedCredentials) .delete(sharedCredentials)
.where(eq(sharedCredentials.originalCredentialId, credentialId)) .where(eq(sharedCredentials.originalCredentialId, credentialId));
.returning({ id: sharedCredentials.id });
} catch (error) { } catch (error) {
databaseLogger.error("Failed to delete shared credentials", error, { databaseLogger.error("Failed to delete shared credentials", error, {
operation: "delete_shared_credentials", operation: "delete_shared_credentials",
@@ -364,9 +362,9 @@ class SharedCredentialManager {
key: cred.key key: cred.key
? this.decryptField(cred.key, ownerDEK, credentialId, "key") ? this.decryptField(cred.key, ownerDEK, credentialId, "key")
: undefined, : undefined,
keyPassword: cred.key_password keyPassword: cred.keyPassword
? this.decryptField( ? this.decryptField(
cred.key_password, cred.keyPassword,
ownerDEK, ownerDEK,
credentialId, credentialId,
"key_password", "key_password",
@@ -534,7 +532,7 @@ class SharedCredentialManager {
recordId.toString(), recordId.toString(),
fieldName, fieldName,
); );
} catch (error) { } catch {
databaseLogger.warn("Field decryption failed, returning as-is", { databaseLogger.warn("Field decryption failed, returning as-is", {
operation: "decrypt_field", operation: "decrypt_field",
fieldName, fieldName,
+6 -5
View File
@@ -1,6 +1,7 @@
import { getDb, DatabaseSaveTrigger } from "../database/db/index.js"; import { getDb, DatabaseSaveTrigger } from "../database/db/index.js";
import { DataCrypto } from "./data-crypto.js"; import { DataCrypto } from "./data-crypto.js";
import type { SQLiteTable } from "drizzle-orm/sqlite-core"; import type { SQLiteTable } from "drizzle-orm/sqlite-core";
import type { SQL } from "drizzle-orm";
type TableName = type TableName =
| "users" | "users"
@@ -11,7 +12,7 @@ type TableName =
class SimpleDBOps { class SimpleDBOps {
static async insert<T extends Record<string, unknown>>( static async insert<T extends Record<string, unknown>>(
table: SQLiteTable<any>, table: SQLiteTable,
tableName: TableName, tableName: TableName,
data: T, data: T,
userId: string, userId: string,
@@ -109,7 +110,7 @@ class SimpleDBOps {
} }
static async update<T extends Record<string, unknown>>( static async update<T extends Record<string, unknown>>(
table: SQLiteTable<any>, table: SQLiteTable,
tableName: TableName, tableName: TableName,
where: unknown, where: unknown,
data: Partial<T>, data: Partial<T>,
@@ -141,7 +142,7 @@ class SimpleDBOps {
const result = await getDb() const result = await getDb()
.update(table) .update(table)
.set(encryptedData) .set(encryptedData)
.where(where as any) .where(where as SQL | undefined)
.returning(); .returning();
DatabaseSaveTrigger.triggerSave(`update_${tableName}`); DatabaseSaveTrigger.triggerSave(`update_${tableName}`);
@@ -157,13 +158,13 @@ class SimpleDBOps {
} }
static async delete( static async delete(
table: SQLiteTable<any>, table: SQLiteTable,
tableName: TableName, tableName: TableName,
where: unknown, where: unknown,
): Promise<unknown[]> { ): Promise<unknown[]> {
const result = await getDb() const result = await getDb()
.delete(table) .delete(table)
.where(where as any) .where(where as SQL | undefined)
.returning(); .returning();
DatabaseSaveTrigger.triggerSave(`delete_${tableName}`); DatabaseSaveTrigger.triggerSave(`delete_${tableName}`);
+8 -117
View File
@@ -1,117 +1,8 @@
import { SocksClient } from "socks"; export {
import type { SocksClientOptions } from "socks"; createSocks5Connection,
import net from "net"; createProxyConnection,
import { sshLogger } from "./logger.js"; createHttpConnectConnection,
import type { ProxyNode } from "../../types/index.js"; createMixedProxyChainConnection,
testProxyConnectivity,
export interface SOCKS5Config { } from "./proxy-helper.js";
useSocks5?: boolean; export type { SOCKS5Config } from "./proxy-helper.js";
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;
}
}
+12 -5
View File
@@ -1,5 +1,4 @@
import ssh2Pkg from "ssh2"; import ssh2Pkg from "ssh2";
import { sshLogger } from "./logger.js";
const ssh2Utils = ssh2Pkg.utils; const ssh2Utils = ssh2Pkg.utils;
function detectKeyTypeFromContent(keyContent: string): string { function detectKeyTypeFromContent(keyContent: string): string {
@@ -85,7 +84,9 @@ function detectKeyTypeFromContent(keyContent: string): string {
} else if (decodedString.includes("1.3.101.112")) { } else if (decodedString.includes("1.3.101.112")) {
return "ssh-ed25519"; return "ssh-ed25519";
} }
} catch (error) {} } catch {
// expected - base64 decode may fail for some key formats
}
if (content.length < 800) { if (content.length < 800) {
return "ssh-ed25519"; return "ssh-ed25519";
@@ -141,7 +142,9 @@ function detectPublicKeyTypeFromContent(publicKeyContent: string): string {
} else if (decodedString.includes("1.3.101.112")) { } else if (decodedString.includes("1.3.101.112")) {
return "ssh-ed25519"; return "ssh-ed25519";
} }
} catch (error) {} } catch {
// expected - base64 decode may fail for some key formats
}
if (content.length < 400) { if (content.length < 400) {
return "ssh-ed25519"; return "ssh-ed25519";
@@ -243,7 +246,9 @@ export function parseSSHKey(
useSSH2 = true; useSSH2 = true;
} }
} catch (error) {} } catch {
// expected - ssh2 key parsing may fail
}
} }
if (!useSSH2) { if (!useSSH2) {
@@ -269,7 +274,9 @@ export function parseSSHKey(
success: true, success: true,
}; };
} }
} catch (error) {} } catch {
// expected - fallback key type detection may fail
}
return { return {
privateKey: privateKeyData, privateKey: privateKeyData,
+13 -17
View File
@@ -52,7 +52,9 @@ class SystemCrypto {
}, },
); );
} }
} catch (fileError) {} } catch {
// expected - env file may not exist
}
await this.generateAndGuideUser(); await this.generateAndGuideUser();
} catch (error) { } catch (error) {
@@ -78,12 +80,6 @@ class SystemCrypto {
const envKey = process.env.DATABASE_KEY; const envKey = process.env.DATABASE_KEY;
if (envKey && envKey.length >= 64) { if (envKey && envKey.length >= 64) {
this.databaseKey = Buffer.from(envKey, "hex"); this.databaseKey = Buffer.from(envKey, "hex");
const keyFingerprint = crypto
.createHash("sha256")
.update(this.databaseKey)
.digest("hex")
.substring(0, 16);
return; return;
} }
@@ -93,17 +89,13 @@ class SystemCrypto {
if (dbKeyMatch && dbKeyMatch[1] && dbKeyMatch[1].length >= 64) { if (dbKeyMatch && dbKeyMatch[1] && dbKeyMatch[1].length >= 64) {
this.databaseKey = Buffer.from(dbKeyMatch[1], "hex"); this.databaseKey = Buffer.from(dbKeyMatch[1], "hex");
process.env.DATABASE_KEY = dbKeyMatch[1]; process.env.DATABASE_KEY = dbKeyMatch[1];
const keyFingerprint = crypto
.createHash("sha256")
.update(this.databaseKey)
.digest("hex")
.substring(0, 16);
return; return;
} else { } else {
// expected - key not found or invalid length in env file
} }
} catch (fileError) {} } catch {
// expected - env file may not exist
}
await this.generateAndGuideDatabaseKey(); await this.generateAndGuideDatabaseKey();
} catch (error) { } catch (error) {
@@ -141,7 +133,9 @@ class SystemCrypto {
process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1]; process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1];
return; return;
} }
} catch (error) {} } catch {
// expected - env file may not exist
}
await this.generateAndGuideInternalAuthToken(); await this.generateAndGuideInternalAuthToken();
} catch (error) { } catch (error) {
@@ -178,7 +172,9 @@ class SystemCrypto {
process.env.CREDENTIAL_SHARING_KEY = csKeyMatch[1]; process.env.CREDENTIAL_SHARING_KEY = csKeyMatch[1];
return; return;
} }
} catch (fileError) {} } catch {
// expected - env file may not exist
}
await this.generateAndGuideCredentialSharingKey(); await this.generateAndGuideCredentialSharingKey();
} catch (error) { } catch (error) {
+20
View File
@@ -1,4 +1,5 @@
import type { Request } from "express"; import type { Request } from "express";
import crypto from "crypto";
export type DeviceType = "web" | "desktop" | "mobile"; export type DeviceType = "web" | "desktop" | "mobile";
@@ -237,3 +238,22 @@ function parseMacVersion(userAgent: string): string {
} }
return "macOS"; 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.storeKEKSalt(userId, newKekSalt);
await this.storeEncryptedDEK(userId, newEncryptedDEK); await this.storeEncryptedDEK(userId, newEncryptedDEK);
const { saveMemoryDatabaseToFile } = const { saveMemoryDatabaseToFile } = await import(
await import("../database/db/index.js"); "../database/db/index.js"
);
await saveMemoryDatabaseToFile(); await saveMemoryDatabaseToFile();
oldKEK.fill(0); oldKEK.fill(0);
@@ -340,8 +341,9 @@ class UserCrypto {
await this.storeKEKSalt(userId, newKekSalt); await this.storeKEKSalt(userId, newKekSalt);
await this.storeEncryptedDEK(userId, newEncryptedDEK); await this.storeEncryptedDEK(userId, newEncryptedDEK);
const { saveMemoryDatabaseToFile } = const { saveMemoryDatabaseToFile } = await import(
await import("../database/db/index.js"); "../database/db/index.js"
);
await saveMemoryDatabaseToFile(); await saveMemoryDatabaseToFile();
newKEK.fill(0); newKEK.fill(0);
@@ -416,8 +418,9 @@ class UserCrypto {
}, },
); );
const { saveMemoryDatabaseToFile } = const { saveMemoryDatabaseToFile } = await import(
await import("../database/db/index.js"); "../database/db/index.js"
);
await saveMemoryDatabaseToFile(); await saveMemoryDatabaseToFile();
} catch (error) { } catch (error) {
databaseLogger.error("Failed to convert to OIDC encryption", error, { databaseLogger.error("Failed to convert to OIDC encryption", error, {
+5 -4
View File
@@ -194,7 +194,7 @@ class UserDataImport {
continue; continue;
} }
const newHostData: any = { const newHostData: Record<string, unknown> = {
...host, ...host,
userId: targetUserId, userId: targetUserId,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
@@ -204,7 +204,7 @@ class UserDataImport {
newHostData.createdAt = new Date().toISOString(); newHostData.createdAt = new Date().toISOString();
} }
let processedHostData: any = newHostData; let processedHostData: Record<string, unknown> = newHostData;
if (options.userDataKey) { if (options.userDataKey) {
processedHostData = DataCrypto.encryptRecord( processedHostData = DataCrypto.encryptRecord(
"ssh_data", "ssh_data",
@@ -275,7 +275,7 @@ class UserDataImport {
continue; continue;
} }
const newCredentialData: any = { const newCredentialData: Record<string, unknown> = {
...credential, ...credential,
userId: targetUserId, userId: targetUserId,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
@@ -287,7 +287,8 @@ class UserDataImport {
newCredentialData.createdAt = new Date().toISOString(); newCredentialData.createdAt = new Date().toISOString();
} }
let processedCredentialData: any = newCredentialData; let processedCredentialData: Record<string, unknown> =
newCredentialData;
if (options.userDataKey) { if (options.userDataKey) {
processedCredentialData = DataCrypto.encryptRecord( processedCredentialData = DataCrypto.encryptRecord(
"ssh_credentials", "ssh_credentials",
+2
View File
@@ -762,6 +762,8 @@ export const DEFAULT_TERMINAL_CONFIG = {
autoMosh: false, autoMosh: false,
moshCommand: "mosh-server new -s -l LANG=en_US.UTF-8", moshCommand: "mosh-server new -s -l LANG=en_US.UTF-8",
sudoPasswordAutoFill: false, sudoPasswordAutoFill: false,
keepaliveInterval: undefined as number | undefined,
keepaliveCountMax: undefined as number | undefined,
}; };
export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG; 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 { function hasExistingAnsiCodes(text: string): boolean {
const ansiCount = ( const ansiCount = (
text.match( 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; ).length;
return ansiCount > MAX_ANSI_CODES; return ansiCount > MAX_ANSI_CODES;
} }
function hasIncompleteAnsiSequence(text: string): boolean { function hasIncompleteAnsiSequence(text: string): boolean {
return /\x1b(?:\[(?:[0-9;]*)?)?$/.test(text); return /\x1b(?:\[(?:[0-9;?>=!]*)?)?$/.test(text);
} }
interface TextSegment { interface TextSegment {
@@ -130,7 +130,7 @@ interface TextSegment {
function parseAnsiSegments(text: string): TextSegment[] { function parseAnsiSegments(text: string): TextSegment[] {
const segments: TextSegment[] = []; const segments: TextSegment[] = [];
const ansiRegex = /\x1b(?:[@-Z\\-_]|\[[0-9;]*[@-~])/g; const ansiRegex = /\x1b(?:[@-Z\\-_]|\[[0-9;?>=!]*[@-~])/g;
let lastIndex = 0; let lastIndex = 0;
let match; let match;
+86 -6
View File
@@ -331,6 +331,8 @@
"twoSplit": "2-Way", "twoSplit": "2-Way",
"threeSplit": "3-Way", "threeSplit": "3-Way",
"fourSplit": "4-Way", "fourSplit": "4-Way",
"fiveSplit": "5-Way",
"sixSplit": "6-Way",
"availableTabs": "Available Tabs", "availableTabs": "Available Tabs",
"dragTabsHint": "Drag tabs to the layout cells below to assign them", "dragTabsHint": "Drag tabs to the layout cells below to assign them",
"layout": "Split Screen Layout", "layout": "Split Screen Layout",
@@ -372,7 +374,11 @@
"warning": "Warning", "warning": "Warning",
"notValidatedWarning": "URL not validated - ensure it's correct", "notValidatedWarning": "URL not validated - ensure it's correct",
"changeServer": "Change Server", "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": { "versionCheck": {
"error": "Version Check Error", "error": "Version Check Error",
@@ -471,6 +477,7 @@
"confirm": "Confirm", "confirm": "Confirm",
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"or": "OR",
"ok": "OK", "ok": "OK",
"enabled": "Enabled", "enabled": "Enabled",
"disabled": "Disabled", "disabled": "Disabled",
@@ -601,6 +608,8 @@
"userDeletedSuccessfully": "User {{username}} deleted successfully", "userDeletedSuccessfully": "User {{username}} deleted successfully",
"failedToDeleteUser": "Failed to delete user", "failedToDeleteUser": "Failed to delete user",
"overrideUserInfoUrl": "Override User Info URL (not required)", "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", "failedToFetchSessions": "Failed to fetch sessions",
"sessionRevokedSuccessfully": "Session revoked successfully", "sessionRevokedSuccessfully": "Session revoked successfully",
"failedToRevokeSession": "Failed to revoke session", "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.", "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?", "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", "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", "sessionManagement": "Session Management",
"loadingSessions": "Loading sessions...", "loadingSessions": "Loading sessions...",
"noActiveSessions": "No active sessions found.", "noActiveSessions": "No active sessions found.",
@@ -849,6 +865,11 @@
"noHostsInJson": "No hosts found in JSON file", "noHostsInJson": "No hosts found in JSON file",
"maxHostsAllowed": "Maximum 100 hosts allowed per import", "maxHostsAllowed": "Maximum 100 hosts allowed per import",
"importCompleted": "Import completed: {{success}} successful, {{failed}} failed", "importCompleted": "Import completed: {{success}} successful, {{failed}} failed",
"importCreated": "created",
"importUpdated": "updated",
"importFailedCount": "failed",
"importSkipExisting": "Import (skip existing)",
"importOverwriteExisting": "Import (overwrite existing)",
"importFailed": "Import failed", "importFailed": "Import failed",
"importError": "Import error", "importError": "Import error",
"failedToImportJson": "Failed to import JSON file", "failedToImportJson": "Failed to import JSON file",
@@ -1027,6 +1048,9 @@
"statusCheckEnabledDesc": "Check if the server is online or offline", "statusCheckEnabledDesc": "Check if the server is online or offline",
"statusCheckInterval": "Status Check Interval", "statusCheckInterval": "Status Check Interval",
"statusCheckIntervalDesc": "How often to check if host is online (5s - 1h)", "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", "metricsEnabled": "Enable Metrics Monitoring",
"metricsEnabledDesc": "Collect CPU, RAM, disk, and other system statistics", "metricsEnabledDesc": "Collect CPU, RAM, disk, and other system statistics",
"metricsInterval": "Metrics Collection Interval", "metricsInterval": "Metrics Collection Interval",
@@ -1184,8 +1208,20 @@
"sudoPasswordAutoFillDesc": "Automatically offer to insert SSH password when sudo prompts for password", "sudoPasswordAutoFillDesc": "Automatically offer to insert SSH password when sudo prompts for password",
"sudoPassword": "Sudo Password", "sudoPassword": "Sudo Password",
"sudoPasswordDesc": "Optional password for sudo commands (useful with key authentication)", "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", "socks4": "SOCKS4",
"socks5": "SOCKS5", "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", "executeSnippetOnConnect": "Execute a snippet when the terminal connects",
"autoMosh": "Auto-MOSH", "autoMosh": "Auto-MOSH",
"autoMoshDesc": "Automatically run MOSH command on connect", "autoMoshDesc": "Automatically run MOSH command on connect",
@@ -1296,7 +1332,35 @@
"showTimestamps": "Show Timestamps", "showTimestamps": "Show Timestamps",
"autoRefresh": "Auto Refresh", "autoRefresh": "Auto Refresh",
"filterLogsPlaceholder": "Filter logs...", "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": { "terminal": {
"title": "Split Screen", "title": "Split Screen",
@@ -1304,6 +1368,8 @@
"twoSplit": "2-Split", "twoSplit": "2-Split",
"threeSplit": "3-Split", "threeSplit": "3-Split",
"fourSplit": "4-Split", "fourSplit": "4-Split",
"fiveSplit": "5-Split",
"sixSplit": "6-Split",
"availableTabs": "Available Tabs", "availableTabs": "Available Tabs",
"dragTabsHint": "Drag tabs into the grid below to position them", "dragTabsHint": "Drag tabs into the grid below to position them",
"layout": "Layout", "layout": "Layout",
@@ -1339,6 +1405,8 @@
"connectionClosed": "Connection closed", "connectionClosed": "Connection closed",
"connectionError": "Connection error: {{message}}", "connectionError": "Connection error: {{message}}",
"connected": "Connected", "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", "sshConnected": "SSH connection established",
"authError": "Authentication failed: {{message}}", "authError": "Authentication failed: {{message}}",
"unknownError": "Unknown error occurred", "unknownError": "Unknown error occurred",
@@ -1393,7 +1461,9 @@
"totpTimeout": "TOTP verification timeout. Please reconnect.", "totpTimeout": "TOTP verification timeout. Please reconnect.",
"passwordTimeout": "Password verification timeout. Please reconnect.", "passwordTimeout": "Password verification timeout. Please reconnect.",
"connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.", "connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.",
"hostKeyRejected": "SSH host key verification rejected. Connection cancelled." "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": { "fileManager": {
"title": "File Manager", "title": "File Manager",
@@ -1718,7 +1788,13 @@
"write": "Write", "write": "Write",
"execute": "Execute", "execute": "Execute",
"permissionsChangedSuccessfully": "Permissions changed successfully", "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": { "tunnel": {
"noTunnelsConfigured": "No Tunnels Configured", "noTunnelsConfigured": "No Tunnels Configured",
@@ -1897,7 +1973,7 @@
"loginButton": "Login", "loginButton": "Login",
"registerButton": "Register", "registerButton": "Register",
"forgotPassword": "Forgot Password?", "forgotPassword": "Forgot Password?",
"rememberMe": "Remember Me", "rememberMe": "Remember Device for 30 Days (includes TOTP)",
"noAccount": "Don't have an account?", "noAccount": "Don't have an account?",
"hasAccount": "Already have an account?", "hasAccount": "Already have an account?",
"loginSuccess": "Login successful", "loginSuccess": "Login successful",
@@ -2062,6 +2138,7 @@
"processing": "Processing...", "processing": "Processing...",
"pleaseWait": "Please wait...", "pleaseWait": "Please wait...",
"registrationDisabled": "New account registration is currently disabled by an admin. Please log in or contact an administrator.", "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", "databaseConnected": "Database connected successfully",
"databaseConnectionFailed": "Failed to connect to the database server", "databaseConnectionFailed": "Failed to connect to the database server",
"checkServerConnection": "Please check your server connection and try again", "checkServerConnection": "Please check your server connection and try again",
@@ -2117,7 +2194,9 @@
"appearanceDesc": "Select the color theme for the application", "appearanceDesc": "Select the color theme for the application",
"terminalSyntaxHighlightingDesc": "Automatically highlight commands, paths, IPs, and log levels in terminal output", "terminalSyntaxHighlightingDesc": "Automatically highlight commands, paths, IPs, and log levels in terminal output",
"enableCommandPaletteShortcut": "Enable Command Palette Shortcut", "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": { "user": {
"failedToLoadVersionInfo": "Failed to load version information" "failedToLoadVersionInfo": "Failed to load version information"
@@ -2153,6 +2232,7 @@
"usernameField": "name", "usernameField": "name",
"scopes": "openid email profile", "scopes": "openid email profile",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/", "userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"allowedUsers": "user@example.com, @company.com",
"enterUsername": "Enter username to make admin", "enterUsername": "Enter username to make admin",
"searchHosts": "Search hosts by name, username, IP, folder, tags...", "searchHosts": "Search hosts by name, username, IP, folder, tags...",
"enterPassword": "Enter your password", "enterPassword": "Enter your password",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "Geloofsbriewe",
"credentialsViewer": "Geloofsbriewe-kyker", "credentialsViewer": "Geloofsbriewe-kyker",
"manageYourSSHCredentials": "Bestuur jou SSH-bewyse veilig", "manageYourSSHCredentials": "Bestuur jou SSH-bewyse veilig",
"addCredential": "Voeg geloofsbriewe by", "addCredential": "Voeg geloofsbriewe by",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "Dit sal die publieke sleutel veilig by die teikengasheer se ~/.ssh/authorized_keys-lêer voeg sonder om bestaande sleutels te oorskryf. Die bewerking is omkeerbaar.", "deploymentProcessDescription": "Dit sal die publieke sleutel veilig by die teikengasheer se ~/.ssh/authorized_keys-lêer voeg sonder om bestaande sleutels te oorskryf. Die bewerking is omkeerbaar.",
"chooseHostToDeploy": "Kies 'n gasheer om na te ontplooi...", "chooseHostToDeploy": "Kies 'n gasheer om na te ontplooi...",
"deploying": "Implementeer tans...", "deploying": "Implementeer tans...",
"copyDeployCommand": "Kopieer Ontplooi-opdrag",
"copyDeployCommandDescription": "Kopieer 'n dopopdrag om die publieke sleutel handmatig by 'n gasheer se authorized_keys-lêer te voeg. Nuttig wanneer wagwoordverifikasie gedeaktiveer is.",
"deployCommandCopied": "Ontplooi-opdrag gekopieer na knipbord",
"manualDeployInfo": "Plak hierdie opdrag in die teikengasheer se terminaal (bv. Proxmox-konsole, IPMI of fisiese toegang) om die SSH-sleutel by te voeg.",
"name": "Naam", "name": "Naam",
"noHostsAvailable": "Geen gashere beskikbaar nie", "noHostsAvailable": "Geen gashere beskikbaar nie",
"noHostsMatchSearch": "Geen gashere stem ooreen met jou soektog nie", "noHostsMatchSearch": "Geen gashere stem ooreen met jou soektog nie",
@@ -303,9 +298,7 @@
"createFolder": "Skep vouer", "createFolder": "Skep vouer",
"editFolder": "Wysig vouer", "editFolder": "Wysig vouer",
"editFolderDescription": "Pas jou brokkie-lêergids aan", "editFolderDescription": "Pas jou brokkie-lêergids aan",
"createFolderDescription": "Organiseer jou brokkies in dopgehou", "createFolderDescription": "Organiseer jou brokkies in dopgehou"
"confirmExecution": "Voer \"{{name}}\" uit?",
"confirmExecutionDesc": "Is jy seker jy wil hierdie fragment uitvoer?"
}, },
"commandHistory": { "commandHistory": {
"title": "Geskiedenis", "title": "Geskiedenis",
@@ -409,7 +402,6 @@
"required": "Vereis", "required": "Vereis",
"optional": "Opsioneel", "optional": "Opsioneel",
"connect": "Verbind", "connect": "Verbind",
"copied": "Gekopieer",
"connecting": "Verbind...", "connecting": "Verbind...",
"creating": "Skep...", "creating": "Skep...",
"clear": "Duidelik", "clear": "Duidelik",
@@ -502,7 +494,6 @@
"checking": "Kontroleer...", "checking": "Kontroleer...",
"checkingDatabase": "Kontroleer databasisverbinding...", "checkingDatabase": "Kontroleer databasisverbinding...",
"checkingAuthentication": "Kontroleer tans verifikasie...", "checkingAuthentication": "Kontroleer tans verifikasie...",
"backendReconnected": "Bedienerverbinding herstel",
"actions": "Aksies", "actions": "Aksies",
"remove": "Verwyder", "remove": "Verwyder",
"revoke": "Herroep", "revoke": "Herroep",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "Gasheerbestuurder", "title": "Gasheerbestuurder",
"hosts": "Gashere",
"sshHosts": "SSH-gashere", "sshHosts": "SSH-gashere",
"noHosts": "Geen SSH-gashere nie", "noHosts": "Geen SSH-gashere nie",
"noHostsMessage": "Jy het nog geen SSH-gashere bygevoeg nie. Klik \"Voeg gasheer by\" om te begin.", "noHostsMessage": "Jy het nog geen SSH-gashere bygevoeg nie. Klik \"Voeg gasheer by\" om te begin.",
@@ -846,7 +836,7 @@
"failedToImportJson": "Kon nie JSON-lêer invoer nie", "failedToImportJson": "Kon nie JSON-lêer invoer nie",
"connectionDetails": "Verbindingsbesonderhede", "connectionDetails": "Verbindingsbesonderhede",
"organization": "Organisasie", "organization": "Organisasie",
"ipAddress": "IP-adres of gasheernaam", "ipAddress": "IP-adres",
"port": "Hawe", "port": "Hawe",
"name": "Naam", "name": "Naam",
"username": "Gebruikersnaam", "username": "Gebruikersnaam",
@@ -925,8 +915,6 @@
"key": "Sleutel", "key": "Sleutel",
"credential": "Geloofsbrief", "credential": "Geloofsbrief",
"none": "Geen", "none": "Geen",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Vra jou vir OPKSSH webmagtiging met elke verbinding. Jou magtigingstoken sal 24 uur lank geldig wees totdat dit hernu moet word.",
"selectCredential": "Kies geloofsbriewe", "selectCredential": "Kies geloofsbriewe",
"selectCredentialPlaceholder": "Kies 'n geloofsbrief...", "selectCredentialPlaceholder": "Kies 'n geloofsbrief...",
"credentialRequired": "Geloofsbriewe word vereis wanneer geloofsbriewe-verifikasie gebruik word", "credentialRequired": "Geloofsbriewe word vereis wanneer geloofsbriewe-verifikasie gebruik word",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "Sleutelbord-Interaktiewe Verifikasie", "noneAuthTitle": "Sleutelbord-Interaktiewe Verifikasie",
"noneAuthDescription": "Hierdie verifikasiemetode sal sleutelbord-interaktiewe verifikasie gebruik wanneer daar aan die SSH-bediener gekoppel word.", "noneAuthDescription": "Hierdie verifikasiemetode sal sleutelbord-interaktiewe verifikasie gebruik wanneer daar aan die SSH-bediener gekoppel word.",
"noneAuthDetails": "Sleutelbord-interaktiewe verifikasie laat die bediener toe om jou vir geloofsbriewe te vra tydens verbinding. Dit is nuttig vir bedieners wat multifaktor-verifikasie vereis of as jy nie geloofsbriewe plaaslik wil stoor nie.", "noneAuthDetails": "Sleutelbord-interaktiewe verifikasie laat die bediener toe om jou vir geloofsbriewe te vra tydens verbinding. Dit is nuttig vir bedieners wat multifaktor-verifikasie vereis of as jy nie geloofsbriewe plaaslik wil stoor nie.",
"opksshAuthTitle": "OPKSSH-verifikasie",
"forceKeyboardInteractive": "Forseer sleutelbord-interaktief", "forceKeyboardInteractive": "Forseer sleutelbord-interaktief",
"forceKeyboardInteractiveDesc": "Dwing die gebruik van sleutelbord-interaktiewe verifikasie af. Dit word soms vereis vir bedieners wat tweefaktor-verifikasie (TOTP/2FA) gebruik.", "forceKeyboardInteractiveDesc": "Dwing die gebruik van sleutelbord-interaktiewe verifikasie af. Dit word soms vereis vir bedieners wat tweefaktor-verifikasie (TOTP/2FA) gebruik.",
"overrideCredentialUsername": "Oorskryf geloofsbriewe gebruikersnaam", "overrideCredentialUsername": "Oorskryf geloofsbriewe gebruikersnaam",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "Maak oop in blaaier", "warpgateOpenBrowser": "Maak oop in blaaier",
"warpgateContinue": "Ek het verifikasie voltooi", "warpgateContinue": "Ek het verifikasie voltooi",
"warpgateTimeout": "Warpgate-verifikasietydverstryking. Koppel asseblief weer.", "warpgateTimeout": "Warpgate-verifikasietydverstryking. Koppel asseblief weer.",
"opksshAuthRequired": "OPKSSH-verifikasie vereis",
"opksshAuthDescription": "Voltooi verifikasie in jou blaaier om voort te gaan. Hierdie sessie sal vir 24 uur geldig bly.",
"opksshAuthUrl": "Verifikasie-URL",
"opksshOpenBrowser": "Maak blaaier oop om te verifieer",
"opksshWaitingForAuth": "Wag vir verifikasie in blaaier...",
"opksshAuthenticating": "Verwerk verifikasie...",
"opksshTimeout": "Verifikasie het verstryk. Probeer asseblief weer.",
"opksshAuthFailed": "Verifikasie het misluk. Kontroleer asseblief jou geloofsbriewe en probeer weer.",
"opksshConfigMissing": "OPKSSH-konfigurasie nie gevind nie. Skep asseblief ~/.opk/config.yml met jou OIDC-verskafferinstellings. Sien dokumentasie: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Wagwoord invoeg?", "sudoPasswordPopupTitle": "Wagwoord invoeg?",
"sudoPasswordPopupHint": "Druk Enter om in te voeg, Esc om te verwerp", "sudoPasswordPopupHint": "Druk Enter om in te voeg, Esc om te verwerp",
"sudoPasswordPopupConfirm": "Invoeg", "sudoPasswordPopupConfirm": "Invoeg",
@@ -1359,8 +1337,7 @@
"automaticFallback": "Probeer outomaties {{method}} verifikasie...", "automaticFallback": "Probeer outomaties {{method}} verifikasie...",
"totpTimeout": "TOTP-verifikasie het uitgeloop. Koppel asseblief weer aan.", "totpTimeout": "TOTP-verifikasie het uitgeloop. Koppel asseblief weer aan.",
"passwordTimeout": "Wagwoordverifikasie 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.", "connectionRejected": "Verbinding deur bediener verwerp. Kontroleer asseblief u verifikasie en netwerkkonfigurasie."
"hostKeyRejected": "SSH-gasheersleutelverifikasie verwerp. Verbinding gekanselleer."
}, },
"fileManager": { "fileManager": {
"title": "Lêerbestuurder", "title": "Lêerbestuurder",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "Wagwoordherstel suksesvol", "passwordResetSuccess": "Wagwoordherstel suksesvol",
"passwordResetSuccessDesc": "Jou wagwoord is suksesvol herstel. Jy kan nou met jou nuwe wagwoord aanmeld." "passwordResetSuccessDesc": "Jou wagwoord is suksesvol herstel. Jy kan nou met jou nuwe wagwoord aanmeld."
}, },
"hostKey": {
"verifyNewHost": "Verifieer SSH-gasheersleutel",
"keyChangedWarning": "SSH-gasheersleutel verander",
"firstConnectionTitle": "Eerste keer dat jy met hierdie gasheer verbind",
"firstConnectionDescription": "Die egtheid van hierdie gasheer kan nie vasgestel word nie. Verifieer dat die vingerafdruk ooreenstem met wat jy verwag.",
"keyChangedDescription": "Die gasheersleutel vir hierdie bediener het verander sedert jou laaste verbinding. Dit kan op 'n sekuriteitsprobleem dui.",
"previousKey": "Vorige Sleutel",
"newFingerprint": "Nuwe vingerafdruk",
"fingerprint": "Vingerafdruk",
"verifyInstructions": "As jy hierdie gasheer vertrou, klik Aanvaar om voort te gaan en hierdie vingerafdruk vir toekomstige verbindings te stoor.",
"securityWarning": "Sekuriteitswaarskuwing",
"acceptAndContinue": "Aanvaar en Gaan voort",
"acceptNewKey": "Aanvaar Nuwe Sleutel & Gaan Voort"
},
"errors": { "errors": {
"notFound": "Bladsy nie gevind nie", "notFound": "Bladsy nie gevind nie",
"unauthorized": "Ongemagtigde toegang", "unauthorized": "Ongemagtigde toegang",
@@ -2063,8 +2026,6 @@
"terminalSettings": "Terminaal", "terminalSettings": "Terminaal",
"hostSidebarSettings": "Gasheer en sybalk", "hostSidebarSettings": "Gasheer en sybalk",
"snippetsSettings": "Brokkies", "snippetsSettings": "Brokkies",
"confirmSnippetExecution": "Bevestig Uitvoering van Brokkie",
"confirmSnippetExecutionDesc": "Wys bevestigingsdialoog voordat brokkies uitgevoer word",
"updateSettings": "Opdaterings", "updateSettings": "Opdaterings",
"disableUpdateCheck": "Deaktiveer Opdateringskontrole", "disableUpdateCheck": "Deaktiveer Opdateringskontrole",
"disableUpdateCheckDesc": "Hou op om vir nuwe weergawes te kyk tydens opstart en op die dashboard. Verminder netwerkversoeke.", "disableUpdateCheckDesc": "Hou op om vir nuwe weergawes te kyk tydens opstart en op die dashboard. Verminder netwerkversoeke.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 of example.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "وثائق التفويض",
"credentialsViewer": "عارض وثائق التفويض", "credentialsViewer": "عارض وثائق التفويض",
"manageYourSSHCredentials": "إدارة بيانات اعتماد SSH الخاصة بك بشكل آمن", "manageYourSSHCredentials": "إدارة بيانات اعتماد SSH الخاصة بك بشكل آمن",
"addCredential": "إضافة بيانات اعتماد", "addCredential": "إضافة بيانات اعتماد",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "سيؤدي هذا بأمان إلى إضافة المفتاح العمومي إلى ملف المضيف الهدف ~/.ssh/authorized_keyys دون الكتابة فوق المفاتيح الموجودة. العملية قابلة للرجوع عنها.", "deploymentProcessDescription": "سيؤدي هذا بأمان إلى إضافة المفتاح العمومي إلى ملف المضيف الهدف ~/.ssh/authorized_keyys دون الكتابة فوق المفاتيح الموجودة. العملية قابلة للرجوع عنها.",
"chooseHostToDeploy": "اختر مضيف ليقوم بنشره إلى...", "chooseHostToDeploy": "اختر مضيف ليقوم بنشره إلى...",
"deploying": "النشر...", "deploying": "النشر...",
"copyDeployCommand": "نسخ أمر النشر",
"copyDeployCommandDescription": "نسخ أمر Shell لإضافة المفتاح العام يدوياً إلى ملف مفتاح_مفاتيح مضيف مضيف. مفيد عندما يتم تعطيل مصادقة كلمة المرور.",
"deployCommandCopied": "نشر أمر نسخ إلى الحافظة",
"manualDeployInfo": "قم بلصق هذا الأمر في محطة المضيف المستهدفة (مثل وحدة تحكم Proxmox أو IPMI، أو الوصول المادي) لإضافة مفتاح SSH.",
"name": "الاسم", "name": "الاسم",
"noHostsAvailable": "لا يوجد مضيفين متاحين", "noHostsAvailable": "لا يوجد مضيفين متاحين",
"noHostsMatchSearch": "لا يوجد مضيف يطابق بحثك", "noHostsMatchSearch": "لا يوجد مضيف يطابق بحثك",
@@ -303,9 +298,7 @@
"createFolder": "إنشاء مجلد", "createFolder": "إنشاء مجلد",
"editFolder": "تعديل المجلد", "editFolder": "تعديل المجلد",
"editFolderDescription": "تخصيص مجلد كتلة الكود", "editFolderDescription": "تخصيص مجلد كتلة الكود",
"createFolderDescription": "تنظيم كتل الكتل في مجلدات", "createFolderDescription": "تنظيم كتل الكتل في مجلدات"
"confirmExecution": "تنفيذ \"{{name}}\"؟",
"confirmExecutionDesc": "هل أنت متأكد من أنك تريد تنفيذ كتلة الكود هذه؟"
}, },
"commandHistory": { "commandHistory": {
"title": "التاريخ", "title": "التاريخ",
@@ -409,7 +402,6 @@
"required": "مطلوب", "required": "مطلوب",
"optional": "اختياري", "optional": "اختياري",
"connect": "الاتصال", "connect": "الاتصال",
"copied": "منسوخ",
"connecting": "جاري الاتصال...", "connecting": "جاري الاتصال...",
"creating": "إنشاء...", "creating": "إنشاء...",
"clear": "مسح", "clear": "مسح",
@@ -502,7 +494,6 @@
"checking": "يتحقق...", "checking": "يتحقق...",
"checkingDatabase": "التحقق من اتصال قاعدة البيانات...", "checkingDatabase": "التحقق من اتصال قاعدة البيانات...",
"checkingAuthentication": "التحقق من المصادقة...", "checkingAuthentication": "التحقق من المصادقة...",
"backendReconnected": "تم استعادة اتصال الخادم",
"actions": "الإجراءات", "actions": "الإجراءات",
"remove": "إزالة", "remove": "إزالة",
"revoke": "Revoke", "revoke": "Revoke",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "مدير المضيف", "title": "مدير المضيف",
"hosts": "المضيفون",
"sshHosts": "مضيفي SSH", "sshHosts": "مضيفي SSH",
"noHosts": "لا يوجد مضيفين SSH", "noHosts": "لا يوجد مضيفين SSH",
"noHostsMessage": "لم تقم بإضافة أي مضيف SSH حتى الآن. انقر فوق \"إضافة مضيف\" للبدء.", "noHostsMessage": "لم تقم بإضافة أي مضيف SSH حتى الآن. انقر فوق \"إضافة مضيف\" للبدء.",
@@ -846,7 +836,7 @@
"failedToImportJson": "فشل استيراد ملف JSON", "failedToImportJson": "فشل استيراد ملف JSON",
"connectionDetails": "تفاصيل الاتصال", "connectionDetails": "تفاصيل الاتصال",
"organization": "المنظمة", "organization": "المنظمة",
"ipAddress": "عنوان IP أو اسم المضيف", "ipAddress": "عنوان IP",
"port": "المنفذ", "port": "المنفذ",
"name": "الاسم", "name": "الاسم",
"username": "اسم المستخدم", "username": "اسم المستخدم",
@@ -925,8 +915,6 @@
"key": "المفتاح", "key": "المفتاح",
"credential": "بيانات", "credential": "بيانات",
"none": "لا", "none": "لا",
"opkssh": "فتح",
"opksshAuthDescription": "يمنحك الحصول على مصادقة ويب OPKSSH عند كل اتصال. رمز المصادقة الخاص بك سوف يستمر لمدة 24 ساعة حتى يجب تجديده.",
"selectCredential": "حدد بيانات الاعتماد", "selectCredential": "حدد بيانات الاعتماد",
"selectCredentialPlaceholder": "اختر بيانات الاعتماد...", "selectCredentialPlaceholder": "اختر بيانات الاعتماد...",
"credentialRequired": "الاعتماد مطلوب عند استخدام مصادقة الاعتماد", "credentialRequired": "الاعتماد مطلوب عند استخدام مصادقة الاعتماد",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "المصادقة التفاعلية على لوحة المفاتيح", "noneAuthTitle": "المصادقة التفاعلية على لوحة المفاتيح",
"noneAuthDescription": "طريقة المصادقة هذه ستستخدم مصادقة لوحة المفاتيح التفاعلية عند الاتصال بخادم SSH.", "noneAuthDescription": "طريقة المصادقة هذه ستستخدم مصادقة لوحة المفاتيح التفاعلية عند الاتصال بخادم SSH.",
"noneAuthDetails": "المصادقة التفاعلية بلوحة المفاتيح تسمح للخادم بمطالبتك للحصول على بيانات الاعتماد أثناء الاتصال. هذا مفيد للخوادم التي تتطلب مصادقة متعددة العناصر أو إذا كنت لا تريد حفظ بيانات الاعتماد محلياً.", "noneAuthDetails": "المصادقة التفاعلية بلوحة المفاتيح تسمح للخادم بمطالبتك للحصول على بيانات الاعتماد أثناء الاتصال. هذا مفيد للخوادم التي تتطلب مصادقة متعددة العناصر أو إذا كنت لا تريد حفظ بيانات الاعتماد محلياً.",
"opksshAuthTitle": "المصادقة المفتوحة",
"forceKeyboardInteractive": "تفعيل لوحة المفاتيح", "forceKeyboardInteractive": "تفعيل لوحة المفاتيح",
"forceKeyboardInteractiveDesc": "يفرض استخدام مصادقة لوحة المفاتيح التفاعلية. هذا مطلوب أحيانا للخوادم التي تستخدم المصادقة الثنائية (TOTP/2FA).", "forceKeyboardInteractiveDesc": "يفرض استخدام مصادقة لوحة المفاتيح التفاعلية. هذا مطلوب أحيانا للخوادم التي تستخدم المصادقة الثنائية (TOTP/2FA).",
"overrideCredentialUsername": "تجاوز اسم المستخدم", "overrideCredentialUsername": "تجاوز اسم المستخدم",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "فتح في المتصفح", "warpgateOpenBrowser": "فتح في المتصفح",
"warpgateContinue": "لقد أكملت المصادقة", "warpgateContinue": "لقد أكملت المصادقة",
"warpgateTimeout": "انتهت مهلة مصادقة التحذير. الرجاء إعادة الاتصال.", "warpgateTimeout": "انتهت مهلة مصادقة التحذير. الرجاء إعادة الاتصال.",
"opksshAuthRequired": "المصادقة مفتوحة مطلوبة",
"opksshAuthDescription": "أكمل المصادقة في المتصفح الخاص بك للمتابعة. ستبقى هذه الجلسة صالحة لمدة 24 ساعة.",
"opksshAuthUrl": "رابط المصادقة",
"opksshOpenBrowser": "فتح المتصفح للمصادقة",
"opksshWaitingForAuth": "في انتظار المصادقة في المتصفح...",
"opksshAuthenticating": "جاري معالجة المصادقة...",
"opksshTimeout": "انتهت مهلة المصادقة. الرجاء المحاولة مرة أخرى.",
"opksshAuthFailed": "فشلت المصادقة. الرجاء التحقق من بيانات الاعتماد الخاصة بك وحاول مرة أخرى.",
"opksshConfigMissing": "لم يتم العثور على تكوين الفتح. الرجاء إنشاء ~/.opk/config.yml مع إعدادات موفر OIDC. انظر الوثائق: https://github.com/openpubkey/opkssh#config.",
"sudoPasswordPopupTitle": "إدراج كلمة المرور؟", "sudoPasswordPopupTitle": "إدراج كلمة المرور؟",
"sudoPasswordPopupHint": "اضغط Enter للإدراج ، Esc للإلغاء", "sudoPasswordPopupHint": "اضغط Enter للإدراج ، Esc للإلغاء",
"sudoPasswordPopupConfirm": "Insert", "sudoPasswordPopupConfirm": "Insert",
@@ -1359,8 +1337,7 @@
"automaticFallback": "جرب مصادقة {{method}} تلقائياً...", "automaticFallback": "جرب مصادقة {{method}} تلقائياً...",
"totpTimeout": "انتهت مهلة التحقق من TOTP. الرجاء إعادة الاتصال.", "totpTimeout": "انتهت مهلة التحقق من TOTP. الرجاء إعادة الاتصال.",
"passwordTimeout": "انتهت مهلة التحقق من كلمة المرور. الرجاء إعادة الاتصال.", "passwordTimeout": "انتهت مهلة التحقق من كلمة المرور. الرجاء إعادة الاتصال.",
"connectionRejected": "تم رفض الاتصال بواسطة الخادم. الرجاء التحقق من المصادقة الخاصة بك وتكوين الشبكة.", "connectionRejected": "تم رفض الاتصال بواسطة الخادم. الرجاء التحقق من المصادقة الخاصة بك وتكوين الشبكة."
"hostKeyRejected": "تم رفض التحقق من مفتاح المضيف SSH. تم إلغاء الاتصال."
}, },
"fileManager": { "fileManager": {
"title": "مدير الملفات", "title": "مدير الملفات",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "تم إعادة تعيين كلمة المرور بنجاح", "passwordResetSuccess": "تم إعادة تعيين كلمة المرور بنجاح",
"passwordResetSuccessDesc": "تم إعادة تعيين كلمة المرور الخاصة بك بنجاح. يمكنك الآن تسجيل الدخول باستخدام كلمة المرور الجديدة." "passwordResetSuccessDesc": "تم إعادة تعيين كلمة المرور الخاصة بك بنجاح. يمكنك الآن تسجيل الدخول باستخدام كلمة المرور الجديدة."
}, },
"hostKey": {
"verifyNewHost": "التحقق من مفتاح استضافة SSH",
"keyChangedWarning": "تم تغيير مفتاح المضيف SSH",
"firstConnectionTitle": "أول مرة تتصل بهذا المضيف",
"firstConnectionDescription": "لا يمكن إنشاء صحة هذا المضيف. تحقق من بصمة الإصبع تطابق ما تتوقعه.",
"keyChangedDescription": "مفتاح المضيف لهذا الخادم قد تغير منذ الاتصال الأخير. يمكن أن يشير هذا إلى مشكلة أمنية.",
"previousKey": "المفتاح السابق",
"newFingerprint": "بصمة جديدة",
"fingerprint": "بصمة",
"verifyInstructions": "إذا كنت تثق بهذا المضيف، انقر فوق قبول للمتابعة وحفظ بصمة الإصبع هذه للاتصال في المستقبل.",
"securityWarning": "تحذير الأمان",
"acceptAndContinue": "قبول ومتابعة",
"acceptNewKey": "قبول المفتاح الجديد ومتابعة"
},
"errors": { "errors": {
"notFound": "لم يتم العثور على الصفحة", "notFound": "لم يتم العثور على الصفحة",
"unauthorized": "الوصول غير المصرح به", "unauthorized": "الوصول غير المصرح به",
@@ -2063,8 +2026,6 @@
"terminalSettings": "المحطة", "terminalSettings": "المحطة",
"hostSidebarSettings": "المضيف و الشريط الجانبي", "hostSidebarSettings": "المضيف و الشريط الجانبي",
"snippetsSettings": "كتل الكود", "snippetsSettings": "كتل الكود",
"confirmSnippetExecution": "تأكيد تنفيذ كتلة الكود",
"confirmSnippetExecutionDesc": "إظهار مربع حوار التأكيد قبل تنفيذ كتل الكود",
"updateSettings": "التحديثات", "updateSettings": "التحديثات",
"disableUpdateCheck": "تعطيل التحقق من التحديث", "disableUpdateCheck": "تعطيل التحقق من التحديث",
"disableUpdateCheckDesc": "إيقاف التحقق من الإصدارات الجديدة عند بدء التشغيل و لوحة التحكم. يقلل من طلبات الشبكة.", "disableUpdateCheckDesc": "إيقاف التحقق من الإصدارات الجديدة عند بدء التشغيل و لوحة التحكم. يقلل من طلبات الشبكة.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1-1 أو مثال.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "Пълномощия",
"credentialsViewer": "Преглед на идентификационни данни", "credentialsViewer": "Преглед на идентификационни данни",
"manageYourSSHCredentials": "Управлявайте сигурно вашите SSH идентификационни данни", "manageYourSSHCredentials": "Управлявайте сигурно вашите SSH идентификационни данни",
"addCredential": "Добавяне на идентификационни данни", "addCredential": "Добавяне на идентификационни данни",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "Това безопасно ще добави публичния ключ към файла ~/.ssh/authorized_keys на целевия хост, без да презаписва съществуващите ключове. Операцията е обратима.", "deploymentProcessDescription": "Това безопасно ще добави публичния ключ към файла ~/.ssh/authorized_keys на целевия хост, без да презаписва съществуващите ключове. Операцията е обратима.",
"chooseHostToDeploy": "Изберете хост, на който да се разположи...", "chooseHostToDeploy": "Изберете хост, на който да се разположи...",
"deploying": "Разгръщане...", "deploying": "Разгръщане...",
"copyDeployCommand": "Копиране на командата за разгръщане",
"copyDeployCommandDescription": "Копирайте команда от shell, за да добавите ръчно публичния ключ към файла authorized_keys на хоста. Полезно е, когато удостоверяването с парола е деактивирано.",
"deployCommandCopied": "Командата за разгръщане е копирана в клипборда",
"manualDeployInfo": "Поставете тази команда в терминала на целевия хост (напр. конзола на Proxmox, IPMI или физически достъп), за да добавите SSH ключа.",
"name": "Име", "name": "Име",
"noHostsAvailable": "Няма налични хостове", "noHostsAvailable": "Няма налични хостове",
"noHostsMatchSearch": "Няма хостове, които да отговарят на вашето търсене", "noHostsMatchSearch": "Няма хостове, които да отговарят на вашето търсене",
@@ -303,9 +298,7 @@
"createFolder": "Създаване на папка", "createFolder": "Създаване на папка",
"editFolder": "Редактиране на папка", "editFolder": "Редактиране на папка",
"editFolderDescription": "Персонализирайте папката си с фрагменти", "editFolderDescription": "Персонализирайте папката си с фрагменти",
"createFolderDescription": "Организирайте фрагментите си в папки", "createFolderDescription": "Организирайте фрагментите си в папки"
"confirmExecution": "Да се изпълни „{{name}}“?",
"confirmExecutionDesc": "Сигурни ли сте, че искате да изпълните този фрагмент?"
}, },
"commandHistory": { "commandHistory": {
"title": "История", "title": "История",
@@ -409,7 +402,6 @@
"required": "Задължително", "required": "Задължително",
"optional": "По избор", "optional": "По избор",
"connect": "Свържете се", "connect": "Свържете се",
"copied": "Копирано",
"connecting": "Свързване...", "connecting": "Свързване...",
"creating": "Създаване...", "creating": "Създаване...",
"clear": "Изчисти", "clear": "Изчисти",
@@ -502,7 +494,6 @@
"checking": "Проверка...", "checking": "Проверка...",
"checkingDatabase": "Проверка на връзката с базата данни...", "checkingDatabase": "Проверка на връзката с базата данни...",
"checkingAuthentication": "Проверка на удостоверяването...", "checkingAuthentication": "Проверка на удостоверяването...",
"backendReconnected": "Връзката със сървъра е възстановена",
"actions": "Действия", "actions": "Действия",
"remove": "Премахване", "remove": "Премахване",
"revoke": "Отмяна", "revoke": "Отмяна",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "Мениджър на домакини", "title": "Мениджър на домакини",
"hosts": "Домакини",
"sshHosts": "SSH хостове", "sshHosts": "SSH хостове",
"noHosts": "Няма SSH хостове", "noHosts": "Няма SSH хостове",
"noHostsMessage": "Все още не сте добавили SSH хостове. Кликнете върху „Добавяне на хост“, за да започнете.", "noHostsMessage": "Все още не сте добавили SSH хостове. Кликнете върху „Добавяне на хост“, за да започнете.",
@@ -846,7 +836,7 @@
"failedToImportJson": "Импортирането на JSON файл не бе успешно", "failedToImportJson": "Импортирането на JSON файл не бе успешно",
"connectionDetails": "Детайли за връзката", "connectionDetails": "Детайли за връзката",
"organization": "Организация", "organization": "Организация",
"ipAddress": "IP адрес или име на хост", "ipAddress": "IP адрес",
"port": "Порт", "port": "Порт",
"name": "Име", "name": "Име",
"username": "Потребителско име", "username": "Потребителско име",
@@ -925,8 +915,6 @@
"key": "Ключ", "key": "Ключ",
"credential": "Пълномощия", "credential": "Пълномощия",
"none": "Няма", "none": "Няма",
"opkssh": "ОПКСШ",
"opksshAuthDescription": "Подканва ви за OPKSSH уеб-авторизация при всяко свързване. Вашият токен за удостоверяване ще бъде валиден 24 часа, докато не се наложи да бъде подновен.",
"selectCredential": "Изберете идентификационни данни", "selectCredential": "Изберете идентификационни данни",
"selectCredentialPlaceholder": "Изберете удостоверение за самоличност...", "selectCredentialPlaceholder": "Изберете удостоверение за самоличност...",
"credentialRequired": "Изисква се удостоверяване на идентификационни данни при използване на удостоверяване с идентификационни данни", "credentialRequired": "Изисква се удостоверяване на идентификационни данни при използване на удостоверяване с идентификационни данни",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "Интерактивно удостоверяване с клавиатура", "noneAuthTitle": "Интерактивно удостоверяване с клавиатура",
"noneAuthDescription": "Този метод за удостоверяване ще използва интерактивно удостоверяване с клавиатура при свързване към SSH сървъра.", "noneAuthDescription": "Този метод за удостоверяване ще използва интерактивно удостоверяване с клавиатура при свързване към SSH сървъра.",
"noneAuthDetails": "Интерактивното удостоверяване с клавиатура позволява на сървъра да ви пита за идентификационни данни по време на връзка. Това е полезно за сървъри, които изискват многофакторно удостоверяване или ако не искате да запазвате идентификационните данни локално.", "noneAuthDetails": "Интерактивното удостоверяване с клавиатура позволява на сървъра да ви пита за идентификационни данни по време на връзка. Това е полезно за сървъри, които изискват многофакторно удостоверяване или ако не искате да запазвате идентификационните данни локално.",
"opksshAuthTitle": "OPKSSH удостоверяване",
"forceKeyboardInteractive": "Принудително интерактивно с клавиатура", "forceKeyboardInteractive": "Принудително интерактивно с клавиатура",
"forceKeyboardInteractiveDesc": "Налага използването на интерактивно удостоверяване с клавиатура. Това понякога е необходимо за сървъри, които използват двуфакторно удостоверяване (TOTP/2FA).", "forceKeyboardInteractiveDesc": "Налага използването на интерактивно удостоверяване с клавиатура. Това понякога е необходимо за сървъри, които използват двуфакторно удостоверяване (TOTP/2FA).",
"overrideCredentialUsername": "Замяна на потребителско име за идентификационни данни", "overrideCredentialUsername": "Замяна на потребителско име за идентификационни данни",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "Отваряне в браузъра", "warpgateOpenBrowser": "Отваряне в браузъра",
"warpgateContinue": "Завърших удостоверяването", "warpgateContinue": "Завърших удостоверяването",
"warpgateTimeout": "Времето за изчакване за удостоверяване на Warpgate изтече. Моля, свържете се отново.", "warpgateTimeout": "Времето за изчакване за удостоверяване на Warpgate изтече. Моля, свържете се отново.",
"opksshAuthRequired": "Изисква се удостоверяване на OPKSSH",
"opksshAuthDescription": "За да продължите, завършете удостоверяването в браузъра си. Тази сесия ще остане валидна 24 часа.",
"opksshAuthUrl": "URL адрес за удостоверяване",
"opksshOpenBrowser": "Отворете браузъра за удостоверяване",
"opksshWaitingForAuth": "Чака се удостоверяване в браузъра...",
"opksshAuthenticating": "Обработва се удостоверяване...",
"opksshTimeout": "Времето за изчакване на удостоверяването изтече. Моля, опитайте отново.",
"opksshAuthFailed": "Удостоверяването не бе успешно. Моля, проверете идентификационните си данни и опитайте отново.",
"opksshConfigMissing": "Конфигурацията на OPKSSH не е намерена. Моля, създайте ~/.opk/config.yml с настройките на вашия OIDC доставчик. Вижте документацията: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Въвеждане на парола?", "sudoPasswordPopupTitle": "Въвеждане на парола?",
"sudoPasswordPopupHint": "Натиснете Enter за вмъкване, Esc за отхвърляне", "sudoPasswordPopupHint": "Натиснете Enter за вмъкване, Esc за отхвърляне",
"sudoPasswordPopupConfirm": "Вмъкване", "sudoPasswordPopupConfirm": "Вмъкване",
@@ -1359,8 +1337,7 @@
"automaticFallback": "Автоматичен опит за {{method}} удостоверяване...", "automaticFallback": "Автоматичен опит за {{method}} удостоверяване...",
"totpTimeout": "Времето за изчакване за проверка на TOTP изтече. Моля, свържете се отново.", "totpTimeout": "Времето за изчакване за проверка на TOTP изтече. Моля, свържете се отново.",
"passwordTimeout": "Времето за изчакване за потвърждение на паролата изтече. Моля, свържете се отново.", "passwordTimeout": "Времето за изчакване за потвърждение на паролата изтече. Моля, свържете се отново.",
"connectionRejected": "Връзката е отхвърлена от сървъра. Моля, проверете удостоверяването и мрежовата си конфигурация.", "connectionRejected": "Връзката е отхвърлена от сървъра. Моля, проверете удостоверяването и мрежовата си конфигурация."
"hostKeyRejected": "Проверката на SSH ключа за хост е отхвърлена. Връзката е прекратена."
}, },
"fileManager": { "fileManager": {
"title": "Файлов мениджър", "title": "Файлов мениджър",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "Успешно възстановяване на паролата", "passwordResetSuccess": "Успешно възстановяване на паролата",
"passwordResetSuccessDesc": "Паролата ви е нулирана успешно. Вече можете да влезете с новата си парола." "passwordResetSuccessDesc": "Паролата ви е нулирана успешно. Вече можете да влезете с новата си парола."
}, },
"hostKey": {
"verifyNewHost": "Проверка на SSH хост ключа",
"keyChangedWarning": "SSH ключът на хоста е променен",
"firstConnectionTitle": "Първо свързване с този хост",
"firstConnectionDescription": "Автентичността на този хост не може да бъде установена. Проверете дали пръстовият отпечатък съответства на очакванията ви.",
"keyChangedDescription": "Ключът на хоста за този сървър се е променил от последното ви свързване. Това може да показва проблем със сигурността.",
"previousKey": "Предишен ключ",
"newFingerprint": "Нов пръстов отпечатък",
"fingerprint": "Пръстов отпечатък",
"verifyInstructions": "Ако имате доверие на този хост, щракнете върху „Приемам“, за да продължите и да запазите този пръстов отпечатък за бъдещи връзки.",
"securityWarning": "Предупреждение за сигурност",
"acceptAndContinue": "Приемам и продължавам",
"acceptNewKey": "Приемете новия ключ и продължете"
},
"errors": { "errors": {
"notFound": "Страницата не е намерена", "notFound": "Страницата не е намерена",
"unauthorized": "Неоторизиран достъп", "unauthorized": "Неоторизиран достъп",
@@ -2063,8 +2026,6 @@
"terminalSettings": "Терминал", "terminalSettings": "Терминал",
"hostSidebarSettings": "Хост и странична лента", "hostSidebarSettings": "Хост и странична лента",
"snippetsSettings": "Откъси", "snippetsSettings": "Откъси",
"confirmSnippetExecution": "Потвърждаване на изпълнението на фрагмента",
"confirmSnippetExecutionDesc": "Показване на диалогов прозорец за потвърждение преди изпълнение на фрагменти",
"updateSettings": "Актуализации", "updateSettings": "Актуализации",
"disableUpdateCheck": "Деактивиране на проверката за актуализации", "disableUpdateCheck": "Деактивиране на проверката за актуализации",
"disableUpdateCheckDesc": "Спрете проверката за нови версии при стартиране и таблото за управление. Намалява мрежовите заявки.", "disableUpdateCheckDesc": "Спрете проверката за нови версии при стартиране и таблото за управление. Намалява мрежовите заявки.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 или example.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "পরিচয়পত্র",
"credentialsViewer": "শংসাপত্র দর্শক", "credentialsViewer": "শংসাপত্র দর্শক",
"manageYourSSHCredentials": "আপনার SSH শংসাপত্রগুলি নিরাপদে পরিচালনা করুন", "manageYourSSHCredentials": "আপনার SSH শংসাপত্রগুলি নিরাপদে পরিচালনা করুন",
"addCredential": "শংসাপত্র যোগ করুন", "addCredential": "শংসাপত্র যোগ করুন",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "এটি বিদ্যমান কীগুলিকে ওভাররাইট না করেই টার্গেট হোস্টের ~/.ssh/authorized_keys ফাইলে নিরাপদে পাবলিক কী যুক্ত করবে। এই অপারেশনটি বিপরীতমুখী।", "deploymentProcessDescription": "এটি বিদ্যমান কীগুলিকে ওভাররাইট না করেই টার্গেট হোস্টের ~/.ssh/authorized_keys ফাইলে নিরাপদে পাবলিক কী যুক্ত করবে। এই অপারেশনটি বিপরীতমুখী।",
"chooseHostToDeploy": "স্থাপনের জন্য একটি হোস্ট বেছে নিন...", "chooseHostToDeploy": "স্থাপনের জন্য একটি হোস্ট বেছে নিন...",
"deploying": "স্থাপন করা হচ্ছে...", "deploying": "স্থাপন করা হচ্ছে...",
"copyDeployCommand": "ডিপ্লয় কমান্ড কপি করুন",
"copyDeployCommandDescription": "হোস্টের authorized_keys ফাইলে পাবলিক কী ম্যানুয়ালি যোগ করার জন্য একটি শেল কমান্ড কপি করুন। পাসওয়ার্ড প্রমাণীকরণ অক্ষম থাকলে এটি কার্যকর।",
"deployCommandCopied": "ডিপ্লয় কমান্ড ক্লিপবোর্ডে কপি করা হয়েছে",
"manualDeployInfo": "SSH কী যোগ করতে টার্গেট হোস্টের টার্মিনালে (যেমন, Proxmox কনসোল, IPMI, অথবা ফিজিক্যাল অ্যাক্সেস) এই কমান্ডটি পেস্ট করুন।",
"name": "নাম", "name": "নাম",
"noHostsAvailable": "কোনও হোস্ট উপলব্ধ নেই", "noHostsAvailable": "কোনও হোস্ট উপলব্ধ নেই",
"noHostsMatchSearch": "আপনার অনুসন্ধানের সাথে কোনও হোস্ট মেলেনি।", "noHostsMatchSearch": "আপনার অনুসন্ধানের সাথে কোনও হোস্ট মেলেনি।",
@@ -303,9 +298,7 @@
"createFolder": "ফোল্ডার তৈরি করুন", "createFolder": "ফোল্ডার তৈরি করুন",
"editFolder": "ফোল্ডার সম্পাদনা করুন", "editFolder": "ফোল্ডার সম্পাদনা করুন",
"editFolderDescription": "আপনার স্নিপেট ফোল্ডারটি কাস্টমাইজ করুন", "editFolderDescription": "আপনার স্নিপেট ফোল্ডারটি কাস্টমাইজ করুন",
"createFolderDescription": "আপনার স্নিপেটগুলিকে ফোল্ডারে সাজান", "createFolderDescription": "আপনার স্নিপেটগুলিকে ফোল্ডারে সাজান"
"confirmExecution": "\"{{name}}\" কার্যকর করবেন?",
"confirmExecutionDesc": "আপনি কি নিশ্চিত যে আপনি এই স্নিপেটটি কার্যকর করতে চান?"
}, },
"commandHistory": { "commandHistory": {
"title": "ইতিহাস", "title": "ইতিহাস",
@@ -409,7 +402,6 @@
"required": "প্রয়োজনীয়", "required": "প্রয়োজনীয়",
"optional": "ঐচ্ছিক", "optional": "ঐচ্ছিক",
"connect": "সংযোগ করুন", "connect": "সংযোগ করুন",
"copied": "কপি করা হয়েছে",
"connecting": "সংযোগ করা হচ্ছে...", "connecting": "সংযোগ করা হচ্ছে...",
"creating": "তৈরি করা হচ্ছে...", "creating": "তৈরি করা হচ্ছে...",
"clear": "পরিষ্কার", "clear": "পরিষ্কার",
@@ -502,7 +494,6 @@
"checking": "পরীক্ষা করা হচ্ছে...", "checking": "পরীক্ষা করা হচ্ছে...",
"checkingDatabase": "ডাটাবেস সংযোগ পরীক্ষা করা হচ্ছে...", "checkingDatabase": "ডাটাবেস সংযোগ পরীক্ষা করা হচ্ছে...",
"checkingAuthentication": "প্রমাণীকরণ পরীক্ষা করা হচ্ছে...", "checkingAuthentication": "প্রমাণীকরণ পরীক্ষা করা হচ্ছে...",
"backendReconnected": "সার্ভার সংযোগ পুনরুদ্ধার করা হয়েছে",
"actions": "কর্ম", "actions": "কর্ম",
"remove": "অপসারণ", "remove": "অপসারণ",
"revoke": "প্রত্যাহার করুন", "revoke": "প্রত্যাহার করুন",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "হোস্ট ম্যানেজার", "title": "হোস্ট ম্যানেজার",
"hosts": "হোস্ট",
"sshHosts": "SSH হোস্ট", "sshHosts": "SSH হোস্ট",
"noHosts": "কোনও SSH হোস্ট নেই", "noHosts": "কোনও SSH হোস্ট নেই",
"noHostsMessage": "আপনি এখনও কোনও SSH হোস্ট যোগ করেননি। শুরু করতে \"হোস্ট যোগ করুন\" এ ক্লিক করুন।", "noHostsMessage": "আপনি এখনও কোনও SSH হোস্ট যোগ করেননি। শুরু করতে \"হোস্ট যোগ করুন\" এ ক্লিক করুন।",
@@ -846,7 +836,7 @@
"failedToImportJson": "JSON ফাইল আমদানি করা যায়নি", "failedToImportJson": "JSON ফাইল আমদানি করা যায়নি",
"connectionDetails": "সংযোগের বিবরণ", "connectionDetails": "সংযোগের বিবরণ",
"organization": "সংগঠন", "organization": "সংগঠন",
"ipAddress": "আইপি ঠিকানা অথবা হোস্টনেম", "ipAddress": "আইপি ঠিকানা",
"port": "বন্দর", "port": "বন্দর",
"name": "নাম", "name": "নাম",
"username": "ব্যবহারকারীর নাম", "username": "ব্যবহারকারীর নাম",
@@ -925,8 +915,6 @@
"key": "চাবি", "key": "চাবি",
"credential": "পরিচয়পত্র", "credential": "পরিচয়পত্র",
"none": "কোনটিই নয়", "none": "কোনটিই নয়",
"opkssh": "OPKSSH সম্পর্কে",
"opksshAuthDescription": "প্রতিটি সংযোগের সময় আপনাকে OPKSSH ওয়েব-অনুমোদনের জন্য অনুরোধ করবে। আপনার অথ টোকেনটি পুনর্নবীকরণ না করা পর্যন্ত 24 ঘন্টা স্থায়ী হবে।",
"selectCredential": "শংসাপত্র নির্বাচন করুন", "selectCredential": "শংসাপত্র নির্বাচন করুন",
"selectCredentialPlaceholder": "একটি শংসাপত্র বেছে নিন...", "selectCredentialPlaceholder": "একটি শংসাপত্র বেছে নিন...",
"credentialRequired": "শংসাপত্র প্রমাণীকরণ ব্যবহার করার সময় শংসাপত্র প্রয়োজন", "credentialRequired": "শংসাপত্র প্রমাণীকরণ ব্যবহার করার সময় শংসাপত্র প্রয়োজন",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণ", "noneAuthTitle": "কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণ",
"noneAuthDescription": "এই প্রমাণীকরণ পদ্ধতিটি SSH সার্ভারের সাথে সংযোগ করার সময় কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণ ব্যবহার করবে।", "noneAuthDescription": "এই প্রমাণীকরণ পদ্ধতিটি SSH সার্ভারের সাথে সংযোগ করার সময় কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণ ব্যবহার করবে।",
"noneAuthDetails": "কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণের মাধ্যমে সার্ভার সংযোগের সময় আপনাকে শংসাপত্রের জন্য অনুরোধ করতে পারে। এটি এমন সার্ভারগুলির জন্য কার্যকর যাদের মাল্টি-ফ্যাক্টর প্রমাণীকরণের প্রয়োজন হয় অথবা যদি আপনি স্থানীয়ভাবে শংসাপত্র সংরক্ষণ করতে না চান।", "noneAuthDetails": "কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণের মাধ্যমে সার্ভার সংযোগের সময় আপনাকে শংসাপত্রের জন্য অনুরোধ করতে পারে। এটি এমন সার্ভারগুলির জন্য কার্যকর যাদের মাল্টি-ফ্যাক্টর প্রমাণীকরণের প্রয়োজন হয় অথবা যদি আপনি স্থানীয়ভাবে শংসাপত্র সংরক্ষণ করতে না চান।",
"opksshAuthTitle": "OPKSSH প্রমাণীকরণ",
"forceKeyboardInteractive": "জোর করে কীবোর্ড-ইন্টারেক্টিভ", "forceKeyboardInteractive": "জোর করে কীবোর্ড-ইন্টারেক্টিভ",
"forceKeyboardInteractiveDesc": "কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণ ব্যবহার করতে বাধ্য করে। কখনও কখনও টু-ফ্যাক্টর প্রমাণীকরণ (TOTP/2FA) ব্যবহার করে এমন সার্ভারগুলির জন্য এটি প্রয়োজন হয়।", "forceKeyboardInteractiveDesc": "কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণ ব্যবহার করতে বাধ্য করে। কখনও কখনও টু-ফ্যাক্টর প্রমাণীকরণ (TOTP/2FA) ব্যবহার করে এমন সার্ভারগুলির জন্য এটি প্রয়োজন হয়।",
"overrideCredentialUsername": "শংসাপত্র ব্যবহারকারীর নাম ওভাররাইড করুন", "overrideCredentialUsername": "শংসাপত্র ব্যবহারকারীর নাম ওভাররাইড করুন",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "ব্রাউজারে খুলুন", "warpgateOpenBrowser": "ব্রাউজারে খুলুন",
"warpgateContinue": "আমি প্রমাণীকরণ সম্পন্ন করেছি।", "warpgateContinue": "আমি প্রমাণীকরণ সম্পন্ন করেছি।",
"warpgateTimeout": "ওয়ার্পগেট প্রমাণীকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।", "warpgateTimeout": "ওয়ার্পগেট প্রমাণীকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।",
"opksshAuthRequired": "OPKSSH প্রমাণীকরণ প্রয়োজন",
"opksshAuthDescription": "চালিয়ে যেতে আপনার ব্রাউজারে প্রমাণীকরণ সম্পূর্ণ করুন। এই সেশনটি 24 ঘন্টার জন্য বৈধ থাকবে।",
"opksshAuthUrl": "প্রমাণীকরণ URL",
"opksshOpenBrowser": "প্রমাণীকরণের জন্য ব্রাউজার খুলুন",
"opksshWaitingForAuth": "ব্রাউজারে প্রমাণীকরণের জন্য অপেক্ষা করা হচ্ছে...",
"opksshAuthenticating": "প্রমাণীকরণ প্রক্রিয়া করা হচ্ছে...",
"opksshTimeout": "প্রমাণীকরণের সময় শেষ। অনুগ্রহ করে আবার চেষ্টা করুন।",
"opksshAuthFailed": "প্রমাণীকরণ ব্যর্থ হয়েছে। অনুগ্রহ করে আপনার শংসাপত্রগুলি পরীক্ষা করে আবার চেষ্টা করুন।",
"opksshConfigMissing": "OPKSSH কনফিগারেশন খুঁজে পাওয়া যায়নি। আপনার OIDC প্রোভাইডার সেটিংস দিয়ে ~/.opk/config.yml তৈরি করুন। ডকুমেন্টেশন দেখুন: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "পাসওয়ার্ড ঢোকাবেন?", "sudoPasswordPopupTitle": "পাসওয়ার্ড ঢোকাবেন?",
"sudoPasswordPopupHint": "সন্নিবেশ করতে Enter টিপুন এবং খারিজ করতে Esc টিপুন", "sudoPasswordPopupHint": "সন্নিবেশ করতে Enter টিপুন এবং খারিজ করতে Esc টিপুন",
"sudoPasswordPopupConfirm": "ঢোকান", "sudoPasswordPopupConfirm": "ঢোকান",
@@ -1359,8 +1337,7 @@
"automaticFallback": "স্বয়ংক্রিয়ভাবে {{method}} প্রমাণীকরণ চেষ্টা করা হচ্ছে...", "automaticFallback": "স্বয়ংক্রিয়ভাবে {{method}} প্রমাণীকরণ চেষ্টা করা হচ্ছে...",
"totpTimeout": "TOTP যাচাইকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।", "totpTimeout": "TOTP যাচাইকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।",
"passwordTimeout": "পাসওয়ার্ড যাচাইকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।", "passwordTimeout": "পাসওয়ার্ড যাচাইকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।",
"connectionRejected": "সার্ভার সংযোগ প্রত্যাখ্যান করেছে। অনুগ্রহ করে আপনার প্রমাণীকরণ এবং নেটওয়ার্ক কনফিগারেশন পরীক্ষা করুন।", "connectionRejected": "সার্ভার সংযোগ প্রত্যাখ্যান করেছে। অনুগ্রহ করে আপনার প্রমাণীকরণ এবং নেটওয়ার্ক কনফিগারেশন পরীক্ষা করুন।"
"hostKeyRejected": "SSH হোস্ট কী যাচাইকরণ প্রত্যাখ্যাত হয়েছে। সংযোগ বাতিল করা হয়েছে।"
}, },
"fileManager": { "fileManager": {
"title": "ফাইল ম্যানেজার", "title": "ফাইল ম্যানেজার",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "পাসওয়ার্ড রিসেট সফল হয়েছে", "passwordResetSuccess": "পাসওয়ার্ড রিসেট সফল হয়েছে",
"passwordResetSuccessDesc": "আপনার পাসওয়ার্ড সফলভাবে রিসেট করা হয়েছে। আপনি এখন আপনার নতুন পাসওয়ার্ড দিয়ে লগ ইন করতে পারেন।" "passwordResetSuccessDesc": "আপনার পাসওয়ার্ড সফলভাবে রিসেট করা হয়েছে। আপনি এখন আপনার নতুন পাসওয়ার্ড দিয়ে লগ ইন করতে পারেন।"
}, },
"hostKey": {
"verifyNewHost": "SSH হোস্ট কী যাচাই করুন",
"keyChangedWarning": "SSH হোস্ট কী পরিবর্তন করা হয়েছে",
"firstConnectionTitle": "এই হোস্টের সাথে প্রথমবার সংযোগ করা হচ্ছে",
"firstConnectionDescription": "এই হোস্টের সত্যতা যাচাই করা যাচ্ছে না। আপনার প্রত্যাশার সাথে আঙুলের ছাপের মিল আছে কিনা তা যাচাই করুন।",
"keyChangedDescription": "আপনার শেষ সংযোগের পর থেকে এই সার্ভারের হোস্ট কী পরিবর্তিত হয়েছে। এটি কোনও নিরাপত্তা সমস্যা নির্দেশ করতে পারে।",
"previousKey": "পূর্ববর্তী কী",
"newFingerprint": "নতুন ফিঙ্গারপ্রিন্ট",
"fingerprint": "আঙুলের ছাপ",
"verifyInstructions": "যদি আপনি এই হোস্টটিকে বিশ্বাস করেন, তাহলে চালিয়ে যেতে \"গ্রহণ করুন\" এ ক্লিক করুন এবং ভবিষ্যতের সংযোগের জন্য এই আঙ্গুলের ছাপটি সংরক্ষণ করুন।",
"securityWarning": "নিরাপত্তা সতর্কতা",
"acceptAndContinue": "গ্রহণ করুন এবং চালিয়ে যান",
"acceptNewKey": "নতুন কী গ্রহণ করুন এবং চালিয়ে যান"
},
"errors": { "errors": {
"notFound": "পৃষ্ঠাটি খুঁজে পাওয়া যায়নি", "notFound": "পৃষ্ঠাটি খুঁজে পাওয়া যায়নি",
"unauthorized": "অননুমোদিত প্রবেশাধিকার", "unauthorized": "অননুমোদিত প্রবেশাধিকার",
@@ -2063,8 +2026,6 @@
"terminalSettings": "টার্মিনাল", "terminalSettings": "টার্মিনাল",
"hostSidebarSettings": "হোস্ট এবং সাইডবার", "hostSidebarSettings": "হোস্ট এবং সাইডবার",
"snippetsSettings": "স্নিপেটস", "snippetsSettings": "স্নিপেটস",
"confirmSnippetExecution": "স্নিপেট এক্সিকিউশন নিশ্চিত করুন",
"confirmSnippetExecutionDesc": "স্নিপেট চালানোর আগে নিশ্চিতকরণ ডায়ালগ দেখান",
"updateSettings": "আপডেট", "updateSettings": "আপডেট",
"disableUpdateCheck": "আপডেট চেক অক্ষম করুন", "disableUpdateCheck": "আপডেট চেক অক্ষম করুন",
"disableUpdateCheckDesc": "স্টার্টআপ এবং ড্যাশবোর্ডে নতুন সংস্করণ পরীক্ষা করা বন্ধ করুন। নেটওয়ার্ক অনুরোধ হ্রাস করে।", "disableUpdateCheckDesc": "স্টার্টআপ এবং ড্যাশবোর্ডে নতুন সংস্করণ পরীক্ষা করা বন্ধ করুন। নেটওয়ার্ক অনুরোধ হ্রাস করে।",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "০০০০০০", "enterCode": "০০০০০০",
"ipAddress": "১৯২.১৬৮.১.১ অথবা example.com", "ipAddress": "১২৭...১",
"port": "২২", "port": "২২",
"maxRetries": "৩", "maxRetries": "৩",
"retryInterval": "১০", "retryInterval": "১০",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "Credencials",
"credentialsViewer": "Visualitzador de credencials", "credentialsViewer": "Visualitzador de credencials",
"manageYourSSHCredentials": "Gestiona les teves credencials SSH de manera segura", "manageYourSSHCredentials": "Gestiona les teves credencials SSH de manera segura",
"addCredential": "Afegeix credencials", "addCredential": "Afegeix credencials",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "Això afegirà de manera segura la clau pública al fitxer ~/.ssh/authorized_keys de l'amfitrió de destinació sense sobreescriure les claus existents. L'operació és reversible.", "deploymentProcessDescription": "Això afegirà de manera segura la clau pública al fitxer ~/.ssh/authorized_keys de l'amfitrió de destinació sense sobreescriure les claus existents. L'operació és reversible.",
"chooseHostToDeploy": "Trieu un amfitrió on implementar...", "chooseHostToDeploy": "Trieu un amfitrió on implementar...",
"deploying": "S'està desplegant...", "deploying": "S'està desplegant...",
"copyDeployCommand": "Copia l'ordre de desplegament",
"copyDeployCommandDescription": "Copia una ordre de shell per afegir manualment la clau pública al fitxer authorized_keys d'un host. Útil quan l'autenticació amb contrasenya està desactivada.",
"deployCommandCopied": "L'ordre de desplegament s'ha copiat al porta-retalls",
"manualDeployInfo": "Enganxeu aquesta ordre al terminal de l'amfitrió de destinació (per exemple, la consola de Proxmox, IPMI o l'accés físic) per afegir la clau SSH.",
"name": "Nom", "name": "Nom",
"noHostsAvailable": "No hi ha amfitrions disponibles", "noHostsAvailable": "No hi ha amfitrions disponibles",
"noHostsMatchSearch": "No hi ha amfitrions que coincideixin amb la teva cerca", "noHostsMatchSearch": "No hi ha amfitrions que coincideixin amb la teva cerca",
@@ -303,9 +298,7 @@
"createFolder": "Crea una carpeta", "createFolder": "Crea una carpeta",
"editFolder": "Edita la carpeta", "editFolder": "Edita la carpeta",
"editFolderDescription": "Personalitza la carpeta de fragments", "editFolderDescription": "Personalitza la carpeta de fragments",
"createFolderDescription": "Organitza els teus fragments en carpetes", "createFolderDescription": "Organitza els teus fragments en carpetes"
"confirmExecution": "Executar \"{{name}}\"?",
"confirmExecutionDesc": "Esteu segur que voleu executar aquest fragment?"
}, },
"commandHistory": { "commandHistory": {
"title": "Història", "title": "Història",
@@ -409,7 +402,6 @@
"required": "Obligatori", "required": "Obligatori",
"optional": "Opcional", "optional": "Opcional",
"connect": "Connecta", "connect": "Connecta",
"copied": "Copiat",
"connecting": "Connectant...", "connecting": "Connectant...",
"creating": "Creant...", "creating": "Creant...",
"clear": "Clar", "clear": "Clar",
@@ -502,7 +494,6 @@
"checking": "S'està revisant...", "checking": "S'està revisant...",
"checkingDatabase": "S'està comprovant la connexió a la base de dades...", "checkingDatabase": "S'està comprovant la connexió a la base de dades...",
"checkingAuthentication": "S'està comprovant l'autenticació...", "checkingAuthentication": "S'està comprovant l'autenticació...",
"backendReconnected": "Connexió al servidor restaurada",
"actions": "Accions", "actions": "Accions",
"remove": "Elimina", "remove": "Elimina",
"revoke": "Revocar", "revoke": "Revocar",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "Gestor d'amfitrions", "title": "Gestor d'amfitrions",
"hosts": "Amfitrions",
"sshHosts": "Amfitrions SSH", "sshHosts": "Amfitrions SSH",
"noHosts": "Sense amfitrions SSH", "noHosts": "Sense amfitrions SSH",
"noHostsMessage": "Encara no has afegit cap amfitrió SSH. Fes clic a \"Afegeix amfitrió\" per començar.", "noHostsMessage": "Encara no has afegit cap amfitrió SSH. Fes clic a \"Afegeix amfitrió\" per començar.",
@@ -846,7 +836,7 @@
"failedToImportJson": "No s'ha pogut importar el fitxer JSON", "failedToImportJson": "No s'ha pogut importar el fitxer JSON",
"connectionDetails": "Detalls de la connexió", "connectionDetails": "Detalls de la connexió",
"organization": "Organització", "organization": "Organització",
"ipAddress": "Adreça IP o nom d'amfitrió", "ipAddress": "Adreça IP",
"port": "Port", "port": "Port",
"name": "Nom", "name": "Nom",
"username": "Nom d'usuari", "username": "Nom d'usuari",
@@ -925,8 +915,6 @@
"key": "Clau", "key": "Clau",
"credential": "Credencial", "credential": "Credencial",
"none": "Cap", "none": "Cap",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Et demana l'autenticació web OPKSSH cada vegada que et connectes. El teu testimoni d'autenticació durarà 24 hores fins que s'hagi de renovar.",
"selectCredential": "Selecciona la credencial", "selectCredential": "Selecciona la credencial",
"selectCredentialPlaceholder": "Trieu una credencial...", "selectCredentialPlaceholder": "Trieu una credencial...",
"credentialRequired": "Cal tenir credencials quan s'utilitza l'autenticació de credencials", "credentialRequired": "Cal tenir credencials quan s'utilitza l'autenticació de credencials",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "Autenticació interactiva amb teclat", "noneAuthTitle": "Autenticació interactiva amb teclat",
"noneAuthDescription": "Aquest mètode d'autenticació utilitzarà l'autenticació interactiva del teclat quan es connecti al servidor SSH.", "noneAuthDescription": "Aquest mètode d'autenticació utilitzarà l'autenticació interactiva del teclat quan es connecti al servidor SSH.",
"noneAuthDetails": "L'autenticació interactiva amb teclat permet que el servidor us demani credencials durant la connexió. Això és útil per a servidors que requereixen autenticació multifactor o si no voleu desar les credencials localment.", "noneAuthDetails": "L'autenticació interactiva amb teclat permet que el servidor us demani credencials durant la connexió. Això és útil per a servidors que requereixen autenticació multifactor o si no voleu desar les credencials localment.",
"opksshAuthTitle": "Autenticació OPKSSH",
"forceKeyboardInteractive": "Forçar la interacció del teclat", "forceKeyboardInteractive": "Forçar la interacció del teclat",
"forceKeyboardInteractiveDesc": "Força l'ús de l'autenticació interactiva amb teclat. De vegades, això és necessari per a servidors que utilitzen l'autenticació de dos factors (TOTP/2FA).", "forceKeyboardInteractiveDesc": "Força l'ús de l'autenticació interactiva amb teclat. De vegades, això és necessari per a servidors que utilitzen l'autenticació de dos factors (TOTP/2FA).",
"overrideCredentialUsername": "Substitueix les credencials i el nom d'usuari", "overrideCredentialUsername": "Substitueix les credencials i el nom d'usuari",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "Obre al navegador", "warpgateOpenBrowser": "Obre al navegador",
"warpgateContinue": "He completat l'autenticació", "warpgateContinue": "He completat l'autenticació",
"warpgateTimeout": "Temps d'espera d'autenticació de Warpgate. Si us plau, torneu a connectar-vos.", "warpgateTimeout": "Temps d'espera d'autenticació de Warpgate. Si us plau, torneu a connectar-vos.",
"opksshAuthRequired": "Cal autenticació OPKSSH",
"opksshAuthDescription": "Completeu l'autenticació al navegador per continuar. Aquesta sessió serà vàlida durant 24 hores.",
"opksshAuthUrl": "URL d'autenticació",
"opksshOpenBrowser": "Obre el navegador per autenticar-te",
"opksshWaitingForAuth": "Esperant l'autenticació al navegador...",
"opksshAuthenticating": "Processant l'autenticació...",
"opksshTimeout": "S'ha esgotat el temps d'autenticació. Torna-ho a provar.",
"opksshAuthFailed": "L'autenticació ha fallat. Si us plau, comproveu les vostres credencials i torneu-ho a intentar.",
"opksshConfigMissing": "No s'ha trobat la configuració d'OPKSSH. Si us plau, creeu ~/.opk/config.yml amb la configuració del vostre proveïdor OIDC. Vegeu la documentació: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Voleu inserir la contrasenya?", "sudoPasswordPopupTitle": "Voleu inserir la contrasenya?",
"sudoPasswordPopupHint": "Premeu Intro per inserir, Esc per tancar", "sudoPasswordPopupHint": "Premeu Intro per inserir, Esc per tancar",
"sudoPasswordPopupConfirm": "Insereix", "sudoPasswordPopupConfirm": "Insereix",
@@ -1359,8 +1337,7 @@
"automaticFallback": "Intentant automàticament l'autenticació {{method}} ...", "automaticFallback": "Intentant automàticament l'autenticació {{method}} ...",
"totpTimeout": "Temps d'espera de verificació TOTP superat. Si us plau, torneu a connectar-vos.", "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.", "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.", "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."
}, },
"fileManager": { "fileManager": {
"title": "Gestor de fitxers", "title": "Gestor de fitxers",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "Restabliment de la contrasenya correcte", "passwordResetSuccess": "Restabliment de la contrasenya correcte",
"passwordResetSuccessDesc": "La teva contrasenya s'ha restablert correctament. Ara pots iniciar la sessió amb la nova contrasenya." "passwordResetSuccessDesc": "La teva contrasenya s'ha restablert correctament. Ara pots iniciar la sessió amb la nova contrasenya."
}, },
"hostKey": {
"verifyNewHost": "Verifica la clau d'amfitrió SSH",
"keyChangedWarning": "Clau d'amfitrió SSH canviada",
"firstConnectionTitle": "Primera vegada que em connecto a aquest amfitrió",
"firstConnectionDescription": "No es pot establir l'autenticitat d'aquest amfitrió. Verifiqueu que l'empremta digital coincideixi amb el que espereu.",
"keyChangedDescription": "La clau d'amfitrió d'aquest servidor ha canviat des de la vostra última connexió. Això podria indicar un problema de seguretat.",
"previousKey": "Clau anterior",
"newFingerprint": "Nova empremta digital",
"fingerprint": "Empremta digital",
"verifyInstructions": "Si confieu en aquest amfitrió, feu clic a Accepta per continuar i desar aquesta empremta digital per a futures connexions.",
"securityWarning": "Avís de seguretat",
"acceptAndContinue": "Accepta i continua",
"acceptNewKey": "Accepta la clau nova i continua"
},
"errors": { "errors": {
"notFound": "Pàgina no trobada", "notFound": "Pàgina no trobada",
"unauthorized": "Accés no autoritzat", "unauthorized": "Accés no autoritzat",
@@ -2063,8 +2026,6 @@
"terminalSettings": "Terminal", "terminalSettings": "Terminal",
"hostSidebarSettings": "Amfitrió i barra lateral", "hostSidebarSettings": "Amfitrió i barra lateral",
"snippetsSettings": "Fragments", "snippetsSettings": "Fragments",
"confirmSnippetExecution": "Confirma l'execució del fragment",
"confirmSnippetExecutionDesc": "Mostra el diàleg de confirmació abans d'executar fragments",
"updateSettings": "Actualitzacions", "updateSettings": "Actualitzacions",
"disableUpdateCheck": "Desactiva la comprovació d'actualitzacions", "disableUpdateCheck": "Desactiva la comprovació d'actualitzacions",
"disableUpdateCheckDesc": "Deixa de comprovar si hi ha noves versions a l'inici i al tauler de control. Redueix les sol·licituds de xarxa.", "disableUpdateCheckDesc": "Deixa de comprovar si hi ha noves versions a l'inici i al tauler de control. Redueix les sol·licituds de xarxa.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 o example.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "Pověření",
"credentialsViewer": "Prohlížeč přihlašovacích údajů", "credentialsViewer": "Prohlížeč přihlašovacích údajů",
"manageYourSSHCredentials": "Bezpečná správa přihlašovacích údajů SSH", "manageYourSSHCredentials": "Bezpečná správa přihlašovacích údajů SSH",
"addCredential": "Přidat pověření", "addCredential": "Přidat pověření",
@@ -100,10 +99,6 @@
"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á.", "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í...", "chooseHostToDeploy": "Vyberte hostitele pro nasazení...",
"deploying": "Nasazování...", "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.",
"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.",
"name": "Název", "name": "Název",
"noHostsAvailable": "Nejsou k dispozici žádní hostitelé", "noHostsAvailable": "Nejsou k dispozici žádní hostitelé",
"noHostsMatchSearch": "Žádné hostitele neodpovídají vašemu hledání", "noHostsMatchSearch": "Žádné hostitele neodpovídají vašemu hledání",
@@ -303,9 +298,7 @@
"createFolder": "Vytvořit složku", "createFolder": "Vytvořit složku",
"editFolder": "Upravit složku", "editFolder": "Upravit složku",
"editFolderDescription": "Přizpůsobit složku snippetu", "editFolderDescription": "Přizpůsobit složku snippetu",
"createFolderDescription": "Uspořádat úryvky do složek", "createFolderDescription": "Uspořádat úryvky do složek"
"confirmExecution": "Spustit \"{{name}}\"?",
"confirmExecutionDesc": "Jste si jisti, že chcete spustit tento snippet?"
}, },
"commandHistory": { "commandHistory": {
"title": "Historie", "title": "Historie",
@@ -409,7 +402,6 @@
"required": "Požadováno", "required": "Požadováno",
"optional": "Nepovinné", "optional": "Nepovinné",
"connect": "Připojit", "connect": "Připojit",
"copied": "Zkopírováno",
"connecting": "Připojování...", "connecting": "Připojování...",
"creating": "Vytváření...", "creating": "Vytváření...",
"clear": "Vyčistit", "clear": "Vyčistit",
@@ -502,7 +494,6 @@
"checking": "Kontroluje...", "checking": "Kontroluje...",
"checkingDatabase": "Kontrola připojení k databázi...", "checkingDatabase": "Kontrola připojení k databázi...",
"checkingAuthentication": "Kontrola ověřování...", "checkingAuthentication": "Kontrola ověřování...",
"backendReconnected": "Připojení k serveru obnoveno",
"actions": "Akce", "actions": "Akce",
"remove": "Odebrat", "remove": "Odebrat",
"revoke": "Revoke", "revoke": "Revoke",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "Manažer serveru", "title": "Manažer serveru",
"hosts": "Hostitelé",
"sshHosts": "SSH hostitelé", "sshHosts": "SSH hostitelé",
"noHosts": "Žádné SSH hostitele", "noHosts": "Žádné SSH hostitele",
"noHostsMessage": "Zatím jste nepřidali žádné SSH hostitele. Chcete-li začít, klikněte na \"Přidat hostitele\".", "noHostsMessage": "Zatím jste nepřidali žádné SSH hostitele. Chcete-li začít, klikněte na \"Přidat hostitele\".",
@@ -846,7 +836,7 @@
"failedToImportJson": "Nepodařilo se importovat soubor JSON", "failedToImportJson": "Nepodařilo se importovat soubor JSON",
"connectionDetails": "Detaily připojení", "connectionDetails": "Detaily připojení",
"organization": "Organizace", "organization": "Organizace",
"ipAddress": "IP adresa nebo název hostitele", "ipAddress": "IP adresa",
"port": "Přístav", "port": "Přístav",
"name": "Název", "name": "Název",
"username": "Uživatelské jméno", "username": "Uživatelské jméno",
@@ -925,8 +915,6 @@
"key": "Klíč", "key": "Klíč",
"credential": "Pověření", "credential": "Pověření",
"none": "Nic", "none": "Nic",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Vyzývá vás k ověření webového serveru OPKSSH při každém připojení. Váš autentizační token bude trvat 24 hodin, dokud nebude nutné jej obnovit.",
"selectCredential": "Vybrat pověření", "selectCredential": "Vybrat pověření",
"selectCredentialPlaceholder": "Zvolte přihlašovací údaje...", "selectCredentialPlaceholder": "Zvolte přihlašovací údaje...",
"credentialRequired": "Při použití ověření pověření je vyžadováno pověření", "credentialRequired": "Při použití ověření pověření je vyžadováno pověření",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "Klávesnice interaktivní ověření", "noneAuthTitle": "Klávesnice interaktivní ověření",
"noneAuthDescription": "Tato metoda ověřování bude při připojování k serveru SSH používat klávesnice-interaktivní ověřování.", "noneAuthDescription": "Tato metoda ověřování bude při připojování k serveru SSH používat klávesnice-interaktivní ověřování.",
"noneAuthDetails": "Klávesnice - interaktivní autentizace umožňuje serveru požádat vás o přihlašovací údaje při připojení. Toto je užitečné pro servery, které vyžadují vícefázové autentifikaci, nebo pokud nechcete lokálně ukládat přihlašovací údaje.", "noneAuthDetails": "Klávesnice - interaktivní autentizace umožňuje serveru požádat vás o přihlašovací údaje při připojení. Toto je užitečné pro servery, které vyžadují vícefázové autentifikaci, nebo pokud nechcete lokálně ukládat přihlašovací údaje.",
"opksshAuthTitle": "Ověření OPKSSH",
"forceKeyboardInteractive": "Vynutit interaktivní klávesnici", "forceKeyboardInteractive": "Vynutit interaktivní klávesnici",
"forceKeyboardInteractiveDesc": "Vynucuje použití klávesnice interaktivní autentizace. To je někdy nutné u serverů, které používají dvoufaktorové ověřování (TOTP/2FA).", "forceKeyboardInteractiveDesc": "Vynucuje použití klávesnice interaktivní autentizace. To je někdy nutné u serverů, které používají dvoufaktorové ověřování (TOTP/2FA).",
"overrideCredentialUsername": "Přepsat pověření uživatelské jméno", "overrideCredentialUsername": "Přepsat pověření uživatelské jméno",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "Otevřít v prohlížeči", "warpgateOpenBrowser": "Otevřít v prohlížeči",
"warpgateContinue": "Dokončil jsem ověření", "warpgateContinue": "Dokončil jsem ověření",
"warpgateTimeout": "Časový limit autentizace Warpgate vypršel. Připojte se.", "warpgateTimeout": "Časový limit autentizace Warpgate vypršel. Připojte se.",
"opksshAuthRequired": "Vyžadováno ověření OPKSSH",
"opksshAuthDescription": "Dokončete ověření ve vašem prohlížeči, abyste mohli pokračovat. Tato relace zůstane v platnosti 24 hodin.",
"opksshAuthUrl": "Ověřovací URL",
"opksshOpenBrowser": "Otevřete prohlížeč pro ověření",
"opksshWaitingForAuth": "Čekání na ověřování v prohlížeči...",
"opksshAuthenticating": "Zpracovávání ověřování...",
"opksshTimeout": "Vypršel časový limit ověření. Zkuste to prosím znovu.",
"opksshAuthFailed": "Ověření se nezdařilo. Zkontrolujte prosím vaše přihlašovací údaje a zkuste to znovu.",
"opksshConfigMissing": "Nastavení OPKSSH nebylo nalezeno. Vytvořte prosím ~/.opk/config.yml s nastavením OIDC poskytovatele. Viz dokumentace: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Vložit heslo?", "sudoPasswordPopupTitle": "Vložit heslo?",
"sudoPasswordPopupHint": "Stiskněte Enter pro vložení, Esc pro zamítnutí", "sudoPasswordPopupHint": "Stiskněte Enter pro vložení, Esc pro zamítnutí",
"sudoPasswordPopupConfirm": "Insert", "sudoPasswordPopupConfirm": "Insert",
@@ -1359,8 +1337,7 @@
"automaticFallback": "Automaticky zkouším {{method}} autentizaci...", "automaticFallback": "Automaticky zkouším {{method}} autentizaci...",
"totpTimeout": "Časový limit ověřování TOTP. Připojte se znovu.", "totpTimeout": "Časový limit ověřování TOTP. Připojte se znovu.",
"passwordTimeout": "Časový limit ověření hesla vypršel. 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ě.", "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."
}, },
"fileManager": { "fileManager": {
"title": "Správce souborů", "title": "Správce souborů",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "Obnovení hesla úspěšné", "passwordResetSuccess": "Obnovení hesla úspěšné",
"passwordResetSuccessDesc": "Vaše heslo bylo úspěšně obnoveno. Nyní se můžete přihlásit svým novým heslem." "passwordResetSuccessDesc": "Vaše heslo bylo úspěšně obnoveno. Nyní se můžete přihlásit svým novým heslem."
}, },
"hostKey": {
"verifyNewHost": "Ověřit klíč SSH hostitele",
"keyChangedWarning": "SSH klíč hostitele změněn",
"firstConnectionTitle": "První připojení k tomuto hostiteli",
"firstConnectionDescription": "Nelze určit pravost tohoto hostitele. Ověřte shodu s tím, co očekáváte.",
"keyChangedDescription": "Hostitelský klíč pro tento server se změnil od vašeho posledního připojení. To by mohlo znamenat bezpečnostní problém.",
"previousKey": "Předchozí klíč",
"newFingerprint": "Nový otisk prstu",
"fingerprint": "Otisk prstu",
"verifyInstructions": "Pokud tomuto hostiteli důvěřujete, klepněte na tlačítko Přijmout pro pokračování a uložení tohoto otisku pro budoucí připojení.",
"securityWarning": "Bezpečnostní varování",
"acceptAndContinue": "Přijmout a pokračovat",
"acceptNewKey": "Přijmout nový klíč a pokračovat"
},
"errors": { "errors": {
"notFound": "Stránka nenalezena", "notFound": "Stránka nenalezena",
"unauthorized": "Neoprávněný přístup", "unauthorized": "Neoprávněný přístup",
@@ -2063,8 +2026,6 @@
"terminalSettings": "Terminál", "terminalSettings": "Terminál",
"hostSidebarSettings": "Hostitel & postranní panel", "hostSidebarSettings": "Hostitel & postranní panel",
"snippetsSettings": "Výstřižky bloků", "snippetsSettings": "Výstřižky bloků",
"confirmSnippetExecution": "Potvrdit spuštění snippetu",
"confirmSnippetExecutionDesc": "Zobrazit potvrzovací dialog před spuštěním snippetů",
"updateSettings": "Aktualizace", "updateSettings": "Aktualizace",
"disableUpdateCheck": "Zakázat kontrolu aktualizací", "disableUpdateCheck": "Zakázat kontrolu aktualizací",
"disableUpdateCheckDesc": "Zastavit kontrolu nových verzí na startu a řídicím panelu. Sníží síťové požadavky.", "disableUpdateCheckDesc": "Zastavit kontrolu nových verzí na startu a řídicím panelu. Sníží síťové požadavky.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 nebo příklad.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "Legitimation",
"credentialsViewer": "Legitimationsfremviser", "credentialsViewer": "Legitimationsfremviser",
"manageYourSSHCredentials": "Administrer dine SSH-akkreditiver sikkert", "manageYourSSHCredentials": "Administrer dine SSH-akkreditiver sikkert",
"addCredential": "Tilføj Credential", "addCredential": "Tilføj Credential",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "Dette vil sikkert tilføje den offentlige nøgle til målet værtens ~/.ssh / authorized_keys fil uden at overskrive eksisterende nøgler. Handlingen er reversibel.", "deploymentProcessDescription": "Dette vil sikkert tilføje den offentlige nøgle til målet værtens ~/.ssh / authorized_keys fil uden at overskrive eksisterende nøgler. Handlingen er reversibel.",
"chooseHostToDeploy": "Vælg en vært at implementere til...", "chooseHostToDeploy": "Vælg en vært at implementere til...",
"deploying": "Implementerer...", "deploying": "Implementerer...",
"copyDeployCommand": "Kopier Implementeringskommando",
"copyDeployCommandDescription": "Kopiér en skal- kommando for manuelt at tilføje den offentlige nøgle til en værts autoriseret _ keys- fil. Nyttigt når adgangskodegodkendelse er deaktiveret.",
"deployCommandCopied": "Deploy kommando kopieret til udklipsholder",
"manualDeployInfo": "Indsæt denne kommando i målet værtens terminal (fx, Proxmox konsol, IPMI, eller fysisk adgang) for at tilføje SSH-nøglen.",
"name": "Navn", "name": "Navn",
"noHostsAvailable": "Ingen værter tilgængelige", "noHostsAvailable": "Ingen værter tilgængelige",
"noHostsMatchSearch": "Ingen værter matcher din søgning", "noHostsMatchSearch": "Ingen værter matcher din søgning",
@@ -303,9 +298,7 @@
"createFolder": "Opret Mappe", "createFolder": "Opret Mappe",
"editFolder": "Rediger Mappe", "editFolder": "Rediger Mappe",
"editFolderDescription": "Tilpas din snippet mappe", "editFolderDescription": "Tilpas din snippet mappe",
"createFolderDescription": "Organiser dine snippets i mapper", "createFolderDescription": "Organiser dine snippets i mapper"
"confirmExecution": "Kør \"{{name}}\"?",
"confirmExecutionDesc": "Er du sikker på, at du vil udføre denne snippet?"
}, },
"commandHistory": { "commandHistory": {
"title": "Historik", "title": "Historik",
@@ -409,7 +402,6 @@
"required": "Påkrævet", "required": "Påkrævet",
"optional": "Valgfri", "optional": "Valgfri",
"connect": "Forbind", "connect": "Forbind",
"copied": "Kopieret",
"connecting": "Forbinder...", "connecting": "Forbinder...",
"creating": "Opretter...", "creating": "Opretter...",
"clear": "Ryd", "clear": "Ryd",
@@ -502,7 +494,6 @@
"checking": "Kontrollerer...", "checking": "Kontrollerer...",
"checkingDatabase": "Kontrollerer databaseforbindelse...", "checkingDatabase": "Kontrollerer databaseforbindelse...",
"checkingAuthentication": "Kontrollerer godkendelse...", "checkingAuthentication": "Kontrollerer godkendelse...",
"backendReconnected": "Server forbindelse gendannet",
"actions": "Handlinger", "actions": "Handlinger",
"remove": "Fjern", "remove": "Fjern",
"revoke": "Revoke", "revoke": "Revoke",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "Vært Manager", "title": "Vært Manager",
"hosts": "Værter",
"sshHosts": "SSH Værter", "sshHosts": "SSH Værter",
"noHosts": "Ingen SSH værter", "noHosts": "Ingen SSH værter",
"noHostsMessage": "Du har endnu ikke tilføjet nogen SSH-værter. Klik på \"Tilføj vært\" for at komme i gang.", "noHostsMessage": "Du har endnu ikke tilføjet nogen SSH-værter. Klik på \"Tilføj vært\" for at komme i gang.",
@@ -846,7 +836,7 @@
"failedToImportJson": "Kunne ikke importere JSON- fil", "failedToImportJson": "Kunne ikke importere JSON- fil",
"connectionDetails": "Forbindelse Detaljer", "connectionDetails": "Forbindelse Detaljer",
"organization": "Organisation", "organization": "Organisation",
"ipAddress": "Ip adresse eller værtsnavn", "ipAddress": "Ip Adresse",
"port": "Port", "port": "Port",
"name": "Navn", "name": "Navn",
"username": "Brugernavn", "username": "Brugernavn",
@@ -925,8 +915,6 @@
"key": "Nøgle", "key": "Nøgle",
"credential": "Credential", "credential": "Credential",
"none": "Ingen", "none": "Ingen",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Spørg dig om OPKSSH web-auth ved hver forbindelse. Din auth token vil vare i 24 timer, indtil den skal fornyes.",
"selectCredential": "Vælg Credential", "selectCredential": "Vælg Credential",
"selectCredentialPlaceholder": "Vælg en legitimationsoplysning...", "selectCredentialPlaceholder": "Vælg en legitimationsoplysning...",
"credentialRequired": "Oplysninger er påkrævet, når du bruger legitimationsoplysninger godkendelse", "credentialRequired": "Oplysninger er påkrævet, når du bruger legitimationsoplysninger godkendelse",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "Tastatur-Interaktiv Autentificering", "noneAuthTitle": "Tastatur-Interaktiv Autentificering",
"noneAuthDescription": "Denne godkendelsesmetode vil bruge tastatur-interaktiv godkendelse, når der oprettes forbindelse til SSH-serveren.", "noneAuthDescription": "Denne godkendelsesmetode vil bruge tastatur-interaktiv godkendelse, når der oprettes forbindelse til SSH-serveren.",
"noneAuthDetails": "Tastatur-interaktiv godkendelse tillader serveren at bede dig om legitimationsoplysninger under forbindelsen. Dette er nyttigt for servere, der kræver multi-faktor godkendelse, eller hvis du ikke ønsker at gemme legitimationsoplysninger lokalt.", "noneAuthDetails": "Tastatur-interaktiv godkendelse tillader serveren at bede dig om legitimationsoplysninger under forbindelsen. Dette er nyttigt for servere, der kræver multi-faktor godkendelse, eller hvis du ikke ønsker at gemme legitimationsoplysninger lokalt.",
"opksshAuthTitle": "OPKSSH- Godkendelse",
"forceKeyboardInteractive": "Tving Tastatur-Interaktiv", "forceKeyboardInteractive": "Tving Tastatur-Interaktiv",
"forceKeyboardInteractiveDesc": "Kræver brugen af tastatur- interaktiv godkendelse. Dette kræves sommetider for servere, der bruger to- faktor godkendelse (TOTP/2FA).", "forceKeyboardInteractiveDesc": "Kræver brugen af tastatur- interaktiv godkendelse. Dette kræves sommetider for servere, der bruger to- faktor godkendelse (TOTP/2FA).",
"overrideCredentialUsername": "Tilsidesæt Credential Brugernavn", "overrideCredentialUsername": "Tilsidesæt Credential Brugernavn",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "Åbn i browser", "warpgateOpenBrowser": "Åbn i browser",
"warpgateContinue": "Jeg Er Afsluttet Godkendelse", "warpgateContinue": "Jeg Er Afsluttet Godkendelse",
"warpgateTimeout": "Warpgate authentication timeout. Genforbind.", "warpgateTimeout": "Warpgate authentication timeout. Genforbind.",
"opksshAuthRequired": "OPKSSH Godkendelse Kræves",
"opksshAuthDescription": "Komplet godkendelse i din browser for at fortsætte. Denne session vil forblive gyldig i 24 timer.",
"opksshAuthUrl": "Godkendelses-URL",
"opksshOpenBrowser": "Åbn browser for at godkende",
"opksshWaitingForAuth": "Venter på godkendelse i browser...",
"opksshAuthenticating": "Behandler godkendelse...",
"opksshTimeout": "Godkendelse fik timeout. Prøv venligst igen.",
"opksshAuthFailed": "Godkendelse mislykkedes. Kontroller dine legitimationsoplysninger og prøv igen.",
"opksshConfigMissing": "OPKSSH konfiguration ikke fundet. Opret venligst ~/.opk/config.yml med dine OIDC udbyder indstillinger. Se dokumentation: https://github.com/openpubkey/opkssh#konfiguration",
"sudoPasswordPopupTitle": "Indsæt Adgangskode?", "sudoPasswordPopupTitle": "Indsæt Adgangskode?",
"sudoPasswordPopupHint": "Tryk på Enter for at indsætte, Esc for at afvise", "sudoPasswordPopupHint": "Tryk på Enter for at indsætte, Esc for at afvise",
"sudoPasswordPopupConfirm": "Insert", "sudoPasswordPopupConfirm": "Insert",
@@ -1359,8 +1337,7 @@
"automaticFallback": "Prøver automatisk {{method}} autentificering...", "automaticFallback": "Prøver automatisk {{method}} autentificering...",
"totpTimeout": "TOTP bekræftelses-timeout. Genforbind.", "totpTimeout": "TOTP bekræftelses-timeout. Genforbind.",
"passwordTimeout": "Adgangskodens bekræftelses-timeout. Opret venligst ny forbindelse.", "passwordTimeout": "Adgangskodens bekræftelses-timeout. Opret venligst ny forbindelse.",
"connectionRejected": "Forbindelse afvist af serveren. Tjek venligst din godkendelse og netværkskonfiguration.", "connectionRejected": "Forbindelse afvist af serveren. Tjek venligst din godkendelse og netværkskonfiguration."
"hostKeyRejected": "SSH-værtnøglebekræftelse afvist. Forbindelse annulleret."
}, },
"fileManager": { "fileManager": {
"title": "Filhåndtering", "title": "Filhåndtering",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "Adgangskode Nulstilling Lykkedes", "passwordResetSuccess": "Adgangskode Nulstilling Lykkedes",
"passwordResetSuccessDesc": "Din adgangskode er blevet nulstillet. Du kan nu logge ind med din nye adgangskode." "passwordResetSuccessDesc": "Din adgangskode er blevet nulstillet. Du kan nu logge ind med din nye adgangskode."
}, },
"hostKey": {
"verifyNewHost": "Verificér SSH-værtnøgle",
"keyChangedWarning": "SSH Værtsnøgle Ændret",
"firstConnectionTitle": "Første gang der oprettes forbindelse til værten",
"firstConnectionDescription": "Denne værts ægthed kan ikke fastslås. Kontrollér, at fingeraftrykket matcher det, du forventer.",
"keyChangedDescription": "Værtsnøglen for denne server er ændret siden din sidste forbindelse. Dette kan indikere et sikkerhedsproblem.",
"previousKey": "Forrige Nøgle",
"newFingerprint": "Nyt Fingeraftryk",
"fingerprint": "Fingeraftryk",
"verifyInstructions": "Hvis du stoler på denne vært, skal du klikke på Accepter for at fortsætte og gemme dette fingeraftryk til fremtidige forbindelser.",
"securityWarning": "Advarsel Om Sikkerhed",
"acceptAndContinue": "Accepter & Fortsæt",
"acceptNewKey": "Accepter Ny Nøgle Og Fortsæt"
},
"errors": { "errors": {
"notFound": "Siden blev ikke fundet", "notFound": "Siden blev ikke fundet",
"unauthorized": "Uautoriseret adgang", "unauthorized": "Uautoriseret adgang",
@@ -2063,8 +2026,6 @@
"terminalSettings": "Terminal", "terminalSettings": "Terminal",
"hostSidebarSettings": "Vært & Sidebjælke", "hostSidebarSettings": "Vært & Sidebjælke",
"snippetsSettings": "Stumper", "snippetsSettings": "Stumper",
"confirmSnippetExecution": "Bekræft Udførelse Af Snippet",
"confirmSnippetExecutionDesc": "Vis bekræftelsesdialog før udførelse af snippets",
"updateSettings": "Opdateringer", "updateSettings": "Opdateringer",
"disableUpdateCheck": "Deaktivér Opdateringstjek", "disableUpdateCheck": "Deaktivér Opdateringstjek",
"disableUpdateCheckDesc": "Stop med at kontrollere for nye versioner ved opstart og dashboard. Reducerer netværksanmodninger.", "disableUpdateCheckDesc": "Stop med at kontrollere for nye versioner ved opstart og dashboard. Reducerer netværksanmodninger.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 eller eksempel.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "Anmeldedaten",
"credentialsViewer": "Zugangsdaten-Betrachter", "credentialsViewer": "Zugangsdaten-Betrachter",
"manageYourSSHCredentials": "SSH Zugangsdaten sicher verwalten", "manageYourSSHCredentials": "SSH Zugangsdaten sicher verwalten",
"addCredential": "Zugangsdaten hinzufügen", "addCredential": "Zugangsdaten hinzufügen",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "Dies fügt den öffentlichen Schlüssel sicher zur Datei ~/.ssh/authorized_keys des Zielservers hinzu, ohne vorhandene Schlüssel zu überschreiben. Die Operation ist reversibel.", "deploymentProcessDescription": "Dies fügt den öffentlichen Schlüssel sicher zur Datei ~/.ssh/authorized_keys des Zielservers hinzu, ohne vorhandene Schlüssel zu überschreiben. Die Operation ist reversibel.",
"chooseHostToDeploy": "Wählen Sie einen Host für den sie bereitstellen soll...", "chooseHostToDeploy": "Wählen Sie einen Host für den sie bereitstellen soll...",
"deploying": "Verteilen...", "deploying": "Verteilen...",
"copyDeployCommand": "Deploy-Befehl kopieren",
"copyDeployCommandDescription": "Kopieren Sie einen Shell-Befehl, um den öffentlichen Schlüssel manuell zu der Datei authorized_keys eines Hosts hinzuzufügen. Nützlich, wenn die Passwort-Authentifizierung deaktiviert ist.",
"deployCommandCopied": "Deploy-Befehl in Zwischenablage kopiert",
"manualDeployInfo": "Fügen Sie diesen Befehl in das Terminal des Ziel-Hosts ein (z.B. Proxmox-Konsole, IPMI oder physikalischer Zugang), um den SSH-Schlüssel hinzuzufügen.",
"name": "Name", "name": "Name",
"noHostsAvailable": "Keine Hosts verfügbar", "noHostsAvailable": "Keine Hosts verfügbar",
"noHostsMatchSearch": "Keine Hosts entsprechen Ihrer Suche", "noHostsMatchSearch": "Keine Hosts entsprechen Ihrer Suche",
@@ -303,9 +298,7 @@
"createFolder": "Ordner erstellen", "createFolder": "Ordner erstellen",
"editFolder": "Ordner bearbeiten", "editFolder": "Ordner bearbeiten",
"editFolderDescription": "Passen Sie Ihren Snippet Ordner an", "editFolderDescription": "Passen Sie Ihren Snippet Ordner an",
"createFolderDescription": "Organisieren Sie Ihre Snippets in Ordner", "createFolderDescription": "Organisieren Sie Ihre Snippets in Ordner"
"confirmExecution": "\"{{name}} \"ausführen?",
"confirmExecutionDesc": "Sind Sie sicher, dass Sie dieses Snippet ausführen möchten?"
}, },
"commandHistory": { "commandHistory": {
"title": "Verlauf", "title": "Verlauf",
@@ -409,7 +402,6 @@
"required": "Benötigt", "required": "Benötigt",
"optional": "Optional", "optional": "Optional",
"connect": "Verbinden", "connect": "Verbinden",
"copied": "Kopiert",
"connecting": "Verbinden...", "connecting": "Verbinden...",
"creating": "Erstellen...", "creating": "Erstellen...",
"clear": "Leeren", "clear": "Leeren",
@@ -502,7 +494,6 @@
"checking": "Überprüfen...", "checking": "Überprüfen...",
"checkingDatabase": "Überprüfe Datenbankverbindung...", "checkingDatabase": "Überprüfe Datenbankverbindung...",
"checkingAuthentication": "Authentifizierung wird überprüft...", "checkingAuthentication": "Authentifizierung wird überprüft...",
"backendReconnected": "Serververbindung wiederhergestellt",
"actions": "Aktionen", "actions": "Aktionen",
"remove": "Entfernen", "remove": "Entfernen",
"revoke": "Revoke", "revoke": "Revoke",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "Host-Manager", "title": "Host-Manager",
"hosts": "Hosts",
"sshHosts": "SSH-Hosts", "sshHosts": "SSH-Hosts",
"noHosts": "Keine SSH-Hosts", "noHosts": "Keine SSH-Hosts",
"noHostsMessage": "Sie haben noch keine SSH-Hosts hinzugefügt. Klicken Sie auf \"Host hinzufügen\", um loszulegen.", "noHostsMessage": "Sie haben noch keine SSH-Hosts hinzugefügt. Klicken Sie auf \"Host hinzufügen\", um loszulegen.",
@@ -846,7 +836,7 @@
"failedToImportJson": "Import der JSON-Datei fehlgeschlagen", "failedToImportJson": "Import der JSON-Datei fehlgeschlagen",
"connectionDetails": "Verbindungsdetails", "connectionDetails": "Verbindungsdetails",
"organization": "Organisation", "organization": "Organisation",
"ipAddress": "IP-Adresse oder Hostname", "ipAddress": "IP-Adresse",
"port": "Port", "port": "Port",
"name": "Name", "name": "Name",
"username": "Benutzername", "username": "Benutzername",
@@ -925,8 +915,6 @@
"key": "Schlüssel", "key": "Schlüssel",
"credential": "Anmeldedaten", "credential": "Anmeldedaten",
"none": "Keine", "none": "Keine",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Fordern Sie bei jeder Verbindung nach OPKSSH web-auth an. Ihr Auth-Token hält 24 Stunden an, bis es erneuert werden muss.",
"selectCredential": "Anmeldeinformationen auswählen", "selectCredential": "Anmeldeinformationen auswählen",
"selectCredentialPlaceholder": "Anmeldedaten auswählen...", "selectCredentialPlaceholder": "Anmeldedaten auswählen...",
"credentialRequired": "Anmeldeinformationen werden benötigt, wenn die Anmeldeinformationen verwendet werden", "credentialRequired": "Anmeldeinformationen werden benötigt, wenn die Anmeldeinformationen verwendet werden",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "Tastatur-Interaktive Authentifizierung", "noneAuthTitle": "Tastatur-Interaktive Authentifizierung",
"noneAuthDescription": "Diese Authentifizierungsmethode verwendet die Tastatur-interaktive Authentifizierung bei der Verbindung zum SSH-Server.", "noneAuthDescription": "Diese Authentifizierungsmethode verwendet die Tastatur-interaktive Authentifizierung bei der Verbindung zum SSH-Server.",
"noneAuthDetails": "Die interaktive Keyboard-Authentifizierung erlaubt es dem Server, Sie während der Verbindung nach Anmeldeinformationen zu fragen. Dies ist nützlich für Server, die Multi-Faktor-Authentifizierung benötigen oder wenn Sie nicht lokal Anmeldedaten speichern möchten.", "noneAuthDetails": "Die interaktive Keyboard-Authentifizierung erlaubt es dem Server, Sie während der Verbindung nach Anmeldeinformationen zu fragen. Dies ist nützlich für Server, die Multi-Faktor-Authentifizierung benötigen oder wenn Sie nicht lokal Anmeldedaten speichern möchten.",
"opksshAuthTitle": "OPKSSH Authentifizierung",
"forceKeyboardInteractive": "Tastatur-Interaktiv erzwingen", "forceKeyboardInteractive": "Tastatur-Interaktiv erzwingen",
"forceKeyboardInteractiveDesc": "Erzwingt die Verwendung von Tastatur-interaktiver Authentifizierung. Dies wird manchmal für Server benötigt, die eine Zwei-Faktor-Authentifizierung verwenden (TOTP/2FA).", "forceKeyboardInteractiveDesc": "Erzwingt die Verwendung von Tastatur-interaktiver Authentifizierung. Dies wird manchmal für Server benötigt, die eine Zwei-Faktor-Authentifizierung verwenden (TOTP/2FA).",
"overrideCredentialUsername": "Benutzername überschreiben", "overrideCredentialUsername": "Benutzername überschreiben",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "Im Browser öffnen", "warpgateOpenBrowser": "Im Browser öffnen",
"warpgateContinue": "Ich habe die Authentifizierung abgeschlossen", "warpgateContinue": "Ich habe die Authentifizierung abgeschlossen",
"warpgateTimeout": "Warpgate Authentifizierungs-Timeout. Bitte verbinden Sie sich erneut.", "warpgateTimeout": "Warpgate Authentifizierungs-Timeout. Bitte verbinden Sie sich erneut.",
"opksshAuthRequired": "OPKSSH Authentifizierung erforderlich",
"opksshAuthDescription": "Vollständige Authentifizierung in Ihrem Browser, um fortzufahren. Diese Sitzung bleibt 24 Stunden gültig.",
"opksshAuthUrl": "Authentifizierungs-URL",
"opksshOpenBrowser": "Browser zum Authentifizieren öffnen",
"opksshWaitingForAuth": "Warte auf Authentifizierung im Browser...",
"opksshAuthenticating": "Authentifizierung wird verarbeitet...",
"opksshTimeout": "Timeout der Authentifizierung. Bitte versuchen Sie es erneut.",
"opksshAuthFailed": "Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten und versuchen Sie es erneut.",
"opksshConfigMissing": "OPKSSH Konfiguration nicht gefunden. Bitte erstellen Sie ~/.opk/config.yml mit Ihren OIDC Providereinstellungen. Siehe Dokumentation: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Passwort einfügen?", "sudoPasswordPopupTitle": "Passwort einfügen?",
"sudoPasswordPopupHint": "Drücken Sie die Eingabetaste, Esc zum Verwerfen", "sudoPasswordPopupHint": "Drücken Sie die Eingabetaste, Esc zum Verwerfen",
"sudoPasswordPopupConfirm": "Insert", "sudoPasswordPopupConfirm": "Insert",
@@ -1359,8 +1337,7 @@
"automaticFallback": "Automatisch {{method}} Authentifizierung...", "automaticFallback": "Automatisch {{method}} Authentifizierung...",
"totpTimeout": "TOTP-Verifizierungs-Timeout. Bitte erneut verbinden.", "totpTimeout": "TOTP-Verifizierungs-Timeout. Bitte erneut verbinden.",
"passwordTimeout": "Timeout zur Passwortüberprüfung. 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.", "connectionRejected": "Verbindung vom Server abgelehnt. Bitte überprüfen Sie Ihre Authentifizierung und Netzwerkkonfiguration."
"hostKeyRejected": "Überprüfung des SSH-Host-Schlüssels abgelehnt. Verbindung abgebrochen."
}, },
"fileManager": { "fileManager": {
"title": "Datei-Manager", "title": "Datei-Manager",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "Passwort erfolgreich zurückgesetzt", "passwordResetSuccess": "Passwort erfolgreich zurückgesetzt",
"passwordResetSuccessDesc": "Ihr Passwort wurde erfolgreich zurückgesetzt. Sie können sich jetzt mit Ihrem neuen Passwort anmelden." "passwordResetSuccessDesc": "Ihr Passwort wurde erfolgreich zurückgesetzt. Sie können sich jetzt mit Ihrem neuen Passwort anmelden."
}, },
"hostKey": {
"verifyNewHost": "SSH-Hostschlüssel überprüfen",
"keyChangedWarning": "SSH-Hostschlüssel geändert",
"firstConnectionTitle": "Verbindung zu diesem Host zum ersten Mal",
"firstConnectionDescription": "Die Authentizität dieses Hosts kann nicht festgestellt werden. Überprüfen Sie den Fingerabdruck mit dem, was Sie erwarten.",
"keyChangedDescription": "Der Host-Schlüssel für diesen Server hat sich seit Ihrer letzten Verbindung geändert. Dies könnte auf ein Sicherheitsproblem hinweisen.",
"previousKey": "Vorheriger Schlüssel",
"newFingerprint": "Neuer Fingerabdruck",
"fingerprint": "Fingerabdruck",
"verifyInstructions": "Wenn Sie diesem Host vertrauen, klicken Sie auf Akzeptieren, um fortzufahren und den Fingerabdruck für zukünftige Verbindungen zu speichern.",
"securityWarning": "Sicherheitswarnung",
"acceptAndContinue": "Akzeptieren & fortfahren",
"acceptNewKey": "Neuen Schlüssel akzeptieren & fortfahren"
},
"errors": { "errors": {
"notFound": "Seite nicht gefunden", "notFound": "Seite nicht gefunden",
"unauthorized": "Unberechtigter Zugriff", "unauthorized": "Unberechtigter Zugriff",
@@ -2063,8 +2026,6 @@
"terminalSettings": "Terminal", "terminalSettings": "Terminal",
"hostSidebarSettings": "Host & Seitenleiste", "hostSidebarSettings": "Host & Seitenleiste",
"snippetsSettings": "Schnipsel", "snippetsSettings": "Schnipsel",
"confirmSnippetExecution": "Snippet Ausführung bestätigen",
"confirmSnippetExecutionDesc": "Bestätigungsdialog vor dem Ausführen von Snippets anzeigen",
"updateSettings": "Updates", "updateSettings": "Updates",
"disableUpdateCheck": "Updateprüfung deaktivieren", "disableUpdateCheck": "Updateprüfung deaktivieren",
"disableUpdateCheckDesc": "Stoppt die Suche nach neuen Versionen beim Start und Dashboard. Reduziert Netzwerkanfragen.", "disableUpdateCheckDesc": "Stoppt die Suche nach neuen Versionen beim Start und Dashboard. Reduziert Netzwerkanfragen.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 oder beispiel.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "Διαπιστευτήρια",
"credentialsViewer": "Προβολέας Πιστοποιητικών", "credentialsViewer": "Προβολέας Πιστοποιητικών",
"manageYourSSHCredentials": "Διαχειριστείτε τα SSH διαπιστευτήρια σας με ασφάλεια", "manageYourSSHCredentials": "Διαχειριστείτε τα SSH διαπιστευτήρια σας με ασφάλεια",
"addCredential": "Προσθήκη Διαπιστευτηρίου", "addCredential": "Προσθήκη Διαπιστευτηρίου",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "Αυτό θα προσθέσει με ασφάλεια το δημόσιο κλειδί στο αρχείο ~/.ssh/authorized_keys του κεντρικού υπολογιστή του στόχου χωρίς να αντικαταστήσει τα υπάρχοντα κλειδιά. Η λειτουργία είναι αναστρέψιμη.", "deploymentProcessDescription": "Αυτό θα προσθέσει με ασφάλεια το δημόσιο κλειδί στο αρχείο ~/.ssh/authorized_keys του κεντρικού υπολογιστή του στόχου χωρίς να αντικαταστήσει τα υπάρχοντα κλειδιά. Η λειτουργία είναι αναστρέψιμη.",
"chooseHostToDeploy": "Επιλέξτε έναν κεντρικό υπολογιστή για ανάπτυξη...", "chooseHostToDeploy": "Επιλέξτε έναν κεντρικό υπολογιστή για ανάπτυξη...",
"deploying": "Εφαρμογή...", "deploying": "Εφαρμογή...",
"copyDeployCommand": "Αντιγραφή Εφαρμογής Εντολής",
"copyDeployCommandDescription": "Αντιγράψτε μια εντολή κελύφους για να προσθέσετε χειροκίνητα το δημόσιο κλειδί στο αρχείο authorized_keys ενός κεντρικού υπολογιστή. Χρήσιμο όταν η ταυτοποίηση κωδικού πρόσβασης είναι απενεργοποιημένη.",
"deployCommandCopied": "Η εντολή ανάπτυξης αντιγράφηκε στο πρόχειρο",
"manualDeployInfo": "Επικολλήστε αυτήν την εντολή στο τερματικό του κεντρικού υπολογιστή προορισμού (π.χ., κονσόλα Proxmox, IPMI ή φυσική πρόσβαση) για να προσθέσετε το SSH κλειδί.",
"name": "Όνομα", "name": "Όνομα",
"noHostsAvailable": "Δεν υπάρχουν διαθέσιμοι υπολογιστές", "noHostsAvailable": "Δεν υπάρχουν διαθέσιμοι υπολογιστές",
"noHostsMatchSearch": "Κανένας υπολογιστής δεν ταιριάζει με την αναζήτησή σας", "noHostsMatchSearch": "Κανένας υπολογιστής δεν ταιριάζει με την αναζήτησή σας",
@@ -303,9 +298,7 @@
"createFolder": "Δημιουργία Φακέλου", "createFolder": "Δημιουργία Φακέλου",
"editFolder": "Επεξεργασία Φακέλου", "editFolder": "Επεξεργασία Φακέλου",
"editFolderDescription": "Προσαρμόστε το φάκελο αποκοπής", "editFolderDescription": "Προσαρμόστε το φάκελο αποκοπής",
"createFolderDescription": "Οργανώστε τα αποσπάσματα σας σε φακέλους", "createFolderDescription": "Οργανώστε τα αποσπάσματα σας σε φακέλους"
"confirmExecution": "Εκτέλεση \"{{name}}\"?",
"confirmExecutionDesc": "Είστε βέβαιοι ότι θέλετε να εκτελέσετε αυτό το απόφραξη?"
}, },
"commandHistory": { "commandHistory": {
"title": "Ιστορικό", "title": "Ιστορικό",
@@ -409,7 +402,6 @@
"required": "Απαιτείται", "required": "Απαιτείται",
"optional": "Προαιρετικό", "optional": "Προαιρετικό",
"connect": "Σύνδεση", "connect": "Σύνδεση",
"copied": "Αντιγράφηκε",
"connecting": "Σύνδεση...", "connecting": "Σύνδεση...",
"creating": "Δημιουργία...", "creating": "Δημιουργία...",
"clear": "Εκκαθάριση", "clear": "Εκκαθάριση",
@@ -502,7 +494,6 @@
"checking": "Έλεγχος...", "checking": "Έλεγχος...",
"checkingDatabase": "Έλεγχος σύνδεσης βάσης δεδομένων...", "checkingDatabase": "Έλεγχος σύνδεσης βάσης δεδομένων...",
"checkingAuthentication": "Έλεγχος ταυτοποίησης...", "checkingAuthentication": "Έλεγχος ταυτοποίησης...",
"backendReconnected": "Έγινε επαναφορά σύνδεσης διακομιστή",
"actions": "Ενέργειες", "actions": "Ενέργειες",
"remove": "Αφαίρεση", "remove": "Αφαίρεση",
"revoke": "Revoke", "revoke": "Revoke",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "Διαχειριστής Υπολογιστών", "title": "Διαχειριστής Υπολογιστών",
"hosts": "Υπολογιστές",
"sshHosts": "SSH Hosts", "sshHosts": "SSH Hosts",
"noHosts": "Δεν Υπάρχουν Υπολογιστές SSH", "noHosts": "Δεν Υπάρχουν Υπολογιστές SSH",
"noHostsMessage": "Δεν έχετε προσθέσει ακόμα κανένα SSH hosts. Κάντε κλικ στο κουμπί \"Προσθήκη Host\" για να ξεκινήσετε.", "noHostsMessage": "Δεν έχετε προσθέσει ακόμα κανένα SSH hosts. Κάντε κλικ στο κουμπί \"Προσθήκη Host\" για να ξεκινήσετε.",
@@ -846,7 +836,7 @@
"failedToImportJson": "Αποτυχία εισαγωγής αρχείου JSON", "failedToImportJson": "Αποτυχία εισαγωγής αρχείου JSON",
"connectionDetails": "Λεπτομέρειες Σύνδεσης", "connectionDetails": "Λεπτομέρειες Σύνδεσης",
"organization": "Οργανισμός", "organization": "Οργανισμός",
"ipAddress": "Διεύθυνση IP ή όνομα κεντρικού υπολογιστή", "ipAddress": "Διεύθυνση IP",
"port": "Θύρα", "port": "Θύρα",
"name": "Όνομα", "name": "Όνομα",
"username": "Όνομα Χρήστη", "username": "Όνομα Χρήστη",
@@ -925,8 +915,6 @@
"key": "Κλειδί", "key": "Κλειδί",
"credential": "Διαπιστευτήριο", "credential": "Διαπιστευτήριο",
"none": "Κανένα", "none": "Κανένα",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Σας παρακινεί για την ταυτοποίηση ιστού OPKSSH σε κάθε σύνδεση. Το διακριτικό ταυτοποίησης θα διαρκέσει 24 ώρες μέχρι να ανανεωθεί.",
"selectCredential": "Επιλέξτε Διαπιστευτήρια", "selectCredential": "Επιλέξτε Διαπιστευτήρια",
"selectCredentialPlaceholder": "Επιλέξτε ένα πιστοποιητικό...", "selectCredentialPlaceholder": "Επιλέξτε ένα πιστοποιητικό...",
"credentialRequired": "Απαιτείται διαπιστευτήριο όταν χρησιμοποιείται έλεγχος ταυτότητας διαπιστευτηρίων", "credentialRequired": "Απαιτείται διαπιστευτήριο όταν χρησιμοποιείται έλεγχος ταυτότητας διαπιστευτηρίων",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "Πληκτρολόγιο-Διαδραστική Ταυτοποίηση", "noneAuthTitle": "Πληκτρολόγιο-Διαδραστική Ταυτοποίηση",
"noneAuthDescription": "Αυτή η μέθοδος ταυτοποίησης θα χρησιμοποιεί τον διαδραστικό έλεγχο ταυτότητας πληκτρολογίου κατά τη σύνδεση με τον διακομιστή SSH.", "noneAuthDescription": "Αυτή η μέθοδος ταυτοποίησης θα χρησιμοποιεί τον διαδραστικό έλεγχο ταυτότητας πληκτρολογίου κατά τη σύνδεση με τον διακομιστή SSH.",
"noneAuthDetails": "Ο διαδραστικός έλεγχος ταυτότητας πληκτρολογίου επιτρέπει στο διακομιστή να σας ζητά διαπιστευτήρια κατά τη διάρκεια της σύνδεσης. Αυτό είναι χρήσιμο για διακομιστές που απαιτούν έλεγχο ταυτότητας πολλαπλών παραγόντων ή αν δεν θέλετε να αποθηκεύσετε τα διαπιστευτήρια τοπικά.", "noneAuthDetails": "Ο διαδραστικός έλεγχος ταυτότητας πληκτρολογίου επιτρέπει στο διακομιστή να σας ζητά διαπιστευτήρια κατά τη διάρκεια της σύνδεσης. Αυτό είναι χρήσιμο για διακομιστές που απαιτούν έλεγχο ταυτότητας πολλαπλών παραγόντων ή αν δεν θέλετε να αποθηκεύσετε τα διαπιστευτήρια τοπικά.",
"opksshAuthTitle": "Επαλήθευση OPKSSH",
"forceKeyboardInteractive": "Εξαναγκασμός Keyboard-Interactive", "forceKeyboardInteractive": "Εξαναγκασμός Keyboard-Interactive",
"forceKeyboardInteractiveDesc": "Δυνάμει της χρήσης διαδραστικού ελέγχου ταυτότητας πληκτρολογίου. Αυτό μερικές φορές απαιτείται για διακομιστές που χρησιμοποιούν έλεγχο ταυτότητας δύο παραγόντων (TOTP/2FA).", "forceKeyboardInteractiveDesc": "Δυνάμει της χρήσης διαδραστικού ελέγχου ταυτότητας πληκτρολογίου. Αυτό μερικές φορές απαιτείται για διακομιστές που χρησιμοποιούν έλεγχο ταυτότητας δύο παραγόντων (TOTP/2FA).",
"overrideCredentialUsername": "Παράκαμψη Ονόματος Διαπιστευτηρίου Χρήστη", "overrideCredentialUsername": "Παράκαμψη Ονόματος Διαπιστευτηρίου Χρήστη",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "Άνοιγμα σε περιηγητή", "warpgateOpenBrowser": "Άνοιγμα σε περιηγητή",
"warpgateContinue": "Έχω Ολοκληρώσει την Πιστοποίηση", "warpgateContinue": "Έχω Ολοκληρώσει την Πιστοποίηση",
"warpgateTimeout": "Λήξη χρονικού ορίου ταυτοποίησης. Παρακαλώ επανασύνδεση.", "warpgateTimeout": "Λήξη χρονικού ορίου ταυτοποίησης. Παρακαλώ επανασύνδεση.",
"opksshAuthRequired": "Απαιτείται Πιστοποίηση OPKSSH",
"opksshAuthDescription": "Πλήρης ταυτοποίηση στο πρόγραμμα περιήγησής σας για να συνεχίσετε. Αυτή η συνεδρία θα παραμείνει έγκυρη για 24 ώρες.",
"opksshAuthUrl": "URL Ταυτοποίησης",
"opksshOpenBrowser": "Άνοιγμα περιηγητή για έλεγχο ταυτότητας",
"opksshWaitingForAuth": "Αναμονή για έλεγχο ταυτότητας στο πρόγραμμα περιήγησης...",
"opksshAuthenticating": "Επεξεργασία ταυτοποίησης...",
"opksshTimeout": "Λήξη χρονικού ορίου επαλήθευσης. Παρακαλώ προσπαθήστε ξανά.",
"opksshAuthFailed": "Ο έλεγχος ταυτότητας απέτυχε. Παρακαλώ ελέγξτε τα στοιχεία σας και προσπαθήστε ξανά.",
"opksshConfigMissing": "Η ρύθμιση παραμέτρων OPKSSH δεν βρέθηκε. Παρακαλώ δημιουργήστε ~/.opk/config.yml με τις ρυθμίσεις παρόχου OIDC. Δείτε την τεκμηρίωση: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Εισαγωγή Κωδικού Πρόσβασης?", "sudoPasswordPopupTitle": "Εισαγωγή Κωδικού Πρόσβασης?",
"sudoPasswordPopupHint": "Πατήστε Enter για εισαγωγή, Esc για απόρριψη", "sudoPasswordPopupHint": "Πατήστε Enter για εισαγωγή, Esc για απόρριψη",
"sudoPasswordPopupConfirm": "Insert", "sudoPasswordPopupConfirm": "Insert",
@@ -1359,8 +1337,7 @@
"automaticFallback": "Αυτόματη προσπάθεια ελέγχου ταυτότητας {{method}}...", "automaticFallback": "Αυτόματη προσπάθεια ελέγχου ταυτότητας {{method}}...",
"totpTimeout": "Λήξη χρονικού ορίου επαλήθευσης TOTP. Επανασύνδεση.", "totpTimeout": "Λήξη χρονικού ορίου επαλήθευσης TOTP. Επανασύνδεση.",
"passwordTimeout": "Λήξη χρονικού ορίου επαλήθευσης κωδικού πρόσβασης. Επανασύνδεση.", "passwordTimeout": "Λήξη χρονικού ορίου επαλήθευσης κωδικού πρόσβασης. Επανασύνδεση.",
"connectionRejected": "Η σύνδεση απορρίφθηκε από το διακομιστή. Παρακαλώ ελέγξτε τον έλεγχο ταυτότητας και τις ρυθμίσεις δικτύου.", "connectionRejected": "Η σύνδεση απορρίφθηκε από το διακομιστή. Παρακαλώ ελέγξτε τον έλεγχο ταυτότητας και τις ρυθμίσεις δικτύου."
"hostKeyRejected": "Η επαλήθευση SSH κεντρικού υπολογιστή απορρίφθηκε. Η σύνδεση ακυρώθηκε."
}, },
"fileManager": { "fileManager": {
"title": "Διαχειριστής Αρχείων", "title": "Διαχειριστής Αρχείων",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "Επιτυχής Επαναφορά Κωδικού Πρόσβασης", "passwordResetSuccess": "Επιτυχής Επαναφορά Κωδικού Πρόσβασης",
"passwordResetSuccessDesc": "Επιτυχής επαναφορά του κωδικού πρόσβασης. Μπορείτε τώρα να συνδεθείτε με το νέο κωδικό πρόσβασης." "passwordResetSuccessDesc": "Επιτυχής επαναφορά του κωδικού πρόσβασης. Μπορείτε τώρα να συνδεθείτε με το νέο κωδικό πρόσβασης."
}, },
"hostKey": {
"verifyNewHost": "Επαλήθευση Κλειδιού Διακομιστή SSH",
"keyChangedWarning": "Το Κλειδί Υπολογιστή SSH Άλλαξε",
"firstConnectionTitle": "Πρώτη σύνδεση σε αυτόν τον υπολογιστή",
"firstConnectionDescription": "Η αυθεντικότητα αυτού του κεντρικού υπολογιστή δεν μπορεί να καθοριστεί. Βεβαιωθείτε ότι το δακτυλικό αποτύπωμα ταιριάζει με αυτό που περιμένετε.",
"keyChangedDescription": "Το κλειδί φιλοξενίας αυτού του διακομιστή έχει αλλάξει από την τελευταία σας σύνδεση. Αυτό μπορεί να υποδηλώνει ένα πρόβλημα ασφάλειας.",
"previousKey": "Προηγούμενο Κλειδί",
"newFingerprint": "Νέο Δακτυλικό Αποτύπωμα",
"fingerprint": "Αποτύπωμα",
"verifyInstructions": "Αν εμπιστεύεστε αυτόν τον οικοδεσπότη, κάντε κλικ για να συνεχίσετε και να αποθηκεύσετε αυτό το δακτυλικό αποτύπωμα για μελλοντικές συνδέσεις.",
"securityWarning": "Προειδοποίηση Ασφαλείας",
"acceptAndContinue": "Αποδοχή & Συνέχεια",
"acceptNewKey": "Αποδοχή Νέου Κλειδιού & Συνέχεια"
},
"errors": { "errors": {
"notFound": "Η σελίδα δεν βρέθηκε", "notFound": "Η σελίδα δεν βρέθηκε",
"unauthorized": "Μη εξουσιοδοτημένη πρόσβαση", "unauthorized": "Μη εξουσιοδοτημένη πρόσβαση",
@@ -2063,8 +2026,6 @@
"terminalSettings": "Τερματικό", "terminalSettings": "Τερματικό",
"hostSidebarSettings": "Κεντρικός Υπολογιστής & Πλαϊνή Μπάρα", "hostSidebarSettings": "Κεντρικός Υπολογιστής & Πλαϊνή Μπάρα",
"snippetsSettings": "Δείγματα", "snippetsSettings": "Δείγματα",
"confirmSnippetExecution": "Επιβεβαίωση Εκτέλεσης Δείκτη",
"confirmSnippetExecutionDesc": "Εμφάνιση διαλόγου επιβεβαίωσης πριν την εκτέλεση αποσπασμάτων",
"updateSettings": "Ενημερώσεις", "updateSettings": "Ενημερώσεις",
"disableUpdateCheck": "Απενεργοποίηση Ελέγχου Ενημέρωσης", "disableUpdateCheck": "Απενεργοποίηση Ελέγχου Ενημέρωσης",
"disableUpdateCheckDesc": "Διακοπή ελέγχου για νέες εκδόσεις κατά την εκκίνηση και τον πίνακα ελέγχου. Μειώνει τα αιτήματα δικτύου.", "disableUpdateCheckDesc": "Διακοπή ελέγχου για νέες εκδόσεις κατά την εκκίνηση και τον πίνακα ελέγχου. Μειώνει τα αιτήματα δικτύου.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 ή example.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "Credenciales",
"credentialsViewer": "Visor de credenciales", "credentialsViewer": "Visor de credenciales",
"manageYourSSHCredentials": "Administrar sus credenciales SSH de forma segura", "manageYourSSHCredentials": "Administrar sus credenciales SSH de forma segura",
"addCredential": "Añadir credencial", "addCredential": "Añadir credencial",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "Esto añadirá con seguridad la clave pública al archivo ~/.ssh/authorized_keys del host de destino sin sobrescribir las claves existentes. La operación es reversible.", "deploymentProcessDescription": "Esto añadirá con seguridad la clave pública al archivo ~/.ssh/authorized_keys del host de destino sin sobrescribir las claves existentes. La operación es reversible.",
"chooseHostToDeploy": "Seleccione un host al que desplegar...", "chooseHostToDeploy": "Seleccione un host al que desplegar...",
"deploying": "Desplegando...", "deploying": "Desplegando...",
"copyDeployCommand": "Copiar comando de despliegue",
"copyDeployCommandDescription": "Copie un comando de shell para agregar manualmente la clave pública al archivo authorized_keys de un host. Útil cuando la autenticación de contraseña está desactivada.",
"deployCommandCopied": "Desplegar comando copiado al portapapeles",
"manualDeployInfo": "Pegue este comando en el terminal del host de destino (por ejemplo, consola Proxmox, IPMI o acceso físico) para añadir la clave SSH.",
"name": "Nombre", "name": "Nombre",
"noHostsAvailable": "No hay hosts disponibles", "noHostsAvailable": "No hay hosts disponibles",
"noHostsMatchSearch": "Ningún host coincide con tu búsqueda", "noHostsMatchSearch": "Ningún host coincide con tu búsqueda",
@@ -303,9 +298,7 @@
"createFolder": "Crear carpeta", "createFolder": "Crear carpeta",
"editFolder": "Editar carpeta", "editFolder": "Editar carpeta",
"editFolderDescription": "Personaliza tu carpeta de fragmentos", "editFolderDescription": "Personaliza tu carpeta de fragmentos",
"createFolderDescription": "Organiza tus fragmentos en carpetas", "createFolderDescription": "Organiza tus fragmentos en carpetas"
"confirmExecution": "¿Ejecutar \"{{name}}\"?",
"confirmExecutionDesc": "¿Está seguro de que desea ejecutar este snippet?"
}, },
"commandHistory": { "commandHistory": {
"title": "Historial", "title": "Historial",
@@ -409,7 +402,6 @@
"required": "Requerido", "required": "Requerido",
"optional": "Opcional", "optional": "Opcional",
"connect": "Conectar", "connect": "Conectar",
"copied": "Copiado",
"connecting": "Conectando...", "connecting": "Conectando...",
"creating": "Creando...", "creating": "Creando...",
"clear": "Claro", "clear": "Claro",
@@ -502,7 +494,6 @@
"checking": "Comprobando...", "checking": "Comprobando...",
"checkingDatabase": "Comprobando conexión a la base de datos...", "checkingDatabase": "Comprobando conexión a la base de datos...",
"checkingAuthentication": "Comprobando autenticación...", "checkingAuthentication": "Comprobando autenticación...",
"backendReconnected": "Conexión con el servidor restaurada",
"actions": "Acciones", "actions": "Acciones",
"remove": "Eliminar", "remove": "Eliminar",
"revoke": "Revoke", "revoke": "Revoke",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "Gestor de Anfitriones", "title": "Gestor de Anfitriones",
"hosts": "Anfitriones",
"sshHosts": "Equipos SSH", "sshHosts": "Equipos SSH",
"noHosts": "No hay hosts SSH", "noHosts": "No hay hosts SSH",
"noHostsMessage": "Aún no has añadido ningún host SSH. Haz clic en \"Añadir Host\" para empezar.", "noHostsMessage": "Aún no has añadido ningún host SSH. Haz clic en \"Añadir Host\" para empezar.",
@@ -846,7 +836,7 @@
"failedToImportJson": "Error al importar el archivo JSON", "failedToImportJson": "Error al importar el archivo JSON",
"connectionDetails": "Detalles de la conexión", "connectionDetails": "Detalles de la conexión",
"organization": "Organización", "organization": "Organización",
"ipAddress": "Dirección IP o nombre de host", "ipAddress": "Dirección IP",
"port": "Puerto", "port": "Puerto",
"name": "Nombre", "name": "Nombre",
"username": "Usuario", "username": "Usuario",
@@ -925,8 +915,6 @@
"key": "Clave", "key": "Clave",
"credential": "Credencial", "credential": "Credencial",
"none": "Ninguna", "none": "Ninguna",
"opkssh": "OPKSH",
"opksshAuthDescription": "Te solicita la autenticación web de OPKSSH en cada conexión. Tu token de autenticación durará 24 horas hasta que deba ser renovado.",
"selectCredential": "Seleccionar credencial", "selectCredential": "Seleccionar credencial",
"selectCredentialPlaceholder": "Elija una credencial...", "selectCredentialPlaceholder": "Elija una credencial...",
"credentialRequired": "Se requiere credencial cuando se utiliza autenticación de credenciales", "credentialRequired": "Se requiere credencial cuando se utiliza autenticación de credenciales",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "Autenticación interactiva del teclado", "noneAuthTitle": "Autenticación interactiva del teclado",
"noneAuthDescription": "Este método de autenticación utilizará la autenticación interactiva del teclado cuando se conecte al servidor SSH.", "noneAuthDescription": "Este método de autenticación utilizará la autenticación interactiva del teclado cuando se conecte al servidor SSH.",
"noneAuthDetails": "La autenticación interactiva de teclado permite al servidor pedir credenciales durante la conexión. Esto es útil para servidores que requieren autenticación de múltiples factores o si no desea guardar las credenciales localmente.", "noneAuthDetails": "La autenticación interactiva de teclado permite al servidor pedir credenciales durante la conexión. Esto es útil para servidores que requieren autenticación de múltiples factores o si no desea guardar las credenciales localmente.",
"opksshAuthTitle": "Autenticación OPKSSH",
"forceKeyboardInteractive": "Forzar interactivo del teclado", "forceKeyboardInteractive": "Forzar interactivo del teclado",
"forceKeyboardInteractiveDesc": "Fuerza el uso de autenticación interactiva de teclado. Esto a veces es necesario para servidores que utilizan Autenticación de Doble Factor (TOTP/2FA).", "forceKeyboardInteractiveDesc": "Fuerza el uso de autenticación interactiva de teclado. Esto a veces es necesario para servidores que utilizan Autenticación de Doble Factor (TOTP/2FA).",
"overrideCredentialUsername": "Reemplazar nombre de usuario de credenciales", "overrideCredentialUsername": "Reemplazar nombre de usuario de credenciales",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "Abrir en el navegador", "warpgateOpenBrowser": "Abrir en el navegador",
"warpgateContinue": "He completado la autenticación", "warpgateContinue": "He completado la autenticación",
"warpgateTimeout": "Se agotó el tiempo de autenticación de Warpgate. Por favor, vuelva a conectar.", "warpgateTimeout": "Se agotó el tiempo de autenticación de Warpgate. Por favor, vuelva a conectar.",
"opksshAuthRequired": "Autenticación OPKSSH requerida",
"opksshAuthDescription": "Autenticación completa en su navegador para continuar. Esta sesión permanecerá válida durante 24 horas.",
"opksshAuthUrl": "URL de autenticación",
"opksshOpenBrowser": "Abrir navegador para autenticar",
"opksshWaitingForAuth": "Esperando la autenticación en el navegador...",
"opksshAuthenticating": "Procesando autenticación...",
"opksshTimeout": "Se ha agotado el tiempo de autenticación. Por favor, inténtalo de nuevo.",
"opksshAuthFailed": "Error de autenticación. Por favor, comprueba tus credenciales e inténtalo de nuevo.",
"opksshConfigMissing": "Configuración OPKSSH no encontrada. Por favor cree ~/.opk/config.yml con la configuración de su proveedor OIDC. Vea la documentación: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "¿Insertar contraseña?", "sudoPasswordPopupTitle": "¿Insertar contraseña?",
"sudoPasswordPopupHint": "Presione Enter para insertar, Esc para descartar", "sudoPasswordPopupHint": "Presione Enter para insertar, Esc para descartar",
"sudoPasswordPopupConfirm": "Insert", "sudoPasswordPopupConfirm": "Insert",
@@ -1359,8 +1337,7 @@
"automaticFallback": "Intentando automáticamente autenticación {{method}}...", "automaticFallback": "Intentando automáticamente autenticación {{method}}...",
"totpTimeout": "Tiempo de verificación TOTP. Por favor, vuelva a conectar.", "totpTimeout": "Tiempo de verificación TOTP. Por favor, vuelva a conectar.",
"passwordTimeout": "Tiempo de verificación de contraseña. 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.", "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."
}, },
"fileManager": { "fileManager": {
"title": "Gestor de archivos", "title": "Gestor de archivos",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "Contraseña restablecida con éxito", "passwordResetSuccess": "Contraseña restablecida con éxito",
"passwordResetSuccessDesc": "Su contraseña se ha restablecido con éxito. Ahora puede iniciar sesión con su nueva contraseña." "passwordResetSuccessDesc": "Su contraseña se ha restablecido con éxito. Ahora puede iniciar sesión con su nueva contraseña."
}, },
"hostKey": {
"verifyNewHost": "Verificar clave SSH Host",
"keyChangedWarning": "SSH Host Key cambiada",
"firstConnectionTitle": "Primera conexión a este host",
"firstConnectionDescription": "La autenticidad de este host no se puede establecer. Verifique que la huella digital coincide con lo que espera.",
"keyChangedDescription": "La clave de host para este servidor ha cambiado desde su última conexión. Esto podría indicar un problema de seguridad.",
"previousKey": "Clave anterior",
"newFingerprint": "Nueva huella dactilar",
"fingerprint": "Huella dactilar",
"verifyInstructions": "Si confía en este host, haga clic en Aceptar para continuar y guardar esta huella dactilar para conexiones futuras.",
"securityWarning": "Advertencia de seguridad",
"acceptAndContinue": "Aceptar y continuar",
"acceptNewKey": "Aceptar nueva clave y continuar"
},
"errors": { "errors": {
"notFound": "Página no encontrada", "notFound": "Página no encontrada",
"unauthorized": "Acceso no autorizado", "unauthorized": "Acceso no autorizado",
@@ -2063,8 +2026,6 @@
"terminalSettings": "Terminal", "terminalSettings": "Terminal",
"hostSidebarSettings": "Anfitrión y barra lateral", "hostSidebarSettings": "Anfitrión y barra lateral",
"snippetsSettings": "Fragmentos", "snippetsSettings": "Fragmentos",
"confirmSnippetExecution": "Confirmar ejecución de Snippet",
"confirmSnippetExecutionDesc": "Mostrar diálogo de confirmación antes de ejecutar fragmentos",
"updateSettings": "Actualizaciones", "updateSettings": "Actualizaciones",
"disableUpdateCheck": "Desactivar revisión de actualización", "disableUpdateCheck": "Desactivar revisión de actualización",
"disableUpdateCheckDesc": "Deja de buscar nuevas versiones en el arranque y el tablero. Reduce las solicitudes de red.", "disableUpdateCheckDesc": "Deja de buscar nuevas versiones en el arranque y el tablero. Reduce las solicitudes de red.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 o ejemplo.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "Käyttäjätunnukset",
"credentialsViewer": "Käyttöoikeustietojen Katselin", "credentialsViewer": "Käyttöoikeustietojen Katselin",
"manageYourSSHCredentials": "Hallitse SSH tunnuksiasi turvallisesti", "manageYourSSHCredentials": "Hallitse SSH tunnuksiasi turvallisesti",
"addCredential": "Lisää Käyttöoikeustieto", "addCredential": "Lisää Käyttöoikeustieto",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "Tämä lisää turvallisesti julkisen avaimen kohdetiedoston ~/.ssh/authorized_keys tiedostoon ilman olemassa olevien avainten korvaamista. Toiminto on palautettavissa.", "deploymentProcessDescription": "Tämä lisää turvallisesti julkisen avaimen kohdetiedoston ~/.ssh/authorized_keys tiedostoon ilman olemassa olevien avainten korvaamista. Toiminto on palautettavissa.",
"chooseHostToDeploy": "Valitse isäntä johon haluat ottaa käyttöön...", "chooseHostToDeploy": "Valitse isäntä johon haluat ottaa käyttöön...",
"deploying": "Otetaan Käyttöön...", "deploying": "Otetaan Käyttöön...",
"copyDeployCommand": "Kopioi Deploy Komento",
"copyDeployCommandDescription": "Kopioi komento, jolla julkinen avain lisätään manuaalisesti isännän authorized_keys tiedostoon. Hyödyllinen, kun salasanan todennus on pois päältä.",
"deployCommandCopied": "Julkaise komento kopioitu leikepöydälle",
"manualDeployInfo": "Liitä tämä komento kohdeisännän päätteeseen (esim. Proxmox konsoli, IPMI tai fyysinen pääsy) lisätäksesi SSH avaimen.",
"name": "Nimi", "name": "Nimi",
"noHostsAvailable": "Ei isäntiä saatavilla", "noHostsAvailable": "Ei isäntiä saatavilla",
"noHostsMatchSearch": "Yksikään isäntä ei vastaa hakuasi", "noHostsMatchSearch": "Yksikään isäntä ei vastaa hakuasi",
@@ -303,9 +298,7 @@
"createFolder": "Luo Kansio", "createFolder": "Luo Kansio",
"editFolder": "Muokkaa Kansiota", "editFolder": "Muokkaa Kansiota",
"editFolderDescription": "Muokkaa snippet-kansiota", "editFolderDescription": "Muokkaa snippet-kansiota",
"createFolderDescription": "Järjestä leikkeet kansioihin", "createFolderDescription": "Järjestä leikkeet kansioihin"
"confirmExecution": "Suorita \"{{name}}\"?",
"confirmExecutionDesc": "Oletko varma, että haluat suorittaa tämän ohjelman?"
}, },
"commandHistory": { "commandHistory": {
"title": "Historia", "title": "Historia",
@@ -409,7 +402,6 @@
"required": "Pakollinen", "required": "Pakollinen",
"optional": "Valinnainen", "optional": "Valinnainen",
"connect": "Yhdistä", "connect": "Yhdistä",
"copied": "Kopioitu",
"connecting": "Yhdistetään...", "connecting": "Yhdistetään...",
"creating": "Luodaan...", "creating": "Luodaan...",
"clear": "Tyhjennä", "clear": "Tyhjennä",
@@ -502,7 +494,6 @@
"checking": "Tarkistetaan...", "checking": "Tarkistetaan...",
"checkingDatabase": "Tarkistetaan tietokantayhteyttä...", "checkingDatabase": "Tarkistetaan tietokantayhteyttä...",
"checkingAuthentication": "Tarkistetaan todennusta...", "checkingAuthentication": "Tarkistetaan todennusta...",
"backendReconnected": "Palvelinyhteys palautettu",
"actions": "Toiminnot", "actions": "Toiminnot",
"remove": "Poista", "remove": "Poista",
"revoke": "Revoke", "revoke": "Revoke",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "Isäntälaitteen Hallinta", "title": "Isäntälaitteen Hallinta",
"hosts": "Isäntä",
"sshHosts": "Ssh Isäntä", "sshHosts": "Ssh Isäntä",
"noHosts": "Ei Ssh Isäntiä", "noHosts": "Ei Ssh Isäntiä",
"noHostsMessage": "Et ole vielä lisännyt yhtään SSH isäntää. Napsauta \"Lisää isäntä\" aloittaaksesi.", "noHostsMessage": "Et ole vielä lisännyt yhtään SSH isäntää. Napsauta \"Lisää isäntä\" aloittaaksesi.",
@@ -846,7 +836,7 @@
"failedToImportJson": "JSON-tiedoston tuonti epäonnistui", "failedToImportJson": "JSON-tiedoston tuonti epäonnistui",
"connectionDetails": "Yhteyden Tiedot", "connectionDetails": "Yhteyden Tiedot",
"organization": "Organisaatio", "organization": "Organisaatio",
"ipAddress": "IP-osoite tai isäntänimi", "ipAddress": "Ip Osoite",
"port": "Portti", "port": "Portti",
"name": "Nimi", "name": "Nimi",
"username": "Käyttäjätunnus", "username": "Käyttäjätunnus",
@@ -925,8 +915,6 @@
"key": "Avain", "key": "Avain",
"credential": "Käyttöoikeustiedot", "credential": "Käyttöoikeustiedot",
"none": "Ei Mitään", "none": "Ei Mitään",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Pyytää sinua OPKSSH web-authiin jokaisen yhteyden yhteydessä. Käyttöoikeustunnuksesi kestää 24 tuntia, kunnes se on uusittava.",
"selectCredential": "Valitse Käyttöoikeustieto", "selectCredential": "Valitse Käyttöoikeustieto",
"selectCredentialPlaceholder": "Valitse käyttäjätunnus...", "selectCredentialPlaceholder": "Valitse käyttäjätunnus...",
"credentialRequired": "Käyttöoikeustiedot vaaditaan, kun käytetään käyttöoikeustietojen todennusta", "credentialRequired": "Käyttöoikeustiedot vaaditaan, kun käytetään käyttöoikeustietojen todennusta",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "Näppäimistöinteraktiivinen todennus", "noneAuthTitle": "Näppäimistöinteraktiivinen todennus",
"noneAuthDescription": "Tämä todennusmenetelmä käyttää näppäimistöinteraktiivista todennusta yhdistettäessä SSH-palvelimeen.", "noneAuthDescription": "Tämä todennusmenetelmä käyttää näppäimistöinteraktiivista todennusta yhdistettäessä SSH-palvelimeen.",
"noneAuthDetails": "Näppäimistötodennus antaa palvelimen pyytää sinulta tunnistetietoja yhteyden muodostamisen aikana. Tämä on hyödyllistä palvelimilla, jotka vaativat monivaiheisen todennuksen tai jos et halua tallentaa tunnistetietoja paikallisesti.", "noneAuthDetails": "Näppäimistötodennus antaa palvelimen pyytää sinulta tunnistetietoja yhteyden muodostamisen aikana. Tämä on hyödyllistä palvelimilla, jotka vaativat monivaiheisen todennuksen tai jos et halua tallentaa tunnistetietoja paikallisesti.",
"opksshAuthTitle": "OPKSSH Todennus",
"forceKeyboardInteractive": "Pakota näppäimistöinteraktiivinen", "forceKeyboardInteractive": "Pakota näppäimistöinteraktiivinen",
"forceKeyboardInteractiveDesc": "Pakottaa näppäimistöinteraktiivisen todennuksen käytön. Tätä vaaditaan joskus palvelimilla, jotka käyttävät kaksivaiheista todennusta (TOTP/2FA).", "forceKeyboardInteractiveDesc": "Pakottaa näppäimistöinteraktiivisen todennuksen käytön. Tätä vaaditaan joskus palvelimilla, jotka käyttävät kaksivaiheista todennusta (TOTP/2FA).",
"overrideCredentialUsername": "Ohita tunnistetiedot Käyttäjätunnus", "overrideCredentialUsername": "Ohita tunnistetiedot Käyttäjätunnus",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "Avaa selaimessa", "warpgateOpenBrowser": "Avaa selaimessa",
"warpgateContinue": "Olen Suorittanut Todentamisen", "warpgateContinue": "Olen Suorittanut Todentamisen",
"warpgateTimeout": "Warpgate autentikoinnin aikakatkaisu. Yhdistä uudelleen.", "warpgateTimeout": "Warpgate autentikoinnin aikakatkaisu. Yhdistä uudelleen.",
"opksshAuthRequired": "OPKSSH Todennus Vaaditaan",
"opksshAuthDescription": "Lopeta todennus selaimessasi jatkaaksesi. Tämä istunto pysyy voimassa 24 tuntia.",
"opksshAuthUrl": "Todennuksen URL",
"opksshOpenBrowser": "Avaa selain todennettavaksi",
"opksshWaitingForAuth": "Odotetaan todennusta selaimessa...",
"opksshAuthenticating": "Käsitellään todennusta...",
"opksshTimeout": "Todennus aikakatkaistiin. Yritä uudelleen.",
"opksshAuthFailed": "Todennus epäonnistui. Tarkista käyttäjätunnuksesi ja yritä uudelleen.",
"opksshConfigMissing": "OPKSSH konfiguraatiota ei löytynyt. Luo ~/.opk/config.yml OIDC palveluntarjoajan asetuksissa. Katso dokumentaatio: https://github.com/openpubkey/opkssh#konfiguraatio",
"sudoPasswordPopupTitle": "Syötä salasana?", "sudoPasswordPopupTitle": "Syötä salasana?",
"sudoPasswordPopupHint": "Lisää painamalla Enter, hylkää painamalla Esc", "sudoPasswordPopupHint": "Lisää painamalla Enter, hylkää painamalla Esc",
"sudoPasswordPopupConfirm": "Lisää", "sudoPasswordPopupConfirm": "Lisää",
@@ -1359,8 +1337,7 @@
"automaticFallback": "Yritetään automaattisesti {{method}} todennusta...", "automaticFallback": "Yritetään automaattisesti {{method}} todennusta...",
"totpTimeout": "TOTP vahvistuksen aikakatkaisu. Yhdistä uudelleen.", "totpTimeout": "TOTP vahvistuksen aikakatkaisu. Yhdistä uudelleen.",
"passwordTimeout": "Salasanan varmennuksen aikakatkaisu. Yhdistä uudelleen.", "passwordTimeout": "Salasanan varmennuksen aikakatkaisu. Yhdistä uudelleen.",
"connectionRejected": "Palvelin hylkäsi yhteyden. Tarkista todennus ja verkon asetukset.", "connectionRejected": "Palvelin hylkäsi yhteyden. Tarkista todennus ja verkon asetukset."
"hostKeyRejected": "SSH isäntäavaimen vahvistus hylätty. Yhteys peruutettu."
}, },
"fileManager": { "fileManager": {
"title": "Tiedostojen Hallinta", "title": "Tiedostojen Hallinta",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "Salasanan palautus onnistui", "passwordResetSuccess": "Salasanan palautus onnistui",
"passwordResetSuccessDesc": "Salasanasi on nollattu onnistuneesti. Voit nyt kirjautua sisään uudella salasanallasi." "passwordResetSuccessDesc": "Salasanasi on nollattu onnistuneesti. Voit nyt kirjautua sisään uudella salasanallasi."
}, },
"hostKey": {
"verifyNewHost": "Vahvista Ssh Isäntäavain",
"keyChangedWarning": "Ssh Isäntäavain Vaihdettu",
"firstConnectionTitle": "Yhteyden muodostaminen tähän palvelimeen ensimmäistä kertaa",
"firstConnectionDescription": "Tämän isännän aitoutta ei voida vahvistaa. Varmista sormenjälki vastaa sitä, mitä odotat.",
"keyChangedDescription": "Palvelimen isäntäavain on muuttunut viimeisen yhteytesi jälkeen. Tämä saattaa merkitä tietoturvaongelmaa.",
"previousKey": "Edellinen Avain",
"newFingerprint": "Uusi Sormenjälki",
"fingerprint": "Sormenjälki",
"verifyInstructions": "Jos luotat tähän isäntään, jatka ja tallenna sormenjälki tulevia yhteyksiä varten.",
"securityWarning": "Turvallisuusvaroitus",
"acceptAndContinue": "Hyväksy & Jatka",
"acceptNewKey": "Hyväksy Uusi Avain Ja Jatka"
},
"errors": { "errors": {
"notFound": "Sivua ei löytynyt", "notFound": "Sivua ei löytynyt",
"unauthorized": "Luvaton pääsy", "unauthorized": "Luvaton pääsy",
@@ -2063,8 +2026,6 @@
"terminalSettings": "Pääte", "terminalSettings": "Pääte",
"hostSidebarSettings": "Isäntä ja sivupalkki", "hostSidebarSettings": "Isäntä ja sivupalkki",
"snippetsSettings": "Projekti", "snippetsSettings": "Projekti",
"confirmSnippetExecution": "Vahvista Projektin Suoritus",
"confirmSnippetExecutionDesc": "Näytä vahvistusvalintaikkuna ennen snippettien suorittamista",
"updateSettings": "Päivitykset", "updateSettings": "Päivitykset",
"disableUpdateCheck": "Poista Päivitystarkistus Käytöstä", "disableUpdateCheck": "Poista Päivitystarkistus Käytöstä",
"disableUpdateCheckDesc": "Lopeta uusien versioiden tarkistaminen käynnistettäessä ja hallintapaneelissa. Vähennä verkkopyyntöjä.", "disableUpdateCheckDesc": "Lopeta uusien versioiden tarkistaminen käynnistettäessä ja hallintapaneelissa. Vähennä verkkopyyntöjä.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 tai esimerkki.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "Identifiants",
"credentialsViewer": "Visionneuse des identifiants", "credentialsViewer": "Visionneuse des identifiants",
"manageYourSSHCredentials": "Gérez vos identifiants SSH en toute sécurité", "manageYourSSHCredentials": "Gérez vos identifiants SSH en toute sécurité",
"addCredential": "Ajouter un mot de passe", "addCredential": "Ajouter un mot de passe",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "Ceci ajoutera la clé publique au fichier ~/.ssh/authorized_keys de l'hôte cible sans écraser les clés existantes. L'opération est réversible.", "deploymentProcessDescription": "Ceci ajoutera la clé publique au fichier ~/.ssh/authorized_keys de l'hôte cible sans écraser les clés existantes. L'opération est réversible.",
"chooseHostToDeploy": "Choisissez un hôte à déployer vers...", "chooseHostToDeploy": "Choisissez un hôte à déployer vers...",
"deploying": "Déploiement...", "deploying": "Déploiement...",
"copyDeployCommand": "Copier la commande de déploiement",
"copyDeployCommandDescription": "Copiez une commande shell pour ajouter manuellement la clé publique au fichier authorized_keys d'un hôte. Utile lorsque l'authentification par mot de passe est désactivée.",
"deployCommandCopied": "Déployer la commande copiée dans le presse-papiers",
"manualDeployInfo": "Collez cette commande dans le terminal de l'hôte cible (par exemple, console Proxmox, IPMI, ou accès physique) pour ajouter la clé SSH.",
"name": "Nom", "name": "Nom",
"noHostsAvailable": "Aucun hôte disponible", "noHostsAvailable": "Aucun hôte disponible",
"noHostsMatchSearch": "Aucun hôte ne correspond à votre recherche", "noHostsMatchSearch": "Aucun hôte ne correspond à votre recherche",
@@ -303,9 +298,7 @@
"createFolder": "Créer un dossier", "createFolder": "Créer un dossier",
"editFolder": "Modifier le dossier", "editFolder": "Modifier le dossier",
"editFolderDescription": "Personnaliser votre dossier de snippet", "editFolderDescription": "Personnaliser votre dossier de snippet",
"createFolderDescription": "Organiser vos modules de texte dans des dossiers", "createFolderDescription": "Organiser vos modules de texte dans des dossiers"
"confirmExecution": "Exécuter \"{{name}} \" ?",
"confirmExecutionDesc": "Êtes-vous sûr de vouloir exécuter ce snippet?"
}, },
"commandHistory": { "commandHistory": {
"title": "Historique", "title": "Historique",
@@ -409,7 +402,6 @@
"required": "Requis", "required": "Requis",
"optional": "Optionnel", "optional": "Optionnel",
"connect": "Connecter", "connect": "Connecter",
"copied": "Copié",
"connecting": "Connexion en cours...", "connecting": "Connexion en cours...",
"creating": "Création en cours...", "creating": "Création en cours...",
"clear": "Nettoyer", "clear": "Nettoyer",
@@ -502,7 +494,6 @@
"checking": "Vérification...", "checking": "Vérification...",
"checkingDatabase": "Vérification de la connexion à la base de données...", "checkingDatabase": "Vérification de la connexion à la base de données...",
"checkingAuthentication": "Vérification de l'authentification...", "checkingAuthentication": "Vérification de l'authentification...",
"backendReconnected": "Connexion au serveur restaurée",
"actions": "Actions", "actions": "Actions",
"remove": "Retirer", "remove": "Retirer",
"revoke": "Revoke", "revoke": "Revoke",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "Responsable d'hôte", "title": "Responsable d'hôte",
"hosts": "Hôtes",
"sshHosts": "Hôtes SSH", "sshHosts": "Hôtes SSH",
"noHosts": "Aucun hôte SSH", "noHosts": "Aucun hôte SSH",
"noHostsMessage": "Vous n'avez pas encore ajouté d'hôtes SSH. Cliquez sur \"Ajouter Hôte\" pour commencer.", "noHostsMessage": "Vous n'avez pas encore ajouté d'hôtes SSH. Cliquez sur \"Ajouter Hôte\" pour commencer.",
@@ -846,7 +836,7 @@
"failedToImportJson": "Impossible d'importer le fichier JSON", "failedToImportJson": "Impossible d'importer le fichier JSON",
"connectionDetails": "Détails de la connexion", "connectionDetails": "Détails de la connexion",
"organization": "Organisation", "organization": "Organisation",
"ipAddress": "Adresse IP ou nom d'hôte", "ipAddress": "Adresse IP",
"port": "Port", "port": "Port",
"name": "Nom", "name": "Nom",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
@@ -925,8 +915,6 @@
"key": "Clés", "key": "Clés",
"credential": "Identification", "credential": "Identification",
"none": "Aucun", "none": "Aucun",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Vous invite à l'authentification OPKSSH à chaque connexion. Votre jeton d'authentification durera 24 heures jusqu'à ce qu'il soit renouvelé.",
"selectCredential": "Sélectionner les informations d'identification", "selectCredential": "Sélectionner les informations d'identification",
"selectCredentialPlaceholder": "Choisissez un identifiant...", "selectCredentialPlaceholder": "Choisissez un identifiant...",
"credentialRequired": "Les informations d'identification sont requises lors de l'utilisation de l'authentification", "credentialRequired": "Les informations d'identification sont requises lors de l'utilisation de l'authentification",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "Authentification interactive du clavier", "noneAuthTitle": "Authentification interactive du clavier",
"noneAuthDescription": "Cette méthode d'authentification utilisera l'authentification interactive au clavier lors de la connexion au serveur SSH.", "noneAuthDescription": "Cette méthode d'authentification utilisera l'authentification interactive au clavier lors de la connexion au serveur SSH.",
"noneAuthDetails": "L'authentification interactive au clavier permet au serveur de vous demander des informations d'identification pendant la connexion. Ceci est utile pour les serveurs qui nécessitent une authentification multi-facteurs ou si vous ne voulez pas enregistrer les identifiants localement.", "noneAuthDetails": "L'authentification interactive au clavier permet au serveur de vous demander des informations d'identification pendant la connexion. Ceci est utile pour les serveurs qui nécessitent une authentification multi-facteurs ou si vous ne voulez pas enregistrer les identifiants localement.",
"opksshAuthTitle": "Authentification OPKSSH",
"forceKeyboardInteractive": "Forcer l'interaction du clavier", "forceKeyboardInteractive": "Forcer l'interaction du clavier",
"forceKeyboardInteractiveDesc": "Force l'utilisation de l'authentification interactive au clavier. Ceci est parfois nécessaire pour les serveurs qui utilisent l'authentification à deux facteurs (TOTP/2FA).", "forceKeyboardInteractiveDesc": "Force l'utilisation de l'authentification interactive au clavier. Ceci est parfois nécessaire pour les serveurs qui utilisent l'authentification à deux facteurs (TOTP/2FA).",
"overrideCredentialUsername": "Remplacer le nom d'utilisateur de l'identifiant", "overrideCredentialUsername": "Remplacer le nom d'utilisateur de l'identifiant",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "Ouvrir dans le navigateur", "warpgateOpenBrowser": "Ouvrir dans le navigateur",
"warpgateContinue": "J'ai terminé l'authentification", "warpgateContinue": "J'ai terminé l'authentification",
"warpgateTimeout": "Délai d'authentification de Warpgate. Veuillez vous reconnecter.", "warpgateTimeout": "Délai d'authentification de Warpgate. Veuillez vous reconnecter.",
"opksshAuthRequired": "Authentification OPKSSH requise",
"opksshAuthDescription": "Authentification complète dans votre navigateur pour continuer. Cette session restera valide pendant 24 heures.",
"opksshAuthUrl": "URL d'authentification",
"opksshOpenBrowser": "Ouvrez le navigateur pour vous authentifier",
"opksshWaitingForAuth": "En attente d'authentification dans le navigateur...",
"opksshAuthenticating": "Traitement de l'authentification...",
"opksshTimeout": "Délai d'authentification dépassé. Veuillez réessayer.",
"opksshAuthFailed": "L'authentification a échoué. Veuillez vérifier vos identifiants et réessayer.",
"opksshConfigMissing": "La configuration OPKSSH est introuvable. Veuillez créer ~/.opk/config.yml avec vos paramètres de fournisseur OIDC. Voir la documentation : https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Insérer un mot de passe ?", "sudoPasswordPopupTitle": "Insérer un mot de passe ?",
"sudoPasswordPopupHint": "Appuyez sur Entrée pour insérer, Échap pour rejeter", "sudoPasswordPopupHint": "Appuyez sur Entrée pour insérer, Échap pour rejeter",
"sudoPasswordPopupConfirm": "Insert", "sudoPasswordPopupConfirm": "Insert",
@@ -1359,8 +1337,7 @@
"automaticFallback": "Essai automatique de l'authentification {{method}}...", "automaticFallback": "Essai automatique de l'authentification {{method}}...",
"totpTimeout": "Délai de vérification TOTP. Veuillez vous reconnecter.", "totpTimeout": "Délai de vérification TOTP. Veuillez vous reconnecter.",
"passwordTimeout": "Délai de vérification du mot de passe. 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.", "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."
}, },
"fileManager": { "fileManager": {
"title": "Gestionnaire de fichiers", "title": "Gestionnaire de fichiers",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "Mot de passe réinitialisé avec succès", "passwordResetSuccess": "Mot de passe réinitialisé avec succès",
"passwordResetSuccessDesc": "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe." "passwordResetSuccessDesc": "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe."
}, },
"hostKey": {
"verifyNewHost": "Vérifier la clé d'hôte SSH",
"keyChangedWarning": "Clé d'hôte SSH modifiée",
"firstConnectionTitle": "Première connexion à cet hôte",
"firstConnectionDescription": "L'authenticité de cet hôte ne peut pas être établie. Vérifiez que l'empreinte digitale correspond à ce que vous attendez.",
"keyChangedDescription": "La clé hôte de ce serveur a changé depuis votre dernière connexion. Cela pourrait indiquer un problème de sécurité.",
"previousKey": "Clé précédente",
"newFingerprint": "Nouvelle empreinte digitale",
"fingerprint": "Empreinte digitale",
"verifyInstructions": "Si vous faites confiance à cet hôte, cliquez sur Accepter pour continuer et enregistrer cette empreinte digitale pour de futures connexions.",
"securityWarning": "Avertissement de sécurité",
"acceptAndContinue": "Accepter et continuer",
"acceptNewKey": "Accepter la nouvelle clé et continuer"
},
"errors": { "errors": {
"notFound": "Page introuvable", "notFound": "Page introuvable",
"unauthorized": "Accès non autorisé", "unauthorized": "Accès non autorisé",
@@ -2063,8 +2026,6 @@
"terminalSettings": "Terminal", "terminalSettings": "Terminal",
"hostSidebarSettings": "Hôte et barre latérale", "hostSidebarSettings": "Hôte et barre latérale",
"snippetsSettings": "Extraits", "snippetsSettings": "Extraits",
"confirmSnippetExecution": "Confirmer l'exécution du snippet",
"confirmSnippetExecutionDesc": "Afficher la boîte de dialogue de confirmation avant d'exécuter les snippets",
"updateSettings": "Mises à jour", "updateSettings": "Mises à jour",
"disableUpdateCheck": "Désactiver la vérification des mises à jour", "disableUpdateCheck": "Désactiver la vérification des mises à jour",
"disableUpdateCheckDesc": "Arrêter la vérification des nouvelles versions au démarrage et au tableau de bord. Réduit les requêtes réseau.", "disableUpdateCheckDesc": "Arrêter la vérification des nouvelles versions au démarrage et au tableau de bord. Réduit les requêtes réseau.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 ou exemple.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "אישורים",
"credentialsViewer": "מציג אישורים", "credentialsViewer": "מציג אישורים",
"manageYourSSHCredentials": "נהל את פרטי ה-SSH שלך בצורה מאובטחת", "manageYourSSHCredentials": "נהל את פרטי ה-SSH שלך בצורה מאובטחת",
"addCredential": "הוסף אישור", "addCredential": "הוסף אישור",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "פעולה זו תוסיף בבטחה את המפתח הציבורי לקובץ ~/.ssh/authorized_keys של מארח היעד מבלי להחליף מפתחות קיימים. הפעולה היא הפיכה.", "deploymentProcessDescription": "פעולה זו תוסיף בבטחה את המפתח הציבורי לקובץ ~/.ssh/authorized_keys של מארח היעד מבלי להחליף מפתחות קיימים. הפעולה היא הפיכה.",
"chooseHostToDeploy": "בחר מארח לפריסה...", "chooseHostToDeploy": "בחר מארח לפריסה...",
"deploying": "פריסה...", "deploying": "פריסה...",
"copyDeployCommand": "העתק את פקודת הפריסה",
"copyDeployCommandDescription": "העתק פקודת מעטפת כדי להוסיף ידנית את המפתח הציבורי לקובץ authorized_keys של מחשב מארח. שימושי כאשר אימות סיסמה מושבת.",
"deployCommandCopied": "פקודת הפריסה הועתקה ללוח",
"manualDeployInfo": "הדבק פקודה זו בטרמינל של מחשב המארח (למשל, קונסולת Proxmox, IPMI או גישה פיזית) כדי להוסיף את מפתח ה-SSH.",
"name": "שֵׁם", "name": "שֵׁם",
"noHostsAvailable": "אין מארחים זמינים", "noHostsAvailable": "אין מארחים זמינים",
"noHostsMatchSearch": "אין מארחים התואמים את החיפוש שלך", "noHostsMatchSearch": "אין מארחים התואמים את החיפוש שלך",
@@ -303,9 +298,7 @@
"createFolder": "צור תיקייה", "createFolder": "צור תיקייה",
"editFolder": "עריכת תיקייה", "editFolder": "עריכת תיקייה",
"editFolderDescription": "התאם אישית את תיקיית הקטעים שלך", "editFolderDescription": "התאם אישית את תיקיית הקטעים שלך",
"createFolderDescription": "ארגנו את הקטעים שלכם בתיקיות", "createFolderDescription": "ארגנו את הקטעים שלכם בתיקיות"
"confirmExecution": "להפעיל את \"{{name}}\"?",
"confirmExecutionDesc": "האם אתה בטוח שאתה רוצה להפעיל את הקטע הזה?"
}, },
"commandHistory": { "commandHistory": {
"title": "הִיסטוֹרִיָה", "title": "הִיסטוֹרִיָה",
@@ -409,7 +402,6 @@
"required": "דָרוּשׁ", "required": "דָרוּשׁ",
"optional": "אופציונלי", "optional": "אופציונלי",
"connect": "לְחַבֵּר", "connect": "לְחַבֵּר",
"copied": "מוּעֲתָק",
"connecting": "מְקַשֵׁר...", "connecting": "מְקַשֵׁר...",
"creating": "יוצר...", "creating": "יוצר...",
"clear": "בָּרוּר", "clear": "בָּרוּר",
@@ -502,7 +494,6 @@
"checking": "בודק...", "checking": "בודק...",
"checkingDatabase": "בודק חיבור למסד הנתונים...", "checkingDatabase": "בודק חיבור למסד הנתונים...",
"checkingAuthentication": "בודק אימות...", "checkingAuthentication": "בודק אימות...",
"backendReconnected": "חיבור השרת שוחזר",
"actions": "פעולות", "actions": "פעולות",
"remove": "לְהַסִיר", "remove": "לְהַסִיר",
"revoke": "לְבַטֵל", "revoke": "לְבַטֵל",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "מנהל מארח", "title": "מנהל מארח",
"hosts": "מארחים",
"sshHosts": "מארחי SSH", "sshHosts": "מארחי SSH",
"noHosts": "אין מארחי SSH", "noHosts": "אין מארחי SSH",
"noHostsMessage": "עדיין לא הוספת מארחי SSH. לחץ על \"הוסף מארח\" כדי להתחיל.", "noHostsMessage": "עדיין לא הוספת מארחי SSH. לחץ על \"הוסף מארח\" כדי להתחיל.",
@@ -846,7 +836,7 @@
"failedToImportJson": "ייבוא קובץ JSON נכשל", "failedToImportJson": "ייבוא קובץ JSON נכשל",
"connectionDetails": "פרטי חיבור", "connectionDetails": "פרטי חיבור",
"organization": "אִרגוּן", "organization": "אִרגוּן",
"ipAddress": "כתובת IP או שם מארח", "ipAddress": "כתובת IP",
"port": "נָמָל", "port": "נָמָל",
"name": "שֵׁם", "name": "שֵׁם",
"username": "שם משתמש", "username": "שם משתמש",
@@ -925,8 +915,6 @@
"key": "מַפְתֵחַ", "key": "מַפְתֵחַ",
"credential": "תְעוּדָה", "credential": "תְעוּדָה",
"none": "אַף לֹא אֶחָד", "none": "אַף לֹא אֶחָד",
"opkssh": "אופקש",
"opksshAuthDescription": "מבקש ממך אימות אינטרנט של OPKSSH בכל חיבור. אסימון האימות שלך יהיה בתוקף למשך 24 שעות עד שיהיה צורך לחדשו.",
"selectCredential": "בחר אישור", "selectCredential": "בחר אישור",
"selectCredentialPlaceholder": "בחר אישור...", "selectCredentialPlaceholder": "בחר אישור...",
"credentialRequired": "נדרשת אישור בעת שימוש באימות אישורים", "credentialRequired": "נדרשת אישור בעת שימוש באימות אישורים",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "אימות אינטראקטיבי באמצעות מקלדת", "noneAuthTitle": "אימות אינטראקטיבי באמצעות מקלדת",
"noneAuthDescription": "שיטת אימות זו תשתמש באימות אינטראקטיבי באמצעות מקלדת בעת התחברות לשרת SSH.", "noneAuthDescription": "שיטת אימות זו תשתמש באימות אינטראקטיבי באמצעות מקלדת בעת התחברות לשרת SSH.",
"noneAuthDetails": "אימות אינטראקטיבי באמצעות מקלדת מאפשר לשרת לבקש ממך אישורים במהלך החיבור. זה שימושי עבור שרתים הדורשים אימות רב-גורמי או אם אינך מעוניין לשמור אישורים באופן מקומי.", "noneAuthDetails": "אימות אינטראקטיבי באמצעות מקלדת מאפשר לשרת לבקש ממך אישורים במהלך החיבור. זה שימושי עבור שרתים הדורשים אימות רב-גורמי או אם אינך מעוניין לשמור אישורים באופן מקומי.",
"opksshAuthTitle": "אימות OPKSSH",
"forceKeyboardInteractive": "כפיית מקלדת אינטראקטיבית", "forceKeyboardInteractive": "כפיית מקלדת אינטראקטיבית",
"forceKeyboardInteractiveDesc": "כופה שימוש באימות אינטראקטיבי באמצעות מקלדת. זה נדרש לעיתים עבור שרתים המשתמשים באימות דו-שלבי (TOTP/2FA).", "forceKeyboardInteractiveDesc": "כופה שימוש באימות אינטראקטיבי באמצעות מקלדת. זה נדרש לעיתים עבור שרתים המשתמשים באימות דו-שלבי (TOTP/2FA).",
"overrideCredentialUsername": "עקיפת שם משתמש של פרטי כניסה", "overrideCredentialUsername": "עקיפת שם משתמש של פרטי כניסה",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "פתח בדפדפן", "warpgateOpenBrowser": "פתח בדפדפן",
"warpgateContinue": "סיימתי את האימות", "warpgateContinue": "סיימתי את האימות",
"warpgateTimeout": "פסק זמן לאימות Warpgate. אנא התחבר מחדש.", "warpgateTimeout": "פסק זמן לאימות Warpgate. אנא התחבר מחדש.",
"opksshAuthRequired": "נדרש אימות OPKSSH",
"opksshAuthDescription": "השלם את האימות בדפדפן שלך כדי להמשיך. סשן זה יישאר תקף למשך 24 שעות.",
"opksshAuthUrl": "כתובת URL לאימות",
"opksshOpenBrowser": "פתח את הדפדפן כדי לאמת",
"opksshWaitingForAuth": "ממתין לאימות בדפדפן...",
"opksshAuthenticating": "מעבד אימות...",
"opksshTimeout": "הזמן שהוקצב לאימות הסתיים. אנא נסה שוב.",
"opksshAuthFailed": "האימות נכשל. אנא בדוק את פרטי הגישה שלך ונסה שוב.",
"opksshConfigMissing": "תצורת OPKSSH לא נמצאה. אנא צור ~/.opk/config.yml עם הגדרות ספק ה-OIDC שלך. עיין בתיעוד: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "להכניס סיסמה?", "sudoPasswordPopupTitle": "להכניס סיסמה?",
"sudoPasswordPopupHint": "לחץ על Enter כדי להוסיף, Esc כדי לסגור", "sudoPasswordPopupHint": "לחץ על Enter כדי להוסיף, Esc כדי לסגור",
"sudoPasswordPopupConfirm": "לְהַכנִיס", "sudoPasswordPopupConfirm": "לְהַכנִיס",
@@ -1359,8 +1337,7 @@
"automaticFallback": "מנסה אוטומטית אימות {{method}} ...", "automaticFallback": "מנסה אוטומטית אימות {{method}} ...",
"totpTimeout": "פסק זמן לאימות TOTP. אנא התחבר מחדש.", "totpTimeout": "פסק זמן לאימות TOTP. אנא התחבר מחדש.",
"passwordTimeout": "פסק זמן לאימות סיסמה. אנא התחבר מחדש.", "passwordTimeout": "פסק זמן לאימות סיסמה. אנא התחבר מחדש.",
"connectionRejected": "החיבור נדחה על ידי השרת. אנא בדוק את האימות ותצורת הרשת שלך.", "connectionRejected": "החיבור נדחה על ידי השרת. אנא בדוק את האימות ותצורת הרשת שלך."
"hostKeyRejected": "אימות מפתח מארח SSH נדחה. החיבור בוטל."
}, },
"fileManager": { "fileManager": {
"title": "מנהל הקבצים", "title": "מנהל הקבצים",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "איפוס הסיסמה הצליח", "passwordResetSuccess": "איפוס הסיסמה הצליח",
"passwordResetSuccessDesc": "הסיסמה שלך אופסה בהצלחה. כעת תוכל להתחבר באמצעות הסיסמה החדשה שלך." "passwordResetSuccessDesc": "הסיסמה שלך אופסה בהצלחה. כעת תוכל להתחבר באמצעות הסיסמה החדשה שלך."
}, },
"hostKey": {
"verifyNewHost": "אימות מפתח מארח SSH",
"keyChangedWarning": "מפתח מארח SSH שונה",
"firstConnectionTitle": "בפעם הראשונה שמתחברת למארח הזה",
"firstConnectionDescription": "לא ניתן לקבוע את האותנטיות של מארח זה. ודא שטביעת האצבע תואמת את מה שאתה מצפה.",
"keyChangedDescription": "מפתח המארח של שרת זה השתנה מאז החיבור האחרון שלך. זה יכול להצביע על בעיית אבטחה.",
"previousKey": "מפתח קודם",
"newFingerprint": "טביעת אצבע חדשה",
"fingerprint": "טְבִיעַת אֶצבָּעוֹת",
"verifyInstructions": "אם אתה בוטח במארח זה, לחץ על קבל כדי להמשיך ולשמור את טביעת האצבע הזו לחיבורים עתידיים.",
"securityWarning": "אזהרת אבטחה",
"acceptAndContinue": "קבל והמשך",
"acceptNewKey": "קבל את המפתח החדש והמשך"
},
"errors": { "errors": {
"notFound": "הדף לא נמצא", "notFound": "הדף לא נמצא",
"unauthorized": "גישה לא מורשית", "unauthorized": "גישה לא מורשית",
@@ -2063,8 +2026,6 @@
"terminalSettings": "מָסוֹף", "terminalSettings": "מָסוֹף",
"hostSidebarSettings": "מארח וסרגל צד", "hostSidebarSettings": "מארח וסרגל צד",
"snippetsSettings": "קטעי טקסט", "snippetsSettings": "קטעי טקסט",
"confirmSnippetExecution": "אישור ביצוע קטע",
"confirmSnippetExecutionDesc": "הצג תיבת דו-שיח לאישור לפני ביצוע קטעי טקסט",
"updateSettings": "עדכונים", "updateSettings": "עדכונים",
"disableUpdateCheck": "השבתת בדיקת עדכונים", "disableUpdateCheck": "השבתת בדיקת עדכונים",
"disableUpdateCheckDesc": "הפסק לבדוק גרסאות חדשות בעת ההפעלה ובלוח המחוונים. מפחית בקשות רשת.", "disableUpdateCheckDesc": "הפסק לבדוק גרסאות חדשות בעת ההפעלה ובלוח המחוונים. מפחית בקשות רשת.",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 או example.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
+4 -43
View File
@@ -1,6 +1,5 @@
{ {
"credentials": { "credentials": {
"credentials": "साख",
"credentialsViewer": "क्रेडेंशियल व्यूअर", "credentialsViewer": "क्रेडेंशियल व्यूअर",
"manageYourSSHCredentials": "अपने SSH क्रेडेंशियल्स को सुरक्षित रूप से प्रबंधित करें", "manageYourSSHCredentials": "अपने SSH क्रेडेंशियल्स को सुरक्षित रूप से प्रबंधित करें",
"addCredential": "क्रेडेंशियल जोड़ें", "addCredential": "क्रेडेंशियल जोड़ें",
@@ -100,10 +99,6 @@
"deploymentProcessDescription": "इससे सार्वजनिक कुंजी सुरक्षित रूप से लक्ष्य होस्ट की ~/.ssh/authorized_keys फ़ाइल में जुड़ जाएगी, और मौजूदा कुंजियों को ओवरराइट नहीं किया जाएगा। यह प्रक्रिया प्रतिवर्ती है।", "deploymentProcessDescription": "इससे सार्वजनिक कुंजी सुरक्षित रूप से लक्ष्य होस्ट की ~/.ssh/authorized_keys फ़ाइल में जुड़ जाएगी, और मौजूदा कुंजियों को ओवरराइट नहीं किया जाएगा। यह प्रक्रिया प्रतिवर्ती है।",
"chooseHostToDeploy": "परिनियोजन के लिए होस्ट चुनें...", "chooseHostToDeploy": "परिनियोजन के लिए होस्ट चुनें...",
"deploying": "तैनाती जारी है...", "deploying": "तैनाती जारी है...",
"copyDeployCommand": "परिनियोजन कमांड कॉपी करें",
"copyDeployCommandDescription": "किसी होस्ट की authorized_keys फ़ाइल में सार्वजनिक कुंजी को मैन्युअल रूप से जोड़ने के लिए शेल कमांड को कॉपी करें। यह तब उपयोगी होता है जब पासवर्ड प्रमाणीकरण अक्षम हो।",
"deployCommandCopied": "डिप्लॉयमेंट कमांड क्लिपबोर्ड पर कॉपी हो गई है",
"manualDeployInfo": "SSH कुंजी जोड़ने के लिए इस कमांड को टारगेट होस्ट के टर्मिनल (जैसे, प्रॉक्समॉक्स कंसोल, IPMI, या फिजिकल एक्सेस) में पेस्ट करें।",
"name": "नाम", "name": "नाम",
"noHostsAvailable": "कोई होस्ट उपलब्ध नहीं है", "noHostsAvailable": "कोई होस्ट उपलब्ध नहीं है",
"noHostsMatchSearch": "आपकी खोज से मेल खाने वाले कोई होस्ट नहीं हैं", "noHostsMatchSearch": "आपकी खोज से मेल खाने वाले कोई होस्ट नहीं हैं",
@@ -303,9 +298,7 @@
"createFolder": "फ़ोल्डर बनाएँ", "createFolder": "फ़ोल्डर बनाएँ",
"editFolder": "फ़ोल्डर संपादित करें", "editFolder": "फ़ोल्डर संपादित करें",
"editFolderDescription": "अपने स्निपेट फ़ोल्डर को अनुकूलित करें", "editFolderDescription": "अपने स्निपेट फ़ोल्डर को अनुकूलित करें",
"createFolderDescription": "अपने लेखों के अंशों को फ़ोल्डरों में व्यवस्थित करें।", "createFolderDescription": "अपने लेखों के अंशों को फ़ोल्डरों में व्यवस्थित करें।"
"confirmExecution": "\"{{name}}\" निष्पादित करें?",
"confirmExecutionDesc": "क्या आप वाकई इस कोड स्निपेट को चलाना चाहते हैं?"
}, },
"commandHistory": { "commandHistory": {
"title": "इतिहास", "title": "इतिहास",
@@ -409,7 +402,6 @@
"required": "आवश्यक", "required": "आवश्यक",
"optional": "वैकल्पिक", "optional": "वैकल्पिक",
"connect": "जोड़ना", "connect": "जोड़ना",
"copied": "कॉपी किया गया",
"connecting": "कनेक्ट हो रहा है...", "connecting": "कनेक्ट हो रहा है...",
"creating": "सृजन...", "creating": "सृजन...",
"clear": "स्पष्ट", "clear": "स्पष्ट",
@@ -502,7 +494,6 @@
"checking": "जाँच चल रही है...", "checking": "जाँच चल रही है...",
"checkingDatabase": "डेटाबेस कनेक्शन की जाँच की जा रही है...", "checkingDatabase": "डेटाबेस कनेक्शन की जाँच की जा रही है...",
"checkingAuthentication": "प्रमाणीकरण की जाँच की जा रही है...", "checkingAuthentication": "प्रमाणीकरण की जाँच की जा रही है...",
"backendReconnected": "सर्वर कनेक्शन बहाल हो गया",
"actions": "कार्रवाई", "actions": "कार्रवाई",
"remove": "निकालना", "remove": "निकालना",
"revoke": "रद्द करना", "revoke": "रद्द करना",
@@ -815,7 +806,6 @@
}, },
"hosts": { "hosts": {
"title": "मेजबान प्रबंधक", "title": "मेजबान प्रबंधक",
"hosts": "मेजबान",
"sshHosts": "एसएसएच होस्ट", "sshHosts": "एसएसएच होस्ट",
"noHosts": "कोई एसएसएच होस्ट नहीं", "noHosts": "कोई एसएसएच होस्ट नहीं",
"noHostsMessage": "आपने अभी तक कोई SSH होस्ट नहीं जोड़ा है। शुरू करने के लिए \"होस्ट जोड़ें\" पर क्लिक करें।", "noHostsMessage": "आपने अभी तक कोई SSH होस्ट नहीं जोड़ा है। शुरू करने के लिए \"होस्ट जोड़ें\" पर क्लिक करें।",
@@ -846,7 +836,7 @@
"failedToImportJson": "JSON फ़ाइल आयात करने में विफल", "failedToImportJson": "JSON फ़ाइल आयात करने में विफल",
"connectionDetails": "कनेक्शन विवरण", "connectionDetails": "कनेक्शन विवरण",
"organization": "संगठन", "organization": "संगठन",
"ipAddress": "आईपी पता या होस्टनाम", "ipAddress": "आईपी पता",
"port": "पत्तन", "port": "पत्तन",
"name": "नाम", "name": "नाम",
"username": "उपयोगकर्ता नाम", "username": "उपयोगकर्ता नाम",
@@ -925,8 +915,6 @@
"key": "चाबी", "key": "चाबी",
"credential": "क्रेडेंशियल", "credential": "क्रेडेंशियल",
"none": "कोई नहीं", "none": "कोई नहीं",
"opkssh": "ओपीकेएसएच",
"opksshAuthDescription": "हर बार कनेक्ट होने पर यह आपसे OPKSSH वेब-प्रमाणीकरण के लिए पूछेगा। आपका प्रमाणीकरण टोकन 24 घंटे तक वैध रहेगा, जिसके बाद इसे नवीनीकृत करना होगा।",
"selectCredential": "क्रेडेंशियल चुनें", "selectCredential": "क्रेडेंशियल चुनें",
"selectCredentialPlaceholder": "एक प्रमाण पत्र चुनें...", "selectCredentialPlaceholder": "एक प्रमाण पत्र चुनें...",
"credentialRequired": "क्रेडेंशियल प्रमाणीकरण का उपयोग करते समय क्रेडेंशियल आवश्यक है।", "credentialRequired": "क्रेडेंशियल प्रमाणीकरण का उपयोग करते समय क्रेडेंशियल आवश्यक है।",
@@ -1095,7 +1083,6 @@
"noneAuthTitle": "कीबोर्ड-इंटरैक्टिव प्रमाणीकरण", "noneAuthTitle": "कीबोर्ड-इंटरैक्टिव प्रमाणीकरण",
"noneAuthDescription": "एसएसएच सर्वर से कनेक्ट करते समय यह प्रमाणीकरण विधि कीबोर्ड-इंटरैक्टिव प्रमाणीकरण का उपयोग करेगी।", "noneAuthDescription": "एसएसएच सर्वर से कनेक्ट करते समय यह प्रमाणीकरण विधि कीबोर्ड-इंटरैक्टिव प्रमाणीकरण का उपयोग करेगी।",
"noneAuthDetails": "कीबोर्ड-इंटरैक्टिव प्रमाणीकरण सर्वर को कनेक्शन के दौरान आपसे क्रेडेंशियल मांगने की अनुमति देता है। यह उन सर्वरों के लिए उपयोगी है जिन्हें मल्टी-फैक्टर प्रमाणीकरण की आवश्यकता होती है या यदि आप क्रेडेंशियल को स्थानीय रूप से सहेजना नहीं चाहते हैं।", "noneAuthDetails": "कीबोर्ड-इंटरैक्टिव प्रमाणीकरण सर्वर को कनेक्शन के दौरान आपसे क्रेडेंशियल मांगने की अनुमति देता है। यह उन सर्वरों के लिए उपयोगी है जिन्हें मल्टी-फैक्टर प्रमाणीकरण की आवश्यकता होती है या यदि आप क्रेडेंशियल को स्थानीय रूप से सहेजना नहीं चाहते हैं।",
"opksshAuthTitle": "ओपीकेएसएसएच प्रमाणीकरण",
"forceKeyboardInteractive": "फ़ोर्स कीबोर्ड-इंटरैक्टिव", "forceKeyboardInteractive": "फ़ोर्स कीबोर्ड-इंटरैक्टिव",
"forceKeyboardInteractiveDesc": "कीबोर्ड-आधारित प्रमाणीकरण को अनिवार्य बनाता है। यह कभी-कभी उन सर्वरों के लिए आवश्यक होता है जो दो-कारक प्रमाणीकरण (TOTP/2FA) का उपयोग करते हैं।", "forceKeyboardInteractiveDesc": "कीबोर्ड-आधारित प्रमाणीकरण को अनिवार्य बनाता है। यह कभी-कभी उन सर्वरों के लिए आवश्यक होता है जो दो-कारक प्रमाणीकरण (TOTP/2FA) का उपयोग करते हैं।",
"overrideCredentialUsername": "क्रेडेंशियल उपयोगकर्ता नाम को ओवरराइड करें", "overrideCredentialUsername": "क्रेडेंशियल उपयोगकर्ता नाम को ओवरराइड करें",
@@ -1329,15 +1316,6 @@
"warpgateOpenBrowser": "ब्राउज़र में खोलें", "warpgateOpenBrowser": "ब्राउज़र में खोलें",
"warpgateContinue": "मैंने प्रमाणीकरण पूरा कर लिया है", "warpgateContinue": "मैंने प्रमाणीकरण पूरा कर लिया है",
"warpgateTimeout": "वॉरपगेट प्रमाणीकरण में समय समाप्त हो गया है। कृपया पुनः कनेक्ट करें।", "warpgateTimeout": "वॉरपगेट प्रमाणीकरण में समय समाप्त हो गया है। कृपया पुनः कनेक्ट करें।",
"opksshAuthRequired": "OPKSSH प्रमाणीकरण आवश्यक है",
"opksshAuthDescription": "आगे बढ़ने के लिए अपने ब्राउज़र में प्रमाणीकरण पूरा करें। यह सत्र 24 घंटे तक वैध रहेगा।",
"opksshAuthUrl": "प्रमाणीकरण यूआरएल",
"opksshOpenBrowser": "प्रमाणीकरण के लिए ब्राउज़र खोलें",
"opksshWaitingForAuth": "ब्राउज़र में प्रमाणीकरण की प्रतीक्षा हो रही है...",
"opksshAuthenticating": "प्रमाणीकरण प्रक्रिया जारी है...",
"opksshTimeout": "प्रमाणीकरण का समय समाप्त हो गया। कृपया पुनः प्रयास करें।",
"opksshAuthFailed": "प्रमाणीकरण विफल रहा। कृपया अपनी पहचान सत्यापित करें और पुनः प्रयास करें।",
"opksshConfigMissing": "OPKSSH कॉन्फ़िगरेशन नहीं मिला। कृपया अपने OIDC प्रदाता सेटिंग्स के साथ ~/.opk/config.yml फ़ाइल बनाएँ। दस्तावेज़ देखें: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "पासवर्ड डालें?", "sudoPasswordPopupTitle": "पासवर्ड डालें?",
"sudoPasswordPopupHint": "सम्मिलित करने के लिए Enter दबाएँ, हटाने के लिए Esc दबाएँ", "sudoPasswordPopupHint": "सम्मिलित करने के लिए Enter दबाएँ, हटाने के लिए Esc दबाएँ",
"sudoPasswordPopupConfirm": "डालना", "sudoPasswordPopupConfirm": "डालना",
@@ -1359,8 +1337,7 @@
"automaticFallback": "Automatically trying {{method}} authentication...", "automaticFallback": "Automatically trying {{method}} authentication...",
"totpTimeout": "टीओटीपी सत्यापन में समय समाप्त हो गया है। कृपया पुनः कनेक्ट करें।", "totpTimeout": "टीओटीपी सत्यापन में समय समाप्त हो गया है। कृपया पुनः कनेक्ट करें।",
"passwordTimeout": "पासवर्ड सत्यापन में समय समाप्त हो गया है। कृपया पुनः कनेक्ट करें।", "passwordTimeout": "पासवर्ड सत्यापन में समय समाप्त हो गया है। कृपया पुनः कनेक्ट करें।",
"connectionRejected": "सर्वर द्वारा कनेक्शन अस्वीकृत कर दिया गया है। कृपया अपनी प्रमाणीकरण और नेटवर्क कॉन्फ़िगरेशन की जाँच करें।", "connectionRejected": "सर्वर द्वारा कनेक्शन अस्वीकृत कर दिया गया है। कृपया अपनी प्रमाणीकरण और नेटवर्क कॉन्फ़िगरेशन की जाँच करें।"
"hostKeyRejected": "SSH होस्ट कुंजी सत्यापन अस्वीकृत। कनेक्शन रद्द।"
}, },
"fileManager": { "fileManager": {
"title": "फ़ाइल मैनेजर", "title": "फ़ाइल मैनेजर",
@@ -1957,20 +1934,6 @@
"passwordResetSuccess": "पासवर्ड रीसेट सफल रहा", "passwordResetSuccess": "पासवर्ड रीसेट सफल रहा",
"passwordResetSuccessDesc": "आपका पासवर्ड सफलतापूर्वक रीसेट हो गया है। अब आप अपने नए पासवर्ड से लॉग इन कर सकते हैं।" "passwordResetSuccessDesc": "आपका पासवर्ड सफलतापूर्वक रीसेट हो गया है। अब आप अपने नए पासवर्ड से लॉग इन कर सकते हैं।"
}, },
"hostKey": {
"verifyNewHost": "SSH होस्ट कुंजी सत्यापित करें",
"keyChangedWarning": "SSH होस्ट कुंजी बदल गई",
"firstConnectionTitle": "पहली बार इस होस्ट से कनेक्ट हो रहा है",
"firstConnectionDescription": "इस होस्ट की प्रामाणिकता स्थापित नहीं की जा सकती। कृपया सुनिश्चित करें कि फिंगरप्रिंट आपकी अपेक्षा के अनुरूप है।",
"keyChangedDescription": "आपके पिछले कनेक्शन के बाद से इस सर्वर की होस्ट कुंजी बदल गई है। यह किसी सुरक्षा समस्या का संकेत हो सकता है।",
"previousKey": "पिछली कुंजी",
"newFingerprint": "नया फिंगरप्रिंट",
"fingerprint": "अंगुली की छाप",
"verifyInstructions": "यदि आप इस होस्ट पर भरोसा करते हैं, तो जारी रखने के लिए स्वीकार करें पर क्लिक करें और भविष्य के कनेक्शनों के लिए इस फिंगरप्रिंट को सहेजें।",
"securityWarning": "सुरक्षा चेतावनी",
"acceptAndContinue": "स्वीकार करें और जारी रखें",
"acceptNewKey": "नई कुंजी स्वीकार करें और जारी रखें"
},
"errors": { "errors": {
"notFound": "पृष्ठ नहीं मिला", "notFound": "पृष्ठ नहीं मिला",
"unauthorized": "अनधिकृत पहुंच", "unauthorized": "अनधिकृत पहुंच",
@@ -2063,8 +2026,6 @@
"terminalSettings": "टर्मिनल", "terminalSettings": "टर्मिनल",
"hostSidebarSettings": "होस्ट और साइडबार", "hostSidebarSettings": "होस्ट और साइडबार",
"snippetsSettings": "स्निपेट्स", "snippetsSettings": "स्निपेट्स",
"confirmSnippetExecution": "स्निपेट निष्पादन की पुष्टि करें",
"confirmSnippetExecutionDesc": "स्निपेट्स को निष्पादित करने से पहले पुष्टिकरण डायलॉग दिखाएँ",
"updateSettings": "अपडेट", "updateSettings": "अपडेट",
"disableUpdateCheck": "अपडेट जांच अक्षम करें", "disableUpdateCheck": "अपडेट जांच अक्षम करें",
"disableUpdateCheckDesc": "स्टार्टअप और डैशबोर्ड पर नए संस्करणों की जाँच करना बंद करें। इससे नेटवर्क अनुरोध कम होते हैं।", "disableUpdateCheckDesc": "स्टार्टअप और डैशबोर्ड पर नए संस्करणों की जाँच करना बंद करें। इससे नेटवर्क अनुरोध कम होते हैं।",
@@ -2085,7 +2046,7 @@
}, },
"placeholders": { "placeholders": {
"enterCode": "000000", "enterCode": "000000",
"ipAddress": "192.168.1.1 या example.com", "ipAddress": "127.0.0.1",
"port": "22", "port": "22",
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",

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