Compare commits

...

8 Commits

Author SHA1 Message Date
Luke Gustafson 59088606b5 New Crowdin updates (#568)
* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Hindi)

* Delete src/locales/translated directory

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Hindi)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Hindi)

* Update source file en.json

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Hindi)
2026-02-12 22:42:36 -06:00
LukeGus 921a762198 chore: remove translations 2026-02-12 22:42:23 -06:00
Luke Gustafson 094f34b671 v1.11.1 (#566)
* 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 directly (#156) (#503)

* fix: resolve merge confli…

* feat: add configurable base path for reverse proxy subpath deployment (#542)

* fix: remote translations

* feat: support OIDC configuration via environment variables (#531)

* Feature request network graph

* Fixing PR442:

- Fixed:
    - UI design elemets
    - UI and button colors
    - JSON export
    - recent activity is default again
- Removed:
    - Online/Offline UI labels
    - left-click menu on hosts
- Added:
    - small pulsing dot inside the hosts to indicate online status like in the left bar

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)

* Update Crowdin configuration file

* Update Crowdin configuration file

* fix: resolve Vite build warnings for mixed static/dynamic imports

- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
  - react-vendor: React and React DOM
  - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
  - monaco: Monaco Editor
  - codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB

This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <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 directly (#156) …

* fix: sort file manager entries alphabetically with directories first (#541)

* fix: remote translations

* feat: support OIDC configuration via environment variables (#531)

* Feature request network graph

* Fixing PR442:

- Fixed:
    - UI design elemets
    - UI and button colors
    - JSON export
    - recent activity is default again
- Removed:
    - Online/Offline UI labels
    - left-click menu on hosts
- Added:
    - small pulsing dot inside the hosts to indicate online status like in the left bar

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)

* Update Crowdin configuration file

* Update Crowdin configuration file

* fix: resolve Vite build warnings for mixed static/dynamic imports

- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
  - react-vendor: React and React DOM
  - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
  - monaco: Monaco Editor
  - codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB

This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <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 directly (#156) (…

* fix: add timeout for SFTP readlink to prevent request hang (#540)

* fix: remote translations

* feat: support OIDC configuration via environment variables (#531)

* Feature request network graph

* Fixing PR442:

- Fixed:
    - UI design elemets
    - UI and button colors
    - JSON export
    - recent activity is default again
- Removed:
    - Online/Offline UI labels
    - left-click menu on hosts
- Added:
    - small pulsing dot inside the hosts to indicate online status like in the left bar

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)

* Update Crowdin configuration file

* Update Crowdin configuration file

* fix: resolve Vite build warnings for mixed static/dynamic imports

- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
  - react-vendor: React and React DOM
  - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
  - monaco: Monaco Editor
  - codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB

This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <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 directly (#156) (#503)

* f…

* feat: add copy deploy command for SSH keys without password auth (#538)

* fix: remote translations

* feat: support OIDC configuration via environment variables (#531)

* Feature request network graph

* Fixing PR442:

- Fixed:
    - UI design elemets
    - UI and button colors
    - JSON export
    - recent activity is default again
- Removed:
    - Online/Offline UI labels
    - left-click menu on hosts
- Added:
    - small pulsing dot inside the hosts to indicate online status like in the left bar

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)

* Update Crowdin configuration file

* Update Crowdin configuration file

* fix: resolve Vite build warnings for mixed static/dynamic imports

- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
  - react-vendor: React and React DOM
  - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
  - monaco: Monaco Editor
  - codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB

This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <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 directly (#156) (#503…

* fix: preserve all ssh_data fields in database export/import (#537)

* fix: remote translations

* feat: support OIDC configuration via environment variables (#531)

* Feature request network graph

* Fixing PR442:

- Fixed:
    - UI design elemets
    - UI and button colors
    - JSON export
    - recent activity is default again
- Removed:
    - Online/Offline UI labels
    - left-click menu on hosts
- Added:
    - small pulsing dot inside the hosts to indicate online status like in the left bar

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)

* Update Crowdin configuration file

* Update Crowdin configuration file

* fix: resolve Vite build warnings for mixed static/dynamic imports

- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
  - react-vendor: React and React DOM
  - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
  - monaco: Monaco Editor
  - codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB

This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <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 directly (#156) (#503)

* …

* fix: change minmax lower bound to 540px from 600px for gridTemplateColumns (#532)

* feat: update/standardize the icon

* feat: update mobile terminal ui to support all new features

* fix: increment ver

* feat: begin opkssh implementation by adding auth type

* feat: finalize opkssh based auth

* fix: update docker/file manager to reflect need for Terminal

* fix: opkssh issues

* fix: ssh.ts build error

* feat: improve logging verbosity (is that a word?)

* fix: remove debug logging

* feat: add host key verification

* feat: consolidate host manager tabs

* feat: allow no password in a credential and fix opkssh issues

* Remove macOS-specific keyboard handling (#544)

* fix: translation with region (#535)

* fix: translation with region

* fix: update `LanguageSwitcher.tsx` and `i18n.ts`

* chore: add temp locales to fix build error

* feat: add opkssh docs button adn update default config

* feat: improve opkssh reliability and docker support

* feat: add remote redirect uri support

* feat: continued opkssh docker support

* feat: continued opkssh docker support

* feat: continued opkssh docker support

* feat: continued opkssh docker support

* feat: continued opkssh docker support

* feat: continued opkssh docker support

* fix: disable browser caching for file manager API responses (#549)

The file manager API endpoint (/ssh/file_manager/ssh/) did not set any
Cache-Control headers. Browsers applied heuristic caching to GET
requests (e.g. listFiles), causing stale file listings to be returned
after creating, deleting, or renaming files.

Users had to clear cookies and re-login to see updated file lists.

Add Cache-Control: no-store, no-cache, must-revalidate to both
nginx.conf and nginx-https.conf for the file manager API location
to ensure browsers always fetch fresh data from the server.

Fixes: Termix-SSH/Support#455

* fix: prevent data loss from encrypted database format mismatch (#548)

* fix: isEncryptedDatabaseFile() fallback to single-file detection when .meta is invalid

Previously, if a stale or corrupted .meta file existed alongside a
valid single-file format encrypted database, isEncryptedDatabaseFile()
would return false without attempting single-file format detection.

This could cause the initialization flow to treat a valid encrypted
database as non-existent, leading to creation of an empty database
that would then overwrite the original encrypted data on the next
periodic save cycle — resulting in permanent data loss.

Now the method falls through to single-file format detection when
.meta validation fails, matching the behavior of decryptDatabaseToBuffer().

Refs: Termix-SSH/Support#452

* fix: decryptDatabaseFile() support single-file encrypted format

decryptDatabaseFile() previously only supported the two-file format
(.encrypted + .meta), but encryptDatabaseFromBuffer() (used by the
periodic 15-second save) writes single-file format and deletes .meta.

This meant /database/restore via restoreFromEncryptedBackup() would
fail on any database that had been saved by the periodic save cycle.

Refactored to delegate to decryptDatabaseToBuffer() which already
handles both formats correctly, eliminating the duplicated decryption
logic and the format mismatch.

Refs: Termix-SSH/Support#452

* fix: force save after database import to prevent data loss

The /database/import endpoint wrote imported data to the in-memory
SQLite database but did not trigger an immediate persist to the
encrypted file on disk. Data persistence relied solely on the
15-second periodic save interval.

If the container crashed or was restarted within that window, all
imported data would be lost. This was the likely cause of users
reporting that imported data "flashed briefly then disappeared."

Now calls DatabaseSaveTrigger.forceSave() immediately after a
successful import to ensure data is written to disk before
returning the response to the client.

Refs: Termix-SSH/Support#452

* fix: getEncryptedFileInfo() support single-file encrypted format

getEncryptedFileInfo() only read metadata from the separate .meta
file, but the periodic save (encryptDatabaseFromBuffer) writes
single-file format and deletes .meta files. This caused the method
to always return null for databases saved by the periodic cycle,
breaking diagnostic APIs and status checks.

Now tries .meta first, then falls back to reading embedded metadata
from the single-file format header, consistent with the detection
logic in isEncryptedDatabaseFile().

Refs: Termix-SSH/Support#452

* feat: file manager disconnecting and added toggle for snippet confirmation

* feat: puid/pgid support

* fix: opkssh timesouts and dockerfile error

* fix: build error

* fix: opkssh auth not loading properly

* fix: show opkssh auth failed logs if failed

* chore: clean up files

* fix: accidental line paste

* fix: remove translatiosn for merge

* New Crowdin updates (#551)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Hindi)

* Delete src/locales/translated directory

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Hindi)

* fix: updated i18n config to support new file naming scheme for crowdin

* fix: host/credential enter key not working and saving overriden username

* feat: update readme

* fix: credential in server stats error and browser refresh and client terminal size issue

* feat: add HTTP proxy support for GitHub API requests (#554)

* feat: add HTTP proxy support for GitHub API requests

* Remove comments from getProxyAgent function

Removed documentation comments for the getProxyAgent function.

---------

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

* fix: desktop app random logouts

* fix: constant logouts

* fix: constant logouts

* fix: unlinked credentials after ujpdating

---------

Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.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: ZacharyZcR <PayasoNorahC@protonmail.com>
2026-02-12 22:28:13 -06:00
Luke Gustafson 726156a734 Update translation path format in crowdin.yml 2026-02-09 20:40:54 -06:00
LukeGus 749be1fc6e fix: sw.js agressive caching 2026-01-26 23:06:34 -06:00
LukeGus 98b760ce64 fix: add host issue, screen flickering issue, 2fa issue, open terminal session icon missing issue 2026-01-26 00:28:40 -06:00
Luke Gustafson 1c9d4560c7 New Crowdin updates (#526)
* New translations en.json (Portuguese)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Portuguese, Brazilian)
2026-01-25 23:46:32 -06:00
LukeGus b388241d09 feat: update cask 2026-01-24 20:38:21 -06:00
124 changed files with 13872 additions and 4176 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
cask "termix" do
version "1.10.0"
sha256 "327c5026006c949f992447835aa6754113f731065b410bedbfa5da5af7cb2386"
version "1.11.0"
sha256 "c62ea2c3097f466229edcbd65a5ea49d898028a716a3023704c5f644d973c70d"
url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg"
name "Termix"
+2 -2
View File
@@ -58,7 +58,7 @@ free and self-hosted alternative to Termius available for all platforms.
- **Command History** - Auto-complete and view previously ran SSH commands
- **Quick Connect** - Connect to a server without having to save the connection data
- **Command Palette** - Double tap left shift to quickly access SSH connections with your keyboard
- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, password autofill, etc.
- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
- **Network Graph** - Customize your Dashboard to visualize your homelab based off your SSH connections with status support
# Planned Features
@@ -136,7 +136,7 @@ channel, however, response times may be longer.
# Screenshots
[![](https://utfs.io/f/nGnSqDveMsqxqiKJF5EnObSopHatulx93N0E1KVsP7hvjMfF)](https://www.youtube.com/watch?v=j1_I1mkhUkE)
[![YouTube](./repo-images/YouTube.png)](https://youtu.be/8OYCPwS7ciA)
<p align="center">
<img src="./repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
+1 -1
View File
@@ -1,3 +1,3 @@
files:
- source: /src/locales/en.json
translation: /src/locales/translated/%two_letters_code%.json
translation: /src/locales/translated/%locale_with_underscore%.json
+4 -5
View File
@@ -51,11 +51,12 @@ ENV DATA_DIR=/app/data \
PORT=8080 \
NODE_ENV=production
RUN apt-get update && apt-get install -y nginx gettext-base openssl && \
RUN apt-get update && apt-get install -y nginx gettext-base openssl ca-certificates gosu && \
update-ca-certificates && \
rm -rf /var/lib/apt/lists/* && \
mkdir -p /app/data /app/uploads /app/nginx /app/nginx/logs /app/nginx/cache /app/nginx/client_body && \
mkdir -p /app/data /app/uploads /app/data/.opk /app/nginx /app/nginx/logs /app/nginx/cache /app/nginx/client_body && \
chown -R node:node /app && \
chmod 755 /app/data /app/uploads /app/nginx && \
chmod 755 /app/data /app/uploads /app/data/.opk /app/nginx && \
touch /app/nginx/nginx.conf && \
chown node:node /app/nginx/nginx.conf
@@ -80,6 +81,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
USER node
CMD ["/entrypoint.sh"]
+36 -2
View File
@@ -1,6 +1,22 @@
#!/bin/sh
set -e
PUID=${PUID:-1000}
PGID=${PGID:-1000}
if [ "$(id -u)" = "0" ]; then
echo "Setting up user permissions (PUID: $PUID, PGID: $PGID)..."
groupmod -o -g "$PGID" node 2>/dev/null || true
usermod -o -u "$PUID" node 2>/dev/null || true
chown -R node:node /app/data /app/uploads /app/nginx 2>/dev/null || true
echo "User node is now UID: $PUID, GID: $PGID"
exec gosu node:node "$0" "$@"
fi
export PORT=${PORT:-8080}
export ENABLE_SSL=${ENABLE_SSL:-false}
export SSL_PORT=${SSL_PORT:-8443}
@@ -19,8 +35,26 @@ fi
envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /app/nginx/nginx.conf
mkdir -p /app/data /app/uploads
chmod 755 /app/data /app/uploads 2>/dev/null || true
mkdir -p /app/data /app/uploads /app/data/.opk
chmod 755 /app/data /app/uploads /app/data/.opk 2>/dev/null || true
if [ -w /app/data ]; then
echo "Data directory is writable"
else
echo "WARNING: Data directory is not writable. OPKSSH may fail."
ls -ld /app/data
fi
if [ -w /app/data/.opk ]; then
echo "OPKSSH directory is writable"
else
echo "WARNING: OPKSSH directory is not writable. OPKSSH authentication will fail."
ls -ld /app/data/.opk
fi
if [ ! -d "/app/opkssh" ]; then
echo "WARNING: OPKSSH binary directory not found at /app/opkssh"
fi
if [ "$ENABLE_SSL" = "true" ]; then
echo "Checking SSL certificate configuration..."
+28
View File
@@ -213,6 +213,28 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/ssh/opkssh-chooser(/.*)?$ {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/ssh/opkssh-callback(/.*)?$ {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
@@ -228,6 +250,10 @@ http {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
@@ -294,6 +320,8 @@ http {
client_max_body_size 5G;
client_body_timeout 300s;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
proxy_pass http://127.0.0.1:30004;
proxy_http_version 1.1;
proxy_set_header Host $host;
+28 -1
View File
@@ -202,6 +202,28 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/ssh/opkssh-chooser(/.*)?$ {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/ssh/opkssh-callback(/.*)?$ {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
@@ -217,7 +239,10 @@ http {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
@@ -283,6 +308,8 @@ http {
client_max_body_size 5G;
client_body_timeout 300s;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
proxy_pass http://127.0.0.1:30004;
proxy_http_version 1.1;
proxy_set_header Host $host;
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- PWA Meta Tags -->
<meta name="theme-color" content="#09090b" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
+4 -2
View File
@@ -1,12 +1,12 @@
{
"name": "termix",
"version": "1.11.0",
"version": "1.11.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "termix",
"version": "1.11.0",
"version": "1.11.1",
"dependencies": {
"@codemirror/autocomplete": "^6.18.7",
"@codemirror/commands": "^6.3.3",
@@ -62,6 +62,7 @@
"dotenv": "^17.2.0",
"drizzle-orm": "^0.44.3",
"express": "^5.1.0",
"https-proxy-agent": "^7.0.6",
"i18n-auto-translation": "^2.2.3",
"i18next": "^25.4.2",
"i18next-browser-languagedetector": "^8.2.0",
@@ -7885,6 +7886,7 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10"
}
+4 -1
View File
@@ -1,7 +1,7 @@
{
"name": "termix",
"private": true,
"version": "1.11.0",
"version": "1.11.1",
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
"author": "Karmaa",
"main": "electron/main.cjs",
@@ -17,6 +17,8 @@
"build": "vite build && tsc -p tsconfig.node.json",
"build:backend": "tsc -p tsconfig.node.json",
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js",
"dev:docker": "docker stop termix-dev 2>nul & docker rm termix-dev 2>nul & docker build -f docker/Dockerfile -t termix:dev --no-cache . && docker run -d --name termix-dev -p 3000:3000 -p 8080:8080 -p 30001-30006:30001-30006 -v \"%cd%\\db\\data:/app/data\" termix:dev",
"dev:docker:restart": "docker stop termix-dev 2>nul & docker rm termix-dev 2>nul & docker run -d --name termix-dev -p 8080:8080 -p 30001-30006:30001-30006 -v \"%cd%\\db\\data:/app/data\" termix:dev",
"generate:openapi": "tsc -p tsconfig.node.json && node ./dist/backend/backend/swagger.js",
"preview": "vite preview",
"electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && electron .\"",
@@ -82,6 +84,7 @@
"dotenv": "^17.2.0",
"drizzle-orm": "^0.44.3",
"express": "^5.1.0",
"https-proxy-agent": "^7.0.6",
"i18n-auto-translation": "^2.2.3",
"i18next": "^25.4.2",
"i18next-browser-languagedetector": "^8.2.0",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 73 KiB

+29 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 483 B

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 986 B

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

+7 -7
View File
@@ -6,31 +6,31 @@
"background_color": "#09090b",
"display": "standalone",
"orientation": "any",
"scope": "/",
"start_url": "/",
"scope": "./",
"start_url": "./",
"icons": [
{
"src": "/icons/48x48.png",
"src": "./icons/48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "/icons/64x64.png",
"src": "./icons/64x64.png",
"sizes": "64x64",
"type": "image/png"
},
{
"src": "/icons/128x128.png",
"src": "./icons/128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/256x256.png",
"src": "./icons/256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "/icons/512x512.png",
"src": "./icons/512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
+12 -11
View File
@@ -60,21 +60,22 @@ self.addEventListener("fetch", (event) => {
if (request.mode === "navigate") {
event.respondWith(
fetch(request)
.then((response) => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
return response;
})
.catch(() => {
return caches.match("/index.html");
}),
fetch(request).catch(() => {
return caches.match("/index.html");
}),
);
return;
}
const isStaticAsset = STATIC_ASSETS.some((asset) => {
if (asset === "/") return url.pathname === "/";
return url.pathname === asset || url.pathname.startsWith("/assets/");
});
if (!isStaticAsset) {
return;
}
event.respondWith(
caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

+4 -6
View File
@@ -27,12 +27,7 @@ app.use(
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const allowedOrigins = [
"http://localhost:5173",
"http://localhost:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
];
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
@@ -90,6 +85,9 @@ app.use(authManager.createAuthMiddleware());
*/
app.get("/uptime", async (req, res) => {
try {
const startTime = Date.now();
const userId = (req as AuthenticatedRequest).userId;
const uptimeMs = Date.now() - serverStartTime;
const uptimeSeconds = Math.floor(uptimeMs / 1000);
const days = Math.floor(uptimeSeconds / 86400);
+91 -14
View File
@@ -25,6 +25,7 @@ import { UserDataExport } from "../utils/user-data-export.js";
import { AutoSSLSetup } from "../utils/auto-ssl-setup.js";
import { eq, and } from "drizzle-orm";
import { parseUserAgent } from "../utils/user-agent-parser.js";
import { getProxyAgent } from "../utils/proxy-agent.js";
import {
users,
sshData,
@@ -42,7 +43,7 @@ import type {
GitHubAPIResponse,
AuthenticatedRequest,
} from "../../types/index.js";
import { getDb } from "./db/index.js";
import { getDb, DatabaseSaveTrigger } from "./db/index.js";
import Database from "better-sqlite3";
const app = express();
@@ -57,12 +58,7 @@ app.use(
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const allowedOrigins = [
"http://localhost:5173",
"http://localhost:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
];
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
@@ -166,12 +162,14 @@ async function fetchGitHubAPI<T>(
}
try {
const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {
const url = `${GITHUB_API_BASE}${endpoint}`;
const response = await fetch(url, {
headers: {
Accept: "application/vnd.github+json",
"User-Agent": "TermixUpdateChecker/1.0",
"X-GitHub-Api-Version": "2022-11-28",
},
agent: getProxyAgent(url),
});
if (!response.ok) {
@@ -696,19 +694,39 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
tags TEXT,
pin INTEGER NOT NULL DEFAULT 0,
auth_type TEXT NOT NULL,
force_keyboard_interactive TEXT,
password TEXT,
key TEXT,
key_password TEXT,
key_type TEXT,
sudo_password TEXT,
autostart_password TEXT,
autostart_key TEXT,
autostart_key_password TEXT,
credential_id INTEGER,
override_credential_username INTEGER,
enable_terminal INTEGER NOT NULL DEFAULT 1,
enable_tunnel INTEGER NOT NULL DEFAULT 1,
tunnel_connections TEXT,
jump_hosts TEXT,
enable_file_manager INTEGER NOT NULL DEFAULT 1,
enable_docker INTEGER NOT NULL DEFAULT 0,
show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1,
show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0,
show_tunnel_in_sidebar INTEGER NOT NULL DEFAULT 0,
show_docker_in_sidebar INTEGER NOT NULL DEFAULT 0,
show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0,
default_path TEXT,
stats_config TEXT,
terminal_config TEXT,
quick_actions TEXT,
notes TEXT,
use_socks5 INTEGER,
socks5_host TEXT,
socks5_port INTEGER,
socks5_username TEXT,
socks5_password TEXT,
socks5_proxy_chain TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -808,8 +826,8 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
.from(sshData)
.where(eq(sshData.userId, userId));
const insertHost = exportDb.prepare(`
INSERT INTO ssh_data (id, user_id, name, ip, port, username, folder, tags, pin, auth_type, password, key, key_password, key_type, autostart_password, autostart_key, autostart_key_password, credential_id, enable_terminal, enable_tunnel, tunnel_connections, enable_file_manager, default_path, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO ssh_data (id, user_id, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, stats_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const host of sshHosts) {
@@ -830,19 +848,39 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.tags || null,
decrypted.pin ? 1 : 0,
decrypted.authType,
decrypted.forceKeyboardInteractive || null,
decrypted.password || null,
decrypted.key || null,
decrypted.key_password || null,
decrypted.keyType || null,
decrypted.sudoPassword || null,
decrypted.autostartPassword || null,
decrypted.autostartKey || null,
decrypted.autostartKeyPassword || null,
decrypted.credentialId || null,
decrypted.enableTerminal ? 1 : 0,
decrypted.enableTunnel ? 1 : 0,
Boolean(decrypted.overrideCredentialUsername) ? 1 : 0,
Boolean(decrypted.enableTerminal) ? 1 : 0,
Boolean(decrypted.enableTunnel) ? 1 : 0,
decrypted.tunnelConnections || null,
decrypted.enableFileManager ? 1 : 0,
decrypted.jumpHosts || null,
Boolean(decrypted.enableFileManager) ? 1 : 0,
Boolean(decrypted.enableDocker) ? 1 : 0,
Boolean(decrypted.showTerminalInSidebar) ? 1 : 0,
Boolean(decrypted.showFileManagerInSidebar) ? 1 : 0,
Boolean(decrypted.showTunnelInSidebar) ? 1 : 0,
Boolean(decrypted.showDockerInSidebar) ? 1 : 0,
Boolean(decrypted.showServerStatsInSidebar) ? 1 : 0,
decrypted.defaultPath || null,
decrypted.statsConfig || null,
decrypted.terminalConfig || null,
decrypted.quickActions || null,
decrypted.notes || null,
Boolean(decrypted.useSocks5) ? 1 : 0,
decrypted.socks5Host || null,
decrypted.socks5Port || null,
decrypted.socks5Username || null,
decrypted.socks5Password || null,
decrypted.socks5ProxyChain || null,
decrypted.createdAt,
decrypted.updatedAt,
);
@@ -1231,19 +1269,45 @@ app.post(
tags: host.tags,
pin: Boolean(host.pin),
authType: host.auth_type,
forceKeyboardInteractive: host.force_keyboard_interactive,
password: host.password,
key: host.key,
keyPassword: host.key_password,
keyType: host.key_type,
sudoPassword: host.sudo_password,
autostartPassword: host.autostart_password,
autostartKey: host.autostart_key,
autostartKeyPassword: host.autostart_key_password,
credentialId: null,
credentialId: host.credential_id || null,
overrideCredentialUsername: Boolean(
host.override_credential_username,
),
enableTerminal: Boolean(host.enable_terminal),
enableTunnel: Boolean(host.enable_tunnel),
tunnelConnections: host.tunnel_connections,
jumpHosts: host.jump_hosts,
enableFileManager: Boolean(host.enable_file_manager),
enableDocker: Boolean(host.enable_docker),
showTerminalInSidebar: Boolean(host.show_terminal_in_sidebar),
showFileManagerInSidebar: Boolean(
host.show_file_manager_in_sidebar,
),
showTunnelInSidebar: Boolean(host.show_tunnel_in_sidebar),
showDockerInSidebar: Boolean(host.show_docker_in_sidebar),
showServerStatsInSidebar: Boolean(
host.show_server_stats_in_sidebar,
),
defaultPath: host.default_path,
statsConfig: host.stats_config,
terminalConfig: host.terminal_config,
quickActions: host.quick_actions,
notes: host.notes,
useSocks5: Boolean(host.use_socks5),
socks5Host: host.socks5_host,
socks5Port: host.socks5_port,
socks5Username: host.socks5_username,
socks5Password: host.socks5_password,
socks5ProxyChain: host.socks5_proxy_chain,
createdAt: host.created_at || new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
@@ -1483,6 +1547,19 @@ app.post(
}
result.success = true;
try {
await DatabaseSaveTrigger.forceSave("database_import");
} catch (saveError) {
apiLogger.error(
"Failed to persist imported data to disk",
saveError,
{
operation: "import_force_save_failed",
userId,
},
);
}
} finally {
if (importDb) {
importDb.close();
+105 -3
View File
@@ -270,7 +270,7 @@ async function initializeCompleteDatabase(): Promise<void> {
folder TEXT,
tags TEXT,
auth_type TEXT NOT NULL,
username TEXT NOT NULL,
username TEXT,
password TEXT,
key TEXT,
key_password TEXT,
@@ -414,9 +414,11 @@ async function initializeCompleteDatabase(): Promise<void> {
`);
try {
sqlite.prepare("DELETE FROM sessions").run();
sqlite
.prepare("DELETE FROM sessions WHERE expires_at < datetime('now')")
.run();
} catch (e) {
databaseLogger.warn("Could not clear sessions on startup", {
databaseLogger.warn("Could not clear expired sessions on startup", {
operation: "db_init_session_cleanup_failed",
error: e,
});
@@ -585,6 +587,13 @@ const migrateSchema = () => {
addColumnIfNotExists("ssh_data", "socks5_password", "TEXT");
addColumnIfNotExists("ssh_data", "socks5_proxy_chain", "TEXT");
addColumnIfNotExists("ssh_data", "host_key_fingerprint", "TEXT");
addColumnIfNotExists("ssh_data", "host_key_type", "TEXT");
addColumnIfNotExists("ssh_data", "host_key_algorithm", "TEXT DEFAULT 'sha256'");
addColumnIfNotExists("ssh_data", "host_key_first_seen", "TEXT");
addColumnIfNotExists("ssh_data", "host_key_last_verified", "TEXT");
addColumnIfNotExists("ssh_data", "host_key_changed_count", "INTEGER DEFAULT 0");
addColumnIfNotExists(
"ssh_data",
"show_terminal_in_sidebar",
@@ -619,6 +628,68 @@ const migrateSchema = () => {
addColumnIfNotExists("ssh_credentials", "system_key", "TEXT");
addColumnIfNotExists("ssh_credentials", "system_key_password", "TEXT");
try {
const tableInfo = sqlite.prepare("PRAGMA table_info(ssh_credentials)").all() as Array<{
cid: number;
name: string;
type: string;
notnull: number;
dflt_value: string | null;
pk: number;
}>;
const usernameCol = tableInfo.find((col) => col.name === "username");
if (usernameCol && usernameCol.notnull === 1) {
const tempTableName = "ssh_credentials_temp_migration";
const allColumns = tableInfo.map((col) => col.name).join(", ");
sqlite.exec(`PRAGMA foreign_keys = OFF`);
sqlite.exec(`
CREATE TABLE ${tempTableName} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
folder TEXT,
tags TEXT,
auth_type TEXT NOT NULL,
username TEXT,
password TEXT,
key TEXT,
key_password TEXT,
key_type TEXT,
usage_count INTEGER NOT NULL DEFAULT 0,
last_used TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
private_key TEXT,
public_key TEXT,
detected_key_type TEXT,
system_password TEXT,
system_key TEXT,
system_key_password TEXT,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
INSERT INTO ${tempTableName} SELECT ${allColumns} FROM ssh_credentials;
DROP TABLE ssh_credentials;
ALTER TABLE ${tempTableName} RENAME TO ssh_credentials;
`);
sqlite.exec(`PRAGMA foreign_keys = ON`);
databaseLogger.info("Successfully migrated ssh_credentials table to remove username NOT NULL constraint", {
operation: "schema_migration_username_nullable",
});
}
} catch (migrationError) {
databaseLogger.warn("Failed to migrate ssh_credentials username column", {
operation: "schema_migration",
error: migrationError,
});
}
addColumnIfNotExists("file_manager_recent", "host_id", "INTEGER NOT NULL");
addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL");
addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL");
@@ -925,6 +996,37 @@ const migrateSchema = () => {
}
}
try {
sqlite.prepare("SELECT id FROM opkssh_tokens LIMIT 1").get();
} catch {
try {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS opkssh_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
ssh_cert TEXT NOT NULL,
private_key TEXT NOT NULL,
email TEXT,
sub TEXT,
issuer TEXT,
audience TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT NOT NULL,
last_used TEXT,
UNIQUE(user_id, host_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
`);
} catch (createError) {
databaseLogger.warn("Failed to create opkssh_tokens table", {
operation: "schema_migration",
error: createError,
});
}
}
try {
const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>;
+32 -1
View File
@@ -118,6 +118,13 @@ export const sshData = sqliteTable("ssh_data", {
socks5Password: text("socks5_password"),
socks5ProxyChain: text("socks5_proxy_chain"),
hostKeyFingerprint: text("host_key_fingerprint"),
hostKeyType: text("host_key_type"),
hostKeyAlgorithm: text("host_key_algorithm").default("sha256"),
hostKeyFirstSeen: text("host_key_first_seen"),
hostKeyLastVerified: text("host_key_last_verified"),
hostKeyChangedCount: integer("host_key_changed_count").default(0),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
@@ -192,7 +199,7 @@ export const sshCredentials = sqliteTable("ssh_credentials", {
folder: text("folder"),
tags: text("tags"),
authType: text("auth_type").notNull(),
username: text("username").notNull(),
username: text("username"),
password: text("password"),
key: text("key", { length: 16384 }),
private_key: text("private_key", { length: 16384 }),
@@ -491,3 +498,27 @@ export const sessionRecordings = sqliteTable("session_recordings", {
.default(false),
terminationReason: text("termination_reason"),
});
export const opksshTokens = sqliteTable("opkssh_tokens", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
hostId: integer("host_id")
.notNull()
.references(() => sshData.id, { onDelete: "cascade" }),
sshCert: text("ssh_cert", { length: 8192 }).notNull(),
privateKey: text("private_key", { length: 8192 }).notNull(),
email: text("email"),
sub: text("sub"),
issuer: text("issuer"),
audience: text("audience"),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
expiresAt: text("expires_at").notNull(),
lastUsed: text("last_used"),
});
+2
View File
@@ -10,6 +10,7 @@ import { eq, and } from "drizzle-orm";
import fetch from "node-fetch";
import { authLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import { getProxyAgent } from "../../utils/proxy-agent.js";
class AlertCache {
private cache: Map<string, CacheEntry> = new Map();
@@ -60,6 +61,7 @@ async function fetchAlertsFromGitHub(): Promise<TermixAlert[]> {
Accept: "application/json",
"User-Agent": "TermixAlertChecker/1.0",
},
agent: getProxyAgent(url),
});
if (!response.ok) {
+27 -33
View File
@@ -149,18 +149,13 @@ router.post(
keyType,
} = req.body;
if (
!isNonEmptyString(userId) ||
!isNonEmptyString(name) ||
!isNonEmptyString(username)
) {
if (!isNonEmptyString(userId) || !isNonEmptyString(name)) {
authLogger.warn("Invalid credential creation data validation failed", {
operation: "credential_create",
userId,
hasName: !!name,
hasUsername: !!username,
});
return res.status(400).json({ error: "Name and username are required" });
return res.status(400).json({ error: "Name is required" });
}
if (!["password", "key"].includes(authType)) {
@@ -227,7 +222,7 @@ router.post(
folder: folder?.trim() || null,
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
authType,
username: username.trim(),
username: username?.trim() || null,
password: plainPassword,
key: plainKey,
private_key: keyInfo?.privateKey || plainKey,
@@ -508,6 +503,12 @@ router.put(
authLogger.warn("Invalid request for credential update");
return res.status(400).json({ error: "Invalid request" });
}
authLogger.info("Updating SSH credential", {
operation: "credential_update",
userId,
credentialId: parseInt(id),
changes: Object.keys(updateData),
});
try {
const existing = await db
@@ -538,7 +539,7 @@ router.put(
: updateData.tags || "";
}
if (updateData.username !== undefined)
updateFields.username = updateData.username.trim();
updateFields.username = updateData.username?.trim() || null;
if (updateData.authType !== undefined)
updateFields.authType = updateData.authType;
if (updateData.keyType !== undefined)
@@ -614,17 +615,11 @@ router.put(
);
const credential = updated[0];
authLogger.success(
`SSH credential updated: ${credential.name} (${credential.authType}) by user ${userId}`,
{
operation: "credential_update_success",
userId,
credentialId: parseInt(id),
name: credential.name,
authType: credential.authType,
username: credential.username,
},
);
authLogger.success("SSH credential updated", {
operation: "credential_update_success",
userId,
credentialId: parseInt(id),
});
res.json(formatCredentialOutput(updated[0]));
} catch (err) {
@@ -673,6 +668,11 @@ router.delete(
authLogger.warn("Invalid request for credential deletion");
return res.status(400).json({ error: "Invalid request" });
}
authLogger.info("Deleting SSH credential", {
operation: "credential_delete",
userId,
credentialId: parseInt(id),
});
try {
const credentialToDelete = await db
@@ -752,17 +752,11 @@ router.delete(
);
const credential = credentialToDelete[0];
authLogger.success(
`SSH credential deleted: ${credential.name} (${credential.authType}) by user ${userId}`,
{
operation: "credential_delete_success",
userId,
credentialId: parseInt(id),
name: credential.name,
authType: credential.authType,
username: credential.username,
},
);
authLogger.success("SSH credential deleted", {
operation: "credential_delete_success",
userId,
credentialId: parseInt(id),
});
res.json({ message: "Credential deleted successfully" });
} catch (err) {
@@ -846,7 +840,7 @@ router.post(
.update(sshData)
.set({
credentialId: parseInt(credentialId),
username: credential.username as string,
username: (credential.username as string) || "",
authType: (credential.auth_type || credential.authType) as string,
password: null,
key: null,
@@ -961,7 +955,7 @@ function formatCredentialOutput(
: []
: [],
authType: credential.authType || credential.auth_type,
username: credential.username,
username: credential.username || null,
publicKey: credential.public_key || credential.publicKey,
keyType: credential.key_type || credential.keyType,
detectedKeyType: credential.detected_key_type || credential.detectedKeyType,
@@ -4,6 +4,7 @@ import { getDb } from "../db/index.js";
import { networkTopology } from "../db/schema.js";
import { AuthManager } from "../../utils/auth-manager.js";
import type { AuthenticatedRequest } from "../../../types/index.js";
import { databaseLogger } from "../../utils/logger.js";
const router = express.Router();
const authManager = AuthManager.getInstance();
@@ -83,7 +84,10 @@ router.get(
return res.json(null);
}
} catch (error) {
console.error("Error fetching network topology:", error);
databaseLogger.error("Failed to fetch network topology", error, {
operation: "network_topology_fetch",
userId: (req as AuthenticatedRequest).userId,
});
return res.status(500).json({
error: "Failed to fetch network topology",
details: (error as Error).message,
@@ -176,7 +180,10 @@ router.post(
return res.json({ success: true });
} catch (error) {
console.error("Error saving network topology:", error);
databaseLogger.error("Failed to save network topology", error, {
operation: "network_topology_save",
userId: (req as AuthenticatedRequest).userId,
});
return res.status(500).json({
error: "Failed to save network topology",
details: (error as Error).message,
+38 -3
View File
@@ -117,10 +117,12 @@ router.post(
.limit(1);
if (host.length === 0) {
databaseLogger.warn("Attempt to share host not owned by user", {
operation: "share_host",
databaseLogger.warn("Permission denied", {
operation: "rbac_permission_denied",
userId,
hostId,
resource: "host",
resourceId: hostId,
action: "share",
});
return res.status(403).json({ error: "Not host owner" });
}
@@ -218,6 +220,13 @@ router.post(
userId,
);
}
databaseLogger.info("Permission granted", {
operation: "rbac_permission_grant",
adminId: userId,
hostId,
resource: "host",
action: "view",
});
return res.json({
success: true,
@@ -254,6 +263,13 @@ router.post(
userId,
);
}
databaseLogger.success("Host shared successfully", {
operation: "rbac_host_share_success",
userId,
hostId,
targetUserId: targetType === "user" ? targetUserId : undefined,
permissionLevel,
});
res.json({
success: true,
@@ -328,6 +344,12 @@ router.delete(
}
await db.delete(hostAccess).where(eq(hostAccess.id, accessId));
databaseLogger.info("Permission revoked", {
operation: "rbac_permission_revoke",
adminId: userId,
hostId,
accessId,
});
res.json({ success: true, message: "Access revoked" });
} catch (error) {
@@ -990,6 +1012,13 @@ router.post(
}
permissionManager.invalidateUserPermissionCache(targetUserId);
databaseLogger.info("Role assigned to user", {
operation: "rbac_role_assign",
adminId: currentUserId,
targetUserId,
roleId,
roleName: role[0].name,
});
res.json({
success: true,
@@ -1082,6 +1111,12 @@ router.delete(
);
permissionManager.invalidateUserPermissionCache(targetUserId);
databaseLogger.info("Role removed from user", {
operation: "rbac_role_remove",
adminId: req.userId!,
targetUserId,
roleId,
});
res.json({
success: true,
+13 -24
View File
@@ -4,7 +4,7 @@ import { db } from "../db/index.js";
import { snippets, snippetFolders } from "../db/schema.js";
import { eq, and, desc, asc, sql } from "drizzle-orm";
import type { Request, Response } from "express";
import { authLogger } from "../../utils/logger.js";
import { authLogger, databaseLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
const router = express.Router();
@@ -1043,9 +1043,8 @@ router.post(
};
const result = await db.insert(snippets).values(insertData).returning();
authLogger.success(`Snippet created: ${name} by user ${userId}`, {
operation: "snippet_create_success",
databaseLogger.info("Command snippet created", {
operation: "snippet_create",
userId,
snippetId: result[0].id,
name,
@@ -1156,16 +1155,11 @@ router.put(
.select()
.from(snippets)
.where(eq(snippets.id, parseInt(id)));
authLogger.success(
`Snippet updated: ${updated[0].name} by user ${userId}`,
{
operation: "snippet_update_success",
userId,
snippetId: parseInt(id),
name: updated[0].name,
},
);
databaseLogger.info("Command snippet updated", {
operation: "snippet_update",
userId,
snippetId: parseInt(id),
});
res.json(updated[0]);
} catch (err) {
@@ -1227,16 +1221,11 @@ router.delete(
await db
.delete(snippets)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
authLogger.success(
`Snippet deleted: ${existing[0].name} by user ${userId}`,
{
operation: "snippet_delete_success",
userId,
snippetId: parseInt(id),
name: existing[0].name,
},
);
databaseLogger.info("Command snippet deleted", {
operation: "snippet_delete",
userId,
snippetId: parseInt(id),
});
res.json({ success: true });
} catch (err) {
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -4,7 +4,7 @@ import { db } from "../db/index.js";
import { commandHistory } from "../db/schema.js";
import { eq, and, desc, sql } from "drizzle-orm";
import type { Request, Response } from "express";
import { authLogger } from "../../utils/logger.js";
import { authLogger, databaseLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
const router = express.Router();
@@ -269,9 +269,8 @@ router.delete(
eq(commandHistory.hostId, hostIdNum),
),
);
authLogger.success(`Command history cleared for host ${hostId}`, {
operation: "command_history_clear_success",
databaseLogger.info("Terminal history cleared", {
operation: "terminal_history_clear",
userId,
hostId: hostIdNum,
});
+152 -49
View File
@@ -40,6 +40,50 @@ import { loginRateLimiter } from "../../utils/login-rate-limiter.js";
const authManager = AuthManager.getInstance();
/**
* Get OIDC configuration from environment variables.
* Returns null if required env vars are not set.
*/
function getOIDCConfigFromEnv(): {
client_id: string;
client_secret: string;
issuer_url: string;
authorization_url: string;
token_url: string;
userinfo_url: string;
identifier_path: string;
name_path: string;
scopes: string;
} | null {
const client_id = process.env.OIDC_CLIENT_ID;
const client_secret = process.env.OIDC_CLIENT_SECRET;
const issuer_url = process.env.OIDC_ISSUER_URL;
const authorization_url = process.env.OIDC_AUTHORIZATION_URL;
const token_url = process.env.OIDC_TOKEN_URL;
if (
!client_id ||
!client_secret ||
!issuer_url ||
!authorization_url ||
!token_url
) {
return null;
}
return {
client_id,
client_secret,
issuer_url,
authorization_url,
token_url,
userinfo_url: process.env.OIDC_USERINFO_URL || "",
identifier_path: process.env.OIDC_IDENTIFIER_PATH || "sub",
name_path: process.env.OIDC_NAME_PATH || "name",
scopes: process.env.OIDC_SCOPES || "openid email profile",
};
}
async function verifyOIDCToken(
idToken: string,
issuerUrl: string,
@@ -256,6 +300,10 @@ router.post("/create", async (req, res) => {
}
const { username, password } = req.body;
authLogger.info("User registration attempt", {
operation: "user_register_attempt",
username,
});
if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
authLogger.warn(
@@ -277,9 +325,10 @@ router.post("/create", async (req, res) => {
.from(users)
.where(eq(users.username, username));
if (existing && existing.length > 0) {
authLogger.warn(`Attempt to create duplicate username: ${username}`, {
operation: "user_create",
authLogger.warn("Registration failed - username exists", {
operation: "user_register_failed",
username,
reason: "username_exists",
});
return res.status(409).json({ error: "Username already exists" });
}
@@ -367,15 +416,12 @@ router.post("/create", async (req, res) => {
});
}
authLogger.success(
`Traditional user created: ${username} (is_admin: ${isFirstUser})`,
{
operation: "user_create",
username,
isAdmin: isFirstUser,
userId: id,
},
);
authLogger.success("User registration successful", {
operation: "user_register_success",
userId: id,
username,
isAdmin: isFirstUser,
});
res.json({
message: "User created",
is_admin: isFirstUser,
@@ -589,6 +635,16 @@ router.delete("/oidc-config", authenticateJWT, async (req, res) => {
*/
router.get("/oidc-config", async (req, res) => {
try {
const envConfig = getOIDCConfigFromEnv();
if (envConfig) {
return res.json({
client_id: envConfig.client_id,
issuer_url: envConfig.issuer_url,
authorization_url: envConfig.authorization_url,
scopes: envConfig.scopes,
});
}
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
.get();
@@ -699,14 +755,20 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => {
*/
router.get("/oidc/authorize", async (req, res) => {
try {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
.get();
if (!row) {
return res.status(404).json({ error: "OIDC not configured" });
}
const envConfig = getOIDCConfigFromEnv();
let config;
const config = JSON.parse((row as Record<string, unknown>).value as string);
if (envConfig) {
config = envConfig;
} else {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
.get();
if (!row) {
return res.status(404).json({ error: "OIDC not configured" });
}
config = JSON.parse((row as Record<string, unknown>).value as string);
}
const state = nanoid();
const nonce = nanoid();
@@ -760,6 +822,10 @@ router.get("/oidc/authorize", async (req, res) => {
*/
router.get("/oidc/callback", async (req, res) => {
const { code, state } = req.query;
authLogger.info("OIDC login callback received", {
operation: "oidc_login_request",
state,
});
if (!isNonEmptyString(code) || !isNonEmptyString(state)) {
return res.status(400).json({ error: "Code and state are required" });
@@ -791,16 +857,22 @@ router.get("/oidc/callback", async (req, res) => {
.prepare("DELETE FROM settings WHERE key = ?")
.run(`oidc_redirect_${state}`);
const configRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
.get();
if (!configRow) {
return res.status(500).json({ error: "OIDC not configured" });
}
const envConfig = getOIDCConfigFromEnv();
let config;
const config = JSON.parse(
(configRow as Record<string, unknown>).value as string,
);
if (envConfig) {
config = envConfig;
} else {
const configRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
.get();
if (!configRow) {
return res.status(500).json({ error: "OIDC not configured" });
}
config = JSON.parse(
(configRow as Record<string, unknown>).value as string,
);
}
const tokenResponse = await fetch(config.token_url, {
method: "POST",
@@ -1121,11 +1193,10 @@ router.get("/oidc/callback", async (req, res) => {
deviceInfo: deviceInfo.deviceInfo,
});
authLogger.success("OIDC user authenticated", {
operation: "oidc_login_success",
authLogger.success("OIDC login successful", {
operation: "oidc_login_complete",
userId: userRecord.id,
deviceType: deviceInfo.type,
deviceInfo: deviceInfo.deviceInfo,
username: userRecord.username,
});
let frontendUrl = (redirectUri as string).replace(
@@ -1145,7 +1216,7 @@ router.get("/oidc/callback", async (req, res) => {
? 30 * 24 * 60 * 60 * 1000
: 7 * 24 * 60 * 60 * 1000;
res.clearCookie("jwt", authManager.getSecureCookieOptions(req));
res.clearCookie("jwt", authManager.getClearCookieOptions(req));
return res
.cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge))
@@ -1205,6 +1276,10 @@ router.get("/oidc/callback", async (req, res) => {
router.post("/login", async (req, res) => {
const { username, password } = req.body;
const clientIp = req.ip || req.socket.remoteAddress || "unknown";
authLogger.info("User login request received", {
operation: "user_login_request",
username,
});
if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
authLogger.warn("Invalid traditional login attempt", {
@@ -1361,14 +1436,12 @@ router.post("/login", async (req, res) => {
loginRateLimiter.resetAttempts(clientIp, username);
authLogger.success(`User logged in successfully: ${username}`, {
operation: "user_login_success",
username,
const payload = await authManager.verifyJWTToken(token);
authLogger.success("User login successful", {
operation: "user_login_complete",
userId: userRecord.id,
dataUnlocked: true,
deviceType: deviceInfo.type,
deviceInfo: deviceInfo.deviceInfo,
ip: clientIp,
username,
sessionId: payload?.sessionId,
});
const response: Record<string, unknown> = {
@@ -1439,7 +1512,7 @@ router.post("/logout", authenticateJWT, async (req, res) => {
}
return res
.clearCookie("jwt", authManager.getSecureCookieOptions(req))
.clearCookie("jwt", authManager.getClearCookieOptions(req))
.json({ success: true, message: "Logged out successfully" });
} catch (err) {
authLogger.error("Logout failed", err);
@@ -2375,6 +2448,10 @@ router.post("/complete-reset", async (req, res) => {
router.post("/change-password", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
const { oldPassword, newPassword } = req.body;
authLogger.info("Password change request", {
operation: "password_change_request",
userId,
});
if (!userId) {
return res.status(401).json({ error: "User not authenticated" });
@@ -2393,6 +2470,11 @@ router.post("/change-password", authenticateJWT, async (req, res) => {
const isMatch = await bcrypt.compare(oldPassword, user[0].password_hash);
if (!isMatch) {
authLogger.warn("Password change failed - old password incorrect", {
operation: "password_change_failed",
userId,
reason: "old_password_wrong",
});
return res.status(401).json({ error: "Incorrect current password" });
}
@@ -2412,6 +2494,10 @@ router.post("/change-password", authenticateJWT, async (req, res) => {
await db.update(users).set({ password_hash }).where(eq(users.id, userId));
authManager.logoutUser(userId);
authLogger.success("Password changed successfully", {
operation: "password_change_complete",
userId,
});
res.json({ message: "Password changed successfully. Please log in again." });
});
@@ -2527,9 +2613,12 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
});
}
authLogger.success(
`User ${username} made admin by ${adminUser[0].username}`,
);
authLogger.info("Admin privileges granted", {
operation: "admin_grant",
adminId: userId,
targetUserId: targetUser[0].id,
targetUsername: username,
});
res.json({ message: `User ${username} is now an admin` });
} catch (err) {
authLogger.error("Failed to make user admin", err);
@@ -2613,9 +2702,12 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => {
});
}
authLogger.success(
`Admin status removed from ${username} by ${adminUser[0].username}`,
);
authLogger.info("Admin privileges revoked", {
operation: "admin_revoke",
adminId: userId,
targetUserId: targetUser[0].id,
targetUsername: username,
});
res.json({ message: `Admin status removed from ${username}` });
} catch (err) {
authLogger.error("Failed to remove admin status", err);
@@ -2753,6 +2845,10 @@ router.post("/totp/enable", authenticateJWT, async (req, res) => {
totp_backup_codes: JSON.stringify(backupCodes),
})
.where(eq(users.id, userId));
authLogger.info("Two-factor authentication enabled", {
operation: "totp_enable",
userId,
});
res.json({
message: "TOTP enabled successfully",
@@ -2843,6 +2939,10 @@ router.post("/totp/disable", authenticateJWT, async (req, res) => {
totp_backup_codes: null,
})
.where(eq(users.id, userId));
authLogger.info("Two-factor authentication disabled", {
operation: "totp_disable",
userId,
});
res.json({ message: "TOTP disabled successfully" });
} catch (err) {
@@ -3205,9 +3305,12 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
await deleteUserAndRelatedData(targetUserId);
authLogger.success(
`User ${username} deleted by admin ${adminUser[0].username}`,
);
authLogger.warn("User account deleted by admin", {
operation: "admin_delete_user",
adminId: userId,
targetUserId,
targetUsername: username,
});
res.json({ message: `User ${username} deleted successfully` });
} catch (err) {
authLogger.error("Failed to delete user", err);
+11 -1
View File
@@ -1,6 +1,6 @@
import { Client } from "ssh2";
import type { WebSocket } from "ws";
import { sshLogger } from "../utils/logger.js";
import { sshLogger, authLogger } from "../utils/logger.js";
import { getDb } from "../database/db/index.js";
import { sshCredentials, sshData } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
@@ -210,6 +210,11 @@ export class SSHAuthManager {
operation: "ssh_keyboard_interactive_totp_retry",
hostId: this.context.hostId,
});
authLogger.warn("TOTP verification failed for SSH session", {
operation: "terminal_totp_failed",
userId: this.context.userId,
hostId: this.context.hostId,
});
this.sendLog("auth", "warning", "Invalid TOTP code");
@@ -262,6 +267,11 @@ export class SSHAuthManager {
}, 180000);
this.sendLog("auth", "info", "TOTP verification required");
authLogger.info("TOTP verification prompt sent to client", {
operation: "terminal_totp_prompt",
userId: this.context.userId,
hostId: this.context.hostId,
});
this.context.ws.send(
JSON.stringify({
+41 -17
View File
@@ -9,7 +9,7 @@ import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { systemLogger } from "../utils/logger.js";
import type { SSHHost } from "../../types/index.js";
const dockerConsoleLogger = systemLogger;
const sshLogger = systemLogger;
interface SSHSession {
client: SSHClient;
@@ -17,6 +17,7 @@ interface SSHSession {
isConnected: boolean;
containerId?: string;
shell?: string;
hostId?: number;
}
const activeSessions = new Map<string, SSHSession>();
@@ -121,7 +122,7 @@ async function createJumpHostChain(
try {
jumpHost.jumpHosts = JSON.parse(jumpHost.jumpHosts);
} catch (e) {
dockerConsoleLogger.error("Failed to parse jump hosts", e, {
sshLogger.error("Failed to parse jump hosts", e, {
hostId: jumpHost.id,
});
jumpHost.jumpHosts = [];
@@ -226,6 +227,11 @@ async function createJumpHostChain(
wss.on("connection", async (ws: WebSocket, req) => {
const userId = (req as any).userId;
const sessionId = `docker-console-${Date.now()}-${Math.random()}`;
sshLogger.info("Docker console WebSocket connected", {
operation: "docker_console_connect",
sessionId,
userId,
});
let sshSession: SSHSession | null = null;
@@ -251,7 +257,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
try {
hostConfig.jumpHosts = JSON.parse(hostConfig.jumpHosts);
} catch (e) {
dockerConsoleLogger.error("Failed to parse jump hosts", e, {
sshLogger.error("Failed to parse jump hosts", e, {
hostId: hostConfig.id,
});
hostConfig.jumpHosts = [];
@@ -383,6 +389,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
stream: null,
isConnected: true,
containerId,
hostId: hostConfig.id,
};
activeSessions.set(sessionId, sshSession);
@@ -417,7 +424,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
);
});
} catch {
dockerConsoleLogger.warn(
sshLogger.warn(
`Requested shell ${shell} not found, detecting available shell`,
{
operation: "shell_validation",
@@ -435,6 +442,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshSession.shell = shellToUse;
const execCommand = `docker exec -it ${containerId} /bin/${shellToUse}`;
sshLogger.info("Attaching to Docker container", {
operation: "docker_attach",
sessionId,
userId,
hostId: hostConfig.id,
containerId,
});
client.exec(
execCommand,
@@ -447,15 +461,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
},
(err, stream) => {
if (err) {
dockerConsoleLogger.error(
"Failed to create docker exec",
err,
{
operation: "docker_exec",
sessionId,
containerId,
},
);
sshLogger.error("Failed to create docker exec", err, {
operation: "docker_exec",
sessionId,
containerId,
});
ws.send(
JSON.stringify({
@@ -467,6 +477,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
sshSession!.stream = stream;
sshLogger.success("Docker container attached", {
operation: "docker_attach_success",
sessionId,
userId,
hostId: hostConfig.id,
containerId,
});
stream.on("data", (data: Buffer) => {
if (ws.readyState === WebSocket.OPEN) {
@@ -510,7 +527,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
},
);
} catch (error) {
dockerConsoleLogger.error("Failed to connect to container", error, {
sshLogger.error("Failed to connect to container", error, {
operation: "console_connect",
sessionId,
containerId: message.data.containerId,
@@ -570,13 +587,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
default:
dockerConsoleLogger.warn("Unknown message type", {
sshLogger.warn("Unknown message type", {
operation: "ws_message",
type: message.type,
});
}
} catch (error) {
dockerConsoleLogger.error("WebSocket message error", error, {
sshLogger.error("WebSocket message error", error, {
operation: "ws_message",
sessionId,
});
@@ -591,6 +608,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
});
ws.on("close", () => {
sshLogger.info("Docker console disconnected", {
operation: "docker_console_disconnect",
sessionId,
userId,
hostId: sshSession?.hostId,
containerId: sshSession?.containerId,
});
if (sshSession) {
if (sshSession.stream) {
sshSession.stream.end();
@@ -601,7 +625,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
});
ws.on("error", (error) => {
dockerConsoleLogger.error("WebSocket error", error, {
sshLogger.error("WebSocket error", error, {
operation: "ws_error",
sessionId,
});
+330 -86
View File
@@ -13,8 +13,9 @@ import { AuthManager } from "../utils/auth-manager.js";
import { createSocks5Connection } from "../utils/socks5-helper.js";
import type { AuthenticatedRequest, SSHHost } from "../../types/index.js";
import type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
const dockerLogger = logger;
const sshLogger = logger;
function createConnectionLog(
type: "info" | "success" | "warning" | "error",
@@ -79,7 +80,7 @@ function cleanupSession(sessionId: string) {
const session = sshSessions[sessionId];
if (session) {
if (session.activeOperations > 0) {
dockerLogger.warn(
sshLogger.warn(
`Deferring session cleanup for ${sessionId} - ${session.activeOperations} active operations`,
{
operation: "cleanup_deferred",
@@ -161,7 +162,7 @@ async function resolveJumpHost(
return host;
} catch (error) {
dockerLogger.error("Failed to resolve jump host", error, {
sshLogger.error("Failed to resolve jump host", error, {
operation: "resolve_jump_host",
hostId,
userId,
@@ -188,7 +189,7 @@ async function createJumpHostChain(
for (let i = 0; i < jumpHostConfigs.length; i++) {
if (!jumpHostConfigs[i]) {
dockerLogger.error(`Jump host ${i + 1} not found`, undefined, {
sshLogger.error(`Jump host ${i + 1} not found`, undefined, {
operation: "jump_host_chain",
hostId: jumpHosts[i].hostId,
});
@@ -203,6 +204,15 @@ async function createJumpHostChain(
const jumpClient = new SSHClient();
clients.push(jumpClient);
const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier(
jumpHostConfig.id,
jumpHostConfig.ip,
jumpHostConfig.port || 22,
null,
userId,
true,
);
const connected = await new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => {
resolve(false);
@@ -215,7 +225,7 @@ async function createJumpHostChain(
jumpClient.on("error", (err) => {
clearTimeout(timeout);
dockerLogger.error(`Jump host ${i + 1} connection failed`, err, {
sshLogger.error(`Jump host ${i + 1} connection failed`, err, {
operation: "jump_host_connect",
hostId: jumpHostConfig.id,
ip: jumpHostConfig.ip,
@@ -229,6 +239,7 @@ async function createJumpHostChain(
username: jumpHostConfig.username,
tryKeyboard: true,
readyTimeout: 30000,
hostVerifier: jumpHostVerifier,
};
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
@@ -275,7 +286,7 @@ async function createJumpHostChain(
return currentClient;
} catch (error) {
dockerLogger.error("Failed to create jump host chain", error, {
sshLogger.error("Failed to create jump host chain", error, {
operation: "jump_host_chain",
});
clients.forEach((c) => c.end());
@@ -286,13 +297,27 @@ async function createJumpHostChain(
async function executeDockerCommand(
session: SSHSession,
command: string,
sessionId?: string,
userId?: string,
hostId?: number,
): Promise<string> {
const startTime = Date.now();
sshLogger.info("Executing Docker command", {
operation: "docker_command_exec",
sessionId,
userId,
hostId,
command: command.split(" ")[1],
});
return new Promise((resolve, reject) => {
session.client.exec(command, (err, stream) => {
if (err) {
dockerLogger.error("Docker command execution error", err, {
sshLogger.error("Docker command execution error", err, {
operation: "execute_docker_command",
command,
sessionId,
userId,
hostId,
command: command.split(" ")[1],
});
return reject(err);
}
@@ -302,14 +327,25 @@ async function executeDockerCommand(
stream.on("close", (code: number) => {
if (code !== 0) {
dockerLogger.error("Docker command failed", undefined, {
sshLogger.error("Docker command failed", undefined, {
operation: "execute_docker_command",
command,
sessionId,
userId,
hostId,
command: command.split(" ")[1],
exitCode: code,
stderr,
});
reject(new Error(stderr || `Command exited with code ${code}`));
} else {
sshLogger.success("Docker command completed", {
operation: "docker_command_success",
sessionId,
userId,
hostId,
command: command.split(" ")[1],
duration: Date.now() - startTime,
});
resolve(stdout);
}
});
@@ -323,9 +359,12 @@ async function executeDockerCommand(
});
stream.on("error", (streamErr: Error) => {
dockerLogger.error("Docker command stream error", streamErr, {
sshLogger.error("Docker command stream error", streamErr, {
operation: "execute_docker_command",
command,
sessionId,
userId,
hostId,
command: command.split(" ")[1],
});
reject(streamErr);
});
@@ -350,12 +389,7 @@ app.use(
return callback(null, true);
}
const allowedOrigins = [
"http://localhost:5173",
"http://localhost:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
];
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
@@ -429,13 +463,10 @@ app.post("/docker/ssh/connect", async (req, res) => {
const connectionLogs: Array<Omit<LogEntry, "id" | "timestamp">> = [];
if (!userId) {
dockerLogger.error(
"Docker SSH connection rejected: no authenticated user",
{
operation: "docker_connect_auth",
sessionId,
},
);
sshLogger.error("Docker SSH connection rejected: no authenticated user", {
operation: "docker_connect_auth",
sessionId,
});
connectionLogs.push(
createConnectionLog(
"error",
@@ -460,7 +491,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
}
if (!sessionId || !hostId) {
dockerLogger.warn("Missing Docker SSH connection parameters", {
sshLogger.warn("Missing Docker SSH connection parameters", {
operation: "docker_connect",
sessionId,
hasHostId: !!hostId,
@@ -512,7 +543,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
);
if (!accessInfo.hasAccess) {
dockerLogger.warn("User does not have access to host", {
sshLogger.warn("User does not have access to host", {
operation: "docker_connect",
hostId,
userId,
@@ -531,7 +562,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
try {
host.jumpHosts = JSON.parse(host.jumpHosts);
} catch (e) {
dockerLogger.error("Failed to parse jump hosts", e, {
sshLogger.error("Failed to parse jump hosts", e, {
hostId: host.id,
});
host.jumpHosts = [];
@@ -539,7 +570,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
}
if (!host.enableDocker) {
dockerLogger.warn("Docker not enabled for host", {
sshLogger.warn("Docker not enabled for host", {
operation: "docker_connect",
hostId,
userId,
@@ -618,7 +649,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
};
}
} catch (error) {
dockerLogger.error("Failed to resolve shared credential", error, {
sshLogger.error("Failed to resolve shared credential", error, {
operation: "docker_connect",
hostId,
userId,
@@ -664,6 +695,14 @@ app.post("/docker/ssh/connect", async (req, res) => {
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
hostId,
host.ip,
host.port || 22,
null,
userId,
false,
),
};
if (resolvedCredentials.authType === "none") {
@@ -671,6 +710,94 @@ app.post("/docker/ssh/connect", async (req, res) => {
if (resolvedCredentials.password) {
config.password = resolvedCredentials.password;
}
} else if (resolvedCredentials.authType === "opkssh") {
try {
const { getOPKSSHToken } = await import("./opkssh-auth.js");
const token = await getOPKSSHToken(userId, hostId);
if (!token) {
connectionLogs.push(
createConnectionLog(
"error",
"docker_auth",
"OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.",
),
);
return res.status(401).json({
error:
"OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.",
requiresOPKSSHAuth: true,
connectionLogs,
});
}
const { promises: fs } = await import("fs");
const path = await import("path");
const os = await import("os");
const tempDir = os.tmpdir();
const keyPath = path.join(tempDir, `opkssh-docker-${userId}-${hostId}`);
const certPath = `${keyPath}-cert.pub`;
await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 });
await fs.writeFile(certPath, token.sshCert, { mode: 0o600 });
config.privateKey = await fs.readFile(keyPath);
connectionLogs.push(
createConnectionLog(
"info",
"docker_auth",
"Using OPKSSH certificate authentication",
),
);
setTimeout(async () => {
try {
const cleanupResults = await Promise.allSettled([
fs.unlink(keyPath),
fs.unlink(certPath),
]);
cleanupResults.forEach((result, index) => {
if (result.status === "rejected") {
sshLogger.warn(`Failed to cleanup OPKSSH temp file`, {
operation: "opkssh_temp_cleanup_failed",
file: index === 0 ? "keyPath" : "certPath",
sessionId,
error: result.reason,
});
}
});
} catch (error) {
sshLogger.error("Failed to cleanup OPKSSH temp files", {
operation: "opkssh_temp_cleanup_error",
sessionId,
error,
});
}
}, 60000);
} catch (opksshError) {
sshLogger.error("OPKSSH authentication error for Docker", {
operation: "docker_connect",
sessionId,
hostId,
error:
opksshError instanceof Error
? opksshError.message
: "Unknown error",
});
connectionLogs.push(
createConnectionLog(
"error",
"docker_auth",
`OPKSSH authentication failed: ${opksshError instanceof Error ? opksshError.message : "Unknown error"}`,
),
);
return res.status(500).json({
error: "OPKSSH authentication failed",
connectionLogs,
});
}
} else if (
resolvedCredentials.authType === "key" &&
resolvedCredentials.sshKey
@@ -680,7 +807,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
!resolvedCredentials.sshKey.includes("-----BEGIN") ||
!resolvedCredentials.sshKey.includes("-----END")
) {
dockerLogger.error("Invalid SSH key format", {
sshLogger.error("Invalid SSH key format", {
operation: "docker_connect",
sessionId,
hostId,
@@ -707,7 +834,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
config.passphrase = resolvedCredentials.keyPassword;
}
} catch (error) {
dockerLogger.error("SSH key processing error", error, {
sshLogger.error("SSH key processing error", error, {
operation: "docker_connect",
sessionId,
hostId,
@@ -725,14 +852,11 @@ app.post("/docker/ssh/connect", async (req, res) => {
});
}
} else if (resolvedCredentials.authType === "key") {
dockerLogger.error(
"SSH key authentication requested but no key provided",
{
operation: "docker_connect",
sessionId,
hostId,
},
);
sshLogger.error("SSH key authentication requested but no key provided", {
operation: "docker_connect",
sessionId,
hostId,
});
connectionLogs.push(
createConnectionLog(
"error",
@@ -814,7 +938,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
client.on("error", (err) => {
if (responseSent) {
dockerLogger.error(
sshLogger.error(
"Docker SSH connection error after response sent",
err,
{
@@ -832,7 +956,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
}
responseSent = true;
dockerLogger.error("Docker SSH connection failed", err, {
sshLogger.error("Docker SSH connection failed", err, {
operation: "docker_connect",
sessionId,
hostId,
@@ -888,6 +1012,15 @@ app.post("/docker/ssh/connect", async (req, res) => {
`Authentication failed: ${err.message}`,
),
);
} else if (err.message.includes("verification failed")) {
errorStage = "handshake";
connectionLogs.push(
createConnectionLog(
"error",
errorStage,
`SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.`,
),
);
} else {
connectionLogs.push(
createConnectionLog(
@@ -1177,7 +1310,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
return;
}
} catch (socks5Error) {
dockerLogger.error("SOCKS5 connection failed", socks5Error, {
sshLogger.error("SOCKS5 connection failed", socks5Error, {
operation: "docker_socks5_connect",
sessionId,
hostId,
@@ -1238,7 +1371,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
host.port || 22,
(err, stream) => {
if (err) {
dockerLogger.error("Failed to forward through jump host", err, {
sshLogger.error("Failed to forward through jump host", err, {
operation: "docker_jump_forward",
sessionId,
hostId,
@@ -1269,7 +1402,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
client.connect(config);
}
} catch (error) {
dockerLogger.error("Docker SSH connection error", error, {
sshLogger.error("Docker SSH connection error", error, {
operation: "docker_connect",
sessionId,
hostId,
@@ -1359,7 +1492,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
const userId = (req as any).userId;
if (!userId) {
dockerLogger.error("TOTP verification rejected: no authenticated user", {
sshLogger.error("TOTP verification rejected: no authenticated user", {
operation: "docker_totp_auth",
sessionId,
});
@@ -1373,7 +1506,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
const session = pendingTOTPSessions[sessionId];
if (!session) {
dockerLogger.warn("TOTP session not found or expired", {
sshLogger.warn("TOTP session not found or expired", {
operation: "docker_totp_verify",
sessionId,
userId,
@@ -1389,7 +1522,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
try {
session.client.end();
} catch {}
dockerLogger.warn("TOTP session timeout before code submission", {
sshLogger.warn("TOTP session timeout before code submission", {
operation: "docker_totp_verify",
sessionId,
userId,
@@ -1471,7 +1604,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
},
);
} catch (error) {
dockerLogger.warn("Failed to log Docker activity (TOTP)", {
sshLogger.warn("Failed to log Docker activity (TOTP)", {
operation: "activity_log_error",
userId: session.userId,
hostId: session.hostId,
@@ -1490,7 +1623,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
delete pendingTOTPSessions[sessionId];
dockerLogger.error("TOTP verification failed", {
sshLogger.error("TOTP verification failed", {
operation: "docker_totp_verify",
sessionId,
userId,
@@ -1504,7 +1637,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
if (!responseSent) {
responseSent = true;
delete pendingTOTPSessions[sessionId];
dockerLogger.warn("TOTP verification timeout", {
sshLogger.warn("TOTP verification timeout", {
operation: "docker_totp_verify",
sessionId,
userId,
@@ -1549,13 +1682,10 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
const userId = (req as any).userId;
if (!userId) {
dockerLogger.error(
"Warpgate verification rejected: no authenticated user",
{
operation: "docker_warpgate_auth",
sessionId,
},
);
sshLogger.error("Warpgate verification rejected: no authenticated user", {
operation: "docker_warpgate_auth",
sessionId,
});
return res.status(401).json({ error: "Authentication required" });
}
@@ -1566,7 +1696,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
const session = pendingTOTPSessions[sessionId];
if (!session) {
dockerLogger.warn("Warpgate session not found or expired", {
sshLogger.warn("Warpgate session not found or expired", {
operation: "docker_warpgate_verify",
sessionId,
userId,
@@ -1586,7 +1716,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
try {
session.client.end();
} catch {}
dockerLogger.warn("Warpgate session timeout before completion", {
sshLogger.warn("Warpgate session timeout before completion", {
operation: "docker_warpgate_verify",
sessionId,
userId,
@@ -1658,7 +1788,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
},
);
} catch (error) {
dockerLogger.warn("Failed to log Docker activity (Warpgate)", {
sshLogger.warn("Failed to log Docker activity (Warpgate)", {
operation: "activity_log_error",
userId: session.userId,
hostId: session.hostId,
@@ -1677,7 +1807,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
delete pendingTOTPSessions[sessionId];
dockerLogger.error("Warpgate verification failed", {
sshLogger.error("Warpgate verification failed", {
operation: "docker_warpgate_verify",
sessionId,
userId,
@@ -1693,7 +1823,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
if (!responseSent) {
responseSent = true;
delete pendingTOTPSessions[sessionId];
dockerLogger.warn("Warpgate verification timeout", {
sshLogger.warn("Warpgate verification timeout", {
operation: "docker_warpgate_verify",
sessionId,
userId,
@@ -1840,12 +1970,21 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
const versionOutput = await executeDockerCommand(
session,
"docker --version",
sessionId,
userId,
session.hostId,
);
const versionMatch = versionOutput.match(/Docker version ([^\s,]+)/);
const version = versionMatch ? versionMatch[1] : "unknown";
try {
await executeDockerCommand(session, "docker ps >/dev/null 2>&1");
await executeDockerCommand(
session,
"docker ps >/dev/null 2>&1",
sessionId,
userId,
session.hostId,
);
session.activeOperations--;
return res.json({
@@ -1892,7 +2031,7 @@ app.get("/docker/validate/:sessionId", async (req, res) => {
}
} catch (error) {
session.activeOperations--;
dockerLogger.error("Docker validation error", error, {
sshLogger.error("Docker validation error", error, {
operation: "docker_validate",
sessionId,
userId,
@@ -1962,7 +2101,13 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
const allFlag = all ? "-a " : "";
const command = `docker ps ${allFlag}--format '{"id":"{{.ID}}","name":"{{.Names}}","image":"{{.Image}}","status":"{{.Status}}","state":"{{.State}}","ports":"{{.Ports}}","created":"{{.CreatedAt}}"}'`;
const output = await executeDockerCommand(session, command);
const output = await executeDockerCommand(
session,
command,
sessionId,
userId,
session.hostId,
);
const containers = output
.split("\n")
@@ -1971,7 +2116,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
try {
return JSON.parse(line);
} catch (e) {
dockerLogger.warn("Failed to parse container line", {
sshLogger.warn("Failed to parse container line", {
operation: "parse_container",
line,
});
@@ -1985,7 +2130,7 @@ app.get("/docker/containers/:sessionId", async (req, res) => {
res.json(containers);
} catch (error) {
session.activeOperations--;
dockerLogger.error("Failed to list Docker containers", error, {
sshLogger.error("Failed to list Docker containers", error, {
operation: "list_containers",
sessionId,
userId,
@@ -2048,7 +2193,13 @@ app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
try {
const command = `docker inspect ${containerId}`;
const output = await executeDockerCommand(session, command);
const output = await executeDockerCommand(
session,
command,
sessionId,
userId,
session.hostId,
);
const details = JSON.parse(output);
session.activeOperations--;
@@ -2072,7 +2223,7 @@ app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
});
}
dockerLogger.error("Failed to get container details", error, {
sshLogger.error("Failed to get container details", error, {
operation: "get_container_details",
sessionId,
containerId,
@@ -2136,7 +2287,21 @@ app.post(
session.activeOperations++;
try {
await executeDockerCommand(session, `docker start ${containerId}`);
sshLogger.info("Docker container operation", {
operation: "docker_container_op",
sessionId,
userId,
hostId: session.hostId,
containerId,
action: "start",
});
await executeDockerCommand(
session,
`docker start ${containerId}`,
sessionId,
userId,
session.hostId,
);
session.activeOperations--;
@@ -2156,7 +2321,7 @@ app.post(
});
}
dockerLogger.error("Failed to start container", error, {
sshLogger.error("Failed to start container", error, {
operation: "start_container",
sessionId,
containerId,
@@ -2222,7 +2387,21 @@ app.post(
session.activeOperations++;
try {
await executeDockerCommand(session, `docker stop ${containerId}`);
sshLogger.info("Docker container operation", {
operation: "docker_container_op",
sessionId,
userId,
hostId: session.hostId,
containerId,
action: "stop",
});
await executeDockerCommand(
session,
`docker stop ${containerId}`,
sessionId,
userId,
session.hostId,
);
session.activeOperations--;
@@ -2242,7 +2421,7 @@ app.post(
});
}
dockerLogger.error("Failed to stop container", error, {
sshLogger.error("Failed to stop container", error, {
operation: "stop_container",
sessionId,
containerId,
@@ -2308,7 +2487,21 @@ app.post(
session.activeOperations++;
try {
await executeDockerCommand(session, `docker restart ${containerId}`);
sshLogger.info("Docker container operation", {
operation: "docker_container_op",
sessionId,
userId,
hostId: session.hostId,
containerId,
action: "restart",
});
await executeDockerCommand(
session,
`docker restart ${containerId}`,
sessionId,
userId,
session.hostId,
);
session.activeOperations--;
@@ -2328,7 +2521,7 @@ app.post(
});
}
dockerLogger.error("Failed to restart container", error, {
sshLogger.error("Failed to restart container", error, {
operation: "restart_container",
sessionId,
containerId,
@@ -2394,7 +2587,21 @@ app.post(
session.activeOperations++;
try {
await executeDockerCommand(session, `docker pause ${containerId}`);
sshLogger.info("Docker container operation", {
operation: "docker_container_op",
sessionId,
userId,
hostId: session.hostId,
containerId,
action: "pause",
});
await executeDockerCommand(
session,
`docker pause ${containerId}`,
sessionId,
userId,
session.hostId,
);
session.activeOperations--;
@@ -2414,7 +2621,7 @@ app.post(
});
}
dockerLogger.error("Failed to pause container", error, {
sshLogger.error("Failed to pause container", error, {
operation: "pause_container",
sessionId,
containerId,
@@ -2480,7 +2687,21 @@ app.post(
session.activeOperations++;
try {
await executeDockerCommand(session, `docker unpause ${containerId}`);
sshLogger.info("Docker container operation", {
operation: "docker_container_op",
sessionId,
userId,
hostId: session.hostId,
containerId,
action: "unpause",
});
await executeDockerCommand(
session,
`docker unpause ${containerId}`,
sessionId,
userId,
session.hostId,
);
session.activeOperations--;
@@ -2500,7 +2721,7 @@ app.post(
});
}
dockerLogger.error("Failed to unpause container", error, {
sshLogger.error("Failed to unpause container", error, {
operation: "unpause_container",
sessionId,
containerId,
@@ -2571,10 +2792,21 @@ app.delete(
session.activeOperations++;
try {
sshLogger.info("Docker container operation", {
operation: "docker_container_op",
sessionId,
userId,
hostId: session.hostId,
containerId,
action: "remove",
});
const forceFlag = force ? "-f " : "";
await executeDockerCommand(
session,
`docker rm ${forceFlag}${containerId}`,
sessionId,
userId,
session.hostId,
);
session.activeOperations--;
@@ -2604,7 +2836,7 @@ app.delete(
});
}
dockerLogger.error("Failed to remove container", error, {
sshLogger.error("Failed to remove container", error, {
operation: "remove_container",
sessionId,
containerId,
@@ -2706,7 +2938,13 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => {
command += ` --until ${until}`;
}
const logs = await executeDockerCommand(session, command);
const logs = await executeDockerCommand(
session,
command,
sessionId,
userId,
session.hostId,
);
session.activeOperations--;
@@ -2726,7 +2964,7 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => {
});
}
dockerLogger.error("Failed to get container logs", error, {
sshLogger.error("Failed to get container logs", error, {
operation: "get_logs",
sessionId,
containerId,
@@ -2793,7 +3031,13 @@ app.get(
try {
const command = `docker stats ${containerId} --no-stream --format '{"cpu":"{{.CPUPerc}}","memory":"{{.MemUsage}}","memoryPercent":"{{.MemPerc}}","netIO":"{{.NetIO}}","blockIO":"{{.BlockIO}}","pids":"{{.PIDs}}"}'`;
const output = await executeDockerCommand(session, command);
const output = await executeDockerCommand(
session,
command,
sessionId,
userId,
session.hostId,
);
const rawStats = JSON.parse(output.trim());
const memoryParts = rawStats.memory.split(" / ");
@@ -2835,7 +3079,7 @@ app.get(
});
}
dockerLogger.error("Failed to get container stats", error, {
sshLogger.error("Failed to get container stats", error, {
operation: "get_stats",
sessionId,
containerId,
@@ -2856,7 +3100,7 @@ app.listen(PORT, async () => {
try {
await authManager.initialize();
} catch (err) {
dockerLogger.error("Failed to initialize Docker backend", err, {
sshLogger.error("Failed to initialize Docker backend", err, {
operation: "startup",
});
}
+319 -16
View File
@@ -12,6 +12,7 @@ import { AuthManager } from "../utils/auth-manager.js";
import type { AuthenticatedRequest } from "../../types/index.js";
import { createSocks5Connection } from "../utils/socks5-helper.js";
import type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
function createConnectionLog(
type: "info" | "success" | "warning" | "error",
@@ -118,12 +119,7 @@ app.use(
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const allowedOrigins = [
"http://localhost:5173",
"http://localhost:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
];
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (origin.startsWith("https://")) {
return callback(null, true);
@@ -244,6 +240,15 @@ async function createJumpHostChain(
const jumpClient = new SSHClient();
clients.push(jumpClient);
const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier(
jumpHostConfig.id,
jumpHostConfig.ip,
jumpHostConfig.port || 22,
null,
userId,
true,
);
const connected = await new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => {
resolve(false);
@@ -270,6 +275,7 @@ async function createJumpHostChain(
username: jumpHostConfig.username,
tryKeyboard: true,
readyTimeout: 30000,
hostVerifier: jumpHostVerifier,
};
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
@@ -785,6 +791,14 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
hostId,
ip,
port,
null,
userId,
false,
),
env: {
TERM: "xterm-256color",
LANG: "en_US.UTF-8",
@@ -910,6 +924,92 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
connectionLogs.push(
createConnectionLog("info", "sftp_auth", "Using password authentication"),
);
} else if (resolvedCredentials.authType === "opkssh") {
try {
const { getOPKSSHToken } = await import("./opkssh-auth.js");
const token = await getOPKSSHToken(userId, hostId);
if (!token) {
connectionLogs.push(
createConnectionLog(
"error",
"sftp_auth",
"OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.",
),
);
return res.status(401).json({
error:
"OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours.",
requiresOPKSSHAuth: true,
connectionLogs,
});
}
const { promises: fs } = await import("fs");
const path = await import("path");
const os = await import("os");
const tempDir = os.tmpdir();
const keyPath = path.join(tempDir, `opkssh-fm-${userId}-${hostId}`);
const certPath = `${keyPath}-cert.pub`;
await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 });
await fs.writeFile(certPath, token.sshCert, { mode: 0o600 });
config.privateKey = await fs.readFile(keyPath);
connectionLogs.push(
createConnectionLog(
"info",
"sftp_auth",
"Using OPKSSH certificate authentication",
),
);
setTimeout(async () => {
try {
const cleanupResults = await Promise.allSettled([
fs.unlink(keyPath),
fs.unlink(certPath),
]);
cleanupResults.forEach((result, index) => {
if (result.status === "rejected") {
fileLogger.warn(`Failed to cleanup OPKSSH temp file`, {
operation: "opkssh_temp_cleanup_failed",
file: index === 0 ? "keyPath" : "certPath",
sessionId,
error: result.reason,
});
}
});
} catch (error) {
fileLogger.error("Failed to cleanup OPKSSH temp files", {
operation: "opkssh_temp_cleanup_error",
sessionId,
error,
});
}
}, 60000);
} catch (opksshError) {
fileLogger.error("OPKSSH authentication error for file manager", {
operation: "file_connect",
sessionId,
hostId,
error:
opksshError instanceof Error ? opksshError.message : "Unknown error",
});
connectionLogs.push(
createConnectionLog(
"error",
"sftp_auth",
`OPKSSH authentication failed: ${opksshError instanceof Error ? opksshError.message : "Unknown error"}`,
),
);
return res.status(500).json({
error: "OPKSSH authentication failed",
connectionLogs,
});
}
} else if (resolvedCredentials.authType === "none") {
connectionLogs.push(
createConnectionLog(
@@ -968,6 +1068,15 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
client.on("ready", () => {
if (responseSent) return;
responseSent = true;
fileLogger.info("File manager SSH connection established", {
operation: "file_ssh_connected",
sessionId,
userId,
hostId,
ip,
port,
username,
});
connectionLogs.push(
createConnectionLog(
"success",
@@ -1104,6 +1213,16 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
{ errorCode: (err as any).code, errorLevel: (err as any).level },
),
);
} else if (err.message.includes("verification failed")) {
errorStage = "handshake";
connectionLogs.push(
createConnectionLog(
"error",
errorStage,
`SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.`,
{ errorCode: (err as any).code, errorLevel: (err as any).level },
),
);
} else {
connectionLogs.push(
createConnectionLog(
@@ -1133,6 +1252,12 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
});
client.on("close", () => {
fileLogger.info("File manager SSH connection closed", {
operation: "file_ssh_disconnected",
sessionId,
userId,
hostId,
});
if (sshSessions[sessionId]) sshSessions[sessionId].isConnected = false;
cleanupSession(sessionId);
});
@@ -1892,6 +2017,12 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => {
*/
app.post("/ssh/file_manager/ssh/disconnect", (req, res) => {
const { sessionId } = req.body;
const userId = (req as AuthenticatedRequest).userId;
fileLogger.info("File manager disconnection requested", {
operation: "file_disconnect_request",
sessionId,
userId,
});
cleanupSession(sessionId);
res.json({ status: "success", message: "SSH connection disconnected" });
});
@@ -2037,6 +2168,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId];
const sshPath = decodeURIComponent((req.query.path as string) || "/");
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -2048,9 +2180,14 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
sshConn.lastActive = Date.now();
sshConn.activeOperations++;
const trySFTP = () => {
try {
fileLogger.info("Opening SFTP channel", {
operation: "file_sftp_open",
sessionId,
userId,
path: sshPath,
});
sshConn.client.sftp((err, sftp) => {
if (err) {
fileLogger.warn(
@@ -2062,6 +2199,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
sftp.readdir(sshPath, (readdirErr, list) => {
if (readdirErr) {
sftp.end();
fileLogger.warn(
`SFTP readdir failed, trying fallback: ${readdirErr.message}`,
);
@@ -2115,11 +2253,24 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
}
if (symlinks.length === 0) {
sftp.end();
sshConn.activeOperations--;
return res.json({ files, path: sshPath });
}
let resolved = 0;
let responded = false;
const sendResponse = () => {
if (responded) return;
responded = true;
sftp.end();
sshConn.activeOperations--;
res.json({ files, path: sshPath });
};
const readlinkTimeout = setTimeout(sendResponse, 5000);
for (const link of symlinks) {
sftp.readlink(link.path, (linkErr, target) => {
resolved++;
@@ -2127,8 +2278,8 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
files[link.index].linkTarget = target;
}
if (resolved === symlinks.length) {
sshConn.activeOperations--;
res.json({ files, path: sshPath });
clearTimeout(readlinkTimeout);
sendResponse();
}
});
}
@@ -2502,6 +2653,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
const sessionId = req.query.sessionId as string;
const sshConn = sshSessions[sessionId];
const filePath = decodeURIComponent(req.query.path as string);
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -2515,6 +2667,12 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
return res.status(400).json({ error: "File path is required" });
}
fileLogger.info("Reading file", {
operation: "file_read",
sessionId,
userId,
path: filePath,
});
sshConn.lastActive = Date.now();
const MAX_READ_SIZE = 500 * 1024 * 1024;
@@ -2613,6 +2771,13 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
}
const isBinary = detectBinary(binaryData);
fileLogger.success("File read successfully", {
operation: "file_read_success",
sessionId,
userId,
path: filePath,
bytes: binaryData.length,
});
if (isBinary) {
const base64Content = binaryData.toString("base64");
@@ -2668,6 +2833,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
const { sessionId, path: filePath, content } = req.body;
const sshConn = sshSessions[sessionId];
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -2685,10 +2851,25 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
return res.status(400).json({ error: "File content is required" });
}
const contentLength =
typeof content === "string" ? content.length : Buffer.byteLength(content);
fileLogger.info("Writing file", {
operation: "file_write",
sessionId,
userId,
path: filePath,
bytes: contentLength,
});
sshConn.lastActive = Date.now();
const trySFTP = () => {
try {
fileLogger.info("Opening SFTP channel", {
operation: "file_sftp_open",
sessionId,
userId,
path: filePath,
});
sshConn.client.sftp((err, sftp) => {
if (err) {
fileLogger.warn(
@@ -2734,6 +2915,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
writeStream.on("error", (streamErr) => {
if (hasError || hasFinished) return;
hasError = true;
sftp.end();
fileLogger.warn(
`SFTP write failed, trying fallback method: ${streamErr.message}`,
);
@@ -2743,6 +2925,14 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
writeStream.on("finish", () => {
if (hasError || hasFinished) return;
hasFinished = true;
sftp.end();
fileLogger.success("File written successfully", {
operation: "file_write_success",
sessionId,
userId,
path: filePath,
bytes: fileBuffer.length,
});
if (!res.headersSent) {
res.json({
message: "File written successfully",
@@ -2755,6 +2945,14 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
writeStream.on("close", () => {
if (hasError || hasFinished) return;
hasFinished = true;
sftp.end();
fileLogger.success("File written successfully", {
operation: "file_write_success",
sessionId,
userId,
path: filePath,
bytes: fileBuffer.length,
});
if (!res.headersSent) {
res.json({
message: "File written successfully",
@@ -2913,6 +3111,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
const { sessionId, path: filePath, content, fileName } = req.body;
const sshConn = sshSessions[sessionId];
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -2938,9 +3137,23 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
const fullPath = filePath.endsWith("/")
? filePath + fileName
: filePath + "/" + fileName;
const uploadStartTime = Date.now();
fileLogger.info("File upload started", {
operation: "file_upload_start",
sessionId,
userId,
path: fullPath,
bytes: contentSize,
});
const trySFTP = () => {
try {
fileLogger.info("Opening SFTP channel", {
operation: "file_sftp_open",
sessionId,
userId,
path: fullPath,
});
sshConn.client.sftp((err, sftp) => {
if (err) {
fileLogger.warn(
@@ -2977,6 +3190,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
writeStream.on("error", (streamErr) => {
if (hasError || hasFinished) return;
hasError = true;
sftp.end();
fileLogger.warn(
`SFTP write failed, trying fallback method: ${streamErr.message}`,
{
@@ -2993,6 +3207,15 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
writeStream.on("finish", () => {
if (hasError || hasFinished) return;
hasFinished = true;
sftp.end();
fileLogger.success("File upload completed", {
operation: "file_upload_complete",
sessionId,
userId,
path: fullPath,
bytes: fileBuffer.length,
duration: Date.now() - uploadStartTime,
});
if (!res.headersSent) {
res.json({
message: "File uploaded successfully",
@@ -3005,6 +3228,15 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
writeStream.on("close", () => {
if (hasError || hasFinished) return;
hasFinished = true;
sftp.end();
fileLogger.success("File upload completed", {
operation: "file_upload_complete",
sessionId,
userId,
path: fullPath,
bytes: fileBuffer.length,
duration: Date.now() - uploadStartTime,
});
if (!res.headersSent) {
res.json({
message: "File uploaded successfully",
@@ -3376,6 +3608,7 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
const { sessionId, path: folderPath, folderName } = req.body;
const sshConn = sshSessions[sessionId];
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -3394,6 +3627,12 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
const fullPath = folderPath.endsWith("/")
? folderPath + folderName
: folderPath + "/" + folderName;
fileLogger.info("Creating directory", {
operation: "file_mkdir",
sessionId,
userId,
path: fullPath,
});
const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
const createCommand = `mkdir -p '${escapedPath}' && echo "SUCCESS" && exit 0`;
@@ -3430,6 +3669,12 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
stream.on("close", (code) => {
if (outputData.includes("SUCCESS")) {
fileLogger.success("Directory created successfully", {
operation: "file_mkdir_success",
sessionId,
userId,
path: fullPath,
});
if (!res.headersSent) {
res.json({
message: "Folder created successfully",
@@ -3456,6 +3701,12 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
return;
}
fileLogger.success("Directory created successfully", {
operation: "file_mkdir_success",
sessionId,
userId,
path: fullPath,
});
if (!res.headersSent) {
res.json({
message: "Folder created successfully",
@@ -3508,6 +3759,7 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
const { sessionId, path: itemPath, isDirectory } = req.body;
const sshConn = sshSessions[sessionId];
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -3521,6 +3773,13 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
return res.status(400).json({ error: "Item path is required" });
}
fileLogger.info("Deleting item", {
operation: "file_delete",
sessionId,
userId,
path: itemPath,
type: isDirectory ? "directory" : "file",
});
sshConn.lastActive = Date.now();
const escapedPath = itemPath.replace(/'/g, "'\"'\"'");
@@ -3598,6 +3857,12 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
}
if (outputData.includes("SUCCESS") || code === 0) {
fileLogger.success("Item deleted successfully", {
operation: "file_delete_success",
sessionId,
userId,
path: itemPath,
});
res.json({
message: "Item deleted successfully",
path: itemPath,
@@ -3663,6 +3928,7 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
const { sessionId, oldPath, newName } = req.body;
const sshConn = sshSessions[sessionId];
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -3682,6 +3948,13 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
const oldDir = oldPath.substring(0, oldPath.lastIndexOf("/") + 1);
const newPath = oldDir + newName;
fileLogger.info("Renaming item", {
operation: "file_rename",
sessionId,
userId,
from: oldPath,
to: newPath,
});
const escapedOldPath = oldPath.replace(/'/g, "'\"'\"'");
const escapedNewPath = newPath.replace(/'/g, "'\"'\"'");
@@ -3719,6 +3992,13 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
stream.on("close", (code) => {
if (outputData.includes("SUCCESS")) {
fileLogger.success("Item renamed successfully", {
operation: "file_rename_success",
sessionId,
userId,
from: oldPath,
to: newPath,
});
if (!res.headersSent) {
res.json({
message: "Item renamed successfully",
@@ -3746,6 +4026,13 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
return;
}
fileLogger.success("Item renamed successfully", {
operation: "file_rename_success",
sessionId,
userId,
from: oldPath,
to: newPath,
});
if (!res.headersSent) {
res.json({
message: "Item renamed successfully",
@@ -3959,6 +4246,7 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
*/
app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
const { sessionId, path: filePath, hostId, userId } = req.body;
const downloadStartTime = Date.now();
if (!sessionId || !filePath) {
fileLogger.warn("Missing download parameters", {
@@ -3969,6 +4257,13 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
return res.status(400).json({ error: "Missing download parameters" });
}
fileLogger.info("File download started", {
operation: "file_download_start",
sessionId,
userId,
path: filePath,
});
const sshConn = sshSessions[sessionId];
if (!sshConn || !sshConn.isConnected) {
fileLogger.warn("SSH session not found or not connected for download", {
@@ -3983,6 +4278,12 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
sshConn.lastActive = Date.now();
scheduleSessionCleanup(sessionId);
fileLogger.info("Opening SFTP channel", {
operation: "file_sftp_open",
sessionId,
userId,
path: filePath,
});
sshConn.client.sftp((err, sftp) => {
if (err) {
@@ -3992,6 +4293,7 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
sftp.stat(filePath, (statErr, stats) => {
if (statErr) {
sftp.end();
fileLogger.error("File stat failed for download:", statErr);
return res
.status(500)
@@ -4027,23 +4329,24 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
sftp.readFile(filePath, (readErr, data) => {
if (readErr) {
sftp.end();
fileLogger.error("File read failed for download:", readErr);
return res
.status(500)
.json({ error: `Failed to read file: ${readErr.message}` });
}
sftp.end();
const base64Content = data.toString("base64");
const fileName = filePath.split("/").pop() || "download";
fileLogger.success("File downloaded successfully", {
operation: "file_download",
fileLogger.success("File download completed", {
operation: "file_download_complete",
sessionId,
filePath,
fileName,
fileSize: stats.size,
hostId,
userId,
hostId,
path: filePath,
bytes: stats.size,
duration: Date.now() - downloadStartTime,
});
res.json({
+471
View File
@@ -0,0 +1,471 @@
import type { WebSocket } from "ws";
import { db } from "../database/db/index.js";
import { sshData } from "../database/db/schema.js";
import { eq } from "drizzle-orm";
import { sshLogger } from "../utils/logger.js";
import crypto from "crypto";
interface HostKeyVerificationData {
scenario: "new" | "changed";
ip: string;
port: number;
hostname?: string;
fingerprint: string;
oldFingerprint?: string;
keyType: string;
oldKeyType?: string;
algorithm: string;
}
interface VerificationResponse {
action: "accept" | "reject";
}
export class SSHHostKeyVerifier {
/**
* Creates a hostVerifier callback for ssh2 Client.connect()
*
* @param hostId - Database ID of the host (null for quick connect)
* @param ip - IP address or hostname
* @param port - SSH port
* @param ws - WebSocket for user prompts (null for non-interactive connections)
* @param userId - User ID for logging
* @param isJumpHost - If true, auto-accepts without prompting
* @returns async hostVerifier callback
*/
static async createHostVerifier(
hostId: number | null,
ip: string,
port: number,
ws: WebSocket | null,
userId: string,
isJumpHost: boolean = false,
): Promise<(hostkey: Buffer, verify: (valid: boolean) => void) => void> {
return (hostkey: Buffer, verify: (valid: boolean) => void): void => {
(async () => {
try {
const fingerprint = hostkey.toString("hex");
const keyType = this.getKeyType(hostkey);
const algorithm = "sha256";
if (!hostId) {
sshLogger.info(
"Host key verification skipped (no hostId - quick connect)",
{
operation: "host_key_skip",
ip,
port,
fingerprint,
keyType,
userId,
},
);
verify(true);
return;
}
const host = await db.query.sshData.findFirst({
where: eq(sshData.id, hostId),
});
if (!host) {
sshLogger.warn(
"Host not found in database during key verification",
{
operation: "host_key_no_host",
hostId,
ip,
port,
userId,
},
);
verify(true);
return;
}
if (!host.hostKeyFingerprint) {
if (isJumpHost) {
await this.storeHostKey(hostId, fingerprint, keyType, algorithm);
sshLogger.info("Jump host key auto-accepted and stored", {
operation: "host_key_stored",
hostId,
ip,
port,
fingerprint,
keyType,
userId,
isJumpHost: true,
});
verify(true);
return;
}
if (!ws) {
sshLogger.warn(
"No WebSocket available for host key verification prompt",
{
operation: "host_key_no_ws",
hostId,
ip,
port,
userId,
},
);
verify(true);
return;
}
const accepted = await this.promptUserForNewKey(
ws,
ip,
port,
host.name || undefined,
fingerprint,
keyType,
algorithm,
);
if (accepted) {
await this.storeHostKey(hostId, fingerprint, keyType, algorithm);
sshLogger.info("New host key accepted by user and stored", {
operation: "host_key_stored",
hostId,
ip,
port,
fingerprint,
keyType,
userId,
});
} else {
sshLogger.warn("User rejected new host key", {
operation: "host_key_rejected",
hostId,
ip,
port,
fingerprint,
keyType,
userId,
});
}
verify(accepted);
return;
}
if (host.hostKeyFingerprint === fingerprint) {
await db
.update(sshData)
.set({
hostKeyLastVerified: new Date().toISOString(),
})
.where(eq(sshData.id, hostId));
sshLogger.info("Host key verified successfully", {
operation: "host_key_verified",
hostId,
ip,
port,
fingerprint,
keyType,
userId,
});
verify(true);
return;
}
sshLogger.error("Host key mismatch detected - SECURITY WARNING", {
operation: "host_key_mismatch",
hostId,
ip,
port,
oldFingerprint: host.hostKeyFingerprint,
newFingerprint: fingerprint,
oldKeyType: host.hostKeyType,
newKeyType: keyType,
userId,
changeCount: host.hostKeyChangedCount || 0,
});
if (isJumpHost) {
await this.updateHostKey(
hostId,
fingerprint,
keyType,
algorithm,
host.hostKeyChangedCount || 0,
);
sshLogger.warn("Jump host key changed - auto-accepted", {
operation: "host_key_updated",
hostId,
ip,
port,
fingerprint,
keyType,
userId,
isJumpHost: true,
});
verify(true);
return;
}
if (!ws) {
sshLogger.error(
"Host key changed - please connect via Terminal to verify the new key",
{
operation: "host_key_no_ws_reject",
hostId,
ip,
port,
userId,
message:
"SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.",
},
);
verify(false);
return;
}
const accepted = await this.promptUserForChangedKey(
ws,
ip,
port,
host.name || undefined,
fingerprint,
host.hostKeyFingerprint,
keyType,
host.hostKeyType || "unknown",
algorithm,
);
if (accepted) {
await this.updateHostKey(
hostId,
fingerprint,
keyType,
algorithm,
host.hostKeyChangedCount || 0,
);
sshLogger.warn("Changed host key accepted by user", {
operation: "host_key_updated",
hostId,
ip,
port,
oldFingerprint: host.hostKeyFingerprint,
newFingerprint: fingerprint,
userId,
changeCount: (host.hostKeyChangedCount || 0) + 1,
});
} else {
sshLogger.error("User rejected changed host key", {
operation: "host_key_change_rejected",
hostId,
ip,
port,
userId,
});
}
verify(accepted);
} catch (error) {
sshLogger.error("Error in host key verification", error, {
operation: "host_key_error",
hostId,
ip,
port,
userId,
});
verify(false);
}
})();
};
}
private static async storeHostKey(
hostId: number,
fingerprint: string,
keyType: string,
algorithm: string,
): Promise<void> {
await db
.update(sshData)
.set({
hostKeyFingerprint: fingerprint,
hostKeyType: keyType,
hostKeyAlgorithm: algorithm,
hostKeyFirstSeen: new Date().toISOString(),
hostKeyLastVerified: new Date().toISOString(),
})
.where(eq(sshData.id, hostId));
}
private static async updateHostKey(
hostId: number,
fingerprint: string,
keyType: string,
algorithm: string,
currentChangeCount: number,
): Promise<void> {
await db
.update(sshData)
.set({
hostKeyFingerprint: fingerprint,
hostKeyType: keyType,
hostKeyAlgorithm: algorithm,
hostKeyLastVerified: new Date().toISOString(),
hostKeyChangedCount: currentChangeCount + 1,
})
.where(eq(sshData.id, hostId));
}
private static async promptUserForNewKey(
ws: WebSocket,
ip: string,
port: number,
hostname: string | undefined,
fingerprint: string,
keyType: string,
algorithm: string,
): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => {
ws.removeListener("message", messageHandler);
sshLogger.warn("Host key verification timeout (new key)", {
operation: "host_key_timeout",
ip,
port,
});
resolve(false);
}, 60000);
const messageHandler = (data: Buffer) => {
try {
const message = JSON.parse(data.toString());
if (message.type === "host_key_verification_response") {
clearTimeout(timeout);
ws.removeListener("message", messageHandler);
const response = message.data as VerificationResponse;
resolve(response.action === "accept");
}
} catch (error) {
sshLogger.error(
"Error parsing host key verification response",
error,
);
}
};
ws.on("message", messageHandler);
const verificationData: HostKeyVerificationData = {
scenario: "new",
ip,
port,
hostname,
fingerprint,
keyType,
algorithm,
};
ws.send(
JSON.stringify({
type: "host_key_verification_required",
data: verificationData,
}),
);
});
}
private static async promptUserForChangedKey(
ws: WebSocket,
ip: string,
port: number,
hostname: string | undefined,
fingerprint: string,
oldFingerprint: string,
keyType: string,
oldKeyType: string,
algorithm: string,
): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => {
ws.removeListener("message", messageHandler);
sshLogger.error("Host key verification timeout (changed key)", {
operation: "host_key_timeout",
ip,
port,
});
resolve(false);
}, 120000);
const messageHandler = (data: Buffer) => {
try {
const message = JSON.parse(data.toString());
if (message.type === "host_key_verification_response") {
clearTimeout(timeout);
ws.removeListener("message", messageHandler);
const response = message.data as VerificationResponse;
resolve(response.action === "accept");
}
} catch (error) {
sshLogger.error(
"Error parsing host key verification response",
error,
);
}
};
ws.on("message", messageHandler);
const verificationData: HostKeyVerificationData = {
scenario: "changed",
ip,
port,
hostname,
fingerprint,
oldFingerprint,
keyType,
oldKeyType,
algorithm,
};
ws.send(
JSON.stringify({
type: "host_key_changed",
data: verificationData,
}),
);
});
}
private static getKeyType(key: Buffer): string {
try {
if (key.length < 4) {
return "unknown";
}
const typeLength = key.readUInt32BE(0);
if (typeLength > key.length - 4 || typeLength > 256) {
return "unknown";
}
const keyType = key.toString("utf8", 4, 4 + typeLength);
if (
(keyType && keyType.startsWith("ssh-")) ||
keyType.startsWith("ecdsa-")
) {
return keyType;
}
return "unknown";
} catch (error) {
sshLogger.error("Error parsing SSH key type", error);
return "unknown";
}
}
}
+803
View File
@@ -0,0 +1,803 @@
import { spawn, ChildProcess } from "child_process";
import { randomUUID } from "crypto";
import { WebSocket } from "ws";
import { IncomingMessage } from "http";
import { OPKSSHBinaryManager } from "../utils/opkssh-binary-manager.js";
import { sshLogger } from "../utils/logger.js";
import { getDb } from "../database/db/index.js";
import { opksshTokens } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { UserCrypto } from "../utils/user-crypto.js";
import { FieldCrypto } from "../utils/field-crypto.js";
import { promises as fs } from "fs";
import path from "path";
import axios from "axios";
const AUTH_TIMEOUT = 60 * 1000;
interface OPKSSHAuthSession {
requestId: string;
userId: string;
hostId: number;
hostname: string;
process: ChildProcess;
localPort: number;
callbackPort: number;
remoteRedirectUri: string;
status:
| "starting"
| "waiting_for_auth"
| "authenticating"
| "completed"
| "error";
ws: WebSocket;
stdoutBuffer: string;
privateKeyBuffer: string;
sshCertBuffer: string;
identity: {
email?: string;
sub?: string;
issuer?: string;
audience?: string;
};
createdAt: Date;
approvalTimeout: NodeJS.Timeout;
cleanup: () => Promise<void>;
}
const activeAuthSessions = new Map<string, OPKSSHAuthSession>();
const cleanupInProgress = new Set<string>();
export function getRequestOrigin(req: IncomingMessage): string {
const protoHeader =
req.headers["x-forwarded-proto"] ||
((req.socket as any).encrypted ? "https" : "http");
const proto =
typeof protoHeader === "string"
? protoHeader.split(",")[0].trim()
: String(protoHeader);
const portHeader = req.headers["x-forwarded-port"];
const port =
typeof portHeader === "string"
? portHeader.split(",")[0].trim()
: undefined;
const hostHeaderRaw =
req.headers["x-forwarded-host"] || req.headers.host || "localhost";
const hostHeader =
typeof hostHeaderRaw === "string"
? hostHeaderRaw.split(",")[0].trim()
: String(hostHeaderRaw);
if (port) {
const hostWithoutPort = hostHeader.split(":")[0];
const isDefaultPort =
(proto === "http" && port === "80") ||
(proto === "https" && port === "443");
return isDefaultPort
? `${proto}://${hostWithoutPort}`
: `${proto}://${hostWithoutPort}:${port}`;
}
return `${proto}://${hostHeader}`;
}
function getOPKConfigPath(): string {
const dataDir =
process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
return path.join(dataDir, ".opk", "config.yml");
}
async function ensureOPKConfigDir(): Promise<void> {
const configPath = getOPKConfigPath();
const configDir = path.dirname(configPath);
await fs.mkdir(configDir, { recursive: true });
}
async function createTemplateConfig(): Promise<void> {
const configPath = getOPKConfigPath();
const template = `
# OPKSSH Configuration
# OPKSSH Documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md
# Termix Documentation: https://docs.termix.site/opkssh
`;
try {
await ensureOPKConfigDir();
await fs.writeFile(configPath, template, "utf8");
sshLogger.info(`Created template OPKSSH config at ${configPath}`);
} catch (error) {
sshLogger.warn("Failed to create template OPKSSH config", error);
}
}
async function checkOPKConfigExists(): Promise<{
exists: boolean;
error?: string;
configPath?: string;
}> {
const configPath = getOPKConfigPath();
const isDocker =
!!process.env.DATA_DIR && process.env.DATA_DIR.startsWith("/app");
const dockerHint = isDocker
? "\n\nDocker: Ensure /app/data is mounted as a volume with write permissions for node:node user."
: "";
try {
const content = await fs.readFile(configPath, "utf8");
if (!content.includes("providers:")) {
return {
exists: false,
configPath,
error: `OPKSSH configuration is missing 'providers' section. Please edit the config file at:\n${configPath}\n\n.`,
};
}
const lines = content.split("\n");
const hasUncommentedProvider = lines.some((line) => {
const trimmed = line.trim();
return (
trimmed.startsWith("- alias:") ||
(trimmed.startsWith("issuer:") && !line.trimStart().startsWith("#"))
);
});
if (!hasUncommentedProvider) {
return {
exists: false,
configPath,
error: `OPKSSH configuration has no active providers. Please edit the config file at:\n${configPath}\n\nUncomment and configure at least one OIDC provider.\nSee documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md${dockerHint}`,
};
}
if (!content.includes("redirect_uris:")) {
return {
exists: false,
configPath,
error: `OPKSSH configuration is missing 'redirect_uris' field.`,
};
}
return { exists: true, configPath };
} catch {
await createTemplateConfig();
return {
exists: false,
configPath,
error: `OPKSSH configuration not found. A template config file has been created at:\n${configPath}\n\nPlease edit this file and configure your OIDC provider (Google, GitHub, Microsoft, etc.).\nSee documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md${dockerHint}`,
};
}
}
export async function startOPKSSHAuth(
userId: string,
hostId: number,
hostname: string,
ws: WebSocket,
requestOrigin: string,
): Promise<string> {
try {
await ensureOPKConfigDir();
const configDir = path.dirname(getOPKConfigPath());
await fs.access(configDir, fs.constants.R_OK | fs.constants.W_OK);
} catch (error) {
sshLogger.error("OPKSSH directory not accessible", error);
const isDocker =
!!process.env.DATA_DIR && process.env.DATA_DIR.startsWith("/app");
const dockerHint = isDocker
? "\n\nDocker: Ensure /app/data is mounted as a volume with write permissions for node:node user."
: "";
ws.send(
JSON.stringify({
type: "opkssh_error",
error: `OPKSSH directory initialization failed: ${error.message}${dockerHint}`,
}),
);
return "";
}
const configCheck = await checkOPKConfigExists();
if (!configCheck.exists) {
ws.send(
JSON.stringify({
type: "opkssh_config_error",
requestId: "",
error: configCheck.error,
instructions: configCheck.error,
}),
);
return "";
}
const requestId = randomUUID();
const remoteRedirectUri = `${requestOrigin}/ssh/opkssh-callback`;
const session: Partial<OPKSSHAuthSession> = {
requestId,
userId,
hostId,
hostname,
localPort: 0,
callbackPort: 0,
remoteRedirectUri,
status: "starting",
ws,
stdoutBuffer: "",
privateKeyBuffer: "",
sshCertBuffer: "",
identity: {},
createdAt: new Date(),
};
try {
const binaryPath = OPKSSHBinaryManager.getBinaryPath();
const configPath = getOPKConfigPath();
const configDir = path.dirname(configPath);
const args = [
"login",
"--print-key",
`--config-path=${configPath}`,
`--remote-redirect-uri=${remoteRedirectUri}`,
];
const opksshProcess = spawn(binaryPath, args, {
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
},
});
session.process = opksshProcess;
const cleanup = async () => {
await cleanupAuthSession(requestId);
};
session.cleanup = cleanup;
const timeout = setTimeout(async () => {
sshLogger.warn(`OPKSSH auth timeout for session ${requestId}`);
ws.send(
JSON.stringify({
type: "opkssh_timeout",
requestId,
}),
);
await cleanup();
}, AUTH_TIMEOUT);
session.approvalTimeout = timeout;
ws.on("close", () => {
cleanup();
});
activeAuthSessions.set(requestId, session as OPKSSHAuthSession);
opksshProcess.stdout?.on("data", (data) => {
const output = data.toString();
handleOPKSSHOutput(requestId, output);
});
opksshProcess.stderr?.on("data", async (data) => {
const stderr = data.toString();
if (stderr.includes("Opening browser to")) {
handleOPKSSHOutput(requestId, stderr);
}
if (stderr.includes("listening on")) {
handleOPKSSHOutput(requestId, stderr);
}
if (stderr.includes("provider not found") || stderr.includes("config")) {
ws.send(
JSON.stringify({
type: "opkssh_config_error",
requestId,
error:
"OPKSSH configuration error. Please verify your config file contains valid OIDC provider settings.",
instructions:
"See documentation: https://github.com/openpubkey/opkssh/blob/main/docs/config.md",
}),
);
cleanup();
}
if (
stderr.includes("level=error") ||
stderr.includes("Error:") ||
stderr.includes("failed")
) {
const isXdgOpenError = stderr.includes('exec: "xdg-open"');
if (!isXdgOpenError) {
if (
stderr.includes("bind: address already in use") ||
stderr.includes("error logging in") ||
stderr.includes("failed to start")
) {
await cleanup();
}
}
}
});
opksshProcess.on("error", (error) => {
ws.send(
JSON.stringify({
type: "opkssh_error",
requestId,
error: `OPKSSH process error: ${error.message}`,
}),
);
cleanup();
});
opksshProcess.on("exit", (code) => {
if (code !== 0 && session.status !== "completed") {
ws.send(
JSON.stringify({
type: "opkssh_error",
requestId,
error: `OPKSSH process exited with code ${code}`,
}),
);
}
cleanup();
});
return requestId;
} catch (error) {
sshLogger.error(`Failed to start OPKSSH auth session`, error);
ws.send(
JSON.stringify({
type: "opkssh_error",
requestId,
error: `Failed to start OPKSSH authentication: ${error instanceof Error ? error.message : "Unknown error"}`,
}),
);
return "";
}
}
function handleOPKSSHOutput(requestId: string, output: string): void {
const session = activeAuthSessions.get(requestId);
if (!session) {
return;
}
session.stdoutBuffer += output;
const chooserUrlMatch = session.stdoutBuffer.match(
/Opening browser to http:\/\/localhost:(\d+)\/chooser/,
);
if (chooserUrlMatch && session.status === "starting") {
const actualPort = parseInt(chooserUrlMatch[1], 10);
const localChooserUrl = `http://localhost:${actualPort}/chooser`;
session.localPort = actualPort;
const baseUrl = session.remoteRedirectUri.replace(
/\/ssh\/opkssh-callback$/,
"",
);
const proxiedChooserUrl = `${baseUrl}/ssh/opkssh-chooser/${requestId}`;
session.status = "waiting_for_auth";
session.ws.send(
JSON.stringify({
type: "opkssh_status",
requestId,
stage: "chooser",
url: proxiedChooserUrl,
localUrl: localChooserUrl,
message: "Please authenticate in your browser",
}),
);
}
const callbackPortMatch = session.stdoutBuffer.match(
/listening on http:\/\/127\.0\.0\.1:(\d+)\//,
);
if (callbackPortMatch && !session.callbackPort) {
session.callbackPort = parseInt(callbackPortMatch[1], 10);
}
if (output.includes("BEGIN OPENSSH PRIVATE KEY")) {
session.status = "authenticating";
session.ws.send(
JSON.stringify({
type: "opkssh_status",
requestId,
stage: "authenticating",
message: "Processing authentication...",
}),
);
}
const privateKeyMatch = session.stdoutBuffer.match(
/(-----BEGIN OPENSSH PRIVATE KEY-----[\s\S]*?-----END OPENSSH PRIVATE KEY-----)/,
);
if (privateKeyMatch) {
session.privateKeyBuffer = privateKeyMatch[1].trim();
}
const certMatch = session.stdoutBuffer.match(
/(ecdsa-sha2-nistp256-cert-v01@openssh\.com\s+[A-Za-z0-9+/=]+|ssh-rsa-cert-v01@openssh\.com\s+[A-Za-z0-9+/=]+|ssh-ed25519-cert-v01@openssh\.com\s+[A-Za-z0-9+/=]+)/,
);
if (certMatch) {
session.sshCertBuffer = certMatch[1].trim();
}
const identityMatch = session.stdoutBuffer.match(
/Email, sub, issuer, audience:\s*\n?\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)/,
);
if (identityMatch) {
session.identity = {
email: identityMatch[1],
sub: identityMatch[2],
issuer: identityMatch[3],
audience: identityMatch[4],
};
}
if (session.privateKeyBuffer && session.sshCertBuffer) {
if (!session.privateKeyBuffer.includes("BEGIN OPENSSH PRIVATE KEY")) {
sshLogger.error(`Invalid private key extracted [${requestId}]`, {
bufferPrefix: session.privateKeyBuffer.substring(0, 50),
});
session.ws.send(
JSON.stringify({
type: "opkssh_error",
requestId,
error: "Failed to extract valid private key from OPKSSH output",
}),
);
return;
}
if (!session.sshCertBuffer.match(/-cert-v01@openssh\.com/)) {
sshLogger.error(`Invalid SSH certificate extracted [${requestId}]`, {
bufferPrefix: session.sshCertBuffer.substring(0, 50),
});
session.ws.send(
JSON.stringify({
type: "opkssh_error",
requestId,
error: "Failed to extract valid SSH certificate from OPKSSH output",
}),
);
return;
}
storeOPKSSHToken(session);
}
}
async function storeOPKSSHToken(session: OPKSSHAuthSession): Promise<void> {
try {
const db = getDb();
const userCrypto = UserCrypto.getInstance();
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24);
const userDataKey = userCrypto.getUserDataKey(session.userId);
if (!userDataKey) {
throw new Error("User data key not found");
}
const tokenId = `opkssh-${session.userId}-${session.hostId}`;
const encryptedCert = FieldCrypto.encryptField(
session.sshCertBuffer,
userDataKey,
tokenId,
"ssh_cert",
);
const encryptedKey = FieldCrypto.encryptField(
session.privateKeyBuffer,
userDataKey,
tokenId,
"private_key",
);
await db
.insert(opksshTokens)
.values({
userId: session.userId,
hostId: session.hostId,
sshCert: encryptedCert,
privateKey: encryptedKey,
email: session.identity.email,
sub: session.identity.sub,
issuer: session.identity.issuer,
audience: session.identity.audience,
expiresAt: expiresAt.toISOString(),
})
.onConflictDoUpdate({
target: [opksshTokens.userId, opksshTokens.hostId],
set: {
sshCert: encryptedCert,
privateKey: encryptedKey,
email: session.identity.email,
sub: session.identity.sub,
issuer: session.identity.issuer,
audience: session.identity.audience,
expiresAt: expiresAt.toISOString(),
createdAt: new Date().toISOString(),
},
});
session.status = "completed";
session.ws.send(
JSON.stringify({
type: "opkssh_completed",
requestId: session.requestId,
expiresAt: expiresAt.toISOString(),
}),
);
try {
await axios.post(
"http://localhost:30006/activity/log",
{
type: "opkssh_authentication",
hostId: session.hostId,
hostName: session.hostname,
status: "approved",
},
{
headers: {
Authorization: `Bearer ${process.env.INTERNAL_AUTH_TOKEN}`,
},
},
);
} catch (activityError) {
sshLogger.warn("Failed to log OPKSSH activity", activityError);
}
await session.cleanup();
} catch (error) {
sshLogger.error(
`Failed to store OPKSSH token for session ${session.requestId}`,
error,
);
session.ws.send(
JSON.stringify({
type: "opkssh_error",
requestId: session.requestId,
error: "Failed to store authentication token",
}),
);
await session.cleanup();
}
}
export async function getOPKSSHToken(
userId: string,
hostId: number,
): Promise<{ sshCert: string; privateKey: string } | null> {
try {
const db = getDb();
const token = await db
.select()
.from(opksshTokens)
.where(
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
)
.limit(1);
if (!token || token.length === 0) {
return null;
}
const tokenData = token[0];
const expiresAt = new Date(tokenData.expiresAt);
if (expiresAt < new Date()) {
await db
.delete(opksshTokens)
.where(
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
);
return null;
}
const userCrypto = UserCrypto.getInstance();
const userDataKey = userCrypto.getUserDataKey(userId);
if (!userDataKey) {
throw new Error("User data key not found");
}
const tokenId = `opkssh-${userId}-${hostId}`;
const decryptedCert = FieldCrypto.decryptField(
tokenData.sshCert,
userDataKey,
tokenId,
"ssh_cert",
);
const decryptedKey = FieldCrypto.decryptField(
tokenData.privateKey,
userDataKey,
tokenId,
"private_key",
);
await db
.update(opksshTokens)
.set({ lastUsed: new Date().toISOString() })
.where(
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
);
return {
sshCert: decryptedCert,
privateKey: decryptedKey,
};
} catch (error) {
sshLogger.error(`Failed to retrieve OPKSSH token`, error);
return null;
}
}
export async function deleteOPKSSHToken(
userId: string,
hostId: number,
): Promise<void> {
const db = getDb();
await db
.delete(opksshTokens)
.where(
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
);
}
export async function invalidateOPKSSHToken(
userId: string,
hostId: number,
reason: string,
): Promise<void> {
try {
const db = getDb();
await db
.delete(opksshTokens)
.where(
and(eq(opksshTokens.userId, userId), eq(opksshTokens.hostId, hostId)),
);
} catch (error) {
sshLogger.error(`Failed to invalidate OPKSSH token`, {
userId,
hostId,
reason,
error,
});
}
}
export async function handleOAuthCallback(
requestId: string,
queryString: string,
): Promise<{ success: boolean; message?: string }> {
const session = activeAuthSessions.get(requestId);
if (!session) {
return { success: false, message: "Invalid authentication session" };
}
try {
const callbackUrl = `http://localhost:${session.localPort}/login-callback?${queryString}`;
await axios.get(callbackUrl, {
timeout: 10000,
validateStatus: () => true,
});
return { success: true };
} catch {
session.ws.send(
JSON.stringify({
type: "opkssh_error",
requestId,
error: "Failed to complete authentication",
}),
);
await session.cleanup();
return { success: false, message: "Authentication failed" };
}
}
async function cleanupAuthSession(requestId: string): Promise<void> {
if (cleanupInProgress.has(requestId)) {
return;
}
cleanupInProgress.add(requestId);
try {
const session = activeAuthSessions.get(requestId);
if (!session) {
cleanupInProgress.delete(requestId);
return;
}
if (session.approvalTimeout) {
clearTimeout(session.approvalTimeout);
}
if (session.process) {
try {
session.process.kill("SIGTERM");
await new Promise<void>((resolve) => {
const killTimeout = setTimeout(() => {
if (session.process && !session.process.killed) {
session.process.kill("SIGKILL");
}
resolve();
}, 3000);
session.process.once("exit", () => {
clearTimeout(killTimeout);
resolve();
});
});
await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (killError) {
sshLogger.warn(
`Failed to kill OPKSSH process for session ${requestId}`,
killError,
);
}
}
activeAuthSessions.delete(requestId);
} finally {
cleanupInProgress.delete(requestId);
}
}
export function cancelAuthSession(requestId: string): void {
const session = activeAuthSessions.get(requestId);
if (session) {
session.cleanup();
}
}
export function getActiveAuthSession(
requestId: string,
): OPKSSHAuthSession | undefined {
return activeAuthSessions.get(requestId);
}
export function getActiveSessionsForUser(userId: string): OPKSSHAuthSession[] {
const sessions: OPKSSHAuthSession[] = [];
for (const session of activeAuthSessions.values()) {
if (session.userId === userId) {
sessions.push(session);
}
}
return sessions;
}
export function getActiveSessionsAll(): OPKSSHAuthSession[] {
return Array.from(activeAuthSessions.values());
}
export async function getUserIdFromRequest(req: any): Promise<string | null> {
try {
const { AuthManager } = await import("../utils/auth-manager.js");
const authManager = AuthManager.getInstance();
const token =
req.cookies?.jwt || req.headers.authorization?.replace("Bearer ", "");
if (!token) {
return null;
}
const decoded = await authManager.verifyJWTToken(token);
return decoded?.userId || null;
} catch (error) {
return null;
}
}
+96 -27
View File
@@ -23,6 +23,7 @@ import { collectLoginStats } from "./widgets/login-stats-collector.js";
import { collectPortsMetrics } from "./widgets/ports-collector.js";
import { collectFirewallMetrics } from "./widgets/firewall-collector.js";
import { createSocks5Connection } from "../utils/socks5-helper.js";
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
function createConnectionLog(
type: "info" | "success" | "warning" | "error",
@@ -125,6 +126,15 @@ async function createJumpHostChain(
const jumpClient = new Client();
clients.push(jumpClient);
const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier(
jumpHostConfig.id,
jumpHostConfig.ip,
jumpHostConfig.port || 22,
null,
userId,
true,
);
const connected = await new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => {
resolve(false);
@@ -151,6 +161,7 @@ async function createJumpHostChain(
username: jumpHostConfig.username,
tryKeyboard: true,
readyTimeout: 30000,
hostVerifier: jumpHostVerifier,
};
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
@@ -378,8 +389,8 @@ class SSHConnectionPool {
private async createConnection(
host: SSHHostWithCredentials,
): Promise<Client> {
const config = await buildSshConfig(host);
return new Promise(async (resolve, reject) => {
const config = buildSshConfig(host);
const client = new Client();
const timeout = setTimeout(() => {
client.end();
@@ -1023,6 +1034,20 @@ class PollingManager {
const statusOnly = options?.statusOnly ?? false;
const viewerUserId = options?.viewerUserId;
const enabledCollectors: string[] = [];
if (statsConfig.statusCheckEnabled) enabledCollectors.push("status");
if (!statusOnly && statsConfig.metricsEnabled) {
enabledCollectors.push(
"cpu",
"memory",
"disk",
"network",
"uptime",
"processes",
"system",
);
}
const existingConfig = this.pollingConfigs.get(host.id);
if (existingConfig) {
@@ -1150,12 +1175,10 @@ class PollingManager {
const latestConfig = this.pollingConfigs.get(refreshedHost.id);
if (latestConfig && latestConfig.statsConfig.metricsEnabled) {
const backoffInfo = pollingBackoff.getBackoffInfo(refreshedHost.id);
statsLogger.error("Failed to collect metrics for host", {
operation: "metrics_poll_failed",
statsLogger.error("Stats collector connection failed", error, {
operation: "stats_connect_failed",
hostId: refreshedHost.id,
hostName: refreshedHost.name,
error: errorMessage,
backoff: backoffInfo,
retryInfo: backoffInfo,
});
}
}
@@ -1172,6 +1195,7 @@ class PollingManager {
clearInterval(config.metricsTimer);
config.metricsTimer = undefined;
}
this.pollingConfigs.delete(hostId);
if (clearData) {
this.statusStore.delete(hostId);
@@ -1333,12 +1357,7 @@ app.use(
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const allowedOrigins = [
"http://localhost:5173",
"http://localhost:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
];
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
@@ -1530,7 +1549,15 @@ async function resolveHostCredentials(
if (credentials.length > 0) {
const credential = credentials[0];
baseHost.credentialId = credential.id;
baseHost.authType = credential.auth_type || credential.authType;
baseHost.authType =
credential.auth_type ||
credential.authType ||
(credential.password
? "password"
: credential.key ||
(credential as Record<string, unknown>).private_key
? "key"
: "none");
if (!host.overrideCredentialUsername) {
baseHost.username = credential.username;
@@ -1551,6 +1578,13 @@ async function resolveHostCredentials(
}
} else {
addLegacyCredentials(baseHost, host);
if (baseHost.authType === "credential") {
baseHost.authType = baseHost.password
? "password"
: baseHost.key
? "key"
: "none";
}
}
}
} catch (error) {
@@ -1558,6 +1592,13 @@ async function resolveHostCredentials(
`Failed to resolve credential ${host.credentialId} for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
);
addLegacyCredentials(baseHost, host);
if (baseHost.authType === "credential") {
baseHost.authType = baseHost.password
? "password"
: baseHost.key
? "key"
: "none";
}
}
} else {
addLegacyCredentials(baseHost, host);
@@ -1582,7 +1623,9 @@ function addLegacyCredentials(
baseHost.keyType = host.keyType;
}
function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
async function buildSshConfig(
host: SSHHostWithCredentials,
): Promise<ConnectConfig> {
const base: ConnectConfig = {
host: host.ip,
port: host.port,
@@ -1593,6 +1636,14 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
host.id,
host.ip,
host.port,
null,
host.userId || "",
false,
),
env: {
TERM: "xterm-256color",
LANG: "en_US.UTF-8",
@@ -1687,6 +1738,25 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
throw new Error(`Invalid SSH key format for host ${host.ip}`);
}
} else if (host.authType === "none") {
} else if (host.authType === "opkssh") {
} else if (host.authType === "credential") {
if (host.password) {
base.password = host.password;
} else if (host.key) {
const cleanKey = host.key
.trim()
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
(base as Record<string, unknown>).privateKey = Buffer.from(
cleanKey,
"utf8",
);
if (host.keyPassword) {
(base as Record<string, unknown>).passphrase = host.keyPassword;
}
} else {
throw new Error(`Credential for host ${host.ip} could not be resolved`);
}
} else {
throw new Error(
`Unsupported authentication type '${host.authType}' for host ${host.ip}`,
@@ -1809,12 +1879,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
};
try {
ports = await collectPortsMetrics(client);
} catch (e) {
statsLogger.debug("Failed to collect ports metrics", {
operation: "ports_metrics_failed",
error: e instanceof Error ? e.message : String(e),
});
}
} catch (e) {}
let firewall: {
type: "iptables" | "nftables" | "none";
@@ -1842,12 +1907,7 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
};
try {
firewall = await collectFirewallMetrics(client);
} catch (e) {
statsLogger.debug("Failed to collect firewall metrics", {
operation: "firewall_metrics_failed",
error: e instanceof Error ? e.message : String(e),
});
}
} catch (e) {}
const result = {
cpu,
@@ -2349,7 +2409,7 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => {
return res.json({ success: true, connectionLogs });
}
const config = buildSshConfig(host);
const config = await buildSshConfig(host);
const client = new Client();
const connectionPromise = new Promise<{
@@ -2524,6 +2584,15 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => {
`Authentication failed: ${errorMessage}`,
),
);
} else if (errorMessage.includes("verification failed")) {
errorStage = "handshake";
connectionLogs.push(
createConnectionLog(
"error",
errorStage,
`SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.`,
),
);
} else {
connectionLogs.push(
createConnectionLog(
+377 -15
View File
@@ -10,12 +10,13 @@ import axios from "axios";
import { getDb } from "../database/db/index.js";
import { sshCredentials, sshData } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { sshLogger } from "../utils/logger.js";
import { sshLogger, authLogger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js";
import { UserCrypto } from "../utils/user-crypto.js";
import { createSocks5Connection } from "../utils/socks5-helper.js";
import { SSHAuthManager } from "./auth-manager.js";
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
interface ConnectToHostData {
cols: number;
@@ -70,6 +71,11 @@ async function resolveJumpHost(
hostId: number,
userId: string,
): Promise<any | null> {
sshLogger.info("Resolving jump host", {
operation: "terminal_jumphost_resolve",
userId,
hostId,
});
try {
const hosts = await SimpleDBOps.select(
getDb()
@@ -159,6 +165,15 @@ async function createJumpHostChain(
const jumpClient = new Client();
clients.push(jumpClient);
const jumpHostVerifier = await SSHHostKeyVerifier.createHostVerifier(
jumpHostConfig.id,
jumpHostConfig.ip,
jumpHostConfig.port || 22,
null,
userId,
true,
);
const connected = await new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => {
resolve(false);
@@ -166,6 +181,13 @@ async function createJumpHostChain(
jumpClient.on("ready", () => {
clearTimeout(timeout);
sshLogger.success("Jump host connection established", {
operation: "terminal_jumphost_connected",
userId,
hostId: jumpHostConfig.id,
ip: jumpHostConfig.ip,
depth: i,
});
resolve(true);
});
@@ -185,6 +207,7 @@ async function createJumpHostChain(
username: jumpHostConfig.username,
tryKeyboard: true,
readyTimeout: 30000,
hostVerifier: jumpHostVerifier,
};
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
@@ -279,6 +302,7 @@ const wss = new WebSocketServer({
wss.on("connection", async (ws: WebSocket, req) => {
let userId: string | undefined;
let sessionId: string | undefined;
try {
const url = parseUrl(req.url!, true);
@@ -296,6 +320,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
userId = payload.userId;
sessionId = payload.sessionId;
} catch (error) {
sshLogger.error(
"WebSocket JWT verification failed during connection",
@@ -327,6 +352,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
const userWs = userConnections.get(userId)!;
userWs.add(ws);
sshLogger.info("Terminal WebSocket connection established", {
operation: "terminal_ws_connect",
sessionId,
userId,
});
let sshConn: Client | null = null;
let sshStream: ClientChannel | null = null;
@@ -342,8 +372,14 @@ wss.on("connection", async (ws: WebSocket, req) => {
let warpgateAuthPromptSent = false;
let warpgateAuthTimeout: NodeJS.Timeout | null = null;
let isAwaitingAuthCredentials = false;
let opksshTempFiles: { keyPath: string; certPath: string } | null = null;
ws.on("close", () => {
sshLogger.info("Terminal WebSocket disconnected", {
operation: "terminal_ws_disconnect",
sessionId,
userId,
});
const userWs = userConnections.get(userId);
if (userWs) {
userWs.delete(ws);
@@ -355,7 +391,17 @@ wss.on("connection", async (ws: WebSocket, req) => {
cleanupSSH();
});
ws.on("message", (msg: RawData) => {
function resetConnectionState() {
isConnecting = false;
isConnected = false;
isKeyboardInteractive = false;
keyboardInteractiveResponded = false;
keyboardInteractiveFinish = null;
totpPromptSent = false;
warpgateAuthPromptSent = false;
}
ws.on("message", async (msg: RawData) => {
const currentDataKey = userCrypto.getUserDataKey(userId);
if (!currentDataKey) {
ws.send(
@@ -568,6 +614,116 @@ wss.on("connection", async (ws: WebSocket, req) => {
break;
}
case "opkssh_start_auth": {
const opksshData = data as { hostId: number };
try {
const { startOPKSSHAuth, getRequestOrigin } =
await import("./opkssh-auth.js");
const db = getDb();
const hostRow = await db
.select()
.from(sshData)
.where(eq(sshData.id, opksshData.hostId))
.limit(1);
if (!hostRow || hostRow.length === 0) {
sshLogger.error(
`Host ${opksshData.hostId} not found for OPKSSH auth`,
{
operation: "opkssh_start_auth_host_not_found",
userId,
hostId: opksshData.hostId,
},
);
ws.send(
JSON.stringify({
type: "opkssh_error",
requestId: "",
error: "Host not found",
}),
);
break;
}
const hostname = hostRow[0].name || hostRow[0].ip;
const requestOrigin = getRequestOrigin(req);
await startOPKSSHAuth(
userId,
opksshData.hostId,
hostname,
ws,
requestOrigin,
);
} catch (error) {
sshLogger.error("Failed to start OPKSSH auth", error, {
operation: "opkssh_start_auth_error",
userId,
hostId: opksshData.hostId,
});
ws.send(
JSON.stringify({
type: "opkssh_error",
requestId: "",
error: "Failed to start OPKSSH authentication",
}),
);
}
break;
}
case "opkssh_cancel": {
const cancelData = data as { requestId: string };
try {
const { cancelAuthSession } = await import("./opkssh-auth.js");
cancelAuthSession(cancelData.requestId);
resetConnectionState();
} catch (error) {
sshLogger.error("Failed to cancel OPKSSH auth", error, {
operation: "opkssh_cancel_error",
userId,
});
}
break;
}
case "opkssh_browser_opened": {
break;
}
case "opkssh_auth_completed": {
const completedData = data as {
hostId: number;
cols?: number;
rows?: number;
hostConfig?: any;
};
resetConnectionState();
const reconnectConfig: ConnectToHostData = {
cols: completedData.cols || 80,
rows: completedData.rows || 24,
hostConfig:
completedData.hostConfig ||
({ id: completedData.hostId, userId } as any),
};
handleConnectToHost(reconnectConfig).catch((error) => {
sshLogger.error("Failed to reconnect after OPKSSH auth", error, {
operation: "opkssh_reconnect_error",
userId,
hostId: completedData.hostId,
});
ws.send(
JSON.stringify({
type: "error",
message:
"Failed to connect after authentication: " +
(error instanceof Error ? error.message : "Unknown error"),
}),
);
});
break;
}
default:
sshLogger.warn("Unknown message type received", {
operation: "websocket_message_unknown_type",
@@ -591,6 +747,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
authType,
credentialId,
} = hostConfig;
sshLogger.info("Resolving SSH host configuration", {
operation: "terminal_host_resolve",
sessionId,
userId,
hostId: id,
});
const sendLog = (
stage: string,
@@ -745,7 +907,21 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshConn.on("ready", () => {
clearTimeout(connectionTimeout);
sshLogger.success("SSH connection established", {
operation: "terminal_ssh_connected",
sessionId,
userId,
hostId: id,
ip,
});
if (totpPromptSent) {
authLogger.success("TOTP verification successful for SSH session", {
operation: "terminal_totp_success",
sessionId,
userId,
hostId: id,
});
}
sendLog("handshake", "success", "SSH handshake completed");
sendLog("auth", "success", `Authentication successful for ${username}`);
sendLog("connected", "success", "Connection established");
@@ -858,6 +1034,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
sshStream = stream;
sshLogger.success("Terminal shell channel opened", {
operation: "terminal_shell_opened",
sessionId,
userId,
hostId: id,
termType: "xterm-256color",
});
stream.on("data", (data: Buffer) => {
try {
@@ -987,6 +1170,57 @@ wss.on("connection", async (ws: WebSocket, req) => {
keyboardInteractiveResponded,
});
if (
resolvedCredentials.authType === "opkssh" &&
err.message.includes("All configured authentication methods failed")
) {
sshLogger.warn("OPKSSH authentication failed - invalidating token", {
operation: "opkssh_auth_failed",
hostId: id,
userId,
error: err.message,
});
(async () => {
try {
const { invalidateOPKSSHToken } = await import("./opkssh-auth.js");
await invalidateOPKSSHToken(userId, id, "SSH auth failed");
} catch (invalidateError) {
sshLogger.error("Failed to invalidate OPKSSH token", {
operation: "opkssh_token_invalidation_error",
userId,
hostId: id,
error: invalidateError,
});
}
})();
clearTimeout(connectionTimeout);
if (sshConn) {
try {
sshConn.end();
} catch (e) {}
sshConn = null;
}
resetConnectionState();
sendLog(
"auth",
"error",
"OPKSSH certificate authentication failed. Please authenticate again.",
);
ws.send(
JSON.stringify({
type: "opkssh_auth_required",
hostId: id,
message:
"OPKSSH authentication failed or expired. Please authenticate again.",
}),
);
return;
}
if (
authMethodNotAvailable &&
resolvedCredentials.authType === "none" &&
@@ -1005,8 +1239,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
} catch (e) {}
sshConn = null;
}
isConnecting = false;
isConnected = false;
resetConnectionState();
ws.send(
JSON.stringify({
type: "auth_method_not_available",
@@ -1031,8 +1264,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
} catch (e) {}
sshConn = null;
}
isConnecting = false;
isConnected = false;
resetConnectionState();
ws.send(
JSON.stringify({
type: "auth_method_not_available",
@@ -1056,6 +1288,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
error: err.message,
},
);
resetConnectionState();
return;
}
@@ -1069,6 +1302,13 @@ wss.on("connection", async (ws: WebSocket, req) => {
err.message.includes("authentication") ||
err.message.includes("Authentication")
) {
authLogger.error("SSH authentication failed", err, {
operation: "terminal_ssh_auth_failed",
sessionId,
userId,
hostId: id,
authType: resolvedCredentials.authType,
});
sendLog("auth", "error", `Authentication failed: ${err.message}`);
} else {
sendLog("error", "error", `Connection failed: ${err.message}`);
@@ -1116,6 +1356,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshConn.on("close", () => {
clearTimeout(connectionTimeout);
sshLogger.info("SSH connection closed", {
operation: "terminal_ssh_disconnected",
sessionId,
userId,
hostId: id,
});
if (isAwaitingAuthCredentials) {
cleanupSSH(connectionTimeout);
@@ -1209,6 +1455,14 @@ wss.on("connection", async (ws: WebSocket, req) => {
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
timeout: 120000,
hostVerifier: await SSHHostKeyVerifier.createHostVerifier(
id,
ip,
port,
ws,
userId,
false,
),
env: {
TERM: "xterm-256color",
LANG: "en_US.UTF-8",
@@ -1229,6 +1483,10 @@ wss.on("connection", async (ws: WebSocket, req) => {
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp256",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group18-sha512",
"diffie-hellman-group17-sha512",
"diffie-hellman-group16-sha512",
"diffie-hellman-group15-sha512",
"diffie-hellman-group14-sha256",
"diffie-hellman-group14-sha1",
"diffie-hellman-group-exchange-sha1",
@@ -1284,7 +1542,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
return;
}
connectConfig.password = resolvedCredentials.password;
if (!hostConfig.forceKeyboardInteractive) {
connectConfig.password = resolvedCredentials.password;
}
sendLog("auth", "info", "Using password authentication");
} else if (
resolvedCredentials.authType === "key" &&
@@ -1333,6 +1593,60 @@ wss.on("connection", async (ws: WebSocket, req) => {
}),
);
return;
} else if (resolvedCredentials.authType === "opkssh") {
sendLog("auth", "info", "Using OPKSSH certificate authentication");
try {
const { getOPKSSHToken } = await import("./opkssh-auth.js");
const token = await getOPKSSHToken(userId, id);
if (!token) {
sendLog(
"auth",
"info",
"No valid OPKSSH token found, requesting authentication",
);
ws.send(
JSON.stringify({
type: "opkssh_auth_required",
hostId: id,
}),
);
return;
}
sendLog("auth", "info", "Using cached OPKSSH certificate");
const { promises: fs } = await import("fs");
const path = await import("path");
const os = await import("os");
const tempDir = os.tmpdir();
const keyPath = path.join(tempDir, `opkssh-${userId}-${id}`);
const certPath = `${keyPath}-cert.pub`;
await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 });
await fs.writeFile(certPath, token.sshCert, { mode: 0o600 });
opksshTempFiles = { keyPath, certPath };
connectConfig.privateKey = await fs.readFile(keyPath);
} catch (opksshError) {
sshLogger.error("OPKSSH authentication error", opksshError, {
operation: "opkssh_auth_error",
userId,
hostId: id,
});
ws.send(
JSON.stringify({
type: "error",
message:
"OPKSSH authentication failed: " +
(opksshError instanceof Error
? opksshError.message
: "Unknown error"),
}),
);
return;
}
} else {
sendLog("auth", "info", "Using keyboard-interactive authentication");
sshLogger.error("No valid authentication method provided");
@@ -1437,6 +1751,16 @@ wss.on("connection", async (ws: WebSocket, req) => {
"Starting SSH session through jump host",
);
sendLog("auth", "info", `Authenticating as ${username}`);
sshLogger.info("Initiating SSH connection", {
operation: "terminal_ssh_connect_attempt",
sessionId,
userId,
hostId: id,
ip,
port,
username,
authType: resolvedCredentials.authType,
});
sshConn.connect(connectConfig);
});
} catch (error) {
@@ -1456,6 +1780,16 @@ wss.on("connection", async (ws: WebSocket, req) => {
} else {
sendLog("handshake", "info", "Starting SSH session");
sendLog("auth", "info", `Authenticating as ${username}`);
sshLogger.info("Initiating SSH connection", {
operation: "terminal_ssh_connect_attempt",
sessionId,
userId,
hostId: id,
ip,
port,
username,
authType: resolvedCredentials.authType,
});
sshConn.connect(connectConfig);
}
}
@@ -1526,15 +1860,43 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshConn = null;
}
totpPromptSent = false;
warpgateAuthPromptSent = false;
isKeyboardInteractive = false;
keyboardInteractiveResponded = false;
keyboardInteractiveFinish = null;
isConnecting = false;
isConnected = false;
resetConnectionState();
isCleaningUp = false;
isAwaitingAuthCredentials = false;
if (opksshTempFiles) {
const tempFilesToClean = opksshTempFiles;
opksshTempFiles = null;
(async () => {
try {
const { promises: fs } = await import("fs");
const cleanupResults = await Promise.allSettled([
fs.unlink(tempFilesToClean.keyPath),
fs.unlink(tempFilesToClean.certPath),
]);
cleanupResults.forEach((result, index) => {
if (result.status === "rejected") {
sshLogger.warn(`Failed to cleanup OPKSSH temp file`, {
operation: "opkssh_temp_cleanup_failed",
file: index === 0 ? "keyPath" : "certPath",
path:
index === 0
? tempFilesToClean.keyPath
: tempFilesToClean.certPath,
error: result.reason,
});
}
});
} catch (error) {
sshLogger.error("Failed to cleanup OPKSSH temp files", {
operation: "opkssh_temp_cleanup_error",
error,
});
}
})();
}
}
// Note: PTY-level keepalive (writing \x00 to the stream) was removed.
+83 -8
View File
@@ -30,12 +30,7 @@ app.use(
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const allowedOrigins = [
"http://localhost:5173",
"http://localhost:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
];
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
@@ -294,7 +289,12 @@ async function cleanupTunnelResources(
if (verification?.timeout) clearTimeout(verification.timeout);
try {
verification?.conn.end();
} catch (error) {}
} catch (error) {
tunnelLogger.error("Error during tunnel cleanup", error, {
operation: "tunnel_cleanup_error",
tunnelName,
});
}
tunnelVerifications.delete(tunnelName);
}
@@ -359,7 +359,12 @@ async function handleDisconnect(
const verification = tunnelVerifications.get(tunnelName);
if (verification?.timeout) clearTimeout(verification.timeout);
verification?.conn.end();
} catch (error) {}
} catch (error) {
tunnelLogger.error("Error during tunnel cleanup", error, {
operation: "tunnel_cleanup_error",
tunnelName,
});
}
tunnelVerifications.delete(tunnelName);
}
@@ -519,6 +524,16 @@ async function connectSSHTunnel(
): Promise<void> {
const tunnelName = tunnelConfig.name;
const tunnelMarker = getTunnelMarker(tunnelName);
tunnelLogger.info("Tunnel creation request received", {
operation: "tunnel_create_request",
userId: tunnelConfig.sourceUserId,
hostId: tunnelConfig.sourceHostId,
tunnelName,
tunnelType: tunnelConfig.tunnelType || "remote",
sourcePort: tunnelConfig.sourcePort,
endpointHost: tunnelConfig.endpointHost,
endpointPort: tunnelConfig.endpointPort,
});
if (manualDisconnects.has(tunnelName)) {
return;
@@ -875,6 +890,12 @@ async function connectSSHTunnel(
conn.on("ready", () => {
clearTimeout(connectionTimeout);
tunnelLogger.info("Creating new SSH connection for tunnel", {
operation: "tunnel_connection_create",
userId: tunnelConfig.sourceUserId,
hostId: tunnelConfig.sourceHostId,
tunnelName,
});
const isAlreadyVerifying = tunnelVerifications.has(tunnelName);
if (isAlreadyVerifying) {
@@ -933,6 +954,14 @@ async function connectSSHTunnel(
}
activeTunnels.set(tunnelName, conn);
tunnelLogger.success("Tunnel port binding successful", {
operation: "tunnel_port_bound",
userId: tunnelConfig.sourceUserId,
hostId: tunnelConfig.sourceHostId,
tunnelName,
sourcePort: tunnelConfig.sourcePort,
endpointPort: tunnelConfig.endpointPort,
});
setTimeout(() => {
if (
@@ -940,6 +969,12 @@ async function connectSSHTunnel(
activeTunnels.has(tunnelName)
) {
tunnelConnecting.delete(tunnelName);
tunnelLogger.success("Tunnel creation complete", {
operation: "tunnel_create_complete",
userId: tunnelConfig.sourceUserId,
hostId: tunnelConfig.sourceHostId,
tunnelName,
});
broadcastTunnelStatus(tunnelName, {
connected: true,
@@ -1279,6 +1314,13 @@ async function killRemoteTunnelByMarker(
callback: (err?: Error) => void,
) {
const tunnelMarker = getTunnelMarker(tunnelName);
tunnelLogger.info("Killing remote tunnel process", {
operation: "tunnel_remote_kill",
userId: tunnelConfig.sourceUserId,
hostId: tunnelConfig.sourceHostId,
tunnelName,
marker: tunnelMarker,
});
let resolvedSourceCredentials = {
password: tunnelConfig.sourcePassword,
@@ -1418,10 +1460,24 @@ async function killRemoteTunnelByMarker(
stream.on("close", () => {
if (!foundProcesses) {
tunnelLogger.warn("Remote tunnel process not found", {
operation: "tunnel_remote_not_found",
userId: tunnelConfig.sourceUserId,
hostId: tunnelConfig.sourceHostId,
tunnelName,
marker: tunnelMarker,
});
conn.end();
callback();
return;
}
tunnelLogger.info("Remote tunnel process found, proceeding to kill", {
operation: "tunnel_remote_found",
userId: tunnelConfig.sourceUserId,
hostId: tunnelConfig.sourceHostId,
tunnelName,
marker: tunnelMarker,
});
const killCmds = [
`pkill -TERM -f '${tunnelMarker}'`,
@@ -1452,6 +1508,13 @@ async function killRemoteTunnelByMarker(
tunnelLogger.warn(
`Some tunnel processes may still be running for '${tunnelName}'`,
);
} else {
tunnelLogger.success("Remote tunnel process killed", {
operation: "tunnel_remote_killed",
userId: tunnelConfig.sourceUserId,
hostId: tunnelConfig.sourceHostId,
tunnelName,
});
}
conn.end();
callback();
@@ -1843,6 +1906,12 @@ app.post(
}
}
tunnelLogger.info("Tunnel stop request received", {
operation: "tunnel_stop_request",
userId,
hostId: config?.sourceHostId,
tunnelName,
});
manualDisconnects.add(tunnelName);
retryCounters.delete(tunnelName);
retryExhaustedTunnels.delete(tunnelName);
@@ -1853,6 +1922,12 @@ app.post(
}
await cleanupTunnelResources(tunnelName, true);
tunnelLogger.info("Tunnel cleanup completed", {
operation: "tunnel_cleanup_complete",
userId,
hostId: config?.sourceHostId,
tunnelName,
});
broadcastTunnelStatus(tunnelName, {
connected: false,
+41
View File
@@ -10,6 +10,7 @@ import { SystemCrypto } from "./utils/system-crypto.js";
import { systemLogger, versionLogger } from "./utils/logger.js";
(async () => {
const initStartTime = Date.now();
try {
dotenv.config({ quiet: true });
@@ -23,6 +24,12 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
}
} catch (error) {}
systemLogger.info("Termix backend initialization started", {
operation: "backend_init_start",
nodeEnv: process.env.NODE_ENV || "production",
port: process.env.PORT || 4090,
});
let version = "unknown";
const versionSources = [
@@ -88,14 +95,41 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
await systemCrypto.initializeInternalAuthToken();
await AutoSSLSetup.initialize();
systemLogger.success("SSL setup completed", {
operation: "backend_init_ssl",
sslEnabled: process.env.SSL_ENABLED === "true",
});
const dbModule = await import("./database/db/index.js");
await dbModule.initializeDatabase();
systemLogger.success("Database initialized", {
operation: "backend_init_db",
});
const authManager = AuthManager.getInstance();
await authManager.initialize();
DataCrypto.initialize();
const { OPKSSHBinaryManager } =
await import("./utils/opkssh-binary-manager.js");
try {
await OPKSSHBinaryManager.ensureBinary();
} catch (error) {
const dataDir =
process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
systemLogger.warn(
"Failed to initialize OPKSSH binary - OPKSSH authentication will not be available",
{
operation: "opkssh_binary_init_failed",
error: error instanceof Error ? error.message : "Unknown error",
stack: error instanceof Error ? error.stack : undefined,
platform: process.platform,
arch: process.arch,
dataDir,
},
);
}
await import("./database/database.js");
await import("./ssh/terminal.js");
@@ -106,6 +140,13 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
await import("./ssh/docker-console.js");
await import("./dashboard.js");
systemLogger.success("Termix backend started successfully", {
operation: "backend_init_complete",
port: process.env.PORT || 4090,
ssl: process.env.SSL_ENABLED === "true",
duration: Date.now() - initStartTime,
});
process.on("SIGINT", () => {
systemLogger.info(
"Received SIGINT signal, initiating graceful shutdown...",
+12 -1
View File
@@ -2,6 +2,7 @@ import swaggerJSDoc from "swagger-jsdoc";
import path from "path";
import { fileURLToPath } from "url";
import { promises as fs } from "fs";
import { systemLogger } from "./utils/logger.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -125,6 +126,10 @@ const swaggerOptions: swaggerJSDoc.Options = {
async function generateOpenAPISpec() {
try {
systemLogger.info("Generating OpenAPI specification", {
operation: "openapi_generate_start",
});
const swaggerSpec = swaggerJSDoc(swaggerOptions);
const outputPath = path.join(projectRoot, "openapi.json");
@@ -134,8 +139,14 @@ async function generateOpenAPISpec() {
JSON.stringify(swaggerSpec, null, 2),
"utf-8",
);
systemLogger.success("OpenAPI specification generated", {
operation: "openapi_generate_success",
});
} catch (error) {
console.error("Failed to generate OpenAPI specification:", error);
systemLogger.error("Failed to generate OpenAPI specification", error, {
operation: "openapi_generation",
});
process.exit(1);
}
}
+21 -1
View File
@@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
import { UserCrypto } from "./user-crypto.js";
import { SystemCrypto } from "./system-crypto.js";
import { DataCrypto } from "./data-crypto.js";
import { databaseLogger } from "./logger.js";
import { databaseLogger, authLogger } from "./logger.js";
import type { Request, Response, NextFunction } from "express";
import { db } from "../database/db/index.js";
import { sessions } from "../database/db/schema.js";
@@ -346,6 +346,11 @@ class AuthManager {
async revokeSession(sessionId: string): Promise<boolean> {
try {
authLogger.info("User session invalidated", {
operation: "user_logout",
sessionId,
});
await db.delete(sessions).where(eq(sessions.id, sessionId));
try {
@@ -387,6 +392,12 @@ class AuthManager {
(s) => !exceptSessionId || s.id !== exceptSessionId,
).length;
authLogger.info("All user sessions invalidated", {
operation: "user_logout_all",
userId,
sessionCount: deletedCount,
});
if (exceptSessionId) {
await db
.delete(sessions)
@@ -518,6 +529,15 @@ class AuthManager {
};
}
getClearCookieOptions(req: RequestWithHeaders) {
return {
httpOnly: false,
secure: req.secure || req.headers["x-forwarded-proto"] === "https",
sameSite: "strict" as const,
path: "/",
};
}
createAuthMiddleware() {
return async (req: Request, res: Response, next: NextFunction) => {
const authReq = req as AuthenticatedRequest;
+33 -80
View File
@@ -372,89 +372,19 @@ class DatabaseFileEncryption {
encryptedPath: string,
targetPath?: string,
): Promise<string> {
if (!fs.existsSync(encryptedPath)) {
throw new Error(
`Encrypted database file does not exist: ${encryptedPath}`,
);
}
const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
if (!fs.existsSync(metadataPath)) {
throw new Error(`Metadata file does not exist: ${metadataPath}`);
}
const decryptedPath =
targetPath || encryptedPath.replace(this.ENCRYPTED_FILE_SUFFIX, "");
try {
const metadataContent = fs.readFileSync(metadataPath, "utf8");
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
const decryptedBuffer = await this.decryptDatabaseToBuffer(encryptedPath);
const encryptedData = fs.readFileSync(encryptedPath);
if (
metadata.dataSize !== undefined &&
encryptedData.length !== metadata.dataSize
) {
databaseLogger.error(
"Encrypted file size mismatch - possible corrupted write or mismatched metadata",
null,
{
operation: "database_file_size_mismatch",
encryptedPath,
actualSize: encryptedData.length,
expectedSize: metadata.dataSize,
},
);
throw new Error(
`Encrypted file size mismatch: expected ${metadata.dataSize} bytes but got ${encryptedData.length} bytes. ` +
`This indicates corrupted files or interrupted write operation.`,
);
}
let key: Buffer;
if (metadata.version === "v2") {
key = await this.systemCrypto.getDatabaseKey();
} else if (metadata.version === "v1") {
databaseLogger.warn(
"Decrypting legacy v1 encrypted database - consider upgrading",
{
operation: "decrypt_legacy_v1",
path: encryptedPath,
},
);
if (!metadata.salt) {
throw new Error("v1 encrypted file missing required salt field");
}
const salt = Buffer.from(metadata.salt, "hex");
const fixedSeed =
process.env.DB_FILE_KEY || "termix-database-file-encryption-seed-v1";
key = crypto.pbkdf2Sync(fixedSeed, salt, 100000, 32, "sha256");
} else {
throw new Error(`Unsupported encryption version: ${metadata.version}`);
}
const decipher = crypto.createDecipheriv(
metadata.algorithm,
key,
Buffer.from(metadata.iv, "hex"),
) as crypto.DecipherGCM;
decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
const decrypted = Buffer.concat([
decipher.update(encryptedData),
decipher.final(),
]);
fs.writeFileSync(decryptedPath, decrypted);
fs.writeFileSync(decryptedPath, decryptedBuffer);
databaseLogger.info("Database file decrypted successfully", {
operation: "database_file_decryption",
encryptedPath,
decryptedPath,
encryptedSize: encryptedData.length,
decryptedSize: decrypted.length,
fingerprintPrefix: metadata.fingerprint,
decryptedSize: decryptedBuffer.length,
});
return decryptedPath;
@@ -480,12 +410,14 @@ class DatabaseFileEncryption {
try {
const metadataContent = fs.readFileSync(metadataPath, "utf8");
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
return (
if (
metadata.version === this.VERSION &&
metadata.algorithm === this.ALGORITHM
);
) {
return true;
}
} catch {
return false;
// .meta parse failed, fall through to single-file detection
}
}
@@ -526,11 +458,32 @@ class DatabaseFileEncryption {
}
try {
const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
const metadataContent = fs.readFileSync(metadataPath, "utf8");
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
const fileStats = fs.statSync(encryptedPath);
let metadata: EncryptedFileMetadata | null = null;
const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
if (fs.existsSync(metadataPath)) {
try {
const metadataContent = fs.readFileSync(metadataPath, "utf8");
metadata = JSON.parse(metadataContent);
} catch {
// .meta parse failed, try single-file format
}
}
if (!metadata) {
const fileBuffer = fs.readFileSync(encryptedPath);
const metadataLength = fileBuffer.readUInt32BE(0);
const metadataEnd = 4 + metadataLength;
const metadataJson = fileBuffer
.subarray(4, metadataEnd)
.toString("utf8");
metadata = JSON.parse(metadataJson);
}
if (!metadata) {
return null;
}
return {
version: metadata.version,
+6
View File
@@ -47,6 +47,12 @@ class FieldCrypto {
"public_key",
"publicKey",
]),
opkssh_tokens: new Set([
"ssh_cert",
"sshCert",
"private_key",
"privateKey",
]),
};
static encryptField(
+226
View File
@@ -0,0 +1,226 @@
import { promises as fs } from "fs";
import path from "path";
import { createWriteStream } from "fs";
import { pipeline } from "stream/promises";
import { systemLogger } from "./logger.js";
const OPKSSH_REPO = "openpubkey/opkssh";
function getBinaryDir(): string {
const dataDir =
process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
return path.join(dataDir, "opkssh");
}
function getVersionFile(): string {
return path.join(getBinaryDir(), "version.txt");
}
interface GitHubAsset {
name: string;
browser_download_url: string;
}
interface GitHubRelease {
tag_name: string;
assets: GitHubAsset[];
}
export class OPKSSHBinaryManager {
private static binaryPath: string | null = null;
static async ensureBinary(): Promise<string> {
if (this.binaryPath) {
return this.binaryPath;
}
const binaryName = this.getBinaryName();
const expectedPath = path.join(getBinaryDir(), binaryName);
try {
await fs.access(expectedPath);
const needsUpdate = await this.checkForUpdate();
if (needsUpdate) {
systemLogger.info("Newer OPKSSH version available, updating...", {
operation: "opkssh_binary_update_start",
});
await this.downloadBinary();
}
this.binaryPath = expectedPath;
return expectedPath;
} catch {
systemLogger.info("OPKSSH binary not found, downloading...", {
operation: "opkssh_binary_download_start",
});
await this.downloadBinary();
this.binaryPath = expectedPath;
return expectedPath;
}
}
static async downloadBinary(): Promise<void> {
try {
await fs.mkdir(getBinaryDir(), { recursive: true });
const release = await this.getLatestRelease();
const asset = this.findMatchingAsset(release.assets);
if (!asset) {
throw new Error(
`No matching OPKSSH binary found for platform ${process.platform} ${process.arch}`,
);
}
const binaryName = this.getBinaryName();
const binaryPath = path.join(getBinaryDir(), binaryName);
const response = await fetch(asset.browser_download_url);
if (!response.ok) {
throw new Error(`Failed to download: ${response.statusText}`);
}
if (!response.body) {
throw new Error("Response body is null");
}
const fileStream = createWriteStream(binaryPath);
await pipeline(
response.body as unknown as NodeJS.ReadableStream,
fileStream,
);
await fs.chmod(binaryPath, 0o755);
await fs.writeFile(getVersionFile(), release.tag_name, "utf8");
systemLogger.info(
`OPKSSH binary downloaded successfully to ${binaryPath}`,
{
operation: "opkssh_binary_download_complete",
path: binaryPath,
version: release.tag_name,
},
);
} catch (error) {
systemLogger.error("Failed to download OPKSSH binary", error, {
operation: "opkssh_binary_download_error",
});
throw error;
}
}
static getBinaryPath(): string {
if (!this.binaryPath) {
throw new Error(
"OPKSSH binary not initialized. Call ensureBinary() first.",
);
}
return this.binaryPath;
}
private static async checkForUpdate(): Promise<boolean> {
try {
let localVersion: string | null = null;
try {
localVersion = await fs.readFile(getVersionFile(), "utf8");
localVersion = localVersion.trim();
} catch {
return true;
}
const release = await this.getLatestRelease();
const latestVersion = release.tag_name;
if (localVersion !== latestVersion) {
return true;
}
return false;
} catch (error) {
systemLogger.warn("Failed to check for OPKSSH updates", {
operation: "opkssh_update_check_failed",
error: error instanceof Error ? error.message : "Unknown error",
});
return false;
}
}
private static async getLatestRelease(): Promise<GitHubRelease> {
const url = `https://api.github.com/repos/${OPKSSH_REPO}/releases/latest`;
const response = await fetch(url, {
headers: {
"User-Agent": "Termix",
},
});
if (!response.ok) {
throw new Error(`Failed to fetch release info: ${response.statusText}`);
}
return (await response.json()) as GitHubRelease;
}
private static findMatchingAsset(assets: GitHubAsset[]): GitHubAsset | null {
const platform = process.platform;
const arch = process.arch;
const osMap: Record<string, string> = {
win32: "windows",
linux: "linux",
darwin: "darwin",
};
const archMap: Record<string, string> = {
x64: "amd64",
arm64: "arm64",
};
const mappedOs = osMap[platform];
const mappedArch = archMap[arch];
if (!mappedOs || !mappedArch) {
return null;
}
const patterns = [
`opkssh-${mappedOs}-${mappedArch}.exe`,
`opkssh-${mappedOs}-${mappedArch}`,
`opkssh_${mappedOs}_${mappedArch}.exe`,
`opkssh_${mappedOs}_${mappedArch}`,
];
for (const pattern of patterns) {
const asset = assets.find(
(a) => a.name.toLowerCase() === pattern.toLowerCase(),
);
if (asset) {
return asset;
}
}
return null;
}
private static getBinaryName(): string {
const platform = process.platform;
const arch = process.arch;
const osMap: Record<string, string> = {
win32: "windows",
linux: "linux",
darwin: "darwin",
};
const archMap: Record<string, string> = {
x64: "amd64",
arm64: "arm64",
};
const mappedOs = osMap[platform] || platform;
const mappedArch = archMap[arch] || arch;
const extension = platform === "win32" ? ".exe" : "";
return `opkssh-${mappedOs}-${mappedArch}${extension}`;
}
}
+32
View File
@@ -0,0 +1,32 @@
import { HttpsProxyAgent } from "https-proxy-agent";
import type { Agent } from "http";
export function getProxyAgent(targetUrl?: string): Agent | undefined {
const proxyUrl =
process.env.https_proxy ||
process.env.HTTPS_PROXY ||
process.env.http_proxy ||
process.env.HTTP_PROXY;
if (!proxyUrl) return undefined;
if (targetUrl) {
const noProxy = process.env.no_proxy || process.env.NO_PROXY || "";
const hostname = new URL(targetUrl).hostname.toLowerCase();
for (const entry of noProxy.split(",")) {
const trimmed = entry.trim().toLowerCase();
if (!trimmed) continue;
const normalized = trimmed
.replace(/^\*\./, "")
.replace(/^\./, "");
if (hostname === normalized || hostname.endsWith(`.${normalized}`)) {
return undefined;
}
}
}
return new HttpsProxyAgent(proxyUrl);
}
+6 -18
View File
@@ -303,9 +303,8 @@ class UserCrypto {
await this.storeKEKSalt(userId, newKekSalt);
await this.storeEncryptedDEK(userId, newEncryptedDEK);
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
oldKEK.fill(0);
@@ -341,9 +340,8 @@ class UserCrypto {
await this.storeKEKSalt(userId, newKekSalt);
await this.storeEncryptedDEK(userId, newEncryptedDEK);
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
newKEK.fill(0);
@@ -364,15 +362,6 @@ class UserCrypto {
}
}
/**
* Convert a password-based user's encryption to DUAL-AUTH encryption.
* This is used when linking an OIDC account to a password account for dual-auth.
*
* IMPORTANT: This does NOT delete the password-based KEK salt!
* The user needs to maintain BOTH password and OIDC authentication methods.
* We keep the password KEK salt so password login still works.
* We also store the DEK encrypted with OIDC system key for OIDC login.
*/
async convertToOIDCEncryption(userId: string): Promise<void> {
try {
const existingEncryptedDEK = await this.getEncryptedDEK(userId);
@@ -427,9 +416,8 @@ class UserCrypto {
},
);
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
} catch (error) {
databaseLogger.error("Failed to convert to OIDC encryption", error, {
+4 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, useState, useCallback } from "react";
import { isElectron } from "@/ui/main-axios";
import { getBasePath } from "@/lib/base-path";
interface ServiceWorkerState {
isSupported: boolean;
@@ -41,7 +42,9 @@ export function useServiceWorker(): ServiceWorkerState {
const registerSW = async () => {
try {
const registration = await navigator.serviceWorker.register("/sw.js");
const registration = await navigator.serviceWorker.register(
`${getBasePath()}/sw.js`,
);
setState((prev) => ({ ...prev, isRegistered: true }));
registration.addEventListener("updatefound", () =>
+54 -44
View File
@@ -3,38 +3,40 @@ import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import enTranslation from "../locales/en.json";
import afTranslation from "../locales/translated/af.json";
import arTranslation from "../locales/translated/ar.json";
import bnTranslation from "../locales/translated/bn.json";
import bgTranslation from "../locales/translated/bg.json";
import caTranslation from "../locales/translated/ca.json";
import csTranslation from "../locales/translated/cs.json";
import daTranslation from "../locales/translated/da.json";
import deTranslation from "../locales/translated/de.json";
import elTranslation from "../locales/translated/el.json";
import esTranslation from "../locales/translated/es.json";
import fiTranslation from "../locales/translated/fi.json";
import frTranslation from "../locales/translated/fr.json";
import heTranslation from "../locales/translated/he.json";
import hiTranslation from "../locales/translated/hi.json";
import huTranslation from "../locales/translated/hu.json";
import idTranslation from "../locales/translated/id.json";
import itTranslation from "../locales/translated/it.json";
import jaTranslation from "../locales/translated/ja.json";
import koTranslation from "../locales/translated/ko.json";
import nlTranslation from "../locales/translated/nl.json";
import noTranslation from "../locales/translated/no.json";
import plTranslation from "../locales/translated/pl.json";
import ptTranslation from "../locales/translated/pt.json";
import roTranslation from "../locales/translated/ro.json";
import ruTranslation from "../locales/translated/ru.json";
import srTranslation from "../locales/translated/sr.json";
import svTranslation from "../locales/translated/sv.json";
import thTranslation from "../locales/translated/th.json";
import trTranslation from "../locales/translated/tr.json";
import ukTranslation from "../locales/translated/uk.json";
import viTranslation from "../locales/translated/vi.json";
import zhTranslation from "../locales/translated/zh.json";
import afTranslation from "../locales/translated/af_ZA.json";
import arTranslation from "../locales/translated/ar_SA.json";
import bnTranslation from "../locales/translated/bn_BD.json";
import bgTranslation from "../locales/translated/bg_BG.json";
import caTranslation from "../locales/translated/ca_ES.json";
import csTranslation from "../locales/translated/cs_CZ.json";
import daTranslation from "../locales/translated/da_DK.json";
import deTranslation from "../locales/translated/de_DE.json";
import elTranslation from "../locales/translated/el_GR.json";
import esESTranslation from "../locales/translated/es_ES.json";
import fiTranslation from "../locales/translated/fi_FI.json";
import frTranslation from "../locales/translated/fr_FR.json";
import heTranslation from "../locales/translated/he_IL.json";
import hiTranslation from "../locales/translated/hi_IN.json";
import huTranslation from "../locales/translated/hu_HU.json";
import idTranslation from "../locales/translated/id_ID.json";
import itTranslation from "../locales/translated/it_IT.json";
import jaTranslation from "../locales/translated/ja_JP.json";
import koTranslation from "../locales/translated/ko_KR.json";
import nlTranslation from "../locales/translated/nl_NL.json";
import noTranslation from "../locales/translated/no_NO.json";
import plTranslation from "../locales/translated/pl_PL.json";
import ptPTTranslation from "../locales/translated/pt_PT.json";
import ptBRTranslation from "../locales/translated/pt_BR.json";
import roTranslation from "../locales/translated/ro_RO.json";
import ruTranslation from "../locales/translated/ru_RU.json";
import srTranslation from "../locales/translated/sr_SP.json";
import svSETranslation from "../locales/translated/sv_SE.json";
import thTranslation from "../locales/translated/th_TH.json";
import trTranslation from "../locales/translated/tr_TR.json";
import ukTranslation from "../locales/translated/uk_UA.json";
import viTranslation from "../locales/translated/vi_VN.json";
import zhCNTranslation from "../locales/translated/zh_CN.json";
import zhTWTranslation from "../locales/translated/zh_TW.json";
i18n
.use(LanguageDetector)
@@ -51,7 +53,7 @@ i18n
"da",
"de",
"el",
"es",
"es-ES",
"fi",
"fr",
"he",
@@ -64,16 +66,18 @@ i18n
"nl",
"no",
"pl",
"pt",
"pt-PT",
"pt-BR",
"ro",
"ru",
"sr",
"sv",
"sv-SE",
"th",
"tr",
"uk",
"vi",
"zh",
"zh-CN",
"zh-TW",
],
fallbackLng: "en",
debug: false,
@@ -117,8 +121,8 @@ i18n
el: {
translation: elTranslation,
},
es: {
translation: esTranslation,
"es-ES": {
translation: esESTranslation,
},
fi: {
translation: fiTranslation,
@@ -156,8 +160,11 @@ i18n
pl: {
translation: plTranslation,
},
pt: {
translation: ptTranslation,
"pt-PT": {
translation: ptPTTranslation,
},
"pt-BR": {
translation: ptBRTranslation,
},
ro: {
translation: roTranslation,
@@ -168,8 +175,8 @@ i18n
sr: {
translation: srTranslation,
},
sv: {
translation: svTranslation,
"sv-SE": {
translation: svSETranslation,
},
th: {
translation: thTranslation,
@@ -183,8 +190,11 @@ i18n
vi: {
translation: viTranslation,
},
zh: {
translation: zhTranslation,
"zh-CN": {
translation: zhCNTranslation,
},
"zh-TW": {
translation: zhTWTranslation,
},
},
+5
View File
@@ -0,0 +1,5 @@
export function getBasePath(): string {
const base = import.meta.env.BASE_URL || "/";
if (base === "./" || base === "/") return "";
return base.endsWith("/") ? base.slice(0, -1) : base;
}
+38 -22
View File
@@ -6,6 +6,8 @@ class DatabaseHealthMonitor {
private lastCheckTime: number = 0;
private checkInProgress: boolean = false;
private listeners: Map<string, EventListener[]> = new Map();
private consecutiveErrorTimer: ReturnType<typeof setTimeout> | null = null;
private confirmedUnhealthy: boolean = false;
private constructor() {}
@@ -40,10 +42,13 @@ class DatabaseHealthMonitor {
}
}
reportDatabaseError(error: any, wasAuthenticated: boolean = false) {
reportSessionExpired() {
this.emit("session-expired", { timestamp: Date.now() });
}
reportDatabaseError(error: any, _wasAuthenticated: boolean = false) {
const errorMessage = error?.response?.data?.error || error?.message || "";
const errorCode = error?.response?.data?.code || error?.code;
const httpStatus = error?.response?.status;
const isDatabaseError =
errorMessage.toLowerCase().includes("database") ||
@@ -58,35 +63,44 @@ class DatabaseHealthMonitor {
(errorMessage.toLowerCase().includes("network error") &&
error?.response === undefined);
const isAuthenticationLost =
wasAuthenticated &&
httpStatus === 401 &&
(errorCode === "AUTH_REQUIRED" ||
errorCode === "SESSION_EXPIRED" ||
errorCode === "SESSION_NOT_FOUND" ||
errorMessage === "Missing authentication token" ||
errorMessage === "Invalid token" ||
errorMessage === "Authentication required");
if (!(isDatabaseError || isBackendUnreachable)) {
return;
}
if (
(isDatabaseError || isBackendUnreachable || isAuthenticationLost) &&
this.dbHealthy
) {
this.dbHealthy = false;
this.emit("database-connection-lost", {
error: errorMessage || "Backend server unreachable",
code: errorCode,
timestamp: Date.now(),
});
if (this.dbHealthy && !this.consecutiveErrorTimer) {
this.consecutiveErrorTimer = setTimeout(() => {
this.consecutiveErrorTimer = null;
if (this.dbHealthy) {
this.dbHealthy = false;
this.confirmedUnhealthy = true;
this.emit("database-connection-lost", {
error: errorMessage || "Backend server unreachable",
code: errorCode,
timestamp: Date.now(),
});
}
}, 10000);
}
}
reportDatabaseSuccess() {
if (!this.dbHealthy) {
this.clearErrorTimer();
if (this.confirmedUnhealthy) {
this.dbHealthy = true;
this.confirmedUnhealthy = false;
this.emit("database-connection-restored", {
timestamp: Date.now(),
});
} else if (!this.dbHealthy) {
this.dbHealthy = true;
}
}
private clearErrorTimer(): void {
if (this.consecutiveErrorTimer !== null) {
clearTimeout(this.consecutiveErrorTimer);
this.consecutiveErrorTimer = null;
}
}
@@ -96,8 +110,10 @@ class DatabaseHealthMonitor {
reset() {
this.dbHealthy = true;
this.confirmedUnhealthy = false;
this.lastCheckTime = 0;
this.checkInProgress = false;
this.clearErrorTimer();
}
}
+43 -4
View File
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Credentials",
"credentialsViewer": "Credentials Viewer",
"manageYourSSHCredentials": "Manage your SSH credentials securely",
"addCredential": "Add Credential",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "This will safely add the public key to the target host's ~/.ssh/authorized_keys file without overwriting existing keys. The operation is reversible.",
"chooseHostToDeploy": "Choose a host to deploy to...",
"deploying": "Deploying...",
"copyDeployCommand": "Copy Deploy Command",
"copyDeployCommandDescription": "Copy a shell command to manually add the public key to a host's authorized_keys file. Useful when password authentication is disabled.",
"deployCommandCopied": "Deploy command copied to clipboard",
"manualDeployInfo": "Paste this command in the target host's terminal (e.g., Proxmox console, IPMI, or physical access) to add the SSH key.",
"name": "Name",
"noHostsAvailable": "No hosts available",
"noHostsMatchSearch": "No hosts match your search",
@@ -298,7 +303,9 @@
"createFolder": "Create Folder",
"editFolder": "Edit Folder",
"editFolderDescription": "Customize your snippet folder",
"createFolderDescription": "Organize your snippets into folders"
"createFolderDescription": "Organize your snippets into folders",
"confirmExecution": "Execute \"{{name}}\"?",
"confirmExecutionDesc": "Are you sure you want to execute this snippet?"
},
"commandHistory": {
"title": "History",
@@ -402,6 +409,7 @@
"required": "Required",
"optional": "Optional",
"connect": "Connect",
"copied": "Copied",
"connecting": "Connecting...",
"creating": "Creating...",
"clear": "Clear",
@@ -494,6 +502,7 @@
"checking": "Checking...",
"checkingDatabase": "Checking database connection...",
"checkingAuthentication": "Checking authentication...",
"backendReconnected": "Server connection restored",
"actions": "Actions",
"remove": "Remove",
"revoke": "Revoke",
@@ -814,6 +823,7 @@
},
"hosts": {
"title": "Host Manager",
"hosts": "Hosts",
"sshHosts": "SSH Hosts",
"noHosts": "No SSH Hosts",
"noHostsMessage": "You haven't added any SSH hosts yet. Click \"Add Host\" to get started.",
@@ -844,7 +854,7 @@
"failedToImportJson": "Failed to import JSON file",
"connectionDetails": "Connection Details",
"organization": "Organization",
"ipAddress": "IP Address",
"ipAddress": "IP Address or Hostname",
"port": "Port",
"name": "Name",
"username": "Username",
@@ -923,6 +933,8 @@
"key": "Key",
"credential": "Credential",
"none": "None",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Prompts you for OPKSSH web-auth upon every connect. Your auth token will last for 24 hours until it must be renewed.",
"selectCredential": "Select Credential",
"selectCredentialPlaceholder": "Choose a credential...",
"credentialRequired": "Credential is required when using credential authentication",
@@ -1093,6 +1105,7 @@
"noneAuthTitle": "Keyboard-Interactive Authentication",
"noneAuthDescription": "This authentication method will use keyboard-interactive authentication when connecting to the SSH server.",
"noneAuthDetails": "Keyboard-interactive authentication allows the server to prompt you for credentials during connection. This is useful for servers that require multi-factor authentication or if you do not want to save credentials locally.",
"opksshAuthTitle": "OPKSSH Authentication",
"forceKeyboardInteractive": "Force Keyboard-Interactive",
"forceKeyboardInteractiveDesc": "Forces the use of keyboard-interactive authentication. This is sometimes required for servers that use Two-Factor Authentication (TOTP/2FA).",
"overrideCredentialUsername": "Override Credential Username",
@@ -1349,6 +1362,15 @@
"warpgateOpenBrowser": "Open in Browser",
"warpgateContinue": "I've Completed Authentication",
"warpgateTimeout": "Warpgate authentication timeout. Please reconnect.",
"opksshAuthRequired": "OPKSSH Authentication Required",
"opksshAuthDescription": "Complete authentication in your browser to continue. This session will remain valid for 24 hours.",
"opksshAuthUrl": "Authentication URL",
"opksshOpenBrowser": "Open Browser to Authenticate",
"opksshWaitingForAuth": "Waiting for authentication in browser...",
"opksshAuthenticating": "Processing authentication...",
"opksshTimeout": "Authentication timed out. Please try again.",
"opksshAuthFailed": "Authentication failed. Please check your credentials and try again.",
"opksshConfigMissing": "OPKSSH configuration not found. Please create ~/.opk/config.yml with your OIDC provider settings. See documentation: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Insert Password?",
"sudoPasswordPopupHint": "Press Enter to insert, Esc to dismiss",
"sudoPasswordPopupConfirm": "Insert",
@@ -1370,7 +1392,8 @@
"automaticFallback": "Automatically trying {{method}} authentication...",
"totpTimeout": "TOTP verification timeout. Please reconnect.",
"passwordTimeout": "Password verification timeout. Please reconnect.",
"connectionRejected": "Connection rejected by server. Please check your authentication and network configuration."
"connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.",
"hostKeyRejected": "SSH host key verification rejected. Connection cancelled."
},
"fileManager": {
"title": "File Manager",
@@ -1973,6 +1996,20 @@
"passwordResetSuccess": "Password Reset Successful",
"passwordResetSuccessDesc": "Your password has been reset successfully. You can now log in with your new password."
},
"hostKey": {
"verifyNewHost": "Verify SSH Host Key",
"keyChangedWarning": "SSH Host Key Changed",
"firstConnectionTitle": "First time connecting to this host",
"firstConnectionDescription": "The authenticity of this host cannot be established. Verify the fingerprint matches what you expect.",
"keyChangedDescription": "The host key for this server has changed since your last connection. This could indicate a security issue.",
"previousKey": "Previous Key",
"newFingerprint": "New Fingerprint",
"fingerprint": "Fingerprint",
"verifyInstructions": "If you trust this host, click Accept to continue and save this fingerprint for future connections.",
"securityWarning": "Security Warning",
"acceptAndContinue": "Accept & Continue",
"acceptNewKey": "Accept New Key & Continue"
},
"errors": {
"notFound": "Page not found",
"unauthorized": "Unauthorized access",
@@ -2065,6 +2102,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Host & Sidebar",
"snippetsSettings": "Snippets",
"confirmSnippetExecution": "Confirm Snippet Execution",
"confirmSnippetExecutionDesc": "Show confirmation dialog before executing snippets",
"updateSettings": "Updates",
"disableUpdateCheck": "Disable Update Check",
"disableUpdateCheckDesc": "Stop checking for new versions on startup and dashboard. Reduces network requests.",
@@ -2085,7 +2124,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 or example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Geloofsbriewe",
"credentialsViewer": "Geloofsbriewe-kyker",
"manageYourSSHCredentials": "Bestuur jou SSH-bewyse veilig",
"addCredential": "Voeg geloofsbriewe by",
@@ -99,6 +100,10 @@
"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...",
"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",
"noHostsAvailable": "Geen gashere beskikbaar nie",
"noHostsMatchSearch": "Geen gashere stem ooreen met jou soektog nie",
@@ -298,7 +303,9 @@
"createFolder": "Skep vouer",
"editFolder": "Wysig vouer",
"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": {
"title": "Geskiedenis",
@@ -402,6 +409,7 @@
"required": "Vereis",
"optional": "Opsioneel",
"connect": "Verbind",
"copied": "Gekopieer",
"connecting": "Verbind...",
"creating": "Skep...",
"clear": "Duidelik",
@@ -494,6 +502,7 @@
"checking": "Kontroleer...",
"checkingDatabase": "Kontroleer databasisverbinding...",
"checkingAuthentication": "Kontroleer tans verifikasie...",
"backendReconnected": "Bedienerverbinding herstel",
"actions": "Aksies",
"remove": "Verwyder",
"revoke": "Herroep",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Gasheerbestuurder",
"hosts": "Gashere",
"sshHosts": "SSH-gashere",
"noHosts": "Geen SSH-gashere nie",
"noHostsMessage": "Jy het nog geen SSH-gashere bygevoeg nie. Klik \"Voeg gasheer by\" om te begin.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Kon nie JSON-lêer invoer nie",
"connectionDetails": "Verbindingsbesonderhede",
"organization": "Organisasie",
"ipAddress": "IP-adres",
"ipAddress": "IP-adres of gasheernaam",
"port": "Hawe",
"name": "Naam",
"username": "Gebruikersnaam",
@@ -915,6 +925,8 @@
"key": "Sleutel",
"credential": "Geloofsbrief",
"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",
"selectCredentialPlaceholder": "Kies 'n geloofsbrief...",
"credentialRequired": "Geloofsbriewe word vereis wanneer geloofsbriewe-verifikasie gebruik word",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Sleutelbord-Interaktiewe Verifikasie",
"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.",
"opksshAuthTitle": "OPKSSH-verifikasie",
"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.",
"overrideCredentialUsername": "Oorskryf geloofsbriewe gebruikersnaam",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Maak oop in blaaier",
"warpgateContinue": "Ek het verifikasie voltooi",
"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?",
"sudoPasswordPopupHint": "Druk Enter om in te voeg, Esc om te verwerp",
"sudoPasswordPopupConfirm": "Invoeg",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Probeer outomaties {{method}} verifikasie...",
"totpTimeout": "TOTP-verifikasie het uitgeloop. Koppel asseblief weer aan.",
"passwordTimeout": "Wagwoordverifikasie het uitgeloop. Koppel asseblief weer aan.",
"connectionRejected": "Verbinding deur bediener verwerp. Kontroleer asseblief u verifikasie en netwerkkonfigurasie."
"connectionRejected": "Verbinding deur bediener verwerp. Kontroleer asseblief u verifikasie en netwerkkonfigurasie.",
"hostKeyRejected": "SSH-gasheersleutelverifikasie verwerp. Verbinding gekanselleer."
},
"fileManager": {
"title": "Lêerbestuurder",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Wagwoordherstel suksesvol",
"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": {
"notFound": "Bladsy nie gevind nie",
"unauthorized": "Ongemagtigde toegang",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminaal",
"hostSidebarSettings": "Gasheer en sybalk",
"snippetsSettings": "Brokkies",
"confirmSnippetExecution": "Bevestig Uitvoering van Brokkie",
"confirmSnippetExecutionDesc": "Wys bevestigingsdialoog voordat brokkies uitgevoer word",
"updateSettings": "Opdaterings",
"disableUpdateCheck": "Deaktiveer Opdateringskontrole",
"disableUpdateCheckDesc": "Hou op om vir nuwe weergawes te kyk tydens opstart en op die dashboard. Verminder netwerkversoeke.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 of example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "وثائق التفويض",
"credentialsViewer": "عارض وثائق التفويض",
"manageYourSSHCredentials": "إدارة بيانات اعتماد SSH الخاصة بك بشكل آمن",
"addCredential": "إضافة بيانات اعتماد",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "سيؤدي هذا بأمان إلى إضافة المفتاح العمومي إلى ملف المضيف الهدف ~/.ssh/authorized_keyys دون الكتابة فوق المفاتيح الموجودة. العملية قابلة للرجوع عنها.",
"chooseHostToDeploy": "اختر مضيف ليقوم بنشره إلى...",
"deploying": "النشر...",
"copyDeployCommand": "نسخ أمر النشر",
"copyDeployCommandDescription": "نسخ أمر Shell لإضافة المفتاح العام يدوياً إلى ملف مفتاح_مفاتيح مضيف مضيف. مفيد عندما يتم تعطيل مصادقة كلمة المرور.",
"deployCommandCopied": "نشر أمر نسخ إلى الحافظة",
"manualDeployInfo": "قم بلصق هذا الأمر في محطة المضيف المستهدفة (مثل وحدة تحكم Proxmox أو IPMI، أو الوصول المادي) لإضافة مفتاح SSH.",
"name": "الاسم",
"noHostsAvailable": "لا يوجد مضيفين متاحين",
"noHostsMatchSearch": "لا يوجد مضيف يطابق بحثك",
@@ -298,7 +303,9 @@
"createFolder": "إنشاء مجلد",
"editFolder": "تعديل المجلد",
"editFolderDescription": "تخصيص مجلد كتلة الكود",
"createFolderDescription": "تنظيم كتل الكتل في مجلدات"
"createFolderDescription": "تنظيم كتل الكتل في مجلدات",
"confirmExecution": "تنفيذ \"{{name}}\"؟",
"confirmExecutionDesc": "هل أنت متأكد من أنك تريد تنفيذ كتلة الكود هذه؟"
},
"commandHistory": {
"title": "التاريخ",
@@ -402,6 +409,7 @@
"required": "مطلوب",
"optional": "اختياري",
"connect": "الاتصال",
"copied": "منسوخ",
"connecting": "جاري الاتصال...",
"creating": "إنشاء...",
"clear": "مسح",
@@ -494,6 +502,7 @@
"checking": "يتحقق...",
"checkingDatabase": "التحقق من اتصال قاعدة البيانات...",
"checkingAuthentication": "التحقق من المصادقة...",
"backendReconnected": "تم استعادة اتصال الخادم",
"actions": "الإجراءات",
"remove": "إزالة",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "مدير المضيف",
"hosts": "المضيفون",
"sshHosts": "مضيفي SSH",
"noHosts": "لا يوجد مضيفين SSH",
"noHostsMessage": "لم تقم بإضافة أي مضيف SSH حتى الآن. انقر فوق \"إضافة مضيف\" للبدء.",
@@ -836,7 +846,7 @@
"failedToImportJson": "فشل استيراد ملف JSON",
"connectionDetails": "تفاصيل الاتصال",
"organization": "المنظمة",
"ipAddress": "عنوان IP",
"ipAddress": "عنوان IP أو اسم المضيف",
"port": "المنفذ",
"name": "الاسم",
"username": "اسم المستخدم",
@@ -915,6 +925,8 @@
"key": "المفتاح",
"credential": "بيانات",
"none": "لا",
"opkssh": "فتح",
"opksshAuthDescription": "يمنحك الحصول على مصادقة ويب OPKSSH عند كل اتصال. رمز المصادقة الخاص بك سوف يستمر لمدة 24 ساعة حتى يجب تجديده.",
"selectCredential": "حدد بيانات الاعتماد",
"selectCredentialPlaceholder": "اختر بيانات الاعتماد...",
"credentialRequired": "الاعتماد مطلوب عند استخدام مصادقة الاعتماد",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "المصادقة التفاعلية على لوحة المفاتيح",
"noneAuthDescription": "طريقة المصادقة هذه ستستخدم مصادقة لوحة المفاتيح التفاعلية عند الاتصال بخادم SSH.",
"noneAuthDetails": "المصادقة التفاعلية بلوحة المفاتيح تسمح للخادم بمطالبتك للحصول على بيانات الاعتماد أثناء الاتصال. هذا مفيد للخوادم التي تتطلب مصادقة متعددة العناصر أو إذا كنت لا تريد حفظ بيانات الاعتماد محلياً.",
"opksshAuthTitle": "المصادقة المفتوحة",
"forceKeyboardInteractive": "تفعيل لوحة المفاتيح",
"forceKeyboardInteractiveDesc": "يفرض استخدام مصادقة لوحة المفاتيح التفاعلية. هذا مطلوب أحيانا للخوادم التي تستخدم المصادقة الثنائية (TOTP/2FA).",
"overrideCredentialUsername": "تجاوز اسم المستخدم",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "فتح في المتصفح",
"warpgateContinue": "لقد أكملت المصادقة",
"warpgateTimeout": "انتهت مهلة مصادقة التحذير. الرجاء إعادة الاتصال.",
"opksshAuthRequired": "المصادقة مفتوحة مطلوبة",
"opksshAuthDescription": "أكمل المصادقة في المتصفح الخاص بك للمتابعة. ستبقى هذه الجلسة صالحة لمدة 24 ساعة.",
"opksshAuthUrl": "رابط المصادقة",
"opksshOpenBrowser": "فتح المتصفح للمصادقة",
"opksshWaitingForAuth": "في انتظار المصادقة في المتصفح...",
"opksshAuthenticating": "جاري معالجة المصادقة...",
"opksshTimeout": "انتهت مهلة المصادقة. الرجاء المحاولة مرة أخرى.",
"opksshAuthFailed": "فشلت المصادقة. الرجاء التحقق من بيانات الاعتماد الخاصة بك وحاول مرة أخرى.",
"opksshConfigMissing": "لم يتم العثور على تكوين الفتح. الرجاء إنشاء ~/.opk/config.yml مع إعدادات موفر OIDC. انظر الوثائق: https://github.com/openpubkey/opkssh#config.",
"sudoPasswordPopupTitle": "إدراج كلمة المرور؟",
"sudoPasswordPopupHint": "اضغط Enter للإدراج ، Esc للإلغاء",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "جرب مصادقة {{method}} تلقائياً...",
"totpTimeout": "انتهت مهلة التحقق من TOTP. الرجاء إعادة الاتصال.",
"passwordTimeout": "انتهت مهلة التحقق من كلمة المرور. الرجاء إعادة الاتصال.",
"connectionRejected": "تم رفض الاتصال بواسطة الخادم. الرجاء التحقق من المصادقة الخاصة بك وتكوين الشبكة."
"connectionRejected": "تم رفض الاتصال بواسطة الخادم. الرجاء التحقق من المصادقة الخاصة بك وتكوين الشبكة.",
"hostKeyRejected": "تم رفض التحقق من مفتاح المضيف SSH. تم إلغاء الاتصال."
},
"fileManager": {
"title": "مدير الملفات",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "تم إعادة تعيين كلمة المرور بنجاح",
"passwordResetSuccessDesc": "تم إعادة تعيين كلمة المرور الخاصة بك بنجاح. يمكنك الآن تسجيل الدخول باستخدام كلمة المرور الجديدة."
},
"hostKey": {
"verifyNewHost": "التحقق من مفتاح استضافة SSH",
"keyChangedWarning": "تم تغيير مفتاح المضيف SSH",
"firstConnectionTitle": "أول مرة تتصل بهذا المضيف",
"firstConnectionDescription": "لا يمكن إنشاء صحة هذا المضيف. تحقق من بصمة الإصبع تطابق ما تتوقعه.",
"keyChangedDescription": "مفتاح المضيف لهذا الخادم قد تغير منذ الاتصال الأخير. يمكن أن يشير هذا إلى مشكلة أمنية.",
"previousKey": "المفتاح السابق",
"newFingerprint": "بصمة جديدة",
"fingerprint": "بصمة",
"verifyInstructions": "إذا كنت تثق بهذا المضيف، انقر فوق قبول للمتابعة وحفظ بصمة الإصبع هذه للاتصال في المستقبل.",
"securityWarning": "تحذير الأمان",
"acceptAndContinue": "قبول ومتابعة",
"acceptNewKey": "قبول المفتاح الجديد ومتابعة"
},
"errors": {
"notFound": "لم يتم العثور على الصفحة",
"unauthorized": "الوصول غير المصرح به",
@@ -2026,6 +2063,8 @@
"terminalSettings": "المحطة",
"hostSidebarSettings": "المضيف و الشريط الجانبي",
"snippetsSettings": "كتل الكود",
"confirmSnippetExecution": "تأكيد تنفيذ كتلة الكود",
"confirmSnippetExecutionDesc": "إظهار مربع حوار التأكيد قبل تنفيذ كتل الكود",
"updateSettings": "التحديثات",
"disableUpdateCheck": "تعطيل التحقق من التحديث",
"disableUpdateCheckDesc": "إيقاف التحقق من الإصدارات الجديدة عند بدء التشغيل و لوحة التحكم. يقلل من طلبات الشبكة.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1-1 أو مثال.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Пълномощия",
"credentialsViewer": "Преглед на идентификационни данни",
"manageYourSSHCredentials": "Управлявайте сигурно вашите SSH идентификационни данни",
"addCredential": "Добавяне на идентификационни данни",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Това безопасно ще добави публичния ключ към файла ~/.ssh/authorized_keys на целевия хост, без да презаписва съществуващите ключове. Операцията е обратима.",
"chooseHostToDeploy": "Изберете хост, на който да се разположи...",
"deploying": "Разгръщане...",
"copyDeployCommand": "Копиране на командата за разгръщане",
"copyDeployCommandDescription": "Копирайте команда от shell, за да добавите ръчно публичния ключ към файла authorized_keys на хоста. Полезно е, когато удостоверяването с парола е деактивирано.",
"deployCommandCopied": "Командата за разгръщане е копирана в клипборда",
"manualDeployInfo": "Поставете тази команда в терминала на целевия хост (напр. конзола на Proxmox, IPMI или физически достъп), за да добавите SSH ключа.",
"name": "Име",
"noHostsAvailable": "Няма налични хостове",
"noHostsMatchSearch": "Няма хостове, които да отговарят на вашето търсене",
@@ -298,7 +303,9 @@
"createFolder": "Създаване на папка",
"editFolder": "Редактиране на папка",
"editFolderDescription": "Персонализирайте папката си с фрагменти",
"createFolderDescription": "Организирайте фрагментите си в папки"
"createFolderDescription": "Организирайте фрагментите си в папки",
"confirmExecution": "Да се изпълни „{{name}}“?",
"confirmExecutionDesc": "Сигурни ли сте, че искате да изпълните този фрагмент?"
},
"commandHistory": {
"title": "История",
@@ -402,6 +409,7 @@
"required": "Задължително",
"optional": "По избор",
"connect": "Свържете се",
"copied": "Копирано",
"connecting": "Свързване...",
"creating": "Създаване...",
"clear": "Изчисти",
@@ -494,6 +502,7 @@
"checking": "Проверка...",
"checkingDatabase": "Проверка на връзката с базата данни...",
"checkingAuthentication": "Проверка на удостоверяването...",
"backendReconnected": "Връзката със сървъра е възстановена",
"actions": "Действия",
"remove": "Премахване",
"revoke": "Отмяна",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Мениджър на домакини",
"hosts": "Домакини",
"sshHosts": "SSH хостове",
"noHosts": "Няма SSH хостове",
"noHostsMessage": "Все още не сте добавили SSH хостове. Кликнете върху „Добавяне на хост“, за да започнете.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Импортирането на JSON файл не бе успешно",
"connectionDetails": "Детайли за връзката",
"organization": "Организация",
"ipAddress": "IP адрес",
"ipAddress": "IP адрес или име на хост",
"port": "Порт",
"name": "Име",
"username": "Потребителско име",
@@ -915,6 +925,8 @@
"key": "Ключ",
"credential": "Пълномощия",
"none": "Няма",
"opkssh": "ОПКСШ",
"opksshAuthDescription": "Подканва ви за OPKSSH уеб-авторизация при всяко свързване. Вашият токен за удостоверяване ще бъде валиден 24 часа, докато не се наложи да бъде подновен.",
"selectCredential": "Изберете идентификационни данни",
"selectCredentialPlaceholder": "Изберете удостоверение за самоличност...",
"credentialRequired": "Изисква се удостоверяване на идентификационни данни при използване на удостоверяване с идентификационни данни",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Интерактивно удостоверяване с клавиатура",
"noneAuthDescription": "Този метод за удостоверяване ще използва интерактивно удостоверяване с клавиатура при свързване към SSH сървъра.",
"noneAuthDetails": "Интерактивното удостоверяване с клавиатура позволява на сървъра да ви пита за идентификационни данни по време на връзка. Това е полезно за сървъри, които изискват многофакторно удостоверяване или ако не искате да запазвате идентификационните данни локално.",
"opksshAuthTitle": "OPKSSH удостоверяване",
"forceKeyboardInteractive": "Принудително интерактивно с клавиатура",
"forceKeyboardInteractiveDesc": "Налага използването на интерактивно удостоверяване с клавиатура. Това понякога е необходимо за сървъри, които използват двуфакторно удостоверяване (TOTP/2FA).",
"overrideCredentialUsername": "Замяна на потребителско име за идентификационни данни",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Отваряне в браузъра",
"warpgateContinue": "Завърших удостоверяването",
"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": "Въвеждане на парола?",
"sudoPasswordPopupHint": "Натиснете Enter за вмъкване, Esc за отхвърляне",
"sudoPasswordPopupConfirm": "Вмъкване",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Автоматичен опит за {{method}} удостоверяване...",
"totpTimeout": "Времето за изчакване за проверка на TOTP изтече. Моля, свържете се отново.",
"passwordTimeout": "Времето за изчакване за потвърждение на паролата изтече. Моля, свържете се отново.",
"connectionRejected": "Връзката е отхвърлена от сървъра. Моля, проверете удостоверяването и мрежовата си конфигурация."
"connectionRejected": "Връзката е отхвърлена от сървъра. Моля, проверете удостоверяването и мрежовата си конфигурация.",
"hostKeyRejected": "Проверката на SSH ключа за хост е отхвърлена. Връзката е прекратена."
},
"fileManager": {
"title": "Файлов мениджър",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Успешно възстановяване на паролата",
"passwordResetSuccessDesc": "Паролата ви е нулирана успешно. Вече можете да влезете с новата си парола."
},
"hostKey": {
"verifyNewHost": "Проверка на SSH хост ключа",
"keyChangedWarning": "SSH ключът на хоста е променен",
"firstConnectionTitle": "Първо свързване с този хост",
"firstConnectionDescription": "Автентичността на този хост не може да бъде установена. Проверете дали пръстовият отпечатък съответства на очакванията ви.",
"keyChangedDescription": "Ключът на хоста за този сървър се е променил от последното ви свързване. Това може да показва проблем със сигурността.",
"previousKey": "Предишен ключ",
"newFingerprint": "Нов пръстов отпечатък",
"fingerprint": "Пръстов отпечатък",
"verifyInstructions": "Ако имате доверие на този хост, щракнете върху „Приемам“, за да продължите и да запазите този пръстов отпечатък за бъдещи връзки.",
"securityWarning": "Предупреждение за сигурност",
"acceptAndContinue": "Приемам и продължавам",
"acceptNewKey": "Приемете новия ключ и продължете"
},
"errors": {
"notFound": "Страницата не е намерена",
"unauthorized": "Неоторизиран достъп",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Терминал",
"hostSidebarSettings": "Хост и странична лента",
"snippetsSettings": "Откъси",
"confirmSnippetExecution": "Потвърждаване на изпълнението на фрагмента",
"confirmSnippetExecutionDesc": "Показване на диалогов прозорец за потвърждение преди изпълнение на фрагменти",
"updateSettings": "Актуализации",
"disableUpdateCheck": "Деактивиране на проверката за актуализации",
"disableUpdateCheckDesc": "Спрете проверката за нови версии при стартиране и таблото за управление. Намалява мрежовите заявки.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 или example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "পরিচয়পত্র",
"credentialsViewer": "শংসাপত্র দর্শক",
"manageYourSSHCredentials": "আপনার SSH শংসাপত্রগুলি নিরাপদে পরিচালনা করুন",
"addCredential": "শংসাপত্র যোগ করুন",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "এটি বিদ্যমান কীগুলিকে ওভাররাইট না করেই টার্গেট হোস্টের ~/.ssh/authorized_keys ফাইলে নিরাপদে পাবলিক কী যুক্ত করবে। এই অপারেশনটি বিপরীতমুখী।",
"chooseHostToDeploy": "স্থাপনের জন্য একটি হোস্ট বেছে নিন...",
"deploying": "স্থাপন করা হচ্ছে...",
"copyDeployCommand": "ডিপ্লয় কমান্ড কপি করুন",
"copyDeployCommandDescription": "হোস্টের authorized_keys ফাইলে পাবলিক কী ম্যানুয়ালি যোগ করার জন্য একটি শেল কমান্ড কপি করুন। পাসওয়ার্ড প্রমাণীকরণ অক্ষম থাকলে এটি কার্যকর।",
"deployCommandCopied": "ডিপ্লয় কমান্ড ক্লিপবোর্ডে কপি করা হয়েছে",
"manualDeployInfo": "SSH কী যোগ করতে টার্গেট হোস্টের টার্মিনালে (যেমন, Proxmox কনসোল, IPMI, অথবা ফিজিক্যাল অ্যাক্সেস) এই কমান্ডটি পেস্ট করুন।",
"name": "নাম",
"noHostsAvailable": "কোনও হোস্ট উপলব্ধ নেই",
"noHostsMatchSearch": "আপনার অনুসন্ধানের সাথে কোনও হোস্ট মেলেনি।",
@@ -298,7 +303,9 @@
"createFolder": "ফোল্ডার তৈরি করুন",
"editFolder": "ফোল্ডার সম্পাদনা করুন",
"editFolderDescription": "আপনার স্নিপেট ফোল্ডারটি কাস্টমাইজ করুন",
"createFolderDescription": "আপনার স্নিপেটগুলিকে ফোল্ডারে সাজান"
"createFolderDescription": "আপনার স্নিপেটগুলিকে ফোল্ডারে সাজান",
"confirmExecution": "\"{{name}}\" কার্যকর করবেন?",
"confirmExecutionDesc": "আপনি কি নিশ্চিত যে আপনি এই স্নিপেটটি কার্যকর করতে চান?"
},
"commandHistory": {
"title": "ইতিহাস",
@@ -402,6 +409,7 @@
"required": "প্রয়োজনীয়",
"optional": "ঐচ্ছিক",
"connect": "সংযোগ করুন",
"copied": "কপি করা হয়েছে",
"connecting": "সংযোগ করা হচ্ছে...",
"creating": "তৈরি করা হচ্ছে...",
"clear": "পরিষ্কার",
@@ -494,6 +502,7 @@
"checking": "পরীক্ষা করা হচ্ছে...",
"checkingDatabase": "ডাটাবেস সংযোগ পরীক্ষা করা হচ্ছে...",
"checkingAuthentication": "প্রমাণীকরণ পরীক্ষা করা হচ্ছে...",
"backendReconnected": "সার্ভার সংযোগ পুনরুদ্ধার করা হয়েছে",
"actions": "কর্ম",
"remove": "অপসারণ",
"revoke": "প্রত্যাহার করুন",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "হোস্ট ম্যানেজার",
"hosts": "হোস্ট",
"sshHosts": "SSH হোস্ট",
"noHosts": "কোনও SSH হোস্ট নেই",
"noHostsMessage": "আপনি এখনও কোনও SSH হোস্ট যোগ করেননি। শুরু করতে \"হোস্ট যোগ করুন\" এ ক্লিক করুন।",
@@ -836,7 +846,7 @@
"failedToImportJson": "JSON ফাইল আমদানি করা যায়নি",
"connectionDetails": "সংযোগের বিবরণ",
"organization": "সংগঠন",
"ipAddress": "আইপি ঠিকানা",
"ipAddress": "আইপি ঠিকানা অথবা হোস্টনেম",
"port": "বন্দর",
"name": "নাম",
"username": "ব্যবহারকারীর নাম",
@@ -915,6 +925,8 @@
"key": "চাবি",
"credential": "পরিচয়পত্র",
"none": "কোনটিই নয়",
"opkssh": "OPKSSH সম্পর্কে",
"opksshAuthDescription": "প্রতিটি সংযোগের সময় আপনাকে OPKSSH ওয়েব-অনুমোদনের জন্য অনুরোধ করবে। আপনার অথ টোকেনটি পুনর্নবীকরণ না করা পর্যন্ত 24 ঘন্টা স্থায়ী হবে।",
"selectCredential": "শংসাপত্র নির্বাচন করুন",
"selectCredentialPlaceholder": "একটি শংসাপত্র বেছে নিন...",
"credentialRequired": "শংসাপত্র প্রমাণীকরণ ব্যবহার করার সময় শংসাপত্র প্রয়োজন",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণ",
"noneAuthDescription": "এই প্রমাণীকরণ পদ্ধতিটি SSH সার্ভারের সাথে সংযোগ করার সময় কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণ ব্যবহার করবে।",
"noneAuthDetails": "কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণের মাধ্যমে সার্ভার সংযোগের সময় আপনাকে শংসাপত্রের জন্য অনুরোধ করতে পারে। এটি এমন সার্ভারগুলির জন্য কার্যকর যাদের মাল্টি-ফ্যাক্টর প্রমাণীকরণের প্রয়োজন হয় অথবা যদি আপনি স্থানীয়ভাবে শংসাপত্র সংরক্ষণ করতে না চান।",
"opksshAuthTitle": "OPKSSH প্রমাণীকরণ",
"forceKeyboardInteractive": "জোর করে কীবোর্ড-ইন্টারেক্টিভ",
"forceKeyboardInteractiveDesc": "কীবোর্ড-ইন্টারেক্টিভ প্রমাণীকরণ ব্যবহার করতে বাধ্য করে। কখনও কখনও টু-ফ্যাক্টর প্রমাণীকরণ (TOTP/2FA) ব্যবহার করে এমন সার্ভারগুলির জন্য এটি প্রয়োজন হয়।",
"overrideCredentialUsername": "শংসাপত্র ব্যবহারকারীর নাম ওভাররাইড করুন",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "ব্রাউজারে খুলুন",
"warpgateContinue": "আমি প্রমাণীকরণ সম্পন্ন করেছি।",
"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": "পাসওয়ার্ড ঢোকাবেন?",
"sudoPasswordPopupHint": "সন্নিবেশ করতে Enter টিপুন এবং খারিজ করতে Esc টিপুন",
"sudoPasswordPopupConfirm": "ঢোকান",
@@ -1337,7 +1359,8 @@
"automaticFallback": "স্বয়ংক্রিয়ভাবে {{method}} প্রমাণীকরণ চেষ্টা করা হচ্ছে...",
"totpTimeout": "TOTP যাচাইকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।",
"passwordTimeout": "পাসওয়ার্ড যাচাইকরণের সময়সীমা শেষ। অনুগ্রহ করে পুনরায় সংযোগ করুন।",
"connectionRejected": "সার্ভার সংযোগ প্রত্যাখ্যান করেছে। অনুগ্রহ করে আপনার প্রমাণীকরণ এবং নেটওয়ার্ক কনফিগারেশন পরীক্ষা করুন।"
"connectionRejected": "সার্ভার সংযোগ প্রত্যাখ্যান করেছে। অনুগ্রহ করে আপনার প্রমাণীকরণ এবং নেটওয়ার্ক কনফিগারেশন পরীক্ষা করুন।",
"hostKeyRejected": "SSH হোস্ট কী যাচাইকরণ প্রত্যাখ্যাত হয়েছে। সংযোগ বাতিল করা হয়েছে।"
},
"fileManager": {
"title": "ফাইল ম্যানেজার",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "পাসওয়ার্ড রিসেট সফল হয়েছে",
"passwordResetSuccessDesc": "আপনার পাসওয়ার্ড সফলভাবে রিসেট করা হয়েছে। আপনি এখন আপনার নতুন পাসওয়ার্ড দিয়ে লগ ইন করতে পারেন।"
},
"hostKey": {
"verifyNewHost": "SSH হোস্ট কী যাচাই করুন",
"keyChangedWarning": "SSH হোস্ট কী পরিবর্তন করা হয়েছে",
"firstConnectionTitle": "এই হোস্টের সাথে প্রথমবার সংযোগ করা হচ্ছে",
"firstConnectionDescription": "এই হোস্টের সত্যতা যাচাই করা যাচ্ছে না। আপনার প্রত্যাশার সাথে আঙুলের ছাপের মিল আছে কিনা তা যাচাই করুন।",
"keyChangedDescription": "আপনার শেষ সংযোগের পর থেকে এই সার্ভারের হোস্ট কী পরিবর্তিত হয়েছে। এটি কোনও নিরাপত্তা সমস্যা নির্দেশ করতে পারে।",
"previousKey": "পূর্ববর্তী কী",
"newFingerprint": "নতুন ফিঙ্গারপ্রিন্ট",
"fingerprint": "আঙুলের ছাপ",
"verifyInstructions": "যদি আপনি এই হোস্টটিকে বিশ্বাস করেন, তাহলে চালিয়ে যেতে \"গ্রহণ করুন\" এ ক্লিক করুন এবং ভবিষ্যতের সংযোগের জন্য এই আঙ্গুলের ছাপটি সংরক্ষণ করুন।",
"securityWarning": "নিরাপত্তা সতর্কতা",
"acceptAndContinue": "গ্রহণ করুন এবং চালিয়ে যান",
"acceptNewKey": "নতুন কী গ্রহণ করুন এবং চালিয়ে যান"
},
"errors": {
"notFound": "পৃষ্ঠাটি খুঁজে পাওয়া যায়নি",
"unauthorized": "অননুমোদিত প্রবেশাধিকার",
@@ -2026,6 +2063,8 @@
"terminalSettings": "টার্মিনাল",
"hostSidebarSettings": "হোস্ট এবং সাইডবার",
"snippetsSettings": "স্নিপেটস",
"confirmSnippetExecution": "স্নিপেট এক্সিকিউশন নিশ্চিত করুন",
"confirmSnippetExecutionDesc": "স্নিপেট চালানোর আগে নিশ্চিতকরণ ডায়ালগ দেখান",
"updateSettings": "আপডেট",
"disableUpdateCheck": "আপডেট চেক অক্ষম করুন",
"disableUpdateCheckDesc": "স্টার্টআপ এবং ড্যাশবোর্ডে নতুন সংস্করণ পরীক্ষা করা বন্ধ করুন। নেটওয়ার্ক অনুরোধ হ্রাস করে।",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "০০০০০০",
"ipAddress": "১২৭...১",
"ipAddress": "১৯২.১৬৮.১.১ অথবা example.com",
"port": "২২",
"maxRetries": "৩",
"retryInterval": "১০",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Credencials",
"credentialsViewer": "Visualitzador de credencials",
"manageYourSSHCredentials": "Gestiona les teves credencials SSH de manera segura",
"addCredential": "Afegeix credencials",
@@ -99,6 +100,10 @@
"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...",
"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",
"noHostsAvailable": "No hi ha amfitrions disponibles",
"noHostsMatchSearch": "No hi ha amfitrions que coincideixin amb la teva cerca",
@@ -298,7 +303,9 @@
"createFolder": "Crea una carpeta",
"editFolder": "Edita la carpeta",
"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": {
"title": "Història",
@@ -402,6 +409,7 @@
"required": "Obligatori",
"optional": "Opcional",
"connect": "Connecta",
"copied": "Copiat",
"connecting": "Connectant...",
"creating": "Creant...",
"clear": "Clar",
@@ -494,6 +502,7 @@
"checking": "S'està revisant...",
"checkingDatabase": "S'està comprovant la connexió a la base de dades...",
"checkingAuthentication": "S'està comprovant l'autenticació...",
"backendReconnected": "Connexió al servidor restaurada",
"actions": "Accions",
"remove": "Elimina",
"revoke": "Revocar",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Gestor d'amfitrions",
"hosts": "Amfitrions",
"sshHosts": "Amfitrions SSH",
"noHosts": "Sense amfitrions SSH",
"noHostsMessage": "Encara no has afegit cap amfitrió SSH. Fes clic a \"Afegeix amfitrió\" per començar.",
@@ -836,7 +846,7 @@
"failedToImportJson": "No s'ha pogut importar el fitxer JSON",
"connectionDetails": "Detalls de la connexió",
"organization": "Organització",
"ipAddress": "Adreça IP",
"ipAddress": "Adreça IP o nom d'amfitrió",
"port": "Port",
"name": "Nom",
"username": "Nom d'usuari",
@@ -915,6 +925,8 @@
"key": "Clau",
"credential": "Credencial",
"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",
"selectCredentialPlaceholder": "Trieu una credencial...",
"credentialRequired": "Cal tenir credencials quan s'utilitza l'autenticació de credencials",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Autenticació interactiva amb teclat",
"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.",
"opksshAuthTitle": "Autenticació OPKSSH",
"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).",
"overrideCredentialUsername": "Substitueix les credencials i el nom d'usuari",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Obre al navegador",
"warpgateContinue": "He completat l'autenticació",
"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?",
"sudoPasswordPopupHint": "Premeu Intro per inserir, Esc per tancar",
"sudoPasswordPopupConfirm": "Insereix",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Intentant automàticament l'autenticació {{method}} ...",
"totpTimeout": "Temps d'espera de verificació TOTP superat. Si us plau, torneu a connectar-vos.",
"passwordTimeout": "Temps d'espera de verificació de la contrasenya. Si us plau, torneu a connectar-vos.",
"connectionRejected": "Connexió rebutjada pel servidor. Si us plau, comproveu l'autenticació i la configuració de xarxa."
"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": {
"title": "Gestor de fitxers",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Restabliment de la contrasenya correcte",
"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": {
"notFound": "Pàgina no trobada",
"unauthorized": "Accés no autoritzat",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Amfitrió i barra lateral",
"snippetsSettings": "Fragments",
"confirmSnippetExecution": "Confirma l'execució del fragment",
"confirmSnippetExecutionDesc": "Mostra el diàleg de confirmació abans d'executar fragments",
"updateSettings": "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.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 o example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Pověření",
"credentialsViewer": "Prohlížeč přihlašovacích údajů",
"manageYourSSHCredentials": "Bezpečná správa přihlašovacích údajů SSH",
"addCredential": "Přidat pověření",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Toto bezpečně přidá veřejný klíč do souboru ~/.ssh/authorized_keys bez přepsání existujících klíčů. Operace je vratná.",
"chooseHostToDeploy": "Vyberte hostitele pro nasazení...",
"deploying": "Nasazování...",
"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",
"noHostsAvailable": "Nejsou k dispozici žádní hostitelé",
"noHostsMatchSearch": "Žádné hostitele neodpovídají vašemu hledání",
@@ -298,7 +303,9 @@
"createFolder": "Vytvořit složku",
"editFolder": "Upravit složku",
"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": {
"title": "Historie",
@@ -402,6 +409,7 @@
"required": "Požadováno",
"optional": "Nepovinné",
"connect": "Připojit",
"copied": "Zkopírováno",
"connecting": "Připojování...",
"creating": "Vytváření...",
"clear": "Vyčistit",
@@ -494,6 +502,7 @@
"checking": "Kontroluje...",
"checkingDatabase": "Kontrola připojení k databázi...",
"checkingAuthentication": "Kontrola ověřování...",
"backendReconnected": "Připojení k serveru obnoveno",
"actions": "Akce",
"remove": "Odebrat",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Manažer serveru",
"hosts": "Hostitelé",
"sshHosts": "SSH hostitelé",
"noHosts": "Žádné SSH hostitele",
"noHostsMessage": "Zatím jste nepřidali žádné SSH hostitele. Chcete-li začít, klikněte na \"Přidat hostitele\".",
@@ -836,7 +846,7 @@
"failedToImportJson": "Nepodařilo se importovat soubor JSON",
"connectionDetails": "Detaily připojení",
"organization": "Organizace",
"ipAddress": "IP adresa",
"ipAddress": "IP adresa nebo název hostitele",
"port": "Přístav",
"name": "Název",
"username": "Uživatelské jméno",
@@ -915,6 +925,8 @@
"key": "Klíč",
"credential": "Pověření",
"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í",
"selectCredentialPlaceholder": "Zvolte přihlašovací údaje...",
"credentialRequired": "Při použití ověření pověření je vyžadováno pověření",
@@ -1083,6 +1095,7 @@
"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í.",
"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",
"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",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Otevřít v prohlížeči",
"warpgateContinue": "Dokončil jsem ověření",
"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?",
"sudoPasswordPopupHint": "Stiskněte Enter pro vložení, Esc pro zamítnutí",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Automaticky zkouším {{method}} autentizaci...",
"totpTimeout": "Časový limit ověřování TOTP. Připojte se znovu.",
"passwordTimeout": "Časový limit ověření hesla vypršel. Připojte se znovu.",
"connectionRejected": "Připojení odmítnuto serverem. Zkontrolujte prosím ověření a nastavení sítě."
"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": {
"title": "Správce souborů",
@@ -1934,6 +1957,20 @@
"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."
},
"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": {
"notFound": "Stránka nenalezena",
"unauthorized": "Neoprávněný přístup",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminál",
"hostSidebarSettings": "Hostitel & postranní panel",
"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",
"disableUpdateCheck": "Zakázat kontrolu aktualizací",
"disableUpdateCheckDesc": "Zastavit kontrolu nových verzí na startu a řídicím panelu. Sníží síťové požadavky.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 nebo příklad.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Legitimation",
"credentialsViewer": "Legitimationsfremviser",
"manageYourSSHCredentials": "Administrer dine SSH-akkreditiver sikkert",
"addCredential": "Tilføj Credential",
@@ -99,6 +100,10 @@
"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...",
"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",
"noHostsAvailable": "Ingen værter tilgængelige",
"noHostsMatchSearch": "Ingen værter matcher din søgning",
@@ -298,7 +303,9 @@
"createFolder": "Opret Mappe",
"editFolder": "Rediger 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": {
"title": "Historik",
@@ -402,6 +409,7 @@
"required": "Påkrævet",
"optional": "Valgfri",
"connect": "Forbind",
"copied": "Kopieret",
"connecting": "Forbinder...",
"creating": "Opretter...",
"clear": "Ryd",
@@ -494,6 +502,7 @@
"checking": "Kontrollerer...",
"checkingDatabase": "Kontrollerer databaseforbindelse...",
"checkingAuthentication": "Kontrollerer godkendelse...",
"backendReconnected": "Server forbindelse gendannet",
"actions": "Handlinger",
"remove": "Fjern",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Vært Manager",
"hosts": "Værter",
"sshHosts": "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.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Kunne ikke importere JSON- fil",
"connectionDetails": "Forbindelse Detaljer",
"organization": "Organisation",
"ipAddress": "Ip Adresse",
"ipAddress": "Ip adresse eller værtsnavn",
"port": "Port",
"name": "Navn",
"username": "Brugernavn",
@@ -915,6 +925,8 @@
"key": "Nøgle",
"credential": "Credential",
"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",
"selectCredentialPlaceholder": "Vælg en legitimationsoplysning...",
"credentialRequired": "Oplysninger er påkrævet, når du bruger legitimationsoplysninger godkendelse",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Tastatur-Interaktiv Autentificering",
"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.",
"opksshAuthTitle": "OPKSSH- Godkendelse",
"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).",
"overrideCredentialUsername": "Tilsidesæt Credential Brugernavn",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Åbn i browser",
"warpgateContinue": "Jeg Er Afsluttet Godkendelse",
"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?",
"sudoPasswordPopupHint": "Tryk på Enter for at indsætte, Esc for at afvise",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Prøver automatisk {{method}} autentificering...",
"totpTimeout": "TOTP bekræftelses-timeout. Genforbind.",
"passwordTimeout": "Adgangskodens bekræftelses-timeout. Opret venligst ny forbindelse.",
"connectionRejected": "Forbindelse afvist af serveren. Tjek venligst din godkendelse og netværkskonfiguration."
"connectionRejected": "Forbindelse afvist af serveren. Tjek venligst din godkendelse og netværkskonfiguration.",
"hostKeyRejected": "SSH-værtnøglebekræftelse afvist. Forbindelse annulleret."
},
"fileManager": {
"title": "Filhåndtering",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Adgangskode Nulstilling Lykkedes",
"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": {
"notFound": "Siden blev ikke fundet",
"unauthorized": "Uautoriseret adgang",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Vært & Sidebjælke",
"snippetsSettings": "Stumper",
"confirmSnippetExecution": "Bekræft Udførelse Af Snippet",
"confirmSnippetExecutionDesc": "Vis bekræftelsesdialog før udførelse af snippets",
"updateSettings": "Opdateringer",
"disableUpdateCheck": "Deaktivér Opdateringstjek",
"disableUpdateCheckDesc": "Stop med at kontrollere for nye versioner ved opstart og dashboard. Reducerer netværksanmodninger.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 eller eksempel.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Anmeldedaten",
"credentialsViewer": "Zugangsdaten-Betrachter",
"manageYourSSHCredentials": "SSH Zugangsdaten sicher verwalten",
"addCredential": "Zugangsdaten hinzufügen",
@@ -99,6 +100,10 @@
"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...",
"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",
"noHostsAvailable": "Keine Hosts verfügbar",
"noHostsMatchSearch": "Keine Hosts entsprechen Ihrer Suche",
@@ -298,7 +303,9 @@
"createFolder": "Ordner erstellen",
"editFolder": "Ordner bearbeiten",
"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": {
"title": "Verlauf",
@@ -402,6 +409,7 @@
"required": "Benötigt",
"optional": "Optional",
"connect": "Verbinden",
"copied": "Kopiert",
"connecting": "Verbinden...",
"creating": "Erstellen...",
"clear": "Leeren",
@@ -494,6 +502,7 @@
"checking": "Überprüfen...",
"checkingDatabase": "Überprüfe Datenbankverbindung...",
"checkingAuthentication": "Authentifizierung wird überprüft...",
"backendReconnected": "Serververbindung wiederhergestellt",
"actions": "Aktionen",
"remove": "Entfernen",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Host-Manager",
"hosts": "Hosts",
"sshHosts": "SSH-Hosts",
"noHosts": "Keine SSH-Hosts",
"noHostsMessage": "Sie haben noch keine SSH-Hosts hinzugefügt. Klicken Sie auf \"Host hinzufügen\", um loszulegen.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Import der JSON-Datei fehlgeschlagen",
"connectionDetails": "Verbindungsdetails",
"organization": "Organisation",
"ipAddress": "IP-Adresse",
"ipAddress": "IP-Adresse oder Hostname",
"port": "Port",
"name": "Name",
"username": "Benutzername",
@@ -915,6 +925,8 @@
"key": "Schlüssel",
"credential": "Anmeldedaten",
"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",
"selectCredentialPlaceholder": "Anmeldedaten auswählen...",
"credentialRequired": "Anmeldeinformationen werden benötigt, wenn die Anmeldeinformationen verwendet werden",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Tastatur-Interaktive Authentifizierung",
"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.",
"opksshAuthTitle": "OPKSSH Authentifizierung",
"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).",
"overrideCredentialUsername": "Benutzername überschreiben",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Im Browser öffnen",
"warpgateContinue": "Ich habe die Authentifizierung abgeschlossen",
"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?",
"sudoPasswordPopupHint": "Drücken Sie die Eingabetaste, Esc zum Verwerfen",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Automatisch {{method}} Authentifizierung...",
"totpTimeout": "TOTP-Verifizierungs-Timeout. Bitte erneut verbinden.",
"passwordTimeout": "Timeout zur Passwortüberprüfung. Bitte erneut verbinden.",
"connectionRejected": "Verbindung vom Server abgelehnt. Bitte überprüfen Sie Ihre Authentifizierung und Netzwerkkonfiguration."
"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": {
"title": "Datei-Manager",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Passwort erfolgreich zurückgesetzt",
"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": {
"notFound": "Seite nicht gefunden",
"unauthorized": "Unberechtigter Zugriff",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Host & Seitenleiste",
"snippetsSettings": "Schnipsel",
"confirmSnippetExecution": "Snippet Ausführung bestätigen",
"confirmSnippetExecutionDesc": "Bestätigungsdialog vor dem Ausführen von Snippets anzeigen",
"updateSettings": "Updates",
"disableUpdateCheck": "Updateprüfung deaktivieren",
"disableUpdateCheckDesc": "Stoppt die Suche nach neuen Versionen beim Start und Dashboard. Reduziert Netzwerkanfragen.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 oder beispiel.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Διαπιστευτήρια",
"credentialsViewer": "Προβολέας Πιστοποιητικών",
"manageYourSSHCredentials": "Διαχειριστείτε τα SSH διαπιστευτήρια σας με ασφάλεια",
"addCredential": "Προσθήκη Διαπιστευτηρίου",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Αυτό θα προσθέσει με ασφάλεια το δημόσιο κλειδί στο αρχείο ~/.ssh/authorized_keys του κεντρικού υπολογιστή του στόχου χωρίς να αντικαταστήσει τα υπάρχοντα κλειδιά. Η λειτουργία είναι αναστρέψιμη.",
"chooseHostToDeploy": "Επιλέξτε έναν κεντρικό υπολογιστή για ανάπτυξη...",
"deploying": "Εφαρμογή...",
"copyDeployCommand": "Αντιγραφή Εφαρμογής Εντολής",
"copyDeployCommandDescription": "Αντιγράψτε μια εντολή κελύφους για να προσθέσετε χειροκίνητα το δημόσιο κλειδί στο αρχείο authorized_keys ενός κεντρικού υπολογιστή. Χρήσιμο όταν η ταυτοποίηση κωδικού πρόσβασης είναι απενεργοποιημένη.",
"deployCommandCopied": "Η εντολή ανάπτυξης αντιγράφηκε στο πρόχειρο",
"manualDeployInfo": "Επικολλήστε αυτήν την εντολή στο τερματικό του κεντρικού υπολογιστή προορισμού (π.χ., κονσόλα Proxmox, IPMI ή φυσική πρόσβαση) για να προσθέσετε το SSH κλειδί.",
"name": "Όνομα",
"noHostsAvailable": "Δεν υπάρχουν διαθέσιμοι υπολογιστές",
"noHostsMatchSearch": "Κανένας υπολογιστής δεν ταιριάζει με την αναζήτησή σας",
@@ -298,7 +303,9 @@
"createFolder": "Δημιουργία Φακέλου",
"editFolder": "Επεξεργασία Φακέλου",
"editFolderDescription": "Προσαρμόστε το φάκελο αποκοπής",
"createFolderDescription": "Οργανώστε τα αποσπάσματα σας σε φακέλους"
"createFolderDescription": "Οργανώστε τα αποσπάσματα σας σε φακέλους",
"confirmExecution": "Εκτέλεση \"{{name}}\"?",
"confirmExecutionDesc": "Είστε βέβαιοι ότι θέλετε να εκτελέσετε αυτό το απόφραξη?"
},
"commandHistory": {
"title": "Ιστορικό",
@@ -402,6 +409,7 @@
"required": "Απαιτείται",
"optional": "Προαιρετικό",
"connect": "Σύνδεση",
"copied": "Αντιγράφηκε",
"connecting": "Σύνδεση...",
"creating": "Δημιουργία...",
"clear": "Εκκαθάριση",
@@ -494,6 +502,7 @@
"checking": "Έλεγχος...",
"checkingDatabase": "Έλεγχος σύνδεσης βάσης δεδομένων...",
"checkingAuthentication": "Έλεγχος ταυτοποίησης...",
"backendReconnected": "Έγινε επαναφορά σύνδεσης διακομιστή",
"actions": "Ενέργειες",
"remove": "Αφαίρεση",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Διαχειριστής Υπολογιστών",
"hosts": "Υπολογιστές",
"sshHosts": "SSH Hosts",
"noHosts": "Δεν Υπάρχουν Υπολογιστές SSH",
"noHostsMessage": "Δεν έχετε προσθέσει ακόμα κανένα SSH hosts. Κάντε κλικ στο κουμπί \"Προσθήκη Host\" για να ξεκινήσετε.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Αποτυχία εισαγωγής αρχείου JSON",
"connectionDetails": "Λεπτομέρειες Σύνδεσης",
"organization": "Οργανισμός",
"ipAddress": "Διεύθυνση IP",
"ipAddress": "Διεύθυνση IP ή όνομα κεντρικού υπολογιστή",
"port": "Θύρα",
"name": "Όνομα",
"username": "Όνομα Χρήστη",
@@ -915,6 +925,8 @@
"key": "Κλειδί",
"credential": "Διαπιστευτήριο",
"none": "Κανένα",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Σας παρακινεί για την ταυτοποίηση ιστού OPKSSH σε κάθε σύνδεση. Το διακριτικό ταυτοποίησης θα διαρκέσει 24 ώρες μέχρι να ανανεωθεί.",
"selectCredential": "Επιλέξτε Διαπιστευτήρια",
"selectCredentialPlaceholder": "Επιλέξτε ένα πιστοποιητικό...",
"credentialRequired": "Απαιτείται διαπιστευτήριο όταν χρησιμοποιείται έλεγχος ταυτότητας διαπιστευτηρίων",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Πληκτρολόγιο-Διαδραστική Ταυτοποίηση",
"noneAuthDescription": "Αυτή η μέθοδος ταυτοποίησης θα χρησιμοποιεί τον διαδραστικό έλεγχο ταυτότητας πληκτρολογίου κατά τη σύνδεση με τον διακομιστή SSH.",
"noneAuthDetails": "Ο διαδραστικός έλεγχος ταυτότητας πληκτρολογίου επιτρέπει στο διακομιστή να σας ζητά διαπιστευτήρια κατά τη διάρκεια της σύνδεσης. Αυτό είναι χρήσιμο για διακομιστές που απαιτούν έλεγχο ταυτότητας πολλαπλών παραγόντων ή αν δεν θέλετε να αποθηκεύσετε τα διαπιστευτήρια τοπικά.",
"opksshAuthTitle": "Επαλήθευση OPKSSH",
"forceKeyboardInteractive": "Εξαναγκασμός Keyboard-Interactive",
"forceKeyboardInteractiveDesc": "Δυνάμει της χρήσης διαδραστικού ελέγχου ταυτότητας πληκτρολογίου. Αυτό μερικές φορές απαιτείται για διακομιστές που χρησιμοποιούν έλεγχο ταυτότητας δύο παραγόντων (TOTP/2FA).",
"overrideCredentialUsername": "Παράκαμψη Ονόματος Διαπιστευτηρίου Χρήστη",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Άνοιγμα σε περιηγητή",
"warpgateContinue": "Έχω Ολοκληρώσει την Πιστοποίηση",
"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": "Εισαγωγή Κωδικού Πρόσβασης?",
"sudoPasswordPopupHint": "Πατήστε Enter για εισαγωγή, Esc για απόρριψη",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Αυτόματη προσπάθεια ελέγχου ταυτότητας {{method}}...",
"totpTimeout": "Λήξη χρονικού ορίου επαλήθευσης TOTP. Επανασύνδεση.",
"passwordTimeout": "Λήξη χρονικού ορίου επαλήθευσης κωδικού πρόσβασης. Επανασύνδεση.",
"connectionRejected": "Η σύνδεση απορρίφθηκε από το διακομιστή. Παρακαλώ ελέγξτε τον έλεγχο ταυτότητας και τις ρυθμίσεις δικτύου."
"connectionRejected": "Η σύνδεση απορρίφθηκε από το διακομιστή. Παρακαλώ ελέγξτε τον έλεγχο ταυτότητας και τις ρυθμίσεις δικτύου.",
"hostKeyRejected": "Η επαλήθευση SSH κεντρικού υπολογιστή απορρίφθηκε. Η σύνδεση ακυρώθηκε."
},
"fileManager": {
"title": "Διαχειριστής Αρχείων",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Επιτυχής Επαναφορά Κωδικού Πρόσβασης",
"passwordResetSuccessDesc": "Επιτυχής επαναφορά του κωδικού πρόσβασης. Μπορείτε τώρα να συνδεθείτε με το νέο κωδικό πρόσβασης."
},
"hostKey": {
"verifyNewHost": "Επαλήθευση Κλειδιού Διακομιστή SSH",
"keyChangedWarning": "Το Κλειδί Υπολογιστή SSH Άλλαξε",
"firstConnectionTitle": "Πρώτη σύνδεση σε αυτόν τον υπολογιστή",
"firstConnectionDescription": "Η αυθεντικότητα αυτού του κεντρικού υπολογιστή δεν μπορεί να καθοριστεί. Βεβαιωθείτε ότι το δακτυλικό αποτύπωμα ταιριάζει με αυτό που περιμένετε.",
"keyChangedDescription": "Το κλειδί φιλοξενίας αυτού του διακομιστή έχει αλλάξει από την τελευταία σας σύνδεση. Αυτό μπορεί να υποδηλώνει ένα πρόβλημα ασφάλειας.",
"previousKey": "Προηγούμενο Κλειδί",
"newFingerprint": "Νέο Δακτυλικό Αποτύπωμα",
"fingerprint": "Αποτύπωμα",
"verifyInstructions": "Αν εμπιστεύεστε αυτόν τον οικοδεσπότη, κάντε κλικ για να συνεχίσετε και να αποθηκεύσετε αυτό το δακτυλικό αποτύπωμα για μελλοντικές συνδέσεις.",
"securityWarning": "Προειδοποίηση Ασφαλείας",
"acceptAndContinue": "Αποδοχή & Συνέχεια",
"acceptNewKey": "Αποδοχή Νέου Κλειδιού & Συνέχεια"
},
"errors": {
"notFound": "Η σελίδα δεν βρέθηκε",
"unauthorized": "Μη εξουσιοδοτημένη πρόσβαση",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Τερματικό",
"hostSidebarSettings": "Κεντρικός Υπολογιστής & Πλαϊνή Μπάρα",
"snippetsSettings": "Δείγματα",
"confirmSnippetExecution": "Επιβεβαίωση Εκτέλεσης Δείκτη",
"confirmSnippetExecutionDesc": "Εμφάνιση διαλόγου επιβεβαίωσης πριν την εκτέλεση αποσπασμάτων",
"updateSettings": "Ενημερώσεις",
"disableUpdateCheck": "Απενεργοποίηση Ελέγχου Ενημέρωσης",
"disableUpdateCheckDesc": "Διακοπή ελέγχου για νέες εκδόσεις κατά την εκκίνηση και τον πίνακα ελέγχου. Μειώνει τα αιτήματα δικτύου.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 ή example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Credenciales",
"credentialsViewer": "Visor de credenciales",
"manageYourSSHCredentials": "Administrar sus credenciales SSH de forma segura",
"addCredential": "Añadir credencial",
@@ -99,6 +100,10 @@
"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...",
"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",
"noHostsAvailable": "No hay hosts disponibles",
"noHostsMatchSearch": "Ningún host coincide con tu búsqueda",
@@ -298,7 +303,9 @@
"createFolder": "Crear carpeta",
"editFolder": "Editar carpeta",
"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": {
"title": "Historial",
@@ -402,6 +409,7 @@
"required": "Requerido",
"optional": "Opcional",
"connect": "Conectar",
"copied": "Copiado",
"connecting": "Conectando...",
"creating": "Creando...",
"clear": "Claro",
@@ -494,6 +502,7 @@
"checking": "Comprobando...",
"checkingDatabase": "Comprobando conexión a la base de datos...",
"checkingAuthentication": "Comprobando autenticación...",
"backendReconnected": "Conexión con el servidor restaurada",
"actions": "Acciones",
"remove": "Eliminar",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Gestor de Anfitriones",
"hosts": "Anfitriones",
"sshHosts": "Equipos 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.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Error al importar el archivo JSON",
"connectionDetails": "Detalles de la conexión",
"organization": "Organización",
"ipAddress": "Dirección IP",
"ipAddress": "Dirección IP o nombre de host",
"port": "Puerto",
"name": "Nombre",
"username": "Usuario",
@@ -915,6 +925,8 @@
"key": "Clave",
"credential": "Credencial",
"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",
"selectCredentialPlaceholder": "Elija una credencial...",
"credentialRequired": "Se requiere credencial cuando se utiliza autenticación de credenciales",
@@ -1083,6 +1095,7 @@
"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.",
"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",
"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",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Abrir en el navegador",
"warpgateContinue": "He completado la autenticación",
"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?",
"sudoPasswordPopupHint": "Presione Enter para insertar, Esc para descartar",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Intentando automáticamente autenticación {{method}}...",
"totpTimeout": "Tiempo de verificación TOTP. Por favor, vuelva a conectar.",
"passwordTimeout": "Tiempo de verificación de contraseña. Por favor, vuelva a conectar.",
"connectionRejected": "Conexión rechazada por el servidor. Por favor, compruebe su autenticación y configuración de red."
"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": {
"title": "Gestor de archivos",
@@ -1934,6 +1957,20 @@
"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."
},
"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": {
"notFound": "Página no encontrada",
"unauthorized": "Acceso no autorizado",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Anfitrión y barra lateral",
"snippetsSettings": "Fragmentos",
"confirmSnippetExecution": "Confirmar ejecución de Snippet",
"confirmSnippetExecutionDesc": "Mostrar diálogo de confirmación antes de ejecutar fragmentos",
"updateSettings": "Actualizaciones",
"disableUpdateCheck": "Desactivar revisión de actualización",
"disableUpdateCheckDesc": "Deja de buscar nuevas versiones en el arranque y el tablero. Reduce las solicitudes de red.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 o ejemplo.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Käyttäjätunnukset",
"credentialsViewer": "Käyttöoikeustietojen Katselin",
"manageYourSSHCredentials": "Hallitse SSH tunnuksiasi turvallisesti",
"addCredential": "Lisää Käyttöoikeustieto",
@@ -99,6 +100,10 @@
"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...",
"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",
"noHostsAvailable": "Ei isäntiä saatavilla",
"noHostsMatchSearch": "Yksikään isäntä ei vastaa hakuasi",
@@ -298,7 +303,9 @@
"createFolder": "Luo Kansio",
"editFolder": "Muokkaa 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": {
"title": "Historia",
@@ -402,6 +409,7 @@
"required": "Pakollinen",
"optional": "Valinnainen",
"connect": "Yhdistä",
"copied": "Kopioitu",
"connecting": "Yhdistetään...",
"creating": "Luodaan...",
"clear": "Tyhjennä",
@@ -494,6 +502,7 @@
"checking": "Tarkistetaan...",
"checkingDatabase": "Tarkistetaan tietokantayhteyttä...",
"checkingAuthentication": "Tarkistetaan todennusta...",
"backendReconnected": "Palvelinyhteys palautettu",
"actions": "Toiminnot",
"remove": "Poista",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Isäntälaitteen Hallinta",
"hosts": "Isäntä",
"sshHosts": "Ssh Isäntä",
"noHosts": "Ei Ssh Isäntiä",
"noHostsMessage": "Et ole vielä lisännyt yhtään SSH isäntää. Napsauta \"Lisää isäntä\" aloittaaksesi.",
@@ -836,7 +846,7 @@
"failedToImportJson": "JSON-tiedoston tuonti epäonnistui",
"connectionDetails": "Yhteyden Tiedot",
"organization": "Organisaatio",
"ipAddress": "Ip Osoite",
"ipAddress": "IP-osoite tai isäntänimi",
"port": "Portti",
"name": "Nimi",
"username": "Käyttäjätunnus",
@@ -915,6 +925,8 @@
"key": "Avain",
"credential": "Käyttöoikeustiedot",
"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",
"selectCredentialPlaceholder": "Valitse käyttäjätunnus...",
"credentialRequired": "Käyttöoikeustiedot vaaditaan, kun käytetään käyttöoikeustietojen todennusta",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Näppäimistöinteraktiivinen todennus",
"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.",
"opksshAuthTitle": "OPKSSH Todennus",
"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).",
"overrideCredentialUsername": "Ohita tunnistetiedot Käyttäjätunnus",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Avaa selaimessa",
"warpgateContinue": "Olen Suorittanut Todentamisen",
"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?",
"sudoPasswordPopupHint": "Lisää painamalla Enter, hylkää painamalla Esc",
"sudoPasswordPopupConfirm": "Lisää",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Yritetään automaattisesti {{method}} todennusta...",
"totpTimeout": "TOTP vahvistuksen 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": {
"title": "Tiedostojen Hallinta",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Salasanan palautus onnistui",
"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": {
"notFound": "Sivua ei löytynyt",
"unauthorized": "Luvaton pääsy",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Pääte",
"hostSidebarSettings": "Isäntä ja sivupalkki",
"snippetsSettings": "Projekti",
"confirmSnippetExecution": "Vahvista Projektin Suoritus",
"confirmSnippetExecutionDesc": "Näytä vahvistusvalintaikkuna ennen snippettien suorittamista",
"updateSettings": "Päivitykset",
"disableUpdateCheck": "Poista Päivitystarkistus Käytöstä",
"disableUpdateCheckDesc": "Lopeta uusien versioiden tarkistaminen käynnistettäessä ja hallintapaneelissa. Vähennä verkkopyyntöjä.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 tai esimerkki.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Identifiants",
"credentialsViewer": "Visionneuse des identifiants",
"manageYourSSHCredentials": "Gérez vos identifiants SSH en toute sécurité",
"addCredential": "Ajouter un mot de passe",
@@ -99,6 +100,10 @@
"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...",
"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",
"noHostsAvailable": "Aucun hôte disponible",
"noHostsMatchSearch": "Aucun hôte ne correspond à votre recherche",
@@ -298,7 +303,9 @@
"createFolder": "Créer un dossier",
"editFolder": "Modifier le dossier",
"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": {
"title": "Historique",
@@ -402,6 +409,7 @@
"required": "Requis",
"optional": "Optionnel",
"connect": "Connecter",
"copied": "Copié",
"connecting": "Connexion en cours...",
"creating": "Création en cours...",
"clear": "Nettoyer",
@@ -494,6 +502,7 @@
"checking": "Vérification...",
"checkingDatabase": "Vérification de la connexion à la base de données...",
"checkingAuthentication": "Vérification de l'authentification...",
"backendReconnected": "Connexion au serveur restaurée",
"actions": "Actions",
"remove": "Retirer",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Responsable d'hôte",
"hosts": "Hôtes",
"sshHosts": "Hôtes SSH",
"noHosts": "Aucun hôte SSH",
"noHostsMessage": "Vous n'avez pas encore ajouté d'hôtes SSH. Cliquez sur \"Ajouter Hôte\" pour commencer.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Impossible d'importer le fichier JSON",
"connectionDetails": "Détails de la connexion",
"organization": "Organisation",
"ipAddress": "Adresse IP",
"ipAddress": "Adresse IP ou nom d'hôte",
"port": "Port",
"name": "Nom",
"username": "Nom d'utilisateur",
@@ -915,6 +925,8 @@
"key": "Clés",
"credential": "Identification",
"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",
"selectCredentialPlaceholder": "Choisissez un identifiant...",
"credentialRequired": "Les informations d'identification sont requises lors de l'utilisation de l'authentification",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Authentification interactive du clavier",
"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.",
"opksshAuthTitle": "Authentification OPKSSH",
"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).",
"overrideCredentialUsername": "Remplacer le nom d'utilisateur de l'identifiant",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Ouvrir dans le navigateur",
"warpgateContinue": "J'ai terminé l'authentification",
"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 ?",
"sudoPasswordPopupHint": "Appuyez sur Entrée pour insérer, Échap pour rejeter",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Essai automatique de l'authentification {{method}}...",
"totpTimeout": "Délai de vérification TOTP. Veuillez vous reconnecter.",
"passwordTimeout": "Délai de vérification du mot de passe. Veuillez vous reconnecter.",
"connectionRejected": "Connexion refusée par le serveur. Veuillez vérifier votre authentification et la configuration du réseau."
"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": {
"title": "Gestionnaire de fichiers",
@@ -1934,6 +1957,20 @@
"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."
},
"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": {
"notFound": "Page introuvable",
"unauthorized": "Accès non autorisé",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Hôte et barre latérale",
"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",
"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.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 ou exemple.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "אישורים",
"credentialsViewer": "מציג אישורים",
"manageYourSSHCredentials": "נהל את פרטי ה-SSH שלך בצורה מאובטחת",
"addCredential": "הוסף אישור",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "פעולה זו תוסיף בבטחה את המפתח הציבורי לקובץ ~/.ssh/authorized_keys של מארח היעד מבלי להחליף מפתחות קיימים. הפעולה היא הפיכה.",
"chooseHostToDeploy": "בחר מארח לפריסה...",
"deploying": "פריסה...",
"copyDeployCommand": "העתק את פקודת הפריסה",
"copyDeployCommandDescription": "העתק פקודת מעטפת כדי להוסיף ידנית את המפתח הציבורי לקובץ authorized_keys של מחשב מארח. שימושי כאשר אימות סיסמה מושבת.",
"deployCommandCopied": "פקודת הפריסה הועתקה ללוח",
"manualDeployInfo": "הדבק פקודה זו בטרמינל של מחשב המארח (למשל, קונסולת Proxmox, IPMI או גישה פיזית) כדי להוסיף את מפתח ה-SSH.",
"name": "שֵׁם",
"noHostsAvailable": "אין מארחים זמינים",
"noHostsMatchSearch": "אין מארחים התואמים את החיפוש שלך",
@@ -298,7 +303,9 @@
"createFolder": "צור תיקייה",
"editFolder": "עריכת תיקייה",
"editFolderDescription": "התאם אישית את תיקיית הקטעים שלך",
"createFolderDescription": "ארגנו את הקטעים שלכם בתיקיות"
"createFolderDescription": "ארגנו את הקטעים שלכם בתיקיות",
"confirmExecution": "להפעיל את \"{{name}}\"?",
"confirmExecutionDesc": "האם אתה בטוח שאתה רוצה להפעיל את הקטע הזה?"
},
"commandHistory": {
"title": "הִיסטוֹרִיָה",
@@ -402,6 +409,7 @@
"required": "דָרוּשׁ",
"optional": "אופציונלי",
"connect": "לְחַבֵּר",
"copied": "מוּעֲתָק",
"connecting": "מְקַשֵׁר...",
"creating": "יוצר...",
"clear": "בָּרוּר",
@@ -494,6 +502,7 @@
"checking": "בודק...",
"checkingDatabase": "בודק חיבור למסד הנתונים...",
"checkingAuthentication": "בודק אימות...",
"backendReconnected": "חיבור השרת שוחזר",
"actions": "פעולות",
"remove": "לְהַסִיר",
"revoke": "לְבַטֵל",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "מנהל מארח",
"hosts": "מארחים",
"sshHosts": "מארחי SSH",
"noHosts": "אין מארחי SSH",
"noHostsMessage": "עדיין לא הוספת מארחי SSH. לחץ על \"הוסף מארח\" כדי להתחיל.",
@@ -836,7 +846,7 @@
"failedToImportJson": "ייבוא קובץ JSON נכשל",
"connectionDetails": "פרטי חיבור",
"organization": "אִרגוּן",
"ipAddress": "כתובת IP",
"ipAddress": "כתובת IP או שם מארח",
"port": "נָמָל",
"name": "שֵׁם",
"username": "שם משתמש",
@@ -915,6 +925,8 @@
"key": "מַפְתֵחַ",
"credential": "תְעוּדָה",
"none": "אַף לֹא אֶחָד",
"opkssh": "אופקש",
"opksshAuthDescription": "מבקש ממך אימות אינטרנט של OPKSSH בכל חיבור. אסימון האימות שלך יהיה בתוקף למשך 24 שעות עד שיהיה צורך לחדשו.",
"selectCredential": "בחר אישור",
"selectCredentialPlaceholder": "בחר אישור...",
"credentialRequired": "נדרשת אישור בעת שימוש באימות אישורים",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "אימות אינטראקטיבי באמצעות מקלדת",
"noneAuthDescription": "שיטת אימות זו תשתמש באימות אינטראקטיבי באמצעות מקלדת בעת התחברות לשרת SSH.",
"noneAuthDetails": "אימות אינטראקטיבי באמצעות מקלדת מאפשר לשרת לבקש ממך אישורים במהלך החיבור. זה שימושי עבור שרתים הדורשים אימות רב-גורמי או אם אינך מעוניין לשמור אישורים באופן מקומי.",
"opksshAuthTitle": "אימות OPKSSH",
"forceKeyboardInteractive": "כפיית מקלדת אינטראקטיבית",
"forceKeyboardInteractiveDesc": "כופה שימוש באימות אינטראקטיבי באמצעות מקלדת. זה נדרש לעיתים עבור שרתים המשתמשים באימות דו-שלבי (TOTP/2FA).",
"overrideCredentialUsername": "עקיפת שם משתמש של פרטי כניסה",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "פתח בדפדפן",
"warpgateContinue": "סיימתי את האימות",
"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": "להכניס סיסמה?",
"sudoPasswordPopupHint": "לחץ על Enter כדי להוסיף, Esc כדי לסגור",
"sudoPasswordPopupConfirm": "לְהַכנִיס",
@@ -1337,7 +1359,8 @@
"automaticFallback": "מנסה אוטומטית אימות {{method}} ...",
"totpTimeout": "פסק זמן לאימות TOTP. אנא התחבר מחדש.",
"passwordTimeout": "פסק זמן לאימות סיסמה. אנא התחבר מחדש.",
"connectionRejected": "החיבור נדחה על ידי השרת. אנא בדוק את האימות ותצורת הרשת שלך."
"connectionRejected": "החיבור נדחה על ידי השרת. אנא בדוק את האימות ותצורת הרשת שלך.",
"hostKeyRejected": "אימות מפתח מארח SSH נדחה. החיבור בוטל."
},
"fileManager": {
"title": "מנהל הקבצים",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "איפוס הסיסמה הצליח",
"passwordResetSuccessDesc": "הסיסמה שלך אופסה בהצלחה. כעת תוכל להתחבר באמצעות הסיסמה החדשה שלך."
},
"hostKey": {
"verifyNewHost": "אימות מפתח מארח SSH",
"keyChangedWarning": "מפתח מארח SSH שונה",
"firstConnectionTitle": "בפעם הראשונה שמתחברת למארח הזה",
"firstConnectionDescription": "לא ניתן לקבוע את האותנטיות של מארח זה. ודא שטביעת האצבע תואמת את מה שאתה מצפה.",
"keyChangedDescription": "מפתח המארח של שרת זה השתנה מאז החיבור האחרון שלך. זה יכול להצביע על בעיית אבטחה.",
"previousKey": "מפתח קודם",
"newFingerprint": "טביעת אצבע חדשה",
"fingerprint": "טְבִיעַת אֶצבָּעוֹת",
"verifyInstructions": "אם אתה בוטח במארח זה, לחץ על קבל כדי להמשיך ולשמור את טביעת האצבע הזו לחיבורים עתידיים.",
"securityWarning": "אזהרת אבטחה",
"acceptAndContinue": "קבל והמשך",
"acceptNewKey": "קבל את המפתח החדש והמשך"
},
"errors": {
"notFound": "הדף לא נמצא",
"unauthorized": "גישה לא מורשית",
@@ -2026,6 +2063,8 @@
"terminalSettings": "מָסוֹף",
"hostSidebarSettings": "מארח וסרגל צד",
"snippetsSettings": "קטעי טקסט",
"confirmSnippetExecution": "אישור ביצוע קטע",
"confirmSnippetExecutionDesc": "הצג תיבת דו-שיח לאישור לפני ביצוע קטעי טקסט",
"updateSettings": "עדכונים",
"disableUpdateCheck": "השבתת בדיקת עדכונים",
"disableUpdateCheckDesc": "הפסק לבדוק גרסאות חדשות בעת ההפעלה ובלוח המחוונים. מפחית בקשות רשת.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 או example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "साख",
"credentialsViewer": "क्रेडेंशियल व्यूअर",
"manageYourSSHCredentials": "अपने SSH क्रेडेंशियल्स को सुरक्षित रूप से प्रबंधित करें",
"addCredential": "क्रेडेंशियल जोड़ें",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "इससे सार्वजनिक कुंजी सुरक्षित रूप से लक्ष्य होस्ट की ~/.ssh/authorized_keys फ़ाइल में जुड़ जाएगी, और मौजूदा कुंजियों को ओवरराइट नहीं किया जाएगा। यह प्रक्रिया प्रतिवर्ती है।",
"chooseHostToDeploy": "परिनियोजन के लिए होस्ट चुनें...",
"deploying": "तैनाती जारी है...",
"copyDeployCommand": "परिनियोजन कमांड कॉपी करें",
"copyDeployCommandDescription": "किसी होस्ट की authorized_keys फ़ाइल में सार्वजनिक कुंजी को मैन्युअल रूप से जोड़ने के लिए शेल कमांड को कॉपी करें। यह तब उपयोगी होता है जब पासवर्ड प्रमाणीकरण अक्षम हो।",
"deployCommandCopied": "डिप्लॉयमेंट कमांड क्लिपबोर्ड पर कॉपी हो गई है",
"manualDeployInfo": "SSH कुंजी जोड़ने के लिए इस कमांड को टारगेट होस्ट के टर्मिनल (जैसे, प्रॉक्समॉक्स कंसोल, IPMI, या फिजिकल एक्सेस) में पेस्ट करें।",
"name": "नाम",
"noHostsAvailable": "कोई होस्ट उपलब्ध नहीं है",
"noHostsMatchSearch": "आपकी खोज से मेल खाने वाले कोई होस्ट नहीं हैं",
@@ -298,7 +303,9 @@
"createFolder": "फ़ोल्डर बनाएँ",
"editFolder": "फ़ोल्डर संपादित करें",
"editFolderDescription": "अपने स्निपेट फ़ोल्डर को अनुकूलित करें",
"createFolderDescription": "अपने लेखों के अंशों को फ़ोल्डरों में व्यवस्थित करें।"
"createFolderDescription": "अपने लेखों के अंशों को फ़ोल्डरों में व्यवस्थित करें।",
"confirmExecution": "\"{{name}}\" निष्पादित करें?",
"confirmExecutionDesc": "क्या आप वाकई इस कोड स्निपेट को चलाना चाहते हैं?"
},
"commandHistory": {
"title": "इतिहास",
@@ -402,6 +409,7 @@
"required": "आवश्यक",
"optional": "वैकल्पिक",
"connect": "जोड़ना",
"copied": "कॉपी किया गया",
"connecting": "कनेक्ट हो रहा है...",
"creating": "सृजन...",
"clear": "स्पष्ट",
@@ -494,6 +502,7 @@
"checking": "जाँच चल रही है...",
"checkingDatabase": "डेटाबेस कनेक्शन की जाँच की जा रही है...",
"checkingAuthentication": "प्रमाणीकरण की जाँच की जा रही है...",
"backendReconnected": "सर्वर कनेक्शन बहाल हो गया",
"actions": "कार्रवाई",
"remove": "निकालना",
"revoke": "रद्द करना",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "मेजबान प्रबंधक",
"hosts": "मेजबान",
"sshHosts": "एसएसएच होस्ट",
"noHosts": "कोई एसएसएच होस्ट नहीं",
"noHostsMessage": "आपने अभी तक कोई SSH होस्ट नहीं जोड़ा है। शुरू करने के लिए \"होस्ट जोड़ें\" पर क्लिक करें।",
@@ -836,7 +846,7 @@
"failedToImportJson": "JSON फ़ाइल आयात करने में विफल",
"connectionDetails": "कनेक्शन विवरण",
"organization": "संगठन",
"ipAddress": "आईपी पता",
"ipAddress": "आईपी पता या होस्टनाम",
"port": "पत्तन",
"name": "नाम",
"username": "उपयोगकर्ता नाम",
@@ -915,6 +925,8 @@
"key": "चाबी",
"credential": "क्रेडेंशियल",
"none": "कोई नहीं",
"opkssh": "ओपीकेएसएच",
"opksshAuthDescription": "हर बार कनेक्ट होने पर यह आपसे OPKSSH वेब-प्रमाणीकरण के लिए पूछेगा। आपका प्रमाणीकरण टोकन 24 घंटे तक वैध रहेगा, जिसके बाद इसे नवीनीकृत करना होगा।",
"selectCredential": "क्रेडेंशियल चुनें",
"selectCredentialPlaceholder": "एक प्रमाण पत्र चुनें...",
"credentialRequired": "क्रेडेंशियल प्रमाणीकरण का उपयोग करते समय क्रेडेंशियल आवश्यक है।",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "कीबोर्ड-इंटरैक्टिव प्रमाणीकरण",
"noneAuthDescription": "एसएसएच सर्वर से कनेक्ट करते समय यह प्रमाणीकरण विधि कीबोर्ड-इंटरैक्टिव प्रमाणीकरण का उपयोग करेगी।",
"noneAuthDetails": "कीबोर्ड-इंटरैक्टिव प्रमाणीकरण सर्वर को कनेक्शन के दौरान आपसे क्रेडेंशियल मांगने की अनुमति देता है। यह उन सर्वरों के लिए उपयोगी है जिन्हें मल्टी-फैक्टर प्रमाणीकरण की आवश्यकता होती है या यदि आप क्रेडेंशियल को स्थानीय रूप से सहेजना नहीं चाहते हैं।",
"opksshAuthTitle": "ओपीकेएसएसएच प्रमाणीकरण",
"forceKeyboardInteractive": "फ़ोर्स कीबोर्ड-इंटरैक्टिव",
"forceKeyboardInteractiveDesc": "कीबोर्ड-आधारित प्रमाणीकरण को अनिवार्य बनाता है। यह कभी-कभी उन सर्वरों के लिए आवश्यक होता है जो दो-कारक प्रमाणीकरण (TOTP/2FA) का उपयोग करते हैं।",
"overrideCredentialUsername": "क्रेडेंशियल उपयोगकर्ता नाम को ओवरराइड करें",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "ब्राउज़र में खोलें",
"warpgateContinue": "मैंने प्रमाणीकरण पूरा कर लिया है",
"warpgateTimeout": "वॉरपगेट प्रमाणीकरण में समय समाप्त हो गया है। कृपया पुनः कनेक्ट करें।",
"opksshAuthRequired": "OPKSSH प्रमाणीकरण आवश्यक है",
"opksshAuthDescription": "आगे बढ़ने के लिए अपने ब्राउज़र में प्रमाणीकरण पूरा करें। यह सत्र 24 घंटे तक वैध रहेगा।",
"opksshAuthUrl": "प्रमाणीकरण यूआरएल",
"opksshOpenBrowser": "प्रमाणीकरण के लिए ब्राउज़र खोलें",
"opksshWaitingForAuth": "ब्राउज़र में प्रमाणीकरण की प्रतीक्षा हो रही है...",
"opksshAuthenticating": "प्रमाणीकरण प्रक्रिया जारी है...",
"opksshTimeout": "प्रमाणीकरण का समय समाप्त हो गया। कृपया पुनः प्रयास करें।",
"opksshAuthFailed": "प्रमाणीकरण विफल रहा। कृपया अपनी पहचान सत्यापित करें और पुनः प्रयास करें।",
"opksshConfigMissing": "OPKSSH कॉन्फ़िगरेशन नहीं मिला। कृपया अपने OIDC प्रदाता सेटिंग्स के साथ ~/.opk/config.yml फ़ाइल बनाएँ। दस्तावेज़ देखें: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "पासवर्ड डालें?",
"sudoPasswordPopupHint": "सम्मिलित करने के लिए Enter दबाएँ, हटाने के लिए Esc दबाएँ",
"sudoPasswordPopupConfirm": "डालना",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Automatically trying {{method}} authentication...",
"totpTimeout": "टीओटीपी सत्यापन में समय समाप्त हो गया है। कृपया पुनः कनेक्ट करें।",
"passwordTimeout": "पासवर्ड सत्यापन में समय समाप्त हो गया है। कृपया पुनः कनेक्ट करें।",
"connectionRejected": "सर्वर द्वारा कनेक्शन अस्वीकृत कर दिया गया है। कृपया अपनी प्रमाणीकरण और नेटवर्क कॉन्फ़िगरेशन की जाँच करें।"
"connectionRejected": "सर्वर द्वारा कनेक्शन अस्वीकृत कर दिया गया है। कृपया अपनी प्रमाणीकरण और नेटवर्क कॉन्फ़िगरेशन की जाँच करें।",
"hostKeyRejected": "SSH होस्ट कुंजी सत्यापन अस्वीकृत। कनेक्शन रद्द।"
},
"fileManager": {
"title": "फ़ाइल मैनेजर",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "पासवर्ड रीसेट सफल रहा",
"passwordResetSuccessDesc": "आपका पासवर्ड सफलतापूर्वक रीसेट हो गया है। अब आप अपने नए पासवर्ड से लॉग इन कर सकते हैं।"
},
"hostKey": {
"verifyNewHost": "SSH होस्ट कुंजी सत्यापित करें",
"keyChangedWarning": "SSH होस्ट कुंजी बदल गई",
"firstConnectionTitle": "पहली बार इस होस्ट से कनेक्ट हो रहा है",
"firstConnectionDescription": "इस होस्ट की प्रामाणिकता स्थापित नहीं की जा सकती। कृपया सुनिश्चित करें कि फिंगरप्रिंट आपकी अपेक्षा के अनुरूप है।",
"keyChangedDescription": "आपके पिछले कनेक्शन के बाद से इस सर्वर की होस्ट कुंजी बदल गई है। यह किसी सुरक्षा समस्या का संकेत हो सकता है।",
"previousKey": "पिछली कुंजी",
"newFingerprint": "नया फिंगरप्रिंट",
"fingerprint": "अंगुली की छाप",
"verifyInstructions": "यदि आप इस होस्ट पर भरोसा करते हैं, तो जारी रखने के लिए स्वीकार करें पर क्लिक करें और भविष्य के कनेक्शनों के लिए इस फिंगरप्रिंट को सहेजें।",
"securityWarning": "सुरक्षा चेतावनी",
"acceptAndContinue": "स्वीकार करें और जारी रखें",
"acceptNewKey": "नई कुंजी स्वीकार करें और जारी रखें"
},
"errors": {
"notFound": "पृष्ठ नहीं मिला",
"unauthorized": "अनधिकृत पहुंच",
@@ -2026,6 +2063,8 @@
"terminalSettings": "टर्मिनल",
"hostSidebarSettings": "होस्ट और साइडबार",
"snippetsSettings": "स्निपेट्स",
"confirmSnippetExecution": "स्निपेट निष्पादन की पुष्टि करें",
"confirmSnippetExecutionDesc": "स्निपेट्स को निष्पादित करने से पहले पुष्टिकरण डायलॉग दिखाएँ",
"updateSettings": "अपडेट",
"disableUpdateCheck": "अपडेट जांच अक्षम करें",
"disableUpdateCheckDesc": "स्टार्टअप और डैशबोर्ड पर नए संस्करणों की जाँच करना बंद करें। इससे नेटवर्क अनुरोध कम होते हैं।",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 या example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Hitelesítő adatok",
"credentialsViewer": "Hitelesítő adatok megtekintője",
"manageYourSSHCredentials": "SSH-hitelesítő adatainak biztonságos kezelése",
"addCredential": "Hitelesítő adat hozzáadása",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Ez biztonságosan hozzáadja a nyilvános kulcsot a célgép ~/.ssh/authorized_keys fájljához a meglévő kulcsok felülírása nélkül. A művelet visszafordítható.",
"chooseHostToDeploy": "Válasszon egy hosztot a telepítéshez...",
"deploying": "Telepítés...",
"copyDeployCommand": "Telepítési parancs másolása",
"copyDeployCommandDescription": "Egy shell parancs másolásával manuálisan hozzáadható a nyilvános kulcs a gazdagép authorized_keys fájljához. Hasznos, ha a jelszó-hitelesítés le van tiltva.",
"deployCommandCopied": "A telepítési parancs a vágólapra másolva",
"manualDeployInfo": "Illeszd be ezt a parancsot a célgép termináljába (pl. Proxmox konzol, IPMI vagy fizikai hozzáférés) az SSH kulcs hozzáadásához.",
"name": "Név",
"noHostsAvailable": "Nincsenek elérhető házigazdák",
"noHostsMatchSearch": "Nincsenek a keresésnek megfelelő házigazdák",
@@ -298,7 +303,9 @@
"createFolder": "Mappa létrehozása",
"editFolder": "Mappa szerkesztése",
"editFolderDescription": "Kódrészlet mappa testreszabása",
"createFolderDescription": "Rendezd a kódrészleteket mappákba"
"createFolderDescription": "Rendezd a kódrészleteket mappákba",
"confirmExecution": "Végrehajtani a következőt: \"{{name}}\"?",
"confirmExecutionDesc": "Biztosan végrehajtod ezt a kódrészletet?"
},
"commandHistory": {
"title": "Történelem",
@@ -402,6 +409,7 @@
"required": "Kívánt",
"optional": "Választható",
"connect": "Csatlakozás",
"copied": "Másolva",
"connecting": "Kapcsolódás...",
"creating": "Létrehozás...",
"clear": "Világos",
@@ -494,6 +502,7 @@
"checking": "Ellenőrzés...",
"checkingDatabase": "Adatbázis-kapcsolat ellenőrzése...",
"checkingAuthentication": "Hitelesítés ellenőrzése...",
"backendReconnected": "Szerverkapcsolat helyreállt",
"actions": "Műveletek",
"remove": "Eltávolítás",
"revoke": "Visszavonás",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Házigazda-kezelő",
"hosts": "Házigazdák",
"sshHosts": "SSH-hosztok",
"noHosts": "Nincsenek SSH-hosztok",
"noHostsMessage": "Még nem adott hozzá SSH-hosztokat. Kattintson a „Hoszt hozzáadása” gombra a kezdéshez.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Nem sikerült importálni a JSON fájlt",
"connectionDetails": "Kapcsolat részletei",
"organization": "Szervezet",
"ipAddress": "IP-cím",
"ipAddress": "IP-cím vagy hostnév",
"port": "Kikötő",
"name": "Név",
"username": "Felhasználónév",
@@ -915,6 +925,8 @@
"key": "Kulcsfontosságú",
"credential": "Hitelesítő adat",
"none": "Egyik sem",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Minden csatlakozáskor OPKSSH webes hitelesítést kér. A hitelesítési token 24 óráig érvényes, amíg meg nem újítják.",
"selectCredential": "Hitelesítő adat kiválasztása",
"selectCredentialPlaceholder": "Válasszon hitelesítő adatlapot...",
"credentialRequired": "Hitelesítő adat szükséges hitelesítő adat alapú hitelesítéshez",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Billentyűzet-interaktív hitelesítés",
"noneAuthDescription": "Ez a hitelesítési módszer billentyűzet-interaktív hitelesítést használ az SSH-kiszolgálóhoz való csatlakozáskor.",
"noneAuthDetails": "A billentyűzet-interaktív hitelesítés lehetővé teszi, hogy a szerver kérje a hitelesítő adatokat a csatlakozás során. Ez olyan szervereknél hasznos, amelyek többtényezős hitelesítést igényelnek, vagy ha nem szeretné helyben menteni a hitelesítő adatokat.",
"opksshAuthTitle": "OPKSSH hitelesítés",
"forceKeyboardInteractive": "Kényszerítse a billentyűzet interaktivitását",
"forceKeyboardInteractiveDesc": "Kényszeríti a billentyűzet-interaktív hitelesítés használatát. Ez néha szükséges a kétfaktoros hitelesítést (TOTP/2FA) használó szervereknél.",
"overrideCredentialUsername": "Hitelesítő adat felülírása Felhasználónév",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Megnyitás böngészőben",
"warpgateContinue": "Befejeztem a hitelesítést",
"warpgateTimeout": "Warpgate hitelesítési időtúllépés. Kérjük, csatlakozzon újra.",
"opksshAuthRequired": "OPKSSH hitelesítés szükséges",
"opksshAuthDescription": "A folytatáshoz végezze el a hitelesítést a böngészőjében. Ez a munkamenet 24 órán át érvényes marad.",
"opksshAuthUrl": "Hitelesítési URL",
"opksshOpenBrowser": "Böngésző megnyitása a hitelesítéshez",
"opksshWaitingForAuth": "Várakozás a böngésző hitelesítésére...",
"opksshAuthenticating": "Hitelesítés feldolgozása...",
"opksshTimeout": "A hitelesítés időtúllépést okozott. Kérjük, próbálja újra.",
"opksshAuthFailed": "Sikertelen hitelesítés. Kérjük, ellenőrizze a hitelesítő adatait, és próbálja újra.",
"opksshConfigMissing": "Az OPKSSH konfiguráció nem található. Kérjük, hozza létre a ~/.opk/config.yml fájlt az OIDC-szolgáltató beállításaival. Lásd a dokumentációt: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Jelszó beszúrása?",
"sudoPasswordPopupHint": "Nyomja meg az Enter billentyűt a beszúráshoz, az Esc billentyűt az elvetéshez",
"sudoPasswordPopupConfirm": "Beszúrás",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Automatikusan megpróbáljuk a {{method}} hitelesítést...",
"totpTimeout": "TOTP ellenőrzési időtúllépés. Kérjük, csatlakozzon újra.",
"passwordTimeout": "Jelszó-ellenőrzési időtúllépés. Kérjük, csatlakozzon újra.",
"connectionRejected": "A szerver elutasította a kapcsolatot. Kérjük, ellenőrizze a hitelesítést és a hálózati konfigurációt."
"connectionRejected": "A szerver elutasította a kapcsolatot. Kérjük, ellenőrizze a hitelesítést és a hálózati konfigurációt.",
"hostKeyRejected": "SSH host kulcs ellenőrzése elutasítva. Kapcsolat megszakítva."
},
"fileManager": {
"title": "Fájlkezelő",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Jelszó visszaállítása sikeres",
"passwordResetSuccessDesc": "Jelszava sikeresen visszaállításra került. Most már bejelentkezhet új jelszavával."
},
"hostKey": {
"verifyNewHost": "SSH host kulcs ellenőrzése",
"keyChangedWarning": "SSH állomáskulcs megváltozott",
"firstConnectionTitle": "Első csatlakozás ehhez a gazdagéphez",
"firstConnectionDescription": "A gazdagép hitelessége nem állapítható meg. Ellenőrizze, hogy az ujjlenyomat megfelel-e a vártnak.",
"keyChangedDescription": "A szerver host key-je megváltozott az utolsó csatlakozás óta. Ez biztonsági problémára utalhat.",
"previousKey": "Előző kulcs",
"newFingerprint": "Új ujjlenyomat",
"fingerprint": "Ujjlenyomat",
"verifyInstructions": "Ha megbízik ebben a gazdagépben, kattintson az Elfogadás gombra a folytatáshoz és az ujjlenyomat mentéséhez a jövőbeli kapcsolatokhoz.",
"securityWarning": "Biztonsági figyelmeztetés",
"acceptAndContinue": "Elfogadom és folytatom",
"acceptNewKey": "Új kulcs elfogadása és folytatás"
},
"errors": {
"notFound": "Az oldal nem található",
"unauthorized": "Jogosulatlan hozzáférés",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminál",
"hostSidebarSettings": "Gazdagép és oldalsáv",
"snippetsSettings": "Kódrészletek",
"confirmSnippetExecution": "Kódrészlet végrehajtásának megerősítése",
"confirmSnippetExecutionDesc": "Megerősítő párbeszédpanel megjelenítése a kódrészletek végrehajtása előtt",
"updateSettings": "Frissítések",
"disableUpdateCheck": "Frissítésellenőrzés letiltása",
"disableUpdateCheckDesc": "Új verziók keresésének leállítása indításkor és az irányítópulton. Csökkenti a hálózati kérések számát.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 vagy example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Kredensial",
"credentialsViewer": "Penampil Kredensial",
"manageYourSSHCredentials": "Kelola kredensial SSH Anda dengan aman.",
"addCredential": "Tambahkan Kredensial",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Ini akan menambahkan kunci publik dengan aman ke file ~/.ssh/authorized_keys pada host target tanpa menimpa kunci yang sudah ada. Operasi ini dapat dibatalkan.",
"chooseHostToDeploy": "Pilih host tempat untuk melakukan deployment...",
"deploying": "Sedang melakukan penyebaran...",
"copyDeployCommand": "Salin Perintah Penyebaran",
"copyDeployCommandDescription": "Salin perintah shell untuk menambahkan kunci publik secara manual ke file authorized_keys host. Berguna saat otentikasi kata sandi dinonaktifkan.",
"deployCommandCopied": "Perintah penyebaran disalin ke papan klip",
"manualDeployInfo": "Salin perintah ini ke terminal host target (misalnya, konsol Proxmox, IPMI, atau akses fisik) untuk menambahkan kunci SSH.",
"name": "Nama",
"noHostsAvailable": "Tidak ada tuan rumah yang tersedia.",
"noHostsMatchSearch": "Tidak ada host yang sesuai dengan pencarian Anda.",
@@ -298,7 +303,9 @@
"createFolder": "Buat Folder",
"editFolder": "Edit Folder",
"editFolderDescription": "Sesuaikan folder cuplikan Anda",
"createFolderDescription": "Susun cuplikan Anda ke dalam folder."
"createFolderDescription": "Susun cuplikan Anda ke dalam folder.",
"confirmExecution": "Jalankan \"{{name}}\"?",
"confirmExecutionDesc": "Apakah Anda yakin ingin menjalankan cuplikan kode ini?"
},
"commandHistory": {
"title": "Sejarah",
@@ -402,6 +409,7 @@
"required": "Diperlukan",
"optional": "Opsional",
"connect": "Menghubungkan",
"copied": "Disalin",
"connecting": "Menghubungkan...",
"creating": "Membuat...",
"clear": "Jernih",
@@ -494,6 +502,7 @@
"checking": "Sedang memeriksa...",
"checkingDatabase": "Memeriksa koneksi basis data...",
"checkingAuthentication": "Memeriksa autentikasi...",
"backendReconnected": "Koneksi server dipulihkan.",
"actions": "Tindakan",
"remove": "Menghapus",
"revoke": "Menarik kembali",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Manajer Host",
"hosts": "Tuan rumah",
"sshHosts": "Host SSH",
"noHosts": "Tidak ada Host SSH",
"noHostsMessage": "Anda belum menambahkan host SSH apa pun. Klik \"Tambah Host\" untuk memulai.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Gagal mengimpor file JSON",
"connectionDetails": "Detail Koneksi",
"organization": "Organisasi",
"ipAddress": "Alamat IP",
"ipAddress": "Alamat IP atau Nama Host",
"port": "Pelabuhan",
"name": "Nama",
"username": "Nama belakang",
@@ -915,6 +925,8 @@
"key": "Kunci",
"credential": "Mandat",
"none": "Tidak ada",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Meminta Anda untuk memasukkan otentikasi web OPKSSH setiap kali terhubung. Token otentikasi Anda akan berlaku selama 24 jam sebelum perlu diperbarui.",
"selectCredential": "Pilih Kredensial",
"selectCredentialPlaceholder": "Pilih sertifikasi...",
"credentialRequired": "Kredensial diperlukan saat menggunakan autentikasi kredensial.",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Otentikasi Interaktif Keyboard",
"noneAuthDescription": "Metode autentikasi ini akan menggunakan autentikasi interaktif keyboard saat terhubung ke server SSH.",
"noneAuthDetails": "Autentikasi interaktif keyboard memungkinkan server untuk meminta kredensial Anda selama koneksi. Ini berguna untuk server yang memerlukan autentikasi multi-faktor atau jika Anda tidak ingin menyimpan kredensial secara lokal.",
"opksshAuthTitle": "Otentikasi OPKSSH",
"forceKeyboardInteractive": "Paksa Interaktif Keyboard",
"forceKeyboardInteractiveDesc": "Memaksa penggunaan autentikasi interaktif keyboard. Ini terkadang diperlukan untuk server yang menggunakan Autentikasi Dua Faktor (TOTP/2FA).",
"overrideCredentialUsername": "Ganti Nama Pengguna Kredensial",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Buka di Browser",
"warpgateContinue": "Saya telah menyelesaikan otentikasi.",
"warpgateTimeout": "Otentikasi Warpgate habis waktu. Silakan sambungkan kembali.",
"opksshAuthRequired": "Otentikasi OPKSSH Diperlukan",
"opksshAuthDescription": "Lakukan otentikasi di browser Anda untuk melanjutkan. Sesi ini akan tetap berlaku selama 24 jam.",
"opksshAuthUrl": "URL Otentikasi",
"opksshOpenBrowser": "Buka Browser untuk Melakukan Otentikasi",
"opksshWaitingForAuth": "Menunggu otentikasi di browser...",
"opksshAuthenticating": "Memproses otentikasi...",
"opksshTimeout": "Autentikasi habis waktu. Silakan coba lagi.",
"opksshAuthFailed": "Autentikasi gagal. Silakan periksa kredensial Anda dan coba lagi.",
"opksshConfigMissing": "Konfigurasi OPKSSH tidak ditemukan. Silakan buat ~/.opk/config.yml dengan pengaturan penyedia OIDC Anda. Lihat dokumentasi: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Masukkan kata sandi?",
"sudoPasswordPopupHint": "Tekan Enter untuk memasukkan, Esc untuk menutup.",
"sudoPasswordPopupConfirm": "Menyisipkan",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Mencoba otentikasi {{method}} secara otomatis...",
"totpTimeout": "Waktu verifikasi TOTP habis. Silakan sambungkan kembali.",
"passwordTimeout": "Waktu verifikasi kata sandi habis. Silakan sambungkan kembali.",
"connectionRejected": "Koneksi ditolak oleh server. Harap periksa otentikasi dan konfigurasi jaringan Anda."
"connectionRejected": "Koneksi ditolak oleh server. Harap periksa otentikasi dan konfigurasi jaringan Anda.",
"hostKeyRejected": "Verifikasi kunci host SSH ditolak. Koneksi dibatalkan."
},
"fileManager": {
"title": "Pengelola File",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Reset Kata Sandi Berhasil",
"passwordResetSuccessDesc": "Kata sandi Anda telah berhasil direset. Anda sekarang dapat masuk dengan kata sandi baru Anda."
},
"hostKey": {
"verifyNewHost": "Verifikasi Kunci Host SSH",
"keyChangedWarning": "Kunci Host SSH Berubah",
"firstConnectionTitle": "Ini adalah kali pertama terhubung ke host ini.",
"firstConnectionDescription": "Keaslian host ini tidak dapat dipastikan. Verifikasi apakah sidik jari sesuai dengan yang Anda harapkan.",
"keyChangedDescription": "Kunci host untuk server ini telah berubah sejak koneksi terakhir Anda. Ini bisa mengindikasikan masalah keamanan.",
"previousKey": "Kunci Sebelumnya",
"newFingerprint": "Sidik Jari Baru",
"fingerprint": "Sidik jari",
"verifyInstructions": "Jika Anda mempercayai host ini, klik Terima untuk melanjutkan dan simpan sidik jari ini untuk koneksi di masa mendatang.",
"securityWarning": "Peringatan Keamanan",
"acceptAndContinue": "Terima & Lanjutkan",
"acceptNewKey": "Terima Kunci Baru & Lanjutkan"
},
"errors": {
"notFound": "Halaman tidak ditemukan",
"unauthorized": "Akses tidak sah",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Host & Sidebar",
"snippetsSettings": "Cuplikan",
"confirmSnippetExecution": "Konfirmasi Eksekusi Cuplikan",
"confirmSnippetExecutionDesc": "Tampilkan dialog konfirmasi sebelum menjalankan cuplikan kode.",
"updateSettings": "Pembaruan",
"disableUpdateCheck": "Nonaktifkan Pemeriksaan Pembaruan",
"disableUpdateCheckDesc": "Hentikan pengecekan versi baru saat startup dan dasbor. Mengurangi permintaan jaringan.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 atau example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Credenziali",
"credentialsViewer": "Visualizzatore Di Credenziali",
"manageYourSSHCredentials": "Gestisci le tue credenziali SSH in modo sicuro",
"addCredential": "Aggiungi Credenziali",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Questo aggiungerà la chiave pubblica al file ~/.ssh/authorized_keys dell'host di destinazione senza sovrascrivere le chiavi esistenti. L'operazione è reversibile.",
"chooseHostToDeploy": "Scegli un host in cui distribuire...",
"deploying": "Distribuzione...",
"copyDeployCommand": "Copia Il Comando Di Deploy",
"copyDeployCommandDescription": "Copia un comando shell per aggiungere manualmente la chiave pubblica al file authorized_keys di un host. Utile quando l'autenticazione con password è disabilitata.",
"deployCommandCopied": "Distribuisci comando copiato negli appunti",
"manualDeployInfo": "Incolla questo comando nel terminale dell'host di destinazione (ad esempio, console Proxmox, IPMI o accesso fisico) per aggiungere il tasto SSH.",
"name": "Nome",
"noHostsAvailable": "Nessun host disponibile",
"noHostsMatchSearch": "Nessun host corrisponde alla tua ricerca",
@@ -298,7 +303,9 @@
"createFolder": "Crea Cartella",
"editFolder": "Modifica Cartella",
"editFolderDescription": "Personalizza la cartella snippet",
"createFolderDescription": "Organizza i pezzetti di codice nelle cartelle"
"createFolderDescription": "Organizza i pezzetti di codice nelle cartelle",
"confirmExecution": "Eseguire \"{{name}}\"?",
"confirmExecutionDesc": "Sei sicuro di voler eseguire questa snippet?"
},
"commandHistory": {
"title": "Storico",
@@ -402,6 +409,7 @@
"required": "Richiesto",
"optional": "Facoltativo",
"connect": "Connetti",
"copied": "Copiato",
"connecting": "Connessione...",
"creating": "Creazione...",
"clear": "Pulisci",
@@ -494,6 +502,7 @@
"checking": "Controllo...",
"checkingDatabase": "Controllo connessione database...",
"checkingAuthentication": "Controllo autenticazione...",
"backendReconnected": "Connessione al server ripristinata",
"actions": "Azioni",
"remove": "Rimuovi",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Gestore Host",
"hosts": "Host",
"sshHosts": "Host SSH",
"noHosts": "Nessun Host SSH",
"noHostsMessage": "Non hai ancora aggiunto nessun host SSH. Clicca su \"Aggiungi host\" per iniziare.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Importazione del file JSON non riuscita",
"connectionDetails": "Dettagli Della Connessione",
"organization": "Organizzazione",
"ipAddress": "Indirizzo IP",
"ipAddress": "Indirizzo IP o nome host",
"port": "Porta",
"name": "Nome",
"username": "Username",
@@ -915,6 +925,8 @@
"key": "Chiave",
"credential": "Credenziali",
"none": "Nessuno",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Ti consente di eseguire il web-auth di OPKSSH su ogni connessione. Il tuo token di autenticazione durerà 24 ore fino a quando deve essere rinnovato.",
"selectCredential": "Seleziona Credenziali",
"selectCredentialPlaceholder": "Scegli una credenziale...",
"credentialRequired": "La credenziale è richiesta quando si utilizza l'autenticazione credenziali",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Autenticazione Tastiera-Interattiva",
"noneAuthDescription": "Questo metodo di autenticazione utilizzerà l'autenticazione interattiva con tastiera durante la connessione al server SSH.",
"noneAuthDetails": "L'autenticazione interattiva con tastiera consente al server di richiedere le credenziali durante la connessione. Questo è utile per i server che richiedono l'autenticazione a più fattori o se non si desidera salvare le credenziali localmente.",
"opksshAuthTitle": "Autenticazione OPKSSH",
"forceKeyboardInteractive": "Forza Tastiera-Interattiva",
"forceKeyboardInteractiveDesc": "Forza l'uso dell'autenticazione interattiva con tastiera. A volte è necessario per i server che utilizzano Autenticazione a due fattori (TOTP/2FA).",
"overrideCredentialUsername": "Ignora Il Nome Utente Credenziale",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Apri nel browser",
"warpgateContinue": "Ho Completato L'Autenticazione",
"warpgateTimeout": "Timeout autenticazione Warpgate. Per favore riconnetterti.",
"opksshAuthRequired": "Richiesta Autenticazione OPKSSH",
"opksshAuthDescription": "Completa l'autenticazione nel tuo browser per continuare. Questa sessione rimarrà valida per 24 ore.",
"opksshAuthUrl": "Url Di Autenticazione",
"opksshOpenBrowser": "Apri Browser per Autenticare",
"opksshWaitingForAuth": "In attesa di autenticazione nel browser...",
"opksshAuthenticating": "Elaborazione autenticazione...",
"opksshTimeout": "Autenticazione scaduta. Riprova.",
"opksshAuthFailed": "Autenticazione non riuscita. Controlla le tue credenziali e riprova.",
"opksshConfigMissing": "Configurazione OPKSSH non trovata. Si prega di creare ~/.opk/config.yml con le impostazioni del provider OIDC. Vedi documentazione: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Inserire Password?",
"sudoPasswordPopupHint": "Premere Invio per inserire, Esc per eliminare",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Prova automaticamente l'autenticazione {{method}}...",
"totpTimeout": "Timeout verifica TOTP. Riconnettersi.",
"passwordTimeout": "Timeout verifica password. Si prega di riconnettersi.",
"connectionRejected": "Connessione rifiutata dal server. Controlla la tua autenticazione e configurazione di rete."
"connectionRejected": "Connessione rifiutata dal server. Controlla la tua autenticazione e configurazione di rete.",
"hostKeyRejected": "Verifica della chiave host SSH rifiutata. Connessione annullata."
},
"fileManager": {
"title": "Gestore File",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Reset Password Con Successo",
"passwordResetSuccessDesc": "La password è stata reimpostata con successo. Ora puoi accedere con la tua nuova password."
},
"hostKey": {
"verifyNewHost": "Verifica La Chiave Host Ssh",
"keyChangedWarning": "Chiave Host Ssh Cambiata",
"firstConnectionTitle": "Prima volta che ci si connette a questo host",
"firstConnectionDescription": "L'autenticità di questo host non può essere stabilita. Verifica che l'impronta digitale corrisponda a quanto ci si aspetta.",
"keyChangedDescription": "La chiave host di questo server è cambiata dall'ultima connessione. Questo potrebbe indicare un problema di sicurezza.",
"previousKey": "Chiave Precedente",
"newFingerprint": "Nuova Impronta Digitale",
"fingerprint": "Impronta",
"verifyInstructions": "Se ti fidi di questo host, fai clic su Accetta per continuare e salvare questa impronta digitale per le connessioni future.",
"securityWarning": "Avviso Di Sicurezza",
"acceptAndContinue": "Accetta E Continua",
"acceptNewKey": "Accetta La Nuova Chiave E Continua"
},
"errors": {
"notFound": "Pagina non trovata",
"unauthorized": "Accesso non autorizzato",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminale",
"hostSidebarSettings": "& Barra Laterale Dell'Host",
"snippetsSettings": "Snippet",
"confirmSnippetExecution": "Conferma Esecuzione Snippet",
"confirmSnippetExecutionDesc": "Mostra la finestra di conferma prima di eseguire snippet",
"updateSettings": "Aggiornamenti",
"disableUpdateCheck": "Disabilita Controllo Aggiornamenti",
"disableUpdateCheckDesc": "Smetti di controllare nuove versioni all'avvio e alla dashboard. Riduce le richieste di rete.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 o example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "資格情報",
"credentialsViewer": "資格情報ビューアー",
"manageYourSSHCredentials": "SSH認証情報を安全に管理します",
"addCredential": "証明書の追加",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "これにより、既存の鍵を上書きすることなく、公開鍵をホストの ~/.ssh/authorized_keys ファイルに安全に追加します。操作は取り消し可能です。",
"chooseHostToDeploy": "デプロイするホストを選択してください...",
"deploying": "デプロイ中...",
"copyDeployCommand": "デプロイコマンドをコピー",
"copyDeployCommandDescription": "シェルコマンドをコピーして、ホストの authorized_keys ファイルに公開鍵を手動で追加します。パスワード認証が無効になっている場合に便利です。",
"deployCommandCopied": "デプロイコマンドがクリップボードにコピーされました",
"manualDeployInfo": "SSHキーを追加するには、このコマンドをターゲットホストの端末(例:Proxmoxコンソール、IPMI、または物理アクセス)に貼り付けます。",
"name": "名前",
"noHostsAvailable": "利用可能なホストがありません",
"noHostsMatchSearch": "検索条件に一致するホストがありません",
@@ -298,7 +303,9 @@
"createFolder": "フォルダを作成",
"editFolder": "フォルダを編集",
"editFolderDescription": "スニペットフォルダをカスタマイズ",
"createFolderDescription": "スニペットをフォルダーに整理する"
"createFolderDescription": "スニペットをフォルダーに整理する",
"confirmExecution": "「{{name}}」を実行しますか?",
"confirmExecutionDesc": "このスニペットを実行してもよろしいですか?"
},
"commandHistory": {
"title": "沿革",
@@ -402,6 +409,7 @@
"required": "必須",
"optional": "省略可能",
"connect": "接続する",
"copied": "コピーしました",
"connecting": "接続中...",
"creating": "作成中...",
"clear": "クリア",
@@ -494,6 +502,7 @@
"checking": "確認しています...",
"checkingDatabase": "データベース接続を確認しています...",
"checkingAuthentication": "認証中char@@0",
"backendReconnected": "サーバー接続が復元されました",
"actions": "アクション",
"remove": "削除",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "ホスト マネージャー",
"hosts": "ホスト",
"sshHosts": "SSH ホスト",
"noHosts": "SSHホストがありません",
"noHostsMessage": "SSHホストはまだ追加されていません。「ホストを追加」をクリックして開始してください。",
@@ -836,7 +846,7 @@
"failedToImportJson": "JSON ファイルのインポートに失敗しました",
"connectionDetails": "接続詳細",
"organization": "組織",
"ipAddress": "IP アドレス",
"ipAddress": "IPアドレスまたはホスト名",
"port": "ポート",
"name": "名前",
"username": "ユーザー名",
@@ -915,6 +925,8 @@
"key": "キー",
"credential": "資格情報",
"none": "なし",
"opkssh": "OPKSSH",
"opksshAuthDescription": "接続ごとにOPKSSHのweb-authを表示します。認証トークンは更新されるまで24時間持続します。",
"selectCredential": "証明書を選択",
"selectCredentialPlaceholder": "資格情報を選択...",
"credentialRequired": "資格情報認証を使用するには認証情報が必要です",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "キーボード対話型認証",
"noneAuthDescription": "この認証方法は、SSH サーバーに接続する際にキーボード対話型認証を使用します。",
"noneAuthDetails": "キーボード対話型認証により、サーバーは接続中に資格情報の入力を求めることができます。 これは、多要素認証を必要とするサーバーや、ローカルで資格情報を保存したくないサーバーに便利です。",
"opksshAuthTitle": "OPKSSH 認証",
"forceKeyboardInteractive": "キーボード対話を強制する",
"forceKeyboardInteractiveDesc": "2要素認証(TOTP/2FA)を使用するサーバーでは、キーボード対話型認証を強制的に使用します。",
"overrideCredentialUsername": "認証情報のユーザー名を上書きする",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "ブラウザで開く",
"warpgateContinue": "認証を完了しました",
"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": "パスワードを入力してください。",
"sudoPasswordPopupHint": "Enterキーを押して挿入、Escキーで閉じる",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "{{method}} 認証を自動的に試みています...",
"totpTimeout": "TOTP 認証がタイムアウトしました。再接続してください。",
"passwordTimeout": "パスワードの検証がタイムアウトしました。再接続してください。",
"connectionRejected": "サーバーによって接続が拒否されました。認証とネットワーク設定を確認してください。"
"connectionRejected": "サーバーによって接続が拒否されました。認証とネットワーク設定を確認してください。",
"hostKeyRejected": "SSHホスト鍵の検証が拒否されました。接続がキャンセルされました。"
},
"fileManager": {
"title": "ファイルマネージャー",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "パスワードをリセットしました",
"passwordResetSuccessDesc": "パスワードが正常にリセットされました。新しいパスワードでログインできるようになりました。"
},
"hostKey": {
"verifyNewHost": "SSHホストキーを確認",
"keyChangedWarning": "SSH ホストキーが変更されました",
"firstConnectionTitle": "初めてこのホストに接続しました",
"firstConnectionDescription": "このホストの信頼性を確立できません。指紋が期待されているものと一致することを確認してください。",
"keyChangedDescription": "このサーバーのホストキーは、最後の接続から変更されました。これはセキュリティ上の問題を示している可能性があります。",
"previousKey": "前のキー",
"newFingerprint": "新しいフィンガープリント",
"fingerprint": "フィンガープリント",
"verifyInstructions": "このホストを信頼している場合は、format@@0をクリックして続行し、今後の接続のためにこの指紋を保存します。",
"securityWarning": "セキュリティ警告",
"acceptAndContinue": "承認して続ける",
"acceptNewKey": "新しいキーを承認して続ける"
},
"errors": {
"notFound": "ページが見つかりません",
"unauthorized": "権限のないアクセス",
@@ -2026,6 +2063,8 @@
"terminalSettings": "ターミナル",
"hostSidebarSettings": "ホスト & サイドバー",
"snippetsSettings": "Snippets",
"confirmSnippetExecution": "スニペットの実行を確認",
"confirmSnippetExecutionDesc": "スニペットを実行する前に確認ダイアログを表示する",
"updateSettings": "更新",
"disableUpdateCheck": "アップデートチェックを無効にする",
"disableUpdateCheckDesc": "起動時とダッシュボードで新しいバージョンの確認を停止します。ネットワークリクエストを削減します。",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1またはexample.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "신임장",
"credentialsViewer": "자격 증명 뷰어",
"manageYourSSHCredentials": "SSH 자격 증명을 안전하게 관리하세요",
"addCredential": "자격 증명 추가",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "이렇게 하면 기존 키를 덮어쓰지 않고 대상 호스트의 ~/.ssh/authorized_keys 파일에 공개 키가 안전하게 추가됩니다. 이 작업은 되돌릴 수 있습니다.",
"chooseHostToDeploy": "배포할 호스트를 선택하세요...",
"deploying": "배포 중...",
"copyDeployCommand": "배포 명령 복사",
"copyDeployCommandDescription": "호스트의 authorized_keys 파일에 공개 키를 수동으로 추가하는 셸 명령어를 복사합니다. 암호 인증이 비활성화된 경우 유용합니다.",
"deployCommandCopied": "배포 명령어가 클립보드에 복사되었습니다.",
"manualDeployInfo": "대상 호스트의 터미널(예: Proxmox 콘솔, IPMI 또는 물리적 접근)에 다음 명령어를 붙여넣어 SSH 키를 추가하십시오.",
"name": "이름",
"noHostsAvailable": "호스트를 사용할 수 없습니다.",
"noHostsMatchSearch": "검색 조건에 맞는 호스트가 없습니다.",
@@ -298,7 +303,9 @@
"createFolder": "폴더 생성",
"editFolder": "폴더 편집",
"editFolderDescription": "스니펫 폴더를 사용자 지정하세요",
"createFolderDescription": "메모들을 폴더별로 정리하세요"
"createFolderDescription": "메모들을 폴더별로 정리하세요",
"confirmExecution": "\"{{name}}\"을 실행하시겠습니까?",
"confirmExecutionDesc": "이 코드 조각을 실행하시겠습니까?"
},
"commandHistory": {
"title": "역사",
@@ -402,6 +409,7 @@
"required": "필수의",
"optional": "선택 과목",
"connect": "연결하다",
"copied": "복사됨",
"connecting": "연결 중...",
"creating": "생성 중...",
"clear": "분명한",
@@ -494,6 +502,7 @@
"checking": "확인 중...",
"checkingDatabase": "데이터베이스 연결을 확인하는 중...",
"checkingAuthentication": "인증 상태를 확인하는 중...",
"backendReconnected": "서버 연결이 복구되었습니다.",
"actions": "행위",
"remove": "제거하다",
"revoke": "취소",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "호스트 관리자",
"hosts": "호스트",
"sshHosts": "SSH 호스트",
"noHosts": "SSH 호스트 없음",
"noHostsMessage": "아직 SSH 호스트를 추가하지 않았습니다. 시작하려면 \"호스트 추가\"를 클릭하세요.",
@@ -836,7 +846,7 @@
"failedToImportJson": "JSON 파일을 가져오는 데 실패했습니다.",
"connectionDetails": "연결 정보",
"organization": "조직",
"ipAddress": "IP 주소",
"ipAddress": "IP 주소 또는 호스트 이름",
"port": "포트",
"name": "이름",
"username": "사용자 이름",
@@ -915,6 +925,8 @@
"key": "열쇠",
"credential": "신임장",
"none": "없음",
"opkssh": "오프크시",
"opksshAuthDescription": "접속할 때마다 OPKSSH 웹 인증을 요청합니다. 인증 토큰은 24시간 동안 유효하며, 갱신해야 합니다.",
"selectCredential": "자격증명을 선택하세요",
"selectCredentialPlaceholder": "자격증을 선택하세요...",
"credentialRequired": "자격 증명 인증을 사용할 때는 자격 증명이 필요합니다.",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "키보드 인터랙티브 인증",
"noneAuthDescription": "이 인증 방식은 SSH 서버에 연결할 때 키보드 대화형 인증을 사용합니다.",
"noneAuthDetails": "키보드 대화형 인증을 사용하면 서버에 연결하는 동안 자격 증명을 입력하라는 메시지가 표시됩니다. 이는 다단계 인증이 필요한 서버나 자격 증명을 로컬에 저장하지 않으려는 경우에 유용합니다.",
"opksshAuthTitle": "OPKSSH 인증",
"forceKeyboardInteractive": "키보드 상호작용 강제 적용",
"forceKeyboardInteractiveDesc": "키보드 입력 방식의 인증을 강제로 사용하게 합니다. 이는 2단계 인증(TOTP/2FA)을 사용하는 서버에서 필요한 경우가 있습니다.",
"overrideCredentialUsername": "자격 증명 사용자 이름 재정의",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "브라우저에서 열기",
"warpgateContinue": "인증이 완료되었습니다",
"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": "비밀번호를 입력하시겠습니까?",
"sudoPasswordPopupHint": "Enter 키를 눌러 삽입하고, Esc 키를 눌러 닫습니다.",
"sudoPasswordPopupConfirm": "끼워 넣다",
@@ -1337,7 +1359,8 @@
"automaticFallback": "{{method}} 인증을 자동으로 시도합니다...",
"totpTimeout": "TOTP 인증 시간이 초과되었습니다. 다시 연결해 주세요.",
"passwordTimeout": "비밀번호 인증 시간이 초과되었습니다. 다시 연결해 주세요.",
"connectionRejected": "서버에서 연결이 거부되었습니다. 인증 및 네트워크 구성을 확인하십시오."
"connectionRejected": "서버에서 연결이 거부되었습니다. 인증 및 네트워크 구성을 확인하십시오.",
"hostKeyRejected": "SSH 호스트 키 인증이 거부되었습니다. 연결이 취소되었습니다."
},
"fileManager": {
"title": "파일 관리자",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "비밀번호 재설정 성공",
"passwordResetSuccessDesc": "비밀번호가 성공적으로 재설정되었습니다. 이제 새 비밀번호로 로그인할 수 있습니다."
},
"hostKey": {
"verifyNewHost": "SSH 호스트 키 확인",
"keyChangedWarning": "SSH 호스트 키가 변경되었습니다.",
"firstConnectionTitle": "이 호스트에 처음 연결합니다.",
"firstConnectionDescription": "이 호스트의 진위 여부를 확인할 수 없습니다. 지문이 예상과 일치하는지 확인하십시오.",
"keyChangedDescription": "이 서버의 호스트 키가 마지막 접속 이후 변경되었습니다. 이는 보안 문제를 나타낼 수 있습니다.",
"previousKey": "이전 키",
"newFingerprint": "새로운 지문",
"fingerprint": "지문",
"verifyInstructions": "이 호스트를 신뢰한다면 '수락'을 클릭하여 계속 진행하고 향후 연결을 위해 이 지문을 저장하세요.",
"securityWarning": "보안 경고",
"acceptAndContinue": "수락 및 계속",
"acceptNewKey": "새 키를 수락하고 계속 진행하세요"
},
"errors": {
"notFound": "페이지를 찾을 수 없습니다",
"unauthorized": "무단 접근",
@@ -2026,6 +2063,8 @@
"terminalSettings": "터미널",
"hostSidebarSettings": "호스트 및 사이드바",
"snippetsSettings": "스니펫",
"confirmSnippetExecution": "스니펫 실행 확인",
"confirmSnippetExecutionDesc": "코드 조각 실행 전에 확인 대화 상자를 표시합니다.",
"updateSettings": "업데이트",
"disableUpdateCheck": "업데이트 확인 비활성화",
"disableUpdateCheckDesc": "시작 시 및 대시보드에서 새 버전 확인을 중지합니다. 네트워크 요청이 줄어듭니다.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 또는 example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Aanmeldgegevens",
"credentialsViewer": "Referenties bekijken",
"manageYourSSHCredentials": "Beheer uw SSH inloggegevens veilig",
"addCredential": "Toegangsgegevens toevoegen",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Dit zal veilig de publieke sleutel toevoegen aan het doel host's ~/.ssh/authorized_keys bestand zonder bestaande sleutels te overschrijven. De bewerking is omkeerbaar.",
"chooseHostToDeploy": "Kies een host om naar te implementeren...",
"deploying": "Implementeren...",
"copyDeployCommand": "Kopieer implementatieopdracht",
"copyDeployCommandDescription": "Kopieer een shell commando om handmatig de publieke sleutel toe te voegen aan het authorized_keys bestand van een host. Handig wanneer wachtwoordauthenticatie is uitgeschakeld.",
"deployCommandCopied": "Implementeer opdracht gekopieerd naar klembord",
"manualDeployInfo": "Plak deze opdracht in de terminal van de doel host (bijv. Proxmox console, IPMI of fysieke toegang) om de SSH sleutel toe te voegen.",
"name": "naam",
"noHostsAvailable": "Geen hosts beschikbaar",
"noHostsMatchSearch": "Er zijn geen hosts die voldoen aan jouw zoekopdracht",
@@ -298,7 +303,9 @@
"createFolder": "Map aanmaken",
"editFolder": "Map bewerken",
"editFolderDescription": "Tekstbouwstenenmap aanpassen",
"createFolderDescription": "Organiseer uw snippets in mappen"
"createFolderDescription": "Organiseer uw snippets in mappen",
"confirmExecution": "Uitvoeren \"{{name}}\"?",
"confirmExecutionDesc": "Weet u zeker dat u deze snippet wilt uitvoeren?"
},
"commandHistory": {
"title": "Geschiedenis",
@@ -402,6 +409,7 @@
"required": "vereist",
"optional": "Optioneel",
"connect": "Verbinden",
"copied": "Gekoppeld",
"connecting": "Verbinden...",
"creating": "Maken...",
"clear": "Verwijderen",
@@ -494,6 +502,7 @@
"checking": "Controleren...",
"checkingDatabase": "Database-verbinding controleren...",
"checkingAuthentication": "Authenticatie controleren...",
"backendReconnected": "Serververbinding hersteld",
"actions": "acties",
"remove": "Verwijderen",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Host Manager",
"hosts": "Verantwoordelijken",
"sshHosts": "SSH hosts",
"noHosts": "Geen SSH hosts",
"noHostsMessage": "U heeft nog geen SSH hosts toegevoegd. Klik op \"Host toevoegen\" om te beginnen.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Kan JSON bestand niet importeren",
"connectionDetails": "Connectie Details",
"organization": "Rekening",
"ipAddress": "IP adres",
"ipAddress": "IP-adres of hostnaam",
"port": "Poort",
"name": "naam",
"username": "Gebruikersnaam",
@@ -915,6 +925,8 @@
"key": "Sleutel",
"credential": "Toegangsgegevens",
"none": "geen",
"opkssh": "OPKENEN",
"opksshAuthDescription": "Verzoekt u naar OPKSSH web-authenticatie bij elke verbinding. Uw autorisatietoken zal 24 uur duren tot het moet worden vernieuwd.",
"selectCredential": "Selecteer referenties",
"selectCredentialPlaceholder": "Kies een inloggegevens...",
"credentialRequired": "Referentiegegevens zijn vereist bij het gebruik van referentieauthenticatie",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Toetsenbord-Interactieve authenticatie",
"noneAuthDescription": "Deze verificatiemethode gebruikt keyboard-interactieve authenticatie bij het verbinden met de SSH-server.",
"noneAuthDetails": "Keyboard-interactieve authenticatie maakt het mogelijk dat de server je om inloggegevens vraagt tijdens het verbinden. Dit is handig voor servers waarvoor multi-factor authenticatie vereist is of als u geen lokale inloggegevens wilt opslaan.",
"opksshAuthTitle": "OPKSSH authenticatie",
"forceKeyboardInteractive": "Forceer toetsenbord-interactief",
"forceKeyboardInteractiveDesc": "Forceert het gebruik van keyboard-interactieve authenticatie. Dit is soms vereist voor servers die gebruik maken van tweestapsverificatie (TOTP/2FA).",
"overrideCredentialUsername": "Aanmeldgebruikersnaam overschrijven",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Open in browser",
"warpgateContinue": "Ik heb authenticatie voltooid",
"warpgateTimeout": "Warpgate authenticatie time-out. Maak opnieuw verbinding.",
"opksshAuthRequired": "OPKSSH authenticatie vereist",
"opksshAuthDescription": "Voltooi de authenticatie in je browser om door te gaan. Deze sessie blijft 24 uur geldig.",
"opksshAuthUrl": "URL authenticatie",
"opksshOpenBrowser": "Browser openen om te verifiëren",
"opksshWaitingForAuth": "Wachten op authenticatie in browser...",
"opksshAuthenticating": "Authenticatie verwerken...",
"opksshTimeout": "Authenticatie is verlopen. Probeer het opnieuw.",
"opksshAuthFailed": "Authenticatie mislukt. Controleer uw inloggegevens en probeer het opnieuw.",
"opksshConfigMissing": "OPKSSH configuratie niet gevonden. Maak ~/.opk/config.yml aan met uw OIDC providerinstellingen. Zie documentatie: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Wachtwoord invoeren?",
"sudoPasswordPopupHint": "Druk op Enter om in te voegen, Esc om te sluiten",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Authenticatie {{method}} automatisch proberen...",
"totpTimeout": "TOTP-verificatie time-out. Gelieve opnieuw te verbinden.",
"passwordTimeout": "Wachtwoord verificatie time-out. Gelieve opnieuw te verbinden.",
"connectionRejected": "Verbinding geweigerd door de server. Controleer uw authenticatie en netwerkconfiguratie."
"connectionRejected": "Verbinding geweigerd door de server. Controleer uw authenticatie en netwerkconfiguratie.",
"hostKeyRejected": "SSH host sleutel verificatie afgewezen. Verbinding geannuleerd."
},
"fileManager": {
"title": "Bestands Beheer",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Wachtwoord opnieuw instellen geslaagd",
"passwordResetSuccessDesc": "Uw wachtwoord is met succes gereset. U kunt nu inloggen met uw nieuwe wachtwoord."
},
"hostKey": {
"verifyNewHost": "Verifieer SSH host-sleutel",
"keyChangedWarning": "SSH host-sleutel gewijzigd",
"firstConnectionTitle": "Eerste keer verbinden met deze host",
"firstConnectionDescription": "De authenticiteit van deze host kan niet worden vastgesteld. Controleer of de vingerafdruk overeenkomt met uw verwachting.",
"keyChangedDescription": "De hostsleutel voor deze server is veranderd sinds je laatste verbinding. Dit kan duiden op een beveiligingsprobleem.",
"previousKey": "Vorige sleutel",
"newFingerprint": "Nieuwe vingerafdruk",
"fingerprint": "Vingerafdruk",
"verifyInstructions": "Als je deze host vertrouwt, klik je op Accepteren om door te gaan en sla deze vingerafdruk op voor toekomstige verbindingen.",
"securityWarning": "Waarschuwing beveiliging",
"acceptAndContinue": "Accepteren & doorgaan",
"acceptNewKey": "Accepteer Nieuwe Sleutel & Doorgaan"
},
"errors": {
"notFound": "Pagina niet gevonden",
"unauthorized": "Onbevoegde toegang",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Host & Sidebar",
"snippetsSettings": "Tekstbouwstenen",
"confirmSnippetExecution": "Bevestig de uitvoering van de Snippet",
"confirmSnippetExecutionDesc": "Toon bevestigingsvenster voor het uitvoeren van snippets",
"updateSettings": "Bijwerken",
"disableUpdateCheck": "Updatecontrole uitschakelen",
"disableUpdateCheckDesc": "Stop met controleren op nieuwe versies bij opstarten en dashboard. Vermindert netwerkaanvragen.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 of voorbeeld.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Legitimasjon",
"credentialsViewer": "Legitimasjonsvisning",
"manageYourSSHCredentials": "Administrer SSH-legitimasjon sikkert",
"addCredential": "Legg til legitimasjon",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Dette legger den offentlige nøkkelen trygt til målserverens ~/.ssh/authorized_keys uten å overskrive eksisterende nøkler. Operasjonen kan reverseres.",
"chooseHostToDeploy": "Velg en vert å distribuere til...",
"deploying": "Distribuerer...",
"copyDeployCommand": "Kopier distribueringskommandoen",
"copyDeployCommandDescription": "Kopier en skallkommando for å legge den offentlige nøkkelen til en verts autorisert_key-fil. Nyttig når passordautentisering er deaktivert.",
"deployCommandCopied": "Distribuer kommandoen kopiert til utklippstavlen",
"manualDeployInfo": "Lim inn denne kommandoen i målvertens terminal (f.eks. Proxmox konsoll, IPMI eller fysisk tilgang) for å legge til SSH-nøkkelen.",
"name": "Navn",
"noHostsAvailable": "Ingen verter tilgjengelig",
"noHostsMatchSearch": "Ingen verter samsvarer med søket",
@@ -298,7 +303,9 @@
"createFolder": "Opprett mappe",
"editFolder": "Rediger mappe",
"editFolderDescription": "Tilpass utdragsmappen din",
"createFolderDescription": "Organiser snippeten dine i mapper"
"createFolderDescription": "Organiser snippeten dine i mapper",
"confirmExecution": "Utfør \"{{name}}\"?",
"confirmExecutionDesc": "Er du sikker på at du vil utføre dette snippet?"
},
"commandHistory": {
"title": "Historikk",
@@ -402,6 +409,7 @@
"required": "Påkrevd",
"optional": "Valgfritt",
"connect": "Koble til",
"copied": "Kopiert",
"connecting": "Kobler til...",
"creating": "Oppretter...",
"clear": "Tøm",
@@ -494,6 +502,7 @@
"checking": "Kontrollerer...",
"checkingDatabase": "Kontrollerer databaseforbindelse...",
"checkingAuthentication": "Kontrollerer autentisering...",
"backendReconnected": "Servertilkobling gjenopprettet",
"actions": "Handlinger",
"remove": "Fjern",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Vertadministrator",
"hosts": "Verter",
"sshHosts": "SSH-verter",
"noHosts": "Ingen SSH-verter",
"noHostsMessage": "Du har ikke lagt til noen SSH-verter ennå. Klikk \"Legg til vert\" for å begynne.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Kunne ikke importere JSON-fil",
"connectionDetails": "Tilkoblingsdetaljer",
"organization": "Organisasjon",
"ipAddress": "IP-adresse",
"ipAddress": "IP adresse eller vertsnavn",
"port": "Port",
"name": "Navn",
"username": "Brukernavn",
@@ -915,6 +925,8 @@
"key": "Nøkkel",
"credential": "Legitimasjon",
"none": "Ingen",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Ledetekster deg for OPKSSH web-auth ved hver tilkobling. Auth token varer i 24 timer til den må fornyes.",
"selectCredential": "Velg legitimasjon",
"selectCredentialPlaceholder": "Velg en legitimasjon...",
"credentialRequired": "Legitimasjon kreves ved legitimasjonsautentisering",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Tastaturinteraktiv autentisering",
"noneAuthDescription": "Denne autentiseringsmetoden bruker tastaturinteraktiv autentisering ved tilkobling til SSH-serveren.",
"noneAuthDetails": "Tastaturinteraktiv autentisering lar serveren be deg om legitimasjon under tilkobling. Nyttig for servere som krever flerfaktorautentisering eller hvis du ikke vil lagre legitimasjon lokalt.",
"opksshAuthTitle": "OPKSSH godkjenning",
"forceKeyboardInteractive": "Tving tastaturinteraktiv",
"forceKeyboardInteractiveDesc": "Tving bruk av tastaturinteraktiv autentisering. Ofte nødvendig for servere som bruker tofaktorautentisering (TOTP/2FA).",
"overrideCredentialUsername": "Overstyr brukernavn fra legitimasjon",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Åpne i nettleser",
"warpgateContinue": "Jeg har fullført godkjenning",
"warpgateTimeout": "Warpgate autentisering tidsavbrudd. Vennligst koble til på nytt.",
"opksshAuthRequired": "OPKSSH godkjenning kreves",
"opksshAuthDescription": "Fullstendig autentisering i nettleseren din for å fortsette. Denne økten er gyldig i 24 timer.",
"opksshAuthUrl": "Autentisering URL",
"opksshOpenBrowser": "Åpne nettleseren for å godkjenne",
"opksshWaitingForAuth": "Venter på godkjenning i nettleseren...",
"opksshAuthenticating": "Behandler autentisering...",
"opksshTimeout": "Autentisering ble tidsavbrutt. Vennligst prøv igjen.",
"opksshAuthFailed": "Godkjenning mislyktes. Kontroller påloggingsinformasjonen, og prøv på nytt.",
"opksshConfigMissing": "OPKSSH konfigurasjon ikke funnet. Lag ~/.opk/config.yml med dine OIDC leverandørinnstillinger. Se dokumentasjon: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Sette inn passord?",
"sudoPasswordPopupHint": "Trykk på Enter for å sette inn, Esc for å fjerne",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Prøver automatisk {{method}} autentisering...",
"totpTimeout": "TOTP-verifisering tidsavbrudd. Vennligst koble til på nytt.",
"passwordTimeout": "Tidsavbrudd for bekreftelse av passord. Vennligst koble til på nytt.",
"connectionRejected": "Tilkobling avvist av serveren. Kontroller godkjennings- og nettverkskonfigurasjonen."
"connectionRejected": "Tilkobling avvist av serveren. Kontroller godkjennings- og nettverkskonfigurasjonen.",
"hostKeyRejected": "SSH vertsnøkkelverifisering ble avvist. Tilkoblingen avbrutt."
},
"fileManager": {
"title": "Filbehandler",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Tilbakestilling av passord vellykket",
"passwordResetSuccessDesc": "Passordet er blitt tilbakestilt. Du kan nå logge inn med ditt nye passord."
},
"hostKey": {
"verifyNewHost": "Verifiser SSH vertsnøkkel",
"keyChangedWarning": "SSH vertsnøkkel endret",
"firstConnectionTitle": "Første gang du kobler til denne verten",
"firstConnectionDescription": "Vertens autensitet kan ikke etableres. Kontroller fingeravtrykket samsvarer med det du forventer.",
"keyChangedDescription": "Vertsnøkkelen for denne serveren er endret siden forrige tilkobling. Dette kan angi et sikkerhetsproblem.",
"previousKey": "Forrige nøkkel",
"newFingerprint": "Nytt fingeravtrykk",
"fingerprint": "Fingeravtrykk",
"verifyInstructions": "Hvis du stoler på denne verten, klikk Godta for å fortsette og lagre dette fingeravtrykket for fremtidige tilkoblinger.",
"securityWarning": "Sikkerhetsadvarsel",
"acceptAndContinue": "Godta og fortsett",
"acceptNewKey": "Aksepter ny nøkkel og fortsett"
},
"errors": {
"notFound": "Siden ble ikke funnet",
"unauthorized": "Uautorisert tilgang",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Verts- og sidepanelet",
"snippetsSettings": "Snippets",
"confirmSnippetExecution": "Bekreft kjøring av bruddtekst",
"confirmSnippetExecutionDesc": "Vis bekreftelsesdialog før du utfører snippets",
"updateSettings": "Oppdateringer",
"disableUpdateCheck": "Deaktiver oppdateringskontroll",
"disableUpdateCheckDesc": "Slutt å sjekke nye versjoner ved oppstart og dashbord. Reduserer nettverksforespørsler.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 eller eksempel.no",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Dane logowania",
"credentialsViewer": "Przeglądarka danych logowania",
"manageYourSSHCredentials": "Zarządzaj swoimi danymi uwierzytelniającymi SSH bezpiecznie",
"addCredential": "Dodaj poświadczenia",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "To bezpiecznie doda klucz publiczny do pliku hosta ~/.ssh/authorized_keys bez nadpisywania istniejących kluczy. Operacja jest odwracalna.",
"chooseHostToDeploy": "Wybierz hosta do wdrożenia...",
"deploying": "Wdrażanie...",
"copyDeployCommand": "Kopiuj polecenie wdrożenia",
"copyDeployCommandDescription": "Skopiuj polecenie powłoki, aby ręcznie dodać klucz publiczny do pliku authorized_keys hosta. Przydatne, gdy uwierzytelnianie hasła jest wyłączone.",
"deployCommandCopied": "Wdrożenie polecenia skopiowane do schowka",
"manualDeployInfo": "Wklej to polecenie w terminalu docelowego hosta (np. konsola Proxmox, IPMI lub fizyczny dostęp) aby dodać klucz SSH.",
"name": "Nazwisko",
"noHostsAvailable": "Brak dostępnych hostów",
"noHostsMatchSearch": "Brak hostów pasujących do Twojego wyszukiwania",
@@ -298,7 +303,9 @@
"createFolder": "Utwórz folder",
"editFolder": "Edytuj folder",
"editFolderDescription": "Dostosuj swój folder snippet",
"createFolderDescription": "Organizuj swoje fragmenty w folderach"
"createFolderDescription": "Organizuj swoje fragmenty w folderach",
"confirmExecution": "Wykonać \"{{name}}\"?",
"confirmExecutionDesc": "Czy na pewno chcesz wykonać ten snippet?"
},
"commandHistory": {
"title": "Historia",
@@ -402,6 +409,7 @@
"required": "Wymagane",
"optional": "Opcjonalnie",
"connect": "Połącz",
"copied": "Skopiowano",
"connecting": "Łączenie...",
"creating": "Tworzenie...",
"clear": "Wyczyść",
@@ -494,6 +502,7 @@
"checking": "Sprawdzanie...",
"checkingDatabase": "Sprawdzanie połączenia z bazą danych...",
"checkingAuthentication": "Sprawdzanie uwierzytelniania...",
"backendReconnected": "Połączenie z serwerem przywrócone",
"actions": "Akcje",
"remove": "Usuń",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Host Manager",
"hosts": "Hosty",
"sshHosts": "Hosty SSH",
"noHosts": "Brak hostów SSH",
"noHostsMessage": "Nie dodano jeszcze żadnych hostów SSH. Kliknij \"Dodaj hosta\", aby rozpocząć.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Nie udało się zaimportować pliku JSON",
"connectionDetails": "Szczegóły połączenia",
"organization": "Organizacja",
"ipAddress": "Adres IP",
"ipAddress": "Adres IP lub nazwa hosta",
"port": "Port",
"name": "Nazwisko",
"username": "Nazwa użytkownika",
@@ -915,6 +925,8 @@
"key": "Klucz",
"credential": "Dane logowania",
"none": "Brak",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Pyta Cię o autoryzację sieciową OPKSSH przy każdym połączeniu. Twój token uwierzytelniania potrwa 24 godziny, dopóki nie będzie musiał zostać przedłużony.",
"selectCredential": "Wybierz dane logowania",
"selectCredentialPlaceholder": "Wybierz poświadczenie...",
"credentialRequired": "Dane logowania są wymagane podczas korzystania z uwierzytelniania logowania",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Interaktywne uwierzytelnianie klawiatury",
"noneAuthDescription": "Ta metoda uwierzytelniania będzie używała interaktywnego uwierzytelniania klawiatury podczas łączenia z serwerem SSH.",
"noneAuthDetails": "Interaktywne uwierzytelnianie klawiatury pozwala na zapytanie serwera o poświadczenia podczas połączenia. Jest to przydatne dla serwerów, które wymagają wieloetapowego uwierzytelniania lub jeśli nie chcesz zapisywać danych logowania lokalnie.",
"opksshAuthTitle": "Uwierzytelnianie OPKSSH",
"forceKeyboardInteractive": "Wymuś Interaktywną Klawiaturę",
"forceKeyboardInteractiveDesc": "Wymusza użycie interaktywnego uwierzytelniania klawiatury. Jest to czasami wymagane dla serwerów, które używają dwustopniowego uwierzytelniania (TOTP/2FA).",
"overrideCredentialUsername": "Zastąp nazwę użytkownika",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Otwórz w przeglądarce",
"warpgateContinue": "Zakończyłem uwierzytelnianie",
"warpgateTimeout": "Limit czasu uwierzytelniania Warpgate. Proszę połączyć się ponownie.",
"opksshAuthRequired": "Wymagane uwierzytelnienie OPKSSH",
"opksshAuthDescription": "Zakończ uwierzytelnianie w przeglądarce, aby kontynuować. Ta sesja pozostanie ważna przez 24 godziny.",
"opksshAuthUrl": "Adres URL uwierzytelniania",
"opksshOpenBrowser": "Otwórz przeglądarkę do uwierzytelniania",
"opksshWaitingForAuth": "Oczekiwanie na uwierzytelnianie w przeglądarce...",
"opksshAuthenticating": "Przetwarzanie uwierzytelniania...",
"opksshTimeout": "Upłynął limit czasu uwierzytelniania. Spróbuj ponownie.",
"opksshAuthFailed": "Uwierzytelnianie nie powiodło się. Sprawdź swoje poświadczenia i spróbuj ponownie.",
"opksshConfigMissing": "Konfiguracja OPKSSH nie została znaleziona. Proszę utworzyć ~/.opk/config.yml z ustawieniami dostawcy OIDC. Zobacz dokumentacja: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Wprowadzić hasło?",
"sudoPasswordPopupHint": "Naciśnij Enter, aby wstawić, Esc, aby odrzucić",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Automatycznie próbuje uwierzytelnić {{method}}...",
"totpTimeout": "Limit czasu weryfikacji TOTP. Proszę ponownie połączyć.",
"passwordTimeout": "Limit czasu weryfikacji hasła. Proszę połączyć się ponownie.",
"connectionRejected": "Połączenie odrzucone przez serwer. Sprawdź swoje uwierzytelnianie i konfigurację sieci."
"connectionRejected": "Połączenie odrzucone przez serwer. Sprawdź swoje uwierzytelnianie i konfigurację sieci.",
"hostKeyRejected": "Weryfikacja klucza hosta SSH odrzucona. Połączenie anulowane."
},
"fileManager": {
"title": "Menedżer plików",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Hasło zostało zresetowane",
"passwordResetSuccessDesc": "Twoje hasło zostało pomyślnie zresetowane. Możesz teraz zalogować się przy użyciu nowego hasła."
},
"hostKey": {
"verifyNewHost": "Zweryfikuj klucz hosta SSH",
"keyChangedWarning": "Klucz hosta SSH został zmieniony",
"firstConnectionTitle": "Pierwsze połączenie z tym hostem",
"firstConnectionDescription": "Nie można ustalić autentyczności tego hosta. Sprawdź, czy odcisk palca odpowiada oczekiwaniom.",
"keyChangedDescription": "Klucz hosta dla tego serwera zmienił się od ostatniego połączenia. Może to wskazywać na problem bezpieczeństwa.",
"previousKey": "Poprzedni klucz",
"newFingerprint": "Nowy odcisk palca",
"fingerprint": "Odcisk palca",
"verifyInstructions": "Jeśli ufasz temu hostowi, kliknij przycisk Akceptuj, aby kontynuować i zapisać ten odcisk palca dla przyszłych połączeń.",
"securityWarning": "Ostrzeżenie bezpieczeństwa",
"acceptAndContinue": "Zaakceptuj i kontynuuj",
"acceptNewKey": "Akceptuj nowy klucz i kontynuuj"
},
"errors": {
"notFound": "Strona nie znaleziona",
"unauthorized": "Nieautoryzowany dostęp",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Pasek hosta i boczny",
"snippetsSettings": "Fragmenty",
"confirmSnippetExecution": "Potwierdź wykonanie Snippet",
"confirmSnippetExecutionDesc": "Pokaż okno dialogowe potwierdzenia przed wykonaniem fragmentów",
"updateSettings": "Aktualizacje",
"disableUpdateCheck": "Wyłącz sprawdzanie aktualizacji",
"disableUpdateCheckDesc": "Zatrzymaj sprawdzanie nowych wersji na tablicy startowej i menedżerskiej. Zmniejsza żądania sieci.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 lub przykład.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Credenciais",
"credentialsViewer": "Visualizador de credenciais",
"manageYourSSHCredentials": "Gerencie suas credenciais SSH de forma segura",
"addCredential": "Adicionar Credencial",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Isto irá adicionar com segurança a chave pública ao arquivo ~/.ssh/authorized_keys sem sobrescrever as chaves existentes. A operação é reversível.",
"chooseHostToDeploy": "Escolha um host para implantar em...",
"deploying": "Implementando...",
"copyDeployCommand": "Copiar Comando de Implementação",
"copyDeployCommandDescription": "Copie um comando do shell para adicionar manualmente a chave pública ao arquivo authorized_keys de um host. Útil quando a autenticação de senha está desativada.",
"deployCommandCopied": "Implantar comando copiado para a área de transferência",
"manualDeployInfo": "Cole este comando no terminal do host alvo (por exemplo, console Proxmox, IPMI ou acesso físico) para adicionar a chave SSH.",
"name": "Nome:",
"noHostsAvailable": "Nenhum host disponível",
"noHostsMatchSearch": "Nenhum host corresponde à sua pesquisa",
@@ -298,7 +303,9 @@
"createFolder": "Criar pasta",
"editFolder": "Editar Pasta",
"editFolderDescription": "Personalize sua pasta de snippet",
"createFolderDescription": "Organize seus snippets em pastas"
"createFolderDescription": "Organize seus snippets em pastas",
"confirmExecution": "Executar \"{{name}}\"?",
"confirmExecutionDesc": "Tem certeza que deseja executar este snippet?"
},
"commandHistory": {
"title": "Histórico",
@@ -402,6 +409,7 @@
"required": "Obrigatório",
"optional": "Opcional",
"connect": "Conectar",
"copied": "Copiado",
"connecting": "Conectandochar@@0",
"creating": "Criando...",
"clear": "Limpar",
@@ -494,6 +502,7 @@
"checking": "Verificandochar@@0",
"checkingDatabase": "Verificando conexão com o banco de dados...",
"checkingAuthentication": "Verificando autenticação...",
"backendReconnected": "Conexão do servidor restaurada",
"actions": "Ações.",
"remove": "Excluir",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Gerenciador de Host",
"hosts": "Anfitriões",
"sshHosts": "Hosts SSH",
"noHosts": "Nenhum host SSH",
"noHostsMessage": "Você ainda não adicionou nenhum host SSH. Clique em \"Adicionar Host\" para começar.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Falha ao importar arquivo JSON",
"connectionDetails": "Detalhes da conexão",
"organization": "Cliente",
"ipAddress": "Endereço IP",
"ipAddress": "Endereço IP ou Nome de Host",
"port": "Porta",
"name": "Nome:",
"username": "Usuário:",
@@ -915,6 +925,8 @@
"key": "Chave",
"credential": "Credencial",
"none": "Nenhuma",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Solicita a autenticação web do OPKSSH em cada conexão. Seu token de autenticação vai durar 24 horas até que ele deve ser renovado.",
"selectCredential": "Selecionar Credencial",
"selectCredentialPlaceholder": "Escolha uma credencial...",
"credentialRequired": "A credencial é necessária para usar autenticação de credenciais",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Autenticação interativa",
"noneAuthDescription": "Este método de autenticação usará a autenticação interativa de teclado ao conectar ao servidor SSH.",
"noneAuthDetails": "A autenticação interativa permite que o servidor lhe peça credenciais durante a conexão. Isso é útil para servidores que requerem autenticação multi-fatores ou você não quer salvar as credenciais localmente.",
"opksshAuthTitle": "Autenticação OPKSSH",
"forceKeyboardInteractive": "Forçar teclado interativo",
"forceKeyboardInteractiveDesc": "Força o uso de autenticação interativa de teclado. Isso às vezes é necessário para servidores que usam a Autenticação de Dois Fatores (TOTP/2FA).",
"overrideCredentialUsername": "Substituir o nome de usuário credencial",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Abrir no Navegador",
"warpgateContinue": "Eu concluí a autenticação",
"warpgateTimeout": "A autenticação do Warpgate expirou. Por favor reconecte-se.",
"opksshAuthRequired": "Autenticação OPKSSH necessária",
"opksshAuthDescription": "Conclua a autenticação no seu navegador para continuar. Essa sessão permanecerá válida por 24 horas.",
"opksshAuthUrl": "URL de autenticação",
"opksshOpenBrowser": "Abra o navegador para autenticar",
"opksshWaitingForAuth": "Aguardando autenticação no navegador...",
"opksshAuthenticating": "Processando autenticação...",
"opksshTimeout": "Tempo de autenticação esgotado. Por favor, tente novamente.",
"opksshAuthFailed": "Falha na autenticação. Por favor, verifique suas credenciais e tente novamente.",
"opksshConfigMissing": "Configuração OPKSSH não encontrada. Por favor, crie ~/.opk/config.yml com as configurações do provedor OIDC. Veja documentação: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Inserir senha?",
"sudoPasswordPopupHint": "Pressione Enter para inserir, Esc para dispensar",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Experimentando automaticamente autenticação {{method}}...",
"totpTimeout": "A verificação TOTP expirou. Por favor, reconecte-se.",
"passwordTimeout": "Verificação de senha expirada. Por favor, reconecte-se.",
"connectionRejected": "Conexão rejeitada pelo servidor. Verifique sua autenticação e configuração de rede."
"connectionRejected": "Conexão rejeitada pelo servidor. Verifique sua autenticação e configuração de rede.",
"hostKeyRejected": "Verificação de chave do host SSH rejeitada. Conexão cancelada."
},
"fileManager": {
"title": "Gerenciador de Arquivos",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Senha redefinida com sucesso",
"passwordResetSuccessDesc": "Sua senha foi redefinida com sucesso. Você agora pode entrar com sua nova senha."
},
"hostKey": {
"verifyNewHost": "Verificar chave do host SSH",
"keyChangedWarning": "Chave do host SSH alterada",
"firstConnectionTitle": "Primeira vez conectando-se a este host",
"firstConnectionDescription": "A autenticidade deste host não pode ser estabelecida. Verifique a impressão digital que você espera.",
"keyChangedDescription": "A chave do host para este servidor mudou desde a sua última conexão. Isso pode indicar um problema de segurança.",
"previousKey": "Chave anterior",
"newFingerprint": "Nova impressão digital",
"fingerprint": "Impressão digital",
"verifyInstructions": "Caso você confie neste host, clique em Aceitar para continuar e salvar esta impressão digital para conexões futuras.",
"securityWarning": "Aviso de segurança",
"acceptAndContinue": "Aceitar e Continuar",
"acceptNewKey": "Aceitar nova chave e continuar"
},
"errors": {
"notFound": "Página não encontrada",
"unauthorized": "Acesso não autorizado",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Host e Barra Lateral",
"snippetsSettings": "Trechos",
"confirmSnippetExecution": "Confirmar execução de Snippet",
"confirmSnippetExecutionDesc": "Mostrar diálogo antes de executar snippets",
"updateSettings": "Atualizações",
"disableUpdateCheck": "Desativar verificação de atualizações",
"disableUpdateCheckDesc": "Parar de verificar novas versões na inicialização e no painel. Reduz as solicitações de rede.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 ou exemplo.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Acreditări",
"credentialsViewer": "Vizualizare acreditări",
"manageYourSSHCredentials": "Gestionează credențialele SSH în siguranță",
"addCredential": "Adaugă acreditare",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Acest lucru va adăuga în siguranță cheia publică la fișierul ~/.ssh/Autorized_keys al gazdei țintă, fără a suprascrie cheile existente. Operația este reversibilă.",
"chooseHostToDeploy": "Alege o gazdă pentru a o implementa...",
"deploying": "Desfășurare...",
"copyDeployCommand": "Copiază comanda de lansare",
"copyDeployCommandDescription": "Copiați o comandă shell pentru a adăuga manual cheia publică la fișierul cu chei autorizate. Useful când autentificarea cu parolă este dezactivată.",
"deployCommandCopied": "Lansează comanda copiată în clipboard",
"manualDeployInfo": "Lipiți această comandă în terminalul gazdei țintă (de exemplu, consola Proxmox, IPMI, sau acces fizic) pentru a adăuga cheia SSH.",
"name": "Nume",
"noHostsAvailable": "Nu sunt gazde disponibile",
"noHostsMatchSearch": "Nicio gazdă nu se potrivește cu căutarea ta",
@@ -298,7 +303,9 @@
"createFolder": "Creare folder",
"editFolder": "Editare dosar",
"editFolderDescription": "Personalizați folderul dvs. snippet",
"createFolderDescription": "Organizați fragmentele în dosare"
"createFolderDescription": "Organizați fragmentele în dosare",
"confirmExecution": "Execută \"{{name}}\"?",
"confirmExecutionDesc": "Ești sigur că vrei să execuți acest snippet?"
},
"commandHistory": {
"title": "Istoric",
@@ -402,6 +409,7 @@
"required": "Necesar",
"optional": "Opţional",
"connect": "Conectează-te",
"copied": "Copiat",
"connecting": "Conectare...",
"creating": "Creare...",
"clear": "Curăță",
@@ -494,6 +502,7 @@
"checking": "Verificare...",
"checkingDatabase": "Verificare conexiune la baza de date...",
"checkingAuthentication": "Verificare autentificare...",
"backendReconnected": "Conexiune server restaurată",
"actions": "Acțiuni",
"remove": "Elimină",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Manager Gazdă",
"hosts": "Gazde",
"sshHosts": "Gazde SSH",
"noHosts": "Fără gazde SSH",
"noHostsMessage": "Nu ai adăugat încă nicio gazdă SSH. Apasă pe \"Adaugă Gazdă\" pentru a începe.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Nu s-a putut importa fișierul JSON",
"connectionDetails": "Detalii conexiune",
"organization": "Organizație",
"ipAddress": "Adresă IP",
"ipAddress": "Adresă IP sau nume gazdă",
"port": "Portul",
"name": "Nume",
"username": "Nume",
@@ -915,6 +925,8 @@
"key": "Cheie",
"credential": "Acreditări",
"none": "Niciunul",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Vă promite pentru OPKSSH web auth la fiecare conectare. Token-ul de autentificare va dura 24 de ore până va trebui reînnoit.",
"selectCredential": "Selectare acreditare",
"selectCredentialPlaceholder": "Alege o acreditare...",
"credentialRequired": "Acreditarea este necesară atunci când se utilizează autentificarea acreditărilor",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Autentificare tastatură-interactivă",
"noneAuthDescription": "Această metodă de autentificare va utiliza autentificarea interactivă a tastaturii la conectarea la serverul SSH.",
"noneAuthDetails": "Autentificarea interactivă tastatură permite serverului să vă solicite acreditări în timpul conectării. Acest lucru este util pentru serverele care necesită autentificare multi-factor sau dacă nu doriţi să salvaţi acreditările la nivel local.",
"opksshAuthTitle": "Autentificare OPKSSH",
"forceKeyboardInteractive": "Forțează tastatura- interactivă",
"forceKeyboardInteractiveDesc": "Forțează utilizarea autentificării interactive a tastaturii. Acest lucru este uneori necesar pentru serverele care utilizează autentificarea în doi pași (TOTP/2FA).",
"overrideCredentialUsername": "Suprascrie utilizatorul de acreditare",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Deschide în browser",
"warpgateContinue": "Am finalizat autentificarea",
"warpgateTimeout": "Autentificare la curbură expirată. Te rugăm să te reconectezi.",
"opksshAuthRequired": "Autentificare OPKSSH necesară",
"opksshAuthDescription": "Finalizați autentificarea în browser-ul dvs. pentru a continua. Această sesiune va rămâne valabilă 24 de ore.",
"opksshAuthUrl": "URL autentificare",
"opksshOpenBrowser": "Deschide browser-ul pentru autentificare",
"opksshWaitingForAuth": "Se așteaptă autentificarea în browser...",
"opksshAuthenticating": "Se procesează autentificarea...",
"opksshTimeout": "Autentificarea a expirat. Încercați din nou.",
"opksshAuthFailed": "Autentificare eșuată. Te rugăm să verifici acreditările și să încerci din nou.",
"opksshConfigMissing": "Configurația OPKSSH nu a fost găsită. Vă rugăm să creați ~/.opk/config.yml cu setările furnizorului OIDC. Vedeți documentația: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Introduceți parola?",
"sudoPasswordPopupHint": "Apăsați Enter pentru inserare, Esc pentru a respinge",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Se încearcă autentificarea {{method}}...",
"totpTimeout": "Verificare TOTP expirată. Te rugăm să te reconectezi.",
"passwordTimeout": "Verificarea parolei a expirat. Vă rugăm să vă reconectați.",
"connectionRejected": "Conexiune respinsă de server. Verificați autentificarea și configurarea rețelei."
"connectionRejected": "Conexiune respinsă de server. Verificați autentificarea și configurarea rețelei.",
"hostKeyRejected": "Verificarea cheii gazdă SSH a fost respinsă. Conexiune anulată."
},
"fileManager": {
"title": "Manager fişiere",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Resetare parola reusita",
"passwordResetSuccessDesc": "Parola ta a fost resetată cu succes. Acum te poți autentifica cu noua parolă."
},
"hostKey": {
"verifyNewHost": "Verificare cheie gazdă SSH",
"keyChangedWarning": "Cheie gazdă SSH schimbată",
"firstConnectionTitle": "Conectare pentru prima dată la această gazdă",
"firstConnectionDescription": "Autenticitatea acestui gazdă nu poate fi stabilită. Verificați ca amprenta să corespundă cu ceea ce așteptați.",
"keyChangedDescription": "Cheia gazdă a acestui server s-a schimbat de la ultima conexiune. Aceasta ar putea indica o problemă de securitate.",
"previousKey": "Cheie anterioară",
"newFingerprint": "Amprentă nouă",
"fingerprint": "Amprentă",
"verifyInstructions": "Dacă aveți încredere în această gazdă, faceți clic pe Accept pentru a continua și a salva amprenta pentru conexiunile viitoare.",
"securityWarning": "Avertizare de securitate",
"acceptAndContinue": "Acceptați și continuați",
"acceptNewKey": "Acceptați o nouă cheie și continuați"
},
"errors": {
"notFound": "Pagina nu a fost găsită",
"unauthorized": "Acces neautorizat",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Găzduire și bară laterală",
"snippetsSettings": "Snippet-uri",
"confirmSnippetExecution": "Confirmare execuție snippet",
"confirmSnippetExecutionDesc": "Arată dialog de confirmare înainte de a executa fragmente",
"updateSettings": "Actualizări",
"disableUpdateCheck": "Dezactivează verificarea actualizării",
"disableUpdateCheckDesc": "Opriți verificarea de noi versiuni la pornire și tablou de bord. Reduce cererile de rețea.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 sau exemplu.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Полномочия",
"credentialsViewer": "Просмотр учетных данных",
"manageYourSSHCredentials": "Безопасное управление учётными данными SSH",
"addCredential": "Добавить учетные данные",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Это безопасно добавит публичный ключ к файлу ~/.ssh/authorized_keys целевого хоста без перезаписи существующих ключей. Операция может быть отменена.",
"chooseHostToDeploy": "Выберите хост для развертывания...",
"deploying": "Развертывание...",
"copyDeployCommand": "Копировать команду развертывания",
"copyDeployCommandDescription": "Скопируйте команду оболочки, чтобы вручную добавить открытый ключ в файл authorized_keys узла. Полезно, если отключена аутентификация пароля.",
"deployCommandCopied": "Команда развертывания скопирована в буфер обмена",
"manualDeployInfo": "Вставьте эту команду в терминал целевого хоста (например, консоль Proxmox, IPMI или физический доступ), чтобы добавить ключ SSH.",
"name": "Наименование",
"noHostsAvailable": "Нет хостов",
"noHostsMatchSearch": "Нет хостов, соответствующих вашему запросу",
@@ -298,7 +303,9 @@
"createFolder": "Создать папку",
"editFolder": "Изменить папку",
"editFolderDescription": "Настройка папки сниппетов",
"createFolderDescription": "Организовать ваши сниппеты в папках"
"createFolderDescription": "Организовать ваши сниппеты в папках",
"confirmExecution": "Выполнить \"{{name}}\"?",
"confirmExecutionDesc": "Вы уверены, что хотите выполнить этот сниппет?"
},
"commandHistory": {
"title": "История",
@@ -402,6 +409,7 @@
"required": "Требуется",
"optional": "Опционально",
"connect": "Подключиться",
"copied": "Скопировано",
"connecting": "Подключение...",
"creating": "Создание...",
"clear": "Очистить",
@@ -494,6 +502,7 @@
"checking": "Проверка...",
"checkingDatabase": "Проверка подключения к базе данных...",
"checkingAuthentication": "Проверка подлинности...",
"backendReconnected": "Соединение с сервером восстановлено",
"actions": "Действия",
"remove": "Удалить",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Менеджер хостов",
"hosts": "Узлы",
"sshHosts": "SSH хосты",
"noHosts": "Нет SSH хостов",
"noHostsMessage": "Вы еще не добавили ни одного хоста SSH. Нажмите \"Добавить хост\", чтобы начать.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Не удалось импортировать JSON файл",
"connectionDetails": "Детали подключения",
"organization": "Организация",
"ipAddress": "IP-адрес",
"ipAddress": "IP-адрес или имя хоста",
"port": "Порт",
"name": "Наименование",
"username": "Имя пользователя",
@@ -915,6 +925,8 @@
"key": "Спецификация",
"credential": "Полномочия",
"none": "Нет",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Запрашивает OPKSSH авторизацию при каждом подключении. Ваш токен авторизации будет длиться 24 часа, пока он не будет продлен.",
"selectCredential": "Выберите учетные данные",
"selectCredentialPlaceholder": "Выберите учетные данные...",
"credentialRequired": "Учетные данные требуются при использовании аутентификации",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Клавиатура - Интерактивная аутентификация",
"noneAuthDescription": "Этот метод аутентификации будет использовать интерактивную аутентификацию с клавиатурой при подключении к SSH серверу.",
"noneAuthDetails": "Интерактивная аутентификация на клавиатуре позволяет серверу запрашивать учетные данные во время подключения. Это полезно для серверов, которые требуют многофакторной аутентификации или если вы не хотите сохранять учетные данные локально.",
"opksshAuthTitle": "OPKSSH аутентификация",
"forceKeyboardInteractive": "Интерактивная клавиатура",
"forceKeyboardInteractiveDesc": "Принудительно использовать интерактивную аутентификацию с клавиатуры. Иногда требуется для серверов, использующих двухфакторную аутентификацию (TOTP/2FA).",
"overrideCredentialUsername": "Переопределить имя пользователя",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Открыть в браузере",
"warpgateContinue": "Я завершаю аутентификацию",
"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": "Вставить пароль?",
"sudoPasswordPopupHint": "Нажмите Enter для вставки, Esc для отключения",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Автоматически пытаться аутентификации {{method}}...",
"totpTimeout": "Тайм-аут проверки TOTP. Пожалуйста, подключитесь снова.",
"passwordTimeout": "Время ожидания подтверждения пароля истекло. Пожалуйста, подключитесь снова.",
"connectionRejected": "Соединение отклонено сервером. Проверьте вашу аутентификацию и конфигурацию сети."
"connectionRejected": "Соединение отклонено сервером. Проверьте вашу аутентификацию и конфигурацию сети.",
"hostKeyRejected": "Проверка SSH ключа хоста отклонена. Соединение отменено."
},
"fileManager": {
"title": "Файловый менеджер",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Пароль успешно сброшен",
"passwordResetSuccessDesc": "Ваш пароль был успешно сброшен. Теперь вы можете войти с новым паролем."
},
"hostKey": {
"verifyNewHost": "Проверка SSH ключа хоста",
"keyChangedWarning": "SSH ключ хоста изменен",
"firstConnectionTitle": "Впервые подключение к этому узлу",
"firstConnectionDescription": "Логичность этого узла не может быть установлена. Проверьте отпечаток пальца так, что вы ожидаете.",
"keyChangedDescription": "Ключ хоста для этого сервера изменился с момента вашего последнего подключения. Это может указывать на проблему безопасности.",
"previousKey": "Предыдущий ключ",
"newFingerprint": "Новый отпечаток пальца",
"fingerprint": "Отпечаток пальца",
"verifyInstructions": "Если вы доверяете этому узлу, нажмите «Принять», чтобы продолжить, и сохраните отпечаток пальца для будущих подключений.",
"securityWarning": "Предупреждение о безопасности",
"acceptAndContinue": "Принять и продолжить",
"acceptNewKey": "Принять новый ключ и продолжить"
},
"errors": {
"notFound": "Страница не найдена",
"unauthorized": "Неавторизованный доступ",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Терминал",
"hostSidebarSettings": "Узел и боковая панель",
"snippetsSettings": "Сниппеты",
"confirmSnippetExecution": "Подтвердите исполнение сниппета",
"confirmSnippetExecutionDesc": "Показывать окно подтверждения перед выполнением сниппетов",
"updateSettings": "Обновления",
"disableUpdateCheck": "Отключить проверку обновлений",
"disableUpdateCheckDesc": "Не проверять наличие новых версий при запуске и панели управления. Снижает сетевые запросы.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 или example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Акредитиви",
"credentialsViewer": "Прегледач акредитива",
"manageYourSSHCredentials": "Безбедно управљајте својим SSH акредитивима",
"addCredential": "Додај акредитив",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Ово ће безбедно додати јавни кључ у датотеку ~/.ssh/authorized_keys циљног хоста без преписивања постојећих кључева. Операција је реверзибилна.",
"chooseHostToDeploy": "Изаберите хост за распоређивање...",
"deploying": "Распоређивање...",
"copyDeployCommand": "Копирај команду за распоређивање",
"copyDeployCommandDescription": "Копирајте команду шелла да бисте ручно додали јавни кључ у датотеку authorized_keys хоста. Корисно када је аутентификација лозинком онемогућена.",
"deployCommandCopied": "Команда за распоређивање копирана у међуспремник",
"manualDeployInfo": "Налепите ову команду у терминал циљног хоста (нпр. Proxmox конзолу, IPMI или физички приступ) да бисте додали SSH кључ.",
"name": "Име",
"noHostsAvailable": "Нема доступних хостова",
"noHostsMatchSearch": "Ниједан хост не одговара вашој претрази",
@@ -298,7 +303,9 @@
"createFolder": "Направи фасциклу",
"editFolder": "Уреди фасциклу",
"editFolderDescription": "Прилагодите своју фасциклу са фрагментима кода",
"createFolderDescription": "Организујте своје фрагменте у фасцикле"
"createFolderDescription": "Организујте своје фрагменте у фасцикле",
"confirmExecution": "Извршити „{{name}}“?",
"confirmExecutionDesc": "Да ли сте сигурни да желите да извршите овај исечак?"
},
"commandHistory": {
"title": "Историја",
@@ -402,6 +409,7 @@
"required": "Обавезно",
"optional": "Опционо",
"connect": "Повежи се",
"copied": "Копирано",
"connecting": "Повезивање...",
"creating": "Креирање...",
"clear": "Јасно",
@@ -494,6 +502,7 @@
"checking": "Провера...",
"checkingDatabase": "Провера везе са базом података...",
"checkingAuthentication": "Провера аутентификације...",
"backendReconnected": "Веза са сервером је враћена",
"actions": "Акције",
"remove": "Уклони",
"revoke": "Поништи",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Менаџер домаћина",
"hosts": "Домаћини",
"sshHosts": "SSH хостови",
"noHosts": "Нема SSH хостова",
"noHostsMessage": "Још нисте додали ниједан SSH хост. Кликните на „Додај хост“ да бисте започели.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Увоз JSON датотеке није успео",
"connectionDetails": "Детаљи везе",
"organization": "Организација",
"ipAddress": "ИП адреса",
"ipAddress": "IP адреса или име хоста",
"port": "Лука",
"name": "Име",
"username": "Корисничко име",
@@ -915,6 +925,8 @@
"key": "Кључ",
"credential": "Акредитив",
"none": "Ниједан",
"opkssh": "ОПКШ",
"opksshAuthDescription": "Пита вас за OPKSSH веб-аутентификацију при сваком повезивању. Ваш токен за аутентификацију ће трајати 24 сата док се не обнови.",
"selectCredential": "Изаберите акредитив",
"selectCredentialPlaceholder": "Изаберите акредитив...",
"credentialRequired": "Акредитив је потребан када се користи аутентификација акредитива",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Интерактивна аутентификација помоћу тастатуре",
"noneAuthDescription": "Ова метода аутентификације ће користити интерактивну аутентификацију помоћу тастатуре приликом повезивања са SSH сервером.",
"noneAuthDetails": "Интерактивна аутентификација помоћу тастатуре омогућава серверу да вас пита за акредитиве током повезивања. Ово је корисно за сервере који захтевају вишефакторску аутентификацију или ако не желите да локално чувате акредитиве.",
"opksshAuthTitle": "OPKSSH аутентификација",
"forceKeyboardInteractive": "Присилно интерактивно коришћење тастатуре",
"forceKeyboardInteractiveDesc": "Приморава употребу интерактивне аутентификације помоћу тастатуре. Ово је понекад потребно за сервере који користе двофакторску аутентификацију (TOTP/2FA).",
"overrideCredentialUsername": "Замени корисничко име за акредитиве",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Отвори у прегледачу",
"warpgateContinue": "Завршио/ла сам аутентификацију",
"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": "Унети лозинку?",
"sudoPasswordPopupHint": "Притисните Enter да бисте унели, Esc да бисте одбацили",
"sudoPasswordPopupConfirm": "Уметни",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Аутоматски покушај {{method}} аутентификације...",
"totpTimeout": "Временско ограничење за верификацију TOTP-а је истекло. Молимо вас да се поново повежете.",
"passwordTimeout": "Временско ограничење за верификацију лозинке је истекло. Молимо вас да се поново повежете.",
"connectionRejected": "Сервер је одбио везу. Проверите аутентификацију и конфигурацију мреже."
"connectionRejected": "Сервер је одбио везу. Проверите аутентификацију и конфигурацију мреже.",
"hostKeyRejected": "Верификација SSH кључа хоста је одбијена. Веза је отказана."
},
"fileManager": {
"title": "Менаџер датотека",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Ресетовање лозинке је успешно",
"passwordResetSuccessDesc": "Ваша лозинка је успешно ресетована. Сада се можете пријавити са новом лозинком."
},
"hostKey": {
"verifyNewHost": "Проверите SSH кључ хоста",
"keyChangedWarning": "SSH кључ хоста је промењен",
"firstConnectionTitle": "Прво повезивање са овим хостом",
"firstConnectionDescription": "Аутентичност овог хоста не може бити утврђена. Проверите да ли отисак прста одговара очекивањима.",
"keyChangedDescription": "Кључ хоста за овај сервер се променио од ваше последње везе. Ово би могло да указује на безбедносни проблем.",
"previousKey": "Претходни кључ",
"newFingerprint": "Нови отисак прста",
"fingerprint": "Отисак прста",
"verifyInstructions": "Ако верујете овом хосту, кликните на „Прихвати“ да бисте наставили и сачували овај отисак прста за будуће везе.",
"securityWarning": "Безбедносно упозорење",
"acceptAndContinue": "Прихвати и настави",
"acceptNewKey": "Прихвати нови кључ и настави"
},
"errors": {
"notFound": "Страница није пронађена",
"unauthorized": "Неовлашћени приступ",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Терминал",
"hostSidebarSettings": "Домаћин и бочна трака",
"snippetsSettings": "Исечци",
"confirmSnippetExecution": "Потврди извршавање исечка",
"confirmSnippetExecutionDesc": "Прикажи дијалог за потврду пре извршавања исечака",
"updateSettings": "Ажурирања",
"disableUpdateCheck": "Онемогући проверу ажурирања",
"disableUpdateCheckDesc": "Зауставите проверу нових верзија при покретању и на контролној табли. Смањује мрежне захтеве.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 или example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Användaruppgifter",
"credentialsViewer": "Visning av användaruppgifter",
"manageYourSSHCredentials": "Hantera dina SSH-uppgifter säkert",
"addCredential": "Lägg till autentiseringsuppgifter",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Detta kommer säkert att lägga till den publika nyckeln till målvärdens ~/.ssh/authorized_keys fil utan att skriva över befintliga nycklar. Åtgärden är vändbar.",
"chooseHostToDeploy": "Välj en värd att distribuera till...",
"deploying": "Utrustar...",
"copyDeployCommand": "Kopiera distribuera kommando",
"copyDeployCommandDescription": "Kopiera ett skalkommando för att manuellt lägga till den offentliga nyckeln till en värds authorized_keys-fil. Användbar när lösenordsautentisering är inaktiverad.",
"deployCommandCopied": "Distributionskommandot kopierat till urklipp",
"manualDeployInfo": "Klistra in detta kommando i målvärdens terminal (t.ex. Proxmox-konsol, IPMI eller fysisk åtkomst) för att lägga till SSH-tangenten.",
"name": "Namn",
"noHostsAvailable": "Inga tillgängliga värdar",
"noHostsMatchSearch": "Inga värdar matchar din sökning",
@@ -298,7 +303,9 @@
"createFolder": "Skapa mapp",
"editFolder": "Redigera mapp",
"editFolderDescription": "Anpassa din textmodul-mapp",
"createFolderDescription": "Organisera dina textmoduler i mappar"
"createFolderDescription": "Organisera dina textmoduler i mappar",
"confirmExecution": "Kör \"{{name}}\"?",
"confirmExecutionDesc": "Är du säker på att du vill köra denna snippet?"
},
"commandHistory": {
"title": "Historik",
@@ -402,6 +409,7 @@
"required": "Krävs",
"optional": "Valfri",
"connect": "Anslut",
"copied": "Kopierad",
"connecting": "Ansluter...",
"creating": "Skapar...",
"clear": "Rensa",
@@ -494,6 +502,7 @@
"checking": "Kontrollerar...",
"checkingDatabase": "Kontrollerar databasanslutning...",
"checkingAuthentication": "Kontrollerar autentisering...",
"backendReconnected": "Serveranslutning återställd",
"actions": "Åtgärder",
"remove": "Radera",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Värdhanterare",
"hosts": "Värdar",
"sshHosts": "SSH värdar",
"noHosts": "Inga SSH-värdar",
"noHostsMessage": "Du har inte lagt till några SSH-värdar än. Klicka på \"Lägg till värd\" för att komma igång.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Det gick inte att importera JSON-fil",
"connectionDetails": "Kontaktuppgifter",
"organization": "Organisation",
"ipAddress": "IP-adress",
"ipAddress": "IP-adress eller värdnamn",
"port": "Port",
"name": "Namn",
"username": "Användarnamn",
@@ -915,6 +925,8 @@
"key": "Nyckel",
"credential": "Uppgifter",
"none": "Ingen",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Fråga dig för OPKSSH web-auth vid varje anslutning. Din auth token kommer att pågå i 24 timmar tills den måste förnyas.",
"selectCredential": "Välj referens",
"selectCredentialPlaceholder": "Välj en inloggning...",
"credentialRequired": "Uppgifter krävs när autentisering används",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Tangentbord-Interaktiv autentisering",
"noneAuthDescription": "Denna autentiseringsmetod kommer att använda tangentbord-interaktiv autentisering när du ansluter till SSH-servern.",
"noneAuthDetails": "Tangentbord-interaktiv autentisering gör det möjligt för servern att fråga dig om autentiseringsuppgifter under anslutningen. Detta är användbart för servrar som kräver multi-faktor autentisering eller om du inte vill spara uppgifter lokalt.",
"opksshAuthTitle": "OPKSSH autentisering",
"forceKeyboardInteractive": "Tvinga interaktiv tangentbord",
"forceKeyboardInteractiveDesc": "Tvingar användning av tangentbords-interaktiv autentisering. Detta krävs ibland för servrar som använder tvåfaktorsautentisering (TOTP/2FA).",
"overrideCredentialUsername": "Åsidosätt Användarnamn",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Öppna i webbläsare",
"warpgateContinue": "Jag har slutfört autentisering",
"warpgateTimeout": "Warpgate-autentiserings-timeout. Vänligen återanslut.",
"opksshAuthRequired": "OPKSSH-autentisering krävs",
"opksshAuthDescription": "Fullständig autentisering i din webbläsare för att fortsätta. Denna session kommer att förbli giltig i 24 timmar.",
"opksshAuthUrl": "URL för autentisering",
"opksshOpenBrowser": "Öppna webbläsaren för att autentisera",
"opksshWaitingForAuth": "Väntar på autentisering i webbläsaren...",
"opksshAuthenticating": "Bearbetar autentisering...",
"opksshTimeout": "Autentisering gick ut. Försök igen.",
"opksshAuthFailed": "Autentisering misslyckades. Kontrollera dina uppgifter och försök igen.",
"opksshConfigMissing": "OPKSSH-konfigurationen hittades inte. Vänligen skapa ~/.opk/config.yml med dina OIDC-leverantörsinställningar. Se dokumentation: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Infoga lösenord?",
"sudoPasswordPopupHint": "Tryck Enter för att infoga Esc för att avfärda",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Försöker automatiskt {{method}} autentisering...",
"totpTimeout": "TOTP-verifiering timeout. Vänligen återanslut.",
"passwordTimeout": "Lösenordsverifiering är avklarad. Vänligen återanslut.",
"connectionRejected": "Anslutning avvisad av servern. Kontrollera din autentisering och nätverkskonfiguration."
"connectionRejected": "Anslutning avvisad av servern. Kontrollera din autentisering och nätverkskonfiguration.",
"hostKeyRejected": "Verifiering av SSH-värdnyckel avvisad. Anslutningen avbröts."
},
"fileManager": {
"title": "Filhanterare",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Lösenordsåterställning lyckades",
"passwordResetSuccessDesc": "Ditt lösenord har återställts. Du kan nu logga in med ditt nya lösenord."
},
"hostKey": {
"verifyNewHost": "Verifiera SSH-värdnyckel",
"keyChangedWarning": "SSH Värdnyckel ändrad",
"firstConnectionTitle": "Första gången du ansluter till denna värd",
"firstConnectionDescription": "Autenticiteten hos denna värd kan inte fastställas. Kontrollera att fingeravtrycket matchar det du förväntar dig.",
"keyChangedDescription": "Värdnyckeln för denna server har ändrats sedan din senaste anslutning. Detta kan indikera ett säkerhetsproblem.",
"previousKey": "Föregående nyckel",
"newFingerprint": "Nytt fingeravtryck",
"fingerprint": "Fingeravtryck",
"verifyInstructions": "Om du litar på detta värd, klicka på Acceptera för att fortsätta och spara detta fingeravtryck för framtida anslutningar.",
"securityWarning": "Varning för säkerhet",
"acceptAndContinue": "Godkänn och fortsätt",
"acceptNewKey": "Acceptera ny nyckel och fortsätt"
},
"errors": {
"notFound": "Sidan hittades inte",
"unauthorized": "Obehörig åtkomst",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Terminal",
"hostSidebarSettings": "Värd och sidofält",
"snippetsSettings": "Textmoduler",
"confirmSnippetExecution": "Bekräfta utförande av textmodulen",
"confirmSnippetExecutionDesc": "Visa bekräftelsedialog innan du utför textmoduler",
"updateSettings": "Uppdateringar",
"disableUpdateCheck": "Inaktivera uppdateringskontroll",
"disableUpdateCheckDesc": "Sluta leta efter nya versioner vid uppstart och instrumentbräda. Minskar nätverksförfrågningar.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 eller exempel.se",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "คุณสมบัติ",
"credentialsViewer": "โปรแกรมดูข้อมูลประจำตัว",
"manageYourSSHCredentials": "จัดการข้อมูลประจำตัว SSH ของคุณอย่างปลอดภัย",
"addCredential": "เพิ่มข้อมูลรับรอง",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "การดำเนินการนี้จะเพิ่มคีย์สาธารณะลงในไฟล์ ~/.ssh/authorized_keys ของโฮสต์เป้าหมายอย่างปลอดภัยโดยไม่เขียนทับคีย์ที่มีอยู่ การดำเนินการนี้สามารถย้อนกลับได้",
"chooseHostToDeploy": "เลือกโฮสต์ที่จะใช้ในการติดตั้ง...",
"deploying": "กำลังติดตั้ง...",
"copyDeployCommand": "คัดลอกคำสั่งปรับใช้",
"copyDeployCommandDescription": "คัดลอกคำสั่งเชลล์เพื่อเพิ่มคีย์สาธารณะลงในไฟล์ authorized_keys ของโฮสต์ด้วยตนเอง มีประโยชน์เมื่อปิดใช้งานการตรวจสอบสิทธิ์ด้วยรหัสผ่าน",
"deployCommandCopied": "คัดลอกคำสั่งปรับใช้ไปยังคลิปบอร์ดแล้ว",
"manualDeployInfo": "วางคำสั่งนี้ลงในเทอร์มินัลของโฮสต์เป้าหมาย (เช่น คอนโซล Proxmox, IPMI หรือการเข้าถึงทางกายภาพ) เพื่อเพิ่มคีย์ SSH",
"name": "ชื่อ",
"noHostsAvailable": "ไม่มีโฮสต์ให้บริการ",
"noHostsMatchSearch": "ไม่พบโฮสต์ใดตรงกับผลการค้นหาของคุณ",
@@ -298,7 +303,9 @@
"createFolder": "สร้างโฟลเดอร์",
"editFolder": "แก้ไขโฟลเดอร์",
"editFolderDescription": "ปรับแต่งโฟลเดอร์โค้ดสั้นของคุณ",
"createFolderDescription": "จัดระเบียบข้อความย่อของคุณลงในโฟลเดอร์"
"createFolderDescription": "จัดระเบียบข้อความย่อของคุณลงในโฟลเดอร์",
"confirmExecution": "ดำเนินการ \"{{name}}\"?",
"confirmExecutionDesc": "คุณแน่ใจหรือไม่ว่าต้องการเรียกใช้โค้ดส่วนนี้?"
},
"commandHistory": {
"title": "ประวัติศาสตร์",
@@ -402,6 +409,7 @@
"required": "ที่จำเป็น",
"optional": "ไม่จำเป็น",
"connect": "เชื่อมต่อ",
"copied": "คัดลอกแล้ว",
"connecting": "กำลังเชื่อมต่อ...",
"creating": "กำลังสร้าง...",
"clear": "ชัดเจน",
@@ -494,6 +502,7 @@
"checking": "กำลังตรวจสอบ...",
"checkingDatabase": "กำลังตรวจสอบการเชื่อมต่อฐานข้อมูล...",
"checkingAuthentication": "กำลังตรวจสอบการยืนยันตัวตน...",
"backendReconnected": "การเชื่อมต่อเซิร์ฟเวอร์กลับมาใช้งานได้แล้ว",
"actions": "การกระทำ",
"remove": "ลบ",
"revoke": "ถอน",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "ผู้จัดการโฮสต์",
"hosts": "โฮสต์",
"sshHosts": "โฮสต์ SSH",
"noHosts": "ไม่มีโฮสต์ SSH",
"noHostsMessage": "คุณยังไม่ได้เพิ่มโฮสต์ SSH ใดๆ คลิก \"เพิ่มโฮสต์\" เพื่อเริ่มต้น",
@@ -836,7 +846,7 @@
"failedToImportJson": "ไม่สามารถนำเข้าไฟล์ JSON ได้",
"connectionDetails": "รายละเอียดการเชื่อมต่อ",
"organization": "องค์กร",
"ipAddress": "ที่อยู่ IP",
"ipAddress": "ที่อยู่ IP หรือชื่อโฮสต์",
"port": "ท่าเรือ",
"name": "ชื่อ",
"username": "ชื่อผู้ใช้",
@@ -915,6 +925,8 @@
"key": "สำคัญ",
"credential": "ใบรับรอง",
"none": "ไม่มี",
"opkssh": "โอพีเคเอสเอช",
"opksshAuthDescription": "ระบบจะขอให้คุณป้อนรหัสยืนยันตัวตน OPKSSH ทุกครั้งที่เชื่อมต่อ รหัสยืนยันตัวตนของคุณจะมีอายุ 24 ชั่วโมงก่อนที่จะต้องต่ออายุใหม่",
"selectCredential": "เลือกข้อมูลประจำตัว",
"selectCredentialPlaceholder": "เลือกข้อมูลประจำตัว...",
"credentialRequired": "จำเป็นต้องระบุข้อมูลประจำตัวเมื่อใช้การตรวจสอบสิทธิ์ด้วยข้อมูลประจำตัว",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "การตรวจสอบสิทธิ์แบบโต้ตอบด้วยแป้นพิมพ์",
"noneAuthDescription": "วิธีการยืนยันตัวตนนี้จะใช้การยืนยันตัวตนผ่านแป้นพิมพ์เมื่อเชื่อมต่อกับเซิร์ฟเวอร์ SSH",
"noneAuthDetails": "การตรวจสอบสิทธิ์แบบโต้ตอบด้วยแป้นพิมพ์ช่วยให้เซิร์ฟเวอร์แจ้งให้คุณป้อนข้อมูลประจำตัวระหว่างการเชื่อมต่อ ซึ่งมีประโยชน์สำหรับเซิร์ฟเวอร์ที่ต้องการการตรวจสอบสิทธิ์แบบหลายปัจจัย หรือหากคุณไม่ต้องการบันทึกข้อมูลประจำตัวไว้ในเครื่อง",
"opksshAuthTitle": "การตรวจสอบสิทธิ์ OPKSSH",
"forceKeyboardInteractive": "แป้นพิมพ์แบบอินเทอร์แอคทีฟของ Force",
"forceKeyboardInteractiveDesc": "บังคับให้ใช้การตรวจสอบสิทธิ์แบบโต้ตอบผ่านแป้นพิมพ์ ซึ่งบางครั้งจำเป็นสำหรับเซิร์ฟเวอร์ที่ใช้การตรวจสอบสิทธิ์แบบสองปัจจัย (TOTP/2FA)",
"overrideCredentialUsername": "แทนที่ชื่อผู้ใช้ข้อมูลประจำตัว",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "เปิดในเบราว์เซอร์",
"warpgateContinue": "ฉันทำการยืนยันตัวตนเสร็จเรียบร้อยแล้ว",
"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": "ใส่รหัสผ่าน?",
"sudoPasswordPopupHint": "กด Enter เพื่อแทรก กด Esc เพื่อปิด",
"sudoPasswordPopupConfirm": "แทรก",
@@ -1337,7 +1359,8 @@
"automaticFallback": "กำลังลองยืนยันตัวตน {{method}} โดยอัตโนมัติ...",
"totpTimeout": "การยืนยัน TOTP หมดเวลา โปรดเชื่อมต่อใหม่อีกครั้ง",
"passwordTimeout": "การตรวจสอบรหัสผ่านหมดเวลา โปรดเชื่อมต่อใหม่อีกครั้ง",
"connectionRejected": "เซิร์ฟเวอร์ปฏิเสธการเชื่อมต่อ โปรดตรวจสอบการยืนยันตัวตนและการกำหนดค่าเครือข่ายของคุณ"
"connectionRejected": "เซิร์ฟเวอร์ปฏิเสธการเชื่อมต่อ โปรดตรวจสอบการยืนยันตัวตนและการกำหนดค่าเครือข่ายของคุณ",
"hostKeyRejected": "การตรวจสอบคีย์โฮสต์ SSH ถูกปฏิเสธ การเชื่อมต่อถูกยกเลิก"
},
"fileManager": {
"title": "ตัวจัดการไฟล์",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "การรีเซ็ตรหัสผ่านสำเร็จ",
"passwordResetSuccessDesc": "รหัสผ่านของคุณถูกรีเซ็ตสำเร็จแล้ว คุณสามารถเข้าสู่ระบบด้วยรหัสผ่านใหม่ของคุณได้แล้ว"
},
"hostKey": {
"verifyNewHost": "ตรวจสอบคีย์โฮสต์ SSH",
"keyChangedWarning": "คีย์โฮสต์ SSH เปลี่ยนแล้ว",
"firstConnectionTitle": "นี่เป็นครั้งแรกที่เชื่อมต่อกับโฮสต์นี้",
"firstConnectionDescription": "ไม่สามารถตรวจสอบความถูกต้องของโฮสต์นี้ได้ โปรดตรวจสอบว่าลายนิ้วมือตรงกับที่คุณคาดหวังหรือไม่",
"keyChangedDescription": "รหัสโฮสต์ของเซิร์ฟเวอร์นี้มีการเปลี่ยนแปลงนับตั้งแต่การเชื่อมต่อครั้งล่าสุดของคุณ ซึ่งอาจบ่งชี้ถึงปัญหาด้านความปลอดภัย",
"previousKey": "คีย์ก่อนหน้า",
"newFingerprint": "ลายนิ้วมือใหม่",
"fingerprint": "ลายนิ้วมือ",
"verifyInstructions": "หากคุณเชื่อถือโฮสต์นี้ โปรดคลิก ยอมรับ เพื่อดำเนินการต่อและบันทึกข้อมูลนี้สำหรับการเชื่อมต่อในอนาคต",
"securityWarning": "คำเตือนด้านความปลอดภัย",
"acceptAndContinue": "ยอมรับและดำเนินการต่อ",
"acceptNewKey": "ยอมรับรหัสใหม่และดำเนินการต่อ"
},
"errors": {
"notFound": "ไม่พบหน้าเว็บ",
"unauthorized": "การเข้าถึงโดยไม่ได้รับอนุญาต",
@@ -2026,6 +2063,8 @@
"terminalSettings": "เทอร์มินัล",
"hostSidebarSettings": "โฮสต์และแถบด้านข้าง",
"snippetsSettings": "เศษเสี้ยว",
"confirmSnippetExecution": "ยืนยันการเรียกใช้โค้ดสั้น",
"confirmSnippetExecutionDesc": "แสดงกล่องโต้ตอบยืนยันก่อนเรียกใช้โค้ดสั้นๆ",
"updateSettings": "การอัปเดต",
"disableUpdateCheck": "ปิดใช้งานการตรวจสอบการอัปเดต",
"disableUpdateCheckDesc": "หยุดการตรวจสอบเวอร์ชันใหม่เมื่อเริ่มต้นระบบและบนแดชบอร์ด ช่วยลดจำนวนการร้องขอเครือข่าย",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 หรือ example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Kimlik Bilgileri",
"credentialsViewer": "Kimlik Bilgileri Görüntüleyici",
"manageYourSSHCredentials": "SSH kimlik bilgilerinizi güvenli bir şekilde yönetin.",
"addCredential": "Kimlik Bilgisi Ekle",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Bu işlem, mevcut anahtarların üzerine yazmadan, hedef sunucunun ~/.ssh/authorized_keys dosyasına genel anahtarı güvenli bir şekilde ekleyecektir. İşlem geri alınabilir.",
"chooseHostToDeploy": "Dağıtım yapılacak sunucuyu seçin...",
"deploying": "Dağıtıma başlanıyor...",
"copyDeployCommand": "Dağıtım Komutunu Kopyala",
"copyDeployCommandDescription": "Bir sunucunun authorized_keys dosyasına genel anahtarı manuel olarak eklemek için bir shell komutunu kopyalayın. Parola kimlik doğrulamasının devre dışı bırakıldığı durumlarda kullanışlıdır.",
"deployCommandCopied": "Dağıtım komutu panoya kopyalandı.",
"manualDeployInfo": "SSH anahtarını eklemek için bu komutu hedef sunucunun terminaline (örneğin, Proxmox konsolu, IPMI veya fiziksel erişim) yapıştırın.",
"name": "İsim",
"noHostsAvailable": "Hiç ev sahibi mevcut değil.",
"noHostsMatchSearch": "Arama kriterlerinize uyan ev sahibi bulunamadı.",
@@ -298,7 +303,9 @@
"createFolder": "Klasör Oluştur",
"editFolder": "Klasörü Düzenle",
"editFolderDescription": "Kod parçacığı klasörünüzü özelleştirin.",
"createFolderDescription": "Kod parçacıklarınızı klasörler halinde düzenleyin."
"createFolderDescription": "Kod parçacıklarınızı klasörler halinde düzenleyin.",
"confirmExecution": "\"{{name}}\"'yi yürüt?",
"confirmExecutionDesc": "Bu kod parçasını çalıştırmak istediğinizden emin misiniz?"
},
"commandHistory": {
"title": "Tarih",
@@ -402,6 +409,7 @@
"required": "Gerekli",
"optional": "İsteğe bağlı",
"connect": "Bağlamak",
"copied": "Kopyalandı",
"connecting": "Bağlanıyor...",
"creating": "Oluşturuluyor...",
"clear": "Temizlemek",
@@ -494,6 +502,7 @@
"checking": "Kontrol ediliyor...",
"checkingDatabase": "Veritabanı bağlantısı kontrol ediliyor...",
"checkingAuthentication": "Kimlik doğrulama kontrol ediliyor...",
"backendReconnected": "Sunucu bağlantısı yeniden kuruldu",
"actions": "Eylemler",
"remove": "Kaldırmak",
"revoke": "Geri çekmek",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Sunucu Yöneticisi",
"hosts": "Ev sahipleri",
"sshHosts": "SSH Sunucuları",
"noHosts": "SSH Sunucusu Yok",
"noHostsMessage": "Henüz hiçbir SSH sunucusu eklemediniz. Başlamak için \"Sunucu Ekle\"ye tıklayın.",
@@ -836,7 +846,7 @@
"failedToImportJson": "JSON dosyasını içe aktarma başarısız oldu.",
"connectionDetails": "Bağlantı Ayrıntıları",
"organization": "Organizasyon",
"ipAddress": "IP Adresi",
"ipAddress": "IP Adresi veya Ana Bilgisayar Adı",
"port": "Liman",
"name": "İsim",
"username": "Kullanıcı adı",
@@ -915,6 +925,8 @@
"key": "Anahtar",
"credential": "Kimlik belgesi",
"none": "Hiçbiri",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Her bağlantıda OPKSSH web kimlik doğrulaması için sizden onay ister. Kimlik doğrulama belirteciniz 24 saat boyunca geçerli olacak ve daha sonra yenilenmesi gerekecektir.",
"selectCredential": "Kimlik Bilgisi Seçin",
"selectCredentialPlaceholder": "Bir yeterlilik belgesi seçin...",
"credentialRequired": "Kimlik doğrulama kullanılırken kimlik bilgisi gereklidir.",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Klavye Etkileşimli Kimlik Doğrulama",
"noneAuthDescription": "Bu kimlik doğrulama yöntemi, SSH sunucusuna bağlanırken klavye etkileşimli kimlik doğrulamasını kullanacaktır.",
"noneAuthDetails": "Klavyeyle etkileşimli kimlik doğrulama, sunucunun bağlantı sırasında sizden kimlik bilgilerini istemesine olanak tanır. Bu, çok faktörlü kimlik doğrulama gerektiren sunucular için veya kimlik bilgilerini yerel olarak kaydetmek istemiyorsanız kullanışlıdır.",
"opksshAuthTitle": "OPKSSH Kimlik Doğrulaması",
"forceKeyboardInteractive": "Klavyeyle Etkileşimi Zorla",
"forceKeyboardInteractiveDesc": "Klavyeyle etkileşimli kimlik doğrulama kullanımını zorunlu kılar. Bu, bazen İki Faktörlü Kimlik Doğrulama (TOTP/2FA) kullanan sunucular için gereklidir.",
"overrideCredentialUsername": "Kimlik Bilgisi Kullanıcı Adını Geçersiz Kıl",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Tarayıcıda aç",
"warpgateContinue": "Kimlik doğrulama işlemini tamamladım.",
"warpgateTimeout": "Warpgate kimlik doğrulama zaman aşımı. Lütfen yeniden bağlanın.",
"opksshAuthRequired": "OPKSSH Kimlik Doğrulaması Gerekli",
"opksshAuthDescription": "Devam etmek için tarayıcınızda kimlik doğrulama işlemini tamamlayın. Bu oturum 24 saat boyunca geçerli olacaktır.",
"opksshAuthUrl": "Kimlik Doğrulama URL'si",
"opksshOpenBrowser": "Kimlik doğrulaması için tarayıcıyı açın.",
"opksshWaitingForAuth": "Tarayıcıda kimlik doğrulaması bekleniyor...",
"opksshAuthenticating": "Kimlik doğrulama işlemi gerçekleştiriliyor...",
"opksshTimeout": "Kimlik doğrulama işlemi zaman aşımına uğradı. Lütfen tekrar deneyin.",
"opksshAuthFailed": "Kimlik doğrulama başarısız oldu. Lütfen kimlik bilgilerinizi kontrol edin ve tekrar deneyin.",
"opksshConfigMissing": "OPKSSH yapılandırması bulunamadı. Lütfen OIDC sağlayıcı ayarlarınızla birlikte ~/.opk/config.yml dosyasını oluşturun. Belgelere bakın: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Şifrenizi girin?",
"sudoPasswordPopupHint": "Eklemek için Enter'a, silmek için Esc'ye basın.",
"sudoPasswordPopupConfirm": "Sokmak",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Otomatik olarak {{method}} kimlik doğrulaması deneniyor...",
"totpTimeout": "TOTP doğrulama zaman aşımına uğradı. Lütfen yeniden bağlanın.",
"passwordTimeout": "Parola doğrulama zaman aşımına uğradı. Lütfen yeniden bağlanın.",
"connectionRejected": "Sunucu bağlantıyı reddetti. Lütfen kimlik doğrulama ve ağ yapılandırmanızı kontrol edin."
"connectionRejected": "Sunucu bağlantıyı reddetti. Lütfen kimlik doğrulama ve ağ yapılandırmanızı kontrol edin.",
"hostKeyRejected": "SSH sunucu anahtarı doğrulama işlemi reddedildi. Bağlantı iptal edildi."
},
"fileManager": {
"title": "Dosya Yöneticisi",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Şifre Sıfırlama Başarılı",
"passwordResetSuccessDesc": "Parolanız başarıyla sıfırlandı. Artık yeni parolanızla giriş yapabilirsiniz."
},
"hostKey": {
"verifyNewHost": "SSH Ana Bilgisayar Anahtarını Doğrulayın",
"keyChangedWarning": "SSH Ana Bilgisayar Anahtarı Değiştirildi",
"firstConnectionTitle": "Bu sunucuya ilk kez bağlanıyorum.",
"firstConnectionDescription": "Bu sunucunun kimliği doğrulanamamıştır. Parmak izinin beklediğinizle eşleştiğini doğrulayın.",
"keyChangedDescription": "Bu sunucunun anahtar kodu son bağlantınızdan bu yana değişti. Bu bir güvenlik sorununa işaret edebilir.",
"previousKey": "Önceki Anahtar",
"newFingerprint": "Yeni Parmak İzi",
"fingerprint": "Parmak izi",
"verifyInstructions": "Bu sunucuya güveniyorsanız, devam etmek ve bu parmak izini gelecekteki bağlantılar için kaydetmek üzere Kabul Et'e tıklayın.",
"securityWarning": "Güvenlik Uyarısı",
"acceptAndContinue": "Kabul Et ve Devam Et",
"acceptNewKey": "Yeni Anahtarı Kabul Et ve Devam Et"
},
"errors": {
"notFound": "Sayfa bulunamadı.",
"unauthorized": "Yetkisiz erişim",
@@ -2026,6 +2063,8 @@
"terminalSettings": "terminal",
"hostSidebarSettings": "Sunucu ve Yan Menü",
"snippetsSettings": "Kısa bölümler",
"confirmSnippetExecution": "Kod Parçasının Yürütülmesini Onayla",
"confirmSnippetExecutionDesc": "Kod parçacıklarını çalıştırmadan önce onay iletişim kutusunu göster.",
"updateSettings": "Güncellemeler",
"disableUpdateCheck": "Güncelleme Kontrolünü Devre Dışı Bırak",
"disableUpdateCheckDesc": "Başlangıçta ve kontrol panelinde yeni sürümleri kontrol etmeyi durdurun. Ağ isteklerini azaltır.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 veya example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Дані доступу",
"credentialsViewer": "Переглядач облікових даних",
"manageYourSSHCredentials": "Безпечно керувати вашими обліковими даними SSH",
"addCredential": "Додати",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Це безпечно додасть відкритий ключ до цільового хоста ~/.ssh/authorized_keys файл без перезапису існуючих ключів. Операція незворотна.",
"chooseHostToDeploy": "Виберіть хост для розгортання...",
"deploying": "Розгортання...",
"copyDeployCommand": "Скопіювати Команду Розгортання",
"copyDeployCommandDescription": "Скопіювати команду оболонки в ручне додавання відкритого ключа в файл authorized_keys. Корисно, коли автентифікація пароля вимкнена.",
"deployCommandCopied": "Команда розгортання скопійована до буферу обміну",
"manualDeployInfo": "Вставити цю команду в терміналі цільового хоста (наприклад, консоль Proxmox, IPMI або фізичний доступ), щоб додати SSH ключ.",
"name": "Ім'я",
"noHostsAvailable": "Немає доступних хостів",
"noHostsMatchSearch": "Немає хостів, які відповідають вашому пошуку",
@@ -298,7 +303,9 @@
"createFolder": "Створити теку",
"editFolder": "Змінити теку",
"editFolderDescription": "Налаштуйте вашу теку зі сніпетом",
"createFolderDescription": "Організуйте ваші сніпети у папки"
"createFolderDescription": "Організуйте ваші сніпети у папки",
"confirmExecution": "Виконати{{name}}\"?",
"confirmExecutionDesc": "Ви впевнені, що хочете зробити цей сніпет?"
},
"commandHistory": {
"title": "Історія",
@@ -402,6 +409,7 @@
"required": "Обов'язково",
"optional": "За бажанням",
"connect": "Підключитися",
"copied": "Скопійовано",
"connecting": "З’єднання...",
"creating": "Створюю...",
"clear": "Очистити",
@@ -494,6 +502,7 @@
"checking": "Перевірка...",
"checkingDatabase": "Перевірка підключення до бази даних...",
"checkingAuthentication": "Перевірка автентифікації...",
"backendReconnected": "З'єднання з сервером відновлено",
"actions": "Дії",
"remove": "Видалити",
"revoke": "Revoke",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Менеджер хостів",
"hosts": "Хости",
"sshHosts": "SSH хости",
"noHosts": "Немає SSH хостів",
"noHostsMessage": "Ви ще не додали жодного SSH хоста. Натисніть \"Додати хоста\".",
@@ -836,7 +846,7 @@
"failedToImportJson": "Не вдалося імпортувати файл JSON",
"connectionDetails": "Відомості про підключення",
"organization": "Організація",
"ipAddress": "IP-адреса",
"ipAddress": "IP-адреса або ім'я хоста",
"port": "Порт",
"name": "Ім'я",
"username": "Ім'я користувача",
@@ -915,6 +925,8 @@
"key": "Ключ",
"credential": "Облікові дані",
"none": "Без ефекту",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Ви заохочуєте вас до ОПК-авторизацію при кожному підключенні. Токен автентифікації буде тривати 24 години, поки він буде буде поновлений.",
"selectCredential": "Оберіть облікові дані",
"selectCredentialPlaceholder": "Виберіть облікові дані...",
"credentialRequired": "Обліковий запис потрібен під час використання аутентифікації облікових даних",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Клавіатура-інтерактивна автентифікація",
"noneAuthDescription": "Цей метод автентифікації використовуватиме клавіатурну інтерактивну автентифікацію при підключенні до SSH сервера.",
"noneAuthDetails": "Увімкнута інтерактивна клавіатура дозволяє серверу підводити Ваші дані під час підключення. Це корисно для серверів, які потребують багатофакторної аутентифікації або якщо ви не хочете зберегти облікові дані локально.",
"opksshAuthTitle": "Аутентифікація OPKSSH",
"forceKeyboardInteractive": "Примусова інтерактивна клавіатура",
"forceKeyboardInteractiveDesc": "Вказує використання клавіатури для автентифікації. Іноді це потрібно для серверів, які використовують двофакторну автентифікацію (TOTP/2FA).",
"overrideCredentialUsername": "Перевизначити ім'я користувача",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Відкрити у браузері",
"warpgateContinue": "Я вже завершив автентифікацію",
"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": "Введіть пароль?",
"sudoPasswordPopupHint": "Натисніть Enter, щоб вставити, Esc для відхилення",
"sudoPasswordPopupConfirm": "Insert",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Автоматично пробує автентифікацію {{method}}...",
"totpTimeout": "Таймаут перевірки TOTP. Будь ласка, під'єднайтеся.",
"passwordTimeout": "Час очікування підтвердження пароля. Будь ласка, під'єднайтеся.",
"connectionRejected": "З'єднання відхилено сервером. Будь ласка, перевірте налаштування аутентифікації та мережі."
"connectionRejected": "З'єднання відхилено сервером. Будь ласка, перевірте налаштування аутентифікації та мережі.",
"hostKeyRejected": "SSH-ключ хоста відхилено. Підключення скасовано."
},
"fileManager": {
"title": "Файловий менеджер",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Пароль успішно скинуто",
"passwordResetSuccessDesc": "Ваш пароль успішно скинуто. Тепер ви можете увійти в систему, використовуючи новий пароль."
},
"hostKey": {
"verifyNewHost": "Перевірити SSH ключ хоста",
"keyChangedWarning": "SSH ключ хоста змінено",
"firstConnectionTitle": "вперше при підключенні до цього хосту",
"firstConnectionDescription": "Неможливо встановити автентифікацію цього хоста. Перевірте, чи збігається ваш запит.",
"keyChangedDescription": "Ключ хоста для цього сервера було змінено після вашого останнього підключення. Це може означати проблему безпеки.",
"previousKey": "Попередній ключ",
"newFingerprint": "Новий відбиток пальця",
"fingerprint": "Відбиток пальця",
"verifyInstructions": "Щоб продовжити і зберегти цей відбиток для майбутніх з'єднань, натисніть кнопку \"Прийняти\".",
"securityWarning": "Попередження системи безпеки",
"acceptAndContinue": "Прийняти та продовжити",
"acceptNewKey": "Прийняти новий ключ і продовжити"
},
"errors": {
"notFound": "Сторінка не знайдена",
"unauthorized": "Неавторизований доступ",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Термінал",
"hostSidebarSettings": "Хост і бічна панель",
"snippetsSettings": "Сніпети",
"confirmSnippetExecution": "Підтвердити виконання сніпета",
"confirmSnippetExecutionDesc": "Показати діалогове вікно підтвердження перед виконанням сніпетів",
"updateSettings": "Оновлення",
"disableUpdateCheck": "Вимкнути перевірку оновлень",
"disableUpdateCheckDesc": "Зупинити перевірку для нових версій при запуску і панелі керування. Зменшує запити на мережі.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 або приклад.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
@@ -1,5 +1,6 @@
{
"credentials": {
"credentials": "Thông tin xác thực",
"credentialsViewer": "Trình xem thông tin đăng nhập",
"manageYourSSHCredentials": "Quản lý thông tin đăng nhập SSH của bạn một cách an toàn",
"addCredential": "Thêm thông tin đăng nhập",
@@ -99,6 +100,10 @@
"deploymentProcessDescription": "Thao tác này sẽ thêm khóa công khai một cách an toàn vào tệp ~/.ssh/authorized_keys của máy chủ đích mà không ghi đè lên các khóa hiện có. Thao tác này có thể đảo ngược.",
"chooseHostToDeploy": "Chọn máy chủ để triển khai...",
"deploying": "Đang triển khai...",
"copyDeployCommand": "Sao chép lệnh triển khai",
"copyDeployCommandDescription": "Sao chép lệnh shell để thêm thủ công khóa công khai vào tệp authorized_keys của máy chủ. Hữu ích khi xác thực bằng mật khẩu bị vô hiệu hóa.",
"deployCommandCopied": "Lệnh triển khai đã được sao chép vào clipboard",
"manualDeployInfo": "Dán lệnh này vào cửa sổ terminal của máy chủ đích (ví dụ: console Proxmox, IPMI hoặc truy cập vật lý) để thêm khóa SSH.",
"name": "Tên",
"noHostsAvailable": "Hiện không có máy chủ nào khả dụng",
"noHostsMatchSearch": "Không có máy chủ nào phù hợp với tìm kiếm của bạn",
@@ -298,7 +303,9 @@
"createFolder": "Tạo thư mục",
"editFolder": "Chỉnh sửa thư mục",
"editFolderDescription": "Tùy chỉnh thư mục đoạn mã của bạn",
"createFolderDescription": "Sắp xếp các đoạn văn bản của bạn vào các thư mục."
"createFolderDescription": "Sắp xếp các đoạn văn bản của bạn vào các thư mục.",
"confirmExecution": "Thực thi \"{{name}}\"?",
"confirmExecutionDesc": "Bạn có chắc chắn muốn thực thi đoạn mã này không?"
},
"commandHistory": {
"title": "Lịch sử",
@@ -402,6 +409,7 @@
"required": "Yêu cầu",
"optional": "Không bắt buộc",
"connect": "Kết nối",
"copied": "Đã sao chép",
"connecting": "Đang kết nối...",
"creating": "Đang tạo...",
"clear": "Thông thoáng",
@@ -494,6 +502,7 @@
"checking": "Đang kiểm tra...",
"checkingDatabase": "Kiểm tra kết nối cơ sở dữ liệu...",
"checkingAuthentication": "Đang kiểm tra xác thực...",
"backendReconnected": "Kết nối máy chủ đã được khôi phục.",
"actions": "Hành động",
"remove": "Di dời",
"revoke": "Thu hồi",
@@ -806,6 +815,7 @@
},
"hosts": {
"title": "Quản lý máy chủ",
"hosts": "Người dẫn chương trình",
"sshHosts": "Máy chủ SSH",
"noHosts": "Không có máy chủ SSH",
"noHostsMessage": "Bạn chưa thêm bất kỳ máy chủ SSH nào. Nhấp vào \"Thêm máy chủ\" để bắt đầu.",
@@ -836,7 +846,7 @@
"failedToImportJson": "Không thể nhập tệp JSON.",
"connectionDetails": "Chi tiết kết nối",
"organization": "Tổ chức",
"ipAddress": "Địa chỉ IP",
"ipAddress": "Địa chỉ IP hoặc tên máy chủ",
"port": "Cảng",
"name": "Tên",
"username": "Tên người dùng",
@@ -915,6 +925,8 @@
"key": "Chìa khóa",
"credential": "Chứng chỉ",
"none": "Không có",
"opkssh": "OPKSSH",
"opksshAuthDescription": "Hệ thống sẽ yêu cầu bạn xác thực web bằng OPKSSH mỗi khi kết nối. Mã xác thực của bạn sẽ có hiệu lực trong 24 giờ trước khi cần được gia hạn.",
"selectCredential": "Chọn thông tin xác thực",
"selectCredentialPlaceholder": "Chọn một loại chứng chỉ...",
"credentialRequired": "Cần có thông tin đăng nhập khi sử dụng phương thức xác thực bằng thông tin đăng nhập.",
@@ -1083,6 +1095,7 @@
"noneAuthTitle": "Xác thực tương tác bàn phím",
"noneAuthDescription": "Phương thức xác thực này sẽ sử dụng xác thực tương tác bàn phím khi kết nối với máy chủ SSH.",
"noneAuthDetails": "Xác thực tương tác bàn phím cho phép máy chủ yêu cầu bạn nhập thông tin đăng nhập trong quá trình kết nối. Điều này hữu ích cho các máy chủ yêu cầu xác thực đa yếu tố hoặc nếu bạn không muốn lưu thông tin đăng nhập cục bộ.",
"opksshAuthTitle": "Xác thực OPKSSH",
"forceKeyboardInteractive": "Buộc tương tác bàn phím",
"forceKeyboardInteractiveDesc": "Buộc sử dụng xác thực tương tác bàn phím. Điều này đôi khi cần thiết đối với các máy chủ sử dụng xác thực hai yếu tố (TOTP/2FA).",
"overrideCredentialUsername": "Ghi đè tên người dùng xác thực",
@@ -1316,6 +1329,15 @@
"warpgateOpenBrowser": "Mở trong trình duyệt",
"warpgateContinue": "Tôi đã hoàn tất xác thực.",
"warpgateTimeout": "Lỗi xác thực Warpgate đã hết thời gian chờ. Vui lòng kết nối lại.",
"opksshAuthRequired": "Yêu cầu xác thực OPKSSH",
"opksshAuthDescription": "Hoàn tất xác thực trên trình duyệt của bạn để tiếp tục. Phiên này sẽ có hiệu lực trong 24 giờ.",
"opksshAuthUrl": "URL xác thực",
"opksshOpenBrowser": "Mở trình duyệt để xác thực.",
"opksshWaitingForAuth": "Đang chờ xác thực trên trình duyệt...",
"opksshAuthenticating": "Đang xử lý xác thực...",
"opksshTimeout": "Quá trình xác thực đã hết hạn. Vui lòng thử lại.",
"opksshAuthFailed": "Xác thực không thành công. Vui lòng kiểm tra lại thông tin đăng nhập của bạn và thử lại.",
"opksshConfigMissing": "Không tìm thấy cấu hình OPKSSH. Vui lòng tạo tệp ~/.opk/config.yml với các cài đặt nhà cung cấp OIDC của bạn. Xem tài liệu: https://github.com/openpubkey/opkssh#configuration",
"sudoPasswordPopupTitle": "Nhập mật khẩu?",
"sudoPasswordPopupHint": "Nhấn Enter để chèn, Esc để xóa",
"sudoPasswordPopupConfirm": "Chèn",
@@ -1337,7 +1359,8 @@
"automaticFallback": "Đang tự động thử xác thực {{method}} ...",
"totpTimeout": "Đã hết thời gian xác thực TOTP. Vui lòng kết nối lại.",
"passwordTimeout": "Đã hết thời gian xác thực mật khẩu. Vui lòng kết nối lại.",
"connectionRejected": "Kết nối bị máy chủ từ chối. Vui lòng kiểm tra cấu hình xác thực và mạng của bạn."
"connectionRejected": "Kết nối bị máy chủ từ chối. Vui lòng kiểm tra cấu hình xác thực và mạng của bạn.",
"hostKeyRejected": "Xác thực khóa máy chủ SSH bị từ chối. Kết nối bị hủy."
},
"fileManager": {
"title": "Trình quản lý tập tin",
@@ -1934,6 +1957,20 @@
"passwordResetSuccess": "Đặt lại mật khẩu thành công",
"passwordResetSuccessDesc": "Mật khẩu của bạn đã được đặt lại thành công. Giờ bạn có thể đăng nhập bằng mật khẩu mới."
},
"hostKey": {
"verifyNewHost": "Xác minh khóa máy chủ SSH",
"keyChangedWarning": "Khóa máy chủ SSH đã thay đổi",
"firstConnectionTitle": "Lần đầu tiên kết nối với máy chủ này.",
"firstConnectionDescription": "Không thể xác minh tính xác thực của máy chủ này. Hãy kiểm tra xem dấu vân tay có khớp với những gì bạn mong đợi hay không.",
"keyChangedDescription": "Khóa máy chủ (host key) của máy chủ này đã thay đổi kể từ lần kết nối cuối cùng của bạn. Điều này có thể cho thấy một vấn đề về bảo mật.",
"previousKey": "Phím trước đó",
"newFingerprint": "Dấu vân tay mới",
"fingerprint": "Dấu vân tay",
"verifyInstructions": "Nếu bạn tin tưởng máy chủ này, hãy nhấp vào Chấp nhận để tiếp tục và lưu dấu vân tay này cho các kết nối trong tương lai.",
"securityWarning": "Cảnh báo an ninh",
"acceptAndContinue": "Chấp nhận và tiếp tục",
"acceptNewKey": "Chấp nhận khóa mới và tiếp tục"
},
"errors": {
"notFound": "Không tìm thấy trang",
"unauthorized": "Truy cập trái phép",
@@ -2026,6 +2063,8 @@
"terminalSettings": "Phần cuối",
"hostSidebarSettings": "Máy chủ & Thanh bên",
"snippetsSettings": "Những đoạn trích",
"confirmSnippetExecution": "Xác nhận việc thực thi đoạn mã",
"confirmSnippetExecutionDesc": "Hiển thị hộp thoại xác nhận trước khi thực thi các đoạn mã.",
"updateSettings": "Cập nhật",
"disableUpdateCheck": "Tắt kiểm tra cập nhật",
"disableUpdateCheckDesc": "Ngừng kiểm tra các phiên bản mới khi khởi động và trên bảng điều khiển. Giảm số lượng yêu cầu mạng.",
@@ -2046,7 +2085,7 @@
},
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
"ipAddress": "192.168.1.1 hoặc example.com",
"port": "22",
"maxRetries": "3",
"retryInterval": "10",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9 -6
View File
@@ -23,7 +23,7 @@ export interface SSHHost {
folder: string;
tags: string[];
pin: boolean;
authType: "password" | "key" | "credential" | "none";
authType: "password" | "key" | "credential" | "none" | "opkssh";
password?: string;
key?: string;
keyPassword?: string;
@@ -95,7 +95,7 @@ export interface SSHHostData {
folder?: string;
tags?: string[];
pin?: boolean;
authType: "password" | "key" | "credential" | "none";
authType: "password" | "key" | "credential" | "none" | "opkssh";
password?: string;
key?: File | null;
keyPassword?: string;
@@ -150,7 +150,7 @@ export interface Credential {
folder?: string;
tags: string[];
authType: "password" | "key";
username: string;
username?: string;
password?: string;
key?: string;
publicKey?: string;
@@ -170,7 +170,7 @@ export interface CredentialBackend {
folder: string | null;
tags: string;
authType: "password" | "key";
username: string;
username: string | null;
password: string | null;
key: string;
private_key?: string;
@@ -190,7 +190,7 @@ export interface CredentialData {
folder?: string;
tags: string[];
authType: "password" | "key";
username: string;
username?: string;
password?: string;
key?: string;
publicKey?: string;
@@ -465,7 +465,7 @@ export type ErrorType =
// AUTHENTICATION TYPES
// ============================================================================
export type AuthType = "password" | "key" | "credential" | "none";
export type AuthType = "password" | "key" | "credential" | "none" | "opkssh";
export type KeyType = "rsa" | "ecdsa" | "ed25519";
@@ -486,11 +486,13 @@ export interface ApiResponse<T = unknown> {
export interface CredentialsManagerProps {
onEditCredential?: (credential: Credential) => void;
onAddCredential?: () => void;
}
export interface CredentialEditorProps {
editingCredential?: Credential | null;
onFormSubmit?: () => void;
onBack?: () => void;
}
export interface CredentialViewerProps {
@@ -523,6 +525,7 @@ export interface SSHManagerHostEditorProps {
export interface SSHManagerHostViewerProps {
onEditHost?: (host: SSHHost) => void;
onAddHost?: () => void;
}
export interface HostProps {
+7 -6
View File
@@ -59,12 +59,15 @@ function AppContent({
useEffect(() => {
const handleDatabaseConnectionLost = () => {
setDbConnectionFailed(true);
setIsAuthenticated(false);
};
const handleDatabaseConnectionRestored = () => {
setDbConnectionFailed(false);
window.location.reload();
toast.success(t("common.backendReconnected"));
};
const handleSessionExpired = () => {
setIsAuthenticated(false);
};
dbHealthMonitor.on(
@@ -75,6 +78,7 @@ function AppContent({
"database-connection-restored",
handleDatabaseConnectionRestored,
);
dbHealthMonitor.on("session-expired", handleSessionExpired);
return () => {
dbHealthMonitor.off(
@@ -85,6 +89,7 @@ function AppContent({
"database-connection-restored",
handleDatabaseConnectionRestored,
);
dbHealthMonitor.off("session-expired", handleSessionExpired);
};
}, []);
@@ -255,10 +260,6 @@ function AppContent({
setTimeout(async () => {
try {
await logoutUser();
if (isElectron()) {
localStorage.removeItem("jwt");
}
} catch (error) {
console.error("Logout failed:", error);
}

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