* feat: add robust OSC 52 clipboard support for tmux/SSH clipboard sync (#587)

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

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

* chore: remove translations

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

* Temporary merge for 1.11.1 syncing (#543)

* fix: remote translations

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

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to open terminal directly (#156) (#503)

* fix: resolve merge conflict artifacts in dev-1.10.1

- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts

* feat: support URL routes to open terminal directly (#156)

- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found

---------

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

* feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501)

Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal.
This adds Ctrl+Alt+<key> as an alternative that sends Ctrl+<key>.

- Ctrl+Alt+W → Ctrl+W (nano search, delete word)
- Ctrl+Alt+T → Ctrl+T (transpose chars)
- Ctrl+Alt+N → Ctrl+N (next line)
- Ctrl+Alt+Q → Ctrl+Q (XON flow control)

Fixes Termix-SSH/Support#407

* feat: remove locales

* New Crowdin updates (#504)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* feat: add option to disable update checker (#502)

* feat: add option to disable update checker

Add a new setting in User Profile > Settings to disable automatic
update checking on startup and dashboard.

- Adds 'Disable Update Check' toggle in profile settings
- Skips GitHub API calls when disabled (reduces network requests)
- Works for both web app and Electron client

Fixes Termix-SSH/Support#410

* feat: remove locales

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* New Crowdin updates (#505)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* feat: add crowdin i18n

* feat: remove locales

* New Crowdin updates (#506)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* feat: update readme

* feat: update readme

* feat: update credential editor to use submitting system and add health monitor

* feat: added toggle for command pallete

* feat: added close button on tab dropdown

* feat: added sidebar management and improved some host manager UI/UX

* feat: re-added missing users.ts route from merge

* feat: add toggle for password reset feature in admin settings (#508)

* feat: add sudo support for file manager operations (#509)

* fix: add sudo support for listFiles and improve permission error handling (#512)

* feat: add sudo support for file manager operations

* fix: add sudo support for listFiles and improve permission error handling

---------

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

* feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated

* feat: add copy password button and fixed new line carriage issues and backend crash for auth key

* feat: added quick connection system (ad-hoc)

* Enter Key for Quick Login (#513)

* feat: added -r and -l support for tunnels

* feat: begin dashboard overhaul by splitting into cards and adding customization

* feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc.

* feat: add auth.tsx suppot for fullscreen

* feat: greatly improve network graph ui/ux and migrated to use translations and theme system

* feat: update to use blacksmith

* feat: improve ui for customized tabs and hide add/edit host/credential when submiting

* feat: add warpgate support with a dialog (terminal only)

* feat: expand warpgate to docker/file manager

* fix: docker not working wtih warpgate and none auth failing for terminal

* fix: prevent owner permission loss when sharing host to own role (#514)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* fix: prevent owner permission loss when sharing host to own role

Fixes #391

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: SSH key passphrase not passed for Docker and Tunnel (#521)

* perf: optimize Host Manager for large host lists

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

* fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel

Database field is `key_password` but code used `keyPassword`.
Added fallback to check both field names.

Affected:
- docker.ts: Docker SSH connections with encrypted keys
- tunnel.ts: Tunnel connections with encrypted keys

---------

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

* perf: optimize Host Manager for large host lists (#520)

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

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

* fix: add missing mimeTypes definition for image preview (#518)

Fixes Termix-SSH/Support#408

* fix: prevent session reset when updating host properties (#517)

Move WebSocket cleanup logic to a separate unmount-only effect to
prevent SSH sessions from being closed when host properties are updated.

Closes Termix-SSH/Support#401

* fix: backend type error

* feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385)

* feat: improved conneciton log ui/logic

* feat: improved conneciton log ui/logic

* feat: expanded connection log to work across all components (readying for release)

* feat: update readme

* feat: update readme

* fix: build error

* fix: build error

* fix: changed ver

* chore: clean up

* chore: continue clean up

* fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies

* fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field

* fix: update readme and run cleaner

* fix: update readme

* fix: update readme

* fix: update readme

* feat: update chinese readme

* feat: support OIDC configuration via environment variables

Add support for configuring OIDC authentication through environment
variables, enabling containerized deployments without database access:

- OIDC_CLIENT_ID
- OIDC_CLIENT_SECRET
- OIDC_ISSUER_URL
- OIDC_AUTHORIZATION_URL
- OIDC_TOKEN_URL
- OIDC_USERINFO_URL (optional)
- OIDC_IDENTIFIER_PATH (optional, default: "sub")
- OIDC_NAME_PATH (optional, default: "name")
- OIDC_SCOPES (optional, default: "openid email profile")

Environment variables take priority over database configuration.

Closes Termix-SSH/Support#16

---------

Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>

* feat: add modern DH group KEX algorithms for better compatibility (#530)

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to open terminal directly (#156) (#503)

* fix: resolve merge conflict artifacts in dev-1.10.1

- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts

* feat: support URL routes to open terminal directly (#156)

- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found

---------

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

* feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501)

Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal.
This adds Ctrl+Alt+<key> as an alternative that sends Ctrl+<key>.

- Ctrl+Alt+W → Ctrl+W (nano search, delete word)
- Ctrl+Alt+T → Ctrl+T (transpose chars)
- Ctrl+Alt+N → Ctrl+N (next line)
- Ctrl+Alt+Q → Ctrl+Q (XON flow control)

Fixes Termix-SSH/Support#407

* feat: remove locales

* New Crowdin updates (#504)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* feat: add option to disable update checker (#502)

* feat: add option to disable update checker

Add a new setting in User Profile > Settings to disable automatic
update checking on startup and dashboard.

- Adds 'Disable Update Check' toggle in profile settings
- Skips GitHub API calls when disabled (reduces network requests)
- Works for both web app and Electron client

Fixes Termix-SSH/Support#410

* feat: remove locales

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* New Crowdin updates (#505)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* feat: add crowdin i18n

* feat: remove locales

* New Crowdin updates (#506)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* feat: update readme

* feat: update readme

* feat: update credential editor to use submitting system and add health monitor

* feat: added toggle for command pallete

* feat: added close button on tab dropdown

* feat: added sidebar management and improved some host manager UI/UX

* feat: re-added missing users.ts route from merge

* feat: add toggle for password reset feature in admin settings (#508)

* feat: add sudo support for file manager operations (#509)

* fix: add sudo support for listFiles and improve permission error handling (#512)

* feat: add sudo support for file manager operations

* fix: add sudo support for listFiles and improve permission error handling

---------

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

* feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated

* feat: add copy password button and fixed new line carriage issues and backend crash for auth key

* feat: added quick connection system (ad-hoc)

* Enter Key for Quick Login (#513)

* feat: added -r and -l support for tunnels

* feat: begin dashboard overhaul by splitting into cards and adding customization

* feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc.

* feat: add auth.tsx suppot for fullscreen

* feat: greatly improve network graph ui/ux and migrated to use translations and theme system

* feat: update to use blacksmith

* feat: improve ui for customized tabs and hide add/edit host/credential when submiting

* feat: add warpgate support with a dialog (terminal only)

* feat: expand warpgate to docker/file manager

* fix: docker not working wtih warpgate and none auth failing for terminal

* fix: prevent owner permission loss when sharing host to own role (#514)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* fix: prevent owner permission loss when sharing host to own role

Fixes #391

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: SSH key passphrase not passed for Docker and Tunnel (#521)

* perf: optimize Host Manager for large host lists

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

* fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel

Database field is `key_password` but code used `keyPassword`.
Added fallback to check both field names.

Affected:
- docker.ts: Docker SSH connections with encrypted keys
- tunnel.ts: Tunnel connections with encrypted keys

---------

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

* perf: optimize Host Manager for large host lists (#520)

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

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

* fix: add missing mimeTypes definition for image preview (#518)

Fixes Termix-SSH/Support#408

* fix: prevent session reset when updating host properties (#517)

Move WebSocket cleanup logic to a separate unmount-only effect to
prevent SSH sessions from being closed when host properties are updated.

Closes Termix-SSH/Support#401

* fix: backend type error

* feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385)

* feat: improved conneciton log ui/logic

* feat: improved conneciton log ui/logic

* feat: expanded connection log to work across all components (readying for release)

* feat: update readme

* feat: update readme

* fix: build error

* fix: build error

* fix: changed ver

* chore: clean up

* chore: continue clean up

* fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies

* fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field

* fix: update readme and run cleaner

* fix: update readme

* fix: update readme

* fix: update readme

* feat: update chinese readme

* feat: add modern DH group KEX algorithms for better compatibility

Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms
which are supported by ssh2 library but were not configured in Termix.

These algorithms provide:
- Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.)
- Stronger security with larger DH groups
- RFC 8268 compliance

Related to Termix-SSH/Support#205

---------

Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>

* feat: clarify that hostname/FQDN is supported in IP address field (#529)

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to open…

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

* Temporary merge for 1.11.1 syncing (#543)

* fix: remote translations

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

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to open terminal directly (#156) (#503)

* fix: resolve merge conflict artifacts in dev-1.10.1

- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts

* feat: support URL routes to open terminal directly (#156)

- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found

---------

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

* feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501)

Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal.
This adds Ctrl+Alt+<key> as an alternative that sends Ctrl+<key>.

- Ctrl+Alt+W → Ctrl+W (nano search, delete word)
- Ctrl+Alt+T → Ctrl+T (transpose chars)
- Ctrl+Alt+N → Ctrl+N (next line)
- Ctrl+Alt+Q → Ctrl+Q (XON flow control)

Fixes Termix-SSH/Support#407

* feat: remove locales

* New Crowdin updates (#504)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* feat: add option to disable update checker (#502)

* feat: add option to disable update checker

Add a new setting in User Profile > Settings to disable automatic
update checking on startup and dashboard.

- Adds 'Disable Update Check' toggle in profile settings
- Skips GitHub API calls when disabled (reduces network requests)
- Works for both web app and Electron client

Fixes Termix-SSH/Support#410

* feat: remove locales

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* New Crowdin updates (#505)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* feat: add crowdin i18n

* feat: remove locales

* New Crowdin updates (#506)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* feat: update readme

* feat: update readme

* feat: update credential editor to use submitting system and add health monitor

* feat: added toggle for command pallete

* feat: added close button on tab dropdown

* feat: added sidebar management and improved some host manager UI/UX

* feat: re-added missing users.ts route from merge

* feat: add toggle for password reset feature in admin settings (#508)

* feat: add sudo support for file manager operations (#509)

* fix: add sudo support for listFiles and improve permission error handling (#512)

* feat: add sudo support for file manager operations

* fix: add sudo support for listFiles and improve permission error handling

---------

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

* feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated

* feat: add copy password button and fixed new line carriage issues and backend crash for auth key

* feat: added quick connection system (ad-hoc)

* Enter Key for Quick Login (#513)

* feat: added -r and -l support for tunnels

* feat: begin dashboard overhaul by splitting into cards and adding customization

* feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc.

* feat: add auth.tsx suppot for fullscreen

* feat: greatly improve network graph ui/ux and migrated to use translations and theme system

* feat: update to use blacksmith

* feat: improve ui for customized tabs and hide add/edit host/credential when submiting

* feat: add warpgate support with a dialog (terminal only)

* feat: expand warpgate to docker/file manager

* fix: docker not working wtih warpgate and none auth failing for terminal

* fix: prevent owner permission loss when sharing host to own role (#514)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* fix: prevent owner permission loss when sharing host to own role

Fixes #391

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: SSH key passphrase not passed for Docker and Tunnel (#521)

* perf: optimize Host Manager for large host lists

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

* fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel

Database field is `key_password` but code used `keyPassword`.
Added fallback to check both field names.

Affected:
- docker.ts: Docker SSH connections with encrypted keys
- tunnel.ts: Tunnel connections with encrypted keys

---------

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

* perf: optimize Host Manager for large host lists (#520)

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

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

* fix: add missing mimeTypes definition for image preview (#518)

Fixes Termix-SSH/Support#408

* fix: prevent session reset when updating host properties (#517)

Move WebSocket cleanup logic to a separate unmount-only effect to
prevent SSH sessions from being closed when host properties are updated.

Closes Termix-SSH/Support#401

* fix: backend type error

* feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385)

* feat: improved conneciton log ui/logic

* feat: improved conneciton log ui/logic

* feat: expanded connection log to work across all components (readying for release)

* feat: update readme

* feat: update readme

* fix: build error

* fix: build error

* fix: changed ver

* chore: clean up

* chore: continue clean up

* fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies

* fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field

* fix: update readme and run cleaner

* fix: update readme

* fix: update readme

* fix: update readme

* feat: update chinese readme

* feat: support OIDC configuration via environment variables

Add support for configuring OIDC authentication through environment
variables, enabling containerized deployments without database access:

- OIDC_CLIENT_ID
- OIDC_CLIENT_SECRET
- OIDC_ISSUER_URL
- OIDC_AUTHORIZATION_URL
- OIDC_TOKEN_URL
- OIDC_USERINFO_URL (optional)
- OIDC_IDENTIFIER_PATH (optional, default: "sub")
- OIDC_NAME_PATH (optional, default: "name")
- OIDC_SCOPES (optional, default: "openid email profile")

Environment variables take priority over database configuration.

Closes Termix-SSH/Support#16

---------

Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>

* feat: add modern DH group KEX algorithms for better compatibility (#530)

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to open terminal directly (#156) (#503)

* fix: resolve merge conflict artifacts in dev-1.10.1

- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts

* feat: support URL routes to open terminal directly (#156)

- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found

---------

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

* feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501)

Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal.
This adds Ctrl+Alt+<key> as an alternative that sends Ctrl+<key>.

- Ctrl+Alt+W → Ctrl+W (nano search, delete word)
- Ctrl+Alt+T → Ctrl+T (transpose chars)
- Ctrl+Alt+N → Ctrl+N (next line)
- Ctrl+Alt+Q → Ctrl+Q (XON flow control)

Fixes Termix-SSH/Support#407

* feat: remove locales

* New Crowdin updates (#504)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* feat: add option to disable update checker (#502)

* feat: add option to disable update checker

Add a new setting in User Profile > Settings to disable automatic
update checking on startup and dashboard.

- Adds 'Disable Update Check' toggle in profile settings
- Skips GitHub API calls when disabled (reduces network requests)
- Works for both web app and Electron client

Fixes Termix-SSH/Support#410

* feat: remove locales

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* New Crowdin updates (#505)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* feat: add crowdin i18n

* feat: remove locales

* New Crowdin updates (#506)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* feat: update readme

* feat: update readme

* feat: update credential editor to use submitting system and add health monitor

* feat: added toggle for command pallete

* feat: added close button on tab dropdown

* feat: added sidebar management and improved some host manager UI/UX

* feat: re-added missing users.ts route from merge

* feat: add toggle for password reset feature in admin settings (#508)

* feat: add sudo support for file manager operations (#509)

* fix: add sudo support for listFiles and improve permission error handling (#512)

* feat: add sudo support for file manager operations

* fix: add sudo support for listFiles and improve permission error handling

---------

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

* feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated

* feat: add copy password button and fixed new line carriage issues and backend crash for auth key

* feat: added quick connection system (ad-hoc)

* Enter Key for Quick Login (#513)

* feat: added -r and -l support for tunnels

* feat: begin dashboard overhaul by splitting into cards and adding customization

* feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc.

* feat: add auth.tsx suppot for fullscreen

* feat: greatly improve network graph ui/ux and migrated to use translations and theme system

* feat: update to use blacksmith

* feat: improve ui for customized tabs and hide add/edit host/credential when submiting

* feat: add warpgate support with a dialog (terminal only)

* feat: expand warpgate to docker/file manager

* fix: docker not working wtih warpgate and none auth failing for terminal

* fix: prevent owner permission loss when sharing host to own role (#514)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* fix: prevent owner permission loss when sharing host to own role

Fixes #391

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: SSH key passphrase not passed for Docker and Tunnel (#521)

* perf: optimize Host Manager for large host lists

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

* fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel

Database field is `key_password` but code used `keyPassword`.
Added fallback to check both field names.

Affected:
- docker.ts: Docker SSH connections with encrypted keys
- tunnel.ts: Tunnel connections with encrypted keys

---------

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

* perf: optimize Host Manager for large host lists (#520)

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

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

* fix: add missing mimeTypes definition for image preview (#518)

Fixes Termix-SSH/Support#408

* fix: prevent session reset when updating host properties (#517)

Move WebSocket cleanup logic to a separate unmount-only effect to
prevent SSH sessions from being closed when host properties are updated.

Closes Termix-SSH/Support#401

* fix: backend type error

* feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385)

* feat: improved conneciton log ui/logic

* feat: improved conneciton log ui/logic

* feat: expanded connection log to work across all components (readying for release)

* feat: update readme

* feat: update readme

* fix: build error

* fix: build error

* fix: changed ver

* chore: clean up

* chore: continue clean up

* fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies

* fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field

* fix: update readme and run cleaner

* fix: update readme

* fix: update readme

* fix: update readme

* feat: update chinese readme

* feat: add modern DH group KEX algorithms for better compatibility

Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms
which are supported by ssh2 library but were not configured in Termix.

These algorithms provide:
- Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.)
- Stronger security with larger DH groups
- RFC 8268 compliance

Related to Termix-SSH/Support#205

---------

Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>

* feat: clarify that hostname/FQDN is supported in IP address field (#529)

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to open terminal dir…

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

* Temporary merge for 1.11.1 syncing (#543)

* fix: remote translations

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

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to open terminal directly (#156) (#503)

* fix: resolve merge conflict artifacts in dev-1.10.1

- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts

* feat: support URL routes to open terminal directly (#156)

- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found

---------

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

* feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501)

Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal.
This adds Ctrl+Alt+<key> as an alternative that sends Ctrl+<key>.

- Ctrl+Alt+W → Ctrl+W (nano search, delete word)
- Ctrl+Alt+T → Ctrl+T (transpose chars)
- Ctrl+Alt+N → Ctrl+N (next line)
- Ctrl+Alt+Q → Ctrl+Q (XON flow control)

Fixes Termix-SSH/Support#407

* feat: remove locales

* New Crowdin updates (#504)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* feat: add option to disable update checker (#502)

* feat: add option to disable update checker

Add a new setting in User Profile > Settings to disable automatic
update checking on startup and dashboard.

- Adds 'Disable Update Check' toggle in profile settings
- Skips GitHub API calls when disabled (reduces network requests)
- Works for both web app and Electron client

Fixes Termix-SSH/Support#410

* feat: remove locales

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* New Crowdin updates (#505)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* feat: add crowdin i18n

* feat: remove locales

* New Crowdin updates (#506)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* feat: update readme

* feat: update readme

* feat: update credential editor to use submitting system and add health monitor

* feat: added toggle for command pallete

* feat: added close button on tab dropdown

* feat: added sidebar management and improved some host manager UI/UX

* feat: re-added missing users.ts route from merge

* feat: add toggle for password reset feature in admin settings (#508)

* feat: add sudo support for file manager operations (#509)

* fix: add sudo support for listFiles and improve permission error handling (#512)

* feat: add sudo support for file manager operations

* fix: add sudo support for listFiles and improve permission error handling

---------

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

* feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated

* feat: add copy password button and fixed new line carriage issues and backend crash for auth key

* feat: added quick connection system (ad-hoc)

* Enter Key for Quick Login (#513)

* feat: added -r and -l support for tunnels

* feat: begin dashboard overhaul by splitting into cards and adding customization

* feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc.

* feat: add auth.tsx suppot for fullscreen

* feat: greatly improve network graph ui/ux and migrated to use translations and theme system

* feat: update to use blacksmith

* feat: improve ui for customized tabs and hide add/edit host/credential when submiting

* feat: add warpgate support with a dialog (terminal only)

* feat: expand warpgate to docker/file manager

* fix: docker not working wtih warpgate and none auth failing for terminal

* fix: prevent owner permission loss when sharing host to own role (#514)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* fix: prevent owner permission loss when sharing host to own role

Fixes #391

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: SSH key passphrase not passed for Docker and Tunnel (#521)

* perf: optimize Host Manager for large host lists

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

* fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel

Database field is `key_password` but code used `keyPassword`.
Added fallback to check both field names.

Affected:
- docker.ts: Docker SSH connections with encrypted keys
- tunnel.ts: Tunnel connections with encrypted keys

---------

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

* perf: optimize Host Manager for large host lists (#520)

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

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

* fix: add missing mimeTypes definition for image preview (#518)

Fixes Termix-SSH/Support#408

* fix: prevent session reset when updating host properties (#517)

Move WebSocket cleanup logic to a separate unmount-only effect to
prevent SSH sessions from being closed when host properties are updated.

Closes Termix-SSH/Support#401

* fix: backend type error

* feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385)

* feat: improved conneciton log ui/logic

* feat: improved conneciton log ui/logic

* feat: expanded connection log to work across all components (readying for release)

* feat: update readme

* feat: update readme

* fix: build error

* fix: build error

* fix: changed ver

* chore: clean up

* chore: continue clean up

* fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies

* fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field

* fix: update readme and run cleaner

* fix: update readme

* fix: update readme

* fix: update readme

* feat: update chinese readme

* feat: support OIDC configuration via environment variables

Add support for configuring OIDC authentication through environment
variables, enabling containerized deployments without database access:

- OIDC_CLIENT_ID
- OIDC_CLIENT_SECRET
- OIDC_ISSUER_URL
- OIDC_AUTHORIZATION_URL
- OIDC_TOKEN_URL
- OIDC_USERINFO_URL (optional)
- OIDC_IDENTIFIER_PATH (optional, default: "sub")
- OIDC_NAME_PATH (optional, default: "name")
- OIDC_SCOPES (optional, default: "openid email profile")

Environment variables take priority over database configuration.

Closes Termix-SSH/Support#16

---------

Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>

* feat: add modern DH group KEX algorithms for better compatibility (#530)

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to open terminal directly (#156) (#503)

* fix: resolve merge conflict artifacts in dev-1.10.1

- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts

* feat: support URL routes to open terminal directly (#156)

- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found

---------

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

* feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501)

Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal.
This adds Ctrl+Alt+<key> as an alternative that sends Ctrl+<key>.

- Ctrl+Alt+W → Ctrl+W (nano search, delete word)
- Ctrl+Alt+T → Ctrl+T (transpose chars)
- Ctrl+Alt+N → Ctrl+N (next line)
- Ctrl+Alt+Q → Ctrl+Q (XON flow control)

Fixes Termix-SSH/Support#407

* feat: remove locales

* New Crowdin updates (#504)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* feat: add option to disable update checker (#502)

* feat: add option to disable update checker

Add a new setting in User Profile > Settings to disable automatic
update checking on startup and dashboard.

- Adds 'Disable Update Check' toggle in profile settings
- Skips GitHub API calls when disabled (reduces network requests)
- Works for both web app and Electron client

Fixes Termix-SSH/Support#410

* feat: remove locales

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* New Crowdin updates (#505)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* feat: add crowdin i18n

* feat: remove locales

* New Crowdin updates (#506)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* feat: update readme

* feat: update readme

* feat: update credential editor to use submitting system and add health monitor

* feat: added toggle for command pallete

* feat: added close button on tab dropdown

* feat: added sidebar management and improved some host manager UI/UX

* feat: re-added missing users.ts route from merge

* feat: add toggle for password reset feature in admin settings (#508)

* feat: add sudo support for file manager operations (#509)

* fix: add sudo support for listFiles and improve permission error handling (#512)

* feat: add sudo support for file manager operations

* fix: add sudo support for listFiles and improve permission error handling

---------

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

* feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated

* feat: add copy password button and fixed new line carriage issues and backend crash for auth key

* feat: added quick connection system (ad-hoc)

* Enter Key for Quick Login (#513)

* feat: added -r and -l support for tunnels

* feat: begin dashboard overhaul by splitting into cards and adding customization

* feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc.

* feat: add auth.tsx suppot for fullscreen

* feat: greatly improve network graph ui/ux and migrated to use translations and theme system

* feat: update to use blacksmith

* feat: improve ui for customized tabs and hide add/edit host/credential when submiting

* feat: add warpgate support with a dialog (terminal only)

* feat: expand warpgate to docker/file manager

* fix: docker not working wtih warpgate and none auth failing for terminal

* fix: prevent owner permission loss when sharing host to own role (#514)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* fix: prevent owner permission loss when sharing host to own role

Fixes #391

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: SSH key passphrase not passed for Docker and Tunnel (#521)

* perf: optimize Host Manager for large host lists

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

* fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel

Database field is `key_password` but code used `keyPassword`.
Added fallback to check both field names.

Affected:
- docker.ts: Docker SSH connections with encrypted keys
- tunnel.ts: Tunnel connections with encrypted keys

---------

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

* perf: optimize Host Manager for large host lists (#520)

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

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

* fix: add missing mimeTypes definition for image preview (#518)

Fixes Termix-SSH/Support#408

* fix: prevent session reset when updating host properties (#517)

Move WebSocket cleanup logic to a separate unmount-only effect to
prevent SSH sessions from being closed when host properties are updated.

Closes Termix-SSH/Support#401

* fix: backend type error

* feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385)

* feat: improved conneciton log ui/logic

* feat: improved conneciton log ui/logic

* feat: expanded connection log to work across all components (readying for release)

* feat: update readme

* feat: update readme

* fix: build error

* fix: build error

* fix: changed ver

* chore: clean up

* chore: continue clean up

* fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies

* fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field

* fix: update readme and run cleaner

* fix: update readme

* fix: update readme

* fix: update readme

* feat: update chinese readme

* feat: add modern DH group KEX algorithms for better compatibility

Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms
which are supported by ssh2 library but were not configured in Termix.

These algorithms provide:
- Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.)
- Stronger security with larger DH groups
- RFC 8268 compliance

Related to Termix-SSH/Support#205

---------

Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>

* feat: clarify that hostname/FQDN is supported in IP address field (#529)

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL r…

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

* Temporary merge for 1.11.1 syncing (#543)

* fix: remote translations

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

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to open terminal directly (#156) (#503)

* fix: resolve merge conflict artifacts in dev-1.10.1

- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts

* feat: support URL routes to open terminal directly (#156)

- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found

---------

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

* feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501)

Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal.
This adds Ctrl+Alt+<key> as an alternative that sends Ctrl+<key>.

- Ctrl+Alt+W → Ctrl+W (nano search, delete word)
- Ctrl+Alt+T → Ctrl+T (transpose chars)
- Ctrl+Alt+N → Ctrl+N (next line)
- Ctrl+Alt+Q → Ctrl+Q (XON flow control)

Fixes Termix-SSH/Support#407

* feat: remove locales

* New Crowdin updates (#504)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* feat: add option to disable update checker (#502)

* feat: add option to disable update checker

Add a new setting in User Profile > Settings to disable automatic
update checking on startup and dashboard.

- Adds 'Disable Update Check' toggle in profile settings
- Skips GitHub API calls when disabled (reduces network requests)
- Works for both web app and Electron client

Fixes Termix-SSH/Support#410

* feat: remove locales

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* New Crowdin updates (#505)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* feat: add crowdin i18n

* feat: remove locales

* New Crowdin updates (#506)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* feat: update readme

* feat: update readme

* feat: update credential editor to use submitting system and add health monitor

* feat: added toggle for command pallete

* feat: added close button on tab dropdown

* feat: added sidebar management and improved some host manager UI/UX

* feat: re-added missing users.ts route from merge

* feat: add toggle for password reset feature in admin settings (#508)

* feat: add sudo support for file manager operations (#509)

* fix: add sudo support for listFiles and improve permission error handling (#512)

* feat: add sudo support for file manager operations

* fix: add sudo support for listFiles and improve permission error handling

---------

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

* feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated

* feat: add copy password button and fixed new line carriage issues and backend crash for auth key

* feat: added quick connection system (ad-hoc)

* Enter Key for Quick Login (#513)

* feat: added -r and -l support for tunnels

* feat: begin dashboard overhaul by splitting into cards and adding customization

* feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc.

* feat: add auth.tsx suppot for fullscreen

* feat: greatly improve network graph ui/ux and migrated to use translations and theme system

* feat: update to use blacksmith

* feat: improve ui for customized tabs and hide add/edit host/credential when submiting

* feat: add warpgate support with a dialog (terminal only)

* feat: expand warpgate to docker/file manager

* fix: docker not working wtih warpgate and none auth failing for terminal

* fix: prevent owner permission loss when sharing host to own role (#514)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* fix: prevent owner permission loss when sharing host to own role

Fixes #391

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: SSH key passphrase not passed for Docker and Tunnel (#521)

* perf: optimize Host Manager for large host lists

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

* fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel

Database field is `key_password` but code used `keyPassword`.
Added fallback to check both field names.

Affected:
- docker.ts: Docker SSH connections with encrypted keys
- tunnel.ts: Tunnel connections with encrypted keys

---------

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

* perf: optimize Host Manager for large host lists (#520)

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

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

* fix: add missing mimeTypes definition for image preview (#518)

Fixes Termix-SSH/Support#408

* fix: prevent session reset when updating host properties (#517)

Move WebSocket cleanup logic to a separate unmount-only effect to
prevent SSH sessions from being closed when host properties are updated.

Closes Termix-SSH/Support#401

* fix: backend type error

* feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385)

* feat: improved conneciton log ui/logic

* feat: improved conneciton log ui/logic

* feat: expanded connection log to work across all components (readying for release)

* feat: update readme

* feat: update readme

* fix: build error

* fix: build error

* fix: changed ver

* chore: clean up

* chore: continue clean up

* fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies

* fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field

* fix: update readme and run cleaner

* fix: update readme

* fix: update readme

* fix: update readme

* feat: update chinese readme

* feat: support OIDC configuration via environment variables

Add support for configuring OIDC authentication through environment
variables, enabling containerized deployments without database access:

- OIDC_CLIENT_ID
- OIDC_CLIENT_SECRET
- OIDC_ISSUER_URL
- OIDC_AUTHORIZATION_URL
- OIDC_TOKEN_URL
- OIDC_USERINFO_URL (optional)
- OIDC_IDENTIFIER_PATH (optional, default: "sub")
- OIDC_NAME_PATH (optional, default: "name")
- OIDC_SCOPES (optional, default: "openid email profile")

Environment variables take priority over database configuration.

Closes Termix-SSH/Support#16

---------

Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>

* feat: add modern DH group KEX algorithms for better compatibility (#530)

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to open terminal directly (#156) (#503)

* fix: resolve merge conflict artifacts in dev-1.10.1

- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts

* feat: support URL routes to open terminal directly (#156)

- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found

---------

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

* feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501)

Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal.
This adds Ctrl+Alt+<key> as an alternative that sends Ctrl+<key>.

- Ctrl+Alt+W → Ctrl+W (nano search, delete word)
- Ctrl+Alt+T → Ctrl+T (transpose chars)
- Ctrl+Alt+N → Ctrl+N (next line)
- Ctrl+Alt+Q → Ctrl+Q (XON flow control)

Fixes Termix-SSH/Support#407

* feat: remove locales

* New Crowdin updates (#504)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* feat: add option to disable update checker (#502)

* feat: add option to disable update checker

Add a new setting in User Profile > Settings to disable automatic
update checking on startup and dashboard.

- Adds 'Disable Update Check' toggle in profile settings
- Skips GitHub API calls when disabled (reduces network requests)
- Works for both web app and Electron client

Fixes Termix-SSH/Support#410

* feat: remove locales

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* New Crowdin updates (#505)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* feat: add crowdin i18n

* feat: remove locales

* New Crowdin updates (#506)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* New translations en.json (Norwegian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Finnish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Bulgarian)

* New translations en.json (Indonesian)

* New translations en.json (Hindi)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* feat: update readme

* feat: update readme

* feat: update credential editor to use submitting system and add health monitor

* feat: added toggle for command pallete

* feat: added close button on tab dropdown

* feat: added sidebar management and improved some host manager UI/UX

* feat: re-added missing users.ts route from merge

* feat: add toggle for password reset feature in admin settings (#508)

* feat: add sudo support for file manager operations (#509)

* fix: add sudo support for listFiles and improve permission error handling (#512)

* feat: add sudo support for file manager operations

* fix: add sudo support for listFiles and improve permission error handling

---------

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

* feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated

* feat: add copy password button and fixed new line carriage issues and backend crash for auth key

* feat: added quick connection system (ad-hoc)

* Enter Key for Quick Login (#513)

* feat: added -r and -l support for tunnels

* feat: begin dashboard overhaul by splitting into cards and adding customization

* feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc.

* feat: add auth.tsx suppot for fullscreen

* feat: greatly improve network graph ui/ux and migrated to use translations and theme system

* feat: update to use blacksmith

* feat: improve ui for customized tabs and hide add/edit host/credential when submiting

* feat: add warpgate support with a dialog (terminal only)

* feat: expand warpgate to docker/file manager

* fix: docker not working wtih warpgate and none auth failing for terminal

* fix: prevent owner permission loss when sharing host to own role (#514)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* fix: prevent owner permission loss when sharing host to own role

Fixes #391

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: SSH key passphrase not passed for Docker and Tunnel (#521)

* perf: optimize Host Manager for large host lists

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

* fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel

Database field is `key_password` but code used `keyPassword`.
Added fallback to check both field names.

Affected:
- docker.ts: Docker SSH connections with encrypted keys
- tunnel.ts: Tunnel connections with encrypted keys

---------

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

* perf: optimize Host Manager for large host lists (#520)

- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)

Fixes performance issues when managing ~1000 hosts with status monitoring enabled.

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

* fix: add missing mimeTypes definition for image preview (#518)

Fixes Termix-SSH/Support#408

* fix: prevent session reset when updating host properties (#517)

Move WebSocket cleanup logic to a separate unmount-only effect to
prevent SSH sessions from being closed when host properties are updated.

Closes Termix-SSH/Support#401

* fix: backend type error

* feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385)

* feat: improved conneciton log ui/logic

* feat: improved conneciton log ui/logic

* feat: expanded connection log to work across all components (readying for release)

* feat: update readme

* feat: update readme

* fix: build error

* fix: build error

* fix: changed ver

* chore: clean up

* chore: continue clean up

* fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies

* fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field

* fix: update readme and run cleaner

* fix: update readme

* fix: update readme

* fix: update readme

* feat: update chinese readme

* feat: add modern DH group KEX algorithms for better compatibility

Add diffie-hellman-group15/16/17/18-sha512 key exchange algorithms
which are supported by ssh2 library but were not configured in Termix.

These algorithms provide:
- Better compatibility with modern SSH servers (FreeBSD, OpenBSD, etc.)
- Stronger security with larger DH groups
- RFC 8268 compliance

Related to Termix-SSH/Support#205

---------

Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>

* feat: clarify that hostname/FQDN is supported in IP address field (#529)

* Feature request network graph

* Fixing PR442:

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

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

* Handle enter button (#481)

* Update Crowdin configuration file

* Update Crowdin configuration file

* Update Linux Portable section with AUR link (#474)

* fix: file manager incorrectly decoding/encoding when editing files (#476)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: build error on docker (#477)

* fix: electron build errors and skip macos job

* fix: testflight submit failure

* fix: made submit job match build type

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

* Update Crowdin configuration file

* Update Crowdin configuration file

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

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

* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)

* fix: build error on docker

---------

Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>

* Increase max old space size for npm builds

* Increase Node.js memory limit in Dockerfile

* Remove NODE_OPTIONS from build commands in Dockerfile

* Change runner to blacksmith-4vcpu-ubuntu-2404

* fix: build error on docker

* Add handle on enter button;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* fix: remove top tech

* fix: update readme

* fix: prevent long container names from overflowing card (#496)

Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.

Fixes #411

* fix: use SFTP readdir for file listing to support non-Linux systems (#495)

The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.

Fixes #317

* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)

The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.

Fixes #404

* feat: add Docker container healthcheck (#493)

* fix: owner should not be marked as shared when host is shared to their role (#492)

* fix: use correct MIME types for image preview (#491)

* fix: prevent session reset when updating host properties (#490)

* fix: add shell creation timeout and improve error handling (#489)

* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)

* fix: delete all related data when removing user (#487)

* fix: nginx permission denied on restricted kernels (#486)

* fix: skip existing hosts and credentials during JSON import (#485)

Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.

This matches the existing behavior of importDismissedAlerts.

Fixes #389

* feat: add firewall status widget for server stats (#484)

* Feature: PWA (#479)

* feat: add PWA support with offline capabilities

- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching

* Update package-lock.json

* New Crowdin updates (#472)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (German)

* feat: add listening ports widget for server stats (#483)

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

* feat: fix network stats merge and add openapi jsdocs comments

* feat: add workflow/config to auto generate openapi json

* feat: remove locales

* feat: support URL routes to …

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes: Termix-SSH/Support#485

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* feat: embed backend server in Electron desktop app

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

* fix: improve electron embedded server reliability

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

* feat: system tray minimize and runtime server switching

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

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

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

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

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

* fix: resolve all ESLint errors blocking CI

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

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

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

---------

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

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

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

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

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

* fix: opkssh 404 and browser issues

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

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

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

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

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

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

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

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

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

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

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

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

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

* refactor: unify Drizzle schema property names to camelCase

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

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

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

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

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

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

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

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

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

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

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

* feat: decouple SSH session lifecycle from WebSocket connections

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

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

* fix: resolve session persistence bugs found during code review

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

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

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

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

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

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

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

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

* feat: add per-host SSH keepalive configuration

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: unify proxy and jump host connection paths

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

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

* feat: add HTTP CONNECT proxy support with mixed chain

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

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

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

* feat: add proxy connectivity test endpoint

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

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

* feat: add connection path visualization and HTTP CONNECT UI

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: backend catch error

* feat: improve selection UI

* fix: remove tab naming

* fix: squished ssh toolbar split ui buttons

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

* fix: tab context creating random context errors

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

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

* fix: desktop build error

* feat: update readme

* feat: update readme

* feat: update readme

* Update README.md (#612)

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

* feat: update readmes to fix spelling error

* feat: update readmes to fix spelling error

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

* fix: buyild erorr

* fix: buyild erorr

* fix: macos build error

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

* chore: clean up files

* fix: oidc http/https redirect issue

* fix: build error

* feat: update json import with all new fields

---------

Co-authored-by: skyam25 <simonkyam@gmail.com>
Co-authored-by: ZacharyZcR <PayasoNorahC@protonmail.com>
Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>
Co-authored-by: Deepansh Khurana <deepanshkhurana@outlook.com>
Co-authored-by: Dylan Ysmal <Xenthys@users.noreply.github.com>
Co-authored-by: TomyJan <TomyJan6@gmail.com>
Co-authored-by: ywaf <52742690+ywaf@users.noreply.github.com>
This commit is contained in:
Luke Gustafson
2026-03-08 18:02:14 -05:00
committed by GitHub
parent 44049b8031
commit 0ff03110c9
153 changed files with 10978 additions and 5141 deletions
+55 -8
View File
@@ -1,4 +1,12 @@
import React, { useState, useEffect, useCallback, useRef } from "react";
import React, {
useState,
useEffect,
useCallback,
useRef,
Component,
type ErrorInfo,
type ReactNode,
} from "react";
import { LeftSidebar } from "@/ui/desktop/navigation/LeftSidebar.tsx";
import { Dashboard } from "@/ui/desktop/apps/dashboard/Dashboard.tsx";
import { AppView } from "@/ui/desktop/navigation/AppView.tsx";
@@ -148,8 +156,9 @@ function AppContent({
if (hostIdentifier) {
const openTerminal = async () => {
try {
const { getSSHHostById, getSSHHosts } =
await import("@/ui/main-axios.ts");
const { getSSHHostById, getSSHHosts } = await import(
"@/ui/main-axios.ts"
);
let host = null;
if (/^\d+$/.test(hostIdentifier)) {
@@ -616,16 +625,54 @@ function AppContent({
);
}
class TabErrorBoundary extends Component<
{ children: ReactNode },
{ hasError: boolean; errorCount: number }
> {
constructor(props: { children: ReactNode }) {
super(props);
this.state = { hasError: false, errorCount: 0 };
}
static getDerivedStateFromError(error: Error) {
if (error.message?.includes("useTabs must be used within a TabProvider")) {
return { hasError: true };
}
throw error;
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
if (error.message?.includes("useTabs must be used within a TabProvider")) {
console.warn(
"TabProvider mounting race condition detected, recovering...",
);
this.setState((prev) => ({ errorCount: prev.errorCount + 1 }));
setTimeout(() => {
this.setState({ hasError: false });
}, 0);
}
}
render() {
if (this.state.hasError) {
return null;
}
return this.props.children;
}
}
function DesktopApp() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
return (
<TabProvider>
<ServerStatusProvider isAuthenticated={isAuthenticated}>
<CommandHistoryProvider>
<AppContent onAuthStateChange={setIsAuthenticated} />
</CommandHistoryProvider>
</ServerStatusProvider>
<TabErrorBoundary>
<ServerStatusProvider isAuthenticated={isAuthenticated}>
<CommandHistoryProvider>
<AppContent onAuthStateChange={setIsAuthenticated} />
</CommandHistoryProvider>
</ServerStatusProvider>
</TabErrorBoundary>
</TabProvider>
);
}
@@ -61,6 +61,7 @@ export function AdminSettings({
name_path: "name",
scopes: "openid email profile",
userinfo_url: "",
allowed_users: "",
});
const [users, setUsers] = React.useState<
@@ -31,15 +31,22 @@ export function DatabaseSecurityTab({
[currentUser?.is_oidc],
);
const handleExportDatabase = async () => {
if (!showPasswordInput) {
setShowPasswordInput(true);
return;
}
const requiresExportPassword = React.useMemo(
() => !currentUser?.is_oidc,
[currentUser?.is_oidc],
);
if (!exportPassword.trim()) {
toast.error(t("admin.passwordRequired"));
return;
const handleExportDatabase = async () => {
if (requiresExportPassword) {
if (!showPasswordInput) {
setShowPasswordInput(true);
return;
}
if (!exportPassword.trim()) {
toast.error(t("admin.passwordRequired"));
return;
}
}
setExportLoading(true);
@@ -65,7 +72,9 @@ export function DatabaseSecurityTab({
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({ password: exportPassword }),
body: JSON.stringify(
requiresExportPassword ? { password: exportPassword } : {},
),
});
if (response.ok) {
@@ -211,7 +220,7 @@ export function DatabaseSecurityTab({
<p className="text-xs text-muted-foreground">
{t("admin.exportDescription")}
</p>
{showPasswordInput && (
{showPasswordInput && requiresExportPassword && (
<div className="space-y-2">
<Label htmlFor="export-password">Password</Label>
<PasswordInput
@@ -234,11 +243,11 @@ export function DatabaseSecurityTab({
>
{exportLoading
? t("admin.exporting")
: showPasswordInput
: showPasswordInput && requiresExportPassword
? t("admin.confirmExport")
: t("admin.export")}
</Button>
{showPasswordInput && (
{showPasswordInput && requiresExportPassword && (
<Button
variant="outline"
onClick={() => {
@@ -1,5 +1,13 @@
import React from "react";
import { Checkbox } from "@/components/ui/checkbox.tsx";
import { Input } from "@/components/ui/input.tsx";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select.tsx";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useConfirmation } from "@/hooks/use-confirmation.ts";
@@ -7,6 +15,8 @@ import {
updateRegistrationAllowed,
updatePasswordLoginAllowed,
updatePasswordResetAllowed,
getGlobalMonitoringSettings,
updateGlobalMonitoringSettings,
} from "@/ui/main-axios.ts";
interface GeneralSettingsTabProps {
@@ -41,6 +51,69 @@ export function GeneralSettingsTab({
const [passwordLoginLoading, setPasswordLoginLoading] = React.useState(false);
const [passwordResetLoading, setPasswordResetLoading] = React.useState(false);
// Global monitoring defaults
const [statusInterval, setStatusInterval] = React.useState(60);
const [metricsInterval, setMetricsInterval] = React.useState(30);
const [statusUnit, setStatusUnit] = React.useState<"seconds" | "minutes">(
"seconds",
);
const [metricsUnit, setMetricsUnit] = React.useState<"seconds" | "minutes">(
"seconds",
);
const [monitoringLoading, setMonitoringLoading] = React.useState(false);
React.useEffect(() => {
getGlobalMonitoringSettings()
.then((data) => {
setStatusInterval(data.statusCheckInterval);
setMetricsInterval(data.metricsInterval);
})
.catch(() => {
// Use defaults silently
});
}, []);
const saveMonitoringDebounce = React.useRef<NodeJS.Timeout | null>(null);
const saveMonitoringSettings = React.useCallback(
(newStatus: number, newMetrics: number) => {
if (saveMonitoringDebounce.current) {
clearTimeout(saveMonitoringDebounce.current);
}
saveMonitoringDebounce.current = setTimeout(async () => {
setMonitoringLoading(true);
try {
await updateGlobalMonitoringSettings({
statusCheckInterval: newStatus,
metricsInterval: newMetrics,
});
toast.success(t("admin.globalSettingsSaved"));
} catch {
toast.error(t("admin.failedToSaveGlobalSettings"));
} finally {
setMonitoringLoading(false);
}
}, 800);
},
[t],
);
const handleStatusIntervalChange = (value: string) => {
const num = parseInt(value) || 0;
const seconds = statusUnit === "minutes" ? num * 60 : num;
const clamped = Math.max(5, Math.min(3600, seconds));
setStatusInterval(clamped);
saveMonitoringSettings(clamped, metricsInterval);
};
const handleMetricsIntervalChange = (value: string) => {
const num = parseInt(value) || 0;
const seconds = metricsUnit === "minutes" ? num * 60 : num;
const clamped = Math.max(5, Math.min(3600, seconds));
setMetricsInterval(clamped);
saveMonitoringSettings(statusInterval, clamped);
};
const handleToggleRegistration = async (checked: boolean) => {
setRegLoading(true);
try {
@@ -113,42 +186,127 @@ export function GeneralSettingsTab({
};
return (
<div className="rounded-lg border-2 border-border bg-card p-4 space-y-4">
<h3 className="text-lg font-semibold">{t("admin.userRegistration")}</h3>
<label className="flex items-center gap-2">
<Checkbox
checked={allowRegistration}
onCheckedChange={handleToggleRegistration}
disabled={regLoading || !allowPasswordLogin}
/>
{t("admin.allowNewAccountRegistration")}
{!allowPasswordLogin && (
<span className="text-xs text-muted-foreground">
({t("admin.requiresPasswordLogin")})
</span>
)}
</label>
<label className="flex items-center gap-2">
<Checkbox
checked={allowPasswordLogin}
onCheckedChange={handleTogglePasswordLogin}
disabled={passwordLoginLoading}
/>
{t("admin.allowPasswordLogin")}
</label>
<label className="flex items-center gap-2">
<Checkbox
checked={allowPasswordReset}
onCheckedChange={handleTogglePasswordReset}
disabled={passwordResetLoading || !allowPasswordLogin}
/>
{t("admin.allowPasswordReset")}
{!allowPasswordLogin && (
<span className="text-xs text-muted-foreground">
({t("admin.requiresPasswordLogin")})
</span>
)}
</label>
<div className="space-y-6">
<div className="rounded-lg border-2 border-border bg-card p-4 space-y-4">
<h3 className="text-lg font-semibold">{t("admin.userRegistration")}</h3>
<label className="flex items-center gap-2">
<Checkbox
checked={allowRegistration}
onCheckedChange={handleToggleRegistration}
disabled={regLoading || !allowPasswordLogin}
/>
{t("admin.allowNewAccountRegistration")}
{!allowPasswordLogin && (
<span className="text-xs text-muted-foreground">
({t("admin.requiresPasswordLogin")})
</span>
)}
</label>
<label className="flex items-center gap-2">
<Checkbox
checked={allowPasswordLogin}
onCheckedChange={handleTogglePasswordLogin}
disabled={passwordLoginLoading}
/>
{t("admin.allowPasswordLogin")}
</label>
<label className="flex items-center gap-2">
<Checkbox
checked={allowPasswordReset}
onCheckedChange={handleTogglePasswordReset}
disabled={passwordResetLoading || !allowPasswordLogin}
/>
{t("admin.allowPasswordReset")}
{!allowPasswordLogin && (
<span className="text-xs text-muted-foreground">
({t("admin.requiresPasswordLogin")})
</span>
)}
</label>
</div>
<div className="rounded-lg border-2 border-border bg-card p-4 space-y-4">
<h3 className="text-lg font-semibold">
{t("admin.monitoringDefaults")}
</h3>
<p className="text-sm text-muted-foreground">
{t("admin.monitoringDefaultsDesc")}
</p>
<div className="space-y-3">
<div>
<label className="text-sm font-medium">
{t("admin.globalStatusCheckInterval")}
</label>
<div className="flex gap-2 mt-1">
<Input
type="number"
value={
statusUnit === "minutes"
? Math.round(statusInterval / 60)
: statusInterval
}
onChange={(e) => handleStatusIntervalChange(e.target.value)}
disabled={monitoringLoading}
className="flex-1"
/>
<Select
value={statusUnit}
onValueChange={(value: "seconds" | "minutes") => {
setStatusUnit(value);
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="seconds">
{t("hosts.intervalSeconds")}
</SelectItem>
<SelectItem value="minutes">
{t("hosts.intervalMinutes")}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<label className="text-sm font-medium">
{t("admin.globalMetricsInterval")}
</label>
<div className="flex gap-2 mt-1">
<Input
type="number"
value={
metricsUnit === "minutes"
? Math.round(metricsInterval / 60)
: metricsInterval
}
onChange={(e) => handleMetricsIntervalChange(e.target.value)}
disabled={monitoringLoading}
className="flex-1"
/>
<Select
value={metricsUnit}
onValueChange={(value: "seconds" | "minutes") => {
setMetricsUnit(value);
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="seconds">
{t("hosts.intervalSeconds")}
</SelectItem>
<SelectItem value="minutes">
{t("hosts.intervalMinutes")}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -7,6 +7,7 @@ import { Label } from "@/components/ui/label.tsx";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useConfirmation } from "@/hooks/use-confirmation.ts";
import { Textarea } from "@/components/ui/textarea.tsx";
import { updateOIDCConfig, disableOIDCConfig } from "@/ui/main-axios.ts";
interface OIDCSettingsTabProps {
@@ -21,6 +22,7 @@ interface OIDCSettingsTabProps {
name_path: string;
scopes: string;
userinfo_url: string;
allowed_users: string;
};
setOidcConfig: React.Dispatch<
React.SetStateAction<{
@@ -33,6 +35,7 @@ interface OIDCSettingsTabProps {
name_path: string;
scopes: string;
userinfo_url: string;
allowed_users: string;
}>
>;
}
@@ -103,6 +106,7 @@ export function OIDCSettingsTab({
name_path: "",
scopes: "",
userinfo_url: "",
allowed_users: "",
};
setOidcConfig(emptyConfig);
setOidcError(null);
@@ -137,6 +141,7 @@ export function OIDCSettingsTab({
name_path: "",
scopes: "",
userinfo_url: "",
allowed_users: "",
};
setOidcConfig(emptyConfig);
setOidcError(null);
@@ -300,6 +305,21 @@ export function OIDCSettingsTab({
placeholder="https://your-provider.com/application/o/userinfo/"
/>
</div>
<div className="space-y-2">
<Label htmlFor="allowed_users">{t("admin.allowedUsers")}</Label>
<p className="text-xs text-muted-foreground">
{t("admin.allowedUsersDescription")}
</p>
<Textarea
id="allowed_users"
value={oidcConfig.allowed_users}
onChange={(e) =>
handleOIDCConfigChange("allowed_users", e.target.value)
}
placeholder={t("placeholders.allowedUsers")}
rows={3}
/>
</div>
<div className="flex gap-2 pt-2">
<Button type="submit" className="flex-1" disabled={oidcLoading}>
{oidcLoading ? t("admin.saving") : t("admin.saveConfiguration")}
@@ -592,7 +592,7 @@ function DockerManagerInner({
<div className="h-full w-full flex flex-col">
<div className="flex-1 overflow-hidden min-h-0 relative">
<SimpleLoader
visible={true && !isConnectionLogExpanded}
visible={!isConnectionLogExpanded}
message={
isValidating ? t("docker.validating") : t("docker.connecting")
}
@@ -2,6 +2,7 @@ import React from "react";
import { useXTerm } from "react-xtermjs";
import { FitAddon } from "@xterm/addon-fit";
import { ClipboardAddon } from "@xterm/addon-clipboard";
import { RobustClipboardProvider } from "@/lib/clipboard-provider";
import { WebLinksAddon } from "@xterm/addon-web-links";
import { Button } from "@/components/ui/button.tsx";
import {
@@ -48,7 +49,8 @@ export function ConsoleTerminal({
if (!terminal) return;
const fitAddon = new FitAddon();
const clipboardAddon = new ClipboardAddon();
const clipboardProvider = new RobustClipboardProvider();
const clipboardAddon = new ClipboardAddon(undefined, clipboardProvider);
const webLinksAddon = new WebLinksAddon();
fitAddonRef.current = fitAddon;
@@ -61,6 +63,94 @@ export function ConsoleTerminal({
terminal.options.fontSize = 14;
terminal.options.fontFamily = "monospace";
terminal.attachCustomKeyEventHandler((e: KeyboardEvent): boolean => {
if (e.type !== "keydown") return true;
if (
((e.ctrlKey && !e.altKey && !e.metaKey) ||
(e.metaKey && !e.ctrlKey && !e.altKey)) &&
e.key.toLowerCase() === "v"
) {
e.preventDefault();
e.stopPropagation();
navigator.clipboard
.readText()
.then((text) => {
if (text) terminal.paste(text);
})
.catch(() => {
toast.error(t("terminal.clipboardReadFailed"));
});
return false;
}
if (
e.ctrlKey &&
!e.shiftKey &&
!e.altKey &&
!e.metaKey &&
e.key.toLowerCase() === "c" &&
terminal.hasSelection()
) {
e.preventDefault();
e.stopPropagation();
const selection = terminal.getSelection();
if (selection) {
navigator.clipboard.writeText(selection).catch(() => {
toast.error(t("terminal.clipboardWriteFailed"));
});
terminal.clearSelection();
}
return false;
}
if (
((e.ctrlKey &&
e.shiftKey &&
!e.altKey &&
!e.metaKey &&
e.key.toLowerCase() === "c") ||
(e.ctrlKey &&
!e.shiftKey &&
!e.altKey &&
!e.metaKey &&
e.key === "Insert")) &&
terminal.hasSelection()
) {
e.preventDefault();
e.stopPropagation();
const selection = terminal.getSelection();
if (selection) {
navigator.clipboard.writeText(selection).catch(() => {
toast.error(t("terminal.clipboardWriteFailed"));
});
}
return false;
}
if (
e.shiftKey &&
!e.ctrlKey &&
!e.altKey &&
!e.metaKey &&
e.key === "Insert"
) {
e.preventDefault();
e.stopPropagation();
navigator.clipboard
.readText()
.then((text) => {
if (text) terminal.paste(text);
})
.catch(() => {
toast.error(t("terminal.clipboardReadFailed"));
});
return false;
}
return true;
});
const backgroundColor = getComputedStyle(document.documentElement)
.getPropertyValue("--bg-elevated")
.trim();
@@ -97,6 +187,7 @@ export function ConsoleTerminal({
return () => {
window.removeEventListener("resize", resizeHandler);
clipboardProvider.dispose();
if (wsRef.current) {
try {
@@ -37,7 +37,16 @@ import {
Search,
Grid3X3,
List,
ArrowUpDown,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu.tsx";
import { TerminalWindow } from "./components/TerminalWindow.tsx";
import type { SSHHost, FileItem } from "../../../types/index.js";
import {
@@ -48,6 +57,7 @@ import { ConnectionLog } from "@/ui/desktop/navigation/connection-log/Connection
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
import {
listSSHFiles,
resolveSSHPath,
uploadSSHFile,
downloadSSHFile,
createSSHFile,
@@ -130,6 +140,16 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
const saved = localStorage.getItem("fileManagerViewMode");
return saved === "grid" || saved === "list" ? saved : "grid";
});
const [sortBy, setSortBy] = useState<"name" | "modified" | "size">(() => {
const saved = localStorage.getItem("fileManagerSortBy");
return saved === "name" || saved === "modified" || saved === "size"
? saved
: "name";
});
const [sortOrder, setSortOrder] = useState<"asc" | "desc">(() => {
const saved = localStorage.getItem("fileManagerSortOrder");
return saved === "asc" || saved === "desc" ? saved : "asc";
});
const [totpRequired, setTotpRequired] = useState(false);
const [totpSessionId, setTotpSessionId] = useState<string | null>(null);
const [totpPrompt, setTotpPrompt] = useState<string>("");
@@ -489,15 +509,24 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
return false;
}
currentLoadingPathRef.current = path;
let resolvedPath = path;
if (path.includes("$") || path.startsWith("~")) {
resolvedPath = await resolveSSHPath(sshSessionId, path);
if (resolvedPath !== path) {
setCurrentPath(resolvedPath);
lastPathChangeRef.current = resolvedPath;
}
}
currentLoadingPathRef.current = resolvedPath;
setIsLoading(true);
setCreateIntent(null);
try {
const response = await listSSHFiles(sshSessionId, path);
const response = await listSSHFiles(sshSessionId, resolvedPath);
if (currentLoadingPathRef.current !== path) {
if (currentLoadingPathRef.current !== resolvedPath) {
return false;
}
@@ -509,7 +538,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
clearSelection();
return true;
} catch (error: unknown) {
if (currentLoadingPathRef.current === path) {
if (currentLoadingPathRef.current === resolvedPath) {
const axiosError = error as {
response?: {
status?: number;
@@ -524,7 +553,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
if (axiosError.response?.data?.needsSudo) {
if (!sudoDialogOpen) {
setPendingSudoOperation({ type: "navigate", path });
setPendingSudoOperation({ type: "navigate", path: resolvedPath });
setSudoDialogOpen(true);
}
@@ -560,7 +589,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
}
return false;
} finally {
if (currentLoadingPathRef.current === path) {
if (currentLoadingPathRef.current === resolvedPath) {
setIsLoading(false);
currentLoadingPathRef.current = "";
}
@@ -2167,6 +2196,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
localStorage.setItem("fileManagerViewMode", viewMode);
}, [viewMode]);
useEffect(() => {
localStorage.setItem("fileManagerSortBy", sortBy);
localStorage.setItem("fileManagerSortOrder", sortOrder);
}, [sortBy, sortOrder]);
const filteredFiles = useMemo(
() =>
files
@@ -2176,12 +2210,25 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
.sort((a, b) => {
if (a.type === "directory" && b.type !== "directory") return -1;
if (a.type !== "directory" && b.type === "directory") return 1;
return a.name.localeCompare(b.name, undefined, {
numeric: true,
sensitivity: "base",
});
let result = 0;
switch (sortBy) {
case "name":
result = a.name.localeCompare(b.name, undefined, {
numeric: true,
sensitivity: "base",
});
break;
case "modified":
result = (a.modified || "").localeCompare(b.modified || "");
break;
case "size":
result = (a.size || 0) - (b.size || 0);
break;
}
return sortOrder === "desc" ? -result : result;
}),
[files, searchQuery],
[files, searchQuery, sortBy, sortOrder],
);
if (!currentHost) {
@@ -2245,6 +2292,44 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
</Button>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-9">
<ArrowUpDown className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuRadioGroup
value={sortBy}
onValueChange={(v) =>
setSortBy(v as "name" | "modified" | "size")
}
>
<DropdownMenuRadioItem value="name">
{t("fileManager.sortByName")}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="modified">
{t("fileManager.sortByDate")}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="size">
{t("fileManager.sortBySize")}
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={sortOrder}
onValueChange={(v) => setSortOrder(v as "asc" | "desc")}
>
<DropdownMenuRadioItem value="asc">
{t("fileManager.ascending")}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="desc">
{t("fileManager.descending")}
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant="outline"
size="sm"
@@ -2321,6 +2406,16 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
onPathChange={setCurrentPath}
onRefresh={handleRefreshDirectory}
onUpload={handleFilesDropped}
sortBy={sortBy}
sortOrder={sortOrder}
onSortChange={(field) => {
if (field === sortBy) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
} else {
setSortBy(field);
setSortOrder("asc");
}
}}
onDownload={(files) => files.forEach(handleDownloadFile)}
onContextMenu={handleContextMenu}
viewMode={viewMode}
@@ -17,6 +17,7 @@ import {
ChevronRight,
RefreshCw,
ArrowUp,
ArrowDown,
FileSymlink,
Move,
GitCompare,
@@ -95,6 +96,9 @@ interface FileManagerGridProps {
onCancelCreate?: () => void;
onNewFile?: () => void;
onNewFolder?: () => void;
sortBy?: "name" | "modified" | "size";
sortOrder?: "asc" | "desc";
onSortChange?: (field: "name" | "modified" | "size") => void;
}
const getFileTypeColor = (file: FileItem): string => {
@@ -215,6 +219,9 @@ export function FileManagerGrid({
onCancelCreate,
onNewFile,
onNewFolder,
sortBy,
sortOrder,
onSortChange,
}: FileManagerGridProps) {
const { t } = useTranslation();
const gridRef = useRef<HTMLDivElement>(null);
@@ -441,9 +448,13 @@ export function FileManagerGrid({
const confirmEditingPath = () => {
const trimmedPath = editPathValue.trim();
if (trimmedPath) {
const normalizedPath = trimmedPath.startsWith("/")
const needsExpansion =
trimmedPath.startsWith("~") || trimmedPath.includes("$");
const normalizedPath = needsExpansion
? trimmedPath
: "/" + trimmedPath;
: trimmedPath.startsWith("/")
? trimmedPath
: "/" + trimmedPath;
onPathChange(normalizedPath);
}
setIsEditingPath(false);
@@ -1128,6 +1139,48 @@ export function FileManagerGrid({
</div>
) : (
<div className="space-y-1">
<div className="flex items-center gap-3 p-2 border-b border-edge text-xs text-muted-foreground font-medium sticky top-0 bg-card z-10">
<div className="flex-shrink-0 w-5" />
<div
className="flex-1 min-w-0 flex items-center gap-1 cursor-pointer hover:text-foreground"
onClick={() => onSortChange?.("name")}
>
{t("fileManager.name")}
{sortBy === "name" &&
(sortOrder === "asc" ? (
<ArrowUp className="w-3 h-3" />
) : (
<ArrowDown className="w-3 h-3" />
))}
</div>
<div
className="flex-shrink-0 w-32 flex items-center gap-1 cursor-pointer hover:text-foreground"
onClick={() => onSortChange?.("modified")}
>
{t("fileManager.modified")}
{sortBy === "modified" &&
(sortOrder === "asc" ? (
<ArrowUp className="w-3 h-3" />
) : (
<ArrowDown className="w-3 h-3" />
))}
</div>
<div
className="flex-shrink-0 w-16 flex items-center gap-1 cursor-pointer hover:text-foreground justify-end"
onClick={() => onSortChange?.("size")}
>
{t("fileManager.size")}
{sortBy === "size" &&
(sortOrder === "asc" ? (
<ArrowUp className="w-3 h-3" />
) : (
<ArrowDown className="w-3 h-3" />
))}
</div>
<div className="flex-shrink-0 w-20 text-right">
{t("fileManager.permissions")}
</div>
</div>
{createIntent && (
<CreateIntentListItem
intent={createIntent}
@@ -1202,6 +1255,9 @@ export function FileManagerGrid({
{file.linkTarget}
</p>
)}
</div>
<div className="flex-shrink-0 w-32">
{file.modified && (
<p className="text-xs text-muted-foreground">
{file.modified}
@@ -1209,7 +1265,7 @@ export function FileManagerGrid({
)}
</div>
<div className="flex-shrink-0 text-right">
<div className="flex-shrink-0 w-16 text-right">
{file.type === "file" &&
file.size !== undefined &&
file.size !== null && (
@@ -21,7 +21,6 @@ import {
SiJavascript,
SiTypescript,
SiPython,
SiOracle,
SiCplusplus,
SiC,
SiDotnet,
@@ -30,14 +29,7 @@ import {
SiGo,
SiRust,
SiHtml5,
SiCss3,
SiSass,
SiLess,
SiJson,
SiXml,
SiYaml,
SiToml,
SiShell,
SiVuedotjs,
SiSvelte,
SiMarkdown,
@@ -114,7 +106,7 @@ function getLanguageIcon(filename: string): React.ReactNode {
ts: <SiTypescript className="w-6 h-6 text-blue-500" />,
tsx: <SiTypescript className="w-6 h-6 text-blue-500" />,
py: <SiPython className="w-6 h-6 text-blue-400" />,
java: <SiOracle className="w-6 h-6 text-red-500" />,
java: <Code className="w-6 h-6 text-red-500" />,
cpp: <SiCplusplus className="w-6 h-6 text-blue-600" />,
c: <SiC className="w-6 h-6 text-blue-700" />,
cs: <SiDotnet className="w-6 h-6 text-purple-600" />,
@@ -123,23 +115,23 @@ function getLanguageIcon(filename: string): React.ReactNode {
go: <SiGo className="w-6 h-6 text-cyan-500" />,
rs: <SiRust className="w-6 h-6 text-orange-600" />,
html: <SiHtml5 className="w-6 h-6 text-orange-500" />,
css: <SiCss3 className="w-6 h-6 text-blue-500" />,
css: <Code className="w-6 h-6 text-blue-500" />,
scss: <SiSass className="w-6 h-6 text-pink-500" />,
sass: <SiSass className="w-6 h-6 text-pink-500" />,
less: <SiLess className="w-6 h-6 text-blue-600" />,
json: <SiJson className="w-6 h-6 text-yellow-500" />,
xml: <SiXml className="w-6 h-6 text-orange-500" />,
yaml: <SiYaml className="w-6 h-6 text-red-400" />,
yml: <SiYaml className="w-6 h-6 text-red-400" />,
toml: <SiToml className="w-6 h-6 text-orange-400" />,
less: <Code className="w-6 h-6 text-blue-600" />,
json: <Code className="w-6 h-6 text-yellow-500" />,
xml: <Code className="w-6 h-6 text-orange-500" />,
yaml: <Code className="w-6 h-6 text-red-400" />,
yml: <Code className="w-6 h-6 text-red-400" />,
toml: <Code className="w-6 h-6 text-orange-400" />,
sql: <SiMysql className="w-6 h-6 text-blue-500" />,
sh: <SiGnubash className="w-6 h-6 text-foreground" />,
bash: <SiGnubash className="w-6 h-6 text-foreground" />,
zsh: <SiShell className="w-6 h-6 text-foreground" />,
zsh: <Code className="w-6 h-6 text-foreground" />,
vue: <SiVuedotjs className="w-6 h-6 text-green-500" />,
svelte: <SiSvelte className="w-6 h-6 text-orange-500" />,
md: <SiMarkdown className="w-6 h-6 text-muted-foreground" />,
conf: <SiShell className="w-6 h-6 text-muted-foreground" />,
conf: <Code className="w-6 h-6 text-muted-foreground" />,
ini: <Code className="w-6 h-6 text-muted-foreground" />,
};
@@ -337,8 +337,6 @@ function ServerStatsInner({
}
let cancelled = false;
let intervalId: number | undefined;
const fetchStatus = async () => {
try {
const res = await getServerStatusById(currentHostConfig?.id);
@@ -364,14 +362,14 @@ function ServerStatsInner({
};
fetchStatus();
intervalId = window.setInterval(
const intervalId = window.setInterval(
fetchStatus,
statsConfig.statusCheckInterval * 1000,
);
return () => {
cancelled = true;
if (intervalId) window.clearInterval(intervalId);
window.clearInterval(intervalId);
};
}, [
currentHostConfig?.id,
@@ -386,8 +384,6 @@ function ServerStatsInner({
let cancelled = false;
let pollingIntervalId: number | undefined;
let debounceTimeout: NodeJS.Timeout | undefined;
if (isActuallyVisible && !metrics) {
setIsLoadingMetrics(true);
setShowStatsUI(true);
@@ -539,7 +535,7 @@ function ServerStatsInner({
}
};
debounceTimeout = setTimeout(() => {
const debounceTimeout = setTimeout(() => {
if (isActuallyVisible) {
if (!hasConnectionError) {
startMetrics();
@@ -551,7 +547,7 @@ function ServerStatsInner({
return () => {
cancelled = true;
if (debounceTimeout) clearTimeout(debounceTimeout);
clearTimeout(debounceTimeout);
if (pollingIntervalId) window.clearInterval(pollingIntervalId);
if (currentHostConfig?.id) {
stopMetricsPolling(currentHostConfig.id).catch(() => {});
@@ -9,6 +9,7 @@ import {
import { useXTerm } from "react-xtermjs";
import { FitAddon } from "@xterm/addon-fit";
import { ClipboardAddon } from "@xterm/addon-clipboard";
import { RobustClipboardProvider } from "@/lib/clipboard-provider";
import { Unicode11Addon } from "@xterm/addon-unicode11";
import { WebLinksAddon } from "@xterm/addon-web-links";
import { useTranslation } from "react-i18next";
@@ -45,9 +46,11 @@ import {
useConnectionLog,
} from "@/ui/desktop/navigation/connection-log/ConnectionLogContext.tsx";
import { ConnectionLog } from "@/ui/desktop/navigation/connection-log/ConnectionLog.tsx";
import { toast } from "sonner";
interface HostConfig {
id?: number;
instanceId?: string;
ip: string;
port: number;
username: string;
@@ -76,6 +79,7 @@ interface SSHTerminalProps {
showTitle?: boolean;
splitScreen?: boolean;
onClose?: () => void;
onTitleChange?: (title: string) => void;
initialPath?: string;
executeCommand?: string;
}
@@ -87,6 +91,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
isVisible,
splitScreen = false,
onClose,
onTitleChange,
initialPath,
executeCommand,
},
@@ -176,6 +181,9 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
data: any;
} | null>(null);
const sessionIdRef = useRef<string | null>(null);
const isAttachingSessionRef = useRef<boolean>(false);
const isVisibleRef = useRef<boolean>(false);
const isFittingRef = useRef(false);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -185,6 +193,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
const shouldNotReconnectRef = useRef(false);
const isReconnectingRef = useRef(false);
const isConnectingRef = useRef(false);
const wasConnectedRef = useRef(false);
useEffect(() => {
isUnmountingRef.current = false;
@@ -192,6 +201,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
isReconnectingRef.current = false;
isConnectingRef.current = false;
reconnectAttempts.current = 0;
wasConnectedRef.current = false;
isAttachingSessionRef.current = false;
return () => {};
}, [hostConfig.id]);
@@ -563,6 +574,12 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
clearTimeout(warpgateTimeoutRef.current);
warpgateTimeoutRef.current = null;
}
if (webSocketRef.current?.readyState === WebSocket.OPEN) {
webSocketRef.current.send(JSON.stringify({ type: "disconnect" }));
}
const tabId = hostConfig.id ?? "default";
localStorage.removeItem(`termix_session_${tabId}`);
sessionIdRef.current = null;
webSocketRef.current?.close();
setIsConnected(false);
setIsConnecting(false);
@@ -624,7 +641,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
isReconnectingRef.current = true;
if (terminal) {
if (terminal && !isAttachingSessionRef.current) {
terminal.clear();
}
@@ -677,7 +694,9 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
}
if (terminal && hostConfig) {
terminal.clear();
if (!isAttachingSessionRef.current) {
terminal.clear();
}
const cols = terminal.cols;
const rows = terminal.rows;
connectToHost(cols, rows);
@@ -694,6 +713,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
isConnectingRef.current = true;
connectionAttemptIdRef.current++;
wasConnectedRef.current = false;
if (!isReconnectingRef.current) {
reconnectAttempts.current = 0;
@@ -800,12 +820,38 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
currentHostIdRef.current = hostConfig.id;
currentHostConfigRef.current = hostConfig;
ws.send(
JSON.stringify({
type: "connectToHost",
data: { cols, rows, hostConfig, initialPath, executeCommand },
}),
);
const persistenceEnabled =
localStorage.getItem("enableTerminalSessionPersistence") === "true";
const tabId = hostConfig.instanceId
? `${hostConfig.id}_${hostConfig.instanceId}`
: `${hostConfig.id}_${Date.now()}`;
const savedSessionId = persistenceEnabled
? localStorage.getItem(`termix_session_${tabId}`)
: null;
if (savedSessionId && !isReconnectingRef.current) {
sessionIdRef.current = savedSessionId;
isAttachingSessionRef.current = true;
ws.send(
JSON.stringify({
type: "attachSession",
data: {
sessionId: savedSessionId,
cols,
rows,
tabInstanceId: hostConfig.instanceId,
},
}),
);
} else {
isAttachingSessionRef.current = false;
ws.send(
JSON.stringify({
type: "connectToHost",
data: { cols, rows, hostConfig, initialPath, executeCommand },
}),
);
}
terminal.onData((data) => {
trackInput(data);
ws.send(JSON.stringify({ type: "input", data }));
@@ -925,6 +971,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
setIsConnecting(false);
} else if (msg.type === "connected") {
opksshFailedRef.current = false;
wasConnectedRef.current = true;
setIsConnected(true);
setIsConnecting(false);
isConnectingRef.current = false;
@@ -1009,12 +1056,19 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
terminal.clear();
}
setIsConnecting(false);
if (
onClose &&
!connectionErrorRef.current &&
!opksshFailedRef.current
) {
onClose();
if (wasConnectedRef.current) {
wasConnectedRef.current = false;
if (
onClose &&
!connectionErrorRef.current &&
!opksshFailedRef.current
) {
onClose();
}
} else if (!connectionErrorRef.current) {
updateConnectionError(
msg.message || t("terminal.connectionRejected"),
);
}
} else if (msg.type === "totp_required") {
setTotpRequired(true);
@@ -1221,6 +1275,76 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
clearTimeout(connectionTimeoutRef.current);
connectionTimeoutRef.current = null;
}
} else if (msg.type === "sessionCreated") {
sessionIdRef.current = msg.sessionId;
const persistenceEnabled =
localStorage.getItem("enableTerminalSessionPersistence") ===
"true";
if (persistenceEnabled && hostConfig.instanceId) {
const tabId = `${hostConfig.id}_${hostConfig.instanceId}`;
localStorage.setItem(`termix_session_${tabId}`, msg.sessionId);
}
} else if (msg.type === "sessionAttached") {
isAttachingSessionRef.current = false;
opksshFailedRef.current = false;
wasConnectedRef.current = true;
setIsConnected(true);
setIsConnecting(false);
isConnectingRef.current = false;
shouldNotReconnectRef.current = false;
updateConnectionError(null);
if (connectionTimeoutRef.current) {
clearTimeout(connectionTimeoutRef.current);
connectionTimeoutRef.current = null;
}
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
reconnectAttempts.current = 0;
isReconnectingRef.current = false;
logTerminalActivity();
addLog({
type: "success",
stage: "connection",
message: t("terminal.reconnected"),
});
} else if (msg.type === "sessionExpired") {
isAttachingSessionRef.current = false;
shouldNotReconnectRef.current = false;
if (hostConfig.instanceId) {
const tabId = `${hostConfig.id}_${hostConfig.instanceId}`;
localStorage.removeItem(`termix_session_${tabId}`);
}
sessionIdRef.current = null;
if (webSocketRef.current) {
webSocketRef.current.close();
}
} else if (msg.type === "sessionTakenOver") {
if (sessionIdRef.current && hostConfig.instanceId) {
const tabId = `${hostConfig.id}_${hostConfig.instanceId}`;
localStorage.removeItem(`termix_session_${tabId}`);
sessionIdRef.current = null;
}
if (terminal) {
terminal.clear();
}
setIsConnected(false);
setIsConnecting(true);
addLog({
type: "warning",
stage: "connection",
message: t("terminal.sessionTakenOver"),
});
const cols = terminal?.cols || 80;
const rows = terminal?.rows || 24;
connectToHost(cols, rows);
} else if (msg.type === "connection_log") {
if (msg.data) {
addLog({
@@ -1290,7 +1414,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
}
if (
!isConnected &&
!wasConnectedRef.current &&
!isAttachingSessionRef.current &&
event.wasClean &&
(event.code === 1005 || event.code === 1000)
) {
@@ -1342,26 +1467,29 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
});
}
async function writeTextToClipboard(text: string): Promise<void> {
async function writeTextToClipboard(text: string): Promise<boolean> {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return;
return true;
}
} catch (error) {
console.error("Terminal operation failed:", error);
} catch {
// fall through to legacy method
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
document.execCommand("copy");
} finally {
document.body.removeChild(textarea);
return true;
} catch {
toast.error(t("terminal.clipboardWriteFailed"));
return false;
}
}
@@ -1370,8 +1498,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
if (navigator.clipboard && navigator.clipboard.readText) {
return await navigator.clipboard.readText();
}
} catch (error) {
console.error("Terminal operation failed:", error);
} catch {
toast.error(t("terminal.clipboardReadFailed"));
}
return "";
}
@@ -1521,7 +1649,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
};
const fitAddon = new FitAddon();
const clipboardAddon = new ClipboardAddon();
const clipboardProvider = new RobustClipboardProvider();
const clipboardAddon = new ClipboardAddon(undefined, clipboardProvider);
const unicode11Addon = new Unicode11Addon();
const webLinksAddon = new WebLinksAddon();
@@ -1599,6 +1728,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
return () => {
isFittingRef.current = false;
resizeObserver.disconnect();
clipboardProvider.dispose();
element?.removeEventListener("contextmenu", handleContextMenu);
element?.removeEventListener("keydown", handleBackspaceMode, true);
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
@@ -1606,24 +1736,56 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
};
}, [xtermRef, terminal, hostConfig, isDarkMode]);
const isMountedRef = useRef(false);
useEffect(() => {
isMountedRef.current = true;
const currentHostId = hostConfig.id;
const currentInstanceId = hostConfig.instanceId;
return () => {
isUnmountingRef.current = true;
shouldNotReconnectRef.current = true;
isReconnectingRef.current = false;
setIsConnecting(false);
if (reconnectTimeoutRef.current)
clearTimeout(reconnectTimeoutRef.current);
if (connectionTimeoutRef.current)
clearTimeout(connectionTimeoutRef.current);
if (totpTimeoutRef.current) clearTimeout(totpTimeoutRef.current);
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
if (!isMountedRef.current) {
return;
}
if (
currentHostIdRef.current !== currentHostId &&
currentHostIdRef.current !== null
) {
isUnmountingRef.current = true;
shouldNotReconnectRef.current = true;
isReconnectingRef.current = false;
setIsConnecting(false);
if (reconnectTimeoutRef.current)
clearTimeout(reconnectTimeoutRef.current);
if (connectionTimeoutRef.current)
clearTimeout(connectionTimeoutRef.current);
if (totpTimeoutRef.current) clearTimeout(totpTimeoutRef.current);
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
}
const persistenceEnabled =
localStorage.getItem("enableTerminalSessionPersistence") === "true";
if (
!persistenceEnabled &&
sessionIdRef.current &&
currentInstanceId
) {
const tabId = `${currentHostId}_${currentInstanceId}`;
localStorage.removeItem(`termix_session_${tabId}`);
}
if (webSocketRef.current) {
webSocketRef.current.close();
}
isMountedRef.current = false;
}
webSocketRef.current?.close();
};
}, []);
}, [hostConfig.id, hostConfig.instanceId]);
useEffect(() => {
if (!terminal) return;
@@ -1634,18 +1796,40 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
}
if (
((e.ctrlKey && !e.altKey && !e.metaKey) ||
(e.metaKey && !e.ctrlKey && !e.altKey)) &&
e.key.toLowerCase() === "v"
e.ctrlKey &&
!e.shiftKey &&
!e.altKey &&
!e.metaKey &&
e.key.toLowerCase() === "c" &&
terminal.hasSelection()
) {
e.preventDefault();
e.stopPropagation();
readTextFromClipboard().then((pasteText) => {
if (pasteText && webSocketRef.current?.readyState === 1) {
terminal.paste(pasteText);
}
});
return false;
const selection = terminal.getSelection();
if (selection) {
e.preventDefault();
e.stopPropagation();
writeTextToClipboard(selection);
terminal.clearSelection();
return false;
}
}
if (
((e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey) ||
(e.metaKey && !e.ctrlKey && !e.altKey) ||
(e.ctrlKey &&
!e.shiftKey &&
!e.altKey &&
!e.metaKey &&
e.key === "Insert")) &&
(e.key.toLowerCase() === "c" || e.key === "Insert")
) {
const selection = terminal.getSelection();
if (selection) {
e.preventDefault();
e.stopPropagation();
writeTextToClipboard(selection);
return false;
}
}
if (e.ctrlKey && e.altKey && !e.metaKey && !e.shiftKey) {
@@ -1750,6 +1934,23 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
return true;
}
if (
e.key === "Tab" &&
e.shiftKey &&
!e.ctrlKey &&
!e.altKey &&
!e.metaKey
) {
e.preventDefault();
e.stopPropagation();
if (webSocketRef.current?.readyState === 1) {
webSocketRef.current.send(
JSON.stringify({ type: "input", data: "\x1b[Z" }),
);
}
return false;
}
if (
e.key === "Tab" &&
!e.ctrlKey &&
@@ -1856,6 +2057,14 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
return;
}
if (
webSocketRef.current &&
(webSocketRef.current.readyState === WebSocket.OPEN ||
webSocketRef.current.readyState === WebSocket.CONNECTING)
) {
return;
}
if (terminal.cols < 10 || terminal.rows < 3) {
requestAnimationFrame(() => {
if (terminal.cols > 0 && terminal.rows > 0) {
@@ -1877,9 +2086,6 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
connectToHost(terminal.cols, terminal.rows);
}
});
// Note: Using hostConfig.id instead of hostConfig object to prevent
// unnecessary reconnections when host properties are updated.
// Only reconnect when switching to a different host.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [terminal, hostConfig.id, isVisible, isConnected, isConnecting]);
@@ -2121,7 +2327,7 @@ style.innerHTML = `
}
.xterm {
font-feature-settings: "liga" 1, "calt" 1;
font-feature-settings: "liga" 0, "calt" 0;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -2129,11 +2335,11 @@ style.innerHTML = `
.xterm .xterm-screen {
font-family: 'Caskaydia Cove Nerd Font Mono', 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace !important;
font-variant-ligatures: contextual;
font-variant-ligatures: none;
}
.xterm .xterm-screen .xterm-char {
font-feature-settings: "liga" 1, "calt" 1;
font-feature-settings: "liga" 0, "calt" 0;
}
`;
document.head.appendChild(style);
@@ -346,8 +346,10 @@ export function HostManagerEditor({
]),
statusCheckEnabled: z.boolean().default(true),
statusCheckInterval: z.number().min(5).max(3600).default(30),
useGlobalStatusInterval: z.boolean().default(true),
metricsEnabled: z.boolean().default(true),
metricsInterval: z.number().min(5).max(3600).default(30),
useGlobalMetricsInterval: z.boolean().default(true),
})
.default({
enabledWidgets: [
@@ -363,8 +365,10 @@ export function HostManagerEditor({
],
statusCheckEnabled: true,
statusCheckInterval: 30,
useGlobalStatusInterval: true,
metricsEnabled: true,
metricsInterval: 30,
useGlobalMetricsInterval: true,
}),
terminalConfig: z
.object({
@@ -394,6 +398,8 @@ export function HostManagerEditor({
moshCommand: z.string(),
sudoPasswordAutoFill: z.boolean(),
sudoPassword: z.string().optional(),
keepaliveInterval: z.number().min(0).max(300000).optional(),
keepaliveCountMax: z.number().min(0).max(100).optional(),
})
.optional(),
forceKeyboardInteractive: z.boolean().optional(),
@@ -482,14 +488,6 @@ export function HostManagerEditor({
path: ["credentialId"],
});
}
} else if (data.authType === "credential") {
if (!data.credentialId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("hosts.credentialRequired"),
path: ["credentialId"],
});
}
}
data.tunnelConnections.forEach((connection, index) => {
@@ -972,6 +970,14 @@ export function HostManagerEditor({
if (errorFields.length === 0) return;
const firstErrorField = errorFields[0];
const firstError = errors[firstErrorField as keyof typeof errors];
const errorMessage =
firstError && typeof firstError === "object" && "message" in firstError
? (firstError.message as string)
: t("hosts.failedToSaveHost");
toast.error(errorMessage);
for (const tab of TAB_PRIORITY) {
const hasErrorInTab = errorFields.some((field) => {
const baseField = field.split(".")[0].split("[")[0];
@@ -31,6 +31,7 @@ import {
getSSHHosts,
deleteSSHHost,
bulkImportSSHHosts,
bulkUpdateSSHHosts,
updateSSHHost,
renameFolder,
exportSSHHostWithCredentials,
@@ -80,6 +81,8 @@ import {
Container,
Link,
Plus,
ListChecks,
ChevronDown,
} from "lucide-react";
import type {
SSHHost,
@@ -87,6 +90,7 @@ import type {
SSHManagerHostViewerProps,
} from "../../../../../types";
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets.ts";
import { Checkbox } from "@/components/ui/checkbox.tsx";
import { FolderEditDialog } from "@/ui/desktop/apps/host-manager/dialogs/FolderEditDialog.tsx";
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
@@ -104,6 +108,7 @@ export function HostManagerViewer({
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [importing, setImporting] = useState(false);
const overwriteRef = useRef(false);
const [draggedHost, setDraggedHost] = useState<SSHHost | null>(null);
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
const [editingFolder, setEditingFolder] = useState<string | null>(null);
@@ -118,6 +123,12 @@ export function HostManagerViewer({
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
new Set(),
);
const [openAccordions, setOpenAccordions] = useState<string[]>([]);
const [selectionMode, setSelectionMode] = useState(false);
const [selectedHostIds, setSelectedHostIds] = useState<Set<number>>(
new Set(),
);
const [bulkUpdating, setBulkUpdating] = useState(false);
const { getStatus } = useServerStatus();
const dragCounter = useRef(0);
@@ -344,11 +355,60 @@ export function HostManagerViewer({
};
const handleEdit = (host: SSHHost) => {
if (selectionMode) return;
if (onEditHost) {
onEditHost(host);
}
};
const toggleHostSelection = (hostId: number) => {
setSelectedHostIds((prev) => {
const next = new Set(prev);
if (next.has(hostId)) next.delete(hostId);
else next.add(hostId);
return next;
});
};
const exitSelectionMode = () => {
setSelectionMode(false);
setSelectedHostIds(new Set());
};
const handleBulkUpdate = async (updates: Record<string, unknown>) => {
if (selectedHostIds.size === 0) return;
try {
setBulkUpdating(true);
const result = await bulkUpdateSSHHosts(
Array.from(selectedHostIds),
updates,
);
if (result.updated > 0) {
toast.success(t("hosts.bulkUpdateSuccess", { count: result.updated }));
}
if (result.errors.length > 0) {
toast.error(result.errors.join(", "));
}
await fetchHosts();
window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
} catch {
toast.error(t("hosts.bulkUpdateFailed"));
} finally {
setBulkUpdating(false);
}
};
const selectAllHosts = () => {
const selectableIds = hosts
.filter((h) => !(h as any).isShared)
.map((h) => h.id);
setSelectedHostIds(new Set(selectableIds));
};
const deselectAllHosts = () => {
setSelectedHostIds(new Set());
};
const handleClone = (host: SSHHost) => {
if (onEditHost) {
const clonedHost = { ...host };
@@ -541,6 +601,9 @@ export function HostManagerViewer({
enableTunnel: false,
enableFileManager: true,
enableDocker: false,
showTerminalInSidebar: true,
showFileManagerInSidebar: true,
showServerStatsInSidebar: true,
defaultPath: "/var/www",
},
{
@@ -560,6 +623,9 @@ export function HostManagerViewer({
enableTunnel: true,
enableFileManager: false,
enableDocker: false,
showTerminalInSidebar: true,
showTunnelInSidebar: true,
showServerStatsInSidebar: true,
tunnelConnections: [
{
sourcePort: 5432,
@@ -594,7 +660,11 @@ export function HostManagerViewer({
enableTunnel: false,
enableFileManager: true,
enableDocker: true,
showTerminalInSidebar: true,
showFileManagerInSidebar: true,
showDockerInSidebar: true,
defaultPath: "/home/developer",
sudoPassword: "dev_sudo_password",
},
{
name: "Jump Host Server",
@@ -727,15 +797,17 @@ export function HostManagerViewer({
throw new Error(t("hosts.maxHostsAllowed"));
}
const result = await bulkImportSSHHosts(hostsArray);
const result = await bulkImportSSHHosts(hostsArray, overwriteRef.current);
if (result.success > 0) {
toast.success(
t("hosts.importCompleted", {
success: result.success,
failed: result.failed,
}),
);
if (result.success > 0 || result.updated > 0) {
const parts: string[] = [];
if (result.success > 0)
parts.push(`${result.success} ${t("hosts.importCreated")}`);
if (result.updated > 0)
parts.push(`${result.updated} ${t("hosts.importUpdated")}`);
if (result.failed > 0)
parts.push(`${result.failed} ${t("hosts.importFailedCount")}`);
toast.success(parts.join(", "));
if (result.errors.length > 0) {
toast.error(`Import errors: ${result.errors.join(", ")}`);
}
@@ -810,6 +882,22 @@ export function HostManagerViewer({
return sortedGrouped;
}, [filteredAndSortedHosts]);
const folderKeys = useMemo(() => Object.keys(hostsByFolder), [hostsByFolder]);
const folderKeysString = useMemo(() => folderKeys.join(","), [folderKeys]);
useEffect(() => {
setOpenAccordions((prev) => {
if (prev.length === 0 && folderKeys.length > 0) {
return folderKeys;
}
const existingFolders = prev.filter((folder) =>
folderKeys.includes(folder),
);
const newFolders = folderKeys.filter((folder) => !prev.includes(folder));
return [...existingFolders, ...newFolders];
});
}, [folderKeysString]);
const toggleFolderExpansion = useCallback((folderName: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev);
@@ -871,31 +959,31 @@ export function HostManagerViewer({
</p>
</div>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
className="relative"
onClick={() =>
document.getElementById("json-import-input")?.click()
}
disabled={importing}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" disabled={importing}>
{importing ? t("hosts.importing") : t("hosts.importJson")}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-sm">
<div className="space-y-2">
<p className="font-semibold text-sm">
{t("hosts.importJsonTitle")}
</p>
<p className="text-xs text-muted-foreground">
{t("hosts.importJsonDesc")}
</p>
</div>
</TooltipContent>
</Tooltip>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
overwriteRef.current = false;
document.getElementById("json-import-input")?.click();
}}
>
{t("hosts.importSkipExisting")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
overwriteRef.current = true;
document.getElementById("json-import-input")?.click();
}}
>
{t("hosts.importOverwriteExisting")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant="outline"
@@ -975,31 +1063,31 @@ export function HostManagerViewer({
</p>
</div>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
className="relative"
onClick={() =>
document.getElementById("json-import-input")?.click()
}
disabled={importing}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" disabled={importing}>
{importing ? t("hosts.importing") : t("hosts.importJson")}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-sm">
<div className="space-y-2">
<p className="font-semibold text-sm">
{t("hosts.importJsonTitle")}
</p>
<p className="text-xs text-muted-foreground">
{t("hosts.importJsonDesc")}
</p>
</div>
</TooltipContent>
</Tooltip>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
overwriteRef.current = false;
document.getElementById("json-import-input")?.click();
}}
>
{t("hosts.importSkipExisting")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
overwriteRef.current = true;
document.getElementById("json-import-input")?.click();
}}
>
{t("hosts.importOverwriteExisting")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm" onClick={handleDownloadSample}>
{t("hosts.downloadSample")}
@@ -1045,6 +1133,17 @@ export function HostManagerViewer({
<Plus className="h-4 w-4 mr-2" />
{t("hosts.addHost")}
</Button>
<Button
variant={selectionMode ? "default" : "outline"}
className="h-9"
onClick={() => {
if (selectionMode) exitSelectionMode();
else setSelectionMode(true);
}}
>
<ListChecks className="h-4 w-4 mr-2" />
{selectionMode ? t("hosts.exitSelectMode") : t("hosts.selectMode")}
</Button>
</div>
<ScrollArea className="flex-1 min-h-0">
@@ -1064,7 +1163,8 @@ export function HostManagerViewer({
>
<Accordion
type="multiple"
defaultValue={Object.keys(hostsByFolder)}
value={openAccordions}
onValueChange={setOpenAccordions}
>
<AccordionItem value={folder} className="border-none">
<AccordionTrigger className="px-2 py-1 bg-muted/20 border-b hover:no-underline rounded-t-md">
@@ -1162,6 +1262,39 @@ export function HostManagerViewer({
<Badge variant="secondary" className="text-xs">
{folderHosts.length}
</Badge>
{selectionMode &&
(() => {
const selectableIds = folderHosts
.filter((h) => !(h as any).isShared)
.map((h) => h.id);
const allSelected =
selectableIds.length > 0 &&
selectableIds.every((id) =>
selectedHostIds.has(id),
);
return (
<Checkbox
checked={allSelected}
onCheckedChange={(checked) => {
setSelectedHostIds((prev) => {
const next = new Set(prev);
if (checked) {
selectableIds.forEach((id) =>
next.add(id),
);
} else {
selectableIds.forEach((id) =>
next.delete(id),
);
}
return next;
});
}}
onClick={(e) => e.stopPropagation()}
className="ml-1"
/>
);
})()}
{folder !== t("hosts.uncategorized") && (
<div className="flex items-center gap-1 ml-auto">
<Tooltip>
@@ -1210,19 +1343,49 @@ export function HostManagerViewer({
<Tooltip key={host.id}>
<TooltipTrigger asChild>
<div
draggable
draggable={!selectionMode}
onDragStart={(e) => handleDragStart(e, host)}
onDragEnd={handleDragEnd}
className={`bg-field border border-input rounded-lg cursor-pointer hover:shadow-lg hover:border-blue-400/50 hover:bg-hover-alt transition-all duration-200 p-3 group relative ${
className={`bg-field border rounded-lg cursor-pointer hover:shadow-lg hover:bg-hover-alt transition-all duration-200 p-3 group relative ${
draggedHost?.id === host.id
? "opacity-50 scale-95"
: ""
} ${
selectionMode && selectedHostIds.has(host.id)
? "ring-2 ring-blue-500 border-blue-500"
: "border-input hover:border-blue-400/50"
} ${
selectionMode && (host as any).isShared
? "opacity-50 pointer-events-none"
: ""
}`}
onClick={() => handleEdit(host)}
onClick={() => {
if (
selectionMode &&
!(host as any).isShared
) {
toggleHostSelection(host.id);
} else {
handleEdit(host);
}
}}
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1">
{selectionMode &&
!(host as any).isShared && (
<Checkbox
checked={selectedHostIds.has(
host.id,
)}
onCheckedChange={() =>
toggleHostSelection(host.id)
}
onClick={(e) => e.stopPropagation()}
className="bg-background border-2 mr-1 flex-shrink-0"
/>
)}
{(() => {
const statsConfig = (() => {
try {
@@ -1748,6 +1911,322 @@ export function HostManagerViewer({
}}
/>
)}
{selectionMode && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 bg-popover border border-border rounded-lg shadow-xl px-4 py-3 flex items-center gap-2 max-w-[90vw]">
<span className="text-sm font-medium whitespace-nowrap">
{t("hosts.selectedCount", { count: selectedHostIds.size })}
</span>
<div className="w-px h-6 bg-border" />
<Button
variant="outline"
size="sm"
onClick={
selectedHostIds.size ===
hosts.filter((h) => !(h as any).isShared).length
? deselectAllHosts
: selectAllHosts
}
>
{selectedHostIds.size ===
hosts.filter((h) => !(h as any).isShared).length
? t("hosts.deselectAll")
: t("hosts.selectAll")}
</Button>
<div className="w-px h-6 bg-border" />
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
disabled={bulkUpdating || selectedHostIds.size === 0}
>
<Share2 className="h-3.5 w-3.5 mr-1.5" />
{t("hosts.bulkMonitoring")}
<ChevronDown className="h-3 w-3 ml-1.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center">
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({
statsConfig: { statusCheckEnabled: true },
});
}}
>
<Check className="h-3.5 w-3.5 mr-2 text-green-500" />
{t("hosts.enableStatusCheck")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({
statsConfig: { statusCheckEnabled: false },
});
}}
>
<X className="h-3.5 w-3.5 mr-2 text-red-500" />
{t("hosts.disableStatusCheck")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({
statsConfig: { useGlobalStatusInterval: true },
});
}}
>
<Globe className="h-3.5 w-3.5 mr-2 text-blue-500" />
{t("hosts.useGlobalStatusDefault")}
</DropdownMenuItem>
<div className="h-px bg-border my-1" />
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ statsConfig: { metricsEnabled: true } });
}}
>
<Check className="h-3.5 w-3.5 mr-2 text-green-500" />
{t("hosts.enableMetrics")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({
statsConfig: { metricsEnabled: false },
});
}}
>
<X className="h-3.5 w-3.5 mr-2 text-red-500" />
{t("hosts.disableMetrics")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({
statsConfig: { useGlobalMetricsInterval: true },
});
}}
>
<Globe className="h-3.5 w-3.5 mr-2 text-blue-500" />
{t("hosts.useGlobalMetricsDefault")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
disabled={bulkUpdating || selectedHostIds.size === 0}
>
<Layers className="h-3.5 w-3.5 mr-1.5" />
{t("hosts.bulkFeatures")}
<ChevronDown className="h-3 w-3 ml-1.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center">
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({
enableTerminal: true,
enableFileManager: true,
enableTunnel: true,
enableDocker: true,
});
}}
>
<Check className="h-3.5 w-3.5 mr-2 text-green-500" />
{t("hosts.enableAllFeatures")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({
enableTerminal: false,
enableFileManager: false,
enableTunnel: false,
enableDocker: false,
});
}}
>
<X className="h-3.5 w-3.5 mr-2 text-red-500" />
{t("hosts.disableAllFeatures")}
</DropdownMenuItem>
<div className="h-px bg-border my-1" />
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ enableTerminal: true });
}}
>
<Terminal className="h-3.5 w-3.5 mr-2" />
{t("hosts.bulkEnableTerminal")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ enableTerminal: false });
}}
>
<Terminal className="h-3.5 w-3.5 mr-2 opacity-30" />
{t("hosts.bulkDisableTerminal")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ enableFileManager: true });
}}
>
<FolderOpen className="h-3.5 w-3.5 mr-2" />
{t("hosts.bulkEnableFileManager")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ enableFileManager: false });
}}
>
<FolderOpen className="h-3.5 w-3.5 mr-2 opacity-30" />
{t("hosts.bulkDisableFileManager")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ enableTunnel: true });
}}
>
<Network className="h-3.5 w-3.5 mr-2" />
{t("hosts.bulkEnableTunnel")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ enableTunnel: false });
}}
>
<Network className="h-3.5 w-3.5 mr-2 opacity-30" />
{t("hosts.bulkDisableTunnel")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ enableDocker: true });
}}
>
<Container className="h-3.5 w-3.5 mr-2" />
{t("hosts.bulkEnableDocker")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ enableDocker: false });
}}
>
<Container className="h-3.5 w-3.5 mr-2 opacity-30" />
{t("hosts.bulkDisableDocker")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
disabled={bulkUpdating || selectedHostIds.size === 0}
>
<Folder className="h-3.5 w-3.5 mr-1.5" />
{t("hosts.bulkMoveFolder")}
<ChevronDown className="h-3 w-3 ml-1.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="center"
className="max-h-[300px] overflow-y-auto"
>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ folder: "" });
}}
>
<FolderOpen className="h-3.5 w-3.5 mr-2" />
{t("hosts.uncategorized")}
</DropdownMenuItem>
{Object.keys(hostsByFolder)
.filter((f) => f !== t("hosts.uncategorized"))
.map((f) => {
const FolderIcon = getFolderIcon(f);
const folderColor = getFolderColor(f);
return (
<DropdownMenuItem
key={f}
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ folder: f });
}}
>
<FolderIcon
className="h-3.5 w-3.5 mr-2"
style={
folderColor ? { color: folderColor } : undefined
}
/>
{f}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
disabled={bulkUpdating || selectedHostIds.size === 0}
>
<Pin className="h-3.5 w-3.5 mr-1.5" />
{t("hosts.pin")}
<ChevronDown className="h-3 w-3 ml-1.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center">
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ pin: true });
}}
>
<Pin className="h-3.5 w-3.5 mr-2 text-yellow-500" />
{t("hosts.bulkPin")}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
handleBulkUpdate({ pin: false });
}}
>
<Pin className="h-3.5 w-3.5 mr-2 opacity-30" />
{t("hosts.bulkUnpin")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="w-px h-6 bg-border" />
<Button variant="ghost" size="sm" onClick={exitSelectionMode}>
<X className="h-4 w-4 mr-1" />
{t("hosts.exitSelectMode")}
</Button>
</div>
)}
</div>
</TooltipProvider>
);
@@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import { Controller } from "react-hook-form";
import { cn } from "@/lib/utils.ts";
import { Button } from "@/components/ui/button.tsx";
@@ -37,9 +37,18 @@ import { Separator } from "@/components/ui/separator.tsx";
import { CredentialSelector } from "@/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx";
import CodeMirror from "@uiw/react-codemirror";
import { EditorView } from "@codemirror/view";
import { Plus, X, Upload, AlertCircle } from "lucide-react";
import {
Plus,
X,
Upload,
AlertCircle,
ArrowRight,
Loader2,
} from "lucide-react";
import type { HostGeneralTabProps } from "./shared/tab-types";
import { JumpHostItem } from "./shared/JumpHostItem";
import { testProxyConnection } from "@/ui/main-axios";
import { toast } from "sonner";
export function HostGeneralTab({
form,
@@ -70,6 +79,103 @@ export function HostGeneralTab({
credentials,
t,
}: HostGeneralTabProps) {
const [proxyTesting, setProxyTesting] = useState(false);
const handleTestProxy = async () => {
setProxyTesting(true);
try {
const chain = form.watch("socks5ProxyChain") || [];
const host = form.watch("socks5Host");
const port = form.watch("socks5Port");
let result;
if (proxyMode === "chain" && chain.length > 0) {
result = await testProxyConnection({ proxyChain: chain });
} else if (host) {
result = await testProxyConnection({
singleProxy: {
host,
port: port || 1080,
type: 5,
username: form.watch("socks5Username") || undefined,
password: form.watch("socks5Password") || undefined,
},
});
} else {
toast.error(
t("hosts.proxyTestFailed", { error: "No proxy configured" }),
);
setProxyTesting(false);
return;
}
if (result.success) {
toast.success(
t("hosts.proxyTestSuccess", { latency: result.latencyMs ?? 0 }),
);
} else {
toast.error(
t("hosts.proxyTestFailed", {
error: result.error || "Unknown error",
}),
);
}
} catch (err) {
toast.error(
t("hosts.proxyTestFailed", {
error: err instanceof Error ? err.message : "Unknown error",
}),
);
} finally {
setProxyTesting(false);
}
};
// Build connection path for visualization
const buildConnectionPath = () => {
const parts: string[] = [
t("hosts.connectionPath") === "Connection Path" ? "You" : "You",
];
const useSocks5 = form.watch("useSocks5");
const jumpHosts = form.watch("jumpHosts") || [];
if (useSocks5) {
if (proxyMode === "chain") {
const chain = form.watch("socks5ProxyChain") || [];
chain.forEach((node: any, i: number) => {
if (node.host) {
const typeLabel =
node.type === "http" ? "HTTP" : `SOCKS${node.type}`;
parts.push(`${typeLabel} ${node.host}:${node.port}`);
}
});
} else {
const host = form.watch("socks5Host");
const port = form.watch("socks5Port") || 1080;
if (host) {
parts.push(`SOCKS5 ${host}:${port}`);
}
}
}
if (jumpHosts.length > 0 && hosts) {
jumpHosts.forEach((jh: any) => {
const found = hosts.find((h: any) => h.id === jh.hostId);
if (found) {
parts.push(`Jump: ${found.name || found.ip}`);
}
});
}
const ip = form.watch("ip");
const port = form.watch("port");
if (ip) {
parts.push(`${ip}:${port || 22}`);
}
return parts;
};
return (
<div className="pt-2">
<FormLabel className="mb-3 font-bold">
@@ -1029,7 +1135,7 @@ export function HostGeneralTab({
{
host: "",
port: 1080,
type: 5 as 4 | 5,
type: 5 as 4 | 5 | "http",
username: "",
password: "",
},
@@ -1135,7 +1241,10 @@ export function HostGeneralTab({
const newChain = [...currentChain];
newChain[index] = {
...newChain[index],
type: parseInt(value) as 4 | 5,
type:
value === "http"
? ("http" as const)
: (parseInt(value) as 4 | 5),
};
form.setValue("socks5ProxyChain", newChain);
}}
@@ -1150,6 +1259,9 @@ export function HostGeneralTab({
<SelectItem value="5">
{t("hosts.socks5")}
</SelectItem>
<SelectItem value="http">
{t("hosts.httpConnect")}
</SelectItem>
</SelectContent>
</Select>
</div>
@@ -1212,6 +1324,58 @@ export function HostGeneralTab({
)}
</div>
)}
<Separator className="my-2" />
<Button
type="button"
variant="outline"
size="sm"
disabled={proxyTesting}
onClick={handleTestProxy}
>
{proxyTesting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{t("hosts.testingProxy")}
</>
) : (
t("hosts.testProxy")
)}
</Button>
{(() => {
const path = buildConnectionPath();
if (path.length <= 2) return null;
return (
<div className="mt-3 p-3 border rounded-lg bg-muted/30">
<FormLabel className="text-xs text-muted-foreground mb-2 block">
{t("hosts.connectionPath")}
</FormLabel>
<div className="flex items-center flex-wrap gap-1 text-xs">
{path.map((part, i) => (
<React.Fragment key={i}>
{i > 0 && (
<ArrowRight className="h-3 w-3 text-muted-foreground shrink-0" />
)}
<span
className={cn(
"px-2 py-0.5 rounded",
i === 0
? "bg-primary/10 text-primary"
: i === path.length - 1
? "bg-green-500/10 text-green-600 dark:text-green-400"
: "bg-muted text-muted-foreground",
)}
>
{part}
</span>
</React.Fragment>
))}
</div>
</div>
);
})()}
</div>
)}
</AccordionContent>
@@ -67,65 +67,90 @@ export function HostStatisticsTab({
/>
{form.watch("statsConfig.statusCheckEnabled") && (
<FormField
control={form.control}
name="statsConfig.statusCheckInterval"
render={({ field }) => {
const displayValue =
statusIntervalUnit === "minutes"
? Math.round((field.value || 30) / 60)
: field.value || 30;
const handleIntervalChange = (value: string) => {
const numValue = parseInt(value) || 0;
const seconds =
statusIntervalUnit === "minutes" ? numValue * 60 : numValue;
field.onChange(seconds);
};
return (
<FormItem>
<FormLabel>{t("hosts.statusCheckInterval")}</FormLabel>
<div className="flex gap-2">
<FormControl>
<Input
type="number"
value={displayValue}
onChange={(e) => handleIntervalChange(e.target.value)}
className="flex-1"
/>
</FormControl>
<Select
value={statusIntervalUnit}
onValueChange={(value: "seconds" | "minutes") => {
setStatusIntervalUnit(value);
const currentSeconds = field.value || 30;
if (value === "minutes") {
const minutes = Math.round(currentSeconds / 60);
field.onChange(minutes * 60);
}
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="seconds">
{t("hosts.intervalSeconds")}
</SelectItem>
<SelectItem value="minutes">
{t("hosts.intervalMinutes")}
</SelectItem>
</SelectContent>
</Select>
</div>
<FormDescription>
{t("hosts.statusCheckIntervalDesc")}
</FormDescription>
<>
<FormField
control={form.control}
name="statsConfig.useGlobalStatusInterval"
render={({ field }) => (
<FormItem className="flex flex-row items-center gap-2 space-y-0">
<FormControl>
<Switch
checked={field.value ?? true}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="font-normal">
{t("hosts.useGlobalStatusInterval")}
</FormLabel>
</FormItem>
);
}}
/>
)}
/>
{form.watch("statsConfig.useGlobalStatusInterval") === false && (
<FormField
control={form.control}
name="statsConfig.statusCheckInterval"
render={({ field }) => {
const displayValue =
statusIntervalUnit === "minutes"
? Math.round((field.value || 30) / 60)
: field.value || 30;
const handleIntervalChange = (value: string) => {
const numValue = parseInt(value) || 0;
const seconds =
statusIntervalUnit === "minutes"
? numValue * 60
: numValue;
field.onChange(seconds);
};
return (
<FormItem>
<FormLabel>{t("hosts.statusCheckInterval")}</FormLabel>
<div className="flex gap-2">
<FormControl>
<Input
type="number"
value={displayValue}
onChange={(e) =>
handleIntervalChange(e.target.value)
}
className="flex-1"
/>
</FormControl>
<Select
value={statusIntervalUnit}
onValueChange={(value: "seconds" | "minutes") => {
setStatusIntervalUnit(value);
const currentSeconds = field.value || 30;
if (value === "minutes") {
const minutes = Math.round(currentSeconds / 60);
field.onChange(minutes * 60);
}
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="seconds">
{t("hosts.intervalSeconds")}
</SelectItem>
<SelectItem value="minutes">
{t("hosts.intervalMinutes")}
</SelectItem>
</SelectContent>
</Select>
</div>
<FormDescription>
{t("hosts.statusCheckIntervalDesc")}
</FormDescription>
</FormItem>
);
}}
/>
)}
</>
)}
</div>
@@ -152,67 +177,90 @@ export function HostStatisticsTab({
/>
{form.watch("statsConfig.metricsEnabled") && (
<FormField
control={form.control}
name="statsConfig.metricsInterval"
render={({ field }) => {
const displayValue =
metricsIntervalUnit === "minutes"
? Math.round((field.value || 30) / 60)
: field.value || 30;
const handleIntervalChange = (value: string) => {
const numValue = parseInt(value) || 0;
const seconds =
metricsIntervalUnit === "minutes"
? numValue * 60
: numValue;
field.onChange(seconds);
};
return (
<FormItem>
<FormLabel>{t("hosts.metricsInterval")}</FormLabel>
<div className="flex gap-2">
<FormControl>
<Input
type="number"
value={displayValue}
onChange={(e) => handleIntervalChange(e.target.value)}
className="flex-1"
/>
</FormControl>
<Select
value={metricsIntervalUnit}
onValueChange={(value: "seconds" | "minutes") => {
setMetricsIntervalUnit(value);
const currentSeconds = field.value || 30;
if (value === "minutes") {
const minutes = Math.round(currentSeconds / 60);
field.onChange(minutes * 60);
}
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="seconds">
{t("hosts.intervalSeconds")}
</SelectItem>
<SelectItem value="minutes">
{t("hosts.intervalMinutes")}
</SelectItem>
</SelectContent>
</Select>
</div>
<FormDescription>
{t("hosts.metricsIntervalDesc")}
</FormDescription>
<>
<FormField
control={form.control}
name="statsConfig.useGlobalMetricsInterval"
render={({ field }) => (
<FormItem className="flex flex-row items-center gap-2 space-y-0">
<FormControl>
<Switch
checked={field.value ?? true}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="font-normal">
{t("hosts.useGlobalMetricsInterval")}
</FormLabel>
</FormItem>
);
}}
/>
)}
/>
{form.watch("statsConfig.useGlobalMetricsInterval") === false && (
<FormField
control={form.control}
name="statsConfig.metricsInterval"
render={({ field }) => {
const displayValue =
metricsIntervalUnit === "minutes"
? Math.round((field.value || 30) / 60)
: field.value || 30;
const handleIntervalChange = (value: string) => {
const numValue = parseInt(value) || 0;
const seconds =
metricsIntervalUnit === "minutes"
? numValue * 60
: numValue;
field.onChange(seconds);
};
return (
<FormItem>
<FormLabel>{t("hosts.metricsInterval")}</FormLabel>
<div className="flex gap-2">
<FormControl>
<Input
type="number"
value={displayValue}
onChange={(e) =>
handleIntervalChange(e.target.value)
}
className="flex-1"
/>
</FormControl>
<Select
value={metricsIntervalUnit}
onValueChange={(value: "seconds" | "minutes") => {
setMetricsIntervalUnit(value);
const currentSeconds = field.value || 30;
if (value === "minutes") {
const minutes = Math.round(currentSeconds / 60);
field.onChange(minutes * 60);
}
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="seconds">
{t("hosts.intervalSeconds")}
</SelectItem>
<SelectItem value="minutes">
{t("hosts.intervalMinutes")}
</SelectItem>
</SelectContent>
</Select>
</div>
<FormDescription>
{t("hosts.metricsIntervalDesc")}
</FormDescription>
</FormItem>
);
}}
/>
)}
</>
)}
</div>
</div>
@@ -49,6 +49,7 @@ import type { HostTerminalTabProps } from "./shared/tab-types";
import React from "react";
export function HostTerminalTab({ form, snippets, t }: HostTerminalTabProps) {
const [snippetPopoverOpen, setSnippetPopoverOpen] = React.useState(false);
return (
<div className="space-y-1">
<FormField
@@ -499,7 +500,8 @@ export function HostTerminalTab({ form, snippets, t }: HostTerminalTabProps) {
control={form.control}
name="terminalConfig.startupSnippetId"
render={({ field }) => {
const [open, setOpen] = React.useState(false);
const open = snippetPopoverOpen;
const setOpen = setSnippetPopoverOpen;
const selectedSnippet = snippets.find(
(s) => s.id === field.value,
);
@@ -759,6 +761,71 @@ export function HostTerminalTab({ form, snippets, t }: HostTerminalTabProps) {
{t("hosts.addVariable")}
</Button>
</div>
<div className="space-y-4 pt-4 border-t">
<label className="text-sm font-medium">
{t("hosts.keepaliveSettings")}
</label>
<FormDescription>
{t("hosts.keepaliveSettingsDesc")}
</FormDescription>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="terminalConfig.keepaliveInterval"
render={({ field }) => (
<FormItem>
<FormLabel>{t("hosts.keepaliveInterval")}</FormLabel>
<FormControl>
<Input
type="number"
min={0}
max={300000}
placeholder="30000"
value={field.value ?? ""}
onChange={(e) => {
const val = e.target.value;
field.onChange(
val === "" ? undefined : Number(val),
);
}}
/>
</FormControl>
<FormDescription>
{t("hosts.keepaliveIntervalDesc")}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="terminalConfig.keepaliveCountMax"
render={({ field }) => (
<FormItem>
<FormLabel>{t("hosts.keepaliveCountMax")}</FormLabel>
<FormControl>
<Input
type="number"
min={0}
max={100}
placeholder="3"
value={field.value ?? ""}
onChange={(e) => {
const val = e.target.value;
field.onChange(
val === "" ? undefined : Number(val),
);
}}
/>
</FormControl>
<FormDescription>
{t("hosts.keepaliveCountMaxDesc")}
</FormDescription>
</FormItem>
)}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
+23 -13
View File
@@ -223,7 +223,9 @@ export function SSHToolsSidebar({
const [historyRefreshCounter, setHistoryRefreshCounter] = useState(0);
const commandHistoryScrollRef = React.useRef<HTMLDivElement>(null);
const [splitMode, setSplitMode] = useState<"none" | "2" | "3" | "4">("none");
const [splitMode, setSplitMode] = useState<
"none" | "2" | "3" | "4" | "5" | "6"
>("none");
const [splitAssignments, setSplitAssignments] = useState<Map<number, number>>(
new Map(),
);
@@ -977,7 +979,9 @@ export function SSHToolsSidebar({
}
};
const handleSplitModeChange = (mode: "none" | "2" | "3" | "4") => {
const handleSplitModeChange = (
mode: "none" | "2" | "3" | "4" | "5" | "6",
) => {
setSplitMode(mode);
if (mode === "none") {
@@ -1731,23 +1735,29 @@ export function SSHToolsSidebar({
value={splitMode}
onValueChange={(value) =>
handleSplitModeChange(
value as "none" | "2" | "3" | "4",
value as "none" | "2" | "3" | "4" | "5" | "6",
)
}
className="w-full"
>
<TabsList className="w-full grid grid-cols-4">
<TabsTrigger value="none">
<TabsList className="w-full grid grid-cols-3 grid-rows-2 h-auto gap-2 p-2">
<TabsTrigger value="none" className="h-10">
{t("splitScreen.none")}
</TabsTrigger>
<TabsTrigger value="2">
{t("splitScreen.twoSplit")}{" "}
<TabsTrigger value="2" className="h-10">
{t("splitScreen.twoSplit")}
</TabsTrigger>
<TabsTrigger value="3">
{t("splitScreen.threeSplit")}{" "}
<TabsTrigger value="3" className="h-10">
{t("splitScreen.threeSplit")}
</TabsTrigger>
<TabsTrigger value="4">
{t("splitScreen.fourSplit")}{" "}
<TabsTrigger value="4" className="h-10">
{t("splitScreen.fourSplit")}
</TabsTrigger>
<TabsTrigger value="5" className="h-10">
{t("splitScreen.fiveSplit")}
</TabsTrigger>
<TabsTrigger value="6" className="h-10">
{t("splitScreen.sixSplit")}
</TabsTrigger>
</TabsList>
</Tabs>
@@ -1807,8 +1817,8 @@ export function SSHToolsSidebar({
className={`grid gap-2 mt-2 ${
splitMode === "2"
? "grid-cols-2"
: splitMode === "3"
? "grid-cols-2 grid-rows-2"
: splitMode === "5" || splitMode === "6"
? "grid-cols-3 grid-rows-2"
: "grid-cols-2 grid-rows-2"
}`}
>
+45 -5
View File
@@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button.tsx";
import { Input } from "@/components/ui/input.tsx";
import { PasswordInput } from "@/components/ui/password-input.tsx";
import { Label } from "@/components/ui/label.tsx";
import { Checkbox } from "@/components/ui/checkbox.tsx";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
import {
Tabs,
@@ -31,6 +32,8 @@ import {
verifyTOTPLogin,
getServerConfig,
isElectron,
getEmbeddedServerStatus,
isEmbeddedMode,
} from "../../main-axios.ts";
import { ElectronServerConfig as ServerConfigComponent } from "@/ui/desktop/authentication/ElectronServerConfig.tsx";
import { ElectronLoginForm } from "@/ui/desktop/authentication/ElectronLoginForm.tsx";
@@ -101,6 +104,7 @@ export function Auth({
const [localUsername, setLocalUsername] = useState("");
const [password, setPassword] = useState("");
const [signupConfirmPassword, setSignupConfirmPassword] = useState("");
const [rememberMe, setRememberMe] = useState(false);
const [loading, setLoading] = useState(false);
const [oidcLoading, setOidcLoading] = useState(false);
const [internalLoggedIn, setInternalLoggedIn] = useState(false);
@@ -267,7 +271,7 @@ export function Auth({
try {
let res;
if (tab === "login") {
res = await loginUser(localUsername, password);
res = await loginUser(localUsername, password, rememberMe);
} else {
if (password !== signupConfirmPassword) {
toast.error(t("errors.passwordMismatch"));
@@ -281,7 +285,7 @@ export function Auth({
}
await registerUser(localUsername, password);
res = await loginUser(localUsername, password);
res = await loginUser(localUsername, password, rememberMe);
}
if (res.requires_totp) {
@@ -494,7 +498,7 @@ export function Auth({
setTotpLoading(true);
try {
const res = await verifyTOTPLogin(totpTempToken, totpCode);
const res = await verifyTOTPLogin(totpTempToken, totpCode, rememberMe);
if (!res || !res.success) {
throw new Error(t("errors.loginFailed"));
@@ -626,6 +630,8 @@ export function Auth({
if (error) {
if (error === "registration_disabled") {
toast.error(t("messages.registrationDisabled"));
} else if (error === "user_not_allowed") {
toast.error(t("messages.userNotAllowed"));
} else {
toast.error(`${t("errors.oidcAuthFailed")}: ${error}`);
}
@@ -680,7 +686,9 @@ export function Auth({
window.location.pathname,
);
})
.catch(() => {
.catch((err) => {
console.error("Failed to get user info after OIDC callback:", err);
toast.error(t("errors.failedUserInfo"));
setInternalLoggedIn(false);
setLoggedIn(false);
setIsAdmin(false);
@@ -744,7 +752,17 @@ export function Auth({
if (isElectron()) {
try {
const config = await getServerConfig();
const [config, status] = await Promise.all([
getServerConfig(),
getEmbeddedServerStatus(),
]);
if (status?.embedded && status?.running && !config?.serverUrl) {
setCurrentServerUrl("");
setShowServerConfig(false);
return;
}
setCurrentServerUrl(config?.serverUrl || "");
setShowServerConfig(!config || !config.serverUrl);
} catch {
@@ -783,6 +801,10 @@ export function Auth({
onServerConfigured={() => {
window.location.reload();
}}
onUseEmbedded={() => {
setShowServerConfig(false);
setCurrentServerUrl("");
}}
onCancel={() => {
setShowServerConfig(false);
}}
@@ -1431,6 +1453,24 @@ export function Auth({
disabled={loading || loggedIn}
/>
</div>
{tab === "login" && (
<div className="flex items-center gap-2">
<Checkbox
id="rememberMe"
checked={rememberMe}
onCheckedChange={(checked) =>
setRememberMe(checked === true)
}
disabled={loading || loggedIn}
/>
<Label
htmlFor="rememberMe"
className="text-sm font-normal cursor-pointer"
>
{t("auth.rememberMe")}
</Label>
</div>
)}
{tab === "signup" && (
<div className="flex flex-col gap-2">
<Label htmlFor="signup-confirm-password">
@@ -233,6 +233,7 @@ export function ElectronLoginForm({
};
const displayUrl = currentUrl.replace(/^https?:\/\//, "");
const isEmbeddedServer = serverUrl.includes("localhost:30001");
return (
<div className="fixed inset-0 w-screen h-screen bg-canvas flex flex-col">
@@ -247,11 +248,14 @@ export function ElectronLoginForm({
{t("serverConfig.changeServer")}
</span>
</button>
<div className="flex-1 mx-4 text-center">
<span className="text-muted-foreground text-sm truncate block">
{displayUrl}
</span>
</div>
{!isEmbeddedServer && (
<div className="flex-1 mx-4 text-center">
<span className="text-muted-foreground text-sm truncate block">
{displayUrl}
</span>
</div>
)}
{isEmbeddedServer && <div className="flex-1" />}
<button
onClick={handleRefresh}
className="p-2 text-foreground hover:text-primary transition-colors"
@@ -7,19 +7,23 @@ import { useTranslation } from "react-i18next";
import {
getServerConfig,
saveServerConfig,
getEmbeddedServerStatus,
setEmbeddedMode,
type ServerConfig,
} from "@/ui/main-axios.ts";
import { Server } from "lucide-react";
import { Server, Monitor, Loader2 } from "lucide-react";
import { useTheme } from "@/components/theme-provider";
interface ServerConfigProps {
onServerConfigured: (serverUrl: string) => void;
onUseEmbedded?: () => void;
onCancel?: () => void;
isFirstTime?: boolean;
}
export function ElectronServerConfig({
onServerConfigured,
onUseEmbedded,
onCancel,
isFirstTime = false,
}: ServerConfigProps) {
@@ -27,7 +31,11 @@ export function ElectronServerConfig({
const { theme } = useTheme();
const [serverUrl, setServerUrl] = useState("");
const [loading, setLoading] = useState(false);
const [embeddedLoading, setEmbeddedLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [embeddedAvailable, setEmbeddedAvailable] = useState<boolean | null>(
null,
);
const isDarkMode =
theme === "dark" ||
@@ -37,6 +45,7 @@ export function ElectronServerConfig({
useEffect(() => {
loadServerConfig();
checkEmbeddedBackend();
}, []);
const loadServerConfig = async () => {
@@ -50,6 +59,71 @@ export function ElectronServerConfig({
}
};
const checkEmbeddedBackend = async () => {
try {
const status = await getEmbeddedServerStatus();
setEmbeddedAvailable(!!status?.embedded);
} catch {
setEmbeddedAvailable(true);
}
};
const probeBackend = async (): Promise<boolean> => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
try {
const res = await fetch("http://localhost:30001/health", {
signal: controller.signal,
});
clearTimeout(timer);
return res.ok;
} catch {
clearTimeout(timer);
}
const controller2 = new AbortController();
const timer2 = setTimeout(() => controller2.abort(), 3000);
try {
await fetch("http://localhost:30001/version", {
signal: controller2.signal,
});
clearTimeout(timer2);
return true;
} catch {
clearTimeout(timer2);
return false;
}
};
const handleUseEmbedded = async () => {
setEmbeddedLoading(true);
setError(null);
try {
const maxRetries = 10;
for (let i = 0; i < maxRetries; i++) {
if (await probeBackend()) {
setEmbeddedMode(true);
if (onUseEmbedded) {
onUseEmbedded();
} else {
onServerConfigured("http://localhost:30001");
}
return;
}
if (i < maxRetries - 1) {
await new Promise((r) => setTimeout(r, 2000));
}
}
setError(t("serverConfig.embeddedNotReady"));
} catch (err) {
setError(
err instanceof Error ? err.message : t("serverConfig.embeddedNotReady"),
);
} finally {
setEmbeddedLoading(false);
}
};
const handleSaveConfig = async () => {
if (!serverUrl.trim()) {
setError(t("serverConfig.enterServerUrl"));
@@ -60,7 +134,7 @@ export function ElectronServerConfig({
setError(null);
try {
let normalizedUrl = serverUrl.trim();
const normalizedUrl = serverUrl.trim();
if (
!normalizedUrl.startsWith("http://") &&
@@ -120,17 +194,58 @@ export function ElectronServerConfig({
{t("serverConfig.description")}
</p>
</div>
{embeddedAvailable !== false && (
<div className="space-y-2">
<Button
type="button"
variant="outline"
className="w-full"
onClick={handleUseEmbedded}
disabled={embeddedLoading || loading}
>
{embeddedLoading ? (
<div className="flex items-center space-x-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span>{t("serverConfig.embeddedConnecting")}</span>
</div>
) : (
<div className="flex items-center justify-center space-x-2">
<Monitor className="w-4 h-4" />
<span>{t("serverConfig.useEmbedded")}</span>
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-yellow-500/20 text-yellow-600 dark:text-yellow-400 rounded border border-yellow-500/30">
BETA
</span>
</div>
)}
</Button>
<p className="text-xs text-muted-foreground text-center">
{t("serverConfig.embeddedDesc")}
</p>
</div>
)}
{embeddedAvailable !== false && (
<div className="flex items-center gap-3">
<div className="h-px flex-1 bg-border" />
<span className="text-xs text-muted-foreground">
{t("common.or") || "OR"}
</span>
<div className="h-px flex-1 bg-border" />
</div>
)}
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="server-url">{t("serverConfig.serverUrl")}</Label>
<Input
id="server-url"
type="text"
placeholder="http://localhost:30001 or https://your-server.com"
placeholder="https://your-server.com"
value={serverUrl}
onChange={(e) => handleUrlChange(e.target.value)}
className="w-full h-10"
disabled={loading}
disabled={loading || embeddedLoading}
/>
</div>
@@ -148,16 +263,17 @@ export function ElectronServerConfig({
variant="outline"
className="flex-1"
onClick={onCancel}
disabled={loading}
disabled={loading || embeddedLoading}
>
Cancel
</Button>
)}
<Button
type="button"
variant="outline"
className={onCancel && !isFirstTime ? "flex-1" : "w-full"}
onClick={handleSaveConfig}
disabled={loading || !serverUrl.trim()}
disabled={loading || embeddedLoading || !serverUrl.trim()}
>
{loading ? (
<div className="flex items-center space-x-2">
+128 -264
View File
@@ -37,6 +37,39 @@ interface TabData {
[key: string]: unknown;
}
type LayoutNode =
| number // leaf: index into layoutTabs[]
| { direction: "horizontal" | "vertical"; children: LayoutNode[] };
const SPLIT_LAYOUTS: Record<number, LayoutNode> = {
2: { direction: "horizontal", children: [0, 1] },
3: {
direction: "vertical",
children: [{ direction: "horizontal", children: [0, 1] }, 2],
},
4: {
direction: "vertical",
children: [
{ direction: "horizontal", children: [0, 1] },
{ direction: "horizontal", children: [2, 3] },
],
},
5: {
direction: "vertical",
children: [
{ direction: "horizontal", children: [0, 1] },
{ direction: "horizontal", children: [2, 3, 4] },
],
},
6: {
direction: "vertical",
children: [
{ direction: "horizontal", children: [0, 1, 2] },
{ direction: "horizontal", children: [3, 4, 5] },
],
},
};
interface TerminalViewProps {
isTopbarOpen?: boolean;
rightSidebarOpen?: boolean;
@@ -48,12 +81,14 @@ export function AppView({
rightSidebarOpen = false,
rightSidebarWidth = 400,
}: TerminalViewProps): React.ReactElement {
const { tabs, currentTab, allSplitScreenTab, removeTab } = useTabs() as {
tabs: TabData[];
currentTab: number;
allSplitScreenTab: number[];
removeTab: (id: number) => void;
};
const { tabs, currentTab, allSplitScreenTab, removeTab, updateTab } =
useTabs() as {
tabs: TabData[];
currentTab: number;
allSplitScreenTab: number[];
removeTab: (id: number) => void;
updateTab: (tabId: number, updates: Partial<Omit<TabData, "id">>) => void;
};
const { state: sidebarState } = useSidebar();
const { theme: appTheme } = useTheme();
@@ -348,6 +383,7 @@ export function AppView({
showTitle={false}
splitScreen={allSplitScreenTab.length > 0}
onClose={() => removeTab(t.id)}
onTitleChange={(title) => updateTab(t.id, { title })}
/>
) : t.type === "server_stats" ? (
<ServerView
@@ -419,6 +455,9 @@ export function AppView({
.filter((t): t is TabData => t !== null && t !== undefined);
if (allSplitScreenTab.length === 0) return null;
const layout = SPLIT_LAYOUTS[layoutTabs.length];
if (!layout) return null;
const handleStyle = {
pointerEvents: "auto",
zIndex: 12,
@@ -432,270 +471,95 @@ export function AppView({
onResize: scheduleMeasureAndFit,
};
if (layoutTabs.length === 2) {
const [a, b] = layoutTabs;
return (
<div className="absolute inset-0 z-[10] pointer-events-none">
<ResizablePrimitive.PanelGroup
key={resetKey}
direction="horizontal"
className="h-full w-full"
{...commonGroupProps}
const renderNode = (
node: LayoutNode,
path: string,
siblingCount: number,
orderIndex: number,
isRoot: boolean,
): React.ReactNode => {
const defaultSize = Math.round(100 / siblingCount);
if (typeof node === "number") {
const tab = layoutTabs[node];
if (!tab) return null;
return (
<ResizablePanel
key={`panel-${tab.id}`}
id={`panel-${tab.id}`}
defaultSize={defaultSize}
minSize={20}
className="!overflow-hidden h-full w-full"
order={orderIndex}
>
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id={`panel-${a.id}`}
order={1}
<div
ref={(el) => {
panelRefs.current[String(tab.id)] = el;
}}
className="h-full w-full flex flex-col relative"
>
<div
ref={(el) => {
panelRefs.current[String(a.id)] = el;
}}
className="h-full w-full flex flex-col bg-transparent relative"
>
<div className="bg-surface text-foreground text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-edge-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
{a.title}
</div>
<div className="bg-surface text-foreground text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-edge-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
{tab.title}
{node === 1 && <ResetButton onClick={handleReset} />}
</div>
</ResizablePanel>
<ResizableHandle style={handleStyle} />
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id={`panel-${b.id}`}
order={2}
>
<div
ref={(el) => {
panelRefs.current[String(b.id)] = el;
}}
className="h-full w-full flex flex-col bg-transparent relative"
>
<div className="bg-surface text-foreground text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-edge-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
{b.title}
<ResetButton onClick={handleReset} />
</div>
</div>
</ResizablePanel>
</ResizablePrimitive.PanelGroup>
</div>
</div>
</ResizablePanel>
);
}
const groupKey = isRoot ? String(resetKey) : `${path}-${resetKey}`;
const groupId = isRoot ? `main-${node.direction}` : `group-${path}`;
const groupContent = (
<ResizablePrimitive.PanelGroup
key={groupKey}
direction={node.direction}
className="h-full w-full"
id={groupId}
{...commonGroupProps}
>
{node.children.flatMap((child, i) => {
const childPath = isRoot ? String(i) : `${path}-${i}`;
const panel = renderNode(
child,
childPath,
node.children.length,
i + 1,
false,
);
if (i === 0) return [panel];
return [
<ResizableHandle
key={`handle-${childPath}`}
style={handleStyle}
/>,
panel,
];
})}
</ResizablePrimitive.PanelGroup>
);
}
if (layoutTabs.length === 3) {
const [a, b, c] = layoutTabs;
if (isRoot) return groupContent;
return (
<div className="absolute inset-0 z-[10] pointer-events-none">
<ResizablePrimitive.PanelGroup
key={resetKey}
direction="vertical"
className="h-full w-full"
id="main-vertical"
{...commonGroupProps}
>
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id="top-panel"
order={1}
>
<ResizablePanelGroup
key={`top-${resetKey}`}
direction="horizontal"
className="h-full w-full"
id="top-horizontal"
{...commonGroupProps}
>
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id={`panel-${a.id}`}
order={1}
>
<div
ref={(el) => {
panelRefs.current[String(a.id)] = el;
}}
className="h-full w-full flex flex-col relative"
>
<div className="bg-surface text-foreground text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-edge-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
{a.title}
</div>
</div>
</ResizablePanel>
<ResizableHandle style={handleStyle} />
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id={`panel-${b.id}`}
order={2}
>
<div
ref={(el) => {
panelRefs.current[String(b.id)] = el;
}}
className="h-full w-full flex flex-col relative"
>
<div className="bg-surface text-foreground text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-edge-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
{b.title}
<ResetButton onClick={handleReset} />
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle style={handleStyle} />
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id="bottom-panel"
order={2}
>
<div
ref={(el) => {
panelRefs.current[String(c.id)] = el;
}}
className="h-full w-full flex flex-col relative"
>
<div className="bg-surface text-foreground text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-edge-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
{c.title}
</div>
</div>
</ResizablePanel>
</ResizablePrimitive.PanelGroup>
</div>
<ResizablePanel
key={`container-${path}`}
id={`container-${path}`}
defaultSize={defaultSize}
minSize={20}
className="!overflow-hidden h-full w-full"
order={orderIndex}
>
{groupContent}
</ResizablePanel>
);
}
if (layoutTabs.length === 4) {
const [a, b, c, d] = layoutTabs;
return (
<div className="absolute inset-0 z-[10] pointer-events-none">
<ResizablePrimitive.PanelGroup
key={resetKey}
direction="vertical"
className="h-full w-full"
id="main-vertical"
{...commonGroupProps}
>
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id="top-panel"
order={1}
>
<ResizablePanelGroup
key={`top-${resetKey}`}
direction="horizontal"
className="h-full w-full"
id="top-horizontal"
{...commonGroupProps}
>
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id={`panel-${a.id}`}
order={1}
>
<div
ref={(el) => {
panelRefs.current[String(a.id)] = el;
}}
className="h-full w-full flex flex-col relative"
>
<div className="bg-surface text-foreground text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-edge-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
{a.title}
</div>
</div>
</ResizablePanel>
<ResizableHandle style={handleStyle} />
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id={`panel-${b.id}`}
order={2}
>
<div
ref={(el) => {
panelRefs.current[String(b.id)] = el;
}}
className="h-full w-full flex flex-col relative"
>
<div className="bg-surface text-foreground text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-edge-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
{b.title}
<ResetButton onClick={handleReset} />
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle style={handleStyle} />
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id="bottom-panel"
order={2}
>
<ResizablePanelGroup
key={`bottom-${resetKey}`}
direction="horizontal"
className="h-full w-full"
id="bottom-horizontal"
{...commonGroupProps}
>
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id={`panel-${c.id}`}
order={1}
>
<div
ref={(el) => {
panelRefs.current[String(c.id)] = el;
}}
className="h-full w-full flex flex-col relative"
>
<div className="bg-surface text-foreground text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-edge-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
{c.title}
</div>
</div>
</ResizablePanel>
<ResizableHandle style={handleStyle} />
<ResizablePanel
defaultSize={50}
minSize={20}
className="!overflow-hidden h-full w-full"
id={`panel-${d.id}`}
order={2}
>
<div
ref={(el) => {
panelRefs.current[String(d.id)] = el;
}}
className="h-full w-full flex flex-col relative"
>
<div className="bg-surface text-foreground text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-edge-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
{d.title}
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePrimitive.PanelGroup>
</div>
);
}
return null;
};
return (
<div className="absolute inset-0 z-[10] pointer-events-none">
{renderNode(layout, "", 1, 1, true)}
</div>
);
};
const currentTabData = tabs.find((tab: TabData) => tab.id === currentTab);
+2 -1
View File
@@ -380,13 +380,14 @@ export function LeftSidebar({
for (const [field, value] of Object.entries(fieldMatches)) {
switch (field) {
case "tag":
case "tags":
case "tags": {
const tags = Array.isArray(h.tags) ? h.tags : [];
const hasMatchingTag = tags.some((tag) =>
tag.toLowerCase().includes(value),
);
if (!hasMatchingTag) return false;
break;
}
case "name":
if (!(h.name || "").toLowerCase().includes(value)) return false;
break;
+123 -8
View File
@@ -2,6 +2,7 @@ import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
type ReactNode,
} from "react";
@@ -47,14 +48,114 @@ interface TabProviderProps {
children: ReactNode;
}
export function clearTermixSessionStorage() {
localStorage.removeItem("termix_tabs");
localStorage.removeItem("termix_currentTab");
// Clear all session keys
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key?.startsWith("termix_session_")) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((k) => localStorage.removeItem(k));
}
export function TabProvider({ children }: TabProviderProps) {
const { t } = useTranslation();
const [tabs, setTabs] = useState<Tab[]>(() => [
{ id: 1, type: "home", title: "Home" },
]);
const [currentTab, setCurrentTab] = useState<number>(1);
const [tabs, setTabs] = useState<Tab[]>(() => {
// Check if persistence is enabled (always enabled for mobile/Electron)
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
const isElectron =
typeof window !== "undefined" && !!(window as any).electronAPI;
const persistenceEnabled =
localStorage.getItem("enableTerminalSessionPersistence") === "true";
const shouldRestore = isMobile || isElectron || persistenceEnabled;
if (!shouldRestore) {
return [{ id: 1, type: "home", title: "Home" }];
}
try {
const saved = localStorage.getItem("termix_tabs");
if (saved) {
const parsed = JSON.parse(saved) as Tab[];
const restored: Tab[] = [{ id: 1, type: "home", title: "Home" }];
let maxId = 1;
for (const tab of parsed) {
if (tab.type === "home") continue;
// Preserve instanceId from saved tab - critical for session lookup
const restoredTab: Tab = {
...tab,
instanceId: tab.instanceId, // Preserve instanceId for session persistence
terminalRef:
tab.type === "terminal"
? React.createRef<{ disconnect?: () => void }>()
: undefined,
hostConfig: tab.hostConfig
? {
...tab.hostConfig,
instanceId: tab.instanceId,
}
: undefined,
};
restored.push(restoredTab);
if (tab.id > maxId) maxId = tab.id;
}
if (restored.length > 1) return restored;
}
} catch {
/* ignore corrupt data */
}
return [{ id: 1, type: "home", title: "Home" }];
});
const [currentTab, setCurrentTab] = useState<number>(() => {
try {
const saved = localStorage.getItem("termix_currentTab");
if (saved) {
const parsed = parseInt(saved, 10);
// Validate against restored tabs
if (parsed && tabs.some((t) => t.id === parsed)) return parsed;
}
} catch {
/* ignore */
}
return 1;
});
const [allSplitScreenTab, setAllSplitScreenTab] = useState<number[]>([]);
const nextTabId = useRef(2);
const [initialMaxId] = useState(() => {
let maxId = 1;
tabs.forEach((tab) => {
if (tab.id > maxId) maxId = tab.id;
});
return maxId + 1;
});
const nextTabId = useRef(initialMaxId);
// Save tabs to localStorage on change
useEffect(() => {
// Check if persistence is enabled (always enabled for mobile/Electron)
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
const isElectron =
typeof window !== "undefined" && !!(window as any).electronAPI;
const persistenceEnabled =
localStorage.getItem("enableTerminalSessionPersistence") === "true";
const shouldSave = isMobile || isElectron || persistenceEnabled;
if (shouldSave) {
// Serialize tabs, preserving instanceId for session persistence
const serializable = tabs
.filter((t) => t.type !== "home")
.map(({ terminalRef, ...rest }) => rest);
localStorage.setItem("termix_tabs", JSON.stringify(serializable));
localStorage.setItem("termix_currentTab", String(currentTab));
} else {
// Clear saved tabs when persistence is disabled
localStorage.removeItem("termix_tabs");
localStorage.removeItem("termix_currentTab");
}
}, [tabs, currentTab]);
React.useEffect(() => {
setTabs((prev) =>
@@ -137,6 +238,7 @@ export function TabProvider({ children }: TabProviderProps) {
}
const id = nextTabId.current++;
const instanceId = `tab_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const needsUniqueTitle =
tabData.type === "terminal" ||
tabData.type === "server_stats" ||
@@ -149,11 +251,18 @@ export function TabProvider({ children }: TabProviderProps) {
const newTab: Tab = {
...tabData,
id,
instanceId,
title: effectiveTitle,
terminalRef:
tabData.type === "terminal"
? React.createRef<{ disconnect?: () => void }>()
: undefined,
hostConfig: tabData.hostConfig
? {
...tabData.hostConfig,
instanceId,
}
: undefined,
};
setTabs((prev) => [...prev, newTab]);
setCurrentTab(id);
@@ -202,7 +311,7 @@ export function TabProvider({ children }: TabProviderProps) {
setAllSplitScreenTab((prev) => {
if (prev.includes(tabId)) {
return prev.filter((id) => id !== tabId);
} else if (prev.length < 4) {
} else if (prev.length < 6) {
return [...prev, tabId];
}
return prev;
@@ -253,13 +362,19 @@ export function TabProvider({ children }: TabProviderProps) {
if (tab.type === "ssh_manager") {
return {
...tab,
hostConfig: newHostConfig,
hostConfig: {
...newHostConfig,
instanceId: tab.hostConfig.instanceId, // Preserve instanceId for session persistence
},
};
}
return {
...tab,
hostConfig: newHostConfig,
hostConfig: {
...newHostConfig,
instanceId: tab.hostConfig.instanceId, // Preserve instanceId for session persistence
},
title: newHostConfig.name?.trim()
? newHostConfig.name
: t("nav.hostTabTitle", {
+32
View File
@@ -146,6 +146,13 @@ export function UserProfile({
const saved = localStorage.getItem("confirmSnippetExecution");
return saved !== null ? saved === "true" : false;
});
const [
enableTerminalSessionPersistence,
setEnableTerminalSessionPersistence,
] = useState<boolean>(() => {
const saved = localStorage.getItem("enableTerminalSessionPersistence");
return saved === "true";
});
const [userRoles, setUserRoles] = useState<UserRole[]>([]);
useEffect(() => {
@@ -240,6 +247,14 @@ export function UserProfile({
localStorage.setItem("confirmSnippetExecution", enabled.toString());
};
const handleTerminalSessionPersistenceToggle = (enabled: boolean) => {
setEnableTerminalSessionPersistence(enabled);
localStorage.setItem(
"enableTerminalSessionPersistence",
enabled.toString(),
);
};
const handleDeleteAccount = async (e: React.FormEvent) => {
e.preventDefault();
setDeleteLoading(true);
@@ -607,6 +622,23 @@ export function UserProfile({
onCheckedChange={handleCommandPaletteShortcutToggle}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label className="text-foreground-secondary">
{t("profile.enableTerminalSessionPersistence")}{" "}
<span className="text-xs text-yellow-500 font-semibold">
(BETA)
</span>
</Label>
<p className="text-sm text-muted-foreground mt-1">
{t("profile.enableTerminalSessionPersistenceDesc")}
</p>
</div>
<Switch
checked={enableTerminalSessionPersistence}
onCheckedChange={handleTerminalSessionPersistenceToggle}
/>
</div>
</div>
</div>