LukeGus/Termix/main 2.2M tokens More Tools
```
├── .commitlintrc.json (100 tokens)
├── .dockerignore (100 tokens)
├── .editorconfig (omitted)
├── .gitattributes (omitted)
├── .github/
   ├── ISSUE_TEMPLATE/
      ├── config.yml (100 tokens)
   ├── dependabot.yml (200 tokens)
   ├── pull_request_template.md (100 tokens)
   ├── workflows/
      ├── docker.yml (500 tokens)
      ├── electron.yml (8.1k tokens)
      ├── openapi.yml (100 tokens)
      ├── pr-check.yml (100 tokens)
├── .gitignore (100 tokens)
├── .husky/
   ├── commit-msg
   ├── pre-commit
├── .npmrc
├── .nvmrc
├── .prettierignore (omitted)
├── .prettierrc
├── CODE_OF_CONDUCT.md (1000 tokens)
├── CONTRIBUTING.md (1100 tokens)
├── Casks/
   ├── termix.rb (200 tokens)
├── LICENSE (omitted)
├── README.md (2.2k tokens)
├── SECURITY.md
├── build/
   ├── Termix_Mac_App_Store.provisionprofile
   ├── entitlements.mac.inherit.plist (100 tokens)
   ├── entitlements.mac.plist (100 tokens)
   ├── entitlements.mas.inherit.plist (100 tokens)
   ├── entitlements.mas.plist (100 tokens)
   ├── notarize.cjs (200 tokens)
├── chocolatey/
   ├── termix-ssh.nuspec (400 tokens)
   ├── tools/
      ├── chocolateyinstall.ps1 (100 tokens)
      ├── chocolateyuninstall.ps1 (200 tokens)
├── components.json (100 tokens)
├── crowdin.yml
├── docker/
   ├── Dockerfile (500 tokens)
   ├── compose.dev.yml (100 tokens)
   ├── docker-compose.yml (100 tokens)
   ├── entrypoint.sh (900 tokens)
   ├── nginx-https.conf (4.3k tokens)
   ├── nginx.conf (4.3k tokens)
├── electron-builder.json (800 tokens)
├── electron/
   ├── main.cjs (13.9k tokens)
   ├── preload.js (400 tokens)
├── eslint.config.js (300 tokens)
├── flatpak/
   ├── com.karmaa.termix.desktop (100 tokens)
   ├── com.karmaa.termix.flatpakref (100 tokens)
   ├── com.karmaa.termix.metainfo.xml (500 tokens)
   ├── com.karmaa.termix.yml (600 tokens)
   ├── flathub.json
├── index.html (300 tokens)
├── package-lock.json (omitted)
├── package.json (1500 tokens)
├── public/
   ├── favicon.ico
   ├── fonts/
      ├── CaskaydiaCoveNerdFontMono-Bold.ttf
      ├── CaskaydiaCoveNerdFontMono-BoldItalic.ttf
      ├── CaskaydiaCoveNerdFontMono-Italic.ttf
      ├── CaskaydiaCoveNerdFontMono-Regular.ttf
   ├── full-icon.png
   ├── icon-mac.png
   ├── icon.icns
   ├── icon.ico
   ├── icon.png
   ├── icon.svg (8.8k tokens)
   ├── icons/
      ├── 1024x1024.png
      ├── 128x128.png
      ├── 16x16.png
      ├── 24x24.png
      ├── 256x256.png
      ├── 32x32.png
      ├── 48x48.png
      ├── 512x512.png
      ├── 64x64.png
      ├── icon.icns
      ├── icon.ico
   ├── manifest.json (200 tokens)
   ├── pdf.worker.min.js (omitted)
   ├── sw.js (400 tokens)
├── readme/
   ├── README-AR.md (2.1k tokens)
   ├── README-CN.md (1500 tokens)
   ├── README-DE.md (2.4k tokens)
   ├── README-ES.md (2.3k tokens)
   ├── README-FR.md (2.4k tokens)
   ├── README-HI.md (2.2k tokens)
   ├── README-IT.md (2.3k tokens)
   ├── README-JA.md (1700 tokens)
   ├── README-KO.md (1700 tokens)
   ├── README-PT.md (2.3k tokens)
   ├── README-RU.md (2.3k tokens)
   ├── README-TR.md (2.3k tokens)
   ├── README-VI.md (2.2k tokens)
├── repo-images/
   ├── HeaderImage.png
   ├── Image 1.png
   ├── Image 10.png
   ├── Image 11.png
   ├── Image 12.png
   ├── Image 2.png
   ├── Image 3.png
   ├── Image 4.png
   ├── Image 5.png
   ├── Image 6.png
   ├── Image 7.png
   ├── Image 8.png
   ├── Image 9.png
   ├── RepoOfTheDay.png
   ├── YouTube.jpg
├── scripts/
   ├── patch-app-builder-lib.cjs (1700 tokens)
   ├── write-electron-build-info.cjs (100 tokens)
├── src/
   ├── backend/
      ├── dashboard.ts (2.9k tokens)
      ├── database/
         ├── database.ts (12.4k tokens)
         ├── db/
            ├── index.ts (10.3k tokens)
            ├── schema.ts (3.9k tokens)
         ├── routes/
            ├── alerts.ts (1600 tokens)
            ├── c2s-tunnel-presets.ts (1500 tokens)
            ├── credentials.ts (11.5k tokens)
            ├── host.ts (33.4k tokens)
            ├── network-topology.ts (1200 tokens)
            ├── rbac.ts (8.5k tokens)
            ├── snippets-reorder.ts (200 tokens)
            ├── snippets.ts (7.5k tokens)
            ├── terminal.ts (2.4k tokens)
            ├── users.ts (27.8k tokens)
      ├── guacamole/
         ├── guacamole-server.ts (800 tokens)
         ├── routes.ts (1800 tokens)
         ├── token-service.ts (900 tokens)
      ├── scripts/
         ├── enable-ssl.sh (400 tokens)
         ├── setup-ssl.sh (600 tokens)
      ├── ssh/
         ├── auth-manager.ts (2.2k tokens)
         ├── docker-console.ts (4.1k tokens)
         ├── docker.ts (16.9k tokens)
         ├── file-manager.ts (34.5k tokens)
         ├── host-key-verifier.ts (2.4k tokens)
         ├── host-resolver.ts (1000 tokens)
         ├── opkssh-auth.ts (5.7k tokens)
         ├── opkssh-cert-auth.ts (1800 tokens)
         ├── server-stats.ts (19.2k tokens)
         ├── ssh-connection-pool.ts (1200 tokens)
         ├── terminal-session-manager.ts (2.3k tokens)
         ├── terminal.ts (15.7k tokens)
         ├── tmux-helper.ts (1000 tokens)
         ├── tunnel.ts (18.4k tokens)
         ├── widgets/
            ├── common-utils.ts (500 tokens)
            ├── cpu-collector.ts (500 tokens)
            ├── disk-collector.ts (400 tokens)
            ├── firewall-collector.ts (1200 tokens)
            ├── login-stats-collector.ts (800 tokens)
            ├── memory-collector.ts (300 tokens)
            ├── network-collector.ts (400 tokens)
            ├── ports-collector.ts (800 tokens)
            ├── processes-collector.ts (400 tokens)
            ├── system-collector.ts (200 tokens)
            ├── uptime-collector.ts (200 tokens)
      ├── starter.ts (1500 tokens)
      ├── swagger.ts (800 tokens)
      ├── utils/
         ├── auth-manager.ts (6.9k tokens)
         ├── auto-ssl-setup.ts (1600 tokens)
         ├── cors-config.ts (400 tokens)
         ├── credential-system-encryption-migration.ts (800 tokens)
         ├── data-crypto.ts (3.2k tokens)
         ├── database-file-encryption.ts (4.4k tokens)
         ├── database-migration.ts (2.4k tokens)
         ├── database-save-trigger.ts (600 tokens)
         ├── field-crypto.ts (600 tokens)
         ├── lazy-field-encryption.ts (2.6k tokens)
         ├── logger.ts (1800 tokens)
         ├── login-rate-limiter.ts (1600 tokens)
         ├── opkssh-binary-manager.ts (1200 tokens)
         ├── permission-manager.ts (2.1k tokens)
         ├── proxy-agent.ts (200 tokens)
         ├── proxy-helper.ts (1800 tokens)
         ├── request-origin.ts (400 tokens)
         ├── shared-credential-manager.ts (3.6k tokens)
         ├── simple-db-ops.ts (900 tokens)
         ├── socks5-helper.ts
         ├── ssh-algorithms.ts (400 tokens)
         ├── ssh-key-utils.ts (2.3k tokens)
         ├── system-crypto.ts (2.5k tokens)
         ├── user-agent-parser.ts (1500 tokens)
         ├── user-crypto.ts (3.4k tokens)
         ├── user-data-export.ts (1600 tokens)
         ├── user-data-import.ts (2.8k tokens)
         ├── wake-on-lan.ts (200 tokens)
   ├── components/
      ├── theme-provider.tsx (500 tokens)
      ├── ui/
         ├── accordion.tsx (400 tokens)
         ├── alert-dialog.tsx (800 tokens)
         ├── alert.tsx (300 tokens)
         ├── badge.tsx (300 tokens)
         ├── button-group.tsx (300 tokens)
         ├── button.tsx (500 tokens)
         ├── card.tsx (400 tokens)
         ├── chart.tsx (200 tokens)
         ├── checkbox.tsx (200 tokens)
         ├── command.tsx (1000 tokens)
         ├── dialog.tsx (800 tokens)
         ├── dropdown-menu.tsx (1500 tokens)
         ├── folder.tsx (3.7k tokens)
         ├── form.tsx (800 tokens)
         ├── input.tsx (200 tokens)
         ├── kbd.tsx (300 tokens)
         ├── label.tsx (100 tokens)
         ├── password-input.tsx (200 tokens)
         ├── popover.tsx (300 tokens)
         ├── progress.tsx (100 tokens)
         ├── resizable.tsx (400 tokens)
         ├── scroll-area.tsx (300 tokens)
         ├── select.tsx (1300 tokens)
         ├── separator.tsx (100 tokens)
         ├── shadcn-io/
            ├── status/
               ├── index.tsx (400 tokens)
         ├── sheet.tsx (800 tokens)
         ├── sidebar.tsx (4.3k tokens)
         ├── skeleton.tsx (100 tokens)
         ├── slider.tsx (400 tokens)
         ├── sonner.tsx (400 tokens)
         ├── switch.tsx (200 tokens)
         ├── table.tsx (500 tokens)
         ├── tabs.tsx (400 tokens)
         ├── textarea.tsx (200 tokens)
         ├── tooltip.tsx (400 tokens)
         ├── version-alert.tsx (700 tokens)
   ├── constants/
      ├── terminal-themes.ts (4k tokens)
   ├── hooks/
      ├── use-confirmation.ts (900 tokens)
      ├── use-mobile.ts (100 tokens)
      ├── use-service-worker.ts (500 tokens)
   ├── i18n/
      ├── i18n.ts (700 tokens)
   ├── index.css (2.9k tokens)
   ├── lib/
      ├── base-path.ts
      ├── client-cache-version.ts (300 tokens)
      ├── clipboard-provider.ts (300 tokens)
      ├── db-health-monitor.ts (800 tokens)
      ├── electron.ts (100 tokens)
      ├── frontend-logger.ts (2.1k tokens)
      ├── terminal-syntax-highlighter.ts (1100 tokens)
      ├── utils.ts
   ├── locales/
      ├── README.md
      ├── en.json (31.5k tokens)
      ├── translated/
         ├── af_ZA.json (32.5k tokens)
         ├── ar_SA.json (30.1k tokens)
         ├── bg_BG.json (34.8k tokens)
         ├── bn_BD.json (33.1k tokens)
         ├── ca_ES.json (34.8k tokens)
         ├── cs_CZ.json (32.5k tokens)
         ├── da_DK.json (32k tokens)
         ├── de_DE.json (34.2k tokens)
         ├── el_GR.json (35.5k tokens)
         ├── es_ES.json (34.2k tokens)
         ├── fi_FI.json (33.3k tokens)
         ├── fr_FR.json (35.6k tokens)
         ├── he_IL.json (28.9k tokens)
         ├── hi_IN.json (32.7k tokens)
         ├── hu_HU.json (34.1k tokens)
         ├── id_ID.json (32.6k tokens)
         ├── it_IT.json (33.8k tokens)
         ├── ja_JP.json (24.9k tokens)
         ├── ko_KR.json (25.1k tokens)
         ├── nl_NL.json (33.3k tokens)
         ├── no_NO.json (31.6k tokens)
         ├── pl_PL.json (33.2k tokens)
         ├── pt_BR.json (33.4k tokens)
         ├── pt_PT.json (33.3k tokens)
         ├── ro_RO.json (33.5k tokens)
         ├── ru_RU.json (33k tokens)
         ├── sr_SP.json (33.1k tokens)
         ├── sv_SE.json (32.4k tokens)
         ├── th_TH.json (31.8k tokens)
         ├── tr_TR.json (33.6k tokens)
         ├── uk_UA.json (33.1k tokens)
         ├── vi_VN.json (33k tokens)
         ├── zh_CN.json (21.8k tokens)
         ├── zh_TW.json (22.5k tokens)
   ├── main.tsx (1100 tokens)
   ├── types/
      ├── connection-log.ts (200 tokens)
      ├── electron.d.ts (omitted)
      ├── guacamole-common-js.d.ts (omitted)
      ├── index.ts (4.2k tokens)
      ├── stats-widgets.ts (300 tokens)
   ├── ui/
      ├── contexts/
         ├── ServerStatusContext.tsx (1100 tokens)
      ├── desktop/
         ├── DesktopApp.tsx (5.3k tokens)
         ├── apps/
            ├── FullScreenAppWrapper.tsx (800 tokens)
            ├── admin/
               ├── AdminSettings.tsx (3.2k tokens)
               ├── dialogs/
                  ├── CreateUserDialog.tsx (900 tokens)
                  ├── LinkAccountDialog.tsx (900 tokens)
                  ├── UserEditDialog.tsx (3.4k tokens)
               ├── tabs/
                  ├── ApiKeysTab.tsx (3.1k tokens)
                  ├── DatabaseSecurityTab.tsx (1600 tokens)
                  ├── GeneralSettingsTab.tsx (3.5k tokens)
                  ├── OIDCSettingsTab.tsx (2.1k tokens)
                  ├── RolesTab.tsx (1900 tokens)
                  ├── SessionManagementTab.tsx (1400 tokens)
                  ├── UserManagementTab.tsx (1200 tokens)
            ├── command-palette/
               ├── CommandPalette.tsx (5.2k tokens)
            ├── dashboard/
               ├── Dashboard.tsx (5.4k tokens)
               ├── apps/
                  ├── UpdateLog.tsx (1500 tokens)
                  ├── alerts/
                     ├── AlertCard.tsx (700 tokens)
                     ├── AlertManager.tsx (1000 tokens)
               ├── cards/
                  ├── NetworkGraphCard.tsx (12.6k tokens)
                  ├── QuickActionsCard.tsx (1000 tokens)
                  ├── RecentActivityCard.tsx (800 tokens)
                  ├── ServerOverviewCard.tsx (1200 tokens)
                  ├── ServerStatsCard.tsx (600 tokens)
               ├── components/
                  ├── DashboardSettingsDialog.tsx (700 tokens)
               ├── hooks/
                  ├── useDashboardPreferences.ts (400 tokens)
            ├── features/
               ├── docker/
                  ├── DockerApp.tsx (300 tokens)
                  ├── DockerManager.tsx (4.7k tokens)
                  ├── components/
                     ├── ConsoleTerminal.tsx (3.2k tokens)
                     ├── ContainerCard.tsx (2.8k tokens)
                     ├── ContainerDetail.tsx (700 tokens)
                     ├── ContainerList.tsx (1000 tokens)
                     ├── ContainerStats.tsx (1800 tokens)
                     ├── LogViewer.tsx (1700 tokens)
               ├── file-manager/
                  ├── DragIndicator.tsx (700 tokens)
                  ├── FileManager.tsx (15.7k tokens)
                  ├── FileManagerApp.tsx (300 tokens)
                  ├── FileManagerContextMenu.tsx (3.1k tokens)
                  ├── FileManagerGrid.tsx (9.8k tokens)
                  ├── FileManagerSidebar.tsx (4.9k tokens)
                  ├── SudoPasswordDialog.tsx (500 tokens)
                  ├── components/
                     ├── AudioPreview.tsx (600 tokens)
                     ├── CodeEditor.tsx (800 tokens)
                     ├── CompressDialog.tsx (1000 tokens)
                     ├── DiffViewer.tsx (2.1k tokens)
                     ├── DiffWindow.tsx (300 tokens)
                     ├── DraggableWindow.tsx (2.4k tokens)
                     ├── FileViewer.tsx (6.3k tokens)
                     ├── FileWindow.tsx (2.4k tokens)
                     ├── ImagePreview.tsx (700 tokens)
                     ├── MarkdownRenderer.tsx (900 tokens)
                     ├── PdfPreview.tsx (1000 tokens)
                     ├── PermissionsDialog.tsx (2.1k tokens)
                     ├── TerminalWindow.tsx (600 tokens)
                     ├── WindowManager.tsx (800 tokens)
                  ├── hooks/
                     ├── useDragAndDrop.ts (800 tokens)
                     ├── useFileSelection.ts (500 tokens)
               ├── guacamole/
                  ├── GuacamoleApp.tsx (600 tokens)
                  ├── GuacamoleDisplay.tsx (3.3k tokens)
               ├── server-stats/
                  ├── ServerStats.tsx (5.8k tokens)
                  ├── ServerStatsApp.tsx (300 tokens)
                  ├── widgets/
                     ├── CpuWidget.tsx (700 tokens)
                     ├── DiskWidget.tsx (800 tokens)
                     ├── FirewallWidget.tsx (1300 tokens)
                     ├── LoginStatsWidget.tsx (1100 tokens)
                     ├── MemoryWidget.tsx (900 tokens)
                     ├── NetworkWidget.tsx (500 tokens)
                     ├── PortsWidget.tsx (800 tokens)
                     ├── ProcessesWidget.tsx (600 tokens)
                     ├── SystemWidget.tsx (500 tokens)
                     ├── UptimeWidget.tsx (400 tokens)
                     ├── index.ts (100 tokens)
               ├── terminal/
                  ├── SudoPasswordPopup.tsx (500 tokens)
                  ├── Terminal.tsx (18.6k tokens)
                  ├── TerminalApp.tsx (300 tokens)
                  ├── TerminalPreview.tsx (700 tokens)
                  ├── command-history/
                     ├── CommandAutocomplete.tsx (400 tokens)
                     ├── CommandHistoryContext.tsx (500 tokens)
               ├── tunnel/
                  ├── Tunnel.tsx (1600 tokens)
                  ├── TunnelApp.tsx (300 tokens)
                  ├── TunnelInlineControls.tsx (900 tokens)
                  ├── TunnelManager.tsx (1100 tokens)
                  ├── TunnelModeSelector.tsx (400 tokens)
                  ├── TunnelObject.tsx (5k tokens)
                  ├── TunnelViewer.tsx (400 tokens)
                  ├── tunnel-form-utils.ts (300 tokens)
            ├── host-manager/
               ├── HostManagerApp.tsx (100 tokens)
               ├── credentials/
                  ├── CredentialEditor.tsx (4k tokens)
                  ├── CredentialSelector.tsx (1500 tokens)
                  ├── CredentialViewer.tsx (3.8k tokens)
                  ├── CredentialsManager.tsx (9.9k tokens)
                  ├── tabs/
                     ├── CredentialAuthenticationTab.tsx (4.2k tokens)
                     ├── CredentialGeneralTab.tsx (1400 tokens)
                     ├── shared/
                        ├── tab-types.ts (300 tokens)
               ├── dialogs/
                  ├── FolderEditDialog.tsx (1200 tokens)
               ├── hosts/
                  ├── HostManager.tsx (2k tokens)
                  ├── HostManagerEditor.tsx (10.2k tokens)
                  ├── HostManagerViewer.tsx (21.5k tokens)
                  ├── tabs/
                     ├── HostDockerTab.tsx (200 tokens)
                     ├── HostFileManagerTab.tsx (300 tokens)
                     ├── HostGeneralTab.tsx (13.3k tokens)
                     ├── HostRemoteDesktopTab.tsx (4.8k tokens)
                     ├── HostSharingTab.tsx (3.9k tokens)
                     ├── HostStatisticsTab.tsx (3.1k tokens)
                     ├── HostStatusTab.tsx (1100 tokens)
                     ├── HostTerminalTab.tsx (6.6k tokens)
                     ├── HostTunnelTab.tsx (5.6k tokens)
                     ├── shared/
                        ├── JumpHostItem.tsx (700 tokens)
                        ├── QuickActionItem.tsx (700 tokens)
                        ├── tab-types.ts (600 tokens)
            ├── tools/
               ├── SSHToolsSidebar.tsx (20.5k tokens)
         ├── authentication/
            ├── Auth.tsx (12k tokens)
            ├── ElectronLoginForm.tsx (1300 tokens)
            ├── ElectronServerConfig.tsx (1900 tokens)
         ├── navigation/
            ├── AppView.tsx (5.2k tokens)
            ├── LeftSidebar.tsx (4.4k tokens)
            ├── TopNavbar.tsx (3.9k tokens)
            ├── animations/
               ├── SimpleLoader.tsx (300 tokens)
            ├── connection-log/
               ├── ConnectionLog.tsx (1200 tokens)
               ├── ConnectionLogContext.tsx (400 tokens)
            ├── dialogs/
               ├── HostKeyVerificationDialog.tsx (1400 tokens)
               ├── OPKSSHDialog.tsx (1000 tokens)
               ├── PassphraseDialog.tsx (600 tokens)
               ├── QuickConnectDialog.tsx (5.1k tokens)
               ├── SSHAuthDialog.tsx (1900 tokens)
               ├── TOTPDialog.tsx (500 tokens)
               ├── TmuxSessionPicker.tsx (800 tokens)
               ├── WarpgateDialog.tsx (800 tokens)
            ├── hosts/
               ├── FolderCard.tsx (700 tokens)
               ├── Host.tsx (2.8k tokens)
            ├── tabs/
               ├── Tab.tsx (2.6k tokens)
               ├── TabContext.tsx (2.6k tokens)
               ├── TabDropdown.tsx (900 tokens)
         ├── user/
            ├── C2STunnelPresetManager.tsx (7.5k tokens)
            ├── ElectronVersionCheck.tsx (1300 tokens)
            ├── LanguageSwitcher.tsx (600 tokens)
            ├── PasswordReset.tsx (900 tokens)
            ├── TOTPSetup.tsx (3.1k tokens)
            ├── UserProfile.tsx (7.7k tokens)
      ├── hooks/
         ├── useCommandHistory.ts (700 tokens)
         ├── useCommandTracker.ts (700 tokens)
         ├── useDragToDesktop.ts (1500 tokens)
         ├── useDragToSystemDesktop.ts (1700 tokens)
      ├── main-axios.ts (26.6k tokens)
      ├── mobile/
         ├── MobileApp.tsx (1800 tokens)
         ├── apps/
            ├── navigation/
               ├── BottomNavbar.tsx (400 tokens)
               ├── LeftSidebar.tsx (1600 tokens)
               ├── hosts/
                  ├── FolderCard.tsx (600 tokens)
                  ├── Host.tsx (600 tokens)
               ├── tabs/
                  ├── TabContext.tsx (500 tokens)
            ├── terminal/
               ├── Terminal.tsx (8.5k tokens)
               ├── TerminalKeyboard.tsx (1100 tokens)
               ├── kb-dark-theme.css (100 tokens)
               ├── kb-light-theme.css (100 tokens)
         ├── authentication/
            ├── Auth.tsx (8.7k tokens)
         ├── navigation/
            ├── BottomNavbar.tsx (400 tokens)
            ├── LeftSidebar.tsx (1800 tokens)
            ├── hosts/
               ├── FolderCard.tsx (600 tokens)
               ├── Host.tsx (700 tokens)
            ├── tabs/
               ├── TabContext.tsx (500 tokens)
   ├── vite-env.d.ts (omitted)
├── tsconfig.app.json (200 tokens)
├── tsconfig.json (100 tokens)
├── tsconfig.node.json (200 tokens)
├── vendor/
   ├── rimraf-compat/
      ├── index.cjs (200 tokens)
      ├── package.json
├── vite.config.ts (600 tokens)
```


## /.commitlintrc.json

```json path="/.commitlintrc.json" 
{
  "extends": ["@commitlint/config-conventional"],
  "rules": {
    "type-enum": [
      2,
      "always",
      [
        "feat",
        "fix",
        "docs",
        "style",
        "refactor",
        "perf",
        "test",
        "chore",
        "revert"
      ]
    ],
    "subject-case": [0]
  }
}

```

## /.dockerignore

```dockerignore path="/.dockerignore" 
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*

dist
build
.next
.nuxt

.env.local
.env.development.local
.env.test.local
.env.production.local

.vscode
.idea
*.swp
*.swo
*~

.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

.git
.gitignore

README.md
CONTRIBUTING.md
LICENSE

repo-images/

uploads/

electron/
electron-builder.json

*.log
*.tmp
*.temp

logs
*.log

pids
*.pid
*.seed
*.pid.lock

coverage

.nyc_output

jspm_packages/

.npm

.eslintcache

.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

.node_repl_history

*.tgz

.yarn-integrity

.cache
.parcel-cache

.next

.nuxt

.vuepress/dist

.serverless

.fusebox/

.dynamodb/

.tern-port

```

## /.github/ISSUE_TEMPLATE/config.yml

```yml path="/.github/ISSUE_TEMPLATE/config.yml" 
blank_issues_enabled: false
contact_links:
  - name: Support Center
    url: https://github.com/Termix-SSH/Support/issues
    about: Report any feature requests or bugs in the support center
  - name: Discord
    url: https://discord.gg/jVQGdvHDrf
    about: Official Termix Discord server for general discussion and quick support

```

## /.github/dependabot.yml

```yml path="/.github/dependabot.yml" 
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "daily"
    groups:
      dev-patch-updates:
        dependency-type: "development"
        update-types:
          - "patch"
      dev-minor-updates:
        dependency-type: "development"
        update-types:
          - "minor"
      prod-patch-updates:
        dependency-type: "production"
        update-types:
          - "patch"
      prod-minor-updates:
        dependency-type: "production"
        update-types:
          - "minor"

  - package-ecosystem: "docker"
    directory: "/docker"
    schedule:
      interval: "daily"
    groups:
      patch-updates:
        update-types:
          - "patch"
      minor-updates:
        update-types:
          - "minor"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

```

## /.github/pull_request_template.md

# Overview

_Short summary of what this PR does_

- [ ] Added: ...
- [ ] Updated: ...
- [ ] Removed: ...
- [ ] Fixed: ...

# Changes Made

_Detailed explanation of changes (if needed)_

- ...

# Related Issues

_Link any issues this PR addresses_

- Closes #ISSUE_NUMBER
- Related to #ISSUE_NUMBER

# Screenshots / Demos

_(Optional: add before/after screenshots, GIFs, or console output)_

# Checklist

- [ ] Code follows project style guidelines
- [ ] Supports mobile and desktop UI/app (if applicable)
- [ ] I have read [Contributing.md](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)
- [ ] This is not a translation request. See [docs](https://docs.termix.site/translations)


## /.github/workflows/docker.yml

```yml path="/.github/workflows/docker.yml" 
name: Build and Push Docker Image

on:
  workflow_dispatch:
    inputs:
      version:
        description: "Version to build (e.g., 1.8.0)"
        required: true
      build_type:
        description: "Build type"
        required: true
        default: "Development"
        type: choice
        options:
          - Development
          - Production

jobs:
  build:
    runs-on: blacksmith-8vcpu-ubuntu-2404
    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v4
        with:
          platforms: linux/amd64,linux/arm64

      - name: Setup Docker Buildx
        uses: docker/setup-buildx-action@v4

      - name: Determine tags
        id: tags
        run: |
          VERSION=${{ github.event.inputs.version }}
          BUILD_TYPE=${{ github.event.inputs.build_type }}

          TAGS=()
          ALL_TAGS=()

          if [ "$BUILD_TYPE" = "Production" ]; then
            TAGS+=("release-$VERSION" "latest")
            for tag in "${TAGS[@]}"; do
              ALL_TAGS+=("ghcr.io/lukegus/termix:$tag")
              ALL_TAGS+=("docker.io/bugattiguy527/termix:$tag")
            done
          else
            TAGS+=("dev-$VERSION")
            for tag in "${TAGS[@]}"; do
              ALL_TAGS+=("ghcr.io/lukegus/termix:$tag")
            done
          fi

          echo "ALL_TAGS=$(IFS=,; echo "${ALL_TAGS[*]}")" >> $GITHUB_ENV

      - name: Login to GHCR
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: lukegus
          password: ${{ secrets.GHCR_TOKEN }}

      - name: Login to Docker Hub (prod only)
        if: ${{ github.event.inputs.build_type == 'Production' }}
        uses: docker/login-action@v4
        with:
          username: bugattiguy527
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push multi-arch image
        uses: docker/build-push-action@v7
        with:
          context: .
          file: ./docker/Dockerfile
          push: true
          platforms: linux/amd64,linux/arm64
          tags: ${{ env.ALL_TAGS }}
          build-args: |
            BUILDKIT_INLINE_CACHE=1
            BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
          labels: |
            org.opencontainers.image.source=https://github.com/${{ github.repository }}
            org.opencontainers.image.revision=${{ github.sha }}
            org.opencontainers.image.created=${{ github.run_id }}
          outputs: type=registry,compression=gzip,compression-level=9

      - name: Cleanup Docker
        if: always()
        run: |
          docker image prune -af
          docker system prune -af --volumes

```

## /.github/workflows/electron.yml

```yml path="/.github/workflows/electron.yml" 
name: Build and Push Electron App

on:
  workflow_dispatch:
    inputs:
      build_type:
        description: "Platform to build for"
        required: true
        default: "all"
        type: choice
        options:
          - all
          - windows
          - linux
          - macos
      artifact_destination:
        description: "What to do with the built app"
        required: true
        default: "file"
        type: choice
        options:
          - none
          - file
          - release
          - submit

jobs:
  build-windows:
    runs-on: windows-latest
    if: (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '') && github.event.inputs.artifact_destination != 'submit'
    permissions:
      contents: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: ".nvmrc"
          cache: "npm"

      - name: Install dependencies
        run: |
          $maxAttempts = 3
          $attempt = 1
          while ($attempt -le $maxAttempts) {
            try {
              npm ci
              break
            } catch {
              if ($attempt -eq $maxAttempts) {
                Write-Error "npm ci failed after $maxAttempts attempts"
                exit 1
              }
              Start-Sleep -Seconds 10
              $attempt++
            }
          }

      - name: Get version
        id: package-version
        run: |
          $VERSION = (Get-Content package.json | ConvertFrom-Json).version
          echo "version=$VERSION" >> $env:GITHUB_OUTPUT

      - name: Build Windows (All Architectures)
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: npm run build && npx electron-builder --win --x64 --ia32

      - name: Upload Windows x64 NSIS Installer
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_windows_x64_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_windows_x64_nsis
          path: release/termix_windows_x64_nsis.exe
          retention-days: 30

      - name: Upload Windows ia32 NSIS Installer
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_windows_ia32_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_windows_ia32_nsis
          path: release/termix_windows_ia32_nsis.exe
          retention-days: 30

      - name: Upload Windows x64 MSI Installer
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_windows_x64_msi.msi') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_windows_x64_msi
          path: release/termix_windows_x64_msi.msi
          retention-days: 30

      - name: Upload Windows ia32 MSI Installer
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_windows_ia32_msi.msi') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_windows_ia32_msi
          path: release/termix_windows_ia32_msi.msi
          retention-days: 30

      - name: Create Windows x64 Portable zip
        if: hashFiles('release/win-unpacked/*') != ''
        run: |
          Compress-Archive -Path "release\win-unpacked\*" -DestinationPath "termix_windows_x64_portable.zip"

      - name: Create Windows ia32 Portable zip
        if: hashFiles('release/win-ia32-unpacked/*') != ''
        run: |
          Compress-Archive -Path "release\win-ia32-unpacked\*" -DestinationPath "termix_windows_ia32_portable.zip"

      - name: Upload Windows x64 Portable
        uses: actions/upload-artifact@v4
        if: hashFiles('termix_windows_x64_portable.zip') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_windows_x64_portable
          path: termix_windows_x64_portable.zip
          retention-days: 30

      - name: Upload Windows ia32 Portable
        uses: actions/upload-artifact@v4
        if: hashFiles('termix_windows_ia32_portable.zip') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_windows_ia32_portable
          path: termix_windows_ia32_portable.zip
          retention-days: 30

  build-linux:
    runs-on: blacksmith-4vcpu-ubuntu-2404
    if: (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '') && github.event.inputs.artifact_destination != 'submit'
    permissions:
      contents: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: ".nvmrc"
          cache: "npm"

      - name: Install system dependencies for AppImage
        run: |
          sudo apt-get update
          sudo apt-get install -y libfuse2

      - name: Install dependencies
        run: |
          for i in 1 2 3;
          do
            if npm ci; then
              break
            else
              if [ $i -eq 3 ]; then
                exit 1
              fi
              sleep 10
            fi
          done
          npm install --force @rollup/rollup-linux-x64-gnu
          npm install --force @rollup/rollup-linux-arm64-gnu
          npm install --force @rollup/rollup-linux-arm-gnueabihf

      - name: Build Linux x64
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          DEBUG: electron-builder
        run: npm run build && npx electron-builder --linux --x64

      - name: Build Linux arm64 and armv7l
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: npx electron-builder --linux --arm64 --armv7l

      - name: Rename Linux artifacts for consistency
        run: |
          cd release

          if [ -f "termix_linux_amd64_deb.deb" ]; then
            mv "termix_linux_amd64_deb.deb" "termix_linux_x64_deb.deb"
          fi

          if [ -f "termix_linux_x86_64_appimage.AppImage" ]; then
            mv "termix_linux_x86_64_appimage.AppImage" "termix_linux_x64_appimage.AppImage"
          fi

          cd ..

      - name: Upload Linux x64 AppImage
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_linux_x64_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_linux_x64_appimage
          path: release/termix_linux_x64_appimage.AppImage
          retention-days: 30

      - name: Upload Linux arm64 AppImage
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_linux_arm64_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_linux_arm64_appimage
          path: release/termix_linux_arm64_appimage.AppImage
          retention-days: 30

      - name: Upload Linux armv7l AppImage
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_linux_armv7l_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_linux_armv7l_appimage
          path: release/termix_linux_armv7l_appimage.AppImage
          retention-days: 30

      - name: Upload Linux x64 DEB
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_linux_x64_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_linux_x64_deb
          path: release/termix_linux_x64_deb.deb
          retention-days: 30

      - name: Upload Linux arm64 DEB
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_linux_arm64_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_linux_arm64_deb
          path: release/termix_linux_arm64_deb.deb
          retention-days: 30

      - name: Upload Linux armv7l DEB
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_linux_armv7l_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_linux_armv7l_deb
          path: release/termix_linux_armv7l_deb.deb
          retention-days: 30

      - name: Upload Linux x64 tar.gz
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_linux_x64_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_linux_x64_portable
          path: release/termix_linux_x64_portable.tar.gz
          retention-days: 30

      - name: Upload Linux arm64 tar.gz
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_linux_arm64_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_linux_arm64_portable
          path: release/termix_linux_arm64_portable.tar.gz
          retention-days: 30

      - name: Upload Linux armv7l tar.gz
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_linux_armv7l_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_linux_armv7l_portable
          path: release/termix_linux_armv7l_portable.tar.gz
          retention-days: 30

      - name: Install Flatpak builder and dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y flatpak flatpak-builder imagemagick

      - name: Add Flathub repository
        run: |
          sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo

      - name: Install Flatpak runtime and SDK
        run: |
          sudo flatpak install -y flathub org.freedesktop.Platform//24.08
          sudo flatpak install -y flathub org.freedesktop.Sdk//24.08
          sudo flatpak install -y flathub org.electronjs.Electron2.BaseApp//24.08

      - name: Get version for Flatpak
        id: flatpak-version
        run: |
          VERSION=$(node -p "require('./package.json').version")
          RELEASE_DATE=$(date +%Y-%m-%d)
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT

      - name: Prepare Flatpak files
        run: |
          VERSION="${{ steps.flatpak-version.outputs.version }}"
          RELEASE_DATE="${{ steps.flatpak-version.outputs.release_date }}"

          CHECKSUM_X64=$(sha256sum "release/termix_linux_x64_appimage.AppImage" | awk '{print $1}')
          CHECKSUM_ARM64=$(sha256sum "release/termix_linux_arm64_appimage.AppImage" | awk '{print $1}')

          mkdir -p flatpak-build
          cp flatpak/com.karmaa.termix.yml flatpak-build/
          cp flatpak/com.karmaa.termix.desktop flatpak-build/
          cp flatpak/com.karmaa.termix.metainfo.xml flatpak-build/
          cp public/icon.svg flatpak-build/com.karmaa.termix.svg
          convert public/icon.png -resize 256x256 flatpak-build/icon-256.png
          convert public/icon.png -resize 128x128 flatpak-build/icon-128.png

          cd flatpak-build
          sed -i "s|https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_x64_appimage.AppImage|file://$(realpath ../release/termix_linux_x64_appimage.AppImage)|g" com.karmaa.termix.yml
          sed -i "s|https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_arm64_appimage.AppImage|file://$(realpath ../release/termix_linux_arm64_appimage.AppImage)|g" com.karmaa.termix.yml
          sed -i "s/CHECKSUM_X64_PLACEHOLDER/$CHECKSUM_X64/g" com.karmaa.termix.yml
          sed -i "s/CHECKSUM_ARM64_PLACEHOLDER/$CHECKSUM_ARM64/g" com.karmaa.termix.yml
          sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" com.karmaa.termix.metainfo.xml
          sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" com.karmaa.termix.metainfo.xml

      - name: Build Flatpak bundle
        run: |
          cd flatpak-build
          flatpak-builder --repo=repo --force-clean --disable-rofiles-fuse build-dir com.karmaa.termix.yml

          ARCH=$(uname -m)
          if [ "$ARCH" = "x86_64" ]; then
            FLATPAK_ARCH="x86_64"
          elif [ "$ARCH" = "aarch64" ]; then
            FLATPAK_ARCH="aarch64"
          else
            FLATPAK_ARCH="$ARCH"
          fi

          flatpak build-bundle repo ../release/termix_linux_flatpak.flatpak com.karmaa.termix --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo

      - name: Create flatpakref file
        run: |
          VERSION="${{ steps.flatpak-version.outputs.version }}"
          cp flatpak/com.karmaa.termix.flatpakref release/
          sed -i "s|VERSION_PLACEHOLDER|release-${VERSION}-tag|g" release/com.karmaa.termix.flatpakref

      - name: Upload Flatpak bundle
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_linux_flatpak.flatpak') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_linux_flatpak
          path: release/termix_linux_flatpak.flatpak
          retention-days: 30

      - name: Upload Flatpakref
        uses: actions/upload-artifact@v4
        if: hashFiles('release/com.karmaa.termix.flatpakref') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_linux_flatpakref
          path: release/com.karmaa.termix.flatpakref
          retention-days: 30

  build-macos:
    runs-on: macos-latest
    if: (github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all') && github.event.inputs.artifact_destination != 'submit'
    needs: []
    permissions:
      contents: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: ".nvmrc"
          cache: "npm"

      - name: Install dependencies
        run: |
          for i in 1 2 3;
          do
            if npm ci; then
              break
            else
              if [ $i -eq 3 ]; then
                exit 1
              fi
              sleep 10
            fi
          done
          npm install --force @rollup/rollup-darwin-arm64
          npm install dmg-license

      - name: Check for Code Signing Certificates
        id: check_certs
        run: |
          if [ -n "${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.MAC_P12_PASSWORD }}" ]; then
            echo "has_certs=true" >> $GITHUB_OUTPUT
          fi

      - name: Import Code Signing Certificates
        if: steps.check_certs.outputs.has_certs == 'true'
        env:
          MAC_BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}
          MAC_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MAC_INSTALLER_CERTIFICATE_BASE64 }}
          MAC_P12_PASSWORD: ${{ secrets.MAC_P12_PASSWORD }}
          MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }}
        run: |
          APP_CERT_PATH=$RUNNER_TEMP/app_certificate.p12
          INSTALLER_CERT_PATH=$RUNNER_TEMP/installer_certificate.p12
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

          echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $APP_CERT_PATH

          if [ -n "$MAC_INSTALLER_CERTIFICATE_BASE64" ]; then
            echo -n "$MAC_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $INSTALLER_CERT_PATH
          fi

          security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

          security import $APP_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH

          if [ -f "$INSTALLER_CERT_PATH" ]; then
            security import $INSTALLER_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          fi

          security list-keychain -d user -s $KEYCHAIN_PATH

          security find-identity -v -p codesigning $KEYCHAIN_PATH

      - name: Build macOS App Store Package
        if: steps.check_certs.outputs.has_certs == 'true'
        env:
          ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NODE_OPTIONS: --max-old-space-size=4096
        run: |
          CURRENT_VERSION=$(node -p "require('./package.json').version")
          BUILD_VERSION="${{ github.run_number }}"

          npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION"

      - name: Clean up MAS keychain before DMG build
        if: steps.check_certs.outputs.has_certs == 'true'
        run: |
          security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true

      - name: Check for Developer ID Certificates
        id: check_dev_id_certs
        run: |
          if [ -n "${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.DEVELOPER_ID_P12_PASSWORD }}" ]; then
            echo "has_dev_id_certs=true" >> $GITHUB_OUTPUT
          fi

      - name: Import Developer ID Certificates
        if: steps.check_dev_id_certs.outputs.has_dev_id_certs == 'true'
        env:
          DEVELOPER_ID_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }}
          DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64 }}
          DEVELOPER_ID_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_P12_PASSWORD }}
          MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }}
        run: |
          DEV_CERT_PATH=$RUNNER_TEMP/dev_certificate.p12
          DEV_INSTALLER_CERT_PATH=$RUNNER_TEMP/dev_installer_certificate.p12
          KEYCHAIN_PATH=$RUNNER_TEMP/dev-signing.keychain-db

          echo -n "$DEVELOPER_ID_CERTIFICATE_BASE64" | base64 --decode -o $DEV_CERT_PATH

          if [ -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" ]; then
            echo -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $DEV_INSTALLER_CERT_PATH
          fi

          security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

          security import $DEV_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH

          if [ -f "$DEV_INSTALLER_CERT_PATH" ]; then
            security import $DEV_INSTALLER_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          fi

          security list-keychain -d user -s $KEYCHAIN_PATH

          security find-identity -v -p codesigning $KEYCHAIN_PATH

      - name: Build macOS DMG
        env:
          ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          NODE_OPTIONS: --max-old-space-size=4096
        run: |
          if [ "${{ steps.check_certs.outputs.has_certs }}" != "true" ]; then
            npm run build
          fi
          export GH_TOKEN="${{ secrets.GITHUB_TOKEN }}"
          npx electron-builder --mac dmg --universal --x64 --arm64 --publish never

      - name: Upload macOS MAS PKG
        if: steps.check_certs.outputs.has_certs == 'true' && hashFiles('release/termix_macos_universal_mas.pkg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release' || github.event.inputs.artifact_destination == 'submit')
        uses: actions/upload-artifact@v4
        with:
          name: termix_macos_universal_mas
          path: release/termix_macos_universal_mas.pkg
          retention-days: 30
          if-no-files-found: warn

      - name: Upload macOS Universal DMG
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_macos_universal_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_macos_universal_dmg
          path: release/termix_macos_universal_dmg.dmg
          retention-days: 30

      - name: Upload macOS x64 DMG
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_macos_x64_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_macos_x64_dmg
          path: release/termix_macos_x64_dmg.dmg
          retention-days: 30

      - name: Upload macOS arm64 DMG
        uses: actions/upload-artifact@v4
        if: hashFiles('release/termix_macos_arm64_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
        with:
          name: termix_macos_arm64_dmg
          path: release/termix_macos_arm64_dmg.dmg
          retention-days: 30

      - name: Get version for Homebrew
        id: homebrew-version
        run: |
          VERSION=$(node -p "require('./package.json').version")
          echo "version=$VERSION" >> $GITHUB_OUTPUT

      - name: Generate Homebrew Cask
        if: hashFiles('release/termix_macos_universal_dmg.dmg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release')
        run: |
          VERSION="${{ steps.homebrew-version.outputs.version }}"
          DMG_PATH="release/termix_macos_universal_dmg.dmg"

          CHECKSUM=$(shasum -a 256 "$DMG_PATH" | awk '{print $1}')

          mkdir -p homebrew-generated
          cp Casks/termix.rb homebrew-generated/termix.rb

          sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-generated/termix.rb
          sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-generated/termix.rb
          sed -i '' "s|version \".*\"|version \"$VERSION\"|g" homebrew-generated/termix.rb
          sed -i '' "s|sha256 \".*\"|sha256 \"$CHECKSUM\"|g" homebrew-generated/termix.rb
          sed -i '' "s|release-[0-9.]*-tag|release-$VERSION-tag|g" homebrew-generated/termix.rb

      - name: Upload Homebrew Cask as artifact
        uses: actions/upload-artifact@v4
        if: hashFiles('homebrew-generated/termix.rb') != '' && github.event.inputs.artifact_destination == 'file'
        with:
          name: termix_macos_homebrew_cask
          path: homebrew-generated/termix.rb
          retention-days: 30

      - name: Upload Homebrew Cask to release
        if: hashFiles('homebrew-generated/termix.rb') != '' && github.event.inputs.artifact_destination == 'release'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          VERSION="${{ steps.homebrew-version.outputs.version }}"
          RELEASE_TAG="release-$VERSION-tag"

          gh release list --repo ${{ github.repository }} --limit 100 | grep -q "$RELEASE_TAG" || {
            echo "Release $RELEASE_TAG not found"
            exit 1
          }

          gh release upload "$RELEASE_TAG" homebrew-generated/termix.rb --repo ${{ github.repository }} --clobber

      - name: Clean up keychains
        if: always()
        run: |
          security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
          security delete-keychain $RUNNER_TEMP/dev-signing.keychain-db || true

  submit-to-chocolatey:
    runs-on: windows-latest
    if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '')
    permissions:
      contents: read

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Get version from package.json
        id: package-version
        run: |
          $VERSION = (Get-Content package.json | ConvertFrom-Json).version
          echo "version=$VERSION" >> $env:GITHUB_OUTPUT

      - name: Download and prepare MSI info from public release
        id: msi-info
        run: |
          $VERSION = "${{ steps.package-version.outputs.version }}"
          $MSI_NAME = "termix_windows_x64_msi.msi"
          $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$($VERSION)-tag/$($MSI_NAME)"

          Write-Host "Downloading from $DOWNLOAD_URL"
          New-Item -ItemType Directory -Force -Path "release_asset"
          $DOWNLOAD_PATH = "release_asset\$MSI_NAME"

          try {
            Invoke-WebRequest -Uri $DOWNLOAD_URL -OutFile $DOWNLOAD_PATH -UseBasicParsing
          } catch {
            Write-Error "Failed to download MSI from $DOWNLOAD_URL. Please ensure the release and asset exist."
            exit 1
          }

          $CHECKSUM = (Get-FileHash -Path $DOWNLOAD_PATH -Algorithm SHA256).Hash
          echo "msi_name=$MSI_NAME" >> $env:GITHUB_OUTPUT
          echo "checksum=$CHECKSUM" >> $env:GITHUB_OUTPUT

      - name: Prepare Chocolatey package
        run: |
          $VERSION = "${{ steps.package-version.outputs.version }}"
          $CHECKSUM = "${{ steps.msi-info.outputs.checksum }}"
          $MSI_NAME = "${{ steps.msi-info.outputs.msi_name }}"

          $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$MSI_NAME"

          New-Item -ItemType Directory -Force -Path "choco-build"
          Copy-Item -Path "chocolatey\*" -Destination "choco-build" -Recurse -Force

          $installScript = Get-Content "choco-build\tools\chocolateyinstall.ps1" -Raw -Encoding UTF8
          $installScript = $installScript -replace 'DOWNLOAD_URL_PLACEHOLDER', $DOWNLOAD_URL
          $installScript = $installScript -replace 'CHECKSUM_PLACEHOLDER', $CHECKSUM
          [System.IO.File]::WriteAllText("$PWD\choco-build\tools\chocolateyinstall.ps1", $installScript, [System.Text.UTF8Encoding]::new($false))

          $nuspec = Get-Content "choco-build\termix-ssh.nuspec" -Raw -Encoding UTF8
          $nuspec = $nuspec -replace 'VERSION_PLACEHOLDER', $VERSION
          [System.IO.File]::WriteAllText("$PWD\choco-build\termix-ssh.nuspec", $nuspec, [System.Text.UTF8Encoding]::new($false))

      - name: Install Chocolatey
        run: |
          Set-ExecutionPolicy Bypass -Scope Process -Force
          [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
          iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

      - name: Pack Chocolatey package
        run: |
          cd choco-build
          choco pack termix-ssh.nuspec

          if ($LASTEXITCODE -ne 0) {
            throw "Chocolatey push failed with exit code $LASTEXITCODE"
          }

      - name: Check for Chocolatey API Key
        id: check_choco_key
        run: |
          if ("${{ secrets.CHOCOLATEY_API_KEY }}" -ne "") {
            echo "has_key=true" >> $env:GITHUB_OUTPUT
          }

      - name: Push to Chocolatey
        if: steps.check_choco_key.outputs.has_key == 'true'
        run: |
          $VERSION = "${{ steps.package-version.outputs.version }}"
          cd choco-build
          choco apikey --key "${{ secrets.CHOCOLATEY_API_KEY }}" --source https://push.chocolatey.org/

          try {
            choco push "termix-ssh.$VERSION.nupkg" --source https://push.chocolatey.org/
            if ($LASTEXITCODE -eq 0) {
            } else {
              throw "Chocolatey push failed with exit code $LASTEXITCODE"
            }
          } catch {
          }

      - name: Upload Chocolatey package as artifact
        uses: actions/upload-artifact@v4
        with:
          name: chocolatey-package
          path: choco-build/*.nupkg
          retention-days: 30

  submit-to-flatpak:
    runs-on: blacksmith-4vcpu-ubuntu-2404
    if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '')
    needs: []
    permissions:
      contents: read

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Get version from package.json
        id: package-version
        run: |
          VERSION=$(node -p "require('./package.json').version")
          RELEASE_DATE=$(date +%Y-%m-%d)
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT

      - name: Download and prepare AppImage info from public release
        id: appimage-info
        run: |
          VERSION="${{ steps.package-version.outputs.version }}"
          mkdir -p release_assets

          APPIMAGE_X64_NAME="termix_linux_x64_appimage.AppImage"
          URL_X64="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_X64_NAME"
          PATH_X64="release_assets/$APPIMAGE_X64_NAME"
          echo "Downloading x64 AppImage from $URL_X64"
          curl -L -o "$PATH_X64" "$URL_X64"
          chmod +x "$PATH_X64"
          CHECKSUM_X64=$(sha256sum "$PATH_X64" | awk '{print $1}')

          APPIMAGE_ARM64_NAME="termix_linux_arm64_appimage.AppImage"
          URL_ARM64="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_ARM64_NAME"
          PATH_ARM64="release_assets/$APPIMAGE_ARM64_NAME"
          echo "Downloading arm64 AppImage from $URL_ARM64"
          curl -L -o "$PATH_ARM64" "$URL_ARM64"
          chmod +x "$PATH_ARM64"
          CHECKSUM_ARM64=$(sha256sum "$PATH_ARM64" | awk '{print $1}')

          echo "appimage_x64_name=$APPIMAGE_X64_NAME" >> $GITHUB_OUTPUT
          echo "checksum_x64=$CHECKSUM_X64" >> $GITHUB_OUTPUT
          echo "appimage_arm64_name=$APPIMAGE_ARM64_NAME" >> $GITHUB_OUTPUT
          echo "checksum_arm64=$CHECKSUM_ARM64" >> $GITHUB_OUTPUT

      - name: Install ImageMagick for icon generation
        run: |
          sudo apt-get update
          sudo apt-get install -y imagemagick

      - name: Prepare Flatpak submission files
        run: |
          VERSION="${{ steps.package-version.outputs.version }}"
          CHECKSUM_X64="${{ steps.appimage-info.outputs.checksum_x64 }}"
          CHECKSUM_ARM64="${{ steps.appimage-info.outputs.checksum_arm64 }}"
          RELEASE_DATE="${{ steps.package-version.outputs.release_date }}"
          APPIMAGE_X64_NAME="${{ steps.appimage-info.outputs.appimage_x64_name }}"
          APPIMAGE_ARM64_NAME="${{ steps.appimage-info.outputs.appimage_arm64_name }}"

          mkdir -p flatpak-submission

          cp flatpak/com.karmaa.termix.yml flatpak-submission/
          cp flatpak/com.karmaa.termix.desktop flatpak-submission/
          cp flatpak/com.karmaa.termix.metainfo.xml flatpak-submission/
          cp flatpak/flathub.json flatpak-submission/

          cp public/icon.svg flatpak-submission/com.karmaa.termix.svg
          convert public/icon.png -resize 256x256 flatpak-submission/icon-256.png
          convert public/icon.png -resize 128x128 flatpak-submission/icon-128.png

          sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.yml
          sed -i "s/CHECKSUM_X64_PLACEHOLDER/$CHECKSUM_X64/g" flatpak-submission/com.karmaa.termix.yml
          sed -i "s/CHECKSUM_ARM64_PLACEHOLDER/$CHECKSUM_ARM64/g" flatpak-submission/com.karmaa.termix.yml

          sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.metainfo.xml
          sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak-submission/com.karmaa.termix.metainfo.xml

      - name: Upload Flatpak submission as artifact
        uses: actions/upload-artifact@v4
        with:
          name: flatpak-submission
          path: flatpak-submission/*
          retention-days: 30

  submit-to-homebrew:
    runs-on: macos-latest
    if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'macos')
    needs: []
    permissions:
      contents: read

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Get version from package.json
        id: package-version
        run: |
          VERSION=$(node -p "require('./package.json').version")
          echo "version=$VERSION" >> $GITHUB_OUTPUT

      - name: Download and prepare DMG info from public release
        id: dmg-info
        run: |
          VERSION="${{ steps.package-version.outputs.version }}"
          DMG_NAME="termix_macos_universal_dmg.dmg"
          URL="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$DMG_NAME"

          mkdir -p release_asset
          DOWNLOAD_PATH="release_asset/$DMG_NAME"
          echo "Downloading DMG from $URL"

          if command -v curl &> /dev/null; then
            curl -L -o "$DOWNLOAD_PATH" "$URL"
          elif command -v wget &> /dev/null; then
            wget -O "$DOWNLOAD_PATH" "$URL"
          else
            echo "Neither curl nor wget is available, installing curl"
            brew install curl
            curl -L -o "$DOWNLOAD_PATH" "$URL"
          fi

          CHECKSUM=$(shasum -a 256 "$DOWNLOAD_PATH" | awk '{print $1}')

          echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT
          echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT

      - name: Prepare Homebrew submission files
        run: |
          VERSION="${{ steps.package-version.outputs.version }}"
          CHECKSUM="${{ steps.dmg-info.outputs.checksum }}"
          DMG_NAME="${{ steps.dmg-info.outputs.dmg_name }}"

          mkdir -p homebrew-submission/Casks/t

          cp Casks/termix.rb homebrew-submission/Casks/t/termix.rb

          sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb
          sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb

      - name: Verify Cask syntax
        run: |
          ruby -c homebrew-submission/Casks/t/termix.rb

      - name: Upload Homebrew submission as artifact
        uses: actions/upload-artifact@v4
        with:
          name: homebrew-submission
          path: homebrew-submission/*
          retention-days: 30

  upload-to-release:
    runs-on: blacksmith-4vcpu-ubuntu-2404
    if: github.event.inputs.artifact_destination == 'release'
    needs: [build-windows, build-linux, build-macos]
    permissions:
      contents: write

    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Get latest release tag
        id: get_release
        run: |
          echo "RELEASE_TAG=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName -q '.[0].tagName')" >> $GITHUB_ENV
        env:
          GH_TOKEN: ${{ github.token }}

      - name: Upload artifacts to latest release
        run: |
          cd artifacts
          for dir in */; do
            cd "$dir"
            for file in *;
            do
              if [ -f "$file" ]; then
                gh release upload "$RELEASE_TAG" "$file" --repo ${{ github.repository }} --clobber
              fi
            done
            cd ..
          done
        env:
          GH_TOKEN: ${{ github.token }}

  submit-to-testflight:
    runs-on: macos-latest
    if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'macos')
    needs: []
    permissions:
      contents: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5
        with:
          fetch-depth: 1

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: ".nvmrc"
          cache: "npm"

      - name: Install dependencies
        run: |
          for i in 1 2 3;
          do
            if npm ci; then
              break
            else
              if [ $i -eq 3 ]; then
                exit 1
              fi
              sleep 10
            fi
          done
          npm install --force @rollup/rollup-darwin-arm64
          npm install dmg-license

      - name: Check for Code Signing Certificates
        id: check_certs
        run: |
          if [ -n "${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.MAC_P12_PASSWORD }}" ]; then
            echo "has_certs=true" >> $GITHUB_OUTPUT
          fi

      - name: Import Code Signing Certificates
        if: steps.check_certs.outputs.has_certs == 'true'
        env:
          MAC_BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}
          MAC_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MAC_INSTALLER_CERTIFICATE_BASE64 }}
          MAC_P12_PASSWORD: ${{ secrets.MAC_P12_PASSWORD }}
          MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }}
        run: |
          APP_CERT_PATH=$RUNNER_TEMP/app_certificate.p12
          INSTALLER_CERT_PATH=$RUNNER_TEMP/installer_certificate.p12
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

          echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $APP_CERT_PATH

          if [ -n "$MAC_INSTALLER_CERTIFICATE_BASE64" ]; then
            echo -n "$MAC_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $INSTALLER_CERT_PATH
          fi

          security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

          security import $APP_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH

          if [ -f "$INSTALLER_CERT_PATH" ]; then
            security import $INSTALLER_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          fi

          security list-keychain -d user -s $KEYCHAIN_PATH

          security find-identity -v -p codesigning $KEYCHAIN_PATH

      - name: Build macOS App Store Package
        if: steps.check_certs.outputs.has_certs == 'true'
        env:
          ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NODE_OPTIONS: --max-old-space-size=4096
        run: |
          CURRENT_VERSION=$(node -p "require('./package.json').version")
          BUILD_VERSION="${{ github.run_number }}"

          npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION"

      - name: Check for App Store Connect API credentials
        id: check_asc_creds
        run: |
          if [ -n "${{ secrets.APPLE_KEY_ID }}" ] && [ -n "${{ secrets.APPLE_ISSUER_ID }}" ] && [ -n "${{ secrets.APPLE_KEY_CONTENT }}" ]; then
            echo "has_credentials=true" >> $GITHUB_OUTPUT
          fi

      - name: Setup Ruby for Fastlane
        if: steps.check_asc_creds.outputs.has_credentials == 'true'
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.2"
          bundler-cache: false

      - name: Install Fastlane
        if: steps.check_asc_creds.outputs.has_credentials == 'true'
        run: |
          gem install fastlane -N

      - name: Deploy to App Store Connect (TestFlight)
        if: steps.check_asc_creds.outputs.has_credentials == 'true'
        run: |
          PKG_FILE=$(find release -name "termix_macos_universal_mas.pkg" -type f | head -n 1)
          if [ -z "$PKG_FILE" ]; then
            echo "PKG file not found, exiting."
            exit 1
          fi

          mkdir -p ~/private_keys
          echo "${{ secrets.APPLE_KEY_CONTENT }}" | base64 --decode > ~/private_keys/AuthKey_${{ secrets.APPLE_KEY_ID }}.p8

          xcrun altool --upload-app -f "$PKG_FILE" \
            --type macos \
            --apiKey "${{ secrets.APPLE_KEY_ID }}" \
            --apiIssuer "${{ secrets.APPLE_ISSUER_ID }}"
        continue-on-error: true

      - name: Clean up keychains
        if: always()
        run: |
          security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true

```

## /.github/workflows/openapi.yml

```yml path="/.github/workflows/openapi.yml" 
name: Generate OpenAPI Specification

on:
  workflow_dispatch:

jobs:
  generate-openapi:
    name: Generate OpenAPI JSON
    runs-on: blacksmith-2vcpu-ubuntu-2404

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: ".nvmrc"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Generate OpenAPI specification
        run: npm run generate:openapi

      - name: Upload OpenAPI artifact
        uses: actions/upload-artifact@v4
        with:
          name: openapi-spec
          path: openapi.json
          retention-days: 90

```

## /.github/workflows/pr-check.yml

```yml path="/.github/workflows/pr-check.yml" 
name: PR Check

on:
  pull_request:
    branches: [main, dev-*]

jobs:
  lint-and-build:
    runs-on: blacksmith-2vcpu-ubuntu-2404

    env:
      NODE_OPTIONS: "--max-old-space-size=4096"

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: ".nvmrc"

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint
        run: npx eslint .

      - name: Run Prettier check
        run: npx prettier --check .

      - name: Type check
        run: npx tsc --noEmit

      - name: Build
        run: npm run build

```

## /.gitignore

```gitignore path="/.gitignore" 
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

.vscode/
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

/db/
/release/
/.claude/
/ssl/
/uploads/
/nul
.env
electron/build-info.cjs
/.mcp.json
/CLAUDE.md

```

## /.husky/commit-msg

```husky/commit-msg path="/.husky/commit-msg" 
npx --no -- commitlint --edit $1

```

## /.husky/pre-commit

```husky/pre-commit path="/.husky/pre-commit" 
npx lint-staged

```

## /.npmrc

```npmrc path="/.npmrc" 
legacy-peer-deps=true

```

## /.nvmrc

```nvmrc path="/.nvmrc" 
24

```

## /.prettierrc

```prettierrc path="/.prettierrc" 
{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80,
  "arrowParens": "always",
  "endOfLine": "lf"
}

```

## /CODE_OF_CONDUCT.md

# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

- The use of sexualized language or imagery, and sexual attention or
  advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
  address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
mail@termix.site.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.

Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.


## /CONTRIBUTING.md

# Contributing

## Prerequisites

- [Node.js](https://nodejs.org/en/download/) (built with v24)
- [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
- [Git](https://git-scm.com/downloads)

## Installation

1. Clone the repository:
   ```sh
   git clone https://github.com/Termix-SSH/Termix
   ```
2. Install the dependencies:
   ```sh
   npm install
   ```

## Running the development server

Run the following commands:

```sh
npm run dev
npm run dev:backend
```

This will start the backend and the frontend Vite server. You can access Termix by going to `http://localhost:5174/`.

## Contributing

1. **Fork the repository**: Click the "Fork" button at the top right of
   the [repository page](https://github.com/Termix-SSH/Termix).
2. **Create a new branch**:
   ```sh
   git checkout -b feature/my-new-feature
   ```
3. **Make your changes**: Implement your feature, fix, or improvement.
4. **Commit your changes**:
   ```sh
   git commit -m "Feature request my new feature"
   ```
5. **Push to your fork**:
   ```sh
   git push origin feature/my-feature-request
   ```
6. **Open a pull request**: Go to the original repository and create a PR with a clear description.

## Guidelines

- Follow the existing code style. Use Tailwind CSS with shadcn components.
- Use the below color scheme with the respective CSS variable placed in the `className` of a div/component.
- Place all API routes in the `main-axios.ts` file. Updating the `openapi.json` is unneeded.
- Include meaningful commit messages.
- Link related issues when applicable.
- `MobileApp.tsx` renders when the users screen width is less than 768px, otherwise it loads the usual `DesktopApp.tsx`.

## Color Scheme

### Background Colors

| CSS Variable                  | Color Value | Usage                       | Description                              |
| ----------------------------- | ----------- | --------------------------- | ---------------------------------------- |
| `--color-dark-bg`             | `#18181b`   | Main dark background        | Primary dark background color            |
| `--color-dark-bg-darker`      | `#0e0e10`   | Darker backgrounds          | Darker variant for panels and containers |
| `--color-dark-bg-darkest`     | `#09090b`   | Darkest backgrounds         | Darkest background (terminal)            |
| `--color-dark-bg-light`       | `#141416`   | Light dark backgrounds      | Lighter variant of dark background       |
| `--color-dark-bg-very-light`  | `#101014`   | Very light dark backgrounds | Very light variant of dark background    |
| `--color-dark-bg-panel`       | `#1b1b1e`   | Panel backgrounds           | Background for panels and cards          |
| `--color-dark-bg-panel-hover` | `#232327`   | Panel hover states          | Background for panels on hover           |

### Element-Specific Backgrounds

| CSS Variable             | Color Value | Usage              | Description                                   |
| ------------------------ | ----------- | ------------------ | --------------------------------------------- |
| `--color-dark-bg-input`  | `#222225`   | Input fields       | Background for input fields and form elements |
| `--color-dark-bg-button` | `#23232a`   | Button backgrounds | Background for buttons and clickable elements |
| `--color-dark-bg-active` | `#1d1d1f`   | Active states      | Background for active/selected elements       |
| `--color-dark-bg-header` | `#131316`   | Header backgrounds | Background for headers and navigation bars    |

### Border Colors

| CSS Variable                 | Color Value | Usage           | Description                              |
| ---------------------------- | ----------- | --------------- | ---------------------------------------- |
| `--color-dark-border`        | `#303032`   | Default borders | Standard border color                    |
| `--color-dark-border-active` | `#2d2d30`   | Active borders  | Border color for active elements         |
| `--color-dark-border-hover`  | `#434345`   | Hover borders   | Border color on hover states             |
| `--color-dark-border-light`  | `#5a5a5d`   | Light borders   | Lighter border color for subtle elements |
| `--color-dark-border-medium` | `#373739`   | Medium borders  | Medium weight border color               |
| `--color-dark-border-panel`  | `#222224`   | Panel borders   | Border color for panels and cards        |

### Interactive States

| CSS Variable             | Color Value | Usage             | Description                                   |
| ------------------------ | ----------- | ----------------- | --------------------------------------------- |
| `--color-dark-hover`     | `#2d2d30`   | Hover states      | Background color for hover effects            |
| `--color-dark-active`    | `#2a2a2c`   | Active states     | Background color for active elements          |
| `--color-dark-pressed`   | `#1a1a1c`   | Pressed states    | Background color for pressed/clicked elements |
| `--color-dark-hover-alt` | `#2a2a2d`   | Alternative hover | Alternative hover state color                 |

## Support

If you need help or want to request a feature with Termix, visit the [Issues](https://github.com/Termix-SSH/Support/issues) page, log in, and press `New Issue`.
Please be as detailed as possible in your issue, preferably written in English. You can also join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support
channel, however, response times may be longer.


## /Casks/termix.rb

```rb path="/Casks/termix.rb" 
cask "termix" do
  version "2.2.1"
  sha256 "7fe7bde53df568c3212a212e6dc0ca4b77c24a3d3d9b740dddadcd765330e93d"

  url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg"
  name "Termix"
  desc "Web-based server management platform with SSH terminal, tunneling, and file editing"
  homepage "https://github.com/Termix-SSH/Termix"

  livecheck do
    url :url
    strategy :github_latest
  end

  app "Termix.app"

  zap trash: [
    "~/Library/Application Support/termix",
    "~/Library/Caches/com.karmaa.termix",
    "~/Library/Caches/com.karmaa.termix.ShipIt",
    "~/Library/Preferences/com.karmaa.termix.plist",
    "~/Library/Saved Application State/com.karmaa.termix.savedState",
  ]
end

```

## /README.md

# Repo Stats

<p align="center">
🇺🇸 English · <a href="readme/README-CN.md">🇨🇳 中文</a> · <a href="readme/README-JA.md">🇯🇵 日本語</a> · <a href="readme/README-KO.md">🇰🇷 한국어</a> · <a href="readme/README-FR.md">🇫🇷 Français</a> · <a href="readme/README-DE.md">🇩🇪 Deutsch</a> · <a href="readme/README-ES.md">🇪🇸 Español</a> · <a href="readme/README-PT.md">🇧🇷 Português</a> · <a href="readme/README-RU.md">🇷🇺 Русский</a> · <a href="readme/README-AR.md">🇸🇦 العربية</a> · <a href="readme/README-HI.md">🇮🇳 हिन्दी</a> · <a href="readme/README-TR.md">🇹🇷 Türkçe</a> · <a href="readme/README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="readme/README-IT.md">🇮🇹 Italiano</a>
</p>

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release&v=1)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>

<p align="center">
  <img src="./repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
  <br>
  <small style="color: #666;">Achieved on September 1st, 2025</small>
</p>

<br />
<p align="center">
  <a href="https://github.com/Termix-SSH/Termix">
    <img alt="Termix Banner" src=./repo-images/HeaderImage.png style="width: auto; height: auto;">  </a>
</p>

# Overview

<p align="center">
  <a href="https://github.com/Termix-SSH/Termix">
    <img alt="Termix Banner" src=./public/icon.svg style="width: 250px; height: 250px;">  </a>
</p>

Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a multi-platform
solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal
access, remote desktop control (RDP, VNC, Telnet), SSH tunneling capabilities, remote SSH file management, and many other tools. Termix is the perfect
free and self-hosted alternative to Termius available for all platforms.

# Features

- **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) with a browser-like tab system. Includes support for customizing the terminal including common terminal themes, fonts, and other components.
- **Remote Desktop Access** - RDP, VNC, and Telnet support over the browser with complete customization and split screening
- **SSH Tunnel Management** - Create and manage server-to-server SSH tunnels with automatic reconnection, health monitoring, and local, remote, or dynamic SOCKS forwarding. Desktop client-to-server tunnel settings are stored locally per desktop install, optional C2S preset snapshots can be saved to the server, renamed, loaded, or deleted when you want to move a local tunnel configuration between clients.
- **Remote File Manager** - Manage files directly on remote servers with support for viewing and editing code, images, audio, and video. Upload, download, rename, delete, and move files seamlessly with sudo support.
- **Docker Management** - Start, stop, pause, remove containers. View container stats. Control container using docker exec terminal. It was not made to replace Portainer or Dockge but rather to simply manage your containers compared to creating them.
- **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders, and easily save reusable login info while being able to automate the deployment of SSH keys
- **Server Stats** - View CPU, memory, and disk usage along with network, uptime, system information, firewall, port monitor, on most Linux based servers
- **Dashboard** - View server information at a glance on your dashboard
- **RBAC** - Create roles and share hosts across users/roles
- **User Authentication** - Secure user management with admin controls and OIDC (with access control) and 2FA (TOTP) support. View active user sessions across all platforms and revoke permissions. Link your OIDC/Local accounts together.
- **Database Encryption** - Backend stored as encrypted SQLite database files. View [docs](https://docs.termix.site/security) for more.
- **API Keys** - Create user-scoped API keys with expiration dates to be used for automation/CI
- **Data Export/Import** - Export and import SSH hosts, credentials, and file manager data
- **Automatic SSL Setup** - Built-in SSL certificate generation and management with HTTPS redirects
- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn. Choose between many different UI themes including light, dark, Dracula, etc. Use URL routes to open any connection in full-screen.
- **Languages** - Built-in support ~30 languages (managed by [Crowdin](https://docs.termix.site/translations))
- **Platform Support** - Available as a web app, desktop application (Windows, Linux, and macOS, can be run standalone without Termix backend), PWA, and dedicated mobile/tablet app for iOS and Android.
- **SSH Tools** - Create reusable command snippets that execute with a single click. Run one command simultaneously across multiple open terminals.
- **Command History** - Auto-complete and view previously ran SSH commands
- **Quick Connect** - Connect to a server without having to save the connection data
- **Command Palette** - Double tap left shift to quickly access SSH connections with your keyboard
- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, etc.
- **Network Graph** - Customize your Dashboard to visualize your homelab based off your SSH connections with status support
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile

# Planned Features

See [Projects](https://github.com/orgs/Termix-SSH/projects/2) for all planned features. If you are looking to contribute, see [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).

# Installation

Supported Devices:

- Website (any modern browser on any platform like Chrome, Safari, and Firefox) (includes PWA support)
- Windows (x64/ia32)
  - Portable
  - MSI Installer
  - Chocolatey Package Manager
- Linux (x64/ia32)
  - Portable
  - AUR
  - AppImage
  - Deb
  - Flatpak
- macOS (x64/ia32 on v12.0+)
  - Apple App Store
  - DMG
  - Homebrew
- iOS/iPadOS (v15.1+)
  - Apple App Store
  - IPA
- Android (v7.0+)
  - Google Play Store
  - APK

Visit the Termix [Docs](https://docs.termix.site/install) for more information on how to install Termix on all platforms. Otherwise, view
a sample Docker Compose file here (you can omit guacd and the network if you don't plan on using remote desktop features):

```yaml
services:
  termix:
    image: ghcr.io/lukegus/termix:latest
    container_name: termix
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - termix-data:/app/data
    environment:
      PORT: "8080"
    depends_on:
      - guacd
    networks:
      - termix-net

  guacd:
    image: guacamole/guacd:1.6.0
    container_name: guacd
    restart: unless-stopped
    ports:
      - "4822:4822"
    networks:
      - termix-net

volumes:
  termix-data:
    driver: local

networks:
  termix-net:
    driver: bridge
```

# Sponsors

<p align="left">
  <a href="https://www.digitalocean.com/">
    <img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://crowdin.com/">
    <img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://www.blacksmith.sh/">
    <img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://www.cloudflare.com/">
    <img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdflare">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://tailscale.com/">
    <img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://akamai.com/">
    <img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://aws.amazon.com/">
    <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
  </a>
</p>

# Support

If you need help or want to request a feature with Termix, visit the [Issues](https://github.com/Termix-SSH/Support/issues) page, log in, and press `New Issue`.
Please be as detailed as possible in your issue, preferably written in English. You can also join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support
channel, however, response times may be longer.

# Screenshots

[![YouTube](./repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)

<p align="center">
  <img src="./repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
  <img src="./repo-images/Image 2.png" width="400" alt="Termix Demo 2"/>
</p>

<p align="center">
  <img src="./repo-images/Image 3.png" width="400" alt="Termix Demo 3"/>
  <img src="./repo-images/Image 4.png" width="400" alt="Termix Demo 4"/>
</p>

<p align="center">
  <img src="./repo-images/Image 5.png" width="400" alt="Termix Demo 5"/>
  <img src="./repo-images/Image 6.png" width="400" alt="Termix Demo 6"/>
</p>

<p align="center">
  <img src="./repo-images/Image 7.png" width="400" alt="Termix Demo 7"/>
  <img src="./repo-images/Image 8.png" width="400" alt="Termix Demo 8"/>
</p>

<p align="center">
  <img src="./repo-images/Image 9.png" width="400" alt="Termix Demo 9"/>
  <img src="./repo-images/Image 10.png" width="400" alt="Termix Demo 10"/>
</p>

<p align="center">
  <img src="./repo-images/Image 11.png" width="400" alt="Termix Demo 11"/>
  <img src="./repo-images/Image 12.png" width="400" alt="Termix Demo 12"/>
</p>

Some videos and images may be out of date or may not perfectly showcase features.

# License

Distributed under the Apache License Version 2.0. See LICENSE for more information.


## /SECURITY.md

# Security Policy

## Reporting a Vulnerability

Please report any vulnerabilities to [GitHub Security](https://github.com/Termix-SSH/Termix/security/advisories).


## /build/Termix_Mac_App_Store.provisionprofile

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/build/Termix_Mac_App_Store.provisionprofile

## /build/entitlements.mac.inherit.plist

```plist path="/build/entitlements.mac.inherit.plist" 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
	<true/>
	<key>com.apple.security.cs.disable-library-validation</key>
	<true/>
	<key>com.apple.security.cs.allow-dyld-environment-variables</key>
	<true/>
</dict>
</plist>

```

## /build/entitlements.mac.plist

```plist path="/build/entitlements.mac.plist" 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
	<true/>
	<key>com.apple.security.cs.disable-library-validation</key>
	<true/>
	<key>com.apple.security.cs.allow-dyld-environment-variables</key>
	<true/>
</dict>
</plist>

```

## /build/entitlements.mas.inherit.plist

```plist path="/build/entitlements.mas.inherit.plist" 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.inherit</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
	<true/>
	<key>com.apple.security.cs.disable-library-validation</key>
	<true/>
</dict>
</plist>

```

## /build/entitlements.mas.plist

```plist path="/build/entitlements.mas.plist" 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.network.client</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
	<key>com.apple.security.files.user-selected.read-write</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
	<true/>
	<key>com.apple.security.cs.disable-library-validation</key>
	<true/>
</dict>
</plist>

```

## /build/notarize.cjs

```cjs path="/build/notarize.cjs" 
const { notarize } = require('@electron/notarize');

exports.default = async function notarizing(context) {
  const { electronPlatformName, appOutDir } = context;

  if (electronPlatformName !== 'darwin') {
    return;
  }

  const appleId = process.env.APPLE_ID;
  const appleIdPassword = process.env.APPLE_ID_PASSWORD;
  const teamId = process.env.APPLE_TEAM_ID;

  if (!appleId || !appleIdPassword || !teamId) {
    return;
  }

  const appName = context.packager.appInfo.productFilename;

  try {
    await notarize({
      appBundleId: 'com.karmaa.termix',
      appPath: `${appOutDir}/${appName}.app`,
      appleId: appleId,
      appleIdPassword: appleIdPassword,
      teamId: teamId,
    });
  } catch (error) {
    console.error('Notarization failed:', error);
  }
};

```

## /chocolatey/termix-ssh.nuspec

```nuspec path="/chocolatey/termix-ssh.nuspec" 
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
  <metadata>
    <id>termix-ssh</id>
    <version>VERSION_PLACEHOLDER</version>
    <packageSourceUrl>https://github.com/Termix-SSH/Termix</packageSourceUrl>
    <owners>bugattiguy527</owners>
    <title>Termix SSH</title>
    <authors>bugattiguy527</authors>
    <projectUrl>https://github.com/Termix-SSH/Termix</projectUrl>
    <iconUrl>https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.png</iconUrl>
    <licenseUrl>https://raw.githubusercontent.com/Termix-SSH/Termix/refs/heads/main/LICENSE</licenseUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <projectSourceUrl>https://github.com/Termix-SSH/Termix</projectSourceUrl>
    <docsUrl>https://docs.termix.site/install</docsUrl>
    <bugTrackerUrl>https://github.com/Termix-SSH/Support/issues</bugTrackerUrl>
    <tags>docker ssh self-hosted file-management ssh-tunnel termix server-management terminal</tags>
    <summary>Termix is a web-based server management platform with SSH terminal, tunneling, and file editing capabilities.</summary>
    <description>
Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a web-based solution for managing your servers and infrastructure through a single, intuitive interface.

Termix offers:
- SSH terminal access
- SSH tunneling capabilities
- Remote file management
- Server monitoring and management

This package installs the desktop application version of Termix.
    </description>
    <releaseNotes>https://github.com/Termix-SSH/Termix/releases</releaseNotes>
  </metadata>
  <files>
    <file src="tools\**" target="tools" />
  </files>
</package>

```

## /chocolatey/tools/chocolateyinstall.ps1

```ps1 path="/chocolatey/tools/chocolateyinstall.ps1" 
$ErrorActionPreference = 'Stop'

$packageName = 'termix-ssh'
$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
$url64 = 'DOWNLOAD_URL_PLACEHOLDER'
$checksum64 = 'CHECKSUM_PLACEHOLDER'
$checksumType64 = 'sha256'

$packageArgs = @{
  packageName    = $packageName
  fileType       = 'msi'
  url64bit       = $url64
  softwareName   = 'Termix*'
  checksum64     = $checksum64
  checksumType64 = $checksumType64
  silentArgs     = "/qn /norestart /l*v `"$($env:TEMP)\$($packageName).$($env:chocolateyPackageVersion).MsiInstall.log`""
  validExitCodes = @(0, 3010, 1641)
}

Install-ChocolateyPackage @packageArgs

```

## /chocolatey/tools/chocolateyuninstall.ps1

```ps1 path="/chocolatey/tools/chocolateyuninstall.ps1" 
$ErrorActionPreference = 'Stop'

$packageName = 'termix-ssh'
$softwareName = 'Termix*'
$installerType = 'msi'

$silentArgs = '/qn /norestart'
$validExitCodes = @(0, 3010, 1605, 1614, 1641)

[array]$key = Get-UninstallRegistryKey -SoftwareName $softwareName

if ($key.Count -eq 1) {
  $key | % {
    $file = "$($_.UninstallString)"

    if ($installerType -eq 'msi') {
      $silentArgs = "$($_.PSChildName) $silentArgs"
      $file = ''
    }

    Uninstall-ChocolateyPackage -PackageName $packageName `
                                 -FileType $installerType `
                                 -SilentArgs "$silentArgs" `
                                 -ValidExitCodes $validExitCodes `
                                 -File "$file"
  }
} elseif ($key.Count -eq 0) {
  Write-Warning "$packageName has already been uninstalled by other means."
} elseif ($key.Count -gt 1) {
  Write-Warning "$($key.Count) matches found!"
  Write-Warning "To prevent accidental data loss, no programs will be uninstalled."
  $key | % {Write-Warning "- $($_.DisplayName)"}
}

```

## /components.json

```json path="/components.json" 
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/index.css",
    "baseColor": "zinc",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "iconLibrary": "lucide"
}

```

## /crowdin.yml

```yml path="/crowdin.yml" 
files:
  - source: /src/locales/en.json
    translation: /src/locales/translated/%locale_with_underscore%.json

```

## /docker/Dockerfile

``` path="/docker/Dockerfile" 
# Stage 1: Install dependencies
FROM node:24-slim AS deps
WORKDIR /app

RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*

COPY package*.json ./
COPY .npmrc ./
COPY vendor ./vendor

RUN npm ci --ignore-scripts && \
    npm cache clean --force

# Stage 2: Build frontend
FROM deps AS frontend-builder
WORKDIR /app

COPY . .

RUN find public/fonts -name "*.ttf" ! -name "*Regular.ttf" ! -name "*Bold.ttf" ! -name "*Italic.ttf" -delete

RUN npm cache clean --force && \
    NODE_OPTIONS="--max-old-space-size=3072" npm run build

# Stage 3: Build backend
FROM deps AS backend-builder
WORKDIR /app

COPY . .

RUN npm rebuild better-sqlite3

RUN npm run build:backend

# Stage 4: Production dependencies only
FROM node:24-slim AS production-deps
WORKDIR /app

RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*

COPY package*.json ./
COPY .npmrc ./
COPY vendor ./vendor

RUN npm ci --omit=dev --ignore-scripts && \
    npm rebuild better-sqlite3 bcryptjs && \
    npm cache clean --force

# Stage 5: Final optimized image
FROM node:24-slim
WORKDIR /app

ENV DATA_DIR=/app/data \
    PORT=8080 \
    NODE_ENV=production

RUN apt-get update && apt-get install -y nginx gettext-base openssl ca-certificates gosu wget && \
    update-ca-certificates && \
    rm -rf /var/lib/apt/lists/* && \
    mkdir -p /app/data /app/uploads /app/data/.opk /app/nginx /tmp/nginx && \
    chown -R node:node /app /tmp/nginx && \
    chmod 755 /app/data /app/uploads /app/data/.opk /app/nginx /tmp/nginx

COPY docker/nginx.conf /app/nginx/nginx.conf.template
COPY docker/nginx-https.conf /app/nginx/nginx-https.conf.template

COPY --chown=node:node --from=frontend-builder /app/dist /app/html
COPY --chown=node:node --from=frontend-builder /app/src/locales /app/html/locales
COPY --chown=node:node --from=frontend-builder /app/public/fonts /app/html/fonts

COPY --chown=node:node --from=production-deps /app/node_modules /app/node_modules
COPY --chown=node:node --from=backend-builder /app/dist/backend ./dist/backend
COPY --chown=node:node package.json ./

VOLUME ["/app/data"]

EXPOSE ${PORT} 30001 30002 30003 30004 30005 30006

HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD wget -q -O /dev/null http://localhost:30001/health || exit 1

COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

CMD ["/entrypoint.sh"]

```

## /docker/compose.dev.yml

```yml path="/docker/compose.dev.yml" 
services:
  termix-dev:
    build:
      context: ..
      dockerfile: docker/Dockerfile
    container_name: termix-dev
    restart: unless-stopped
    ports:
      - "8081:8080"
    volumes:
      - termix-dev-data:/app/data
    environment:
      PORT: "8080"
      NODE_ENV: development
    depends_on:
      - guacd-dev
    networks:
      - termix-dev-net

  guacd-dev:
    image: guacamole/guacd:1.6.0
    container_name: guacd-dev
    restart: unless-stopped
    networks:
      - termix-dev-net

volumes:
  termix-dev-data:
    driver: local

networks:
  termix-dev-net:
    driver: bridge

```

## /docker/docker-compose.yml

```yml path="/docker/docker-compose.yml" 
services:
  termix:
    image: ghcr.io/lukegus/termix:latest
    container_name: termix
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - termix-data:/app/data
    environment:
      PORT: "8080"
    depends_on:
      - guacd
    networks:
      - termix-net

  guacd:
    image: guacamole/guacd:1.6.0
    container_name: guacd
    restart: unless-stopped
    ports:
      - "4822:4822"
    networks:
      - termix-net

volumes:
  termix-data:
    driver: local

networks:
  termix-net:
    driver: bridge

```

## /docker/entrypoint.sh

```sh path="/docker/entrypoint.sh" 
#!/bin/sh
set -e

PUID=${PUID:-1000}
PGID=${PGID:-1000}

if [ "$(id -u)" = "0" ]; then
    if [ "$PUID" = "0" ]; then
        echo "Running as root (PUID=0, PGID=$PGID)"
        chown -R root:root /app/data /app/uploads /tmp/nginx 2>/dev/null || true
    else
        echo "Setting up user permissions (PUID: $PUID, PGID: $PGID)..."

        groupmod -o -g "$PGID" node 2>/dev/null || true
        usermod -o -u "$PUID" node 2>/dev/null || true

        chown -R node:node /app/data /app/uploads /tmp/nginx 2>/dev/null || true

        echo "User node is now UID: $PUID, GID: $PGID"

        exec gosu node:node "$0" "$@"
    fi
fi

export PORT=${PORT:-8080}
export ENABLE_SSL=${ENABLE_SSL:-false}
export SSL_PORT=${SSL_PORT:-8443}
export SSL_CERT_PATH=${SSL_CERT_PATH:-/app/data/ssl/termix.crt}
export SSL_KEY_PATH=${SSL_KEY_PATH:-/app/data/ssl/termix.key}

echo "Configuring web UI to run on port: $PORT"

if [ "$ENABLE_SSL" = "true" ]; then
    echo "SSL enabled - using HTTPS configuration with redirect"
    NGINX_CONF_SOURCE="/app/nginx/nginx-https.conf.template"
else
    echo "SSL disabled - using HTTP-only configuration (default)"
    NGINX_CONF_SOURCE="/app/nginx/nginx.conf.template"
fi

mkdir -p /tmp/nginx
envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /tmp/nginx/nginx.conf

mkdir -p /app/data /app/uploads /app/data/.opk
chmod 755 /app/data /app/uploads /app/data/.opk 2>/dev/null || true

if [ -w /app/data ]; then
    echo "Data directory is writable"
else
    echo "WARNING: Data directory is not writable. OPKSSH may fail."
    ls -ld /app/data
fi

if [ -w /app/data/.opk ]; then
    echo "OPKSSH directory is writable"
else
    echo "WARNING: OPKSSH directory is not writable. OPKSSH authentication will fail."
    ls -ld /app/data/.opk
fi

OPKSSH_DIR="${DATA_DIR:-/app/data}/opkssh"
if [ ! -d "$OPKSSH_DIR" ]; then
    echo "WARNING: OPKSSH binary directory not found at $OPKSSH_DIR"
    echo "OPKSSH will be downloaded automatically on first use."
else
    echo "OPKSSH binary directory found at $OPKSSH_DIR"
fi

if [ "$ENABLE_SSL" = "true" ]; then
    echo "Checking SSL certificate configuration..."
    mkdir -p /app/data/ssl
    chmod 755 /app/data/ssl 2>/dev/null || true

    DOMAIN=${SSL_DOMAIN:-localhost}
    
    if [ -f "/app/data/ssl/termix.crt" ] && [ -f "/app/data/ssl/termix.key" ]; then
        echo "SSL certificates found, checking validity..."
        
        if openssl x509 -in /app/data/ssl/termix.crt -checkend 2592000 -noout >/dev/null 2>&1; then
            echo "SSL certificates are valid and will be reused for domain: $DOMAIN"
        else
            echo "SSL certificate is expired or expiring soon, regenerating..."
            rm -f /app/data/ssl/termix.crt /app/data/ssl/termix.key
        fi
    else
        echo "SSL certificates not found, will generate new ones..."
    fi
    
    if [ ! -f "/app/data/ssl/termix.crt" ] || [ ! -f "/app/data/ssl/termix.key" ]; then
        echo "Generating SSL certificates for domain: $DOMAIN"

        cat > /app/data/ssl/openssl.conf << EOF
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req

[dn]
C=US
ST=State
L=City
O=Termix
OU=IT Department
CN=$DOMAIN

[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = $DOMAIN
DNS.2 = localhost
DNS.3 = 127.0.0.1
IP.1 = 127.0.0.1
IP.2 = ::1
IP.3 = 0.0.0.0
EOF

        openssl genrsa -out /app/data/ssl/termix.key 2048

        openssl req -new -x509 -key /app/data/ssl/termix.key -out /app/data/ssl/termix.crt -days 365 -config /app/data/ssl/openssl.conf -extensions v3_req

        chmod 600 /app/data/ssl/termix.key
        chmod 644 /app/data/ssl/termix.crt

        rm -f /app/data/ssl/openssl.conf
        
        echo "SSL certificates generated successfully for domain: $DOMAIN"
    fi
fi

echo "Starting nginx..."
nginx -c /tmp/nginx/nginx.conf

echo "Starting backend services..."
cd /app
export NODE_ENV=production

if [ -f "package.json" ]; then
    VERSION=$(grep '"version"' package.json | sed 's/.*"version": *"\([^"]*\)".*/\1/')
    if [ -n "$VERSION" ]; then
        export VERSION
    else
        echo "Warning: Could not extract version from package.json"
    fi
else
    echo "Warning: package.json not found"
fi

node dist/backend/backend/starter.js

echo "All services started"

tail -f /dev/null
```

## /docker/nginx-https.conf

```conf path="/docker/nginx-https.conf" 
worker_processes 1;
master_process off;
pid /tmp/nginx/nginx.pid;
error_log /tmp/nginx/error.log warn;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log /tmp/nginx/access.log;

    client_body_temp_path /tmp/nginx/client_body;
    proxy_temp_path /tmp/nginx/proxy_temp;
    fastcgi_temp_path /tmp/nginx/fastcgi_temp;
    uwsgi_temp_path /tmp/nginx/uwsgi_temp;
    scgi_temp_path /tmp/nginx/scgi_temp;

    sendfile on;
    keepalive_timeout 65;
    client_header_timeout 300s;

    set_real_ip_from 127.0.0.1;
    real_ip_header X-Forwarded-For;

    map $http_x_forwarded_proto $proxy_x_forwarded_proto {
        default $http_x_forwarded_proto;
        ''      $scheme;
    }

    map $http_x_forwarded_host $proxy_x_forwarded_host {
        default $http_x_forwarded_host;
        ''      $http_host;
    }

    map $http_x_forwarded_port $proxy_x_forwarded_port {
        default $http_x_forwarded_port;
        ''      '';
    }

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    server {
        listen ${PORT};
        server_name _;

        return 301 https://$host:${SSL_PORT}$request_uri;
    }

    server {
        listen ${SSL_PORT} ssl;
        server_name _;

        ssl_certificate ${SSL_CERT_PATH};
        ssl_certificate_key ${SSL_KEY_PATH};

        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        add_header X-Content-Type-Options nosniff always;
        add_header X-XSS-Protection "1; mode=block" always;

        location = /sw.js {
            root /app/html;
            expires off;
            add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
            try_files $uri =404;
        }

        location = /manifest.json {
            root /app/html;
            expires off;
            add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
            try_files $uri =404;
        }

        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            root /app/html;
            expires 1y;
            add_header Cache-Control "public, max-age=31536000, immutable" always;
            try_files $uri =404;
        }

        location / {
            root /app/html;
            index index.html index.htm;
            expires off;
            add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
            try_files $uri $uri/ /index.html;
        }

        location ~* \.map$ {
            return 404;
            access_log off;
            log_not_found off;
        }

        location ~ ^/users/sessions(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
        }

        location ~ ^/users(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
            proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
            proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
        }

        location ~ ^/version(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/releases(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/alerts(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/rbac(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/credentials(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
        }

        location ~ ^/snippets(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/c2s-tunnel-presets(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/terminal(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/database(/.*)?$ {
            client_max_body_size 5G;
            client_body_timeout 300s;

            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;

            proxy_request_buffering off;
            proxy_buffering off;
        }

        location ~ ^/db(/.*)?$ {
            client_max_body_size 5G;
            client_body_timeout 300s;

            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;

            proxy_request_buffering off;
            proxy_buffering off;
        }

        location ~ ^/encryption(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /host/quick-connect {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $http_host;
            proxy_cache_bypass $http_upgrade;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/host/opkssh-chooser(/.*)?$ {
            proxy_pass http://127.0.0.1:30001/host/opkssh-chooser$1$is_args$args;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
            proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;

            proxy_cache_bypass 1;
            proxy_no_cache 1;
            add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
        }

        location ~ ^/host/opkssh-callback(/.*)?$ {
            proxy_pass http://127.0.0.1:30001/host/opkssh-callback$1$is_args$args;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
            proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;

            proxy_cache_bypass 1;
            proxy_no_cache 1;
            add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
        }

        location /host/ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /ssh/websocket/ {
            proxy_pass http://127.0.0.1:30002/;
            proxy_http_version 1.1;

            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
            proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
            proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
            proxy_cache_bypass $http_upgrade;

            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_read_timeout 86400s;
            proxy_send_timeout 86400s;
            proxy_connect_timeout 10s;

            proxy_buffering off;
            proxy_request_buffering off;

            proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
        }

        location ^~ /guacamole/websocket/ {
            proxy_pass http://127.0.0.1:30008/;
            proxy_http_version 1.1;

            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $http_host;
            proxy_cache_bypass $http_upgrade;

            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Port $server_port;
            proxy_set_header X-Forwarded-Host $http_host;

            proxy_read_timeout 86400s;
            proxy_send_timeout 86400s;
            proxy_connect_timeout 10s;

            proxy_buffering off;
            proxy_request_buffering off;

            proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
        }

        location ~ ^/guacamole(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /host/tunnel/ {
            proxy_pass http://127.0.0.1:30003;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /ssh/tunnel/ {
            proxy_pass http://127.0.0.1:30003;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_read_timeout 86400s;
            proxy_send_timeout 86400s;
            proxy_buffering off;
            proxy_cache off;
        }

        location /host/file_manager/recent {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /host/file_manager/pinned {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /host/file_manager/shortcuts {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /host/file_manager/sudo-password {
            proxy_pass http://127.0.0.1:30004;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /ssh/file_manager/ {
            client_max_body_size 5G;
            client_body_timeout 300s;

            add_header Cache-Control "no-store, no-cache, must-revalidate" always;

            proxy_pass http://127.0.0.1:30004;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;

            proxy_request_buffering off;
            proxy_buffering off;
        }

        location /host/file_manager/ssh/ {
            client_max_body_size 5G;
            client_body_timeout 300s;

            add_header Cache-Control "no-store, no-cache, must-revalidate" always;

            proxy_pass http://127.0.0.1:30004;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;

            proxy_request_buffering off;
            proxy_buffering off;
        }

        location ~ ^/network-topology(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /health {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/status(/.*)?$ {
            proxy_pass http://127.0.0.1:30005;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/metrics(/.*)?$ {
            proxy_pass http://127.0.0.1:30005;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }

        location ~ ^/(refresh|host-updated)$ {
            proxy_pass http://127.0.0.1:30005;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/global-settings(/.*)?$ {
            proxy_pass http://127.0.0.1:30005;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/uptime(/.*)?$ {
            proxy_pass http://127.0.0.1:30006;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/activity(/.*)?$ {
            proxy_pass http://127.0.0.1:30006;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/dashboard/preferences(/.*)?$ {
            proxy_pass http://127.0.0.1:30006;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ^~ /docker/console/ {
            proxy_pass http://127.0.0.1:30009/;
            proxy_http_version 1.1;

            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $http_host;
            proxy_cache_bypass $http_upgrade;

            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Port $server_port;
            proxy_set_header X-Forwarded-Host $http_host;

            proxy_read_timeout 86400s;
            proxy_send_timeout 86400s;
            proxy_connect_timeout 10s;

            proxy_buffering off;
            proxy_request_buffering off;

            proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
        }

        location ~ ^/docker(/.*)?$ {
            proxy_pass http://127.0.0.1:30007;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root /app/html;
        }
    }
}

```

## /docker/nginx.conf

```conf path="/docker/nginx.conf" 
worker_processes 1;
master_process off;
pid /tmp/nginx/nginx.pid;
error_log /tmp/nginx/error.log warn;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log /tmp/nginx/access.log;

    client_body_temp_path /tmp/nginx/client_body;
    proxy_temp_path /tmp/nginx/proxy_temp;
    fastcgi_temp_path /tmp/nginx/fastcgi_temp;
    uwsgi_temp_path /tmp/nginx/uwsgi_temp;
    scgi_temp_path /tmp/nginx/scgi_temp;

    sendfile on;
    keepalive_timeout 65;
    client_header_timeout 300s;

    set_real_ip_from 127.0.0.1;
    real_ip_header X-Forwarded-For;

    map $http_x_forwarded_proto $proxy_x_forwarded_proto {
        default $http_x_forwarded_proto;
        ''      $scheme;
    }

    map $http_x_forwarded_host $proxy_x_forwarded_host {
        default $http_x_forwarded_host;
        ''      $http_host;
    }

    map $http_x_forwarded_port $proxy_x_forwarded_port {
        default $http_x_forwarded_port;
        ''      '';
    }

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    server {
        listen ${PORT};
        server_name localhost;

        add_header X-Content-Type-Options nosniff always;
        add_header X-XSS-Protection "1; mode=block" always;

        location = /sw.js {
            root /app/html;
            expires off;
            add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
            try_files $uri =404;
        }

        location = /manifest.json {
            root /app/html;
            expires off;
            add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
            try_files $uri =404;
        }

        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            root /app/html;
            expires 1y;
            add_header Cache-Control "public, max-age=31536000, immutable" always;
            try_files $uri =404;
        }

        location / {
            root /app/html;
            index index.html index.htm;
            expires off;
            add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
            try_files $uri $uri/ /index.html;
        }

        location ~* \.map$ {
            return 404;
            access_log off;
            log_not_found off;
        }

        location ~ ^/users/sessions(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
        }

        location ~ ^/users(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
            proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
            proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
        }

        location ~ ^/version(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/releases(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/alerts(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/rbac(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/credentials(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
        }

        location ~ ^/snippets(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/c2s-tunnel-presets(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/terminal(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/database(/.*)?$ {
            client_max_body_size 5G;
            client_body_timeout 300s;

            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;

            proxy_request_buffering off;
            proxy_buffering off;
        }

        location ~ ^/db(/.*)?$ {
            client_max_body_size 5G;
            client_body_timeout 300s;

            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;

            proxy_request_buffering off;
            proxy_buffering off;
        }

        location ~ ^/encryption(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /host/quick-connect {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $http_host;
            proxy_cache_bypass $http_upgrade;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/host/opkssh-chooser(/.*)?$ {
            proxy_pass http://127.0.0.1:30001/host/opkssh-chooser$1$is_args$args;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
            proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;

            proxy_cache_bypass 1;
            proxy_no_cache 1;
            add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
        }

        location ~ ^/host/opkssh-callback(/.*)?$ {
            proxy_pass http://127.0.0.1:30001/host/opkssh-callback$1$is_args$args;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
            proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;

            proxy_cache_bypass 1;
            proxy_no_cache 1;
            add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
        }

        location /host/ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /ssh/websocket/ {
            proxy_pass http://127.0.0.1:30002/;
            proxy_http_version 1.1;

            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
            proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
            proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
            proxy_cache_bypass $http_upgrade;

            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_read_timeout 86400s;
            proxy_send_timeout 86400s;
            proxy_connect_timeout 10s;

            proxy_buffering off;
            proxy_request_buffering off;

            proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
        }

        location ^~ /guacamole/websocket/ {
            proxy_pass http://127.0.0.1:30008/;
            proxy_http_version 1.1;

            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $http_host;
            proxy_cache_bypass $http_upgrade;

            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Port $server_port;
            proxy_set_header X-Forwarded-Host $http_host;

            proxy_read_timeout 86400s;
            proxy_send_timeout 86400s;
            proxy_connect_timeout 10s;

            proxy_buffering off;
            proxy_request_buffering off;

            proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
        }

        location ~ ^/guacamole(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /host/tunnel/ {
            proxy_pass http://127.0.0.1:30003;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /ssh/tunnel/ {
            proxy_pass http://127.0.0.1:30003;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_read_timeout 86400s;
            proxy_send_timeout 86400s;
            proxy_buffering off;
            proxy_cache off;
        }

        location /host/file_manager/recent {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /host/file_manager/pinned {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /host/file_manager/shortcuts {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /host/file_manager/sudo-password {
            proxy_pass http://127.0.0.1:30004;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /ssh/file_manager/ {
            client_max_body_size 5G;
            client_body_timeout 300s;

            add_header Cache-Control "no-store, no-cache, must-revalidate" always;

            proxy_pass http://127.0.0.1:30004;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;

            proxy_request_buffering off;
            proxy_buffering off;
        }

        location /host/file_manager/ssh/ {
            client_max_body_size 5G;
            client_body_timeout 300s;

            add_header Cache-Control "no-store, no-cache, must-revalidate" always;

            proxy_pass http://127.0.0.1:30004;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;

            proxy_request_buffering off;
            proxy_buffering off;
        }

        location ~ ^/network-topology(/.*)?$ {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /health {
            proxy_pass http://127.0.0.1:30001;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/status(/.*)?$ {
            proxy_pass http://127.0.0.1:30005;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/metrics(/.*)?$ {
            proxy_pass http://127.0.0.1:30005;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }

        location ~ ^/(refresh|host-updated)$ {
            proxy_pass http://127.0.0.1:30005;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/global-settings(/.*)?$ {
            proxy_pass http://127.0.0.1:30005;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/uptime(/.*)?$ {
            proxy_pass http://127.0.0.1:30006;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/activity(/.*)?$ {
            proxy_pass http://127.0.0.1:30006;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ~ ^/dashboard/preferences(/.*)?$ {
            proxy_pass http://127.0.0.1:30006;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location ^~ /docker/console/ {
            proxy_pass http://127.0.0.1:30009/;
            proxy_http_version 1.1;

            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $http_host;
            proxy_cache_bypass $http_upgrade;

            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Port $server_port;
            proxy_set_header X-Forwarded-Host $http_host;

            proxy_read_timeout 86400s;
            proxy_send_timeout 86400s;
            proxy_connect_timeout 10s;

            proxy_buffering off;
            proxy_request_buffering off;

            proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
        }

        location ~ ^/docker(/.*)?$ {
            proxy_pass http://127.0.0.1:30007;
            proxy_http_version 1.1;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root /app/html;
        }
    }
}

```

## /electron-builder.json

```json path="/electron-builder.json" 
{
  "appId": "com.karmaa.termix",
  "productName": "Termix",
  "publish": null,
  "directories": {
    "output": "release"
  },
  "asar": true,
  "asarUnpack": [
    "dist/backend/**/*",
    "node_modules/**/*",
    "public/icons/**/*",
    "public/icon.*"
  ],
  "files": [
    "dist/**/*",
    "electron/**/*",
    "public/**/*",
    "!src/**/*",
    "!*.md",
    "!tsconfig*.json",
    "!vite.config.ts",
    "!eslint.config.js",
    "!node_modules/@napi-rs/canvas*/**/*",
    "!node_modules/@rollup/rollup-darwin-*/**/*",
    "!node_modules/@rollup/rollup-linux-*/**/*",
    "!node_modules/@rollup/rollup-win32-*/**/*",
    "!dist/icon-mac.png",
    "!public/icon-mac.png",
    "!dist/icon.ico",
    "!public/icon.ico",
    "!dist/icon.icns",
    "!public/icon.icns",
    "!dist/icons/**/*",
    "!public/icons/**/*"
  ],
  "extraMetadata": {
    "main": "electron/main.cjs"
  },
  "buildDependenciesFromSource": false,
  "nodeGypRebuild": false,
  "npmRebuild": true,
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": ["x64", "ia32"]
      },
      {
        "target": "msi",
        "arch": ["x64", "ia32"]
      }
    ],
    "icon": "public/icon.ico",
    "executableName": "Termix"
  },
  "nsis": {
    "oneClick": false,
    "allowToChangeInstallationDirectory": true,
    "artifactName": "termix_windows_${arch}_nsis.${ext}",
    "createDesktopShortcut": true,
    "createStartMenuShortcut": true,
    "shortcutName": "Termix",
    "uninstallDisplayName": "Termix"
  },
  "msi": {
    "artifactName": "termix_windows_${arch}_msi.${ext}"
  },
  "linux": {
    "artifactName": "termix_linux_${arch}_portable.${ext}",
    "target": [
      {
        "target": "AppImage",
        "arch": ["x64", "arm64", "armv7l"]
      },
      {
        "target": "deb",
        "arch": ["x64", "arm64", "armv7l"]
      },
      {
        "target": "tar.gz",
        "arch": ["x64", "arm64", "armv7l"]
      }
    ],
    "icon": "public/icon.png",
    "category": "Development",
    "executableName": "termix",
    "maintainer": "Termix <mail@termix.site>",
    "desktop": {
      "entry": {
        "Name": "Termix",
        "Comment": "A web-based server management platform",
        "Keywords": "terminal;ssh;server;management;",
        "StartupWMClass": "termix"
      }
    }
  },
  "appImage": {
    "artifactName": "termix_linux_${arch}_appimage.${ext}"
  },
  "deb": {
    "artifactName": "termix_linux_${arch}_deb.${ext}"
  },

  "mac": {
    "target": [
      {
        "target": "mas",
        "arch": "universal"
      },
      {
        "target": "dmg",
        "arch": ["universal", "x64", "arm64"]
      }
    ],
    "icon": "public/icon.icns",
    "category": "public.app-category.developer-tools",
    "hardenedRuntime": true,
    "gatekeeperAssess": false,
    "entitlements": "build/entitlements.mac.plist",
    "entitlementsInherit": "build/entitlements.mac.inherit.plist",
    "type": "distribution",
    "minimumSystemVersion": "10.15",
    "mergeASARs": false,
    "singleArchFiles": "**/*.{node,bare}",
    "x64ArchFiles": "**/*.{node,bare}"
  },
  "dmg": {
    "artifactName": "termix_macos_${arch}_dmg.${ext}",
    "sign": true
  },
  "afterSign": "build/notarize.cjs",
  "mas": {
    "provisioningProfile": "build/Termix_Mac_App_Store.provisionprofile",
    "entitlements": "build/entitlements.mas.plist",
    "entitlementsInherit": "build/entitlements.mas.inherit.plist",
    "hardenedRuntime": false,
    "gatekeeperAssess": false,
    "type": "distribution",
    "category": "public.app-category.developer-tools",
    "artifactName": "termix_macos_${arch}_mas.${ext}",
    "extendInfo": {
      "ITSAppUsesNonExemptEncryption": false,
      "NSAppleEventsUsageDescription": "Termix needs access to control other applications for terminal operations."
    }
  },
  "generateUpdatesFilesForAllChannels": true
}

```

## /electron/main.cjs

```cjs path="/electron/main.cjs" 
const {
  app,
  BrowserWindow,
  shell,
  ipcMain,
  dialog,
  Menu,
  session,
  safeStorage,
  Tray,
  clipboard,
} = require("electron");
const path = require("path");
const fs = require("fs");
const os = require("os");
const https = require("https");
const http = require("http");
const net = require("net");
const { URL } = require("url");
const { fork } = require("child_process");
const WebSocket = require("ws");

const logFile = path.join(app.getPath("userData"), "termix-main.log");
const electronAuthCookiesPath = path.join(
  app.getPath("userData"),
  "electron-auth-cookies.json",
);
const electronAuthCookies = new Map();

function logToFile(...args) {
  const timestamp = new Date().toISOString();
  const msg = args
    .map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a)))
    .join(" ");
  const line = `[${timestamp}] ${msg}\n`;
  try {
    fs.appendFileSync(logFile, line);
  } catch {
    // ignore
  }
  console.log(...args);
}

function getCookieOrigin(url) {
  try {
    const parsedUrl = new URL(url);
    const protocol =
      parsedUrl.protocol === "ws:"
        ? "http:"
        : parsedUrl.protocol === "wss:"
          ? "https:"
          : parsedUrl.protocol;
    return `${protocol}//${parsedUrl.host}`;
  } catch {
    return null;
  }
}

function parseCookieTarget(url) {
  try {
    const parsedUrl = new URL(url);
    if (parsedUrl.protocol === "ws:") {
      parsedUrl.protocol = "http:";
    } else if (parsedUrl.protocol === "wss:") {
      parsedUrl.protocol = "https:";
    }
    return parsedUrl;
  } catch {
    return null;
  }
}

function getElectronAuthCookieKey(name, origin) {
  return origin ? `${origin}|${name}` : null;
}

function getSafeStorageAvailable() {
  try {
    return safeStorage.isEncryptionAvailable();
  } catch {
    return false;
  }
}

function encodeElectronAuthCookieValue(value) {
  return {
    encrypted: true,
    value: safeStorage.encryptString(value).toString("base64"),
  };
}

function decodeElectronAuthCookieValue(record) {
  if (!record.encrypted || !getSafeStorageAvailable()) {
    return null;
  }

  try {
    return safeStorage.decryptString(Buffer.from(record.value, "base64"));
  } catch (error) {
    logToFile(
      "Failed to decrypt persisted Electron auth cookie:",
      error.message,
    );
    return null;
  }
}

function isElectronAuthCookieExpired(cookie) {
  return Number.isFinite(cookie.expiresAt) && cookie.expiresAt <= Date.now();
}

function saveElectronAuthCookiesToDisk() {
  try {
    if (!getSafeStorageAvailable()) {
      if (fs.existsSync(electronAuthCookiesPath)) {
        fs.rmSync(electronAuthCookiesPath, { force: true });
      }
      return;
    }

    const records = [];

    for (const [key, cookie] of electronAuthCookies.entries()) {
      if (isElectronAuthCookieExpired(cookie)) {
        electronAuthCookies.delete(key);
        continue;
      }

      records.push({
        key,
        name: cookie.name,
        origin: cookie.origin,
        path: cookie.path,
        expiresAt: cookie.expiresAt,
        ...encodeElectronAuthCookieValue(cookie.value),
      });
    }

    fs.writeFileSync(
      electronAuthCookiesPath,
      JSON.stringify({ version: 1, records }, null, 2),
    );
  } catch (error) {
    logToFile("Failed to persist Electron auth cookies:", error.message);
  }
}

function loadElectronAuthCookiesFromDisk() {
  electronAuthCookies.clear();

  try {
    if (!getSafeStorageAvailable()) {
      if (fs.existsSync(electronAuthCookiesPath)) {
        fs.rmSync(electronAuthCookiesPath, { force: true });
      }
      return;
    }

    if (!fs.existsSync(electronAuthCookiesPath)) {
      return;
    }

    const data = JSON.parse(fs.readFileSync(electronAuthCookiesPath, "utf8"));
    const records = Array.isArray(data.records) ? data.records : [];

    for (const record of records) {
      if (
        !record ||
        typeof record.key !== "string" ||
        typeof record.name !== "string" ||
        typeof record.origin !== "string"
      ) {
        continue;
      }

      const value = decodeElectronAuthCookieValue(record);
      if (!value) {
        continue;
      }

      const cookie = {
        name: record.name,
        value,
        origin: record.origin,
        path: typeof record.path === "string" ? record.path : "/",
        expiresAt: Number.isFinite(record.expiresAt) ? record.expiresAt : null,
      };

      if (!isElectronAuthCookieExpired(cookie)) {
        electronAuthCookies.set(record.key, cookie);
      }
    }

    saveElectronAuthCookiesToDisk();
  } catch (error) {
    logToFile("Failed to load persisted Electron auth cookies:", error.message);
  }
}

function clearPersistedElectronAuthCookies() {
  electronAuthCookies.clear();
  try {
    if (fs.existsSync(electronAuthCookiesPath)) {
      fs.rmSync(electronAuthCookiesPath, { force: true });
    }
  } catch (error) {
    logToFile(
      "Failed to clear persisted Electron auth cookies:",
      error.message,
    );
  }
}

function parseSetCookieHeader(header) {
  const [cookiePair, ...attributes] = String(header || "").split(";");
  const separatorIndex = cookiePair.indexOf("=");
  if (separatorIndex <= 0) return null;

  const parsed = {
    name: cookiePair.slice(0, separatorIndex).trim(),
    value: cookiePair.slice(separatorIndex + 1).trim(),
    path: "/",
    maxAge: null,
    expires: null,
  };

  for (const attribute of attributes) {
    const [rawName, ...rawValueParts] = attribute.trim().split("=");
    const attrName = rawName.toLowerCase();
    const attrValue = rawValueParts.join("=");

    if (attrName === "path" && attrValue) {
      parsed.path = attrValue;
    } else if (attrName === "max-age" && attrValue) {
      const maxAge = Number(attrValue);
      parsed.maxAge = Number.isFinite(maxAge) ? maxAge : null;
    } else if (attrName === "expires" && attrValue) {
      const expires = Date.parse(attrValue);
      parsed.expires = Number.isFinite(expires) ? expires : null;
    }
  }

  return parsed;
}

function rememberElectronAuthCookieFromHeader(url, header) {
  const origin = getCookieOrigin(url);
  if (!origin) return;

  const cookie = parseSetCookieHeader(header);
  if (!cookie || cookie.name !== "jwt") return;

  const key = getElectronAuthCookieKey(cookie.name, origin);
  if (!key) return;

  const expired =
    cookie.maxAge === 0 ||
    (cookie.expires !== null && cookie.expires <= Date.now());

  if (expired || !cookie.value) {
    electronAuthCookies.delete(key);
    saveElectronAuthCookiesToDisk();
    return;
  }

  const expiresAt =
    cookie.maxAge !== null ? Date.now() + cookie.maxAge * 1000 : cookie.expires;

  electronAuthCookies.set(key, {
    name: cookie.name,
    value: cookie.value,
    origin,
    path: cookie.path,
    expiresAt,
  });
  saveElectronAuthCookiesToDisk();
}

function getRememberedElectronAuthCookie(name, targetUrl) {
  const target = parseCookieTarget(targetUrl);
  if (!target) return null;

  const exactKey = getElectronAuthCookieKey(name, target.origin);
  const exactCookie = exactKey ? electronAuthCookies.get(exactKey) : null;
  if (exactCookie && !isElectronAuthCookieExpired(exactCookie)) {
    return exactCookie;
  }

  if (target.protocol !== "https:") {
    return null;
  }

  const httpOrigin = `http://${target.host}`;
  const httpKey = getElectronAuthCookieKey(name, httpOrigin);
  const httpCookie = httpKey ? electronAuthCookies.get(httpKey) : null;
  return httpCookie && !isElectronAuthCookieExpired(httpCookie)
    ? httpCookie
    : null;
}

function getHeaderName(headers, name) {
  const lowerName = name.toLowerCase();
  return Object.keys(headers || {}).find(
    (key) => key.toLowerCase() === lowerName,
  );
}

function setCookieHeaderValue(requestHeaders, name, value) {
  const headerName = getHeaderName(requestHeaders, "Cookie") || "Cookie";
  const existing = requestHeaders[headerName];
  const existingValue = Array.isArray(existing)
    ? existing.join("; ")
    : existing;
  const nextCookie = `${name}=${value}`;
  const otherCookies = String(existingValue || "")
    .split(";")
    .map((cookie) => cookie.trim())
    .filter((cookie) => cookie && !cookie.startsWith(`${name}=`));

  requestHeaders[headerName] =
    otherCookies.length > 0
      ? `${otherCookies.join("; ")}; ${nextCookie}`
      : nextCookie;
}

function parseSemver(version) {
  const match = String(version || "").match(/(\d+)\.(\d+)(?:\.(\d+))?/);
  if (!match) return null;

  return [Number(match[1]), Number(match[2]), Number(match[3] || 0)];
}

function compareSemver(a, b) {
  const parsedA = parseSemver(a);
  const parsedB = parseSemver(b);
  if (!parsedA || !parsedB) return null;

  for (let i = 0; i < 3; i += 1) {
    if (parsedA[i] > parsedB[i]) return 1;
    if (parsedA[i] < parsedB[i]) return -1;
  }

  return 0;
}

const INSECURE_MODE_VALUES = new Set(["true", "1", "yes"]);

function isInsecureModeEnabled() {
  return INSECURE_MODE_VALUES.has(
    String(process.env.ENABLE_INSECURE_MODE || "")
      .trim()
      .toLowerCase(),
  );
}

function getTlsVerificationOptions() {
  return {
    rejectUnauthorized: !isInsecureModeEnabled(),
  };
}

function getWebSocketOptions(url, options = {}) {
  return {
    ...options,
    ...(String(url).startsWith("wss:") ? getTlsVerificationOptions() : {}),
  };
}

function httpFetch(url, options = {}) {
  return new Promise((resolve, reject) => {
    const urlObj = new URL(url);
    const isHttps = urlObj.protocol === "https:";
    const client = isHttps ? https : http;

    const requestOptions = {
      method: options.method || "GET",
      headers: options.headers || {},
      timeout: options.timeout || 10000,
      ...(isHttps ? getTlsVerificationOptions() : {}),
    };

    const req = client.request(url, requestOptions, (res) => {
      let data = "";
      res.on("data", (chunk) => (data += chunk));
      res.on("end", () => {
        resolve({
          ok: res.statusCode >= 200 && res.statusCode < 300,
          status: res.statusCode,
          text: () => Promise.resolve(data),
          json: () => Promise.resolve(JSON.parse(data)),
        });
      });
    });

    req.on("error", reject);
    req.on("timeout", () => {
      req.destroy();
      reject(new Error("Request timeout"));
    });

    if (options.body) {
      req.write(options.body);
    }
    req.end();
  });
}

if (process.platform === "linux") {
  app.commandLine.appendSwitch("--ozone-platform-hint=auto");

  app.commandLine.appendSwitch("--enable-features=VaapiVideoDecoder");
}

if (isInsecureModeEnabled()) {
  logToFile(
    "[security] ENABLE_INSECURE_MODE is enabled; TLS certificate validation is disabled.",
  );
  app.commandLine.appendSwitch("--ignore-certificate-errors");
  app.commandLine.appendSwitch("--ignore-ssl-errors");
  app.commandLine.appendSwitch("--ignore-certificate-errors-spki-list");
}
app.commandLine.appendSwitch("--enable-features=NetworkService");

let mainWindow = null;
let backendProcess = null;
let tray = null;
let isQuitting = false;

const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
const appRoot = isDev ? process.cwd() : path.join(__dirname, "..");
const electronCacheBuildPath = path.join(
  app.getPath("userData"),
  "client-cache-build.json",
);
const termixSessionPartition = "persist:termix";

function getElectronBuildTimestamp() {
  try {
    const buildInfo = require("./build-info.cjs");
    if (Number.isInteger(buildInfo.buildTimestamp)) {
      return buildInfo.buildTimestamp;
    }
  } catch {
    // Development runs may not have generated build metadata yet.
  }

  return 0;
}

async function clearElectronClientCacheIfBuildChanged() {
  const buildTimestamp = getElectronBuildTimestamp();
  let cacheTimestamp = 0;

  try {
    if (fs.existsSync(electronCacheBuildPath)) {
      const data = JSON.parse(fs.readFileSync(electronCacheBuildPath, "utf8"));
      cacheTimestamp = Number.isInteger(data.buildTimestamp)
        ? data.buildTimestamp
        : 0;
    }
  } catch (error) {
    logToFile(
      "Failed to read Electron client cache build info:",
      error.message,
    );
  }

  if (cacheTimestamp === buildTimestamp) {
    return;
  }

  const clearStep = async (label, action) => {
    try {
      await action();
    } catch (error) {
      logToFile(`Failed to clear Electron ${label}:`, error.message);
    }
  };

  try {
    const defaultSession = session.defaultSession;
    await clearStep("HTTP cache", () => defaultSession.clearCache());
    await clearStep("code cache", () =>
      defaultSession.clearCodeCaches({ urls: [] }),
    );
    await clearStep("auth cache", () => defaultSession.clearAuthCache());
    await clearStep("storage data", () =>
      defaultSession.clearStorageData({
        storages: [
          "appcache",
          "cookies",
          "filesystem",
          "shadercache",
          "websql",
          "serviceworkers",
          "cachestorage",
        ],
      }),
    );

    fs.writeFileSync(
      electronCacheBuildPath,
      JSON.stringify(
        {
          buildTimestamp,
          appVersion: app.getVersion(),
          updatedAt: new Date().toISOString(),
        },
        null,
        2,
      ),
    );

    logToFile("Electron client cache cleared for build change", {
      from: cacheTimestamp,
      to: buildTimestamp,
      appVersion: app.getVersion(),
    });
  } catch (error) {
    logToFile("Failed to clear Electron client cache:", error.message);
  }
}

function getCookieRemovalUrl(cookie) {
  const scheme = cookie.secure ? "https" : "http";
  const domain = cookie.domain?.startsWith(".")
    ? cookie.domain.slice(1)
    : cookie.domain || "localhost";
  return `${scheme}://${domain}${cookie.path || "/"}`;
}

async function clearElectronJwtCookiesAtStartup() {
  loadElectronAuthCookiesFromDisk();

  const targetSessions = new Set([
    session.defaultSession,
    session.fromPartition(termixSessionPartition),
  ]);

  for (const targetSession of targetSessions) {
    try {
      const cookies = await targetSession.cookies.get({ name: "jwt" });
      await Promise.all(
        cookies.map((cookie) =>
          targetSession.cookies.remove(
            getCookieRemovalUrl(cookie),
            cookie.name,
          ),
        ),
      );

      if (cookies.length > 0) {
        logToFile("Cleared Electron JWT cookies from cookie store", {
          count: cookies.length,
        });
      }
    } catch (error) {
      logToFile("Failed to clear Electron JWT cookies:", error.message);
    }
  }
}

function getBackendPaths() {
  if (isDev) {
    const backendDir = path.join(appRoot, "dist", "backend", "backend");
    return { entryPath: path.join(backendDir, "starter.js"), backendCwd: backendDir };
  }
  // fork() does not go through Electron's asar redirector — use the unpacked path
  const unpackedRoot = appRoot.replace("app.asar", "app.asar.unpacked");
  const backendDir = path.join(unpackedRoot, "dist", "backend", "backend");
  return { entryPath: path.join(backendDir, "starter.js"), backendCwd: backendDir };
}

function getBackendDataDir() {
  const userDataPath = app.getPath("userData");
  const dataDir = path.join(userDataPath, "server-data");
  if (!fs.existsSync(dataDir)) {
    fs.mkdirSync(dataDir, { recursive: true });
  }
  return dataDir;
}

function startBackendServer() {
  return new Promise((resolve) => {
    const { entryPath, backendCwd } = getBackendPaths();

    logToFile("isDev:", isDev, "appRoot:", appRoot);
    logToFile("app.isPackaged:", app.isPackaged);
    logToFile("process.env.NODE_ENV:", process.env.NODE_ENV);

    if (!fs.existsSync(entryPath)) {
      logToFile("Backend entry not found:", entryPath);
      resolve(false);
      return;
    }

    const dataDir = getBackendDataDir();
    logToFile("Starting embedded backend server...");
    logToFile("Backend entry:", entryPath);
    logToFile("Data directory:", dataDir);
    logToFile("Backend cwd:", backendCwd);

    logToFile("Checking paths...");
    logToFile("  entryPath exists:", fs.existsSync(entryPath));
    logToFile("  dataDir exists:", fs.existsSync(dataDir));
    logToFile("  backendCwd exists:", fs.existsSync(backendCwd));

    backendProcess = fork(entryPath, [], {
      cwd: backendCwd,
      env: {
        ...process.env,
        DATA_DIR: dataDir,
        NODE_ENV: "production",
        ELECTRON_EMBEDDED: "true",
        PORT: "30001",
      },
      stdio: ["pipe", "pipe", "pipe", "ipc"],
    });

    logToFile("Backend process spawned, pid:", backendProcess.pid);

    let resolved = false;
    const readyTimeout = setTimeout(() => {
      if (!resolved) {
        resolved = true;
        logToFile("Backend ready timeout (15s), proceeding anyway...");
        resolve(true);
      }
    }, 15000);

    backendProcess.stdout.on("data", (data) => {
      const msg = data.toString().trim();
      logToFile("[backend]", msg);
      if (!resolved && msg.includes("started successfully")) {
        resolved = true;
        clearTimeout(readyTimeout);
        logToFile("Backend ready signal received");
        resolve(true);
      }
    });

    backendProcess.stderr.on("data", (data) => {
      logToFile("[backend:stderr]", data.toString().trim());
    });

    backendProcess.on("exit", (code, signal) => {
      logToFile(`Backend process exited with code ${code}, signal ${signal}`);
      backendProcess = null;
      if (!resolved) {
        resolved = true;
        clearTimeout(readyTimeout);
        resolve(false);
      }
    });

    backendProcess.on("error", (err) => {
      logToFile("Failed to start backend process:", err.message);
      backendProcess = null;
      if (!resolved) {
        resolved = true;
        clearTimeout(readyTimeout);
        resolve(false);
      }
    });
  });
}

function stopBackendServer() {
  if (!backendProcess) return;

  console.log("Stopping embedded backend server...");

  try {
    backendProcess.send({ type: "shutdown" });
  } catch {
    // IPC channel may already be closed
  }

  const forceKillTimeout = setTimeout(() => {
    if (backendProcess) {
      console.log("Force killing backend process...");
      backendProcess.kill("SIGKILL");
      backendProcess = null;
    }
  }, 5000);

  backendProcess.on("exit", () => {
    clearTimeout(forceKillTimeout);
    backendProcess = null;
  });
}

const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
  console.log("Another instance is already running, quitting...");
  app.quit();
  process.exit(0);
} else {
  app.on("second-instance", (event, commandLine, workingDirectory) => {
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore();
      mainWindow.focus();
      mainWindow.show();
    }
  });
}

function createTray() {
  try {
    const { nativeImage } = require("electron");

    // Native APIs (Tray, nativeImage) can't load files from inside app.asar —
    // use the unpacked path so the OS sees a real file.
    const publicRoot = isDev
      ? path.join(appRoot, "public")
      : path.join(appRoot.replace("app.asar", "app.asar.unpacked"), "public");

    let trayIcon;
    if (process.platform === "darwin") {
      const iconPath = path.join(publicRoot, "icons", "16x16.png");
      trayIcon = nativeImage.createFromPath(iconPath);
      trayIcon.setTemplateImage(true);
    } else if (process.platform === "win32") {
      trayIcon = path.join(publicRoot, "icon.ico");
    } else {
      trayIcon = path.join(publicRoot, "icons", "32x32.png");
    }

    tray = new Tray(trayIcon);
    tray.setToolTip("Termix");

    const contextMenu = Menu.buildFromTemplate([
      {
        label: "Show Window",
        click: () => {
          if (mainWindow) {
            mainWindow.show();
            mainWindow.focus();
          }
        },
      },
      {
        label: "Quit",
        click: () => {
          isQuitting = true;
          app.quit();
        },
      },
    ]);

    tray.setContextMenu(contextMenu);

    tray.on("click", () => {
      if (mainWindow) {
        if (mainWindow.isVisible()) {
          mainWindow.hide();
        } else {
          mainWindow.show();
          mainWindow.focus();
        }
      }
    });

    console.log("System tray created successfully");
  } catch (err) {
    console.error("Failed to create system tray:", err);
  }
}

function createWindow() {
  const appVersion = app.getVersion();
  const electronVersion = process.versions.electron;
  const platform =
    process.platform === "win32"
      ? "Windows"
      : process.platform === "darwin"
        ? "macOS"
        : "Linux";

  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    title: "Termix",
    icon: path.join(appRoot, "public", "icon.png"),
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      webSecurity: false,
      preload: path.join(__dirname, "preload.js"),
      partition: termixSessionPartition,
      allowRunningInsecureContent: true,
      webviewTag: true,
      offscreen: false,
    },
    show: true,
  });

  mainWindow.webContents.session.setPermissionRequestHandler(
    (webContents, permission, callback) => {
      if (
        permission === "clipboard-read" ||
        permission === "clipboard-write" ||
        permission === "clipboard-sanitized-write"
      ) {
        callback(true);
        return;
      }
      callback(false);
    },
  );

  if (process.platform !== "darwin") {
    mainWindow.setMenuBarVisibility(false);
  }

  const customUserAgent = `Termix-Desktop/${appVersion} (${platform}; Electron/${electronVersion})`;
  mainWindow.webContents.setUserAgent(customUserAgent);

  mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
    (details, callback) => {
      details.requestHeaders["X-Electron-App"] = "true";

      details.requestHeaders["User-Agent"] = customUserAgent;

      const rememberedJwt = getRememberedElectronAuthCookie("jwt", details.url);
      if (rememberedJwt) {
        setCookieHeaderValue(
          details.requestHeaders,
          rememberedJwt.name,
          rememberedJwt.value,
        );
      }

      callback({ requestHeaders: details.requestHeaders });
    },
  );

  if (isDev) {
    mainWindow.loadURL("http://localhost:5173");
    mainWindow.webContents.openDevTools();
  } else {
    const indexPath = path.join(appRoot, "dist", "index.html");
    mainWindow.loadFile(indexPath).catch((err) => {
      console.error("Failed to load file:", err);
    });
  }

  mainWindow.webContents.session.webRequest.onHeadersReceived(
    (details, callback) => {
      const headers = details.responseHeaders;

      if (headers) {
        delete headers["x-frame-options"];
        delete headers["X-Frame-Options"];

        if (headers["content-security-policy"]) {
          headers["content-security-policy"] = headers[
            "content-security-policy"
          ]
            .map((value) => value.replace(/frame-ancestors[^;]*/gi, ""))
            .filter((value) => value.trim().length > 0);

          if (headers["content-security-policy"].length === 0) {
            delete headers["content-security-policy"];
          }
        }
        if (headers["Content-Security-Policy"]) {
          headers["Content-Security-Policy"] = headers[
            "Content-Security-Policy"
          ]
            .map((value) => value.replace(/frame-ancestors[^;]*/gi, ""))
            .filter((value) => value.trim().length > 0);

          if (headers["Content-Security-Policy"].length === 0) {
            delete headers["Content-Security-Policy"];
          }
        }

        const setCookieHeaderName = getHeaderName(headers, "Set-Cookie");
        if (setCookieHeaderName) {
          const setCookieHeaders = Array.isArray(headers[setCookieHeaderName])
            ? headers[setCookieHeaderName]
            : [headers[setCookieHeaderName]];

          setCookieHeaders.forEach((cookie) => {
            rememberElectronAuthCookieFromHeader(details.url, cookie);
          });

          headers[setCookieHeaderName] = setCookieHeaders.map((cookie) => {
            let modified = cookie.replace(
              /;\s*SameSite=Strict/gi,
              "; SameSite=None",
            );
            modified = modified.replace(
              /;\s*SameSite=Lax/gi,
              "; SameSite=None",
            );
            if (!modified.includes("SameSite=")) {
              modified += "; SameSite=None";
            }
            if (
              !modified.includes("Secure") &&
              details.url.startsWith("https")
            ) {
              modified += "; Secure";
            }
            return modified;
          });
        }
      }

      callback({ responseHeaders: headers });
    },
  );

  mainWindow.once("ready-to-show", () => {
    mainWindow.show();
  });

  setTimeout(() => {
    if (mainWindow && !mainWindow.isVisible()) {
      mainWindow.show();
    }
  }, 3000);

  mainWindow.webContents.on(
    "did-fail-load",
    (event, errorCode, errorDescription, validatedURL) => {
      console.error(
        "Failed to load:",
        errorCode,
        errorDescription,
        validatedURL,
      );
    },
  );

  mainWindow.webContents.on("did-finish-load", () => {
    console.log("Frontend loaded successfully");
  });

  mainWindow.on("close", (event) => {
    if (!isQuitting && tray && !tray.isDestroyed()) {
      event.preventDefault();
      mainWindow.hide();
    }
  });

  mainWindow.on("closed", () => {
    mainWindow = null;
  });

  mainWindow.webContents.setWindowOpenHandler(({ url }) => {
    shell.openExternal(url);
    return { action: "deny" };
  });
}

ipcMain.handle("get-app-version", () => {
  return app.getVersion();
});

const GITHUB_API_BASE = "https://api.github.com";
const REPO_OWNER = "Termix-SSH";
const REPO_NAME = "Termix";

const githubCache = new Map();
const CACHE_DURATION = 30 * 60 * 1000;

async function fetchGitHubAPI(endpoint, cacheKey) {
  const cached = githubCache.get(cacheKey);
  if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
    return {
      data: cached.data,
      cached: true,
      cache_age: Date.now() - cached.timestamp,
    };
  }

  try {
    const response = await httpFetch(`${GITHUB_API_BASE}${endpoint}`, {
      headers: {
        Accept: "application/vnd.github+json",
        "User-Agent": "TermixElectronUpdateChecker/1.0",
        "X-GitHub-Api-Version": "2022-11-28",
      },
      timeout: 10000,
    });

    if (!response.ok) {
      throw new Error(
        `GitHub API error: ${response.status} ${response.statusText}`,
      );
    }

    const data = await response.json();

    githubCache.set(cacheKey, {
      data,
      timestamp: Date.now(),
    });

    return {
      data: data,
      cached: false,
    };
  } catch (error) {
    console.error("Failed to fetch from GitHub API:", error);
    throw error;
  }
}

ipcMain.handle("check-electron-update", async () => {
  try {
    const localVersion = app.getVersion();

    const releaseData = await fetchGitHubAPI(
      `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
      "latest_release_electron",
    );

    const rawTag = releaseData.data.tag_name || releaseData.data.name || "";
    const remoteVersionMatch = rawTag.match(/(\d+\.\d+(\.\d+)?)/);
    const remoteVersion = remoteVersionMatch ? remoteVersionMatch[1] : null;

    if (!remoteVersion) {
      return {
        success: false,
        error: "Remote version not found",
        localVersion,
      };
    }

    const versionComparison = compareSemver(localVersion, remoteVersion);
    const status =
      versionComparison === null || versionComparison === 0
        ? "up_to_date"
        : versionComparison > 0
          ? "beta"
          : "requires_update";

    const result = {
      success: true,
      status,
      localVersion: localVersion,
      remoteVersion: remoteVersion,
      latest_release: {
        tag_name: releaseData.data.tag_name,
        name: releaseData.data.name,
        published_at: releaseData.data.published_at,
        html_url: releaseData.data.html_url,
        body: releaseData.data.body,
      },
      cached: releaseData.cached,
      cache_age: releaseData.cache_age,
    };

    return result;
  } catch (error) {
    return {
      success: false,
      error: error.message,
      localVersion: app.getVersion(),
    };
  }
});

ipcMain.handle("get-platform", () => {
  return process.platform;
});

ipcMain.handle("get-embedded-server-status", () => {
  return {
    running: backendProcess !== null && !backendProcess.killed,
    embedded: !isDev,
    dataDir: isDev ? null : getBackendDataDir(),
  };
});

ipcMain.handle("get-server-config", () => {
  try {
    const userDataPath = app.getPath("userData");
    const configPath = path.join(userDataPath, "server-config.json");

    if (fs.existsSync(configPath)) {
      const configData = fs.readFileSync(configPath, "utf8");
      return JSON.parse(configData);
    }
    return null;
  } catch (error) {
    console.error("Error reading server config:", error);
    return null;
  }
});

ipcMain.handle("save-server-config", (event, config) => {
  try {
    const userDataPath = app.getPath("userData");
    const configPath = path.join(userDataPath, "server-config.json");

    if (!fs.existsSync(userDataPath)) {
      fs.mkdirSync(userDataPath, { recursive: true });
    }

    fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
    return { success: true };
  } catch (error) {
    console.error("Error saving server config:", error);
    return { success: false, error: error.message };
  }
});

function getC2STunnelConfigPath() {
  return path.join(app.getPath("userData"), "c2s-tunnels.json");
}

ipcMain.handle("get-c2s-tunnel-config", () => {
  try {
    const configPath = getC2STunnelConfigPath();
    if (!fs.existsSync(configPath)) {
      return [];
    }
    const configData = fs.readFileSync(configPath, "utf8");
    const parsed = JSON.parse(configData);
    return Array.isArray(parsed) ? parsed : [];
  } catch (error) {
    console.error("Error reading C2S tunnel config:", error);
    return [];
  }
});

ipcMain.handle("save-c2s-tunnel-config", async (_event, config) => {
  try {
    if (!Array.isArray(config)) {
      return { success: false, error: "C2S tunnel config must be an array" };
    }
    const autoStartListeners = new Set();
    const autoStartRemoteListeners = new Set();
    for (const tunnel of config) {
      if (!tunnel?.autoStart) continue;
      const mode = tunnel.mode || tunnel.tunnelType || "local";
      if (mode === "remote") {
        const sourceHostId = Number(tunnel.sourceHostId);
        const sourcePort = Number(tunnel.sourcePort);
        if (
          !Number.isInteger(sourceHostId) ||
          sourceHostId < 1 ||
          !Number.isInteger(sourcePort) ||
          sourcePort < 1 ||
          sourcePort > 65535
        ) {
          return {
            success: false,
            error: "Invalid remote client tunnel endpoint or port",
          };
        }
        const listenerKey = `${sourceHostId}:${sourcePort}`;
        if (autoStartRemoteListeners.has(listenerKey)) {
          return {
            success: false,
            error: `Another auto-start client tunnel already uses remote ${listenerKey}`,
          };
        }
        autoStartRemoteListeners.add(listenerKey);
        continue;
      }

      const bindHost = tunnel.bindHost || "127.0.0.1";
      const sourcePort = Number(tunnel.sourcePort);
      const listenerKey = `${bindHost}:${sourcePort}`;
      if (autoStartListeners.has(listenerKey)) {
        return {
          success: false,
          error: `Another auto-start client tunnel already uses ${listenerKey}`,
        };
      }
      autoStartListeners.add(listenerKey);
    }
    for (const listenerKey of autoStartListeners) {
      const [bindHost, sourcePort] = listenerKey.split(":");
      const result = await checkLocalPortAvailable(
        bindHost,
        Number(sourcePort),
      );
      const ownedByClientTunnel = Array.from(c2sTunnelRuntimes.values()).some(
        (runtime) =>
          runtime.bindHost === bindHost &&
          runtime.sourcePort === Number(sourcePort),
      );
      if (!result.available && !ownedByClientTunnel) {
        return {
          success: false,
          error: `Cannot auto-start client tunnel on ${listenerKey}: ${result.error || "port is already in use"}`,
        };
      }
    }
    const userDataPath = app.getPath("userData");
    if (!fs.existsSync(userDataPath)) {
      fs.mkdirSync(userDataPath, { recursive: true });
    }
    fs.writeFileSync(getC2STunnelConfigPath(), JSON.stringify(config, null, 2));
    return { success: true };
  } catch (error) {
    console.error("Error saving C2S tunnel config:", error);
    return { success: false, error: error.message };
  }
});

function checkLocalPortAvailable(host, port) {
  return new Promise((resolve) => {
    const server = net.createServer();
    server.once("error", (error) => {
      resolve({ available: false, error: error.message });
    });
    server.once("listening", () => {
      server.close(() => resolve({ available: true }));
    });
    server.listen({ host, port });
  });
}

function checkTcpConnection(host, port) {
  return new Promise((resolve) => {
    const socket = net.createConnection({ host, port });
    const timer = setTimeout(() => {
      socket.destroy();
      resolve({ success: false, error: "Connection timed out" });
    }, 5000);

    socket.once("connect", () => {
      clearTimeout(timer);
      socket.destroy();
      resolve({ success: true });
    });
    socket.once("error", (error) => {
      clearTimeout(timer);
      socket.destroy();
      resolve({ success: false, error: error.message });
    });
  });
}

const c2sTunnelRuntimes = new Map();
const C2S_WS_HIGH_WATERMARK = 1024 * 1024;
const C2S_WS_LOW_WATERMARK = 256 * 1024;
const C2S_STREAM_WRITE_LIMIT = 8 * 1024 * 1024;

function getServerConfigSync() {
  try {
    const configPath = path.join(app.getPath("userData"), "server-config.json");
    if (!fs.existsSync(configPath)) return null;
    return JSON.parse(fs.readFileSync(configPath, "utf8"));
  } catch {
    return null;
  }
}

function getC2SRelayUrl() {
  const config = getServerConfigSync();
  const serverUrl =
    config?.serverUrl || (!isDev ? "http://127.0.0.1:30003" : null);
  if (!serverUrl) {
    throw new Error("No Termix server configured");
  }

  const base = serverUrl.replace(/\/$/, "");
  const relayHttpUrl = base.endsWith(":30003")
    ? `${base}/ssh/tunnel/c2s/stream`
    : `${base}/ssh/tunnel/c2s/stream`;
  return relayHttpUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
}

async function getC2SRelayHeaders(relayUrl) {
  if (!mainWindow?.webContents?.session) return {};

  const cookieUrl = relayUrl
    .replace(/^ws:/, "http:")
    .replace(/^wss:/, "https:");
  const cookies = await mainWindow.webContents.session.cookies.get({
    url: cookieUrl,
    name: "jwt",
  });
  const jwt = cookies[0]?.value;
  if (!jwt) return {};

  return {
    Cookie: `jwt=${encodeURIComponent(jwt)}`,
  };
}

function getC2STunnelName(tunnel, index = 0) {
  if (tunnel.name) return tunnel.name;
  return [
    "c2s",
    index,
    tunnel.sourceHostId || 0,
    tunnel.mode || tunnel.tunnelType || "local",
    tunnel.bindHost || "127.0.0.1",
    tunnel.sourcePort,
    tunnel.endpointPort || 0,
  ].join("::");
}

function getC2STunnelStatus(tunnelName) {
  return (
    c2sTunnelRuntimes.get(tunnelName)?.status || {
      connected: false,
      status: "DISCONNECTED",
    }
  );
}

function getAllC2STunnelStatuses() {
  const statuses = {};
  for (const [tunnelName] of c2sTunnelRuntimes.entries()) {
    statuses[tunnelName] = getC2STunnelStatus(tunnelName);
  }
  return statuses;
}

function emitC2STunnelStatuses() {
  if (!mainWindow || mainWindow.isDestroyed()) return;
  mainWindow.webContents.send("c2s-tunnel-statuses", getAllC2STunnelStatuses());
}

function setC2STunnelStatus(tunnelName, status) {
  const runtime = c2sTunnelRuntimes.get(tunnelName);
  if (runtime) {
    runtime.status = status;
    emitC2STunnelStatuses();
  }
}

function setC2STunnelError(tunnelName, message) {
  logToFile(`[c2s] ${tunnelName} failed:`, message);
  setC2STunnelStatus(tunnelName, {
    connected: false,
    status: "ERROR",
    reason: message,
  });
}

function parseSocks5Target(buffer) {
  if (buffer.length < 7 || buffer[0] !== 0x05 || buffer[1] !== 0x01) {
    return null;
  }

  const addressType = buffer[3];
  let offset = 4;
  let host;

  if (addressType === 0x01) {
    if (buffer.length < offset + 4 + 2) return null;
    host = Array.from(buffer.subarray(offset, offset + 4)).join(".");
    offset += 4;
  } else if (addressType === 0x03) {
    const length = buffer[offset];
    offset += 1;
    if (buffer.length < offset + length + 2) return null;
    host = buffer.subarray(offset, offset + length).toString("utf8");
    offset += length;
  } else if (addressType === 0x04) {
    if (buffer.length < offset + 16 + 2) return null;
    const parts = [];
    for (let i = 0; i < 16; i += 2) {
      parts.push(buffer.readUInt16BE(offset + i).toString(16));
    }
    host = parts.join(":");
    offset += 16;
  } else {
    throw new Error("Unsupported SOCKS5 address type");
  }

  const port = buffer.readUInt16BE(offset);
  return { host, port, bytesRead: offset + 2 };
}

async function openC2SRelay(
  tunnel,
  targetHost,
  targetPort,
  socket,
  initialData,
) {
  const tunnelName = tunnel.name || getC2STunnelName(tunnel);
  const relayUrl = getC2SRelayUrl();
  const headers = await getC2SRelayHeaders(relayUrl);
  logToFile(`[c2s] opening relay for ${tunnelName}`, {
    relayUrl,
    targetHost,
    targetPort,
  });
  setC2STunnelStatus(tunnelName, {
    connected: false,
    status: "CONNECTING",
    reason: `Opening relay to ${targetHost}:${targetPort}`,
  });
  const ws = new WebSocket(
    relayUrl,
    getWebSocketOptions(relayUrl, { headers }),
  );
  const pendingChunks = [];
  let ready = false;
  let closed = false;

  const cleanup = () => {
    if (closed) return;
    closed = true;
    try {
      socket.destroy();
    } catch {
      // expected during shutdown
    }
    try {
      ws.close();
    } catch {
      // expected during shutdown
    }
  };

  const sendChunk = (chunk) => {
    if (ready && ws.readyState === WebSocket.OPEN) {
      ws.send(chunk);
    } else {
      pendingChunks.push(chunk);
    }
  };

  socket.on("data", sendChunk);
  socket.on("close", cleanup);
  socket.on("error", (error) => {
    setC2STunnelError(tunnelName, error.message || "Local socket error");
    cleanup();
  });
  ws.on("close", cleanup);
  ws.on("error", (error) => {
    setC2STunnelError(tunnelName, error.message || "Relay connection failed");
    cleanup();
  });

  ws.on("open", () => {
    logToFile(`[c2s] relay connected for ${tunnelName}`);
    ws.send(
      JSON.stringify({
        type: "open",
        tunnelConfig: tunnel,
        targetHost,
        targetPort,
      }),
    );
  });

  ws.on("message", (data, isBinary) => {
    if (isBinary) {
      socket.write(Buffer.isBuffer(data) ? data : Buffer.from(data));
      return;
    }

    try {
      const message = JSON.parse(data.toString());
      if (message.type === "ready") {
        ready = true;
        logToFile(`[c2s] relay ready for ${tunnelName}`);
        setC2STunnelStatus(tunnelName, {
          connected: true,
          status: "CONNECTED",
        });
        if (initialData?.length) {
          ws.send(initialData);
        }
        while (pendingChunks.length > 0) {
          ws.send(pendingChunks.shift());
        }
      } else if (message.type === "error") {
        logToFile("[c2s] relay error:", message.error);
        setC2STunnelError(
          tunnelName,
          message.error || "Relay rejected the client tunnel",
        );
        cleanup();
      }
    } catch (error) {
      logToFile("[c2s] invalid relay message:", error.message);
      setC2STunnelError(tunnelName, error.message || "Invalid relay response");
      cleanup();
    }
  });
}

async function testC2SRelay(tunnel, targetHost, targetPort) {
  const relayUrl = getC2SRelayUrl();
  const headers = await getC2SRelayHeaders(relayUrl);
  const ws = new WebSocket(
    relayUrl,
    getWebSocketOptions(relayUrl, { headers }),
  );

  return new Promise((resolve) => {
    let settled = false;
    const settle = (result) => {
      if (settled) return;
      settled = true;
      try {
        ws.close();
      } catch {
        // expected during shutdown
      }
      resolve(result);
    };

    const timer = setTimeout(() => {
      settle({ success: false, error: "Tunnel test timed out" });
    }, 15000);

    ws.on("open", () => {
      ws.send(
        JSON.stringify({
          type: "test",
          tunnelConfig: tunnel,
          targetHost,
          targetPort,
        }),
      );
    });
    ws.on("message", (data, isBinary) => {
      if (isBinary) return;

      try {
        const message = JSON.parse(data.toString());
        if (message.type === "ready") {
          clearTimeout(timer);
          settle({ success: true });
        } else if (message.type === "error") {
          clearTimeout(timer);
          settle({
            success: false,
            error: message.error || "Tunnel test failed",
          });
        }
      } catch (error) {
        clearTimeout(timer);
        settle({ success: false, error: error.message });
      }
    });
    ws.on("error", (error) => {
      clearTimeout(timer);
      settle({ success: false, error: error.message });
    });
    ws.on("close", () => {
      clearTimeout(timer);
      settle({ success: false, error: "Tunnel test connection closed" });
    });
  });
}

async function testC2STunnel(tunnel, index = 0) {
  const mode = tunnel.mode || tunnel.tunnelType || "local";
  const testTunnel = {
    ...tunnel,
    name: `${getC2STunnelName(tunnel, index)}::test`,
    mode,
  };
  const bindHost = tunnel.bindHost || "127.0.0.1";
  const sourcePort = Number(tunnel.sourcePort);
  const endpointPort = Number(tunnel.endpointPort);

  if (!tunnel.sourceHostId) {
    return { success: false, error: "Endpoint SSH host is required" };
  }

  if (mode === "remote") {
    const localTarget = await checkTcpConnection(bindHost, endpointPort);
    if (!localTarget.success) {
      return {
        success: false,
        error: `Local target ${bindHost}:${endpointPort} is not reachable: ${localTarget.error}`,
      };
    }

    return testC2SRelay(testTunnel, undefined, undefined);
  }

  if (!Number.isInteger(sourcePort) || sourcePort < 1 || sourcePort > 65535) {
    return { success: false, error: "Invalid local port" };
  }

  const runtime = c2sTunnelRuntimes.get(getC2STunnelName(tunnel, index));
  if (!runtime) {
    const availability = await checkLocalPortAvailable(bindHost, sourcePort);
    if (!availability.available) {
      return {
        success: false,
        error: `Local listener ${bindHost}:${sourcePort} is not available: ${availability.error}`,
      };
    }
  }

  if (mode === "dynamic") {
    return testC2SRelay(testTunnel, undefined, undefined);
  }

  if (!Number.isInteger(endpointPort) || endpointPort < 1) {
    return { success: false, error: "Invalid remote port" };
  }

  return testC2SRelay(
    testTunnel,
    tunnel.targetHost || "127.0.0.1",
    endpointPort,
  );
}

function handleC2SDynamicConnection(tunnel, socket) {
  const tunnelName = tunnel.name || getC2STunnelName(tunnel);
  let buffer = Buffer.alloc(0);
  let stage = "greeting";

  const fail = (code = 0x01, message = "SOCKS5 request failed") => {
    setC2STunnelError(tunnelName, message);
    if (!socket.destroyed) {
      socket.write(Buffer.from([0x05, code, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
      socket.destroy();
    }
  };

  const onData = (chunk) => {
    buffer = Buffer.concat([buffer, chunk]);

    try {
      if (stage === "greeting") {
        if (buffer.length < 2) return;
        if (buffer[0] !== 0x05) {
          fail(0x01, "Invalid SOCKS5 greeting");
          return;
        }
        const methodsLength = buffer[1];
        if (buffer.length < 2 + methodsLength) return;
        socket.write(Buffer.from([0x05, 0x00]));
        buffer = buffer.subarray(2 + methodsLength);
        stage = "connect";
      }

      if (stage === "connect") {
        const target = parseSocks5Target(buffer);
        if (!target) return;

        stage = "piping";
        socket.off("data", onData);
        const remainder = buffer.subarray(target.bytesRead);
        socket.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
        openC2SRelay(tunnel, target.host, target.port, socket, remainder).catch(
          (error) => {
            logToFile("[c2s] dynamic relay failed:", error.message);
            fail(0x05, error.message || "Dynamic relay failed");
          },
        );
      }
    } catch (error) {
      logToFile("[c2s] SOCKS5 parse failed:", error.message);
      fail(0x01, error.message || "SOCKS5 parse failed");
    }
  };

  socket.on("data", onData);
  socket.on("error", () => socket.destroy());
}

function handleC2SLocalConnection(tunnel, socket) {
  const tunnelName = tunnel.name || getC2STunnelName(tunnel);
  const targetHost = tunnel.targetHost || "127.0.0.1";
  const targetPort = Number(tunnel.endpointPort);
  openC2SRelay(tunnel, targetHost, targetPort, socket).catch((error) => {
    logToFile("[c2s] local relay failed:", error.message);
    setC2STunnelError(tunnelName, error.message || "Local relay failed");
    socket.destroy();
  });
}

function pauseSourceForC2SWebSocket(ws, source) {
  if (!source?.pause || !source?.resume) return;
  if (ws.bufferedAmount <= C2S_WS_HIGH_WATERMARK) return;

  source.pause();
  const resumeTimer = setInterval(() => {
    if (
      ws.readyState !== WebSocket.OPEN ||
      source.destroyed ||
      ws.bufferedAmount <= C2S_WS_LOW_WATERMARK
    ) {
      clearInterval(resumeTimer);
      if (ws.readyState === WebSocket.OPEN && !source.destroyed) {
        source.resume();
      }
    }
  }, 25);
}

function sendC2SRemoteMessage(ws, message, source) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(message), (error) => {
      if (error && source?.destroy) {
        source.destroy(error);
      }
    });
    pauseSourceForC2SWebSocket(ws, source);
  }
}

function writeC2SRemoteChunk(target, chunk, ws, closeTarget) {
  if (!target || target.destroyed) return;

  if (target.writableLength > C2S_STREAM_WRITE_LIMIT) {
    closeTarget();
    return;
  }

  const canContinue = target.write(chunk);
  if (!canContinue && typeof ws.pause === "function") {
    ws.pause();
    target.once("drain", () => {
      if (ws.readyState === WebSocket.OPEN && typeof ws.resume === "function") {
        ws.resume();
      }
    });
  }
}

async function startC2SRemoteTunnel(tunnel, index = 0) {
  const tunnelName = getC2STunnelName(tunnel, index);
  const localHost = tunnel.bindHost || "127.0.0.1";
  const localPort = Number(tunnel.endpointPort);
  const remotePort = Number(tunnel.sourcePort);

  if (!tunnel.sourceHostId) {
    return { success: false, error: "Endpoint SSH host is required" };
  }
  if (!Number.isInteger(remotePort) || remotePort < 1 || remotePort > 65535) {
    return { success: false, error: "Invalid remote port" };
  }
  if (!Number.isInteger(localPort) || localPort < 1 || localPort > 65535) {
    return { success: false, error: "Invalid local port" };
  }

  const localTarget = await checkTcpConnection(localHost, localPort);
  if (!localTarget.success) {
    return {
      success: false,
      error: `Local target ${localHost}:${localPort} is not reachable: ${localTarget.error}`,
    };
  }

  const existing = c2sTunnelRuntimes.get(tunnelName);
  if (existing) {
    return { success: true, tunnelName };
  }

  for (const runtime of c2sTunnelRuntimes.values()) {
    if (
      runtime.mode === "remote" &&
      runtime.sourceHostId === Number(tunnel.sourceHostId) &&
      runtime.sourcePort === remotePort
    ) {
      return {
        success: false,
        error: `Another client remote tunnel already uses ${remotePort} on this endpoint`,
      };
    }
  }

  const relayUrl = getC2SRelayUrl();
  const headers = await getC2SRelayHeaders(relayUrl);
  const ws = new WebSocket(
    relayUrl,
    getWebSocketOptions(relayUrl, { headers }),
  );
  const sockets = new Map();
  let closed = false;

  const cleanup = () => {
    if (closed) return;
    closed = true;
    for (const socket of sockets.values()) {
      socket.destroy();
    }
    sockets.clear();
    try {
      ws.close();
    } catch {
      // expected during shutdown
    }
  };

  c2sTunnelRuntimes.set(tunnelName, {
    ws,
    sockets,
    mode: "remote",
    sourceHostId: Number(tunnel.sourceHostId),
    sourcePort: remotePort,
    bindHost: localHost,
    status: { connected: false, status: "CONNECTING" },
    close: cleanup,
  });
  emitC2STunnelStatuses();

  return new Promise((resolve) => {
    let settled = false;
    const settle = (result) => {
      if (settled) return;
      settled = true;
      resolve(result);
    };

    ws.on("open", () => {
      logToFile(`[c2s] opening remote tunnel ${tunnelName}`, {
        relayUrl,
        remotePort,
        localHost,
        localPort,
      });
      ws.send(
        JSON.stringify({
          type: "open",
          tunnelConfig: { ...tunnel, name: tunnelName, mode: "remote" },
        }),
      );
    });

    ws.on("message", (data, isBinary) => {
      if (isBinary) return;

      let message;
      try {
        message = JSON.parse(data.toString());
      } catch (error) {
        setC2STunnelError(tunnelName, error.message || "Invalid relay message");
        cleanup();
        settle({ success: false, error: error.message });
        return;
      }

      if (message.type === "ready") {
        setC2STunnelStatus(tunnelName, {
          connected: true,
          status: "CONNECTED",
        });
        settle({ success: true, tunnelName });
        return;
      }

      if (message.type === "error") {
        const error = message.error || "Relay rejected the client tunnel";
        setC2STunnelError(tunnelName, error);
        cleanup();
        c2sTunnelRuntimes.delete(tunnelName);
        emitC2STunnelStatuses();
        settle({ success: false, error });
        return;
      }

      if (message.type === "connection" && message.streamId) {
        const socket = net.createConnection(
          { host: localHost, port: localPort },
          () => {
            logToFile(`[c2s] remote stream ${message.streamId} connected`, {
              tunnelName,
              localHost,
              localPort,
            });
          },
        );
        sockets.set(message.streamId, socket);
        socket.on("data", (chunk) => {
          sendC2SRemoteMessage(
            ws,
            {
              type: "data",
              streamId: message.streamId,
              data: chunk.toString("base64"),
            },
            socket,
          );
        });
        socket.on("close", () => {
          sockets.delete(message.streamId);
          sendC2SRemoteMessage(ws, {
            type: "close",
            streamId: message.streamId,
          });
        });
        socket.on("error", (error) => {
          logToFile(`[c2s] remote stream ${message.streamId} failed:`, {
            tunnelName,
            error: error.message,
          });
          sockets.delete(message.streamId);
          sendC2SRemoteMessage(ws, {
            type: "close",
            streamId: message.streamId,
            error: error.message,
          });
        });
        return;
      }

      if (message.type === "data" && message.streamId && message.data) {
        const socket = sockets.get(message.streamId);
        writeC2SRemoteChunk(
          socket,
          Buffer.from(message.data, "base64"),
          ws,
          () => {
            if (socket) {
              sockets.delete(message.streamId);
              socket.destroy();
            }
          },
        );
        return;
      }

      if (message.type === "close" && message.streamId) {
        const socket = sockets.get(message.streamId);
        if (socket) {
          sockets.delete(message.streamId);
          socket.destroy();
        }
      }
    });

    ws.on("close", () => {
      cleanup();
      c2sTunnelRuntimes.delete(tunnelName);
      emitC2STunnelStatuses();
      settle({ success: false, error: "Remote tunnel relay closed" });
    });

    ws.on("error", (error) => {
      setC2STunnelError(tunnelName, error.message || "Relay connection failed");
      cleanup();
      c2sTunnelRuntimes.delete(tunnelName);
      emitC2STunnelStatuses();
      settle({ success: false, error: error.message });
    });
  });
}

async function startC2STunnel(tunnel, index = 0) {
  const mode = tunnel.mode || tunnel.tunnelType || "local";
  const tunnelName = getC2STunnelName(tunnel, index);
  const bindHost = tunnel.bindHost || "127.0.0.1";
  const sourcePort = Number(tunnel.sourcePort);
  logToFile(`[c2s] starting tunnel ${tunnelName}`, {
    mode,
    bindHost,
    sourcePort,
    sourceHostId: tunnel.sourceHostId,
    endpointPort: tunnel.endpointPort,
  });

  if (mode === "remote") {
    return startC2SRemoteTunnel(tunnel, index);
  }
  if (!tunnel.sourceHostId) {
    return { success: false, error: "Endpoint SSH host is required" };
  }
  if (!Number.isInteger(sourcePort) || sourcePort < 1 || sourcePort > 65535) {
    return { success: false, error: "Invalid local port" };
  }

  const existing = c2sTunnelRuntimes.get(tunnelName);
  if (existing) {
    return { success: true, tunnelName };
  }

  for (const runtime of c2sTunnelRuntimes.values()) {
    if (
      runtime.mode !== "remote" &&
      runtime.bindHost === bindHost &&
      runtime.sourcePort === sourcePort
    ) {
      return {
        success: false,
        error: `Another client tunnel already uses ${bindHost}:${sourcePort}`,
      };
    }
  }

  const availability = await checkLocalPortAvailable(bindHost, sourcePort);
  if (!availability.available) {
    return {
      success: false,
      error: availability.error || "Port is already in use",
    };
  }

  const sockets = new Set();
  const server = net.createServer((socket) => {
    sockets.add(socket);
    socket.on("close", () => sockets.delete(socket));
    if (mode === "dynamic") {
      handleC2SDynamicConnection({ ...tunnel, name: tunnelName, mode }, socket);
    } else {
      handleC2SLocalConnection({ ...tunnel, name: tunnelName, mode }, socket);
    }
  });

  c2sTunnelRuntimes.set(tunnelName, {
    server,
    sockets,
    bindHost,
    sourcePort,
    status: { connected: false, status: "CONNECTING" },
  });

  return new Promise((resolve) => {
    server.once("error", (error) => {
      c2sTunnelRuntimes.delete(tunnelName);
      logToFile(`[c2s] failed to listen for ${tunnelName}:`, error.message);
      emitC2STunnelStatuses();
      resolve({ success: false, error: error.message });
    });
    server.listen({ host: bindHost, port: sourcePort }, () => {
      logToFile(
        `[c2s] listening for ${tunnelName} on ${bindHost}:${sourcePort}`,
      );
      setC2STunnelStatus(tunnelName, {
        connected: true,
        status: "CONNECTED",
      });
      resolve({ success: true, tunnelName });
    });
  });
}

async function stopC2STunnel(tunnelName) {
  const runtime = c2sTunnelRuntimes.get(tunnelName);
  if (!runtime) {
    return { success: true };
  }

  setC2STunnelStatus(tunnelName, {
    connected: false,
    status: "DISCONNECTING",
  });

  return new Promise((resolve) => {
    if (typeof runtime.close === "function") {
      runtime.close();
      c2sTunnelRuntimes.delete(tunnelName);
      emitC2STunnelStatuses();
      resolve({ success: true });
      return;
    }

    for (const socket of runtime.sockets || []) {
      socket.destroy();
    }
    runtime.server?.close(() => {
      c2sTunnelRuntimes.delete(tunnelName);
      emitC2STunnelStatuses();
      resolve({ success: true });
    });
  });
}

function stopAllC2STunnels() {
  for (const [tunnelName, runtime] of c2sTunnelRuntimes.entries()) {
    try {
      if (typeof runtime.close === "function") {
        runtime.close();
      } else {
        for (const socket of runtime.sockets || []) {
          socket.destroy();
        }
        runtime.server?.close();
      }
    } catch (error) {
      logToFile(`[c2s] failed to stop tunnel ${tunnelName}:`, error.message);
    }
    c2sTunnelRuntimes.delete(tunnelName);
  }
  emitC2STunnelStatuses();
}

async function startC2SAutoStartTunnels() {
  const configPath = getC2STunnelConfigPath();
  if (!fs.existsSync(configPath)) {
    return { success: true, started: 0, errors: [] };
  }

  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
  const tunnels = Array.isArray(config) ? config : [];
  const errors = [];
  let started = 0;

  for (let index = 0; index < tunnels.length; index += 1) {
    const tunnel = tunnels[index];
    if (!tunnel?.autoStart) continue;
    const result = await startC2STunnel(tunnel, index);
    if (result.success) {
      started += 1;
    } else {
      errors.push(result.error || "Failed to start client tunnel");
    }
  }

  return { success: errors.length === 0, started, errors };
}

ipcMain.handle("check-local-port-available", async (_event, host, port) => {
  const sourcePort = Number(port);
  if (
    !host ||
    !Number.isInteger(sourcePort) ||
    sourcePort < 1 ||
    sourcePort > 65535
  ) {
    return { available: false, error: "Invalid local bind address or port" };
  }
  return checkLocalPortAvailable(host, sourcePort);
});

ipcMain.handle("start-c2s-tunnel", async (_event, tunnel, index) => {
  try {
    return await startC2STunnel(tunnel, Number(index) || 0);
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("test-c2s-tunnel", async (_event, tunnel, index) => {
  try {
    return await testC2STunnel(tunnel, Number(index) || 0);
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("stop-c2s-tunnel", async (_event, tunnelName) => {
  try {
    return await stopC2STunnel(tunnelName);
  } catch (error) {
    return { success: false, error: error.message };
  }
});

ipcMain.handle("get-c2s-tunnel-statuses", () => {
  return getAllC2STunnelStatuses();
});

ipcMain.handle("start-c2s-autostart-tunnels", async () => {
  try {
    return await startC2SAutoStartTunnels();
  } catch (error) {
    return { success: false, started: 0, errors: [error.message] };
  }
});

ipcMain.handle("get-c2s-tunnel-preset-default-name", () => {
  const now = new Date();
  const date = now.toISOString().slice(0, 10);
  const platform =
    process.platform === "darwin"
      ? "macOS"
      : process.platform === "win32"
        ? "Windows"
        : "Linux";
  const release = os.release();
  const computerName = os.hostname();
  return `[${date}] ${computerName} (${platform} ${release})`;
});

ipcMain.handle("get-setting", (event, key) => {
  try {
    const userDataPath = app.getPath("userData");
    const settingsPath = path.join(userDataPath, "settings.json");

    if (!fs.existsSync(settingsPath)) {
      return null;
    }

    const settingsData = fs.readFileSync(settingsPath, "utf8");
    const settings = JSON.parse(settingsData);
    return settings[key] !== undefined ? settings[key] : null;
  } catch (error) {
    console.error("Error reading setting:", error);
    return null;
  }
});

ipcMain.handle("set-setting", (event, key, value) => {
  try {
    const userDataPath = app.getPath("userData");
    const settingsPath = path.join(userDataPath, "settings.json");

    if (!fs.existsSync(userDataPath)) {
      fs.mkdirSync(userDataPath, { recursive: true });
    }

    let settings = {};
    if (fs.existsSync(settingsPath)) {
      const settingsData = fs.readFileSync(settingsPath, "utf8");
      settings = JSON.parse(settingsData);
    }

    settings[key] = value;
    fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
    return { success: true };
  } catch (error) {
    console.error("Error saving setting:", error);
    return { success: false, error: error.message };
  }
});

ipcMain.handle("get-session-cookie", async (_event, name, targetUrl) => {
  try {
    const ses = mainWindow?.webContents?.session;
    if (!ses) return null;
    const cookies = await ses.cookies.get({
      name,
      ...(targetUrl ? { url: targetUrl } : {}),
    });
    const cookie = cookies.find((candidate) =>
      cookieMatchesUrl(candidate, targetUrl),
    );
    return (
      cookie?.value ||
      getRememberedElectronAuthCookie(name, targetUrl)?.value ||
      null
    );
  } catch (error) {
    console.error("Failed to get session cookie:", error);
    return getRememberedElectronAuthCookie(name, targetUrl)?.value || null;
  }
});

function cookieMatchesUrl(cookie, targetUrl) {
  if (!targetUrl) return true;

  try {
    const targetHost = new URL(targetUrl).hostname;
    const cookieDomain = (cookie.domain || "").replace(/^\./, "");

    return (
      cookieDomain === targetHost ||
      targetHost.endsWith(`.${cookieDomain}`) ||
      (!cookieDomain && targetHost === "localhost")
    );
  } catch {
    return true;
  }
}

ipcMain.handle(
  "wait-session-cookie",
  async (_event, name, targetUrl, previousValue, timeoutMs = 5000) => {
    const ses = mainWindow?.webContents?.session;
    if (!ses) return { success: false, error: "No Electron session" };

    const existingCookies = await ses.cookies.get({
      name,
      ...(targetUrl ? { url: targetUrl } : {}),
    });
    const existingCookie = existingCookies.find((cookie) =>
      cookieMatchesUrl(cookie, targetUrl),
    );
    if (existingCookie?.value && existingCookie.value !== previousValue) {
      return { success: true, value: existingCookie.value };
    }

    const rememberedCookie = getRememberedElectronAuthCookie(name, targetUrl);
    if (rememberedCookie?.value && rememberedCookie.value !== previousValue) {
      return { success: true, value: rememberedCookie.value };
    }

    return new Promise((resolve) => {
      const timeout = setTimeout(() => {
        ses.cookies.off("changed", onCookieChanged);
        resolve({ success: false, error: "Timed out waiting for cookie" });
      }, timeoutMs);

      function onCookieChanged(_event, cookie, _cause, removed) {
        if (
          removed ||
          cookie.name !== name ||
          !cookie.value ||
          cookie.value === previousValue ||
          !cookieMatchesUrl(cookie, targetUrl)
        ) {
          return;
        }

        clearTimeout(timeout);
        ses.cookies.off("changed", onCookieChanged);
        resolve({ success: true, value: cookie.value });
      }

      ses.cookies.on("changed", onCookieChanged);
    });
  },
);

ipcMain.handle("clear-session-cookies", async () => {
  try {
    clearPersistedElectronAuthCookies();
    const ses = mainWindow?.webContents?.session;
    if (ses) {
      const cookies = await ses.cookies.get({});
      for (const cookie of cookies) {
        await ses.cookies.remove(getCookieRemovalUrl(cookie), cookie.name);
      }
    }
  } catch (error) {
    console.error("Failed to clear session cookies:", error);
  }
});

ipcMain.handle("clipboard-write-text", (_event, text) => {
  clipboard.writeText(typeof text === "string" ? text : String(text ?? ""));
  return true;
});

ipcMain.handle("clipboard-read-text", () => clipboard.readText());

ipcMain.handle("test-server-connection", async (event, serverUrl) => {
  try {
    const normalizedServerUrl = serverUrl.replace(/\/$/, "");

    const healthUrl = `${normalizedServerUrl}/health`;

    try {
      const response = await httpFetch(healthUrl, {
        method: "GET",
        timeout: 10000,
      });

      if (response.ok) {
        const data = await response.text();

        if (
          data.includes("<html") ||
          data.includes("<!DOCTYPE") ||
          data.includes("<head>") ||
          data.includes("<body>")
        ) {
          return {
            success: false,
            error:
              "Server returned HTML instead of JSON. This does not appear to be a Termix server.",
          };
        }

        try {
          const healthData = JSON.parse(data);
          if (
            healthData &&
            (healthData.status === "ok" ||
              healthData.status === "healthy" ||
              healthData.healthy === true ||
              healthData.database === "connected")
          ) {
            return {
              success: true,
              status: response.status,
              testedUrl: healthUrl,
            };
          }
        } catch (parseError) {
          console.log("Health endpoint did not return valid JSON");
        }
      }
    } catch (urlError) {
      console.error("Health check failed:", urlError);
    }

    try {
      const versionUrl = `${normalizedServerUrl}/version`;
      const response = await httpFetch(versionUrl, {
        method: "GET",
        timeout: 10000,
      });

      if (response.ok) {
        const data = await response.text();

        if (
          data.includes("<html") ||
          data.includes("<!DOCTYPE") ||
          data.includes("<head>") ||
          data.includes("<body>")
        ) {
          return {
            success: false,
            error:
              "Server returned HTML instead of JSON. This does not appear to be a Termix server.",
          };
        }

        try {
          const versionData = JSON.parse(data);
          if (
            versionData &&
            (versionData.status === "up_to_date" ||
              versionData.status === "requires_update" ||
              (versionData.localVersion &&
                versionData.version &&
                versionData.latest_release))
          ) {
            return {
              success: true,
              status: response.status,
              testedUrl: versionUrl,
              warning:
                "Health endpoint not available, but server appears to be running",
            };
          }
        } catch (parseError) {
          console.log("Version endpoint did not return valid JSON");
        }
      }
    } catch (versionError) {
      console.error("Version check failed:", versionError);
    }

    return {
      success: false,
      error:
        "Server is not responding or does not appear to be a valid Termix server. Please ensure the server is running and accessible.",
    };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

function createMenu() {
  if (process.platform === "darwin") {
    const template = [
      {
        label: app.name,
        submenu: [
          { role: "about" },
          { type: "separator" },
          { role: "services" },
          { type: "separator" },
          { role: "hide" },
          { role: "hideOthers" },
          { role: "unhide" },
          { type: "separator" },
          { role: "quit" },
        ],
      },
      {
        label: "Edit",
        submenu: [
          { role: "undo" },
          { role: "redo" },
          { type: "separator" },
          { role: "cut" },
          { role: "copy" },
          { role: "paste" },
          { role: "selectAll" },
        ],
      },
      {
        label: "View",
        submenu: [
          { role: "reload" },
          { role: "forceReload" },
          { role: "toggleDevTools" },
          { type: "separator" },
          { role: "resetZoom" },
          { role: "zoomIn" },
          { role: "zoomOut" },
          { type: "separator" },
          { role: "togglefullscreen" },
        ],
      },
      {
        label: "Window",
        submenu: [
          { role: "minimize" },
          { role: "zoom" },
          { type: "separator" },
          { role: "front" },
          { type: "separator" },
          { role: "window" },
        ],
      },
    ];
    const menu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(menu);
  }
}

app.whenReady().then(async () => {
  logToFile("=== App ready ===");
  logToFile(
    "isDev:",
    isDev,
    "platform:",
    process.platform,
    "arch:",
    process.arch,
  );
  createMenu();
  await clearElectronClientCacheIfBuildChanged();
  await clearElectronJwtCookiesAtStartup();

  if (!isDev) {
    const result = await startBackendServer();
    logToFile("startBackendServer result:", result);
  } else {
    logToFile(
      "Skipping embedded backend (isDev=true) - expecting separate dev:backend process",
    );
  }

  createTray();
  createWindow();
  logToFile("=== Startup complete ===");
});

app.on("window-all-closed", () => {
  if (!tray || tray.isDestroyed()) {
    app.quit();
  }
});

app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

app.on("before-quit", () => {
  isQuitting = true;
});

app.on("will-quit", () => {
  console.log("App will quit...");
  stopAllC2STunnels();
  stopBackendServer();
});

process.on("uncaughtException", (error) => {
  console.error("Uncaught Exception:", error);
});

process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection at:", promise, "reason:", reason);
});

```

## /electron/preload.js

```js path="/electron/preload.js" 
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
  getAppVersion: () => ipcRenderer.invoke("get-app-version"),

  removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel),
  isElectron: true,
  isDev: process.env.NODE_ENV === "development",

  getSetting: (key) => ipcRenderer.invoke("get-setting", key),
  setSetting: (key, value) => ipcRenderer.invoke("set-setting", key, value),
  getC2STunnelConfig: () => ipcRenderer.invoke("get-c2s-tunnel-config"),
  saveC2STunnelConfig: (config) =>
    ipcRenderer.invoke("save-c2s-tunnel-config", config),
  checkLocalPortAvailable: (host, port) =>
    ipcRenderer.invoke("check-local-port-available", host, port),
  getC2STunnelPresetDefaultName: () =>
    ipcRenderer.invoke("get-c2s-tunnel-preset-default-name"),
  startC2STunnel: (tunnel, index) =>
    ipcRenderer.invoke("start-c2s-tunnel", tunnel, index),
  testC2STunnel: (tunnel, index) =>
    ipcRenderer.invoke("test-c2s-tunnel", tunnel, index),
  stopC2STunnel: (tunnelName) =>
    ipcRenderer.invoke("stop-c2s-tunnel", tunnelName),
  getC2STunnelStatuses: () => ipcRenderer.invoke("get-c2s-tunnel-statuses"),
  onC2STunnelStatuses: (callback) => {
    const listener = (_event, statuses) => callback(statuses);
    ipcRenderer.on("c2s-tunnel-statuses", listener);
    return () => ipcRenderer.removeListener("c2s-tunnel-statuses", listener);
  },
  startC2SAutoStartTunnels: () =>
    ipcRenderer.invoke("start-c2s-autostart-tunnels"),

  clearSessionCookies: () => ipcRenderer.invoke("clear-session-cookies"),
  getSessionCookie: (name, targetUrl) =>
    ipcRenderer.invoke("get-session-cookie", name, targetUrl),
  waitForSessionCookie: (name, targetUrl, previousValue, timeoutMs) =>
    ipcRenderer.invoke(
      "wait-session-cookie",
      name,
      targetUrl,
      previousValue,
      timeoutMs,
    ),

  invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
});

contextBridge.exposeInMainWorld("electronClipboard", {
  writeText: (text) => ipcRenderer.invoke("clipboard-write-text", text),
  readText: () => ipcRenderer.invoke("clipboard-read-text"),
});

window.IS_ELECTRON = true;

```

## /eslint.config.js

```js path="/eslint.config.js" 
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import unusedImports from "eslint-plugin-unused-imports";
import tseslint from "typescript-eslint";
import { globalIgnores } from "eslint/config";

export default tseslint.config([
  globalIgnores(["dist", "release", "Mobile"]),
  {
    files: ["**/*.{ts,tsx}"],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactRefresh.configs.vite,
    ],
    plugins: {
      "react-hooks": reactHooks,
      "unused-imports": unusedImports,
    },
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    rules: {
      "unused-imports/no-unused-imports": "error",
      "unused-imports/no-unused-vars": [
        "warn",
        {
          vars: "all",
          varsIgnorePattern: "^_",
          args: "after-used",
          argsIgnorePattern: "^_",
        },
      ],
      "@typescript-eslint/no-unused-vars": "off",
      "@typescript-eslint/no-explicit-any": "warn",
      "@typescript-eslint/no-unused-expressions": "warn",
      "no-empty": "warn",
      "no-control-regex": "off",
      "no-useless-assignment": "off",
      "preserve-caught-error": "off",
      "react-hooks/exhaustive-deps": "warn",
      "react-hooks/rules-of-hooks": "error",
      "react-refresh/only-export-components": "warn",
    },
  },
]);

```

## /flatpak/com.karmaa.termix.desktop

```desktop path="/flatpak/com.karmaa.termix.desktop" 
[Desktop Entry]
Name=Termix
Comment=Web-based server management platform with SSH terminal, tunneling, and file editing
Exec=run.sh %U
Icon=com.karmaa.termix
Terminal=false
Type=Application
Categories=Development;Network;System;
Keywords=ssh;terminal;server;management;tunnel;
StartupWMClass=termix
StartupNotify=true

```

## /flatpak/com.karmaa.termix.flatpakref

```flatpakref path="/flatpak/com.karmaa.termix.flatpakref" 
[Flatpak Ref]
Name=Termix
Branch=stable
Title=Termix - SSH Server Management Platform
IsRuntime=false
Url=https://github.com/Termix-SSH/Termix/releases/download/VERSION_PLACEHOLDER/termix_linux_flatpak.flatpak
RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo
Comment=Web-based server management platform with SSH terminal, tunneling, and file editing
Description=Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides SSH terminal access, tunneling capabilities, and remote file management.
Icon=https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.png
Homepage=https://github.com/Termix-SSH/Termix

```

## /flatpak/com.karmaa.termix.metainfo.xml

```xml path="/flatpak/com.karmaa.termix.metainfo.xml" 
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
  <id>com.karmaa.termix</id>
  <name>Termix</name>
  <summary>Web-based server management platform with SSH terminal, tunneling, and file editing</summary>

  <metadata_license>CC0-1.0</metadata_license>
  <project_license>Apache-2.0</project_license>

  <developer_name>bugattiguy527</developer_name>

  <description>
    <p>
      Termix is an open-source, forever-free, self-hosted all-in-one server management platform.
      It provides a web-based solution for managing your servers and infrastructure through a single, intuitive interface.
    </p>
    <p>Features:</p>
    <ul>
      <li>SSH terminal access with full terminal emulation</li>
      <li>SSH tunneling capabilities for secure port forwarding</li>
      <li>Remote file management with editor support</li>
      <li>Server monitoring and management tools</li>
      <li>Self-hosted solution - keep your data private</li>
      <li>Modern, intuitive web interface</li>
    </ul>
  </description>

  <launchable type="desktop-id">com.karmaa.termix.desktop</launchable>

  <screenshots>
    <screenshot type="default">
      <image>https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/screenshots/terminal.png</image>
      <caption>SSH Terminal Interface</caption>
    </screenshot>
  </screenshots>

  <url type="homepage">https://github.com/Termix-SSH/Termix</url>
  <url type="bugtracker">https://github.com/Termix-SSH/Support/issues</url>
  <url type="help">https://docs.termix.site</url>
  <url type="vcs-browser">https://github.com/Termix-SSH/Termix</url>

  <content_rating type="oars-1.1">
    <content_attribute id="social-info">moderate</content_attribute>
  </content_rating>

  <releases>
    <release version="VERSION_PLACEHOLDER" date="DATE_PLACEHOLDER">
      <description>
        <p>Latest release of Termix</p>
      </description>
      <url>https://github.com/Termix-SSH/Termix/releases</url>
    </release>
  </releases>

  <categories>
    <category>Development</category>
    <category>Network</category>
    <category>System</category>
  </categories>

  <keywords>
    <keyword>ssh</keyword>
    <keyword>terminal</keyword>
    <keyword>server</keyword>
    <keyword>management</keyword>
    <keyword>tunnel</keyword>
    <keyword>file-manager</keyword>
  </keywords>

  <provides>
    <binary>termix</binary>
  </provides>

  <requires>
    <internet>always</internet>
  </requires>
</component>

```

## /flatpak/com.karmaa.termix.yml

```yml path="/flatpak/com.karmaa.termix.yml" 
app-id: com.karmaa.termix
runtime: org.freedesktop.Platform
runtime-version: "24.08"
sdk: org.freedesktop.Sdk
base: org.electronjs.Electron2.BaseApp
base-version: "24.08"
command: run.sh
separate-locales: false

finish-args:
  - --socket=x11
  - --socket=wayland
  - --socket=pulseaudio
  - --share=network
  - --share=ipc
  - --device=dri
  - --filesystem=home
  - --socket=ssh-auth
  - --socket=session-bus
  - --talk-name=org.freedesktop.secrets
  - --env=ELECTRON_TRASH=gio
  - --env=XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons
  - --env=ELECTRON_OZONE_PLATFORM_HINT=auto

modules:
  - name: termix
    buildsystem: simple
    build-commands:
      - chmod +x termix.AppImage
      - ./termix.AppImage --appimage-extract

      - install -Dm755 squashfs-root/termix /app/bin/termix
      - cp -r squashfs-root/resources /app/bin/
      - cp -r squashfs-root/locales /app/bin/ || true

      - cp squashfs-root/*.so /app/bin/ || true
      - cp squashfs-root/*.pak /app/bin/ || true
      - cp squashfs-root/*.bin /app/bin/ || true
      - cp squashfs-root/*.dat /app/bin/ || true
      - cp squashfs-root/*.json /app/bin/ || true

      - |
        cat > run.sh << 'EOF'
        #!/bin/bash
        export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID"
        exec zypak-wrapper /app/bin/termix "$@"
        EOF
      - chmod +x run.sh
      - install -Dm755 run.sh /app/bin/run.sh

      - install -Dm644 com.karmaa.termix.desktop /app/share/applications/com.karmaa.termix.desktop

      - install -Dm644 com.karmaa.termix.metainfo.xml /app/share/metainfo/com.karmaa.termix.metainfo.xml

      - install -Dm644 com.karmaa.termix.svg /app/share/icons/hicolor/scalable/apps/com.karmaa.termix.svg
      - install -Dm644 icon-256.png /app/share/icons/hicolor/256x256/apps/com.karmaa.termix.png || true
      - install -Dm644 icon-128.png /app/share/icons/hicolor/128x128/apps/com.karmaa.termix.png || true

    sources:
      - type: file
        url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_x64_appimage.AppImage
        sha256: CHECKSUM_X64_PLACEHOLDER
        dest-filename: termix.AppImage
        only-arches:
          - x86_64

      - type: file
        url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_arm64_appimage.AppImage
        sha256: CHECKSUM_ARM64_PLACEHOLDER
        dest-filename: termix.AppImage
        only-arches:
          - aarch64

      - type: file
        path: com.karmaa.termix.desktop

      - type: file
        path: com.karmaa.termix.metainfo.xml

      - type: file
        path: com.karmaa.termix.svg

      - type: file
        path: icon-256.png

      - type: file
        path: icon-128.png

```

## /flatpak/flathub.json

```json path="/flatpak/flathub.json" 
{
  "only-arches": ["x86_64", "aarch64"],
  "skip-icons-check": false,
  "skip-appstream-check": false
}

```

## /index.html

```html path="/index.html" 
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <meta name="theme-color" content="#09090b" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta
      name="apple-mobile-web-app-status-bar-style"
      content="black-translucent"
    />
    <meta name="apple-mobile-web-app-title" content="Termix" />
    <link rel="apple-touch-icon" href="/icons/512x512.png" />
    <link rel="manifest" href="/manifest.json" />
    <title>Termix</title>
    <style>
      .hide-scrollbar {
        scrollbar-width: none;
        -ms-overflow-style: none;
      }

      .hide-scrollbar::-webkit-scrollbar {
        display: none;
      }

      .skinny-scrollbar {
        scrollbar-width: thin;
        scrollbar-color: #4a4a4a #1e1e21;
      }

      .skinny-scrollbar::-webkit-scrollbar {
        width: 6px;
        height: 6px;
      }

      .skinny-scrollbar::-webkit-scrollbar-track {
        background: #1e1e21;
      }

      .skinny-scrollbar::-webkit-scrollbar-thumb {
        background-color: #4a4a4a;
        border-radius: 3px;
        border: 1px solid #1e1e21;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

## /package.json

```json path="/package.json" 
{
  "name": "termix",
  "private": true,
  "version": "2.2.1",
  "description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
  "author": "Karmaa",
  "main": "electron/main.cjs",
  "type": "module",
  "engines": {
    "node": ">=22.12.0",
    "npm": ">=11"
  },
  "scripts": {
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "postinstall": "node scripts/patch-app-builder-lib.cjs",
    "prebuild": "node scripts/write-electron-build-info.cjs",
    "lint": "eslint .",
    "lint:fix": "eslint --fix .",
    "type-check": "tsc --noEmit",
    "dev": "vite",
    "build": "vite build && tsc -p tsconfig.node.json",
    "build:backend": "tsc -p tsconfig.node.json",
    "dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js",
    "dev:docker": "docker stop termix-dev 2>nul & docker rm termix-dev 2>nul & docker build -f docker/Dockerfile -t termix:dev --no-cache . && docker run -d --name termix-dev -p 3000:3000 -p 8080:8080 -p 30001-30006:30001-30006 -v \"%cd%\\db\\data:/app/data\" termix:dev",
    "dev:docker:restart": "docker stop termix-dev 2>nul & docker rm termix-dev 2>nul & docker run -d --name termix-dev -p 8080:8080 -p 30001-30006:30001-30006 -v \"%cd%\\db\\data:/app/data\" termix:dev",
    "generate:openapi": "tsc -p tsconfig.node.json && node ./dist/backend/backend/swagger.js",
    "preview": "vite preview",
    "electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && electron .\"",
    "electron:patch-builder": "node scripts/patch-app-builder-lib.cjs",
    "electron:rebuild": "electron-rebuild -f -w better-sqlite3",
    "build:win-portable": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --win --dir",
    "build:win-installer": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --win --publish=never",
    "build:linux-portable": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux --dir",
    "build:linux-appimage": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux AppImage",
    "build:linux-targz": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux tar.gz",
    "build:mac": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --mac --universal",
    "build:mac-dev": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --mac dir --publish=never"
  },
  "dependencies": {
    "axios": "^1.15.2",
    "bcryptjs": "^3.0.3",
    "better-sqlite3": "^12.9.0",
    "body-parser": "^2.2.2",
    "chalk": "^5.6.2",
    "cookie-parser": "^1.4.7",
    "cors": "^2.8.6",
    "dotenv": "^17.4.2",
    "drizzle-orm": "^0.45.2",
    "express": "^5.2.1",
    "guacamole-lite": "^1.2.0",
    "https-proxy-agent": "^7.0.6",
    "jose": "^6.2.2",
    "js-yaml": "^4.1.1",
    "jsonwebtoken": "^9.0.3",
    "jszip": "^3.10.1",
    "motion": "^12.38.0",
    "multer": "^2.1.1",
    "nanoid": "^5.1.9",
    "qrcode": "^1.5.4",
    "react-is": "^19.2.5",
    "socks": "^2.8.7",
    "speakeasy": "^2.0.0",
    "ssh2": "^1.17.0",
    "undici": "^7.0.0",
    "ws": "^8.20.0"
  },
  "devDependencies": {
    "@codemirror/autocomplete": "^6.20.1",
    "@codemirror/commands": "^6.10.3",
    "@codemirror/search": "^6.7.0",
    "@codemirror/theme-one-dark": "^6.1.3",
    "@codemirror/view": "^6.41.1",
    "@commitlint/cli": "^20.5.0",
    "@commitlint/config-conventional": "^20.5.0",
    "@deadendjs/swagger-jsdoc": "^8.1.2",
    "@electron/notarize": "^3.1.1",
    "@electron/rebuild": "^4.0.4",
    "@eslint/js": "^9.0.0",
    "@hookform/resolvers": "^5.2.2",
    "@monaco-editor/react": "^4.7.0",
    "@radix-ui/react-accordion": "^1.2.12",
    "@radix-ui/react-alert-dialog": "^1.1.15",
    "@radix-ui/react-checkbox": "^1.3.3",
    "@radix-ui/react-dialog": "^1.1.15",
    "@radix-ui/react-dropdown-menu": "^2.1.16",
    "@radix-ui/react-label": "^2.1.8",
    "@radix-ui/react-popover": "^1.1.15",
    "@radix-ui/react-progress": "^1.1.8",
    "@radix-ui/react-scroll-area": "^1.2.10",
    "@radix-ui/react-select": "^2.2.6",
    "@radix-ui/react-separator": "^1.1.8",
    "@radix-ui/react-slider": "^1.3.6",
    "@radix-ui/react-slot": "^1.2.4",
    "@radix-ui/react-switch": "^1.2.6",
    "@radix-ui/react-tabs": "^1.1.13",
    "@radix-ui/react-tooltip": "^1.2.8",
    "@tailwindcss/vite": "^4.2.4",
    "@types/better-sqlite3": "^7.6.13",
    "@types/cookie-parser": "^1.4.10",
    "@types/cors": "^2.8.19",
    "@types/express": "^5.0.6",
    "@types/guacamole-common-js": "^1.5.5",
    "@types/js-yaml": "^4.0.9",
    "@types/jsonwebtoken": "^9.0.10",
    "@types/multer": "^2.1.0",
    "@types/node": "^24.12.2",
    "@types/qrcode": "^1.5.6",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
    "@types/speakeasy": "^2.0.10",
    "@types/ssh2": "^1.15.5",
    "@types/ws": "^8.18.1",
    "@uiw/codemirror-extensions-langs": "^4.25.9",
    "@uiw/codemirror-theme-github": "^4.25.9",
    "@uiw/react-codemirror": "^4.25.9",
    "@vitejs/plugin-react": "^6.0.1",
    "@xterm/addon-clipboard": "^0.2.0",
    "@xterm/addon-fit": "^0.10.0",
    "@xterm/addon-unicode11": "^0.8.0",
    "@xterm/addon-web-links": "^0.11.0",
    "@xterm/xterm": "^5.5.0",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "^1.1.1",
    "concurrently": "^9.2.1",
    "cytoscape": "^3.33.2",
    "electron": "^41.3.0",
    "electron-builder": "^26.8.1",
    "eslint": "^9.0.0",
    "eslint-plugin-react-hooks": "^7.1.1",
    "eslint-plugin-react-refresh": "^0.5.2",
    "eslint-plugin-unused-imports": "^4.4.1",
    "globals": "^17.5.0",
    "guacamole-common-js": "^1.5.0",
    "husky": "^9.1.7",
    "i18next": "^26.0.8",
    "i18next-browser-languagedetector": "^8.2.1",
    "lint-staged": "^16.4.0",
    "lucide-react": "^1.11.0",
    "next-themes": "^0.4.6",
    "prettier": "3.8.3",
    "react": "^19.2.5",
    "react-cytoscapejs": "^2.0.0",
    "react-dom": "^19.2.5",
    "react-grid-layout": "^2.2.3",
    "react-h5-audio-player": "^3.10.2",
    "react-hook-form": "^7.73.1",
    "react-i18next": "^17.0.4",
    "react-icons": "^5.6.0",
    "react-markdown": "^10.1.0",
    "react-pdf": "^10.4.1",
    "react-photo-view": "^1.2.7",
    "react-resizable-panels": "^4.10.0",
    "react-simple-keyboard": "^3.8.196",
    "react-syntax-highlighter": "^16.1.1",
    "react-xtermjs": "^1.0.10",
    "recharts": "^3.8.1",
    "remark-gfm": "^4.0.1",
    "sonner": "^2.0.7",
    "tailwind-merge": "^3.5.0",
    "tailwindcss": "^4.2.4",
    "typescript": "~6.0.3",
    "typescript-eslint": "^8.59.0",
    "vite": "^8.0.10",
    "zod": "^4.3.6"
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{js,jsx}": [
      "prettier --write"
    ],
    "*.{json,css,md}": [
      "prettier --write"
    ]
  },
  "overrides": {
    "@electron/asar": "^4.2.0",
    "@electron/get": "^5.0.0",
    "dompurify": "^3.4.1",
    "eslint-visitor-keys": "^4.2.1",
    "prebuild-install": "npm:@mmomtchev/prebuild-install@1.0.2",
    "rimraf": "file:vendor/rimraf-compat"
  }
}

```

## /public/favicon.ico

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/favicon.ico

## /public/fonts/CaskaydiaCoveNerdFontMono-Bold.ttf

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/fonts/CaskaydiaCoveNerdFontMono-Bold.ttf

## /public/fonts/CaskaydiaCoveNerdFontMono-BoldItalic.ttf

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/fonts/CaskaydiaCoveNerdFontMono-BoldItalic.ttf

## /public/fonts/CaskaydiaCoveNerdFontMono-Italic.ttf

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/fonts/CaskaydiaCoveNerdFontMono-Italic.ttf

## /public/fonts/CaskaydiaCoveNerdFontMono-Regular.ttf

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/fonts/CaskaydiaCoveNerdFontMono-Regular.ttf

## /public/full-icon.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/full-icon.png

## /public/icon-mac.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icon-mac.png

## /public/icon.icns

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icon.icns

## /public/icon.ico

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icon.ico

## /public/icon.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icon.png

## /public/icons/1024x1024.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icons/1024x1024.png

## /public/icons/128x128.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icons/128x128.png

## /public/icons/16x16.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icons/16x16.png

## /public/icons/24x24.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icons/24x24.png

## /public/icons/256x256.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icons/256x256.png

## /public/icons/32x32.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icons/32x32.png

## /public/icons/48x48.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icons/48x48.png

## /public/icons/512x512.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icons/512x512.png

## /public/icons/64x64.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icons/64x64.png

## /public/icons/icon.icns

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icons/icon.icns

## /public/icons/icon.ico

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/public/icons/icon.ico

## /public/manifest.json

```json path="/public/manifest.json" 
{
  "name": "Termix",
  "short_name": "Termix",
  "description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
  "theme_color": "#09090b",
  "background_color": "#09090b",
  "display": "standalone",
  "orientation": "any",
  "scope": "./",
  "start_url": "./",
  "icons": [
    {
      "src": "./icons/48x48.png",
      "sizes": "48x48",
      "type": "image/png"
    },
    {
      "src": "./icons/64x64.png",
      "sizes": "64x64",
      "type": "image/png"
    },
    {
      "src": "./icons/128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "./icons/256x256.png",
      "sizes": "256x256",
      "type": "image/png"
    },
    {
      "src": "./icons/512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "categories": ["utilities", "developer", "productivity"]
}

```

## /public/sw.js

```js path="/public/sw.js" 
const CACHE_NAME = "termix-static-v2";
const STATIC_ASSETS = [
  "/favicon.ico",
  "/icons/48x48.png",
  "/icons/128x128.png",
  "/icons/256x256.png",
  "/icons/512x512.png",
];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(CACHE_NAME)
      .then((cache) => {
        return cache.addAll(STATIC_ASSETS);
      })
      .then(() => {
        return self.skipWaiting();
      }),
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches
      .keys()
      .then((cacheNames) => {
        return Promise.all(
          cacheNames
            .filter((name) => name !== CACHE_NAME)
            .map((name) => {
              return caches.delete(name);
            }),
        );
      })
      .then(() => {
        return self.clients.claim();
      }),
  );
});

self.addEventListener("fetch", (event) => {
  const { request } = event;
  const url = new URL(request.url);

  if (request.method !== "GET") {
    return;
  }

  if (url.pathname.startsWith("/api/") || url.pathname.startsWith("/ws")) {
    return;
  }

  if (
    url.pathname.startsWith("/host/opkssh-chooser/") ||
    url.pathname.startsWith("/host/opkssh-callback/")
  ) {
    return;
  }

  if (url.origin !== self.location.origin) {
    return;
  }

  if (request.mode === "navigate") {
    event.respondWith(fetch(request));
    return;
  }

  const isStaticAsset = STATIC_ASSETS.some((asset) => url.pathname === asset);

  if (!isStaticAsset) {
    return;
  }

  event.respondWith(
    caches.match(request).then((cachedResponse) => {
      if (cachedResponse) {
        return cachedResponse;
      }

      return fetch(request).then((response) => {
        if (!response || response.status !== 200 || response.type !== "basic") {
          return response;
        }

        const responseClone = response.clone();
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(request, responseClone);
        });

        return response;
      });
    }),
  );
});

```

## /readme/README-AR.md

# إحصائيات المستودع

<p align="center">
<a href="../README.md">🇺🇸 English</a> · <a href="README-CN.md">🇨🇳 中文</a> · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · 🇸🇦 العربية · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>

<p align="center">
  <img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
  <br>
  <small style="color: #666;">تم تحقيقه في 1 سبتمبر 2025</small>
</p>

<br />
<p align="center">
  <a href="https://github.com/Termix-SSH/Termix">
    <img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;">  </a>
</p>

# نظرة عامة

<p align="center">
  <a href="https://github.com/Termix-SSH/Termix">
    <img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;">  </a>
</p>

Termix هي منصة مفتوحة المصدر ومجانية للأبد وذاتية الاستضافة لإدارة الخوادم بشكل شامل. توفر حلاً متعدد المنصات لإدارة خوادمك وبنيتك التحتية من خلال واجهة واحدة وسهلة الاستخدام. يوفر Termix الوصول إلى طرفية SSH، والتحكم في سطح المكتب البعيد (RDP، VNC، Telnet)، وقدرات إنشاء أنفاق SSH، وإدارة ملفات SSH عن بُعد، والعديد من الأدوات الأخرى. يُعد Termix البديل المثالي المجاني وذاتي الاستضافة لـ Termius المتاح لجميع المنصات.

# الميزات

- **الوصول إلى طرفية SSH** - طرفية كاملة الميزات مع دعم تقسيم الشاشة (حتى 4 لوحات) مع نظام علامات تبويب شبيه بالمتصفح. يتضمن دعم تخصيص الطرفية بما في ذلك سمات الطرفية الشائعة والخطوط والمكونات الأخرى.
- **الوصول إلى سطح المكتب البعيد** - دعم RDP و VNC و Telnet عبر المتصفح مع تخصيص كامل وتقسيم الشاشة.
- **إدارة أنفاق SSH** - إنشاء وإدارة أنفاق SSH بين الخوادم مع إعادة الاتصال التلقائي ومراقبة الحالة وإعادة التوجيه المحلي أو البعيد أو SOCKS الديناميكي. يتم تخزين إعدادات نفق العميل-المكتبي إلى السيرفر محلياً لكل تثبيت مكتبي؛ يمكن حفظ لقطات C2S الاختيارية على الخادم وإعادة تسميتها وتحميلها أو حذفها لنقل تكوين النفق المحلي بين العملاء.
- **مدير الملفات عن بُعد** - إدارة الملفات مباشرة على الخوادم البعيدة مع دعم عرض وتحرير الكود والصور والصوت والفيديو. رفع وتنزيل وإعادة تسمية وحذف ونقل الملفات بسلاسة مع دعم sudo.
- **إدارة Docker** - تشغيل وإيقاف وتعليق وحذف الحاويات. عرض إحصائيات الحاويات. التحكم في الحاوية باستخدام طرفية docker exec. لم يُصمم ليحل محل Portainer أو Dockge بل لإدارة حاوياتك ببساطة مقارنة بإنشائها.
- **مدير مضيفات SSH** - حفظ وتنظيم وإدارة اتصالات SSH الخاصة بك باستخدام العلامات والمجلدات، وحفظ بيانات تسجيل الدخول القابلة لإعادة الاستخدام بسهولة مع إمكانية أتمتة نشر مفاتيح SSH.
- **إحصائيات الخادم** - عرض استخدام المعالج والذاكرة والقرص إلى جانب الشبكة ووقت التشغيل ومعلومات النظام وجدار الحماية ومراقب المنافذ على معظم الخوادم المبنية على Linux.
- **لوحة التحكم** - عرض معلومات الخادم بنظرة واحدة على لوحة التحكم.
- **RBAC** - إنشاء الأدوار ومشاركة المضيفات عبر المستخدمين/الأدوار.
- **مصادقة المستخدمين** - إدارة آمنة للمستخدمين مع ضوابط إدارية ودعم OIDC (مع التحكم في الوصول) و 2FA (TOTP). عرض جلسات المستخدمين النشطة عبر جميع المنصات وإلغاء الصلاحيات. ربط حسابات OIDC/المحلية معاً.
- **تشفير قاعدة البيانات** - يُخزَّن الخادم الخلفي كملفات قاعدة بيانات SQLite مشفرة. اطلع على [الوثائق](https://docs.termix.site/security) لمزيد من المعلومات.
- **مفاتيح API** - إنشاء مفاتيح API محددة النطاق للمستخدم مع تواريخ انتهاء صلاحية للاستخدام في الأتمتة/CI.
- **تصدير/استيراد البيانات** - تصدير واستيراد مضيفات SSH وبيانات الاعتماد وبيانات مدير الملفات.
- **إعداد SSL تلقائي** - إنشاء وإدارة شهادات SSL مدمجة مع إعادة التوجيه إلى HTTPS.
- **واجهة مستخدم حديثة** - واجهة نظيفة متوافقة مع سطح المكتب والهاتف المحمول مبنية بـ React و Tailwind CSS و Shadcn. الاختيار بين العديد من سمات واجهة المستخدم بما في ذلك الفاتح والداكن و Dracula وغيرها. استخدام مسارات URL لفتح أي اتصال في وضع ملء الشاشة.
- **اللغات** - دعم مدمج لحوالي 30 لغة (تُدار بواسطة [Crowdin](https://docs.termix.site/translations)).
- **دعم المنصات** - متاح كتطبيق ويب، وتطبيق سطح مكتب (Windows و Linux و macOS، يمكن تشغيله بشكل مستقل بدون خادم Termix الخلفي)، و PWA، وتطبيق مخصص للهاتف المحمول/الجهاز اللوحي لـ iOS و Android.
- **أدوات SSH** - إنشاء مقتطفات أوامر قابلة لإعادة الاستخدام تُنفَّذ بنقرة واحدة. تشغيل أمر واحد في وقت واحد عبر عدة طرفيات مفتوحة.
- **سجل الأوامر** - الإكمال التلقائي وعرض أوامر SSH التي تم تنفيذها سابقاً.
- **الاتصال السريع** - الاتصال بخادم دون الحاجة إلى حفظ بيانات الاتصال.
- **لوحة الأوامر** - اضغط مرتين على Shift الأيسر للوصول السريع إلى اتصالات SSH باستخدام لوحة المفاتيح.
- **ميزات SSH الغنية** - دعم مضيفات القفز، Warpgate، الاتصالات المبنية على TOTP، SOCKS5، التحقق من مفتاح المضيف، الملء التلقائي لكلمة المرور، [OPKSSH](https://github.com/openpubkey/opkssh)، tmux، port knocking، إلخ.
- **الرسم البياني للشبكة** - تخصيص لوحة التحكم لتصور مختبرك المنزلي بناءً على اتصالات SSH مع دعم الحالة.
- **علامات التبويب الدائمة** - تبقى جلسات SSH وعلامات التبويب مفتوحة عبر الأجهزة/التحديثات إذا تم تفعيلها في ملف تعريف المستخدم.

# الميزات المخططة

راجع [المشاريع](https://github.com/orgs/Termix-SSH/projects/2) لعرض جميع الميزات المخططة. إذا كنت تتطلع للمساهمة، راجع [المساهمة](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md).

# التثبيت

الأجهزة المدعومة:

- الموقع الإلكتروني (أي متصفح حديث على أي منصة مثل Chrome و Safari و Firefox) (يتضمن دعم PWA)
- Windows (x64/ia32)
  - نسخة محمولة
  - مثبت MSI
  - مدير حزم Chocolatey
- Linux (x64/ia32)
  - نسخة محمولة
  - AUR
  - AppImage
  - Deb
  - Flatpak
- macOS (x64/ia32 على الإصدار 12.0+)
  - Apple App Store
  - DMG
  - Homebrew
- iOS/iPadOS (الإصدار 15.1+)
  - Apple App Store
  - IPA
- Android (الإصدار 7.0+)
  - Google Play Store
  - APK

قم بزيارة [وثائق](https://docs.termix.site/install) Termix للحصول على مزيد من المعلومات حول كيفية تثبيت Termix على جميع المنصات. بخلاف ذلك، يمكنك الاطلاع على نموذج ملف Docker Compose هنا (يمكنك حذف guacd والشبكة إذا كنت لا تخطط لاستخدام ميزات سطح المكتب البعيد):

```yaml
services:
  termix:
    image: ghcr.io/lukegus/termix:latest
    container_name: termix
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - termix-data:/app/data
    environment:
      PORT: "8080"
    depends_on:
      - guacd
    networks:
      - termix-net

  guacd:
    image: guacamole/guacd:1.6.0
    container_name: guacd
    restart: unless-stopped
    ports:
      - "4822:4822"
    networks:
      - termix-net

volumes:
  termix-data:
    driver: local

networks:
  termix-net:
    driver: bridge
```

# الرعاة

<p align="left">
  <a href="https://www.digitalocean.com/">
    <img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://crowdin.com/">
    <img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://www.blacksmith.sh/">
    <img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://www.cloudflare.com/">
    <img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://tailscale.com/">
    <img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://akamai.com/">
    <img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://aws.amazon.com/">
    <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
  </a>
</p>

# الدعم

إذا كنت بحاجة إلى مساعدة أو ترغب في طلب ميزة لـ Termix، قم بزيارة صفحة [المشكلات](https://github.com/Termix-SSH/Support/issues)، وسجل الدخول، واضغط على `New Issue`.
يرجى أن تكون مفصلاً قدر الإمكان في مشكلتك، ويُفضَّل كتابتها باللغة الإنجليزية. يمكنك أيضاً الانضمام إلى خادم [Discord](https://discord.gg/jVQGdvHDrf) وزيارة قناة الدعم، ومع ذلك قد تكون أوقات الاستجابة أطول.

# لقطات الشاشة

[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)

<p align="center">
  <img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
  <img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>

<p align="center">
  <img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
  <img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>

<p align="center">
  <img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
  <img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>

<p align="center">
  <img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
  <img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>

<p align="center">
  <img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
  <img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>

<p align="center">
  <img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
  <img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>

قد تكون بعض مقاطع الفيديو والصور قديمة أو قد لا تعرض الميزات بشكل مثالي.

# الترخيص

موزع بموجب رخصة Apache License الإصدار 2.0. راجع ملف LICENSE لمزيد من المعلومات.


## /readme/README-CN.md

# 仓库统计

<p align="center">
<a href="../README.md">🇺🇸 English</a> · 🇨🇳 中文 · <a href="README-JA.md">🇯🇵 日本語</a> · <a href="README-KO.md">🇰🇷 한국어</a> · <a href="README-FR.md">🇫🇷 Français</a> · <a href="README-DE.md">🇩🇪 Deutsch</a> · <a href="README-ES.md">🇪🇸 Español</a> · <a href="README-PT.md">🇧🇷 Português</a> · <a href="README-RU.md">🇷🇺 Русский</a> · <a href="README-AR.md">🇸🇦 العربية</a> · <a href="README-HI.md">🇮🇳 हिन्दी</a> · <a href="README-TR.md">🇹🇷 Türkçe</a> · <a href="README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="README-IT.md">🇮🇹 Italiano</a>
</p>

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>

<p align="center">
  <img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
  <br>
  <small style="color: #666;">获得于 2025年9月1日</small>
</p>

<br />
<p align="center">
  <a href="https://github.com/Termix-SSH/Termix">
    <img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;">  </a>
</p>

# 概览

<p align="center">
  <a href="https://github.com/Termix-SSH/Termix">
    <img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;">  </a>
</p>

Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个多平台解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix 提供 SSH 终端访问、远程桌面控制(RDP、VNC、Telnet)、SSH 隧道功能、远程 SSH 文件管理以及许多其他工具。Termix 是适用于所有平台的完美免费自托管 Termius 替代品。

# 功能

- **SSH 终端访问** - 功能齐全的终端,支持分屏(最多 4 个面板),并配有类似浏览器的标签系统。包括对自定义终端的支持,如常用的终端主题、字体和其他组件。
- **远程桌面访问** - 通过浏览器支持 RDP、VNC 和 Telnet,具有完整的自定义和分屏功能。
- **SSH 隧道管理** - 创建和管理具有自动重连和健康监测功能的服务器间 SSH 隧道,支持本地、远程或动态 SOCKS 转发。桌面客户端到服务器的隧道设置按桌面安装本地存储,可选的 C2S 预设快照可保存到服务器、重命名、加载或删除,以便在客户端之间迁移本地隧道配置。
- **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。支持通过 sudo 无缝上传、下载、重命名、删除和移动文件。
- **Docker 管理** - 启动、停止、暂停、移除容器。查看容器统计信息。通过 docker exec 终端控制容器。它的初衷不是取代 Portainer 或 Dockge,而是为了比直接创建容器更简单地管理它们。
- **SSH 主机管理器** - 通过标签和文件夹保存、组织和管理您的 SSH 连接,轻松保存可重用的登录信息,并能自动化部署 SSH 密钥。
- **服务器统计** - 在大多数基于 Linux 的服务器上查看 CPU、内存、磁盘使用情况以及网络、运行时间、系统信息、防火墙和端口监控。
- **仪表板** - 在仪表板上一目了然地查看服务器信息。
- **RBAC** - 创建角色并在用户/角色之间共享主机。
- **用户认证** - 安全的用户管理,具有管理员控制、OIDC(带访问控制)和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDC/本地账户链接在一起。
- **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://docs.termix.site/security)了解更多。
- **API 密钥** - 创建带有到期日期的用户范围 API 密钥,用于自动化/CI。
- **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据。
- **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向。
- **现代 UI** - 使用 React、Tailwind CSS 和 Shadcn 构建的整洁的桌面/移动友好界面。有多种 UI 主题可选,包括浅色、深色、Dracula 等。使用 URL 路由全屏打开任何连接。
- **语言** - 内置支持约 30 种语言(由 [Crowdin](https://docs.termix.site/translations) 管理)。
- **平台支持** - 提供 Web 应用、桌面应用(Windows、Linux 和 macOS,可脱离 Termix 后端独立运行)、PWA 以及 iOS 和 Android 专用移动/平板应用。
- **SSH 工具** - 创建可重用的命令片段,只需点击一下即可执行。在多个打开的终端中同时运行一个命令。
- **命令历史** - 自动完成并查看之前运行过的 SSH 命令。
- **快速连接** - 无需保存连接数据即可连接到服务器。
- **命令面板** - 双击左 Shift 键即可通过键盘快速访问 SSH 连接。
- **丰富的功能** - 支持跳转主机、Warpgate、基于 TOTP 的连接、SOCKS5、主机密钥验证、密码自动填充、[OPKSSH](https://github.com/openpubkey/opkssh)、tmux、端口敲击等。
- **网络图** - 自定义您的仪表板,根据您的 SSH 连接可视化您的家庭实验室,并支持状态监测。
- **持久标签页** - 如果在用户个人资料中启用,SSH 会话和标签页将在设备/刷新后保持打开状态。

# 计划功能

查看 [Projects](https://github.com/orgs/Termix-SSH/projects/2) 了解所有计划功能。如果您想贡献代码,请参阅 [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)。

# 安装

支持的设备:

- 网站(任何平台上的任何现代浏览器,如 Chrome、Safari 和 Firefox)(包括 PWA 支持)
- Windows (x64/ia32)
  - 便携版
  - MSI 安装程序
  - Chocolatey 软件包管理器
- Linux (x64/ia32)
  - 便携版
  - AUR
  - AppImage
  - Deb
  - Flatpak
- macOS (x64/ia32, v12.0+)
  - Apple App Store
  - DMG
  - Homebrew
- iOS/iPadOS (v15.1+)
  - Apple App Store
  - IPA
- Android (v7.0+)
  - Google Play 商店
  - APK

访问 Termix [文档](https://docs.termix.site/install) 了解有关如何在所有平台上安装 Termix 的更多信息。此外,这里有一个示例 Docker Compose 文件(如果您不打算使用远程桌面功能,可以省略 guacd 和网络部分):

```yaml
services:
  termix:
    image: ghcr.io/lukegus/termix:latest
    container_name: termix
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - termix-data:/app/data
    environment:
      PORT: "8080"
    depends_on:
      - guacd
    networks:
      - termix-net

  guacd:
    image: guacamole/guacd:1.6.0
    container_name: guacd
    restart: unless-stopped
    ports:
      - "4822:4822"
    networks:
      - termix-net

volumes:
  termix-data:
    driver: local

networks:
  termix-net:
    driver: bridge
```

# 赞助商

<p align="left">
  <a href="https://www.digitalocean.com/">
    <img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://crowdin.com/">
    <img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://www.blacksmith.sh/">
    <img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://www.cloudflare.com/">
    <img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://tailscale.com/">
    <img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://akamai.com/">
    <img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
  </a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://aws.amazon.com/">
    <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
  </a>
</p>

# 支持

如果您需要 Termix 的帮助或想要请求功能,请访问 [Issues](https://github.com/Termix-SSH/Support/issues) 页面,登录并点击 `New Issue`。
请尽可能详细地描述您的问题,建议使用英语。您也可以加入 [Discord](https://discord.gg/jVQGdvHDrf) 服务器并访问支持频道,但响应时间可能较长。

# 展示

[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)

<p align="center">
  <img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
  <img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>

<p align="center">
  <img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
  <img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>

<p align="center">
  <img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
  <img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>

<p align="center">
  <img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
  <img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>

<p align="center">
  <img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
  <img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>

<p align="center">
  <img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
  <img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>

某些视频和图像可能已过时,或者可能无法完美展示功能。

# 许可证

根据 Apache License Version 2.0 发布。更多信息请参见 LICENSE。


## /repo-images/HeaderImage.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/repo-images/HeaderImage.png

## /repo-images/Image 1.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/repo-images/Image 1.png

## /repo-images/Image 10.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/repo-images/Image 10.png

## /repo-images/Image 11.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/repo-images/Image 11.png

## /repo-images/Image 12.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/repo-images/Image 12.png

## /repo-images/Image 2.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/repo-images/Image 2.png

## /repo-images/Image 3.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/repo-images/Image 3.png

## /repo-images/Image 4.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/repo-images/Image 4.png

## /repo-images/Image 5.png

Binary file available at https://raw.githubusercontent.com/LukeGus/Termix/refs/heads/main/repo-images/Image 5.png


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!