33 Commits

Author SHA1 Message Date
starhound
247c1b5c0a feat: Enhance Guacamole integration with extended configuration options
- Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings.
- Implemented logging for token requests and received options for better debugging.
- Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values.
- Integrated Guacamole configuration parsing in HostManagerViewer and Host components.
- Enhanced API requests to include extended Guacamole configuration parameters in the token request.
- Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API.
2025-12-20 09:59:29 -05:00
starhound
776f581377 feat: Add support for RDP and VNC connections in SSH host management
- Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures.
- Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert.
- Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options.
- Implemented token retrieval for RDP/VNC connections using Guacamole API.
- Updated UI components to reflect connection type changes and provide appropriate connection buttons.
- Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor.
- Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions.
2025-12-19 16:08:27 -05:00
starhound
3ac7ad0bd7 feat: enhance Mouse.State constructor to accept optional parameters and object destructuring 2025-12-19 04:03:20 -05:00
starhound
bc6264bb50 feat: add TypeScript definitions for guacamole-common-js module 2025-12-19 03:58:26 -05:00
starhound
5d61112a4e feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay 2025-12-19 03:55:46 -05:00
LukeGus
d047beab13 fix: merge syntax errors 2025-12-18 13:58:02 -06:00
Luke Gustafson
f2285b1abb Merge branch 'dev-1.10.0' into starhound/guacd-docker-compose 2025-12-18 13:50:39 -06:00
LukeGus
48933e9b11 fix: finalize adding docker to db 2025-12-18 12:56:17 -06:00
LukeGus
3248b2336b Merge remote-tracking branch 'origin/dev-1.10.0' into dev-1.10.0 2025-12-18 02:18:15 -06:00
LukeGus
4b4bff4b29 feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation 2025-12-18 02:18:08 -06:00
starhound
2f092bd367 feat: enhance Guacamole support with RDP and VNC connection settings and UI updates 2025-12-17 19:14:19 -05:00
starhound
42e27e7389 feat: add Guacamole support for RDP, VNC, and Telnet connections
- Implemented WebSocket support for Guacamole in Nginx configuration.
- Added REST API endpoints for generating connection tokens and checking guacd status.
- Created Guacamole server using guacamole-lite for handling connections.
- Developed frontend components for testing RDP/VNC connections and displaying the remote session.
- Updated package dependencies to include guacamole-common-js and guacamole-lite.
- Enhanced logging for Guacamole operations.
2025-12-17 15:47:42 -05:00
Wesley Reid
aa1476fc13 Remove PTY-level keepalive (#449)
* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
2025-12-16 22:27:53 -06:00
Nunzio Marfè
7c9762562b Translations (#447)
* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Added Italian Language;

* Fix translations;

Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations.

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
2025-12-16 14:41:59 -06:00
Luke Gustafson
a84eb5636e Auto collapse snippet folders (#448)
* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* feat: Add collapsable snippets (customizable in user profile)
2025-12-16 14:13:07 -06:00
Nunzio Marfè
65466bc3f9 Added Italian Language; (#445)
* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Added Italian Language;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
2025-12-12 11:26:40 -06:00
Nunzio Marfè
208110a433 Sudo auto fill password (#441)
* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Feature Sudo password auto-fill;

* Fix locale json shema;

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
2025-12-07 23:04:48 -06:00
SlimGary
a98359ebc1 Adding Comment at the end of the public_key on the host on deploy (#440)
* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* -Add New Interface for Credential DB
-Add Credential Name as a comment into the server authorized_key file

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
2025-12-07 23:03:40 -06:00
LukeGus
05a1b3bfaf fix: Remove comming soon for app stores in readme 2025-11-26 23:39:22 -06:00
LukeGus
dfb9e7afe7 fix: Sandbox error with Linux 2025-11-26 19:44:19 -06:00
LukeGus
e405f8a6fa fix: Change name for electron macos cask output 2025-11-26 02:52:30 -06:00
LukeGus
f8de3369c3 fix: No sandbox flag issue 2025-11-25 20:57:24 -06:00
LukeGus
150d5796f8 feat: Re-add homebrew cask and move scripts to backend 2025-11-25 20:56:00 -06:00
LukeGus
18f31ade1e fix: Flatpak runtime error and install freedesktop ver warning 2025-11-25 20:31:12 -06:00
LukeGus
4863776f9b fix: Flatpak runtime error and install freedesktop ver warning 2025-11-25 20:31:03 -06:00
LukeGus
84c7b9f9fc fix: Build error with runtime repo flag 2025-11-25 17:41:03 -06:00
LukeGus
2754585988 fix: Add imagemagik to electron builder to resolve build error 2025-11-25 17:13:08 -06:00
LukeGus
a06e62b81a Merge remote-tracking branch 'origin/dev-1.10.0' into dev-1.10.0 2025-11-25 14:33:10 -06:00
LukeGus
69dfebab37 feat: Automate flatpak 2025-11-25 14:32:59 -06:00
junu
4da2b985ad Add Korean translation (#439)
Co-authored-by: 송준우 <2484@coreit.co.kr>
2025-11-25 14:11:46 -06:00
LukeGus
b57cc52c94 fix: Remove homebrew old stuff 2025-11-25 00:48:47 -06:00
LukeGus
757d0c246d fix: Checksum issue with chocolatey 2025-11-22 20:32:07 -06:00
Tran Trung Kien
7975a077ea fix select edit host but not update view (#438) 2025-11-21 12:03:05 -06:00
71 changed files with 12008 additions and 2298 deletions

View File

@@ -27,7 +27,7 @@ on:
jobs: jobs:
build-windows: build-windows:
runs-on: windows-latest runs-on: windows-latest
if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '' 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: permissions:
contents: write contents: write
@@ -72,10 +72,6 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build && npx electron-builder --win --x64 --ia32 run: npm run build && npx electron-builder --win --x64 --ia32
- name: List release files
run: |
dir release
- name: Upload Windows x64 NSIS Installer - name: Upload Windows x64 NSIS Installer
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: hashFiles('release/termix_windows_x64_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none' if: hashFiles('release/termix_windows_x64_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none'
@@ -136,7 +132,7 @@ jobs:
build-linux: build-linux:
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '' 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: permissions:
contents: write contents: write
@@ -199,17 +195,6 @@ jobs:
cd .. cd ..
- name: List release files
run: |
ls -la release/
- name: Debug electron-builder output
if: always()
run: |
if [ -f "release/builder-debug.yml" ]; then
cat release/builder-debug.yml
fi
- name: Upload Linux x64 AppImage - name: Upload Linux x64 AppImage
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: hashFiles('release/termix_linux_x64_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none' if: hashFiles('release/termix_linux_x64_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none'
@@ -282,6 +267,93 @@ jobs:
path: release/termix_linux_armv7l_portable.tar.gz path: release/termix_linux_armv7l_portable.tar.gz
retention-days: 30 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
# Determine the architecture
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
FLATPAK_ARCH="x86_64"
elif [ "$ARCH" = "aarch64" ]; then
FLATPAK_ARCH="aarch64"
else
FLATPAK_ARCH="$ARCH"
fi
# Build bundle for the current architecture
flatpak build-bundle repo ../release/termix_linux_flatpak.flatpak com.karmaa.termix --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
- name: Create flatpakref file
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: build-macos:
runs-on: macos-latest runs-on: macos-latest
if: github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all' if: github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all'
@@ -425,11 +497,6 @@ jobs:
export GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" export GH_TOKEN="${{ secrets.GITHUB_TOKEN }}"
npx electron-builder --mac dmg --universal --x64 --arm64 --publish never npx electron-builder --mac dmg --universal --x64 --arm64 --publish never
- name: List release directory
if: steps.check_certs.outputs.has_certs == 'true'
run: |
ls -R release/ || echo "Release directory not found"
- name: Upload macOS MAS PKG - 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') 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 uses: actions/upload-artifact@v4
@@ -463,42 +530,51 @@ jobs:
path: release/termix_macos_arm64_dmg.dmg path: release/termix_macos_arm64_dmg.dmg
retention-days: 30 retention-days: 30
- name: Check for App Store Connect API credentials - name: Get version for Homebrew
if: steps.check_certs.outputs.has_certs == 'true' id: homebrew-version
id: check_asc_creds
run: | run: |
if [ -n "${{ secrets.APPLE_KEY_ID }}" ] && [ -n "${{ secrets.APPLE_ISSUER_ID }}" ] && [ -n "${{ secrets.APPLE_KEY_CONTENT }}" ]; then VERSION=$(node -p "require('./package.json').version")
echo "has_credentials=true" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
fi
- name: Setup Ruby for Fastlane - name: Generate Homebrew Cask
if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' if: hashFiles('release/termix_macos_universal_dmg.dmg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release')
uses: ruby/setup-ruby@v1 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 homebrew/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: with:
ruby-version: "3.2" name: termix_macos_homebrew_cask
bundler-cache: false path: homebrew-generated/termix.rb
retention-days: 30
- name: Install Fastlane - name: Upload Homebrew Cask to release
if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' if: hashFiles('homebrew-generated/termix.rb') != '' && github.event.inputs.artifact_destination == 'release'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
gem install fastlane -N VERSION="${{ steps.homebrew-version.outputs.version }}"
RELEASE_TAG="release-$VERSION-tag"
- name: Deploy to App Store Connect (TestFlight) gh release list --repo ${{ github.repository }} --limit 100 | grep -q "$RELEASE_TAG" || {
if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' echo "Release $RELEASE_TAG not found"
run: |
PKG_FILE=$(find release -name "*.pkg" -type f | head -n 1)
if [ -z "$PKG_FILE" ]; then
exit 1 exit 1
fi }
mkdir -p ~/private_keys gh release upload "$RELEASE_TAG" homebrew-generated/termix.rb --repo ${{ github.repository }} --clobber
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 - name: Clean up keychains
if: always() if: always()
@@ -509,7 +585,6 @@ jobs:
submit-to-chocolatey: submit-to-chocolatey:
runs-on: windows-latest runs-on: windows-latest
if: github.event.inputs.artifact_destination == 'submit' if: github.event.inputs.artifact_destination == 'submit'
needs: [build-windows]
permissions: permissions:
contents: read contents: read
@@ -525,20 +600,25 @@ jobs:
$VERSION = (Get-Content package.json | ConvertFrom-Json).version $VERSION = (Get-Content package.json | ConvertFrom-Json).version
echo "version=$VERSION" >> $env:GITHUB_OUTPUT echo "version=$VERSION" >> $env:GITHUB_OUTPUT
- name: Download Windows x64 MSI artifact - name: Download and prepare MSI info from public release
uses: actions/download-artifact@v4
with:
name: termix_windows_x64_msi
path: artifact
- name: Get MSI file info
id: msi-info id: msi-info
run: | run: |
$VERSION = "${{ steps.package-version.outputs.version }}" $VERSION = "${{ steps.package-version.outputs.version }}"
$MSI_FILE = Get-ChildItem -Path artifact -Filter "*.msi" | Select-Object -First 1 $MSI_NAME = "termix_windows_x64_msi.msi"
$MSI_NAME = $MSI_FILE.Name $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$($VERSION)-tag/$($MSI_NAME)"
$CHECKSUM = (Get-FileHash -Path $MSI_FILE.FullName -Algorithm SHA256).Hash
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 "msi_name=$MSI_NAME" >> $env:GITHUB_OUTPUT
echo "checksum=$CHECKSUM" >> $env:GITHUB_OUTPUT echo "checksum=$CHECKSUM" >> $env:GITHUB_OUTPUT
@@ -610,7 +690,7 @@ jobs:
submit-to-flatpak: submit-to-flatpak:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.inputs.artifact_destination == 'submit' if: github.event.inputs.artifact_destination == 'submit'
needs: [build-linux] needs: []
permissions: permissions:
contents: read contents: read
@@ -628,30 +708,27 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT
- name: Download Linux x64 AppImage artifact - name: Download and prepare AppImage info from public release
uses: actions/download-artifact@v4
with:
name: termix_linux_x64_appimage
path: artifact-x64
- name: Download Linux arm64 AppImage artifact
uses: actions/download-artifact@v4
with:
name: termix_linux_arm64_appimage
path: artifact-arm64
- name: Get AppImage file info
id: appimage-info id: appimage-info
run: | run: |
VERSION="${{ steps.package-version.outputs.version }}" VERSION="${{ steps.package-version.outputs.version }}"
mkdir -p release_assets
APPIMAGE_X64_FILE=$(find artifact-x64 -name "*.AppImage" -type f | head -n 1) APPIMAGE_X64_NAME="termix_linux_x64_appimage.AppImage"
APPIMAGE_X64_NAME=$(basename "$APPIMAGE_X64_FILE") URL_X64="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_X64_NAME"
CHECKSUM_X64=$(sha256sum "$APPIMAGE_X64_FILE" | awk '{print $1}') 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_FILE=$(find artifact-arm64 -name "*.AppImage" -type f | head -n 1) APPIMAGE_ARM64_NAME="termix_linux_arm64_appimage.AppImage"
APPIMAGE_ARM64_NAME=$(basename "$APPIMAGE_ARM64_FILE") URL_ARM64="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_ARM64_NAME"
CHECKSUM_ARM64=$(sha256sum "$APPIMAGE_ARM64_FILE" | awk '{print $1}') 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 "appimage_x64_name=$APPIMAGE_X64_NAME" >> $GITHUB_OUTPUT
echo "checksum_x64=$CHECKSUM_X64" >> $GITHUB_OUTPUT echo "checksum_x64=$CHECKSUM_X64" >> $GITHUB_OUTPUT
@@ -690,10 +767,6 @@ jobs:
sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.metainfo.xml 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 sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak-submission/com.karmaa.termix.metainfo.xml
- name: List submission files
run: |
ls -la flatpak-submission/
- name: Upload Flatpak submission as artifact - name: Upload Flatpak submission as artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -704,7 +777,7 @@ jobs:
submit-to-homebrew: submit-to-homebrew:
runs-on: macos-latest runs-on: macos-latest
if: github.event.inputs.artifact_destination == 'submit' if: github.event.inputs.artifact_destination == 'submit'
needs: [build-macos] needs: []
permissions: permissions:
contents: read contents: read
@@ -720,19 +793,19 @@ jobs:
VERSION=$(node -p "require('./package.json').version") VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Download macOS Universal DMG artifact - name: Download and prepare DMG info from public release
uses: actions/download-artifact@v4
with:
name: termix_macos_universal_dmg
path: artifact
- name: Get DMG file info
id: dmg-info id: dmg-info
run: | run: |
VERSION="${{ steps.package-version.outputs.version }}" VERSION="${{ steps.package-version.outputs.version }}"
DMG_FILE=$(find artifact -name "*.dmg" -type f | head -n 1) DMG_NAME="termix_macos_universal_dmg.dmg"
DMG_NAME=$(basename "$DMG_FILE") URL="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$DMG_NAME"
CHECKSUM=$(shasum -a 256 "$DMG_FILE" | awk '{print $1}')
mkdir -p release_asset
PATH="release_asset/$DMG_NAME"
echo "Downloading DMG from $URL"
curl -L -o "$PATH" "$URL"
CHECKSUM=$(shasum -a 256 "$PATH" | awk '{print $1}')
echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT
echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT
@@ -752,16 +825,8 @@ jobs:
- name: Verify Cask syntax - name: Verify Cask syntax
run: | run: |
if ! command -v brew &> /dev/null; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
ruby -c homebrew-submission/Casks/t/termix.rb ruby -c homebrew-submission/Casks/t/termix.rb
- name: List submission files
run: |
find homebrew-submission -type f
- name: Upload Homebrew submission as artifact - name: Upload Homebrew submission as artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -789,10 +854,6 @@ jobs:
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
- name: Display artifact structure
run: |
ls -R artifacts/
- name: Upload artifacts to latest release - name: Upload artifacts to latest release
run: | run: |
cd artifacts cd artifacts
@@ -808,3 +869,130 @@ jobs:
done done
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
submit-to-testflight:
runs-on: macos-latest
if: 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: "20"
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 }}
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 artifact-mas -name "*.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

View File

@@ -1,8 +1,8 @@
cask "termix" do cask "termix" do
version "VERSION_PLACEHOLDER" version "1.9.0"
sha256 "CHECKSUM_PLACEHOLDER" sha256 "8fedd242b3cae1ebfd0c391a36f1c246a26ecac258b02478ee8dea2f33cd6d96"
url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_#{version}_dmg.dmg" url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg"
name "Termix" name "Termix"
desc "Web-based server management platform with SSH terminal, tunneling, and file editing" desc "Web-based server management platform with SSH terminal, tunneling, and file editing"
homepage "https://github.com/Termix-SSH/Termix" homepage "https://github.com/Termix-SSH/Termix"

View File

@@ -80,16 +80,16 @@ Supported Devices:
- Windows (x64/ia32) - Windows (x64/ia32)
- Portable - Portable
- MSI Installer - MSI Installer
- Chocolatey Package Manager (coming soon) - Chocolatey Package Manager
- Linux (x64/ia32) - Linux (x64/ia32)
- Portable - Portable
- AppImage - AppImage
- Deb - Deb
- Flatpak (coming soon) - Flatpak
- macOS (x64/ia32 on v12.0+) - macOS (x64/ia32 on v12.0+)
- Apple App Store (coming soon) - Apple App Store
- DMG - DMG
- Homebrew (coming soon) - Homebrew
- iOS/iPadOS (v15.1+) - iOS/iPadOS (v15.1+)
- Apple App Store - Apple App Store
- ISO - ISO

49
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,49 @@
services:
termix:
build:
context: ..
dockerfile: docker/Dockerfile
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix_data:/app/db/data
environment:
- NODE_ENV=production
- PORT=8080
- GUACD_HOST=guacd
- GUACD_PORT=4822
- ENABLE_GUACAMOLE=true
depends_on:
- guacd
networks:
- termix-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
guacd:
image: guacamole/guacd:latest
container_name: termix-guacd
restart: unless-stopped
networks:
- termix-network
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "4822"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
termix-network:
driver: bridge
volumes:
termix_data:
driver: local

View File

@@ -203,6 +203,41 @@ http {
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
} }
# Guacamole WebSocket for RDP/VNC/Telnet
# ^~ modifier ensures this takes precedence over the regex location below
location ^~ /guacamole/websocket/ {
proxy_pass http://127.0.0.1:30007/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $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_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;
}
# Guacamole REST API
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 /ssh/tunnel/ { location /ssh/tunnel/ {
proxy_pass http://127.0.0.1:30003; proxy_pass http://127.0.0.1:30003;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@@ -200,6 +200,41 @@ http {
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
} }
# Guacamole WebSocket for RDP/VNC/Telnet
# ^~ modifier ensures this takes precedence over the regex location below
location ^~ /guacamole/websocket/ {
proxy_pass http://127.0.0.1:30007/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $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_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;
}
# Guacamole REST API
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 /ssh/tunnel/ { location /ssh/tunnel/ {
proxy_pass http://127.0.0.1:30003; proxy_pass http://127.0.0.1:30003;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@@ -124,5 +124,6 @@
"ITSAppUsesNonExemptEncryption": false, "ITSAppUsesNonExemptEncryption": false,
"NSAppleEventsUsageDescription": "Termix needs access to control other applications for terminal operations." "NSAppleEventsUsageDescription": "Termix needs access to control other applications for terminal operations."
} }
} },
"generateUpdatesFilesForAllChannels": true
} }

View File

@@ -11,13 +11,11 @@ const fs = require("fs");
const os = require("os"); const os = require("os");
if (process.platform === "linux") { if (process.platform === "linux") {
app.commandLine.appendSwitch("--no-sandbox"); // Enable Ozone platform auto-detection for Wayland/X11 support
app.commandLine.appendSwitch("--disable-setuid-sandbox"); app.commandLine.appendSwitch("--ozone-platform-hint=auto");
app.commandLine.appendSwitch("--disable-dev-shm-usage");
app.disableHardwareAcceleration(); // Enable hardware video decoding if available
app.commandLine.appendSwitch("--disable-gpu"); app.commandLine.appendSwitch("--enable-features=VaapiVideoDecoder");
app.commandLine.appendSwitch("--disable-gpu-compositing");
} }
app.commandLine.appendSwitch("--ignore-certificate-errors"); app.commandLine.appendSwitch("--ignore-certificate-errors");

View File

@@ -1,7 +1,7 @@
[Desktop Entry] [Desktop Entry]
Name=Termix Name=Termix
Comment=Web-based server management platform with SSH terminal, tunneling, and file editing Comment=Web-based server management platform with SSH terminal, tunneling, and file editing
Exec=termix %U Exec=run.sh %U
Icon=com.karmaa.termix Icon=com.karmaa.termix
Terminal=false Terminal=false
Type=Application Type=Application

View File

@@ -0,0 +1,12 @@
[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
GPGKey=
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

View File

@@ -5,7 +5,7 @@
<summary>Web-based server management platform with SSH terminal, tunneling, and file editing</summary> <summary>Web-based server management platform with SSH terminal, tunneling, and file editing</summary>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license> <project_license>Apache-2.0</project_license>
<developer_name>bugattiguy527</developer_name> <developer_name>bugattiguy527</developer_name>

View File

@@ -1,10 +1,10 @@
app-id: com.karmaa.termix app-id: com.karmaa.termix
runtime: org.freedesktop.Platform runtime: org.freedesktop.Platform
runtime-version: "23.08" runtime-version: "24.08"
sdk: org.freedesktop.Sdk sdk: org.freedesktop.Sdk
base: org.electronjs.Electron2.BaseApp base: org.electronjs.Electron2.BaseApp
base-version: "23.08" base-version: "24.08"
command: termix command: run.sh
separate-locales: false separate-locales: false
finish-args: finish-args:
@@ -16,8 +16,11 @@ finish-args:
- --device=dri - --device=dri
- --filesystem=home - --filesystem=home
- --socket=ssh-auth - --socket=ssh-auth
- --talk-name=org.freedesktop.Notifications - --socket=session-bus
- --talk-name=org.freedesktop.secrets - --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: modules:
- name: termix - name: termix
@@ -30,6 +33,21 @@ modules:
- cp -r squashfs-root/resources /app/bin/ - cp -r squashfs-root/resources /app/bin/
- cp -r squashfs-root/locales /app/bin/ || true - 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.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.metainfo.xml /app/share/metainfo/com.karmaa.termix.metainfo.xml
@@ -40,14 +58,14 @@ modules:
sources: sources:
- type: file - type: file
url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_x64_VERSION_PLACEHOLDER_appimage.AppImage url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_x64_appimage.AppImage
sha256: CHECKSUM_X64_PLACEHOLDER sha256: CHECKSUM_X64_PLACEHOLDER
dest-filename: termix.AppImage dest-filename: termix.AppImage
only-arches: only-arches:
- x86_64 - x86_64
- type: file - type: file
url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_arm64_VERSION_PLACEHOLDER_appimage.AppImage url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_arm64_appimage.AppImage
sha256: CHECKSUM_ARM64_PLACEHOLDER sha256: CHECKSUM_ARM64_PLACEHOLDER
dest-filename: termix.AppImage dest-filename: termix.AppImage
only-arches: only-arches:

View File

@@ -1,34 +0,0 @@
#!/bin/bash
set -e
VERSION="$1"
CHECKSUM="$2"
RELEASE_DATE="$3"
if [ -z "$VERSION" ] || [ -z "$CHECKSUM" ] || [ -z "$RELEASE_DATE" ]; then
echo "Usage: $0 <version> <checksum> <release-date>"
echo "Example: $0 1.8.0 abc123... 2025-10-26"
exit 1
fi
echo "Preparing Flatpak submission for version $VERSION"
cp public/icon.svg flatpak/com.karmaa.termix.svg
echo "✓ Copied SVG icon"
if command -v convert &> /dev/null; then
convert public/icon.png -resize 256x256 flatpak/icon-256.png
convert public/icon.png -resize 128x128 flatpak/icon-128.png
echo "✓ Generated PNG icons"
else
cp public/icon.png flatpak/icon-256.png
cp public/icon.png flatpak/icon-128.png
echo "⚠ ImageMagick not found, using original icon"
fi
sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak/com.karmaa.termix.yml
sed -i "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" flatpak/com.karmaa.termix.yml
echo "✓ Updated manifest with version $VERSION"
sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak/com.karmaa.termix.metainfo.xml
sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak/com.karmaa.termix.metainfo.xml

29
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "termix", "name": "termix",
"version": "1.8.1", "version": "1.9.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "termix", "name": "termix",
"version": "1.8.1", "version": "1.9.0",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.18.7", "@codemirror/autocomplete": "^6.18.7",
"@codemirror/commands": "^6.3.3", "@codemirror/commands": "^6.3.3",
@@ -33,6 +33,7 @@
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.9", "@types/cookie-parser": "^1.4.9",
"@types/guacamole-common-js": "^1.5.5",
"@types/jszip": "^3.4.0", "@types/jszip": "^3.4.0",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
@@ -57,6 +58,8 @@
"dotenv": "^17.2.0", "dotenv": "^17.2.0",
"drizzle-orm": "^0.44.3", "drizzle-orm": "^0.44.3",
"express": "^5.1.0", "express": "^5.1.0",
"guacamole-common-js": "^1.5.0",
"guacamole-lite": "^1.2.0",
"i18next": "^25.4.2", "i18next": "^25.4.2",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"jose": "^5.2.3", "jose": "^5.2.3",
@@ -5030,6 +5033,11 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/guacamole-common-js": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@types/guacamole-common-js/-/guacamole-common-js-1.5.5.tgz",
"integrity": "sha512-dqDYo/PhbOXFGSph23rFDRZRzXdKPXy/nsTkovFMb6P3iGrd0qGB5r5BXHmX5Cr/LK7L1TK9nYrTMbtPkhdXyg=="
},
"node_modules/@types/hast": { "node_modules/@types/hast": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
@@ -9812,6 +9820,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/guacamole-common-js": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/guacamole-common-js/-/guacamole-common-js-1.5.0.tgz",
"integrity": "sha512-zxztif3GGhKbg1RgOqwmqot8kXgv2HmHFg1EvWwd4q7UfEKvBcYZ0f+7G8HzvU+FUxF0Psqm9Kl5vCbgfrRgJg=="
},
"node_modules/guacamole-lite": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/guacamole-lite/-/guacamole-lite-1.2.0.tgz",
"integrity": "sha512-NeSYgbT5s5rxF0SE/kzJsV5Gg0IvnqoTOCbNIUMl23z1+SshaVfLExpxrEXSGTG0cdvY5lfZC1fOAepYriaXGg==",
"dependencies": {
"deep-extend": "^0.6.0",
"ws": "^8.15.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",

View File

@@ -52,6 +52,7 @@
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.9", "@types/cookie-parser": "^1.4.9",
"@types/guacamole-common-js": "^1.5.5",
"@types/jszip": "^3.4.0", "@types/jszip": "^3.4.0",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
@@ -76,6 +77,8 @@
"dotenv": "^17.2.0", "dotenv": "^17.2.0",
"drizzle-orm": "^0.44.3", "drizzle-orm": "^0.44.3",
"express": "^5.1.0", "express": "^5.1.0",
"guacamole-common-js": "^1.5.0",
"guacamole-lite": "^1.2.0",
"i18next": "^25.4.2", "i18next": "^25.4.2",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"jose": "^5.2.3", "jose": "^5.2.3",

View File

@@ -8,6 +8,7 @@ import alertRoutes from "./routes/alerts.js";
import credentialsRoutes from "./routes/credentials.js"; import credentialsRoutes from "./routes/credentials.js";
import snippetsRoutes from "./routes/snippets.js"; import snippetsRoutes from "./routes/snippets.js";
import terminalRoutes from "./routes/terminal.js"; import terminalRoutes from "./routes/terminal.js";
import guacamoleRoutes from "../guacamole/routes.js";
import cors from "cors"; import cors from "cors";
import fetch from "node-fetch"; import fetch from "node-fetch";
import fs from "fs"; import fs from "fs";
@@ -1436,6 +1437,7 @@ app.use("/alerts", alertRoutes);
app.use("/credentials", credentialsRoutes); app.use("/credentials", credentialsRoutes);
app.use("/snippets", snippetsRoutes); app.use("/snippets", snippetsRoutes);
app.use("/terminal", terminalRoutes); app.use("/terminal", terminalRoutes);
app.use("/guacamole", guacamoleRoutes);
app.use( app.use(
( (

View File

@@ -201,12 +201,14 @@ async function initializeCompleteDatabase(): Promise<void> {
enable_tunnel INTEGER NOT NULL DEFAULT 1, enable_tunnel INTEGER NOT NULL DEFAULT 1,
tunnel_connections TEXT, tunnel_connections TEXT,
enable_file_manager INTEGER NOT NULL DEFAULT 1, enable_file_manager INTEGER NOT NULL DEFAULT 1,
enable_docker INTEGER NOT NULL DEFAULT 0,
default_path TEXT, default_path TEXT,
autostart_password TEXT, autostart_password TEXT,
autostart_key TEXT, autostart_key TEXT,
autostart_key_password TEXT, autostart_key_password TEXT,
force_keyboard_interactive TEXT, force_keyboard_interactive TEXT,
stats_config TEXT, stats_config TEXT,
docker_config TEXT,
terminal_config TEXT, terminal_config TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -486,6 +488,19 @@ const migrateSchema = () => {
addColumnIfNotExists("ssh_data", "stats_config", "TEXT"); addColumnIfNotExists("ssh_data", "stats_config", "TEXT");
addColumnIfNotExists("ssh_data", "terminal_config", "TEXT"); addColumnIfNotExists("ssh_data", "terminal_config", "TEXT");
addColumnIfNotExists("ssh_data", "quick_actions", "TEXT"); addColumnIfNotExists("ssh_data", "quick_actions", "TEXT");
addColumnIfNotExists(
"ssh_data",
"enable_docker",
"INTEGER NOT NULL DEFAULT 0",
);
addColumnIfNotExists("ssh_data", "docker_config", "TEXT");
// Connection type columns for RDP/VNC/Telnet support
addColumnIfNotExists("ssh_data", "connection_type", 'TEXT NOT NULL DEFAULT "ssh"');
addColumnIfNotExists("ssh_data", "domain", "TEXT");
addColumnIfNotExists("ssh_data", "security", "TEXT");
addColumnIfNotExists("ssh_data", "ignore_cert", "INTEGER NOT NULL DEFAULT 0");
addColumnIfNotExists("ssh_data", "guacamole_config", "TEXT");
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT"); addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT"); addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");

View File

@@ -52,6 +52,8 @@ export const sshData = sqliteTable("ssh_data", {
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => users.id, { onDelete: "cascade" }), .references(() => users.id, { onDelete: "cascade" }),
// Connection type: ssh, rdp, vnc, telnet
connectionType: text("connection_type").notNull().default("ssh"),
name: text("name"), name: text("name"),
ip: text("ip").notNull(), ip: text("ip").notNull(),
port: integer("port").notNull(), port: integer("port").notNull(),
@@ -86,10 +88,20 @@ export const sshData = sqliteTable("ssh_data", {
enableFileManager: integer("enable_file_manager", { mode: "boolean" }) enableFileManager: integer("enable_file_manager", { mode: "boolean" })
.notNull() .notNull()
.default(true), .default(true),
enableDocker: integer("enable_docker", { mode: "boolean" })
.notNull()
.default(false),
defaultPath: text("default_path"), defaultPath: text("default_path"),
statsConfig: text("stats_config"), statsConfig: text("stats_config"),
dockerConfig: text("docker_config"),
terminalConfig: text("terminal_config"), terminalConfig: text("terminal_config"),
quickActions: text("quick_actions"), quickActions: text("quick_actions"),
// RDP/VNC specific fields
domain: text("domain"),
security: text("security"),
ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false),
// RDP/VNC extended configuration (stored as JSON)
guacamoleConfig: text("guacamole_config"),
createdAt: text("created_at") createdAt: text("created_at")
.notNull() .notNull()
.default(sql`CURRENT_TIMESTAMP`), .default(sql`CURRENT_TIMESTAMP`),

View File

@@ -1,4 +1,7 @@
import type { AuthenticatedRequest } from "../../../types/index.js"; import type {
AuthenticatedRequest,
CredentialBackend,
} from "../../../types/index.js";
import express from "express"; import express from "express";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { sshCredentials, sshCredentialUsage, sshData } from "../db/schema.js"; import { sshCredentials, sshCredentialUsage, sshData } from "../db/schema.js";
@@ -1124,10 +1127,9 @@ router.post(
async function deploySSHKeyToHost( async function deploySSHKeyToHost(
hostConfig: Record<string, unknown>, hostConfig: Record<string, unknown>,
publicKey: string, credData: CredentialBackend,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_credentialData: Record<string, unknown>,
): Promise<{ success: boolean; message?: string; error?: string }> { ): Promise<{ success: boolean; message?: string; error?: string }> {
const publicKey = credData.public_key as string;
return new Promise((resolve) => { return new Promise((resolve) => {
const conn = new Client(); const conn = new Client();
@@ -1248,7 +1250,7 @@ async function deploySSHKeyToHost(
.replace(/'/g, "'\\''"); .replace(/'/g, "'\\''");
conn.exec( conn.exec(
`printf '%s\\n' '${escapedKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`, `printf '%s\\n' '${escapedKey} ${credData.name}@Termix' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
(err, stream) => { (err, stream) => {
if (err) { if (err) {
clearTimeout(addTimeout); clearTimeout(addTimeout);
@@ -1510,7 +1512,7 @@ router.post(
}); });
} }
const credData = credential[0]; const credData = credential[0] as unknown as CredentialBackend;
if (credData.authType !== "key") { if (credData.authType !== "key") {
return res.status(400).json({ return res.status(400).json({
@@ -1519,7 +1521,7 @@ router.post(
}); });
} }
const publicKey = credData.public_key || credData.publicKey; const publicKey = credData.public_key;
if (!publicKey) { if (!publicKey) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
@@ -1601,7 +1603,6 @@ router.post(
const deployResult = await deploySSHKeyToHost( const deployResult = await deploySSHKeyToHost(
hostConfig, hostConfig,
publicKey as string,
credData, credData,
); );

View File

@@ -218,6 +218,7 @@ router.post(
} }
const { const {
connectionType,
name, name,
folder, folder,
tags, tags,
@@ -235,13 +236,20 @@ router.post(
enableTerminal, enableTerminal,
enableTunnel, enableTunnel,
enableFileManager, enableFileManager,
enableDocker,
defaultPath, defaultPath,
tunnelConnections, tunnelConnections,
jumpHosts, jumpHosts,
quickActions, quickActions,
statsConfig, statsConfig,
dockerConfig,
terminalConfig, terminalConfig,
forceKeyboardInteractive, forceKeyboardInteractive,
// RDP/VNC specific fields
domain,
security,
ignoreCert,
guacamoleConfig,
} = hostData; } = hostData;
if ( if (
!isNonEmptyString(userId) || !isNonEmptyString(userId) ||
@@ -259,8 +267,10 @@ router.post(
} }
const effectiveAuthType = authType || authMethod; const effectiveAuthType = authType || authMethod;
const effectiveConnectionType = connectionType || "ssh";
const sshDataObj: Record<string, unknown> = { const sshDataObj: Record<string, unknown> = {
userId: userId, userId: userId,
connectionType: effectiveConnectionType,
name, name,
folder: folder || null, folder: folder || null,
tags: Array.isArray(tags) ? tags.join(",") : tags || "", tags: Array.isArray(tags) ? tags.join(",") : tags || "",
@@ -280,10 +290,17 @@ router.post(
? JSON.stringify(quickActions) ? JSON.stringify(quickActions)
: null, : null,
enableFileManager: enableFileManager ? 1 : 0, enableFileManager: enableFileManager ? 1 : 0,
enableDocker: enableDocker ? 1 : 0,
defaultPath: defaultPath || null, defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null, statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null, terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false", forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
// RDP/VNC specific fields
domain: domain || null,
security: security || null,
ignoreCert: ignoreCert ? 1 : 0,
guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
}; };
if (effectiveAuthType === "password") { if (effectiveAuthType === "password") {
@@ -341,9 +358,16 @@ router.post(
? JSON.parse(createdHost.jumpHosts as string) ? JSON.parse(createdHost.jumpHosts as string)
: [], : [],
enableFileManager: !!createdHost.enableFileManager, enableFileManager: !!createdHost.enableFileManager,
enableDocker: !!createdHost.enableDocker,
statsConfig: createdHost.statsConfig statsConfig: createdHost.statsConfig
? JSON.parse(createdHost.statsConfig as string) ? JSON.parse(createdHost.statsConfig as string)
: undefined, : undefined,
dockerConfig: createdHost.dockerConfig
? JSON.parse(createdHost.dockerConfig as string)
: undefined,
guacamoleConfig: createdHost.guacamoleConfig
? JSON.parse(createdHost.guacamoleConfig as string)
: undefined,
}; };
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost; const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
@@ -440,6 +464,7 @@ router.put(
} }
const { const {
connectionType,
name, name,
folder, folder,
tags, tags,
@@ -457,13 +482,20 @@ router.put(
enableTerminal, enableTerminal,
enableTunnel, enableTunnel,
enableFileManager, enableFileManager,
enableDocker,
defaultPath, defaultPath,
tunnelConnections, tunnelConnections,
jumpHosts, jumpHosts,
quickActions, quickActions,
statsConfig, statsConfig,
dockerConfig,
terminalConfig, terminalConfig,
forceKeyboardInteractive, forceKeyboardInteractive,
// RDP/VNC specific fields
domain,
security,
ignoreCert,
guacamoleConfig,
} = hostData; } = hostData;
if ( if (
!isNonEmptyString(userId) || !isNonEmptyString(userId) ||
@@ -484,6 +516,7 @@ router.put(
const effectiveAuthType = authType || authMethod; const effectiveAuthType = authType || authMethod;
const sshDataObj: Record<string, unknown> = { const sshDataObj: Record<string, unknown> = {
connectionType: connectionType || "ssh",
name, name,
folder, folder,
tags: Array.isArray(tags) ? tags.join(",") : tags || "", tags: Array.isArray(tags) ? tags.join(",") : tags || "",
@@ -503,10 +536,17 @@ router.put(
? JSON.stringify(quickActions) ? JSON.stringify(quickActions)
: null, : null,
enableFileManager: enableFileManager ? 1 : 0, enableFileManager: enableFileManager ? 1 : 0,
enableDocker: enableDocker ? 1 : 0,
defaultPath: defaultPath || null, defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null, statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null, terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false", forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
// RDP/VNC specific fields
domain: domain || null,
security: security || null,
ignoreCert: ignoreCert ? 1 : 0,
guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
}; };
if (effectiveAuthType === "password") { if (effectiveAuthType === "password") {
@@ -582,9 +622,16 @@ router.put(
? JSON.parse(updatedHost.jumpHosts as string) ? JSON.parse(updatedHost.jumpHosts as string)
: [], : [],
enableFileManager: !!updatedHost.enableFileManager, enableFileManager: !!updatedHost.enableFileManager,
enableDocker: !!updatedHost.enableDocker,
statsConfig: updatedHost.statsConfig statsConfig: updatedHost.statsConfig
? JSON.parse(updatedHost.statsConfig as string) ? JSON.parse(updatedHost.statsConfig as string)
: undefined, : undefined,
dockerConfig: updatedHost.dockerConfig
? JSON.parse(updatedHost.dockerConfig as string)
: undefined,
guacamoleConfig: updatedHost.guacamoleConfig
? JSON.parse(updatedHost.guacamoleConfig as string)
: undefined,
}; };
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost; const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
@@ -683,12 +730,19 @@ router.get(
? JSON.parse(row.quickActions as string) ? JSON.parse(row.quickActions as string)
: [], : [],
enableFileManager: !!row.enableFileManager, enableFileManager: !!row.enableFileManager,
enableDocker: !!row.enableDocker,
statsConfig: row.statsConfig statsConfig: row.statsConfig
? JSON.parse(row.statsConfig as string) ? JSON.parse(row.statsConfig as string)
: undefined, : undefined,
dockerConfig: row.dockerConfig
? JSON.parse(row.dockerConfig as string)
: undefined,
terminalConfig: row.terminalConfig terminalConfig: row.terminalConfig
? JSON.parse(row.terminalConfig as string) ? JSON.parse(row.terminalConfig as string)
: undefined, : undefined,
guacamoleConfig: row.guacamoleConfig
? JSON.parse(row.guacamoleConfig as string)
: undefined,
forceKeyboardInteractive: row.forceKeyboardInteractive === "true", forceKeyboardInteractive: row.forceKeyboardInteractive === "true",
}; };
@@ -764,6 +818,9 @@ router.get(
terminalConfig: host.terminalConfig terminalConfig: host.terminalConfig
? JSON.parse(host.terminalConfig) ? JSON.parse(host.terminalConfig)
: undefined, : undefined,
guacamoleConfig: host.guacamoleConfig
? JSON.parse(host.guacamoleConfig)
: undefined,
forceKeyboardInteractive: host.forceKeyboardInteractive === "true", forceKeyboardInteractive: host.forceKeyboardInteractive === "true",
}; };

View File

@@ -0,0 +1,108 @@
import GuacamoleLite from "guacamole-lite";
import { parse as parseUrl } from "url";
import { guacLogger } from "../utils/logger.js";
import { AuthManager } from "../utils/auth-manager.js";
import { GuacamoleTokenService } from "./token-service.js";
import type { IncomingMessage } from "http";
const authManager = AuthManager.getInstance();
const tokenService = GuacamoleTokenService.getInstance();
// Configuration from environment
const GUACD_HOST = process.env.GUACD_HOST || "localhost";
const GUACD_PORT = parseInt(process.env.GUACD_PORT || "4822", 10);
const GUAC_WS_PORT = 30007;
const websocketOptions = {
port: GUAC_WS_PORT,
};
const guacdOptions = {
host: GUACD_HOST,
port: GUACD_PORT,
};
const clientOptions = {
crypt: {
cypher: "AES-256-CBC",
key: tokenService.getEncryptionKey(),
},
log: {
level: process.env.NODE_ENV === "production" ? "ERRORS" : "VERBOSE",
stdLog: (...args: unknown[]) => {
guacLogger.info(args.join(" "), { operation: "guac_log" });
},
errorLog: (...args: unknown[]) => {
guacLogger.error(args.join(" "), { operation: "guac_error" });
},
},
// Allow width, height, and dpi to be passed as query parameters
// This allows the client to request the appropriate resolution at connection time
allowedUnencryptedConnectionSettings: {
rdp: ["width", "height", "dpi"],
vnc: ["width", "height", "dpi"],
telnet: ["width", "height"],
},
connectionDefaultSettings: {
rdp: {
security: "any",
"ignore-cert": true,
"enable-wallpaper": false,
"enable-font-smoothing": true,
"enable-desktop-composition": false,
"disable-audio": false,
"enable-drive": false,
"resize-method": "display-update",
width: 1280,
height: 720,
dpi: 96,
},
vnc: {
"swap-red-blue": false,
"cursor": "remote",
width: 1280,
height: 720,
},
telnet: {
"terminal-type": "xterm-256color",
},
},
};
// Create the guacamole-lite server
const guacServer = new GuacamoleLite(
websocketOptions,
guacdOptions,
clientOptions
);
// Add authentication via processConnectionSettings callback
guacServer.on("open", (clientConnection: { connectionSettings?: Record<string, unknown> }) => {
guacLogger.info("Guacamole connection opened", {
operation: "guac_connection_open",
type: clientConnection.connectionSettings?.type,
});
});
guacServer.on("close", (clientConnection: { connectionSettings?: Record<string, unknown> }) => {
guacLogger.info("Guacamole connection closed", {
operation: "guac_connection_close",
type: clientConnection.connectionSettings?.type,
});
});
guacServer.on("error", (clientConnection: { connectionSettings?: Record<string, unknown> }, error: Error) => {
guacLogger.error("Guacamole connection error", error, {
operation: "guac_connection_error",
type: clientConnection.connectionSettings?.type,
});
});
guacLogger.info(`Guacamole WebSocket server started on port ${GUAC_WS_PORT}`, {
operation: "guac_server_start",
guacdHost: GUACD_HOST,
guacdPort: GUACD_PORT,
});
export { guacServer, tokenService };

View File

@@ -0,0 +1,159 @@
import express from "express";
import { GuacamoleTokenService } from "./token-service.js";
import { guacLogger } from "../utils/logger.js";
import { AuthManager } from "../utils/auth-manager.js";
import type { AuthenticatedRequest } from "../../types/index.js";
const router = express.Router();
const tokenService = GuacamoleTokenService.getInstance();
const authManager = AuthManager.getInstance();
// Apply authentication middleware
router.use(authManager.createAuthMiddleware());
/**
* POST /guacamole/token
* Generate an encrypted connection token for guacamole-lite
*
* Body: {
* type: "rdp" | "vnc" | "telnet",
* hostname: string,
* port?: number,
* username?: string,
* password?: string,
* domain?: string,
* // Additional protocol-specific options
* }
*/
router.post("/token", async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
const { type, hostname, port, username, password, domain, ...options } = req.body;
if (!type || !hostname) {
return res.status(400).json({ error: "Missing required fields: type and hostname" });
}
if (!["rdp", "vnc", "telnet"].includes(type)) {
return res.status(400).json({ error: "Invalid connection type. Must be rdp, vnc, or telnet" });
}
// Log received options for debugging
guacLogger.info("Guacamole token request received", {
operation: "guac_token_request",
type,
hostname,
port,
optionKeys: Object.keys(options),
optionsCount: Object.keys(options).length,
});
// Log specific option values for debugging
if (Object.keys(options).length > 0) {
guacLogger.info("Guacamole options received", {
operation: "guac_token_options",
options: JSON.stringify(options),
});
}
let token: string;
switch (type) {
case "rdp":
token = tokenService.createRdpToken(hostname, username || "", password || "", {
port: port || 3389,
domain,
...options,
});
break;
case "vnc":
token = tokenService.createVncToken(hostname, password, {
port: port || 5900,
...options,
});
break;
case "telnet":
token = tokenService.createTelnetToken(hostname, username, password, {
port: port || 23,
...options,
});
break;
default:
return res.status(400).json({ error: "Invalid connection type" });
}
guacLogger.info("Generated guacamole connection token", {
operation: "guac_token_generated",
userId,
type,
hostname,
});
res.json({ token });
} catch (error) {
guacLogger.error("Failed to generate guacamole token", error, {
operation: "guac_token_error",
});
res.status(500).json({ error: "Failed to generate connection token" });
}
});
/**
* GET /guacamole/status
* Check if guacd is reachable
*/
router.get("/status", async (req, res) => {
try {
const guacdHost = process.env.GUACD_HOST || "localhost";
const guacdPort = parseInt(process.env.GUACD_PORT || "4822", 10);
// Simple TCP check to see if guacd is responding
const net = await import("net");
const checkConnection = (): Promise<boolean> => {
return new Promise((resolve) => {
const socket = new net.Socket();
socket.setTimeout(3000);
socket.on("connect", () => {
socket.destroy();
resolve(true);
});
socket.on("timeout", () => {
socket.destroy();
resolve(false);
});
socket.on("error", () => {
socket.destroy();
resolve(false);
});
socket.connect(guacdPort, guacdHost);
});
};
const isConnected = await checkConnection();
res.json({
guacd: {
host: guacdHost,
port: guacdPort,
status: isConnected ? "connected" : "disconnected",
},
websocket: {
port: 30007,
status: "running",
},
});
} catch (error) {
guacLogger.error("Failed to check guacamole status", error, {
operation: "guac_status_error",
});
res.status(500).json({ error: "Failed to check status" });
}
});
export default router;

View File

@@ -0,0 +1,198 @@
import crypto from "crypto";
import { guacLogger } from "../utils/logger.js";
export interface GuacamoleConnectionSettings {
type: "rdp" | "vnc" | "telnet";
settings: {
hostname: string;
port?: number;
username?: string;
password?: string;
domain?: string;
width?: number;
height?: number;
dpi?: number;
// RDP specific
security?: string;
"ignore-cert"?: boolean;
"enable-wallpaper"?: boolean;
"enable-drive"?: boolean;
"drive-path"?: string;
"create-drive-path"?: boolean;
// VNC specific
"swap-red-blue"?: boolean;
cursor?: string;
// Telnet specific
"terminal-type"?: string;
[key: string]: unknown;
};
}
export interface GuacamoleToken {
connection: GuacamoleConnectionSettings;
}
const CIPHER = "aes-256-cbc";
const KEY_LENGTH = 32; // 256 bits = 32 bytes
export class GuacamoleTokenService {
private static instance: GuacamoleTokenService;
private encryptionKey: Buffer;
private constructor() {
// Use existing JWT secret or generate a dedicated key
this.encryptionKey = this.initializeKey();
}
static getInstance(): GuacamoleTokenService {
if (!GuacamoleTokenService.instance) {
GuacamoleTokenService.instance = new GuacamoleTokenService();
}
return GuacamoleTokenService.instance;
}
private initializeKey(): Buffer {
// Check for dedicated guacamole key first (must be 32 bytes / 64 hex chars)
const existingKey = process.env.GUACAMOLE_ENCRYPTION_KEY;
if (existingKey) {
// If it's hex encoded (64 chars = 32 bytes)
if (existingKey.length === 64 && /^[0-9a-fA-F]+$/.test(existingKey)) {
return Buffer.from(existingKey, "hex");
}
// If it's already 32 bytes
if (existingKey.length === KEY_LENGTH) {
return Buffer.from(existingKey, "utf8");
}
}
// Generate a deterministic key from JWT_SECRET if available
const jwtSecret = process.env.JWT_SECRET;
if (jwtSecret) {
// SHA-256 produces exactly 32 bytes - perfect for AES-256
return crypto.createHash("sha256").update(jwtSecret + "_guacamole").digest();
}
// Last resort: generate random key (note: won't persist across restarts)
guacLogger.warn("No persistent encryption key found, generating random key", {
operation: "guac_key_generation",
});
return crypto.randomBytes(KEY_LENGTH);
}
getEncryptionKey(): Buffer {
return this.encryptionKey;
}
/**
* Encrypt connection settings into a token for guacamole-lite
*/
encryptToken(tokenObject: GuacamoleToken): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(CIPHER, this.encryptionKey, iv);
let encrypted = cipher.update(JSON.stringify(tokenObject), "utf8", "base64");
encrypted += cipher.final("base64");
const data = {
iv: iv.toString("base64"),
value: encrypted,
};
return Buffer.from(JSON.stringify(data)).toString("base64");
}
/**
* Decrypt a token (for verification/debugging purposes)
*/
decryptToken(token: string): GuacamoleToken | null {
try {
const data = JSON.parse(Buffer.from(token, "base64").toString("utf8"));
const iv = Buffer.from(data.iv, "base64");
const decipher = crypto.createDecipheriv(CIPHER, this.encryptionKey, iv);
let decrypted = decipher.update(data.value, "base64", "utf8");
decrypted += decipher.final("utf8");
return JSON.parse(decrypted) as GuacamoleToken;
} catch (error) {
guacLogger.error("Failed to decrypt guacamole token", error, {
operation: "guac_token_decrypt_error",
});
return null;
}
}
/**
* Create a connection token for RDP
* security options: "any", "nla", "nla-ext", "tls", "rdp", "vmconnect"
*/
createRdpToken(
hostname: string,
username: string,
password: string,
options: Partial<GuacamoleConnectionSettings["settings"]> = {}
): string {
const token: GuacamoleToken = {
connection: {
type: "rdp",
settings: {
hostname,
username,
password,
port: 3389,
security: "nla", // NLA is required for modern Windows (10/11, Server 2016+)
"ignore-cert": true,
...options,
},
},
};
return this.encryptToken(token);
}
/**
* Create a connection token for VNC
*/
createVncToken(
hostname: string,
password?: string,
options: Partial<GuacamoleConnectionSettings["settings"]> = {}
): string {
const token: GuacamoleToken = {
connection: {
type: "vnc",
settings: {
hostname,
password,
port: 5900,
...options,
},
},
};
return this.encryptToken(token);
}
/**
* Create a connection token for Telnet
*/
createTelnetToken(
hostname: string,
username?: string,
password?: string,
options: Partial<GuacamoleConnectionSettings["settings"]> = {}
): string {
const token: GuacamoleToken = {
connection: {
type: "telnet",
settings: {
hostname,
username,
password,
port: 23,
...options,
},
},
};
return this.encryptToken(token);
}
}

View File

@@ -316,7 +316,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
let sshConn: Client | null = null; let sshConn: Client | null = null;
let sshStream: ClientChannel | null = null; let sshStream: ClientChannel | null = null;
let pingInterval: NodeJS.Timeout | null = null;
let keyboardInteractiveFinish: ((responses: string[]) => void) | null = null; let keyboardInteractiveFinish: ((responses: string[]) => void) | null = null;
let totpPromptSent = false; let totpPromptSent = false;
let isKeyboardInteractive = false; let isKeyboardInteractive = false;
@@ -802,8 +801,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
); );
}); });
setupPingInterval();
if (initialPath && initialPath.trim() !== "") { if (initialPath && initialPath.trim() !== "") {
const cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}" && pwd\n`; const cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}" && pwd\n`;
stream.write(cdCommand); stream.write(cdCommand);
@@ -1279,11 +1276,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
if (sshStream) { if (sshStream) {
try { try {
sshStream.end(); sshStream.end();
@@ -1320,24 +1312,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
}, 100); }, 100);
} }
function setupPingInterval() { // Note: PTY-level keepalive (writing \x00 to the stream) was removed.
pingInterval = setInterval(() => { // It was causing ^@ characters to appear in terminals with echoctl enabled.
if (sshConn && sshStream) { // SSH-level keepalive is configured via connectConfig (keepaliveInterval,
try { // keepaliveCountMax, tcpKeepAlive), which handles connection health monitoring
sshStream.write("\x00"); // without producing visible output on the terminal.
} catch (e: unknown) { //
sshLogger.error( // See: https://github.com/Termix-SSH/Support/issues/232
"SSH keepalive failed: " + // See: https://github.com/Termix-SSH/Support/issues/309
(e instanceof Error ? e.message : "Unknown error"),
);
cleanupSSH();
}
} else if (!sshConn || !sshStream) {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
}
}, 30000);
}
}); });

View File

@@ -104,6 +104,19 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
await import("./ssh/server-stats.js"); await import("./ssh/server-stats.js");
await import("./dashboard.js"); await import("./dashboard.js");
// Initialize Guacamole server for RDP/VNC/Telnet support
if (process.env.ENABLE_GUACAMOLE !== "false") {
try {
await import("./guacamole/guacamole-server.js");
systemLogger.info("Guacamole server initialized", { operation: "guac_init" });
} catch (error) {
systemLogger.warn("Failed to initialize Guacamole server (guacd may not be available)", {
operation: "guac_init_skip",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
process.on("SIGINT", () => { process.on("SIGINT", () => {
systemLogger.info( systemLogger.info(
"Received SIGINT signal, initiating graceful shutdown...", "Received SIGINT signal, initiating graceful shutdown...",

View File

@@ -254,5 +254,6 @@ export const authLogger = new Logger("AUTH", "🔐", "#ef4444");
export const systemLogger = new Logger("SYSTEM", "🚀", "#14b8a6"); export const systemLogger = new Logger("SYSTEM", "🚀", "#14b8a6");
export const versionLogger = new Logger("VERSION", "📦", "#8b5cf6"); export const versionLogger = new Logger("VERSION", "📦", "#8b5cf6");
export const dashboardLogger = new Logger("DASHBOARD", "📊", "#ec4899"); export const dashboardLogger = new Logger("DASHBOARD", "📊", "#ec4899");
export const guacLogger = new Logger("GUACAMOLE", "🖼️", "#ff6b6b");
export const logger = systemLogger; export const logger = systemLogger;

View File

@@ -705,6 +705,7 @@ export const DEFAULT_TERMINAL_CONFIG = {
startupSnippetId: null as number | null, startupSnippetId: null as number | null,
autoMosh: false, autoMosh: false,
moshCommand: "mosh-server new -s -l LANG=en_US.UTF-8", moshCommand: "mosh-server new -s -l LANG=en_US.UTF-8",
sudoPasswordAutoFill: false,
}; };
export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG; export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG;

View File

@@ -8,12 +8,13 @@ import deTranslation from "../locales/de/translation.json";
import ptbrTranslation from "../locales/pt-BR/translation.json"; import ptbrTranslation from "../locales/pt-BR/translation.json";
import ruTranslation from "../locales/ru/translation.json"; import ruTranslation from "../locales/ru/translation.json";
import frTranslation from "../locales/fr/translation.json"; import frTranslation from "../locales/fr/translation.json";
import koTranslation from "../locales/ko/translation.json";
i18n i18n
.use(LanguageDetector) .use(LanguageDetector)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
supportedLngs: ["en", "zh", "de", "ptbr", "ru", "fr"], supportedLngs: ["en", "zh", "de", "ptbr", "ru", "fr", "ko"],
fallbackLng: "en", fallbackLng: "en",
debug: false, debug: false,
@@ -44,6 +45,9 @@ i18n
fr: { fr: {
translation: frTranslation, translation: frTranslation,
}, },
ko: {
translation: koTranslation,
},
}, },
interpolation: { interpolation: {

File diff suppressed because it is too large Load Diff

View File

@@ -59,7 +59,6 @@
"keyTypeRSA": "RSA", "keyTypeRSA": "RSA",
"keyTypeECDSA": "ECDSA", "keyTypeECDSA": "ECDSA",
"keyTypeEd25519": "Ed25519", "keyTypeEd25519": "Ed25519",
"updateCredential": "Update Credential",
"basicInfo": "Basic Info", "basicInfo": "Basic Info",
"authentication": "Authentication", "authentication": "Authentication",
"organization": "Organization", "organization": "Organization",
@@ -119,7 +118,6 @@
"credentialSecuredDescription": "All sensitive data is encrypted with AES-256", "credentialSecuredDescription": "All sensitive data is encrypted with AES-256",
"passwordAuthentication": "Password Authentication", "passwordAuthentication": "Password Authentication",
"keyAuthentication": "Key Authentication", "keyAuthentication": "Key Authentication",
"keyType": "Key Type",
"securityReminder": "Security Reminder", "securityReminder": "Security Reminder",
"securityReminderText": "Never share your credentials. All data is encrypted at rest.", "securityReminderText": "Never share your credentials. All data is encrypted at rest.",
"hostsUsingCredential": "Hosts Using This Credential", "hostsUsingCredential": "Hosts Using This Credential",
@@ -299,7 +297,7 @@
"warning": "Warning", "warning": "Warning",
"info": "Info", "info": "Info",
"success": "Success", "success": "Success",
"loading": "Loading", "loading": "Loading...",
"required": "Required", "required": "Required",
"optional": "Optional", "optional": "Optional",
"connect": "Connect", "connect": "Connect",
@@ -315,7 +313,6 @@
"updateAvailable": "Update Available", "updateAvailable": "Update Available",
"sshPath": "SSH Path", "sshPath": "SSH Path",
"localPath": "Local Path", "localPath": "Local Path",
"loading": "Loading...",
"noAuthCredentials": "No authentication credentials available for this SSH host", "noAuthCredentials": "No authentication credentials available for this SSH host",
"noReleases": "No Releases", "noReleases": "No Releases",
"updatesAndReleases": "Updates & Releases", "updatesAndReleases": "Updates & Releases",
@@ -330,13 +327,10 @@
"resetPassword": "Reset Password", "resetPassword": "Reset Password",
"resetCode": "Reset Code", "resetCode": "Reset Code",
"newPassword": "New Password", "newPassword": "New Password",
"sshPath": "SSH Path",
"localPath": "Local Path",
"folder": "Folder", "folder": "Folder",
"file": "File", "file": "File",
"renamedSuccessfully": "renamed successfully", "renamedSuccessfully": "renamed successfully",
"deletedSuccessfully": "deleted successfully", "deletedSuccessfully": "deleted successfully",
"noAuthCredentials": "No authentication credentials available for this SSH host",
"noTunnelConnections": "No tunnel connections configured", "noTunnelConnections": "No tunnel connections configured",
"sshTools": "SSH Tools", "sshTools": "SSH Tools",
"english": "English", "english": "English",
@@ -348,14 +342,12 @@
"login": "Login", "login": "Login",
"logout": "Logout", "logout": "Logout",
"register": "Register", "register": "Register",
"username": "Username",
"password": "Password", "password": "Password",
"version": "Version", "version": "Version",
"confirmPassword": "Confirm Password", "confirmPassword": "Confirm Password",
"back": "Back", "back": "Back",
"email": "Email", "email": "Email",
"submit": "Submit", "submit": "Submit",
"cancel": "Cancel",
"change": "Change", "change": "Change",
"save": "Save", "save": "Save",
"saving": "Saving...", "saving": "Saving...",
@@ -363,22 +355,15 @@
"edit": "Edit", "edit": "Edit",
"add": "Add", "add": "Add",
"search": "Search", "search": "Search",
"loading": "Loading...",
"error": "Error",
"success": "Success",
"warning": "Warning",
"info": "Info",
"confirm": "Confirm", "confirm": "Confirm",
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"ok": "OK", "ok": "OK",
"close": "Close",
"enabled": "Enabled", "enabled": "Enabled",
"disabled": "Disabled", "disabled": "Disabled",
"important": "Important", "important": "Important",
"notEnabled": "Not Enabled", "notEnabled": "Not Enabled",
"settingUp": "Setting up...", "settingUp": "Setting up...",
"back": "Back",
"next": "Next", "next": "Next",
"previous": "Previous", "previous": "Previous",
"refresh": "Refresh", "refresh": "Refresh",
@@ -431,7 +416,7 @@
"userManagement": "User Management", "userManagement": "User Management",
"makeAdmin": "Make Admin", "makeAdmin": "Make Admin",
"removeAdmin": "Remove Admin", "removeAdmin": "Remove Admin",
"deleteUser": "Delete User", "deleteUser": "Delete user {{username}}? This cannot be undone.",
"allowRegistration": "Allow Registration", "allowRegistration": "Allow Registration",
"oidcSettings": "OIDC Settings", "oidcSettings": "OIDC Settings",
"clientId": "Client ID", "clientId": "Client ID",
@@ -485,7 +470,6 @@
"removeAdminStatus": "Remove admin status from {{username}}?", "removeAdminStatus": "Remove admin status from {{username}}?",
"adminStatusRemoved": "Admin status removed from {{username}}", "adminStatusRemoved": "Admin status removed from {{username}}",
"failedToRemoveAdminStatus": "Failed to remove admin status", "failedToRemoveAdminStatus": "Failed to remove admin status",
"deleteUser": "Delete user {{username}}? This cannot be undone.",
"userDeletedSuccessfully": "User {{username}} deleted successfully", "userDeletedSuccessfully": "User {{username}} deleted successfully",
"failedToDeleteUser": "Failed to delete user", "failedToDeleteUser": "Failed to delete user",
"overrideUserInfoUrl": "Override User Info URL (not required)", "overrideUserInfoUrl": "Override User Info URL (not required)",
@@ -563,7 +547,6 @@
"verificationCompleted": "Compatibility verification completed - no data was changed", "verificationCompleted": "Compatibility verification completed - no data was changed",
"verificationInProgress": "Verification completed", "verificationInProgress": "Verification completed",
"dataMigrationCompleted": "Data migration completed successfully!", "dataMigrationCompleted": "Data migration completed successfully!",
"migrationCompleted": "Migration completed",
"verificationFailed": "Compatibility verification failed", "verificationFailed": "Compatibility verification failed",
"migrationFailed": "Migration failed", "migrationFailed": "Migration failed",
"runningVerification": "Running compatibility verification...", "runningVerification": "Running compatibility verification...",
@@ -641,7 +624,8 @@
"requiresPasswordLogin": "Requires password login enabled", "requiresPasswordLogin": "Requires password login enabled",
"passwordLoginDisabledWarning": "Password login is disabled. Ensure OIDC is properly configured or you will not be able to log in to Termix.", "passwordLoginDisabledWarning": "Password login is disabled. Ensure OIDC is properly configured or you will not be able to log in to Termix.",
"oidcRequiredWarning": "CRITICAL: Password login is disabled. If you reset or misconfigure OIDC, you will lose all access to Termix and brick your instance. Only proceed if you are absolutely certain.", "oidcRequiredWarning": "CRITICAL: Password login is disabled. If you reset or misconfigure OIDC, you will lose all access to Termix and brick your instance. Only proceed if you are absolutely certain.",
"confirmDisableOIDCWarning": "WARNING: You are about to disable OIDC while password login is also disabled. This will brick your Termix instance and you will lose all access. Are you absolutely sure you want to proceed?" "confirmDisableOIDCWarning": "WARNING: You are about to disable OIDC while password login is also disabled. This will brick your Termix instance and you will lose all access. Are you absolutely sure you want to proceed?",
"failedToUpdatePasswordLoginStatus": "Failed to update password login status"
}, },
"hosts": { "hosts": {
"title": "Host Manager", "title": "Host Manager",
@@ -908,7 +892,9 @@
"quickActionName": "Action name", "quickActionName": "Action name",
"noSnippetFound": "No snippet found", "noSnippetFound": "No snippet found",
"quickActionsOrder": "Quick action buttons will appear in the order listed above on the Server Stats page", "quickActionsOrder": "Quick action buttons will appear in the order listed above on the Server Stats page",
"advancedAuthSettings": "Advanced Authentication Settings" "advancedAuthSettings": "Advanced Authentication Settings",
"sudoPasswordAutoFill": "Sudo Password Auto-Fill",
"sudoPasswordAutoFillDesc": "Automatically offer to insert SSH password when sudo prompts for password"
}, },
"terminal": { "terminal": {
"title": "Terminal", "title": "Terminal",
@@ -946,7 +932,11 @@
"totpRequired": "Two-Factor Authentication Required", "totpRequired": "Two-Factor Authentication Required",
"totpCodeLabel": "Verification Code", "totpCodeLabel": "Verification Code",
"totpPlaceholder": "000000", "totpPlaceholder": "000000",
"totpVerify": "Verify" "totpVerify": "Verify",
"sudoPasswordPopupTitle": "Insert Password?",
"sudoPasswordPopupHint": "Press Enter to insert, Esc to dismiss",
"sudoPasswordPopupConfirm": "Insert",
"sudoPasswordPopupDismiss": "Dismiss"
}, },
"fileManager": { "fileManager": {
"title": "File Manager", "title": "File Manager",
@@ -1048,7 +1038,6 @@
"copyPaths": "Copy Paths", "copyPaths": "Copy Paths",
"delete": "Delete", "delete": "Delete",
"properties": "Properties", "properties": "Properties",
"preview": "Preview",
"refresh": "Refresh", "refresh": "Refresh",
"downloadFiles": "Download {{count}} files to Browser", "downloadFiles": "Download {{count}} files to Browser",
"copyFiles": "Copy {{count}} items", "copyFiles": "Copy {{count}} items",
@@ -1063,18 +1052,11 @@
"failedToDeleteItem": "Failed to delete item", "failedToDeleteItem": "Failed to delete item",
"itemRenamedSuccessfully": "{{type}} renamed successfully", "itemRenamedSuccessfully": "{{type}} renamed successfully",
"failedToRenameItem": "Failed to rename item", "failedToRenameItem": "Failed to rename item",
"upload": "Upload",
"download": "Download", "download": "Download",
"newFile": "New File",
"newFolder": "New Folder",
"rename": "Rename",
"delete": "Delete",
"permissions": "Permissions", "permissions": "Permissions",
"size": "Size", "size": "Size",
"modified": "Modified", "modified": "Modified",
"path": "Path", "path": "Path",
"fileName": "File Name",
"folderName": "Folder Name",
"confirmDelete": "Are you sure you want to delete {{name}}?", "confirmDelete": "Are you sure you want to delete {{name}}?",
"uploadSuccess": "File uploaded successfully", "uploadSuccess": "File uploaded successfully",
"uploadFailed": "File upload failed", "uploadFailed": "File upload failed",
@@ -1094,10 +1076,7 @@
"fileSavedSuccessfully": "File saved successfully", "fileSavedSuccessfully": "File saved successfully",
"saveTimeout": "Save operation timed out. The file may have been saved successfully, but the operation took too long to complete. Check the Docker logs for confirmation.", "saveTimeout": "Save operation timed out. The file may have been saved successfully, but the operation took too long to complete. Check the Docker logs for confirmation.",
"failedToSaveFile": "Failed to save file", "failedToSaveFile": "Failed to save file",
"folder": "Folder",
"file": "File",
"deletedSuccessfully": "deleted successfully", "deletedSuccessfully": "deleted successfully",
"failedToDeleteItem": "Failed to delete item",
"connectToServer": "Connect to a Server", "connectToServer": "Connect to a Server",
"selectServerToEdit": "Select a server from the sidebar to start editing files", "selectServerToEdit": "Select a server from the sidebar to start editing files",
"fileOperations": "File Operations", "fileOperations": "File Operations",
@@ -1154,10 +1133,8 @@
"unpinFile": "Unpin file", "unpinFile": "Unpin file",
"removeShortcut": "Remove shortcut", "removeShortcut": "Remove shortcut",
"saveFilesToSystem": "Save {{count}} files as...", "saveFilesToSystem": "Save {{count}} files as...",
"saveToSystem": "Save as...",
"pinFile": "Pin file", "pinFile": "Pin file",
"addToShortcuts": "Add to shortcuts", "addToShortcuts": "Add to shortcuts",
"selectLocationToSave": "Select location to save",
"downloadToDefaultLocation": "Download to default location", "downloadToDefaultLocation": "Download to default location",
"pasteFailed": "Paste failed", "pasteFailed": "Paste failed",
"noUndoableActions": "No undoable actions", "noUndoableActions": "No undoable actions",
@@ -1175,7 +1152,6 @@
"editPath": "Edit path", "editPath": "Edit path",
"confirm": "Confirm", "confirm": "Confirm",
"cancel": "Cancel", "cancel": "Cancel",
"folderName": "Folder name",
"find": "Find...", "find": "Find...",
"replaceWith": "Replace with...", "replaceWith": "Replace with...",
"replace": "Replace", "replace": "Replace",
@@ -1201,23 +1177,18 @@
"outdent": "Outdent", "outdent": "Outdent",
"autoComplete": "Auto Complete", "autoComplete": "Auto Complete",
"imageLoadError": "Failed to load image", "imageLoadError": "Failed to load image",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"rotate": "Rotate", "rotate": "Rotate",
"originalSize": "Original Size", "originalSize": "Original Size",
"startTyping": "Start typing...", "startTyping": "Start typing...",
"unknownSize": "Unknown size", "unknownSize": "Unknown size",
"fileIsEmpty": "File is empty", "fileIsEmpty": "File is empty",
"modified": "Modified",
"largeFileWarning": "Large File Warning", "largeFileWarning": "Large File Warning",
"largeFileWarningDesc": "This file is {{size}} in size, which may cause performance issues when opened as text.", "largeFileWarningDesc": "This file is {{size}} in size, which may cause performance issues when opened as text.",
"fileNotFoundAndRemoved": "File \"{{name}}\" not found and has been removed from recent/pinned files", "fileNotFoundAndRemoved": "File \"{{name}}\" not found and has been removed from recent/pinned files",
"failedToLoadFile": "Failed to load file: {{error}}", "failedToLoadFile": "Failed to load file: {{error}}",
"serverErrorOccurred": "Server error occurred. Please try again later.", "serverErrorOccurred": "Server error occurred. Please try again later.",
"fileSavedSuccessfully": "File saved successfully",
"autoSaveFailed": "Auto-save failed", "autoSaveFailed": "Auto-save failed",
"fileAutoSaved": "File auto-saved", "fileAutoSaved": "File auto-saved",
"fileDownloadedSuccessfully": "File downloaded successfully",
"moveFileFailed": "Failed to move {{name}}", "moveFileFailed": "Failed to move {{name}}",
"moveOperationFailed": "Move operation failed", "moveOperationFailed": "Move operation failed",
"canOnlyCompareFiles": "Can only compare two files", "canOnlyCompareFiles": "Can only compare two files",
@@ -1311,17 +1282,8 @@
"local": "Local", "local": "Local",
"remote": "Remote", "remote": "Remote",
"dynamic": "Dynamic", "dynamic": "Dynamic",
"noSshTunnels": "No SSH Tunnels",
"createFirstTunnelMessage": "Create your first SSH tunnel to get started. Use the SSH Manager to add hosts with tunnel connections.",
"unknownConnectionStatus": "Unknown", "unknownConnectionStatus": "Unknown",
"connected": "Connected",
"connecting": "Connecting...",
"disconnecting": "Disconnecting...",
"disconnected": "Disconnected",
"portMapping": "Port {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", "portMapping": "Port {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
"disconnect": "Disconnect",
"connect": "Connect",
"canceling": "Canceling...",
"endpointHostNotFound": "Endpoint host not found", "endpointHostNotFound": "Endpoint host not found",
"discord": "Discord", "discord": "Discord",
"githubIssue": "GitHub issue", "githubIssue": "GitHub issue",
@@ -1334,7 +1296,7 @@
"disk": "Disk", "disk": "Disk",
"network": "Network", "network": "Network",
"uptime": "Uptime", "uptime": "Uptime",
"loadAverage": "Load Average", "loadAverage": "Avg: {{avg1}}, {{avg5}}, {{avg15}}",
"processes": "Processes", "processes": "Processes",
"connections": "Connections", "connections": "Connections",
"usage": "Usage", "usage": "Usage",
@@ -1350,7 +1312,6 @@
"cpuCores_one": "{{count}} CPU", "cpuCores_one": "{{count}} CPU",
"cpuCores_other": "{{count}} CPUs", "cpuCores_other": "{{count}} CPUs",
"naCpus": "N/A CPU(s)", "naCpus": "N/A CPU(s)",
"loadAverage": "Avg: {{avg1}}, {{avg5}}, {{avg15}}",
"loadAverageNA": "Avg: N/A", "loadAverageNA": "Avg: N/A",
"cpuUsage": "CPU Usage", "cpuUsage": "CPU Usage",
"memoryUsage": "Memory Usage", "memoryUsage": "Memory Usage",
@@ -1369,7 +1330,6 @@
"totpRequired": "TOTP Authentication Required", "totpRequired": "TOTP Authentication Required",
"totpUnavailable": "Server Stats unavailable for TOTP-enabled servers", "totpUnavailable": "Server Stats unavailable for TOTP-enabled servers",
"load": "Load", "load": "Load",
"available": "Available",
"editLayout": "Edit Layout", "editLayout": "Edit Layout",
"cancelEdit": "Cancel", "cancelEdit": "Cancel",
"addWidget": "Add Widget", "addWidget": "Add Widget",
@@ -1508,7 +1468,9 @@
"authenticating": "Authenticating...", "authenticating": "Authenticating...",
"dataLossWarning": "Resetting your password this way will delete all your saved SSH hosts, credentials, and other encrypted data. This action cannot be undone. Only use this if you have forgotten your password and are not logged in.", "dataLossWarning": "Resetting your password this way will delete all your saved SSH hosts, credentials, and other encrypted data. This action cannot be undone. Only use this if you have forgotten your password and are not logged in.",
"authenticationDisabled": "Authentication Disabled", "authenticationDisabled": "Authentication Disabled",
"authenticationDisabledDesc": "All authentication methods are currently disabled. Please contact your administrator." "authenticationDisabledDesc": "All authentication methods are currently disabled. Please contact your administrator.",
"passwordResetSuccess": "Password Reset Successful",
"passwordResetSuccessDesc": "Your password has been reset successfully. You can now log in with your new password."
}, },
"errors": { "errors": {
"notFound": "Page not found", "notFound": "Page not found",
@@ -1586,6 +1548,8 @@
"fileColorCodingDesc": "Color-code files by type: folders (red), files (blue), symlinks (green)", "fileColorCodingDesc": "Color-code files by type: folders (red), files (blue), symlinks (green)",
"commandAutocomplete": "Command Autocomplete", "commandAutocomplete": "Command Autocomplete",
"commandAutocompleteDesc": "Enable Tab key autocomplete suggestions for terminal commands based on your command history", "commandAutocompleteDesc": "Enable Tab key autocomplete suggestions for terminal commands based on your command history",
"defaultSnippetFoldersCollapsed": "Collapse Snippet Folders by Default",
"defaultSnippetFoldersCollapsedDesc": "When enabled, all snippet folders will be collapsed when you open the snippets tab",
"currentPassword": "Current Password", "currentPassword": "Current Password",
"passwordChangedSuccess": "Password changed successfully! Please log in again.", "passwordChangedSuccess": "Password changed successfully! Please log in again.",
"failedToChangePassword": "Failed to change password. Please check your current password and try again." "failedToChangePassword": "Failed to change password. Please check your current password and try again."
@@ -1672,7 +1636,6 @@
"deleteItem": "Delete Item", "deleteItem": "Delete Item",
"createNewFile": "Create New File", "createNewFile": "Create New File",
"createNewFolder": "Create New Folder", "createNewFolder": "Create New Folder",
"deleteItem": "Delete Item",
"renameItem": "Rename Item", "renameItem": "Rename Item",
"clickToSelectFile": "Click to select a file", "clickToSelectFile": "Click to select a file",
"noSshHosts": "No SSH Hosts", "noSshHosts": "No SSH Hosts",

View File

@@ -164,7 +164,8 @@
"generateKeyPairNote": "Générez une nouvelle paire de clés SSH directement. Cela remplacera toute clé existante dans le formulaire.", "generateKeyPairNote": "Générez une nouvelle paire de clés SSH directement. Cela remplacera toute clé existante dans le formulaire.",
"invalidKey": "Clé invalide", "invalidKey": "Clé invalide",
"detectionError": "Erreur de détection", "detectionError": "Erreur de détection",
"unknown": "Inconnu" "unknown": "Inconnu",
"credentialId": "ID de l'identifiant"
}, },
"dragIndicator": { "dragIndicator": {
"error": "Erreur : {{error}}", "error": "Erreur : {{error}}",
@@ -385,7 +386,8 @@
"documentation": "Documentation", "documentation": "Documentation",
"retry": "Réessayer", "retry": "Réessayer",
"checking": "Vérification...", "checking": "Vérification...",
"checkingDatabase": "Vérification de la connexion à la base de données..." "checkingDatabase": "Vérification de la connexion à la base de données...",
"saving": "Enregistrement..."
}, },
"nav": { "nav": {
"home": "Accueil", "home": "Accueil",
@@ -395,7 +397,7 @@
"tunnels": "Tunnels", "tunnels": "Tunnels",
"fileManager": "Gestionnaire de fichiers", "fileManager": "Gestionnaire de fichiers",
"serverStats": "Statistiques serveur", "serverStats": "Statistiques serveur",
"admin": "Admin", "admin": "Administrateur",
"userProfile": "Profil utilisateur", "userProfile": "Profil utilisateur",
"tools": "Outils", "tools": "Outils",
"snippets": "Extraits", "snippets": "Extraits",
@@ -447,7 +449,7 @@
"makeUserAdmin": "Nommer l'utilisateur administrateur", "makeUserAdmin": "Nommer l'utilisateur administrateur",
"adding": "Ajout...", "adding": "Ajout...",
"currentAdmins": "Administrateurs actuels", "currentAdmins": "Administrateurs actuels",
"adminBadge": "Admin", "adminBadge": "Administrateur",
"removeAdminButton": "Retirer l'administrateur", "removeAdminButton": "Retirer l'administrateur",
"general": "Général", "general": "Général",
"userRegistration": "Inscription utilisateur", "userRegistration": "Inscription utilisateur",
@@ -470,7 +472,7 @@
"failedToRemoveAdminStatus": "Échec du retrait du statut d'administrateur", "failedToRemoveAdminStatus": "Échec du retrait du statut d'administrateur",
"userDeletedSuccessfully": "Utilisateur {{username}} supprimé avec succès", "userDeletedSuccessfully": "Utilisateur {{username}} supprimé avec succès",
"failedToDeleteUser": "Échec de la suppression de l'utilisateur", "failedToDeleteUser": "Échec de la suppression de l'utilisateur",
"overrideUserInfoUrl": "Remplacer l'URL User Info (optionnel)", "overrideUserInfoUrl": "Remplacer l'URL Utilisateur Info (optionnel)",
"failedToFetchSessions": "Échec de la récupération des sessions", "failedToFetchSessions": "Échec de la récupération des sessions",
"sessionRevokedSuccessfully": "Session révoquée avec succès", "sessionRevokedSuccessfully": "Session révoquée avec succès",
"failedToRevokeSession": "Échec de la révocation de la session", "failedToRevokeSession": "Échec de la révocation de la session",
@@ -604,7 +606,26 @@
"requiresPasswordLogin": "Nécessite la connexion par mot de passe activée", "requiresPasswordLogin": "Nécessite la connexion par mot de passe activée",
"passwordLoginDisabledWarning": "La connexion par mot de passe est désactivée. Vérifiez qu'OIDC est correctement configuré sinon vous ne pourrez plus vous connecter à Termix.", "passwordLoginDisabledWarning": "La connexion par mot de passe est désactivée. Vérifiez qu'OIDC est correctement configuré sinon vous ne pourrez plus vous connecter à Termix.",
"oidcRequiredWarning": "CRITIQUE : la connexion par mot de passe est désactivée. Si vous réinitialisez ou mal configurez OIDC, vous perdrez tout accès à Termix et bloquerez l'instance. Ne continuez que si vous en êtes absolument certain.", "oidcRequiredWarning": "CRITIQUE : la connexion par mot de passe est désactivée. Si vous réinitialisez ou mal configurez OIDC, vous perdrez tout accès à Termix et bloquerez l'instance. Ne continuez que si vous en êtes absolument certain.",
"confirmDisableOIDCWarning": "AVERTISSEMENT : vous êtes sur le point de désactiver OIDC alors que la connexion par mot de passe est désactivée. Cela bloquera votre instance Termix et vous perdrez tout accès. Êtes-vous vraiment sûr de vouloir continuer ?" "confirmDisableOIDCWarning": "AVERTISSEMENT : vous êtes sur le point de désactiver OIDC alors que la connexion par mot de passe est désactivée. Cela bloquera votre instance Termix et vous perdrez tout accès. Êtes-vous vraiment sûr de vouloir continuer ?",
"accountsLinkedSuccessfully": "Le compte OIDC {{oidcUsername}} a été lié à {{targetUsername}}",
"failedToLinkAccounts": "Impossible de lier les comptes",
"failedToUnlinkOIDC": "Impossible de délier OIDC",
"linkAccountsButton": "Lier les comptes",
"linkOIDCActionAddCapability": "Ajouter la capacité de connexion OIDC au compte mot de passe cible",
"linkOIDCActionDeleteUser": "Supprimer le compte utilisateur OIDC et toutes ses données",
"linkOIDCActionDualAuth": "Autoriser le compte mot de passe à se connecter avec le mot de passe et OIDC",
"linkOIDCDialogDescription": "Lier {{username}} (utilisateur OIDC) à un compte mot de passe existant. Cela activera la double authentification pour le compte mot de passe.",
"linkOIDCDialogTitle": "Lier Compte OIDC au Compte Mot de Passe",
"linkOIDCWarningTitle": "Avertissement : Les données de l'utilisateur OIDC seront supprimées",
"linkTargetUsernameLabel": "Nom d'utilisateur du compte mot de passe cible",
"linkTargetUsernamePlaceholder": "Entrer le nom d'utilisateur du compte mot de passe",
"linkTargetUsernameRequired": "Le nom d'utilisateur cible est requis",
"linkToPasswordAccount": "Lier au Compte Mot de Passe",
"linkingAccounts": "Liaison en cours...",
"unlinkOIDCDescription": "Retirer l'authentification OIDC de {{username}} ? L'utilisateur ne pourra se connecter qu'avec nom d'utilisateur/mot de passe après cela.",
"unlinkOIDCSuccess": "OIDC délié de {{username}}",
"unlinkOIDCTitle": "Délier l'Authentification OIDC",
"failedToUpdatePasswordLoginStatus": "Impossible de mettre à jour le statut de connexion par mot de passe"
}, },
"hosts": { "hosts": {
"title": "Gestionnaire d'hôtes", "title": "Gestionnaire d'hôtes",
@@ -720,7 +741,7 @@
"updateKey": "Mettre à jour la clé", "updateKey": "Mettre à jour la clé",
"existingKey": "Clé existante (cliquez pour modifier)", "existingKey": "Clé existante (cliquez pour modifier)",
"existingCredential": "Identifiant existant (cliquez pour modifier)", "existingCredential": "Identifiant existant (cliquez pour modifier)",
"addTagsSpaceToAdd": "ajouter des labels (espace pour valider)", "addTagsSpaceToAdd": "Ajouter des labels (espace pour valider)",
"terminalBadge": "Terminal", "terminalBadge": "Terminal",
"tunnelBadge": "Tunnel", "tunnelBadge": "Tunnel",
"fileManagerBadge": "Gestionnaire de fichiers", "fileManagerBadge": "Gestionnaire de fichiers",
@@ -790,7 +811,88 @@
"searchServers": "Rechercher des serveurs...", "searchServers": "Rechercher des serveurs...",
"noServerFound": "Aucun serveur trouvé", "noServerFound": "Aucun serveur trouvé",
"jumpHostsOrder": "Les connexions seront établies dans l'ordre : Serveur de rebond 1 → Serveur de rebond 2 → ... → Serveur cible", "jumpHostsOrder": "Les connexions seront établies dans l'ordre : Serveur de rebond 1 → Serveur de rebond 2 → ... → Serveur cible",
"advancedAuthSettings": "Paramètres d'authentification avancés" "advancedAuthSettings": "Paramètres d'authentification avancés",
"addQuickAction": "Ajouter une action rapide",
"adjustFontSize": "Ajuster la taille de la police du terminal",
"adjustLetterSpacing": "Ajuster l'espacement des lettres",
"adjustLineHeight": "Ajuster l'espacement des lignes",
"advanced": "Avancé",
"allHostsInFolderDeleted": "{{count}} hôtes supprimés du dossier \"{{folder}}\" avec succès",
"appearance": "Apparence",
"backspaceMode": "Mode retour arrière (Backspace)",
"backspaceModeControlH": "Control-H (^H)",
"backspaceModeDesc": "Comportement de la touche retour arrière pour la compatibilité",
"backspaceModeNormal": "Normal (DEL)",
"behavior": "Comportement",
"bellStyle": "Style de la cloche (Bell)",
"bellStyleBoth": "Les deux",
"bellStyleDesc": "Comment gérer la cloche du terminal (caractère BEL, \\x07). Les programmes déclenchent ceci lors de la fin de tâches, d'erreurs ou pour les notifications. \"Son\" joue un bip audio, \"Visuel\" fait clignoter l'écran brièvement, \"Les deux\" fait les deux, \"Aucun\" désactive les alertes.",
"bellStyleNone": "Aucun",
"bellStyleSound": "Son",
"bellStyleVisual": "Visuel",
"chooseColorTheme": "Choisir un thème de couleur pour le terminal",
"chooseCursorAppearance": "Choisir l'apparence du curseur",
"confirmDeleteAllHostsInFolder": "Êtes-vous sûr de vouloir supprimer les {{count}} hôtes du dossier \"{{folder}}\" ? Cette action ne peut pas être annulée.",
"cursorBlink": "Clignotement du curseur",
"cursorStyle": "Style du curseur",
"cursorStyleBar": "Barre",
"cursorStyleBlock": "Bloc",
"cursorStyleUnderline": "Souligné",
"deleteAllHostsInFolder": "Supprimer tous les hôtes du dossier",
"editFolderAppearance": "Modifier l'apparence du dossier",
"editFolderAppearanceDesc": "Personnalisez la couleur et l'icône du dossier",
"enableCursorBlink": "Activer l'animation de clignotement du curseur",
"failedToDeleteHostsInFolder": "Impossible de supprimer les hôtes du dossier",
"failedToUpdateFolderAppearance": "Impossible de mettre à jour l'apparence du dossier",
"fastScrollModifier": "Touche de défilement rapide",
"fastScrollModifierDesc": "Touche modificatrice pour le défilement rapide",
"fastScrollSensitivity": "Sensibilité du défilement rapide",
"fastScrollSensitivityDesc": "Multiplicateur de vitesse de défilement lorsque la touche est maintenue",
"fastScrollSensitivityValue": "Sensibilité du défilement rapide : {{value}}",
"folderAppearanceUpdated": "Apparence du dossier mise à jour avec succès",
"folderColor": "Couleur du dossier",
"folderIcon": "Icône du dossier",
"fontFamily": "Famille de police",
"fontSize": "Taille de police",
"fontSizeValue": "Taille de police : {{value}}px",
"letterSpacing": "Espacement des lettres",
"letterSpacingValue": "Espacement des lettres : {{value}}px",
"lineHeight": "Hauteur de ligne",
"lineHeightValue": "Hauteur de ligne : {{value}}",
"minimumContrastRatio": "Ratio de contraste minimum",
"minimumContrastRatioDesc": "Ajuster automatiquement les couleurs pour une meilleure lisibilité",
"minimumContrastRatioValue": "Ratio de contraste minimum : {{value}}",
"modifierAlt": "Alt",
"modifierCtrl": "Ctrl",
"modifierShift": "Shift",
"noSnippetFound": "Aucun extrait trouvé",
"preview": "Aperçu",
"quickActionName": "Nom de l'action",
"quickActions": "Actions rapides",
"quickActionsDescription": "Les actions rapides vous permettent de créer des boutons personnalisés qui exécutent des extraits SSH sur ce serveur. Ces boutons apparaîtront en haut de la page Statistiques Serveur pour un accès rapide.",
"quickActionsList": "Liste des actions rapides",
"quickActionsOrder": "Les boutons d'action rapide apparaîtront dans l'ordre indiqué ci-dessus sur la page Statistiques Serveur",
"rightClickSelectsWord": "Clic droit sélectionne le mot",
"rightClickSelectsWordDesc": "Le clic droit sélectionne le mot sous le curseur",
"scrollbackBuffer": "Tampon de défilement",
"scrollbackBufferDesc": "Nombre de lignes à conserver dans l'historique de défilement",
"scrollbackBufferValue": "Tampon de défilement : {{value}} lignes",
"searchSnippets": "Rechercher des extraits...",
"selectBackspaceMode": "Sélectionner le mode retour arrière",
"selectBellStyle": "Sélectionner le style de cloche",
"selectCursorStyle": "Sélectionner le style de curseur",
"selectFont": "Sélectionner la police",
"selectFontDesc": "Sélectionner la police à utiliser dans le terminal",
"selectModifier": "Sélectionner la touche modificatrice",
"selectSnippet": "Sélectionner un extrait",
"selectTheme": "Sélectionner un thème",
"snippetNone": "Aucun",
"sshAgentForwarding": "Transfert d'agent SSH",
"sshAgentForwardingDesc": "Transférer l'agent d'authentification SSH à l'hôte distant",
"startupSnippet": "Extrait au démarrage",
"terminalCustomization": "Personnalisation du terminal",
"theme": "Thème",
"themePreview": "Aperçu du thème"
}, },
"terminal": { "terminal": {
"title": "Terminal", "title": "Terminal",
@@ -828,7 +930,11 @@
"totpRequired": "Authentification à deux facteurs requise", "totpRequired": "Authentification à deux facteurs requise",
"totpCodeLabel": "Code de vérification", "totpCodeLabel": "Code de vérification",
"totpPlaceholder": "000000", "totpPlaceholder": "000000",
"totpVerify": "Vérifier" "totpVerify": "Vérifier",
"sudoPasswordPopupTitle": "Insérer le mot de passe ?",
"sudoPasswordPopupHint": "Appuyez sur Entrée pour insérer, Échap pour annuler",
"sudoPasswordPopupConfirm": "Insérer",
"sudoPasswordPopupDismiss": "Annuler"
}, },
"fileManager": { "fileManager": {
"title": "Gestionnaire de fichiers", "title": "Gestionnaire de fichiers",
@@ -943,7 +1049,7 @@
"internalServerError": "Une erreur interne du serveur est survenue", "internalServerError": "Une erreur interne du serveur est survenue",
"serverError": "Erreur serveur", "serverError": "Erreur serveur",
"error": "Erreur", "error": "Erreur",
"requestFailed": "Réquête échouée avec le code", "requestFailed": "Requête échouée avec le code",
"unknownFileError": "Erreur de fichier inconnue", "unknownFileError": "Erreur de fichier inconnue",
"cannotReadFile": "Impossible de lire le fichier", "cannotReadFile": "Impossible de lire le fichier",
"noSshSessionId": "Aucun ID de session SSH", "noSshSessionId": "Aucun ID de session SSH",
@@ -1100,7 +1206,35 @@
"sshConnectionFailed": "La connexion SSH a échoué. Vérifiez votre connexion à {{name}} ({{ip}}:{{port}})", "sshConnectionFailed": "La connexion SSH a échoué. Vérifiez votre connexion à {{name}} ({{ip}}:{{port}})",
"loadFileFailed": "Échec du chargement du fichier : {{error}}", "loadFileFailed": "Échec du chargement du fichier : {{error}}",
"connectedSuccessfully": "Connexion réussie", "connectedSuccessfully": "Connexion réussie",
"totpVerificationFailed": "Échec de la vérification TOTP" "totpVerificationFailed": "Échec de la vérification TOTP",
"andMoreFiles": "et {{count}} plus...",
"archiveExtractedSuccessfully": "{{name}} extrait avec succès",
"archiveName": "Nom de l'archive",
"changePermissions": "Modifier les permissions",
"changePermissionsDesc": "Modifier les permissions du fichier pour",
"compress": "Compresser",
"compressFailed": "Échec de la compression",
"compressFile": "Compresser le fichier",
"compressFiles": "Compresser les fichiers",
"compressFilesDesc": "Compresser {{count}} éléments dans une archive",
"compressingFiles": "Compression de {{count}} éléments dans {{name}}...",
"compressionFormat": "Format de compression",
"currentPermissions": "Permissions actuelles",
"enterArchiveName": "Entrer le nom de l'archive...",
"execute": "Exécuter",
"extractArchive": "Extraire l'archive",
"extractFailed": "Échec de l'extraction",
"extractingArchive": "Extraction de {{name}}...",
"failedToChangePermissions": "Impossible de modifier les permissions",
"filesCompressedSuccessfully": "{{name}} créé avec succès",
"group": "Groupe",
"newPermissions": "Nouvelles permissions",
"others": "Autres",
"owner": "Propriétaire",
"permissionsChangedSuccessfully": "Permissions modifiées avec succès",
"read": "Lecture",
"selectedFiles": "Fichiers sélectionnés",
"write": "Écriture"
}, },
"tunnels": { "tunnels": {
"title": "Tunnels SSH", "title": "Tunnels SSH",
@@ -1211,7 +1345,20 @@
"noInterfacesFound": "Aucune interface trouvée", "noInterfacesFound": "Aucune interface trouvée",
"totalProcesses": "Processus totaux", "totalProcesses": "Processus totaux",
"running": "En cours d'exécution", "running": "En cours d'exécution",
"noProcessesFound": "Aucun processus trouvé" "noProcessesFound": "Aucun processus trouvé",
"executeQuickAction": "Exécuter {{name}}",
"executingQuickAction": "Exécution de {{name}}...",
"from": "de",
"loginStats": "Statistiques de connexion SSH",
"noRecentLoginData": "Aucune donnée de connexion récente",
"quickActionError": "Impossible d'exécuter {{name}}",
"quickActionFailed": "{{name}} a échoué",
"quickActionSuccess": "{{name}} terminé avec succès",
"quickActions": "Actions rapides",
"recentFailedAttempts": "Tentatives échouées récentes",
"recentSuccessfulLogins": "Connexions réussies récentes",
"totalLogins": "Total des connexions",
"uniqueIPs": "IP uniques"
}, },
"auth": { "auth": {
"loginTitle": "Connexion à Termix", "loginTitle": "Connexion à Termix",
@@ -1314,7 +1461,14 @@
"authenticating": "Authentification...", "authenticating": "Authentification...",
"dataLossWarning": "Réinitialiser votre mot de passe de cette manière supprimera tous vos hôtes, identifiants et autres données chiffrées. Action irréversible. À utiliser uniquement si vous avez oublié votre mot de passe et n'êtes pas connecté.", "dataLossWarning": "Réinitialiser votre mot de passe de cette manière supprimera tous vos hôtes, identifiants et autres données chiffrées. Action irréversible. À utiliser uniquement si vous avez oublié votre mot de passe et n'êtes pas connecté.",
"authenticationDisabled": "Authentification désactivée", "authenticationDisabled": "Authentification désactivée",
"authenticationDisabledDesc": "Toutes les méthodes d'authentification sont actuellement désactivées. Contactez votre administrateur." "authenticationDisabledDesc": "Toutes les méthodes d'authentification sont actuellement désactivées. Contactez votre administrateur.",
"continueExternal": "Continuer avec un fournisseur externe",
"createAccount": "Créer votre compte TERMIX",
"description": "Gestion de connexions SSH sécurisée, puissante et intuitive",
"tagline": "GESTIONNAIRE DE SERVEURS SSH",
"welcomeBack": "Bon retour sur TERMIX",
"passwordResetSuccess": "Réinitialisation du mot de passe réussie",
"passwordResetSuccessDesc": "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe."
}, },
"errors": { "errors": {
"notFound": "Page introuvable", "notFound": "Page introuvable",
@@ -1391,9 +1545,12 @@
"fileColorCodingDesc": "Codage couleur des fichiers par type : dossiers (rouge), fichiers (bleu), liens symboliques (vert)", "fileColorCodingDesc": "Codage couleur des fichiers par type : dossiers (rouge), fichiers (bleu), liens symboliques (vert)",
"commandAutocomplete": "Autocomplétion des commandes", "commandAutocomplete": "Autocomplétion des commandes",
"commandAutocompleteDesc": "Activer les suggestions d'autocomplétion avec la touche Tab pour les commandes du terminal basées sur votre historique", "commandAutocompleteDesc": "Activer les suggestions d'autocomplétion avec la touche Tab pour les commandes du terminal basées sur votre historique",
"defaultSnippetFoldersCollapsed": "Réduire les dossiers de snippets par défaut",
"defaultSnippetFoldersCollapsedDesc": "Lorsque cette option est activée, tous les dossiers de snippets seront réduits à l'ouverture de l'onglet snippets",
"currentPassword": "Mot de passe actuel", "currentPassword": "Mot de passe actuel",
"passwordChangedSuccess": "Mot de passe modifié avec succès ! Veuillez vous reconnecter.", "passwordChangedSuccess": "Mot de passe modifié avec succès ! Veuillez vous reconnecter.",
"failedToChangePassword": "Échec de la modification du mot de passe. Vérifiez votre mot de passe actuel et réessayez." "failedToChangePassword": "Échec de la modification du mot de passe. Vérifiez votre mot de passe actuel et réessayez.",
"externalAndLocal": "Double Auth"
}, },
"user": { "user": {
"failedToLoadVersionInfo": "Échec du chargement des informations de version" "failedToLoadVersionInfo": "Échec du chargement des informations de version"
@@ -1405,11 +1562,11 @@
"maxRetries": "3", "maxRetries": "3",
"retryInterval": "10", "retryInterval": "10",
"language": "Langue", "language": "Langue",
"username": "nom d'utilisateur", "username": "Nom d'utilisateur",
"hostname": "nom d'hôte", "hostname": "Nom d'hôte",
"folder": "dossier", "folder": "Dossier",
"password": "mot de passe", "password": "Mot de passe",
"keyPassword": "mot de passe de la clé", "keyPassword": "Mot de passe de la clé",
"pastePrivateKey": "Collez votre clé privée ici...", "pastePrivateKey": "Collez votre clé privée ici...",
"pastePublicKey": "Collez votre clé publique ici...", "pastePublicKey": "Collez votre clé publique ici...",
"credentialName": "Mon serveur SSH", "credentialName": "Mon serveur SSH",
@@ -1605,5 +1762,28 @@
"cpu": "Processeur (CPU)", "cpu": "Processeur (CPU)",
"ram": "Mémoire (RAM)", "ram": "Mémoire (RAM)",
"notAvailable": "N/D" "notAvailable": "N/D"
},
"commandPalette": {
"addCredential": "Ajouter Identifiant",
"addHost": "Ajouter Hôte",
"adminSettings": "Paramètres Administrateur",
"close": "Fermer",
"discord": "Discord",
"donate": "Faire un don",
"edit": "Modifier",
"github": "GitHub",
"hostManager": "Gestionnaire d'hôtes",
"hosts": "Hôtes",
"links": "Liens",
"navigation": "Navigation",
"openFileManager": "Ouvrir Gestionnaire de fichiers",
"openServerDetails": "Ouvrir Détails Serveur",
"press": "Appuyer",
"recentActivity": "Activité Récente",
"searchPlaceholder": "Rechercher hôtes ou actions rapides...",
"support": "Support",
"toToggle": "pour basculer",
"updateLog": "Journal de mise à jour",
"userProfile": "Profil utilisateur"
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -59,7 +59,6 @@
"keyTypeRSA": "RSA", "keyTypeRSA": "RSA",
"keyTypeECDSA": "ECDSA", "keyTypeECDSA": "ECDSA",
"keyTypeEd25519": "Ed25519", "keyTypeEd25519": "Ed25519",
"updateCredential": "Atualizar Credencial",
"basicInfo": "Informações básicas", "basicInfo": "Informações básicas",
"authentication": "Autenticação", "authentication": "Autenticação",
"organization": "Organização", "organization": "Organização",
@@ -93,7 +92,7 @@
"deploySSHKey": "Implantar Chave SSH", "deploySSHKey": "Implantar Chave SSH",
"deploySSHKeyDescription": "Implantar chave pública no servidor de destino", "deploySSHKeyDescription": "Implantar chave pública no servidor de destino",
"sourceCredential": "Credencial de Origem", "sourceCredential": "Credencial de Origem",
"targetHost": "Host de Destino", "targetHost": "Host de Destinenhum",
"deploymentProcess": "Processo de Implantação", "deploymentProcess": "Processo de Implantação",
"deploymentProcessDescription": "Isso adicionará com segurança a chave pública ao arquivo ~/.ssh/authorized_keys do host de destino sem sobrescrever chaves existentes. A operação é reversível.", "deploymentProcessDescription": "Isso adicionará com segurança a chave pública ao arquivo ~/.ssh/authorized_keys do host de destino sem sobrescrever chaves existentes. A operação é reversível.",
"chooseHostToDeploy": "Escolha um host para implantar...", "chooseHostToDeploy": "Escolha um host para implantar...",
@@ -118,7 +117,6 @@
"credentialSecuredDescription": "Todos os dados sensíveis são criptografados com AES-256", "credentialSecuredDescription": "Todos os dados sensíveis são criptografados com AES-256",
"passwordAuthentication": "Autenticação por senha", "passwordAuthentication": "Autenticação por senha",
"keyAuthentication": "Autenticação por chave", "keyAuthentication": "Autenticação por chave",
"keyType": "Tipo de chave",
"securityReminder": "Lembrete de segurança", "securityReminder": "Lembrete de segurança",
"securityReminderText": "Nunca compartilhe suas credenciais. Todos os dados são criptografados em repouso.", "securityReminderText": "Nunca compartilhe suas credenciais. Todos os dados são criptografados em repouso.",
"hostsUsingCredential": "Hosts usando esta credencial", "hostsUsingCredential": "Hosts usando esta credencial",
@@ -166,7 +164,8 @@
"generateKeyPairNote": "Gere um novo par de chaves SSH diretamente. Isso substituirá quaisquer chaves existentes no formulário.", "generateKeyPairNote": "Gere um novo par de chaves SSH diretamente. Isso substituirá quaisquer chaves existentes no formulário.",
"invalidKey": "Chave inválida", "invalidKey": "Chave inválida",
"detectionError": "Erro de detecção", "detectionError": "Erro de detecção",
"unknown": "Desconhecido" "unknown": "Desconhecido",
"credentialId": "Credencial ID"
}, },
"dragIndicator": { "dragIndicator": {
"error": "Erro: {{error}}", "error": "Erro: {{error}}",
@@ -201,7 +200,7 @@
"noResults": "Nenhum comando encontrado", "noResults": "Nenhum comando encontrado",
"noResultsHint": "Nenhum comando correspondente a \"{{query}}\"", "noResultsHint": "Nenhum comando correspondente a \"{{query}}\"",
"deleteSuccess": "Comando removido do histórico", "deleteSuccess": "Comando removido do histórico",
"deleteFailed": "Falha ao excluir comando.", "deleteFailed": "Falha ao excluir comeo.",
"deleteTooltip": "Excluir comando", "deleteTooltip": "Excluir comando",
"tabHint": "Use Tab no Terminal para autocompletar do histórico de comandos" "tabHint": "Use Tab no Terminal para autocompletar do histórico de comandos"
}, },
@@ -219,21 +218,25 @@
"testConnectionFirst": "Por favor, teste a conexão primeiro", "testConnectionFirst": "Por favor, teste a conexão primeiro",
"connectionSuccess": "Conexão bem-sucedida!", "connectionSuccess": "Conexão bem-sucedida!",
"connectionFailed": "Conexão falhou", "connectionFailed": "Conexão falhou",
"connectionError": "Ocorreu um erro de conexão", "connectionError": "Ocoureu um erro de conexão",
"connected": "Conectado", "connected": "Conectado",
"disconnected": "Desconectado", "disconnected": "Desconectado",
"configSaved": "Configuração salva com sucesso", "configSaved": "Configuração salva com sucesso",
"saveFailed": "Falha ao salvar configuração", "saveFailed": "Falha ao salvar configuração",
"saveError": "Erro ao salvar configuração", "saveError": "Erro ao salvar configuração",
"saving": "Salvando...", "saving": "Salveo...",
"saveConfig": "Salvar Configuração", "saveConfig": "Salvar Configuração",
"helpText": "Digite a URL onde seu servidor Termix está rodando (ex.: http://localhost:30001 ou https://seu-servidor.com)" "helpText": "Digite a URL onde seu servidor Termix está rodando (ex.: http://localhost:30001 ou https://seu-servidor.com)",
"changeServer": "Alterar Servidor",
"mustIncludeProtocol": "URL do Servidor deve começar com http:// ou https://",
"notValidatedWarning": "URL não validada - verifique se está correta",
"warning": "Aviso"
}, },
"versionCheck": { "versionCheck": {
"error": "Erro na verificação de versão", "error": "Erro na verificação de versão",
"checkFailed": "Falha ao verificar atualizações", "checkFailed": "Falha ao verificar atualizações",
"upToDate": "Aplicativo atualizado", "upToDate": "Aplicativo atualizado",
"currentVersion": "Você está usando a versão {{version}}", "currentVersion": "Você está useo a versão {{version}}",
"updateAvailable": "Atualização disponível", "updateAvailable": "Atualização disponível",
"newVersionAvailable": "Uma nova versão está disponível! Você está usando {{current}}, mas {{latest}} está disponível.", "newVersionAvailable": "Uma nova versão está disponível! Você está usando {{current}}, mas {{latest}} está disponível.",
"releasedOn": "Lançada em {{date}}", "releasedOn": "Lançada em {{date}}",
@@ -255,12 +258,12 @@
"continue": "Continuar", "continue": "Continuar",
"maintenance": "Manutenção", "maintenance": "Manutenção",
"degraded": "Degradado", "degraded": "Degradado",
"discord": "Discord", "discord": "Discoud",
"error": "Erro", "error": "Erro",
"warning": "Aviso", "warning": "Aviso",
"info": "Info", "info": "Info",
"success": "Sucesso", "success": "Sucesso",
"loading": "Carregando", "loading": "Carregando...",
"required": "Obrigatório", "required": "Obrigatório",
"optional": "Opcional", "optional": "Opcional",
"clear": "Limpar", "clear": "Limpar",
@@ -274,14 +277,13 @@
"updateAvailable": "Atualização Disponível", "updateAvailable": "Atualização Disponível",
"sshPath": "Caminho SSH", "sshPath": "Caminho SSH",
"localPath": "Caminho Local", "localPath": "Caminho Local",
"loading": "Carregando...",
"noAuthCredentials": "Não há credenciais de autenticação disponíveis para este host SSH", "noAuthCredentials": "Não há credenciais de autenticação disponíveis para este host SSH",
"noReleases": "Sem Versões", "noReleases": "Sem Versões",
"updatesAndReleases": "Atualizações e Versões", "updatesAndReleases": "Atualizações e Versões",
"newVersionAvailable": "Uma nova versão ({{version}}) está disponível.", "newVersionAvailable": "Uma nova versão ({{version}}) está disponível.",
"failedToFetchUpdateInfo": "Falha ao buscar informações de atualização", "failedToFetchUpdateInfo": "Falha ao buscar informações de atualização",
"preRelease": "Pré-lançamento", "preRelease": "Pré-lançamento",
"loginFailed": "Falha no login", "loginFailed": "Falha nenhum login",
"noReleasesFound": "Nenhuma versão encontrada.", "noReleasesFound": "Nenhuma versão encontrada.",
"yourBackupCodes": "Seus Códigos de Backup", "yourBackupCodes": "Seus Códigos de Backup",
"sendResetCode": "Enviar Código de Redefinição", "sendResetCode": "Enviar Código de Redefinição",
@@ -289,13 +291,10 @@
"resetPassword": "Redefinir Senha", "resetPassword": "Redefinir Senha",
"resetCode": "Código de Redefinição", "resetCode": "Código de Redefinição",
"newPassword": "Nova Senha", "newPassword": "Nova Senha",
"sshPath": "Caminho SSH",
"localPath": "Caminho Local",
"folder": "Pasta", "folder": "Pasta",
"file": "Arquivo", "file": "Arquivo",
"renamedSuccessfully": "renomeado com sucesso", "renamedSuccessfully": "renomeado com sucesso",
"deletedSuccessfully": "excluído com sucesso", "deletedSuccessfully": "excluído com sucesso",
"noAuthCredentials": "Não há credenciais de autenticação disponíveis para este host SSH",
"noTunnelConnections": "Não há conexões de túnel configuradas", "noTunnelConnections": "Não há conexões de túnel configuradas",
"sshTools": "Ferramentas SSH", "sshTools": "Ferramentas SSH",
"english": "Inglês", "english": "Inglês",
@@ -307,36 +306,27 @@
"login": "Entrar", "login": "Entrar",
"logout": "Sair", "logout": "Sair",
"register": "Registrar", "register": "Registrar",
"username": "Usuário",
"password": "Senha", "password": "Senha",
"version": "Versão", "version": "Versão",
"confirmPassword": "Confirmar Senha", "confirmPassword": "Confirmar Senha",
"back": "Voltar", "back": "Voltar",
"email": "Email", "email": "Email",
"submit": "Enviar", "submit": "Enviar",
"cancel": "Cancelar",
"change": "Alterar", "change": "Alterar",
"save": "Salvar", "save": "Salvar",
"delete": "Excluir", "delete": "Excluir",
"edit": "Editar", "edit": "Editar",
"add": "Adicionar", "add": "Adicionar",
"search": "Buscar", "search": "Buscar",
"loading": "Carregando...",
"error": "Erro",
"success": "Sucesso",
"warning": "Aviso",
"info": "Info",
"confirm": "Confirmar", "confirm": "Confirmar",
"yes": "Sim", "yes": "Sim",
"no": "Não", "no": "Não",
"ok": "OK", "ok": "OK",
"close": "Fechar",
"enabled": "Habilitado", "enabled": "Habilitado",
"disabled": "Desabilitado", "disabled": "Desabilitado",
"important": "Importante", "important": "Importante",
"notEnabled": "Não Habilitado", "notEnabled": "Não Habilitado",
"settingUp": "Configurando...", "settingUp": "Configurando...",
"back": "Voltar",
"next": "Próximo", "next": "Próximo",
"previous": "Anterior", "previous": "Anterior",
"refresh": "Atualizar", "refresh": "Atualizar",
@@ -353,7 +343,7 @@
"passwordResetDescription": "Você está prestes a redefinir sua senha. Isso fará com que você seja desconectado de todas as sessões ativas.", "passwordResetDescription": "Você está prestes a redefinir sua senha. Isso fará com que você seja desconectado de todas as sessões ativas.",
"enterSixDigitCode": "Digite o código de 6 dígitos dos logs do container docker para o usuário:", "enterSixDigitCode": "Digite o código de 6 dígitos dos logs do container docker para o usuário:",
"enterNewPassword": "Digite sua nova senha para o usuário:", "enterNewPassword": "Digite sua nova senha para o usuário:",
"passwordsDoNotMatch": "As senhas não correspondem", "passwordsDoNotMatch": "As senhas não courespondem",
"passwordMinLength": "A senha deve ter pelo menos 6 caracteres", "passwordMinLength": "A senha deve ter pelo menos 6 caracteres",
"passwordResetSuccess": "Senha redefinida com sucesso! Você pode agora entrar com sua nova senha.", "passwordResetSuccess": "Senha redefinida com sucesso! Você pode agora entrar com sua nova senha.",
"failedToInitiatePasswordReset": "Falha ao iniciar redefinição de senha", "failedToInitiatePasswordReset": "Falha ao iniciar redefinição de senha",
@@ -362,7 +352,8 @@
"documentation": "Documentação", "documentation": "Documentação",
"retry": "Tentar Novamente", "retry": "Tentar Novamente",
"checking": "Verificando...", "checking": "Verificando...",
"checkingDatabase": "Verificando conexão com o banco de dados..." "checkingDatabase": "Verificando conexão com o banco de dados...",
"saving": "Salveo..."
}, },
"nav": { "nav": {
"home": "Início", "home": "Início",
@@ -372,7 +363,7 @@
"tunnels": "Túneis", "tunnels": "Túneis",
"fileManager": "Gerenciador de Arquivos", "fileManager": "Gerenciador de Arquivos",
"serverStats": "Estatísticas do Servidor", "serverStats": "Estatísticas do Servidor",
"admin": "Admin", "admin": "Administrador",
"userProfile": "Perfil do Usuário", "userProfile": "Perfil do Usuário",
"tools": "Ferramentas", "tools": "Ferramentas",
"newTab": "Nova Aba", "newTab": "Nova Aba",
@@ -381,15 +372,16 @@
"sshManager": "Gerenciador SSH", "sshManager": "Gerenciador SSH",
"hostManager": "Gerenciador de Hosts", "hostManager": "Gerenciador de Hosts",
"cannotSplitTab": "Não é possível dividir esta aba", "cannotSplitTab": "Não é possível dividir esta aba",
"tabNavigation": "Navegação de Abas" "tabNavigation": "Navegação de Abas",
"snippets": "Snippets"
}, },
"admin": { "admin": {
"title": "Configurações de Admin", "title": "Configurações de Administrador",
"oidc": "OIDC", "oidc": "OIDC",
"users": "Usuários", "users": "Usuários",
"userManagement": "Gerenciamento de Usuários", "userManagement": "Gerenciamento de Usuários",
"makeAdmin": "Tornar Admin", "makeAdmin": "Tornar Administrador",
"removeAdmin": "Remover Admin", "removeAdmin": "Remover Administrador",
"deleteUser": "Excluir Usuário", "deleteUser": "Excluir Usuário",
"allowRegistration": "Permitir Registro", "allowRegistration": "Permitir Registro",
"oidcSettings": "Configurações OIDC", "oidcSettings": "Configurações OIDC",
@@ -400,11 +392,11 @@
"tokenUrl": "URL do Token", "tokenUrl": "URL do Token",
"updateSettings": "Atualizar Configurações", "updateSettings": "Atualizar Configurações",
"confirmDelete": "Tem certeza que deseja excluir este usuário?", "confirmDelete": "Tem certeza que deseja excluir este usuário?",
"confirmMakeAdmin": "Tem certeza que deseja tornar este usuário um admin?", "confirmMakeAdmin": "Tem certeza que deseja tornar este usuário um administrador?",
"confirmRemoveAdmin": "Tem certeza que deseja remover os privilégios de admin deste usuário?", "confirmRemoveAdmin": "Tem certeza que deseja remover os privilégios de administrador deste usuário?",
"externalAuthentication": "Autenticação Externa (OIDC)", "externalAuthentication": "Autenticação Externa (OIDC)",
"configureExternalProvider": "Configure o provedor de identidade externo para autenticação OIDC/OAuth2.", "configureExternalProvider": "Configure o provedor de identidade externo para autenticação OIDC/OAuth2.",
"userIdentifierPath": "Caminho do Identificador do Usuário", "userIdentifierPath": "Caminho do Identificadou do Usuário",
"displayNamePath": "Caminho do Nome de Exibição", "displayNamePath": "Caminho do Nome de Exibição",
"scopes": "Escopos", "scopes": "Escopos",
"saving": "Salvando...", "saving": "Salvando...",
@@ -419,12 +411,12 @@
"actions": "Ações", "actions": "Ações",
"external": "Externo", "external": "Externo",
"local": "Local", "local": "Local",
"adminManagement": "Gerenciamento de Admin", "adminManagement": "Gerenciamento de Administrador",
"makeUserAdmin": "Tornar Usuário Admin", "makeUserAdmin": "Tornar Usuário Administrador",
"adding": "Adicionando...", "adding": "Adicionando...",
"currentAdmins": "Admins Atuais", "currentAdmins": "Administradores Atuais",
"adminBadge": "Admin", "adminBadge": "Administrador",
"removeAdminButton": "Remover Admin", "removeAdminButton": "Remover Administrador",
"general": "Geral", "general": "Geral",
"userRegistration": "Registro de Usuário", "userRegistration": "Registro de Usuário",
"allowNewAccountRegistration": "Permitir registro de novas contas", "allowNewAccountRegistration": "Permitir registro de novas contas",
@@ -436,13 +428,12 @@
"oidcConfigurationDisabled": "Configuração OIDC desativada com sucesso!", "oidcConfigurationDisabled": "Configuração OIDC desativada com sucesso!",
"failedToUpdateOidcConfig": "Falha ao atualizar configuração OIDC", "failedToUpdateOidcConfig": "Falha ao atualizar configuração OIDC",
"failedToDisableOidcConfig": "Falha ao desativar configuração OIDC", "failedToDisableOidcConfig": "Falha ao desativar configuração OIDC",
"enterUsernameToMakeAdmin": "Insira o nome de usuário para tornar admin", "enterUsernameToMakeAdmin": "Insira o nome de usuário para tornar administrador",
"userIsNowAdmin": "O usuário {{username}} agora é um administrador", "userIsNowAdmin": "O usuário {{username}} agora é um administrador",
"failedToMakeUserAdmin": "Falha ao tornar o usuário administrador", "failedToMakeUserAdmin": "Falha ao tornar o usuário administrador",
"removeAdminStatus": "Remover status de administrador de {{username}}?", "removeAdminStatus": "Remover status de administrador de {{username}}?",
"adminStatusRemoved": "Status de administrador removido de {{username}}", "adminStatusRemoved": "Status de administrador removido de {{username}}",
"failedToRemoveAdminStatus": "Falha ao remover o status de administrador", "failedToRemoveAdminStatus": "Falha ao remover o status de administrador",
"confirmDeleteUser": "Excluir usuário {{username}}? Esta ação não pode ser desfeita.",
"userDeletedSuccessfully": "Usuário {{username}} excluído com sucesso", "userDeletedSuccessfully": "Usuário {{username}} excluído com sucesso",
"failedToDeleteUser": "Falha ao excluir usuário", "failedToDeleteUser": "Falha ao excluir usuário",
"overrideUserInfoUrl": "Sobrescrever URL de informações do usuário (não obrigatório)", "overrideUserInfoUrl": "Sobrescrever URL de informações do usuário (não obrigatório)",
@@ -495,7 +486,6 @@
"verificationCompleted": "Verificação de compatibilidade concluída - nenhum dado foi alterado", "verificationCompleted": "Verificação de compatibilidade concluída - nenhum dado foi alterado",
"verificationInProgress": "Verificação concluída", "verificationInProgress": "Verificação concluída",
"dataMigrationCompleted": "Migração de dados concluída com sucesso!", "dataMigrationCompleted": "Migração de dados concluída com sucesso!",
"migrationCompleted": "Migração concluída",
"verificationFailed": "Falha na verificação de compatibilidade", "verificationFailed": "Falha na verificação de compatibilidade",
"migrationFailed": "Falha na migração", "migrationFailed": "Falha na migração",
"runningVerification": "Executando verificação de compatibilidade...", "runningVerification": "Executando verificação de compatibilidade...",
@@ -540,13 +530,13 @@
"databaseImportFailed": "Falha na importação do banco de dados SQLite", "databaseImportFailed": "Falha na importação do banco de dados SQLite",
"manageEncryptionAndBackups": "Gerenciar chaves de criptografia, segurança do banco de dados e operações de backup", "manageEncryptionAndBackups": "Gerenciar chaves de criptografia, segurança do banco de dados e operações de backup",
"activeSecurityFeatures": "Medidas e proteções de segurança atualmente ativas", "activeSecurityFeatures": "Medidas e proteções de segurança atualmente ativas",
"deviceBindingTechnology": "Tecnologia avançada de proteção de chave baseada em hardware", "deviceBindingTechnology": "Tecnenhumlogia avançada de proteção de chave baseada em hardware",
"backupAndRecovery": "Opções seguras de criação de backup e recuperação de banco de dados", "backupAndRecovery": "Opções seguras de criação de backup e recuperação de banco de dados",
"crossSystemDataTransfer": "Exportar e importar bancos de dados entre diferentes sistemas", "crossSystemDataTransfer": "Exportar e importar bancos de dados entre diferentes sistemas",
"noMigrationNeeded": "Nenhuma migração necessária", "noMigrationNeeded": "Nenhuma migração necessária",
"encryptionKey": "Chave de Criptografia", "encryptionKey": "Chave de Criptografia",
"keyProtection": "Proteção da Chave", "keyProtection": "Proteção da Chave",
"active": "Ativo", "active": "Ativa",
"legacy": "Legado", "legacy": "Legado",
"dataStatus": "Status dos Dados", "dataStatus": "Status dos Dados",
"encrypted": "Criptografado", "encrypted": "Criptografado",
@@ -576,13 +566,38 @@
"confirmDisableOIDCWarning": "AVISO: Você está prestes a desativar o OIDC enquanto o login por senha também está desativado. Isso inutilizará sua instância do Termix e você perderá todo o acesso. Tem absoluta certeza de que deseja continuar?", "confirmDisableOIDCWarning": "AVISO: Você está prestes a desativar o OIDC enquanto o login por senha também está desativado. Isso inutilizará sua instância do Termix e você perderá todo o acesso. Tem absoluta certeza de que deseja continuar?",
"allowPasswordLogin": "Permitir login com nome de usuário/senha", "allowPasswordLogin": "Permitir login com nome de usuário/senha",
"failedToFetchPasswordLoginStatus": "Falha ao buscar status do login por senha", "failedToFetchPasswordLoginStatus": "Falha ao buscar status do login por senha",
"failedToUpdatePasswordLoginStatus": "Falha ao atualizar status do login por senha" "failedToUpdatePasswordLoginStatus": "Falha ao atualizar status do login por senha",
"accountsLinkedSuccessfully": "Usuário OIDC {{oidcUsername}} foi vinculado a {{targetUsername}}",
"confirmRevokeAllSessions": "Tem certeza de que deseja revogar todas as sessões para este usuário?",
"confirmRevokeSession": "Tem certeza de que deseja revogar esta sessão?",
"failedToFetchSessions": "Falha ao buscar sessões",
"failedToLinkAccounts": "Falha ao vincular contas",
"failedToRevokeSession": "Falha ao revogar sessão",
"failedToRevokeSessions": "Falha ao revogar sessões",
"failedToUnlinkOIDC": "Falha ao desvincular OIDC",
"linkAccountsButton": "Vincular Contas",
"linkOIDCActionAddCapability": "Adicionar capacidade de login OIDC à conta de senha de destino",
"linkOIDCActionDeleteUser": "Excluir a conta de usuário OIDC e todos os outros dados",
"linkOIDCActionDualAuth": "Permitir que a conta de senha faça login com senha e OIDC",
"linkOIDCDialogDescription": "Vincular {{username}} (usuário OIDC) a uma conta de senha existente. Isso ativará autenticação dupla para a conta de senha.",
"linkOIDCDialogTitle": "Link OIDC Account para Senha Account",
"linkOIDCWarningTitle": "Aviso: Dados do Usuário OIDC Serão Deletados",
"linkTargetUsernameLabel": "Nome de usuário da conta de senha de destino",
"linkTargetUsernamePlaceholder": "Inserir nome de usuário da conta de senha",
"linkTargetUsernameRequired": "Nome de usuário de destino é obrigatório",
"linkToPasswordAccount": "Vincular à Conta de Senha",
"linkingAccounts": "Vinculando...",
"sessionRevokedSuccessfully": "Sessão revogada com sucesso",
"sessionsRevokedSuccessfully": "Sessões revogadas com sucesso",
"unlinkOIDCDescription": "Remover autenticação OIDC de {{username}}? O usuário só poderá fazer login com nome de usuário/senha após isso.",
"unlinkOIDCSuccess": "OIDC desvinculado de {{username}}",
"unlinkOIDCTitle": "Desvincular Autenticação OIDC"
}, },
"hosts": { "hosts": {
"title": "Gerenciador de Hosts", "title": "Gerenciador de Hosts",
"sshHosts": "Hosts SSH", "sshHosts": "Hosts SSH",
"noHosts": "Sem Hosts SSH", "noHosts": "Sem Hosts SSH",
"noHostsMessage": "Você ainda não adicionou nenhum host SSH. Clique em \"Adicionar Host\" para começar.", "noHostsMessage": "Você ainda não adicionenhumu nenhum host SSH. Clique em \"Adicionar Host\" para começar.",
"loadingHosts": "Carregando hosts...", "loadingHosts": "Carregando hosts...",
"failedToLoadHosts": "Falha ao carregar hosts", "failedToLoadHosts": "Falha ao carregar hosts",
"retry": "Tentar Novamente", "retry": "Tentar Novamente",
@@ -596,12 +611,12 @@
"formatGuide": "Guia de Formato", "formatGuide": "Guia de Formato",
"exportCredentialWarning": "Aviso: O host \"{{name}}\" usa autenticação por credencial. O arquivo exportado não incluirá os dados da credencial e precisará ser reconfigurado manualmente após a importação. Deseja continuar?", "exportCredentialWarning": "Aviso: O host \"{{name}}\" usa autenticação por credencial. O arquivo exportado não incluirá os dados da credencial e precisará ser reconfigurado manualmente após a importação. Deseja continuar?",
"exportSensitiveDataWarning": "Aviso: O host \"{{name}}\" contém dados de autenticação sensíveis (senha/chave SSH). O arquivo exportado incluirá esses dados em texto simples. Mantenha o arquivo seguro e exclua-o após o uso. Deseja continuar?", "exportSensitiveDataWarning": "Aviso: O host \"{{name}}\" contém dados de autenticação sensíveis (senha/chave SSH). O arquivo exportado incluirá esses dados em texto simples. Mantenha o arquivo seguro e exclua-o após o uso. Deseja continuar?",
"uncategorized": "Sem categoria", "uncategorized": "Sem categouia",
"confirmDelete": "Tem certeza que deseja excluir \"{{name}}\"?", "confirmDelete": "Tem certeza que deseja excluir \"{{name}}\"?",
"failedToDeleteHost": "Falha ao excluir host", "failedToDeleteHost": "Falha ao excluir host",
"failedToExportHost": "Falha ao exportar host. Certifique-se de que está logado e tem acesso aos dados do host.", "failedToExportHost": "Falha ao exportar host. Certifique-se de que está logado e tem acesso aos dados do host.",
"jsonMustContainHosts": "O JSON deve conter um array \"hosts\" ou ser um array de hosts", "jsonMustContainHosts": "O JSON deve conter um array \"hosts\" ou ser um array de hosts",
"noHostsInJson": "Nenhum host encontrado no arquivo JSON", "noHostsInJson": "Nenhum host encontrado nenhum arquivo JSON",
"maxHostsAllowed": "Máximo de 100 hosts permitidos por importação", "maxHostsAllowed": "Máximo de 100 hosts permitidos por importação",
"importCompleted": "Importação concluída: {{success}} com sucesso, {{failed}} falhas", "importCompleted": "Importação concluída: {{success}} com sucesso, {{failed}} falhas",
"importFailed": "Falha na importação", "importFailed": "Falha na importação",
@@ -611,7 +626,7 @@
"organization": "Organização", "organization": "Organização",
"ipAddress": "Endereço IP", "ipAddress": "Endereço IP",
"port": "Porta", "port": "Porta",
"name": "Nome", "name": "Nenhumme",
"username": "Usuário", "username": "Usuário",
"folder": "Pasta", "folder": "Pasta",
"tags": "Tags", "tags": "Tags",
@@ -632,13 +647,13 @@
"enableTerminalDesc": "Habilitar/desabilitar visibilidade do host na aba Terminal", "enableTerminalDesc": "Habilitar/desabilitar visibilidade do host na aba Terminal",
"enableTunnel": "Habilitar Túnel", "enableTunnel": "Habilitar Túnel",
"enableTunnelDesc": "Habilitar/desabilitar visibilidade do host na aba Túnel", "enableTunnelDesc": "Habilitar/desabilitar visibilidade do host na aba Túnel",
"enableFileManager": "Habilitar Gerenciador de Arquivos", "enableFileManager": "Habilitar Gerenciadou de Arquivos",
"enableFileManagerDesc": "Habilitar/desabilitar visibilidade do host na aba Gerenciador de Arquivos", "enableFileManagerDesc": "Habilitar/desabilitar visibilidade do host na aba Gerenciadou de Arquivos",
"defaultPath": "Caminho Padrão", "defaultPath": "Caminho Padrão",
"defaultPathDesc": "Diretório padrão ao abrir o gerenciador de arquivos para este host", "defaultPathDesc": "Diretório padrão ao abrir o gerenciadou de arquivos para este host",
"tunnelConnections": "Conexões de Túnel", "tunnelConnections": "Conexões de Túnel",
"connection": "Conexão", "connection": "Conexão",
"remove": "Remover", "remove": "Removerr",
"sourcePort": "Porta de Origem", "sourcePort": "Porta de Origem",
"sourcePortDesc": "(Source refere-se aos Detalhes da Conexão Atual na aba Geral)", "sourcePortDesc": "(Source refere-se aos Detalhes da Conexão Atual na aba Geral)",
"endpointPort": "Porta de Destino", "endpointPort": "Porta de Destino",
@@ -695,12 +710,12 @@
"addTagsSpaceToAdd": "adicionar tags (espaço para adicionar)", "addTagsSpaceToAdd": "adicionar tags (espaço para adicionar)",
"terminalBadge": "Terminal", "terminalBadge": "Terminal",
"tunnelBadge": "Túnel", "tunnelBadge": "Túnel",
"fileManagerBadge": "Gerenciador de Arquivos", "fileManagerBadge": "Gerenciadou de Arquivos",
"general": "Geral", "general": "Geral",
"terminal": "Terminal", "terminal": "Terminal",
"tunnel": "Túnel", "tunnel": "Túnel",
"fileManager": "Gerenciador de Arquivos", "fileManager": "Gerenciadou de Arquivos",
"hostViewer": "Visualizador de Host", "hostViewer": "Visualizadou de Host",
"confirmRemoveFromFolder": "Tem certeza que deseja remover \"{{name}}\" da pasta \"{{folder}}\"? O host será movido para \"Sem Pasta\".", "confirmRemoveFromFolder": "Tem certeza que deseja remover \"{{name}}\" da pasta \"{{folder}}\"? O host será movido para \"Sem Pasta\".",
"removedFromFolder": "Host \"{{name}}\" removido da pasta com sucesso", "removedFromFolder": "Host \"{{name}}\" removido da pasta com sucesso",
"failedToRemoveFromFolder": "Falha ao remover host da pasta", "failedToRemoveFromFolder": "Falha ao remover host da pasta",
@@ -745,7 +760,105 @@
"searchServers": "Pesquisar servidores...", "searchServers": "Pesquisar servidores...",
"noServerFound": "Nenhum servidor encontrado", "noServerFound": "Nenhum servidor encontrado",
"jumpHostsOrder": "As conexões serão feitas na ordem: Host de Salto 1 → Host de Salto 2 → ... → Servidor de Destino", "jumpHostsOrder": "As conexões serão feitas na ordem: Host de Salto 1 → Host de Salto 2 → ... → Servidor de Destino",
"advancedAuthSettings": "Configurações Avançadas de Autenticação" "advancedAuthSettings": "Configurações Avançadas de Autenticação",
"addQuickAction": "Adicionar Ação Rápida",
"adjustFontSize": "Ajustar o tamanho da fonte do terminal",
"adjustLetterSpacing": "Ajustar espaçamento entre caracteres",
"adjustLineHeight": "Ajustar espaçamento entre linhas",
"advanced": "Avançado",
"allHostsInFolderDeleted": "Excluídos {{count}} hosts da pasta \"{{folder}}\" com sucesso",
"appearance": "Aparência",
"backspaceMode": "Modo Backspace",
"backspaceModeControlH": "Control-H (^H)",
"backspaceModeDesc": "Comportamento da tecla Backspace para compatibilidade",
"backspaceModeNormal": "Normal (DEL)",
"behavior": "Comportamento",
"bellStyle": "Estilo de Campainha",
"bellStyleBoth": "Both",
"bellStyleDesc": "Como lidar com a campainha do terminal (caractere BEL, \\x07). Programas acionam isso ao completar tarefas, encontrar erros, ou para notificações. \"Som\" toca um bipe de áudio, \"Visual\" pisca a tela brevemente, \"Ambos\" faz os dois, \"Nenhum\" desativa os alertas de campainha.",
"bellStyleNone": "Nenhum",
"bellStyleSound": "Som",
"bellStyleVisual": "Visual",
"chooseColorTheme": "Escolha um tema de cor para o terminal",
"chooseCursorAppearance": "Escolha a aparência do cursor",
"confirmDeleteAllHostsInFolder": "Tem certeza de que deseja excluir todos os {{count}} hosts na pasta \"{{folder}}\"? Esta ação não pode ser desfeita.",
"cursorBlink": "Piscar Cursor",
"cursorStyle": "Estilo do Cursor",
"cursorStyleBar": "Barra",
"cursorStyleBlock": "Bloco",
"cursorStyleUnderline": "Sublinhado",
"customCommands": "Comandos Personalizados (Em Breve)",
"customCommandsDesc": "Defina comandos personalizados de desligamento e reinicialização para este servidor",
"deleteAllHostsInFolder": "Excluir Todos os Hosts na Pasta",
"displayItems": "Exibir Itens",
"displayItemsDesc": "Escolha quais métricas exibir na página de estatísticas do servidor",
"editFolderAppearance": "Editar Aparência da Pasta",
"editFolderAppearanceDesc": "Personalize a cor e o ícone da pasta",
"enableCpu": "Uso da CPU",
"enableCursorBlink": "Ativar animação de piscar do cursor",
"enableDisk": "Uso de Disco",
"enableHostname": "Hostname (Em Breve)",
"enableMemory": "Uso de Memória",
"enableNetwork": "Estatísticas de Rede (Em Breve)",
"enableOs": "Sistema Operacional (Em Breve)",
"enableProcesses": "Contagem de Processos (Em Breve)",
"enableServerStats": "Ativar Estatísticas do Servidor",
"enableServerStatsDesc": "Ativar/desativar coleta de estatísticas do servidor para este host",
"enableUptime": "Tempo de Atividade (Em Breve)",
"failedToDeleteHostsInFolder": "Falha ao excluir hosts na pasta",
"failedToUpdateFolderAppearance": "Falha ao atualizar aparência da pasta",
"fastScrollModifier": "Modificador de Rolagem Rápida",
"fastScrollModifierDesc": "Tecla modificadora para rolagem rápida",
"fastScrollSensitivity": "Sensibilidade de Rolagem Rápida",
"fastScrollSensitivityDesc": "Multiplicador de velocidade de rolagem quando modificador é segurado",
"fastScrollSensitivityValue": "Sensibilidade de Rolagem Rápida: {{value}}",
"folderAppearanceUpdated": "Aparência da pasta atualizada com sucesso",
"folderColor": "Cor da Pasta",
"folderIcon": "Ícone da Pasta",
"fontFamily": "Família da Fonte",
"fontSize": "Tamanho da Fonte",
"fontSizeValue": "Tamanho da Fonte: {{value}}px",
"letterSpacing": "Espaçamento entre Letras",
"letterSpacingValue": "Espaçamento entre Letras: {{value}}px",
"lineHeight": "Altura da Linha",
"lineHeightValue": "Altura da Linha: {{value}}",
"minimumContrastRatio": "Taxa de Contraste Mínima",
"minimumContrastRatioDesc": "Ajustar cores automaticamente para melhor legibilidade",
"minimumContrastRatioValue": "Taxa de Contraste Mínima: {{value}}",
"modifierAlt": "Alt",
"modifierCtrl": "Ctrl",
"modifierShift": "Shift",
"noSnippetFound": "Nenhum snippet encontrado",
"preview": "Pré-visualização",
"quickActionName": "Nome da Ação",
"quickActions": "Ações Rápidas",
"quickActionsDescription": "Ações rápidas permitem criar botões personalizados que executam snippets SSH neste servidor. Esses botões aparecerão no topo da página de Estatísticas do Servidor para acesso rápido.",
"quickActionsList": "Lista de Ações Rápidas",
"quickActionsOrder": "Botões de ação rápida aparecerão na ordem listada acima na página de Estatísticas do Servidor",
"rebootCommand": "Comando de Reinicialização",
"rightClickSelectsWord": "Clique Direito Seleciona Palavra",
"rightClickSelectsWordDesc": "Clicar com o botão direito seleciona a palavra sob o cursor",
"scrollbackBuffer": "Histórico de Rolagem",
"scrollbackBufferDesc": "Número de linhas para manter no histórico de rolagem",
"scrollbackBufferValue": "Histórico de Rolagem: {{value}} linhas",
"searchSnippets": "Pesquisar snippets...",
"selectBackspaceMode": "Selecionar modo backspace",
"selectBellStyle": "Selecionar estilo de campainha",
"selectCursorStyle": "Selecionar estilo de cursor",
"selectFont": "Selecionar fonte",
"selectFontDesc": "Selecione a fonte para usar no terminal",
"selectModifier": "Selecionar modificador",
"selectSnippet": "Selecionar snippet",
"selectTheme": "Selecionar tema",
"serverStats": "Estatísticas do Servidor",
"shutdownCommand": "Comando de Desligamento",
"snippetNone": "Nenhum",
"sshAgentForwarding": "Encaminhamento de Agente SSH",
"sshAgentForwardingDesc": "Encaminhar agente de autenticação SSH para host remoto",
"startupSnippet": "Snippet de Inicialização",
"terminalCustomization": "Personalização do Terminal",
"theme": "Tema",
"themePreview": "Pré-visualização do Tema"
}, },
"terminal": { "terminal": {
"title": "Terminal", "title": "Terminal",
@@ -779,7 +892,11 @@
"connectionTimeout": "Tempo limite de conexão esgotado", "connectionTimeout": "Tempo limite de conexão esgotado",
"terminalTitle": "Terminal - {{host}}", "terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}", "terminalWithPath": "Terminal - {{host}}:{{path}}",
"runTitle": "Executando {{command}} - {{host}}" "runTitle": "Executando {{command}} - {{host}}",
"totpCodeLabel": "Código de Verificação",
"totpPlaceholder": "000000",
"totpRequired": "Autenticação de Dois Fatores Obrigatória",
"totpVerify": "Verificar"
}, },
"fileManager": { "fileManager": {
"title": "Gerenciador de Arquivos", "title": "Gerenciador de Arquivos",
@@ -865,7 +982,6 @@
"copyPaths": "Copiar caminhos", "copyPaths": "Copiar caminhos",
"delete": "Excluir", "delete": "Excluir",
"properties": "Propriedades", "properties": "Propriedades",
"preview": "Visualizar",
"refresh": "Atualizar", "refresh": "Atualizar",
"downloadFiles": "Baixar {{count}} arquivos para o Navegador", "downloadFiles": "Baixar {{count}} arquivos para o Navegador",
"copyFiles": "Copiar {{count}} itens", "copyFiles": "Copiar {{count}} itens",
@@ -880,18 +996,11 @@
"failedToDeleteItem": "Falha ao excluir item", "failedToDeleteItem": "Falha ao excluir item",
"itemRenamedSuccessfully": "{{type}} renomeado com sucesso", "itemRenamedSuccessfully": "{{type}} renomeado com sucesso",
"failedToRenameItem": "Falha ao renomear item", "failedToRenameItem": "Falha ao renomear item",
"upload": "Enviar",
"download": "Baixar", "download": "Baixar",
"newFile": "Novo Arquivo",
"newFolder": "Nova Pasta",
"rename": "Renomear",
"delete": "Excluir",
"permissions": "Permissões", "permissions": "Permissões",
"size": "Tamanho", "size": "Tamanho",
"modified": "Modificado", "modified": "Modificado",
"path": "Caminho", "path": "Caminho",
"fileName": "Nome do Arquivo",
"folderName": "Nome da Pasta",
"confirmDelete": "Tem certeza que deseja excluir {{name}}?", "confirmDelete": "Tem certeza que deseja excluir {{name}}?",
"uploadSuccess": "Arquivo enviado com sucesso", "uploadSuccess": "Arquivo enviado com sucesso",
"uploadFailed": "Falha ao enviar arquivo", "uploadFailed": "Falha ao enviar arquivo",
@@ -911,10 +1020,7 @@
"fileSavedSuccessfully": "Arquivo salvo com sucesso", "fileSavedSuccessfully": "Arquivo salvo com sucesso",
"saveTimeout": "Tempo limite da operação de salvamento esgotado. O arquivo pode ter sido salvo com sucesso, mas a operação demorou muito para ser concluída. Verifique os logs do Docker para confirmação.", "saveTimeout": "Tempo limite da operação de salvamento esgotado. O arquivo pode ter sido salvo com sucesso, mas a operação demorou muito para ser concluída. Verifique os logs do Docker para confirmação.",
"failedToSaveFile": "Falha ao salvar arquivo", "failedToSaveFile": "Falha ao salvar arquivo",
"folder": "Pasta",
"file": "Arquivo",
"deletedSuccessfully": "excluído com sucesso", "deletedSuccessfully": "excluído com sucesso",
"failedToDeleteItem": "Falha ao excluir item",
"connectToServer": "Conectar a um Servidor", "connectToServer": "Conectar a um Servidor",
"selectServerToEdit": "Selecione um servidor da barra lateral para começar a editar arquivos", "selectServerToEdit": "Selecione um servidor da barra lateral para começar a editar arquivos",
"fileOperations": "Operações de Arquivo", "fileOperations": "Operações de Arquivo",
@@ -943,7 +1049,7 @@
"sshReconnectionTimeout": "Tempo limite excedido na reconexão SSH", "sshReconnectionTimeout": "Tempo limite excedido na reconexão SSH",
"saveOperationTimeout": "Tempo limite excedido na operação de salvar", "saveOperationTimeout": "Tempo limite excedido na operação de salvar",
"cannotSaveFile": "Não é possível salvar o arquivo", "cannotSaveFile": "Não é possível salvar o arquivo",
"dragSystemFilesToUpload": "Arraste arquivos do sistema aqui para fazer upload", "dragSystemFilesToUpload": "Arraste arquivos do sistema aqui para enviar",
"dragFilesToWindowToDownload": "Arraste arquivos para fora da janela para baixar", "dragFilesToWindowToDownload": "Arraste arquivos para fora da janela para baixar",
"openTerminalHere": "Abrir Terminal Aqui", "openTerminalHere": "Abrir Terminal Aqui",
"run": "Executar", "run": "Executar",
@@ -971,14 +1077,12 @@
"unpinFile": "Desfixar arquivo", "unpinFile": "Desfixar arquivo",
"removeShortcut": "Remover atalho", "removeShortcut": "Remover atalho",
"saveFilesToSystem": "Salvar {{count}} arquivos como...", "saveFilesToSystem": "Salvar {{count}} arquivos como...",
"saveToSystem": "Salvar como...",
"pinFile": "Fixar arquivo", "pinFile": "Fixar arquivo",
"addToShortcuts": "Adicionar aos atalhos", "addToShortcuts": "Adicionar aos atalhos",
"selectLocationToSave": "Selecionar local para salvar",
"downloadToDefaultLocation": "Baixar para o local padrão", "downloadToDefaultLocation": "Baixar para o local padrão",
"pasteFailed": "Falha ao colar", "pasteFailed": "Falha ao colar",
"noUndoableActions": "Nenhuma ação pode ser desfeita", "noUndoableActions": "Nenhuma ação pode ser desfeita",
"undoCopySuccess": "Operação de cópia desfeita: {{count}} arquivos copiados foram excluídos", "undoCopySuccess": "oOperação de cópia desfeita: {{count}} arquivos copiados foram excluídos",
"undoCopyFailedDelete": "Falha ao desfazer: Não foi possível excluir os arquivos copiados", "undoCopyFailedDelete": "Falha ao desfazer: Não foi possível excluir os arquivos copiados",
"undoCopyFailedNoInfo": "Falha ao desfazer: Não foi possível encontrar informações do arquivo copiado", "undoCopyFailedNoInfo": "Falha ao desfazer: Não foi possível encontrar informações do arquivo copiado",
"undoMoveSuccess": "Operação de mover desfeita: {{count}} arquivos movidos de volta ao local original", "undoMoveSuccess": "Operação de mover desfeita: {{count}} arquivos movidos de volta ao local original",
@@ -989,10 +1093,9 @@
"undoOperationFailed": "Falha na operação de desfazer", "undoOperationFailed": "Falha na operação de desfazer",
"unknownError": "Erro desconhecido", "unknownError": "Erro desconhecido",
"enterPath": "Digite o caminho...", "enterPath": "Digite o caminho...",
"editPath": "Editar caminho", "editPath": "Editarar caminho",
"confirm": "Confirmar", "confirm": "Confirmar",
"cancel": "Cancelar", "cancel": "Cancelar",
"folderName": "Nome da pasta",
"find": "Localizar...", "find": "Localizar...",
"replaceWith": "Substituir por...", "replaceWith": "Substituir por...",
"replace": "Substituir", "replace": "Substituir",
@@ -1016,25 +1119,20 @@
"toggleComment": "Alternar Comentário", "toggleComment": "Alternar Comentário",
"indent": "Indentar", "indent": "Indentar",
"outdent": "Remover Indentação", "outdent": "Remover Indentação",
"autoComplete": "Auto Completar", "autoComplete": "Autocompletar",
"imageLoadError": "Falha ao carregar imagem", "imageLoadError": "Falha ao carregar imagem",
"zoomIn": "Aumentar Zoom",
"zoomOut": "Diminuir Zoom",
"rotate": "Rotacionar", "rotate": "Rotacionar",
"originalSize": "Tamanho Original", "originalSize": "Tamanho Original",
"startTyping": "Comece a digitar...", "startTyping": "Comece a digitar...",
"unknownSize": "Tamanho desconhecido", "unknownSize": "Tamanho desconhecido",
"fileIsEmpty": "Arquivo está vazio", "fileIsEmpty": "Arquivo está vazio",
"modified": "Modificado",
"largeFileWarning": "Aviso de Arquivo Grande", "largeFileWarning": "Aviso de Arquivo Grande",
"largeFileWarningDesc": "Este arquivo tem {{size}} de tamanho, o que pode causar problemas de desempenho quando aberto como texto.", "largeFileWarningDesc": "Este arquivo tem {{size}} de tamanho, o que pode causar problemas de desempenho quando aberto como texto.",
"fileNotFoundAndRemoved": "Arquivo \"{{name}}\" não encontrado e foi removido dos arquivos recentes/fixados", "fileNotFoundAndRemoved": "Arquivo \"{{name}}\" não encontrado e foi removido dos arquivos recentes/fixados",
"failedToLoadFile": "Falha ao carregar arquivo: {{error}}", "failedToLoadFile": "Falha ao carregar arquivo: {{error}}",
"serverErrorOccurred": "Ocorreu um erro no servidor. Por favor, tente novamente mais tarde.", "serverErrorOccurred": "Ocorreu um erro no servidor. Por favor, tente novamente mais tarde.",
"fileSavedSuccessfully": "Arquivo salvo com sucesso",
"autoSaveFailed": "Falha no salvamento automático", "autoSaveFailed": "Falha no salvamento automático",
"fileAutoSaved": "Arquivo salvo automaticamente", "fileAutoSaved": "Arquivo salvo automaticamente",
"moveFileFailed": "Falha ao mover {{name}}", "moveFileFailed": "Falha ao mover {{name}}",
"moveOperationFailed": "Falha na operação de mover", "moveOperationFailed": "Falha na operação de mover",
"canOnlyCompareFiles": "Só é possível comparar dois arquivos", "canOnlyCompareFiles": "Só é possível comparar dois arquivos",
@@ -1049,7 +1147,7 @@
"operationCompletedSuccessfully": "{{operation}} {{count}} itens com sucesso", "operationCompletedSuccessfully": "{{operation}} {{count}} itens com sucesso",
"operationCompleted": "{{operation}} {{count}} itens", "operationCompleted": "{{operation}} {{count}} itens",
"downloadFileSuccess": "Arquivo {{name}} baixado com sucesso", "downloadFileSuccess": "Arquivo {{name}} baixado com sucesso",
"downloadFileFailed": "Falha no download", "downloadFileFailed": "Falha no baixar",
"moveTo": "Mover para {{name}}", "moveTo": "Mover para {{name}}",
"diffCompareWith": "Comparar diferenças com {{name}}", "diffCompareWith": "Comparar diferenças com {{name}}",
"dragOutsideToDownload": "Arraste para fora da janela para baixar ({{count}} arquivos)", "dragOutsideToDownload": "Arraste para fora da janela para baixar ({{count}} arquivos)",
@@ -1068,12 +1166,42 @@
"fileComparison": "Comparação de Arquivos: {{file1}} vs {{file2}}", "fileComparison": "Comparação de Arquivos: {{file1}} vs {{file2}}",
"fileTooLarge": "Arquivo muito grande: {{error}}", "fileTooLarge": "Arquivo muito grande: {{error}}",
"sshConnectionFailed": "Falha na conexão SSH. Por favor, verifique sua conexão com {{name}} ({{ip}}:{{port}})", "sshConnectionFailed": "Falha na conexão SSH. Por favor, verifique sua conexão com {{name}} ({{ip}}:{{port}})",
"loadFileFailed": "Falha ao carregar arquivo: {{error}}" "loadFileFailed": "Falha ao carregar arquivo: {{error}}",
"andMoreFiles": "e {{count}} mais...",
"archiveExtractedSuccessfully": "{{name}} extraído com sucesso",
"archiveName": "Nome do Arquivo",
"changePermissions": "Alterar Permissões",
"changePermissionsDesc": "Modificar permissões do arquivo para",
"compress": "Comprimir",
"compressFailed": "Falha na compressão",
"compressFile": "Comprimir Arquivo",
"compressFiles": "Comprimir Arquivos",
"compressFilesDesc": "Comprimir {{count}} itens em um arquivo",
"compressingFiles": "Comprimindo {{count}} itens em {{name}}...",
"compressionFormat": "Formato de Compressão",
"connectedSuccessfully": "Conectado com sucesso",
"currentPermissions": "Permissões Atuais",
"enterArchiveName": "Inserir nome do arquivo...",
"execute": "Executar",
"extractArchive": "Extrair Arquivo",
"extractFailed": "Extração falhou",
"extractingArchive": "Extraindo {{name}}...",
"failedToChangePermissions": "Falha ao alterar permissões",
"filesCompressedSuccessfully": "{{name}} criado com sucesso",
"group": "Grupo",
"newPermissions": "Novas Permissões",
"others": "Outros",
"owner": "Proprietário",
"permissionsChangedSuccessfully": "Permissões alteradas com sucesso",
"read": "Leitura",
"selectedFiles": "Arquivos Selecionados",
"totpVerificationFailed": "Verificação TOTP falhou",
"write": "Escrita"
}, },
"tunnels": { "tunnels": {
"title": "Túneis SSH", "title": "Túneis SSH",
"noSshTunnels": "Sem Túneis SSH", "noSshTunnels": "Sem Túneis SSH",
"createFirstTunnelMessage": "Você ainda não criou nenhum túnel SSH. Configure conexões de túnel no Gerenciador de Hosts para começar.", "createFirstTunnelMessage": "Crie seu primeiro túnel SSH para começar. Use o Gerenciador SSH para adicionar hosts com conexões de túnel.",
"connected": "Conectado", "connected": "Conectado",
"disconnected": "Desconectado", "disconnected": "Desconectado",
"connecting": "Conectando...", "connecting": "Conectando...",
@@ -1093,7 +1221,7 @@
"port": "Porta", "port": "Porta",
"attempt": "Tentativa {{current}} de {{max}}", "attempt": "Tentativa {{current}} de {{max}}",
"nextRetryIn": "Próxima tentativa em {{seconds}} segundos", "nextRetryIn": "Próxima tentativa em {{seconds}} segundos",
"checkDockerLogs": "Verifique seus logs do Docker para ver o motivo do erro, entre no", "checkDockerLogs": "Verifique seus logs do Docker para ver o motivo do erro",
"noTunnelConnections": "Nenhuma conexão de túnel configurada", "noTunnelConnections": "Nenhuma conexão de túnel configurada",
"tunnelConnections": "Conexões de Túnel", "tunnelConnections": "Conexões de Túnel",
"addTunnel": "Adicionar Túnel", "addTunnel": "Adicionar Túnel",
@@ -1114,18 +1242,9 @@
"local": "Local", "local": "Local",
"remote": "Remoto", "remote": "Remoto",
"dynamic": "Dinâmico", "dynamic": "Dinâmico",
"noSshTunnels": "Sem Túneis SSH",
"createFirstTunnelMessage": "Crie seu primeiro túnel SSH para começar. Use o Gerenciador SSH para adicionar hosts com conexões de túnel.",
"unknownConnectionStatus": "Desconhecido", "unknownConnectionStatus": "Desconhecido",
"connected": "Conectado",
"connecting": "Conectando...",
"disconnecting": "Desconectando...",
"disconnected": "Desconectado",
"portMapping": "Porta {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", "portMapping": "Porta {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
"disconnect": "Desconectar", "endpointHostNotFound": "Host de destinenhum não encontrado",
"connect": "Conectar",
"canceling": "Cancelando...",
"endpointHostNotFound": "Host de destino não encontrado",
"discord": "Discord", "discord": "Discord",
"githubIssue": "issue no GitHub", "githubIssue": "issue no GitHub",
"forHelp": "para ajuda" "forHelp": "para ajuda"
@@ -1136,8 +1255,8 @@
"memory": "Memória", "memory": "Memória",
"disk": "Disco", "disk": "Disco",
"network": "Rede", "network": "Rede",
"uptime": "Tempo Ativo", "uptime": "Tempo de Atividade",
"loadAverage": "Carga Média", "loadAverage": "Média: {{avg1}}, {{avg5}}, {{avg15}}",
"processes": "Processos", "processes": "Processos",
"connections": "Conexões", "connections": "Conexões",
"usage": "Uso", "usage": "Uso",
@@ -1153,7 +1272,6 @@
"cpuCores_one": "{{count}} CPU", "cpuCores_one": "{{count}} CPU",
"cpuCores_other": "{{count}} CPUs", "cpuCores_other": "{{count}} CPUs",
"naCpus": "N/D CPU(s)", "naCpus": "N/D CPU(s)",
"loadAverage": "Média: {{avg1}}, {{avg5}}, {{avg15}}",
"loadAverageNA": "Média: N/D", "loadAverageNA": "Média: N/D",
"cpuUsage": "Uso da CPU", "cpuUsage": "Uso da CPU",
"memoryUsage": "Uso de Memória", "memoryUsage": "Uso de Memória",
@@ -1169,8 +1287,40 @@
"serverOffline": "Servidor Offline", "serverOffline": "Servidor Offline",
"cannotFetchMetrics": "Não é possível buscar métricas do servidor offline", "cannotFetchMetrics": "Não é possível buscar métricas do servidor offline",
"load": "Carga", "load": "Carga",
"free": "Livre", "addWidget": "Adicionar Widget",
"available": "Disponível" "cancelEdit": "Cancelar",
"diskUsage": "Uso de Disco",
"editLayout": "Editar Layout",
"executeQuickAction": "Executar {{name}}",
"executingQuickAction": "Executando {{name}}...",
"failedToSaveLayout": "Falha ao salvar layout",
"from": "de",
"hostname": "Hostname",
"kernel": "Kernel",
"layoutSaved": "Layout salvo com sucesso",
"loginStats": "Estatísticas de Login SSH",
"networkInterfaces": "Interfaces de Rede",
"noInterfacesFound": "Nenhuma interface de rede encontrada",
"noProcessesFound": "Nenhum processo encontrado",
"noRecentLoginData": "Nenhum dado de login recente",
"operatingSystem": "Sistema Operacional",
"quickActionError": "Falha ao executar {{name}}",
"quickActionFailed": "{{name}} falhou",
"quickActionSuccess": "{{name}} concluído com sucesso",
"quickActions": "Ações Rápidas",
"recentFailedAttempts": "Tentativas Falhas Recentemente",
"recentSuccessfulLogins": "Logins Bem-sucedidos Recentemente",
"running": "Executando",
"saveLayout": "Salvar Layout",
"seconds": "segundos",
"systemInfo": "Informações do Sistema",
"totalLogins": "Total de Logins",
"totalProcesses": "Total de Processos",
"totalUptime": "Tempo de Atividade Total",
"totpRequired": "Autenticação TOTP Obrigatória",
"totpUnavailable": "Estatísticas do Servidor indisponíveis para servidores com TOTP ativado",
"uniqueIPs": "IPs Únicos",
"unsavedChanges": "Alterações não salvas"
}, },
"auth": { "auth": {
"tagline": "GERENCIADOR DE TERMINAL SSH", "tagline": "GERENCIADOR DE TERMINAL SSH",
@@ -1242,7 +1392,7 @@
"enableTwoFactorButton": "Ativar Autenticação de Dois Fatores", "enableTwoFactorButton": "Ativar Autenticação de Dois Fatores",
"addExtraSecurityLayer": "Adicione uma camada extra de segurança à sua conta", "addExtraSecurityLayer": "Adicione uma camada extra de segurança à sua conta",
"firstUser": "Primeiro Usuário", "firstUser": "Primeiro Usuário",
"firstUserMessage": "Você é o primeiro usuário e será tornado admin. Você pode ver as configurações de admin no menu suspenso do usuário na barra lateral. Se você acha que isso é um erro, verifique os logs do docker ou crie uma issue no GitHub.", "firstUserMessage": "Você é o primeiro usuário e será tornado administrador. Você pode ver as configurações de administrador no menu suspenso do usuário na barra lateral. Se você acha que isso é um erro, verifique os logs do docker ou crie uma issue no GitHub.",
"external": "Externo", "external": "Externo",
"loginWithExternal": "Entrar com Provedor Externo", "loginWithExternal": "Entrar com Provedor Externo",
"loginWithExternalDesc": "Entre usando seu provedor de identidade externo configurado", "loginWithExternalDesc": "Entre usando seu provedor de identidade externo configurado",
@@ -1269,7 +1419,18 @@
"sshTimeoutDescription": "A tentativa de autenticação expirou. Por favor, tente novamente.", "sshTimeoutDescription": "A tentativa de autenticação expirou. Por favor, tente novamente.",
"sshProvideCredentialsDescription": "Por favor, forneça suas credenciais SSH para conectar a este servidor.", "sshProvideCredentialsDescription": "Por favor, forneça suas credenciais SSH para conectar a este servidor.",
"sshPasswordDescription": "Digite a senha para esta conexão SSH.", "sshPasswordDescription": "Digite a senha para esta conexão SSH.",
"sshKeyPasswordDescription": "Se sua chave SSH estiver criptografada, digite a senha aqui." "sshKeyPasswordDescription": "Se sua chave SSH estiver criptografada, digite a senha aqui.",
"authenticating": "Autenticando...",
"authenticationDisabled": "Autenticação Desativada",
"authenticationDisabledDesc": "Todos os métodos de autenticação estão atualmente desativados. Por favor, contate seu administrador.",
"desktopApp": "App Desktop",
"loadingServer": "Carregando servidor...",
"loggingInToDesktopApp": "Entrando no app desktop",
"loggingInToDesktopAppViaWeb": "Entrando no app desktop via interface web",
"loggingInToMobileApp": "Entrando no app mobile",
"mobileApp": "App Mobile",
"redirectingToApp": "Redirecionando para o app...",
"passwordResetSuccessDesc": "Sua senha foi redefinida com sucesso. Você pode agora entrar com sua nova senha."
}, },
"errors": { "errors": {
"notFound": "Página não encontrada", "notFound": "Página não encontrada",
@@ -1300,7 +1461,8 @@
"emailExists": "Email já existe", "emailExists": "Email já existe",
"loadFailed": "Falha ao carregar dados", "loadFailed": "Falha ao carregar dados",
"saveError": "Falha ao salvar", "saveError": "Falha ao salvar",
"sessionExpired": "Sessão expirada - por favor, faça login novamente" "sessionExpired": "Sessão expirada - por favor, faça login novamente",
"passwordLoginDisabled": "Login com nome de usuário/senha está atualmente desativado"
}, },
"messages": { "messages": {
"saveSuccess": "Salvo com sucesso", "saveSuccess": "Salvo com sucesso",
@@ -1345,9 +1507,12 @@
"fileColorCodingDesc": "Codificar arquivos por cores por tipo: pastas (vermelho), arquivos (azul), links simbólicos (verde)", "fileColorCodingDesc": "Codificar arquivos por cores por tipo: pastas (vermelho), arquivos (azul), links simbólicos (verde)",
"commandAutocomplete": "Autocompletar Comandos", "commandAutocomplete": "Autocompletar Comandos",
"commandAutocompleteDesc": "Ativar sugestões de autocompletar com a tecla Tab para comandos do terminal baseado no seu histórico", "commandAutocompleteDesc": "Ativar sugestões de autocompletar com a tecla Tab para comandos do terminal baseado no seu histórico",
"defaultSnippetFoldersCollapsed": "Recolher Pastas de Snippets por Padrão",
"defaultSnippetFoldersCollapsedDesc": "Quando ativado, todas as pastas de snippets serão recolhidas ao abrir a aba de snippets",
"currentPassword": "Senha Atual", "currentPassword": "Senha Atual",
"passwordChangedSuccess": "Senha alterada com sucesso! Por favor, faça login novamente.", "passwordChangedSuccess": "Senha alterada com sucesso! Por favor, faça login novamente.",
"failedToChangePassword": "Falha ao alterar a senha. Por favor, verifique sua senha atual e tente novamente." "failedToChangePassword": "Falha ao alterar a senha. Por favor, verifique sua senha atual e tente novamente.",
"externalAndLocal": "Autenticação Dupla"
}, },
"user": { "user": {
"failedToLoadVersionInfo": "Falha ao carregar informações da versão" "failedToLoadVersionInfo": "Falha ao carregar informações da versão"
@@ -1377,10 +1542,10 @@
"redirectUrl": "https://seu-provedor.com/application/o/termix/", "redirectUrl": "https://seu-provedor.com/application/o/termix/",
"tokenUrl": "https://seu-provedor.com/application/o/token/", "tokenUrl": "https://seu-provedor.com/application/o/token/",
"userIdField": "sub", "userIdField": "sub",
"usernameField": "name", "usernameField": "nome",
"scopes": "openid email profile", "scopes": "openid email profile",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/", "userinfoUrl": "https://seu-provider.com/application/o/userinfo/",
"enterUsername": "Digite o nome de usuário para tornar admin", "enterUsername": "Digite o nome de usuário para tornar administrador",
"searchHosts": "Procurar hosts por nome, usuário, IP, pasta, tags...", "searchHosts": "Procurar hosts por nome, usuário, IP, pasta, tags...",
"enterPassword": "Digite sua senha", "enterPassword": "Digite sua senha",
"totpCode": "Código TOTP de 6 dígitos", "totpCode": "Código TOTP de 6 dígitos",
@@ -1398,9 +1563,9 @@
"noFolder": "Sem Pasta", "noFolder": "Sem Pasta",
"passwordRequired": "Senha é obrigatória", "passwordRequired": "Senha é obrigatória",
"failedToDeleteAccount": "Falha ao excluir conta", "failedToDeleteAccount": "Falha ao excluir conta",
"failedToMakeUserAdmin": "Falha ao tornar usuário admin", "failedToMakeUserAdmin": "Falha ao tornar usuário administrador",
"userIsNowAdmin": "Usuário {{username}} agora é um admin", "userIsNowAdmin": "Usuário {{username}} agora é um administrador",
"removeAdminConfirm": "Tem certeza que deseja remover o status de admin de {{username}}?", "removeAdminConfirm": "Tem certeza que deseja remover o status de administrador de {{username}}?",
"deleteUserConfirm": "Tem certeza que deseja excluir o usuário {{username}}? Esta ação não pode ser desfeita.", "deleteUserConfirm": "Tem certeza que deseja excluir o usuário {{username}}? Esta ação não pode ser desfeita.",
"deleteAccount": "Excluir Conta", "deleteAccount": "Excluir Conta",
"closeDeleteAccount": "Fechar Exclusão de Conta", "closeDeleteAccount": "Fechar Exclusão de Conta",
@@ -1408,7 +1573,7 @@
"deleteAccountWarningDetails": "Excluir sua conta removerá todos os seus dados, incluindo hosts SSH, configurações e preferências. Esta ação é irreversível.", "deleteAccountWarningDetails": "Excluir sua conta removerá todos os seus dados, incluindo hosts SSH, configurações e preferências. Esta ação é irreversível.",
"deleteAccountWarningShort": "Esta ação é irreversível e excluirá permanentemente sua conta.", "deleteAccountWarningShort": "Esta ação é irreversível e excluirá permanentemente sua conta.",
"cannotDeleteAccount": "Não é Possível Excluir Conta", "cannotDeleteAccount": "Não é Possível Excluir Conta",
"lastAdminWarning": "Você é o último usuário administrador. Você não pode excluir sua conta pois isso deixaria o sistema sem administradores. Por favor, torne outro usuário administrador primeiro, ou contate o suporte do sistema.", "lastAdminWarning": "Você é o último administrador. Você não pode excluir sua conta pois isso deixaria o sistema sem administradores. Por favor, torne outro usuário administrador primeiro, ou contate o suporte do sistema.",
"confirmPassword": "Confirmar Senha", "confirmPassword": "Confirmar Senha",
"deleting": "Excluindo...", "deleting": "Excluindo...",
"cancel": "Cancelar" "cancel": "Cancelar"
@@ -1431,7 +1596,6 @@
"deleteItem": "Excluir Item", "deleteItem": "Excluir Item",
"createNewFile": "Criar Novo Arquivo", "createNewFile": "Criar Novo Arquivo",
"createNewFolder": "Criar Nova Pasta", "createNewFolder": "Criar Nova Pasta",
"deleteItem": "Excluir Item",
"renameItem": "Renomear Item", "renameItem": "Renomear Item",
"clickToSelectFile": "Clique para selecionar um arquivo", "clickToSelectFile": "Clique para selecionar um arquivo",
"noSshHosts": "Sem Hosts SSH", "noSshHosts": "Sem Hosts SSH",
@@ -1527,5 +1691,95 @@
"mobileAppInProgressDesc": "Estamos trabalhando em um aplicativo móvel dedicado para proporcionar uma melhor experiência em dispositivos móveis.", "mobileAppInProgressDesc": "Estamos trabalhando em um aplicativo móvel dedicado para proporcionar uma melhor experiência em dispositivos móveis.",
"viewMobileAppDocs": "Instalar Aplicativo Móvel", "viewMobileAppDocs": "Instalar Aplicativo Móvel",
"mobileAppDocumentation": "Documentação do Aplicativo Móvel" "mobileAppDocumentation": "Documentação do Aplicativo Móvel"
},
"commandPalette": {
"addCredential": "Adicionar Credencial",
"addHost": "Adicionar Host",
"adminSettings": "Configurações de Administrador",
"close": "Fechar",
"discord": "Discord",
"donate": "Doar",
"edit": "Editar",
"github": "GitHub",
"hostManager": "Gerenciador de Hosts",
"hosts": "Hosts",
"links": "Links",
"navigation": "Navegação",
"openFileManager": "Abrir Gerenciador de Arquivos",
"openServerDetails": "Abrir Detalhes do Servidor",
"press": "Pressione",
"recentActivity": "Atividade Recente",
"searchPlaceholder": "Pesquisar por hosts ou ações rápidas...",
"support": "Suporte",
"toToggle": "para alternar",
"updateLog": "Log de Atualizações",
"userProfile": "Perfil do Usuário"
},
"dashboard": {
"addCredential": "Adicionar Credencial",
"addHost": "Adicionar Host",
"adminSettings": "Configurações de Administrador",
"cpu": "CPU",
"database": "Banco de Dados",
"discord": "Discord",
"donate": "Doar",
"error": "Erro",
"github": "GitHub",
"healthy": "Saudável",
"loadingRecentActivity": "Carregando atividade recente...",
"loadingServerStats": "Carregando estatísticas do servidor...",
"noRecentActivity": "Nenhuma atividade recente",
"noServerData": "Nenhum dado do servidor disponível",
"notAvailable": "N/D",
"quickActions": "Ações Rápidas",
"ram": "RAM",
"recentActivity": "Atividade Recente",
"reset": "Redefinir",
"serverOverview": "Visão Geral do Servidor",
"serverStats": "Estatísticas do Servidor",
"support": "Suporte",
"title": "Painel",
"totalCredentials": "Total de Credenciais",
"totalServers": "Total de Servidores",
"totalTunnels": "Total de Túneis",
"upToDate": "Atualizado",
"updateAvailable": "Atualização Disponível",
"uptime": "Tempo de Atividade",
"userProfile": "Perfil do Usuário",
"version": "Versão"
},
"snippets": {
"content": "Comando",
"contentPlaceholder": "ex: sudo systemctl restart nginx",
"contentRequired": "Comando é obrigatório",
"copySuccess": "Copiado \"{{name}}\" para área de transferência",
"copyTooltip": "Copiar snippet para área de transferência",
"create": "Criar Snippet",
"createDescription": "Criar um novo snippet de comando para execução rápida",
"createFailed": "Falha ao criar snippet",
"createSuccess": "Snippet criado com sucesso",
"deleteConfirmDescription": "Tem certeza de que deseja excluir \"{{name}}\"?",
"deleteConfirmTitle": "Excluir Snippet",
"deleteFailed": "Falha ao excluir snippet",
"deleteSuccess": "Snippet excluído com sucesso",
"deleteTooltip": "Excluir este snippet",
"description": "Descrição",
"descriptionPlaceholder": "Descrição opcional",
"edit": "Editar Snippet",
"editDescription": "Editar este snippet de comando",
"editTooltip": "Editar este snippet",
"empty": "Nenhum snippet ainda",
"emptyHint": "Crie um snippet para salvar comandos comumente usados",
"executeSuccess": "Executando: {{name}}",
"failedToFetch": "Falha ao buscar snippets",
"name": "Nome",
"namePlaceholder": "ex: Reiniciar Nginx",
"nameRequired": "Nome é obrigatório",
"new": "Novo Snippet",
"run": "Executar",
"runTooltip": "Executar este snippet no terminal",
"title": "Snippets",
"updateFailed": "Falha ao atualizar snippet",
"updateSuccess": "Snippet atualizado com sucesso"
} }
} }

View File

@@ -59,7 +59,6 @@
"keyTypeRSA": "RSA", "keyTypeRSA": "RSA",
"keyTypeECDSA": "ECDSA", "keyTypeECDSA": "ECDSA",
"keyTypeEd25519": "Ed25519", "keyTypeEd25519": "Ed25519",
"updateCredential": "Обновить учетные данные",
"basicInfo": "Основная информация", "basicInfo": "Основная информация",
"authentication": "Аутентификация", "authentication": "Аутентификация",
"organization": "Организация", "organization": "Организация",
@@ -118,7 +117,6 @@
"credentialSecuredDescription": "Все конфиденциальные данные зашифрованы с помощью AES-256", "credentialSecuredDescription": "Все конфиденциальные данные зашифрованы с помощью AES-256",
"passwordAuthentication": "Аутентификация по паролю", "passwordAuthentication": "Аутентификация по паролю",
"keyAuthentication": "Аутентификация по ключу", "keyAuthentication": "Аутентификация по ключу",
"keyType": "Тип ключа",
"securityReminder": "Напоминание о безопасности", "securityReminder": "Напоминание о безопасности",
"securityReminderText": "Никогда не передавайте ваши учетные данные. Все данные зашифрованы при хранении.", "securityReminderText": "Никогда не передавайте ваши учетные данные. Все данные зашифрованы при хранении.",
"hostsUsingCredential": "Хосты, использующие эти учетные данные", "hostsUsingCredential": "Хосты, использующие эти учетные данные",
@@ -166,7 +164,8 @@
"generateKeyPairNote": "Сгенерировать новую пару SSH-ключей напрямую. Это заменит любые существующие ключи в форме.", "generateKeyPairNote": "Сгенерировать новую пару SSH-ключей напрямую. Это заменит любые существующие ключи в форме.",
"invalidKey": "Неверный ключ", "invalidKey": "Неверный ключ",
"detectionError": "Ошибка определения", "detectionError": "Ошибка определения",
"unknown": "Неизвестно" "unknown": "Неизвестно",
"credentialId": "Учётные данные ID"
}, },
"dragIndicator": { "dragIndicator": {
"error": "Ошибка: {{error}}", "error": "Ошибка: {{error}}",
@@ -261,7 +260,11 @@
"saveError": "Ошибка сохранения конфигурации", "saveError": "Ошибка сохранения конфигурации",
"saving": "Сохранение...", "saving": "Сохранение...",
"saveConfig": "Сохранить конфигурацию", "saveConfig": "Сохранить конфигурацию",
"helpText": "Введите URL, где работает ваш сервер Termix (например, http://localhost:30001 или https://your-server.com)" "helpText": "Введите URL, где работает ваш сервер Termix (например, http://localhost:30001 или https://your-server.com)",
"changeServer": "Сменить сервер",
"mustIncludeProtocol": "URL сервера должен начинаться с http:// или https://",
"notValidatedWarning": "URL не проверен - убедитесь, что он правильный",
"warning": "Предупреждение"
}, },
"versionCheck": { "versionCheck": {
"error": "Ошибка проверки версии", "error": "Ошибка проверки версии",
@@ -294,7 +297,7 @@
"warning": "Предупреждение", "warning": "Предупреждение",
"info": "Информация", "info": "Информация",
"success": "Успех", "success": "Успех",
"loading": "Загрузка", "loading": "Загрузка...",
"required": "Обязательно", "required": "Обязательно",
"optional": "Опционально", "optional": "Опционально",
"clear": "Очистить", "clear": "Очистить",
@@ -308,7 +311,6 @@
"updateAvailable": "Доступно обновление", "updateAvailable": "Доступно обновление",
"sshPath": "SSH-путь", "sshPath": "SSH-путь",
"localPath": "Локальный путь", "localPath": "Локальный путь",
"loading": "Загрузка...",
"noAuthCredentials": "Нет учетных данных аутентификации для этого SSH-хоста", "noAuthCredentials": "Нет учетных данных аутентификации для этого SSH-хоста",
"noReleases": "Нет выпусков", "noReleases": "Нет выпусков",
"updatesAndReleases": "Обновления и выпуски", "updatesAndReleases": "Обновления и выпуски",
@@ -323,17 +325,13 @@
"resetPassword": "Сбросить пароль", "resetPassword": "Сбросить пароль",
"resetCode": "Код сброса", "resetCode": "Код сброса",
"newPassword": "Новый пароль", "newPassword": "Новый пароль",
"sshPath": "SSH-путь",
"localPath": "Локальный путь",
"folder": "Папка", "folder": "Папка",
"file": "Файл", "file": "Файл",
"renamedSuccessfully": "успешно переименован", "renamedSuccessfully": "успешно переименован",
"deletedSuccessfully": "успешно удален", "deletedSuccessfully": "успешно удален",
"noAuthCredentials": "Нет учетных данных аутентификации для этого SSH-хоста",
"noTunnelConnections": "Нет настроенных туннельных подключений", "noTunnelConnections": "Нет настроенных туннельных подключений",
"sshTools": "SSH-инструменты", "sshTools": "SSH-инструменты",
"english": "Английский", "english": "Английский",
"russia": "Русский",
"chinese": "Китайский", "chinese": "Китайский",
"german": "Немецкий", "german": "Немецкий",
"cancel": "Отмена", "cancel": "Отмена",
@@ -342,36 +340,27 @@
"login": "Войти", "login": "Войти",
"logout": "Выйти", "logout": "Выйти",
"register": "Зарегистрироваться", "register": "Зарегистрироваться",
"username": "Имя пользователя",
"password": "Пароль", "password": "Пароль",
"version": "Версия", "version": "Версия",
"confirmPassword": "Подтвердите пароль", "confirmPassword": "Подтвердите пароль",
"back": "Назад", "back": "Назад",
"email": "Email", "email": "Email",
"submit": "Отправить", "submit": "Отправить",
"cancel": "Отмена",
"change": "Изменить", "change": "Изменить",
"save": "Сохранить", "save": "Сохранить",
"delete": "Удалить", "delete": "Удалить",
"edit": "Редактировать", "edit": "Редактировать",
"add": "Добавить", "add": "Добавить",
"search": "Поиск", "search": "Поиск",
"loading": "Загрузка...",
"error": "Ошибка",
"success": "Успех",
"warning": "Предупреждение",
"info": "Информация",
"confirm": "Подтвердить", "confirm": "Подтвердить",
"yes": "Да", "yes": "Да",
"no": "Нет", "no": "Нет",
"ok": "OK", "ok": "OK",
"close": "Закрыть",
"enabled": "Включено", "enabled": "Включено",
"disabled": "Отключено", "disabled": "Отключено",
"important": "Важно", "important": "Важно",
"notEnabled": "Не включено", "notEnabled": "Не включено",
"settingUp": "Настройка...", "settingUp": "Настройка...",
"back": "Назад",
"next": "Далее", "next": "Далее",
"previous": "Назад", "previous": "Назад",
"refresh": "Обновить", "refresh": "Обновить",
@@ -395,7 +384,10 @@
"documentation": "Документация", "documentation": "Документация",
"retry": "Повторить", "retry": "Повторить",
"checking": "Проверка...", "checking": "Проверка...",
"checkingDatabase": "Проверка подключения к базе данных..." "checkingDatabase": "Проверка подключения к базе данных...",
"connect": "Подключить",
"connecting": "Подключение...",
"saving": "Сохранение..."
}, },
"nav": { "nav": {
"home": "Главная", "home": "Главная",
@@ -424,7 +416,7 @@
"userManagement": "Управление пользователями", "userManagement": "Управление пользователями",
"makeAdmin": "Сделать администратором", "makeAdmin": "Сделать администратором",
"removeAdmin": "Убрать администратора", "removeAdmin": "Убрать администратора",
"deleteUser": "Удалить пользователя", "deleteUser": "Удалить пользователя {{username}}? Это нельзя отменить.",
"allowRegistration": "Разрешить регистрацию", "allowRegistration": "Разрешить регистрацию",
"oidcSettings": "Настройки OIDC", "oidcSettings": "Настройки OIDC",
"clientId": "Client ID", "clientId": "Client ID",
@@ -478,10 +470,9 @@
"removeAdminStatus": "Убрать статус администратора у {{username}}?", "removeAdminStatus": "Убрать статус администратора у {{username}}?",
"adminStatusRemoved": "Статус администратора убран у {{username}}", "adminStatusRemoved": "Статус администратора убран у {{username}}",
"failedToRemoveAdminStatus": "Не удалось убрать статус администратора", "failedToRemoveAdminStatus": "Не удалось убрать статус администратора",
"deleteUser": "Удалить пользователя {{username}}? Это нельзя отменить.",
"userDeletedSuccessfully": "Пользователь {{username}} успешно удален", "userDeletedSuccessfully": "Пользователь {{username}} успешно удален",
"failedToDeleteUser": "Не удалось удалить пользователя", "failedToDeleteUser": "Не удалось удалить пользователя",
"overrideUserInfoUrl": "Переопределить User Info URL (не требуется)", "overrideUserInfoUrl": "Переопределить Пользователь Info URL (не требуется)",
"databaseSecurity": "Безопасность базы данных", "databaseSecurity": "Безопасность базы данных",
"encryptionStatus": "Статус шифрования", "encryptionStatus": "Статус шифрования",
"encryptionEnabled": "Шифрование включено", "encryptionEnabled": "Шифрование включено",
@@ -531,7 +522,6 @@
"verificationCompleted": "Проверка совместимости завершена - данные не изменялись", "verificationCompleted": "Проверка совместимости завершена - данные не изменялись",
"verificationInProgress": "Проверка завершена", "verificationInProgress": "Проверка завершена",
"dataMigrationCompleted": "Миграция данных успешно завершена!", "dataMigrationCompleted": "Миграция данных успешно завершена!",
"migrationCompleted": "Миграция завершена",
"verificationFailed": "Проверка совместимости не удалась", "verificationFailed": "Проверка совместимости не удалась",
"migrationFailed": "Миграция не удалась", "migrationFailed": "Миграция не удалась",
"runningVerification": "Выполняется проверка совместимости...", "runningVerification": "Выполняется проверка совместимости...",
@@ -609,7 +599,33 @@
"requiresPasswordLogin": "Требуется включенный вход по паролю", "requiresPasswordLogin": "Требуется включенный вход по паролю",
"passwordLoginDisabledWarning": "Вход по паролю отключен. Убедитесь, что OIDC правильно настроен, иначе вы не сможете войти в Termix.", "passwordLoginDisabledWarning": "Вход по паролю отключен. Убедитесь, что OIDC правильно настроен, иначе вы не сможете войти в Termix.",
"oidcRequiredWarning": "КРИТИЧЕСКИ: Вход по паролю отключен. Если вы сбросите или неправильно настроите OIDC, вы потеряете весь доступ к Termix и заблокируете свой экземпляр. Продолжайте только если вы абсолютно уверены.", "oidcRequiredWarning": "КРИТИЧЕСКИ: Вход по паролю отключен. Если вы сбросите или неправильно настроите OIDC, вы потеряете весь доступ к Termix и заблокируете свой экземпляр. Продолжайте только если вы абсолютно уверены.",
"confirmDisableOIDCWarning": "ПРЕДУПРЕЖДЕНИЕ: Вы собираетесь отключить OIDC, пока вход по паролю также отключен. Это заблокирует ваш экземпляр Termix, и вы потеряете весь доступ. Вы абсолютно уверены, что хотите продолжить?" "confirmDisableOIDCWarning": "ПРЕДУПРЕЖДЕНИЕ: Вы собираетесь отключить OIDC, пока вход по паролю также отключен. Это заблокирует ваш экземпляр Termix, и вы потеряете весь доступ. Вы абсолютно уверены, что хотите продолжить?",
"accountsLinkedSuccessfully": "OIDC пользователь {{oidcUsername}} связан с {{targetUsername}}",
"confirmRevokeAllSessions": "Вы уверены, что хотите отозвать все сессии для этого пользователя?",
"confirmRevokeSession": "Вы уверены, что хотите отозвать эту сессию?",
"failedToFetchSessions": "Не удалось загрузить сессии",
"failedToLinkAccounts": "Не удалось связать аккаунты",
"failedToRevokeSession": "Не удалось отозвать сессию",
"failedToRevokeSessions": "Не удалось отозвать сессии",
"failedToUnlinkOIDC": "Не удалось отвязать OIDC",
"linkAccountsButton": "Связать аккаунты",
"linkOIDCActionAddCapability": "Добавить возможность входа через OIDC к целевому аккаунту с паролем",
"linkOIDCActionDeleteUser": "Удалить аккаунт пользователя OIDC и все его данные",
"linkOIDCActionDualAuth": "Разрешить аккаунту с паролем вход как по паролю, так и через OIDC",
"linkOIDCDialogDescription": "Связать {{username}} (пользователь OIDC) с существующим аккаунтом с паролем. Это включит двойную аутентификацию для аккаунта с паролем.",
"linkOIDCDialogTitle": "Связать аккаунт OIDC с аккаунтом с паролем",
"linkOIDCWarningTitle": "Предупреждение: Данные пользователя OIDC будут удалены",
"linkTargetUsernameLabel": "Имя пользователя целевого аккаунта с паролем",
"linkTargetUsernamePlaceholder": "Введите имя пользователя аккаунта с паролем",
"linkTargetUsernameRequired": "Целевое имя пользователя обязательно",
"linkToPasswordAccount": "Связать с аккаунтом с паролем",
"linkingAccounts": "Связывание...",
"sessionRevokedSuccessfully": "Сессия успешно отозвана",
"sessionsRevokedSuccessfully": "Сессии успешно отозваны",
"unlinkOIDCDescription": "Удалить аутентификацию OIDC для {{username}}? После этого пользователь сможет войти только с помощью имени пользователя/пароля.",
"unlinkOIDCSuccess": "OIDC отвязан от {{username}}",
"unlinkOIDCTitle": "Отвязать аутентификацию OIDC",
"failedToUpdatePasswordLoginStatus": "Не удалось обновить статус входа по паролю"
}, },
"hosts": { "hosts": {
"title": "Менеджер хостов", "title": "Менеджер хостов",
@@ -834,11 +850,11 @@
"minimumContrastRatioDesc": "Автоматически настраивать цвета для лучшей читаемости", "minimumContrastRatioDesc": "Автоматически настраивать цвета для лучшей читаемости",
"sshAgentForwarding": "Переадресация SSH-агента", "sshAgentForwarding": "Переадресация SSH-агента",
"sshAgentForwardingDesc": "Переадресовать агент SSH-аутентификации на удаленный хост", "sshAgentForwardingDesc": "Переадресовать агент SSH-аутентификации на удаленный хост",
"backspaceMode": "Режим Backspace", "backspaceMode": "Режим Назадspace",
"selectBackspaceMode": "Выбрать режим Backspace", "selectBackspaceMode": "Выбрать режим Назадspace",
"backspaceModeNormal": "Обычный (DEL)", "backspaceModeNormal": "Обычный (DEL)",
"backspaceModeControlH": "Control-H (^H)", "backspaceModeControlH": "Control-H (^H)",
"backspaceModeDesc": "Поведение клавиши Backspace для совместимости", "backspaceModeDesc": "Поведение клавиши Назадspace для совместимости",
"startupSnippet": "Сниппет запуска", "startupSnippet": "Сниппет запуска",
"selectSnippet": "Выбрать сниппет", "selectSnippet": "Выбрать сниппет",
"searchSnippets": "Поиск сниппетов...", "searchSnippets": "Поиск сниппетов...",
@@ -858,7 +874,25 @@
"searchServers": "Поиск серверов...", "searchServers": "Поиск серверов...",
"noServerFound": "Сервер не найден", "noServerFound": "Сервер не найден",
"jumpHostsOrder": "Подключения будут выполнены в порядке: Промежуточный хост 1 → Промежуточный хост 2 → ... → Целевой сервер", "jumpHostsOrder": "Подключения будут выполнены в порядке: Промежуточный хост 1 → Промежуточный хост 2 → ... → Целевой сервер",
"advancedAuthSettings": "Расширенные настройки аутентификации" "advancedAuthSettings": "Расширенные настройки аутентификации",
"addQuickAction": "Добавить Quick Action",
"allHostsInFolderDeleted": "{{count}} хостов успешно удалены из папки \"{{folder}}\"",
"confirmDeleteAllHostsInFolder": "Вы уверены, что хотите удалить все {{count}} хостов в папке \"{{folder}}\"? Это действие нельзя отменить.",
"deleteAllHostsInFolder": "Удалить все хосты в папке",
"editFolderAppearance": "Редактировать вид папки",
"editFolderAppearanceDesc": "Настроить цвет и иконку для папки",
"failedToDeleteHostsInFolder": "Не удалось удалить хосты в папке",
"failedToUpdateFolderAppearance": "Не удалось обновить вид папки",
"folderAppearanceUpdated": "Вид папки успешно обновлен",
"folderColor": "Цвет папки",
"folderIcon": "Иконка папки",
"noSnippetFound": "Сниппет не найден",
"preview": "Предпросмотр",
"quickActionName": "Название действия",
"quickActions": "Быстрые действия",
"quickActionsDescription": "Быстрые действия позволяют создавать пользовательские кнопки, выполняющие SSH-сниппеты на этом сервере. Эти кнопки появятся в верхней части страницы статистики сервера для быстрого доступа.",
"quickActionsList": "Список быстрых действий",
"quickActionsOrder": "Кнопки быстрых действий появятся в указанном выше порядке на странице статистики сервера"
}, },
"terminal": { "terminal": {
"title": "Терминал", "title": "Терминал",
@@ -896,7 +930,11 @@
"totpRequired": "Требуется двухфакторная аутентификация", "totpRequired": "Требуется двухфакторная аутентификация",
"totpCodeLabel": "Код проверки", "totpCodeLabel": "Код проверки",
"totpPlaceholder": "000000", "totpPlaceholder": "000000",
"totpVerify": "Проверить" "totpVerify": "Проверить",
"sudoPasswordPopupTitle": "Вставить пароль?",
"sudoPasswordPopupHint": "Нажмите Enter для вставки, Esc для отмены",
"sudoPasswordPopupConfirm": "Вставить",
"sudoPasswordPopupDismiss": "Отмена"
}, },
"fileManager": { "fileManager": {
"title": "Файловый менеджер", "title": "Файловый менеджер",
@@ -982,7 +1020,6 @@
"copyPaths": "Копировать пути", "copyPaths": "Копировать пути",
"delete": "Удалить", "delete": "Удалить",
"properties": "Свойства", "properties": "Свойства",
"preview": "Просмотр",
"refresh": "Обновить", "refresh": "Обновить",
"downloadFiles": "Скачать {{count}} файлов в браузер", "downloadFiles": "Скачать {{count}} файлов в браузер",
"copyFiles": "Копировать {{count}} элементов", "copyFiles": "Копировать {{count}} элементов",
@@ -997,18 +1034,11 @@
"failedToDeleteItem": "Не удалось удалить элемент", "failedToDeleteItem": "Не удалось удалить элемент",
"itemRenamedSuccessfully": "{{type}} успешно переименован", "itemRenamedSuccessfully": "{{type}} успешно переименован",
"failedToRenameItem": "Не удалось переименовать элемент", "failedToRenameItem": "Не удалось переименовать элемент",
"upload": "Загрузить",
"download": "Скачать", "download": "Скачать",
"newFile": "Новый файл",
"newFolder": "Новая папка",
"rename": "Переименовать",
"delete": "Удалить",
"permissions": "Права доступа", "permissions": "Права доступа",
"size": "Размер", "size": "Размер",
"modified": "Изменен", "modified": "Изменен",
"path": "Путь", "path": "Путь",
"fileName": "Имя файла",
"folderName": "Имя папки",
"confirmDelete": "Вы уверены, что хотите удалить {{name}}?", "confirmDelete": "Вы уверены, что хотите удалить {{name}}?",
"uploadSuccess": "Файл успешно загружен", "uploadSuccess": "Файл успешно загружен",
"uploadFailed": "Не удалось загрузить файл", "uploadFailed": "Не удалось загрузить файл",
@@ -1028,10 +1058,7 @@
"fileSavedSuccessfully": "Файл успешно сохранен", "fileSavedSuccessfully": "Файл успешно сохранен",
"saveTimeout": "Операция сохранения превысила время ожидания. Файл мог быть успешно сохранен, но операция заняла слишком много времени для завершения. Проверьте логи Docker для подтверждения.", "saveTimeout": "Операция сохранения превысила время ожидания. Файл мог быть успешно сохранен, но операция заняла слишком много времени для завершения. Проверьте логи Docker для подтверждения.",
"failedToSaveFile": "Не удалось сохранить файл", "failedToSaveFile": "Не удалось сохранить файл",
"folder": "Папка",
"file": "Файл",
"deletedSuccessfully": "успешно удален", "deletedSuccessfully": "успешно удален",
"failedToDeleteItem": "Не удалось удалить элемент",
"connectToServer": "Подключиться к серверу", "connectToServer": "Подключиться к серверу",
"selectServerToEdit": "Выберите сервер на боковой панели, чтобы начать редактирование файлов", "selectServerToEdit": "Выберите сервер на боковой панели, чтобы начать редактирование файлов",
"fileOperations": "Файловые операции", "fileOperations": "Файловые операции",
@@ -1088,10 +1115,8 @@
"unpinFile": "Открепить файл", "unpinFile": "Открепить файл",
"removeShortcut": "Удалить ярлык", "removeShortcut": "Удалить ярлык",
"saveFilesToSystem": "Сохранить {{count}} файлов как...", "saveFilesToSystem": "Сохранить {{count}} файлов как...",
"saveToSystem": "Сохранить как...",
"pinFile": "Закрепить файл", "pinFile": "Закрепить файл",
"addToShortcuts": "Добавить в ярлыки", "addToShortcuts": "Добавить в ярлыки",
"selectLocationToSave": "Выберите место для сохранения",
"downloadToDefaultLocation": "Скачать в место по умолчанию", "downloadToDefaultLocation": "Скачать в место по умолчанию",
"pasteFailed": "Вставка не удалась", "pasteFailed": "Вставка не удалась",
"noUndoableActions": "Нет действий для отмены", "noUndoableActions": "Нет действий для отмены",
@@ -1109,7 +1134,6 @@
"editPath": "Редактировать путь", "editPath": "Редактировать путь",
"confirm": "Подтвердить", "confirm": "Подтвердить",
"cancel": "Отмена", "cancel": "Отмена",
"folderName": "Имя папки",
"find": "Найти...", "find": "Найти...",
"replaceWith": "Заменить на...", "replaceWith": "Заменить на...",
"replace": "Заменить", "replace": "Заменить",
@@ -1135,23 +1159,18 @@
"outdent": "Уменьшить отступ", "outdent": "Уменьшить отступ",
"autoComplete": "Автозавершение", "autoComplete": "Автозавершение",
"imageLoadError": "Не удалось загрузить изображение", "imageLoadError": "Не удалось загрузить изображение",
"zoomIn": "Увеличить",
"zoomOut": "Уменьшить",
"rotate": "Повернуть", "rotate": "Повернуть",
"originalSize": "Оригинальный размер", "originalSize": "Оригинальный размер",
"startTyping": "Начните печатать...", "startTyping": "Начните печатать...",
"unknownSize": "Неизвестный размер", "unknownSize": "Неизвестный размер",
"fileIsEmpty": "Файл пуст", "fileIsEmpty": "Файл пуст",
"modified": "Изменен",
"largeFileWarning": "Предупреждение о большом файле", "largeFileWarning": "Предупреждение о большом файле",
"largeFileWarningDesc": "Этот файл имеет размер {{size}}, что может вызвать проблемы с производительностью при открытии как текста.", "largeFileWarningDesc": "Этот файл имеет размер {{size}}, что может вызвать проблемы с производительностью при открытии как текста.",
"fileNotFoundAndRemoved": "Файл \"{{name}}\" не найден и был удален из недавних/закрепленных файлов", "fileNotFoundAndRemoved": "Файл \"{{name}}\" не найден и был удален из недавних/закрепленных файлов",
"failedToLoadFile": "Не удалось загрузить файл: {{error}}", "failedToLoadFile": "Не удалось загрузить файл: {{error}}",
"serverErrorOccurred": "Произошла ошибка сервера. Пожалуйста, попробуйте позже.", "serverErrorOccurred": "Произошла ошибка сервера. Пожалуйста, попробуйте позже.",
"fileSavedSuccessfully": "Файл успешно сохранен",
"autoSaveFailed": "Автосохранение не удалось", "autoSaveFailed": "Автосохранение не удалось",
"fileAutoSaved": "Файл автосохранен", "fileAutoSaved": "Файл автосохранен",
"fileDownloadedSuccessfully": "Файл успешно скачан",
"moveFileFailed": "Не удалось переместить {{name}}", "moveFileFailed": "Не удалось переместить {{name}}",
"moveOperationFailed": "Операция перемещения не удалась", "moveOperationFailed": "Операция перемещения не удалась",
"canOnlyCompareFiles": "Можно сравнивать только два файла", "canOnlyCompareFiles": "Можно сравнивать только два файла",
@@ -1187,12 +1206,40 @@
"sshConnectionFailed": "SSH-подключение не удалось. Пожалуйста, проверьте ваше подключение к {{name}} ({{ip}}:{{port}})", "sshConnectionFailed": "SSH-подключение не удалось. Пожалуйста, проверьте ваше подключение к {{name}} ({{ip}}:{{port}})",
"loadFileFailed": "Не удалось загрузить файл: {{error}}", "loadFileFailed": "Не удалось загрузить файл: {{error}}",
"connectedSuccessfully": "Успешно подключено", "connectedSuccessfully": "Успешно подключено",
"totpVerificationFailed": "Проверка TOTP не удалась" "totpVerificationFailed": "Проверка TOTP не удалась",
"andMoreFiles": "and {{count}} more...",
"archiveExtractedSuccessfully": "{{name}} успешно извлечен",
"archiveName": "Имя архива",
"changePermissions": "Изменить права",
"changePermissionsDesc": "Изменить права файла для",
"compress": "Сжать",
"compressFailed": "Сжатие не удалось",
"compressFile": "Сжать файл",
"compressFiles": "Сжать файлы",
"compressFilesDesc": "Сжать {{count}} элементов в архив",
"compressingFiles": "Сжатие {{count}} элементов в {{name}}...",
"compressionFormat": "Формат сжатия",
"currentPermissions": "Текущие права",
"enterArchiveName": "Введите имя архива...",
"execute": "Выполнить",
"extractArchive": "Извлечь архив",
"extractFailed": "Извлечение не удалось",
"extractingArchive": "Извлечение {{name}}...",
"failedToChangePermissions": "Не удалось изменить права",
"filesCompressedSuccessfully": "{{name}} успешно создан",
"group": "Группа",
"newPermissions": "Новые права",
"others": "Другие",
"owner": "Владелец",
"permissionsChangedSuccessfully": "Права успешно изменены",
"read": "Чтение",
"selectedFiles": "Выбранные файлы",
"write": "Запись"
}, },
"tunnels": { "tunnels": {
"title": "SSH-туннели", "title": "SSH-туннели",
"noSshTunnels": "Нет SSH-туннелей", "noSshTunnels": "Нет SSH-туннелей",
"createFirstTunnelMessage": "Вы еще не создали SSH-туннели. Настройте туннельные подключения в Менеджере хостов, чтобы начать.", "createFirstTunnelMessage": "Создайте ваш первый SSH-туннель, чтобы начать. Используйте SSH-менеджер для добавления хостов с туннельными подключениями.",
"connected": "Подключено", "connected": "Подключено",
"disconnected": "Отключено", "disconnected": "Отключено",
"connecting": "Подключение...", "connecting": "Подключение...",
@@ -1233,17 +1280,8 @@
"local": "Локальный", "local": "Локальный",
"remote": "Удаленный", "remote": "Удаленный",
"dynamic": "Динамический", "dynamic": "Динамический",
"noSshTunnels": "Нет SSH-туннелей",
"createFirstTunnelMessage": "Создайте ваш первый SSH-туннель, чтобы начать. Используйте SSH-менеджер для добавления хостов с туннельными подключениями.",
"unknownConnectionStatus": "Неизвестно", "unknownConnectionStatus": "Неизвестно",
"connected": "Подключено",
"connecting": "Подключение...",
"disconnecting": "Отключение...",
"disconnected": "Отключено",
"portMapping": "Порт {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", "portMapping": "Порт {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
"disconnect": "Отключить",
"connect": "Подключить",
"canceling": "Отмена...",
"endpointHostNotFound": "Хост конечной точки не найден", "endpointHostNotFound": "Хост конечной точки не найден",
"discord": "Discord", "discord": "Discord",
"githubIssue": "Проблема на GitHub", "githubIssue": "Проблема на GitHub",
@@ -1256,7 +1294,7 @@
"disk": "Диск", "disk": "Диск",
"network": "Сеть", "network": "Сеть",
"uptime": "Время работы", "uptime": "Время работы",
"loadAverage": "Средняя загрузка", "loadAverage": "Средняя: {{avg1}}, {{avg5}}, {{avg15}}",
"processes": "Процессы", "processes": "Процессы",
"connections": "Подключения", "connections": "Подключения",
"usage": "Использование", "usage": "Использование",
@@ -1272,7 +1310,6 @@
"cpuCores_one": "{{count}} CPU", "cpuCores_one": "{{count}} CPU",
"cpuCores_other": "{{count}} CPU", "cpuCores_other": "{{count}} CPU",
"naCpus": "N/A CPU", "naCpus": "N/A CPU",
"loadAverage": "Средняя: {{avg1}}, {{avg5}}, {{avg15}}",
"loadAverageNA": "Средняя: N/A", "loadAverageNA": "Средняя: N/A",
"cpuUsage": "Использование CPU", "cpuUsage": "Использование CPU",
"memoryUsage": "Использование памяти", "memoryUsage": "Использование памяти",
@@ -1291,8 +1328,6 @@
"totpRequired": "Требуется TOTP-аутентификация", "totpRequired": "Требуется TOTP-аутентификация",
"totpUnavailable": "Статистика сервера недоступна для серверов с включенным TOTP", "totpUnavailable": "Статистика сервера недоступна для серверов с включенным TOTP",
"load": "Загрузка", "load": "Загрузка",
"free": "Свободно",
"available": "Доступно",
"editLayout": "Редактировать макет", "editLayout": "Редактировать макет",
"cancelEdit": "Отмена", "cancelEdit": "Отмена",
"addWidget": "Добавить виджет", "addWidget": "Добавить виджет",
@@ -1317,7 +1352,13 @@
"recentSuccessfulLogins": "Последние успешные входы", "recentSuccessfulLogins": "Последние успешные входы",
"recentFailedAttempts": "Последние неудачные попытки", "recentFailedAttempts": "Последние неудачные попытки",
"noRecentLoginData": "Нет данных о недавних входах", "noRecentLoginData": "Нет данных о недавних входах",
"from": "с" "from": "с",
"executeQuickAction": "Выполнить {{name}}",
"executingQuickAction": "Выполнение {{name}}...",
"quickActionError": "Не удалось выполнить {{name}}",
"quickActionFailed": "{{name}} завершилось ошибкой",
"quickActionSuccess": "{{name}} завершено успешно",
"quickActions": "Быстрые действия"
}, },
"auth": { "auth": {
"tagline": "SSH ТЕРМИНАЛ МЕНЕДЖЕР", "tagline": "SSH ТЕРМИНАЛ МЕНЕДЖЕР",
@@ -1407,7 +1448,27 @@
"signUp": "Зарегистрироваться", "signUp": "Зарегистрироваться",
"dataLossWarning": "Сброс пароля этим способом удалит все ваши сохраненные SSH-хосты, учетные данные и другие зашифрованные данные. Это действие нельзя отменить. Используйте это только если вы забыли пароль и не вошли в систему.", "dataLossWarning": "Сброс пароля этим способом удалит все ваши сохраненные SSH-хосты, учетные данные и другие зашифрованные данные. Это действие нельзя отменить. Используйте это только если вы забыли пароль и не вошли в систему.",
"authenticationDisabled": "Аутентификация отключена", "authenticationDisabled": "Аутентификация отключена",
"authenticationDisabledDesc": "Все методы аутентификации в настоящее время отключены. Пожалуйста, свяжитесь с вашим администратором." "authenticationDisabledDesc": "Все методы аутентификации в настоящее время отключены. Пожалуйста, свяжитесь с вашим администратором.",
"authenticating": "Аутентификация...",
"desktopApp": "Настольное приложение",
"loadingServer": "Загрузка сервера...",
"loggingInToDesktopApp": "Вход в настольное приложение",
"loggingInToDesktopAppViaWeb": "Вход в настольное приложение через веб-интерфейс",
"loggingInToMobileApp": "Вход в мобильное приложение",
"mobileApp": "Мобильное приложение",
"redirectingToApp": "Перенаправление в приложение...",
"sshAuthFailedDescription": "Предоставленные учетные данные неверны. Пожалуйста, попробуйте снова с правильными учетными данными.",
"sshAuthenticationFailed": "Аутентификация не удалась",
"sshAuthenticationRequired": "Требуется SSH-аутентификация",
"sshAuthenticationTimeout": "Тайм-аут аутентификации",
"sshKeyPasswordDescription": "Если ваш SSH-ключ зашифрован, введите парольную фразу здесь.",
"sshNoKeyboardInteractive": "Клавиатурная интерактивная аутентификация недоступна",
"sshNoKeyboardInteractiveDescription": "Сервер не поддерживает клавиатурную интерактивную аутентификацию. Пожалуйста, укажите ваш пароль или SSH-ключ.",
"sshPasswordDescription": "Введите пароль для этого SSH-подключения.",
"sshProvideCredentialsDescription": "Пожалуйста, предоставьте ваши SSH-учетные данные для подключения к этому серверу.",
"sshTimeoutDescription": "Попытка аутентификации истекла по времени. Пожалуйста, попробуйте снова.",
"passwordResetSuccess": "Сброс пароля прошел успешно",
"passwordResetSuccessDesc": "Ваш пароль был успешно сброшен. Теперь вы можете войти с новым паролем."
}, },
"errors": { "errors": {
"notFound": "Страница не найдена", "notFound": "Страница не найдена",
@@ -1484,9 +1545,12 @@
"fileColorCodingDesc": "Цветовая кодировка файлов по типу: папки (красный), файлы (синий), символические ссылки (зелёный)", "fileColorCodingDesc": "Цветовая кодировка файлов по типу: папки (красный), файлы (синий), символические ссылки (зелёный)",
"commandAutocomplete": "Автодополнение команд", "commandAutocomplete": "Автодополнение команд",
"commandAutocompleteDesc": "Включить автодополнение команд терминала клавишей Tab на основе вашей истории команд", "commandAutocompleteDesc": "Включить автодополнение команд терминала клавишей Tab на основе вашей истории команд",
"defaultSnippetFoldersCollapsed": "Сворачивать папки сниппетов по умолчанию",
"defaultSnippetFoldersCollapsedDesc": "Если включено, все папки сниппетов будут свёрнуты при открытии вкладки сниппетов",
"currentPassword": "Текущий пароль", "currentPassword": "Текущий пароль",
"passwordChangedSuccess": "Пароль успешно изменен! Пожалуйста, войдите снова.", "passwordChangedSuccess": "Пароль успешно изменен! Пожалуйста, войдите снова.",
"failedToChangePassword": "Не удалось изменить пароль. Пожалуйста, проверьте ваш текущий пароль и попробуйте снова." "failedToChangePassword": "Не удалось изменить пароль. Пожалуйста, проверьте ваш текущий пароль и попробуйте снова.",
"externalAndLocal": "Двойная аутентификация"
}, },
"user": { "user": {
"failedToLoadVersionInfo": "Не удалось загрузить информацию о версии" "failedToLoadVersionInfo": "Не удалось загрузить информацию о версии"
@@ -1549,7 +1613,8 @@
"lastAdminWarning": "Вы последний пользователь-администратор. Вы не можете удалить свою учетную запись, так как это оставит систему без администраторов. Пожалуйста, сначала сделайте другого пользователя администратором или свяжитесь с поддержкой системы.", "lastAdminWarning": "Вы последний пользователь-администратор. Вы не можете удалить свою учетную запись, так как это оставит систему без администраторов. Пожалуйста, сначала сделайте другого пользователя администратором или свяжитесь с поддержкой системы.",
"confirmPassword": "Подтвердите пароль", "confirmPassword": "Подтвердите пароль",
"deleting": "Удаление...", "deleting": "Удаление...",
"cancel": "Отмена" "cancel": "Отмена",
"deleteAccountWarningShort": "Это действие необратимо и приведет к окончательному удалению вашей учетной записи."
}, },
"interface": { "interface": {
"sidebar": "Боковая панель", "sidebar": "Боковая панель",
@@ -1569,7 +1634,6 @@
"deleteItem": "Удалить элемент", "deleteItem": "Удалить элемент",
"createNewFile": "Создать новый файл", "createNewFile": "Создать новый файл",
"createNewFolder": "Создать новую папку", "createNewFolder": "Создать новую папку",
"deleteItem": "Удалить элемент",
"renameItem": "Переименовать элемент", "renameItem": "Переименовать элемент",
"clickToSelectFile": "Нажмите для выбора файла", "clickToSelectFile": "Нажмите для выбора файла",
"noSshHosts": "Нет SSH-хостов", "noSshHosts": "Нет SSH-хостов",

View File

@@ -1,7 +1,6 @@
{ {
"credentials": { "credentials": {
"credentialsViewer": "凭证查看器", "credentialsViewer": "凭证查看器",
"credentialsManager": "凭据管理器",
"manageYourSSHCredentials": "安全管理您的SSH凭据", "manageYourSSHCredentials": "安全管理您的SSH凭据",
"addCredential": "添加凭据", "addCredential": "添加凭据",
"createCredential": "创建凭据", "createCredential": "创建凭据",
@@ -164,7 +163,9 @@
"failedToGenerateKeyPair": "生成密钥对失败", "failedToGenerateKeyPair": "生成密钥对失败",
"generateKeyPairNote": "直接生成新的SSH密钥对。这将替换表单中的现有密钥。", "generateKeyPairNote": "直接生成新的SSH密钥对。这将替换表单中的现有密钥。",
"invalidKey": "无效密钥", "invalidKey": "无效密钥",
"detectionError": "检测错误" "detectionError": "检测错误",
"credentialId": "凭据 ID",
"unknown": "未知"
}, },
"dragIndicator": { "dragIndicator": {
"error": "错误:{{error}}", "error": "错误:{{error}}",
@@ -259,7 +260,11 @@
"saveError": "保存配置时出错", "saveError": "保存配置时出错",
"saving": "保存中...", "saving": "保存中...",
"saveConfig": "保存配置", "saveConfig": "保存配置",
"helpText": "输入您的 Termix 服务器运行地址例如http://localhost:30001 或 https://your-server.com" "helpText": "输入您的 Termix 服务器运行地址例如http://localhost:30001 或 https://your-server.com",
"changeServer": "更换服务器",
"mustIncludeProtocol": "服务器URL必须以 http:// 或 https:// 开头",
"notValidatedWarning": "URL 未经验证 - 请确保其正确",
"warning": "警告"
}, },
"versionCheck": { "versionCheck": {
"error": "版本检查错误", "error": "版本检查错误",
@@ -335,13 +340,11 @@
"login": "登录", "login": "登录",
"logout": "登出", "logout": "登出",
"register": "注册", "register": "注册",
"username": "用户名",
"password": "密码", "password": "密码",
"confirmPassword": "确认密码", "confirmPassword": "确认密码",
"back": "返回", "back": "返回",
"email": "邮箱", "email": "邮箱",
"submit": "提交", "submit": "提交",
"cancel": "取消",
"change": "更改", "change": "更改",
"save": "保存", "save": "保存",
"delete": "删除", "delete": "删除",
@@ -382,7 +385,9 @@
"documentation": "文档", "documentation": "文档",
"retry": "重试", "retry": "重试",
"checking": "检查中...", "checking": "检查中...",
"checkingDatabase": "正在检查数据库连接..." "checkingDatabase": "正在检查数据库连接...",
"saving": "保存中...",
"version": "Version"
}, },
"nav": { "nav": {
"home": "首页", "home": "首页",
@@ -511,7 +516,7 @@
"loadingEncryptionStatus": "正在加载加密状态...", "loadingEncryptionStatus": "正在加载加密状态...",
"testMigrationDescription": "验证现有数据是否可以安全地迁移到加密格式,不会实际修改任何数据", "testMigrationDescription": "验证现有数据是否可以安全地迁移到加密格式,不会实际修改任何数据",
"serverMigrationGuide": "服务器迁移指南", "serverMigrationGuide": "服务器迁移指南",
"migrationInstructions": "要将加密数据迁移到新服务器1) 备份数据库文件2) 在新服务器设置环境变量 DB_ENCRYPTION_KEY=\"你的密钥\"3) 恢复数据库文件", "migrationInstructions": "要将加密数据迁移到新服务器1) 备份数据库文件2) 在新服务器设置环境变量 DB_ENCRYPTION_KEY=\"你的key\"3) 恢复数据库文件",
"environmentProtection": "环境保护", "environmentProtection": "环境保护",
"environmentProtectionDesc": "基于服务器环境信息(主机名、路径等)保护加密密钥,可通过环境变量实现迁移", "environmentProtectionDesc": "基于服务器环境信息(主机名、路径等)保护加密密钥,可通过环境变量实现迁移",
"verificationCompleted": "兼容性验证完成 - 未修改任何数据", "verificationCompleted": "兼容性验证完成 - 未修改任何数据",
@@ -595,7 +600,32 @@
"passwordLoginDisabledWarning": "密码登录已禁用。请确保 OIDC 已正确配置,否则您将无法登录 Termix。", "passwordLoginDisabledWarning": "密码登录已禁用。请确保 OIDC 已正确配置,否则您将无法登录 Termix。",
"oidcRequiredWarning": "严重警告:密码登录已禁用。如果您重置或错误配置 OIDC您将失去对 Termix 的所有访问权限并使您的实例无法使用。只有在您完全确定的情况下才能继续。", "oidcRequiredWarning": "严重警告:密码登录已禁用。如果您重置或错误配置 OIDC您将失去对 Termix 的所有访问权限并使您的实例无法使用。只有在您完全确定的情况下才能继续。",
"confirmDisableOIDCWarning": "警告:您即将在密码登录也已禁用的情况下禁用 OIDC。这将使您的 Termix 实例无法使用,您将失去所有访问权限。您确定要继续吗?", "confirmDisableOIDCWarning": "警告:您即将在密码登录也已禁用的情况下禁用 OIDC。这将使您的 Termix 实例无法使用,您将失去所有访问权限。您确定要继续吗?",
"failedToUpdatePasswordLoginStatus": "更新密码登录状态失败" "failedToUpdatePasswordLoginStatus": "更新密码登录状态失败",
"accountsLinkedSuccessfully": "OIDC 用户 {{oidcUsername}} 已关联到 {{targetUsername}}",
"confirmRevokeAllSessions": "您确定要撤销此用户的所有会话吗?",
"confirmRevokeSession": "您确定要撤销此会话吗?",
"failedToFetchSessions": "获取会话失败",
"failedToLinkAccounts": "关联账户失败",
"failedToRevokeSession": "撤销会话失败",
"failedToRevokeSessions": "撤销会话失败",
"failedToUnlinkOIDC": "取消 OIDC 关联失败",
"linkAccountsButton": "关联账户",
"linkOIDCActionAddCapability": "将 OIDC 登录功能添加到目标密码账户",
"linkOIDCActionDeleteUser": "删除 OIDC 用户账户及其所有数据",
"linkOIDCActionDualAuth": "允许密码账户同时使用密码和 OIDC 登录",
"linkOIDCDialogDescription": "将 {{username}} (OIDC 用户) 关联到现有的密码账户。这将为密码账户启用双重认证。",
"linkOIDCDialogTitle": "将 OIDC 账户关联到密码账户",
"linkOIDCWarningTitle": "警告: OIDC 用户数据将被删除",
"linkTargetUsernameLabel": "目标密码账户用户名",
"linkTargetUsernamePlaceholder": "输入密码账户的用户名",
"linkTargetUsernameRequired": "目标用户名是必需的",
"linkToPasswordAccount": "关联到密码账户",
"linkingAccounts": "关联中...",
"sessionRevokedSuccessfully": "会话撤销成功",
"sessionsRevokedSuccessfully": "会话撤销成功",
"unlinkOIDCDescription": "移除 {{username}} 的 OIDC 认证?此操作后用户只能使用用户名/密码登录。",
"unlinkOIDCSuccess": "已取消 {{username}} 的 OIDC 关联",
"unlinkOIDCTitle": "取消 OIDC 认证关联"
}, },
"hosts": { "hosts": {
"title": "主机管理", "title": "主机管理",
@@ -632,7 +662,6 @@
"port": "端口", "port": "端口",
"name": "名称", "name": "名称",
"username": "用户名", "username": "用户名",
"hostName": "主机名",
"folder": "文件夹", "folder": "文件夹",
"tags": "标签", "tags": "标签",
"passwordRequired": "使用密码认证时需要密码", "passwordRequired": "使用密码认证时需要密码",
@@ -642,10 +671,6 @@
"addHost": "添加主机", "addHost": "添加主机",
"editHost": "编辑主机", "editHost": "编辑主机",
"cloneHost": "克隆主机", "cloneHost": "克隆主机",
"deleteHost": "删除主机",
"authType": "认证类型",
"passwordAuth": "密码",
"keyAuth": "SSH 密钥",
"keyPassword": "密钥密码", "keyPassword": "密钥密码",
"keyType": "密钥类型", "keyType": "密钥类型",
"pin": "固定", "pin": "固定",
@@ -653,15 +678,6 @@
"enableTunnel": "启用隧道", "enableTunnel": "启用隧道",
"enableFileManager": "启用文件管理器", "enableFileManager": "启用文件管理器",
"defaultPath": "默认路径", "defaultPath": "默认路径",
"testConnection": "测试连接",
"connect": "连接",
"disconnect": "断开连接",
"connected": "已连接",
"disconnected": "已断开",
"connecting": "连接中...",
"connectionFailed": "连接失败",
"connectionSuccess": "连接成功",
"addTags": "添加标签(空格添加)",
"sourcePort": "源端口", "sourcePort": "源端口",
"sourcePortDesc": "(源指通用标签页中的当前连接详情)", "sourcePortDesc": "(源指通用标签页中的当前连接详情)",
"endpointPort": "目标端口", "endpointPort": "目标端口",
@@ -671,20 +687,7 @@
"remove": "移除", "remove": "移除",
"addConnection": "添加连接", "addConnection": "添加连接",
"sshpassRequired": "密码认证需要安装 Sshpass", "sshpassRequired": "密码认证需要安装 Sshpass",
"sshpassInstallCommand": "安装命令sudo apt install sshpass",
"sshServerConfig": "需要配置 SSH 服务器",
"sshServerConfigInstructions": "运行以下命令以允许密码认证:",
"sshConfigCommand1": "sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
"sshConfigCommand2": "sudo systemctl restart sshd",
"localPortForwarding": "本地端口转发",
"localPortForwardingDesc": "通过 SSH 连接将本地端口转发到远程服务器",
"remotePortForwarding": "远程端口转发",
"remotePortForwardingDesc": "通过 SSH 连接将远程端口转发到本地服务器",
"dynamicPortForwarding": "动态端口转发SOCKS 代理)",
"dynamicPortForwardingDesc": "在本地计算机上创建 SOCKS 代理,通过 SSH 连接路由流量",
"bindAddress": "绑定地址",
"hostViewer": "主机查看器", "hostViewer": "主机查看器",
"configuration": "配置",
"maxRetries": "最大重试次数", "maxRetries": "最大重试次数",
"tunnelConnections": "隧道连接", "tunnelConnections": "隧道连接",
"enableTerminalDesc": "启用/禁用在终端选项卡中显示此主机", "enableTerminalDesc": "启用/禁用在终端选项卡中显示此主机",
@@ -693,8 +696,6 @@
"autoStartDesc": "容器启动时自动启动此隧道", "autoStartDesc": "容器启动时自动启动此隧道",
"defaultPathDesc": "打开此主机文件管理器时的默认目录", "defaultPathDesc": "打开此主机文件管理器时的默认目录",
"tunnelForwardDescription": "此隧道将从源计算机(常规选项卡中的当前连接详情)的端口 {{sourcePort}} 转发流量到端点计算机的端口 {{endpointPort}}。", "tunnelForwardDescription": "此隧道将从源计算机(常规选项卡中的当前连接详情)的端口 {{sourcePort}} 转发流量到端点计算机的端口 {{endpointPort}}。",
"endpointSshConfiguration": "端点 SSH 配置",
"sourcePortDescription": "(源指的是常规选项卡中的当前连接详情)",
"autoStartContainer": "容器启动时自动启动", "autoStartContainer": "容器启动时自动启动",
"upload": "上传", "upload": "上传",
"authentication": "认证方式", "authentication": "认证方式",
@@ -715,20 +716,12 @@
"centosRhelFedora": "CentOS/RHEL/Fedora", "centosRhelFedora": "CentOS/RHEL/Fedora",
"macos": "macOS", "macos": "macOS",
"windows": "Windows", "windows": "Windows",
"sshpassOSInstructions": { "sshpassOSInstructions": {},
"centos": "CentOS/RHEL/Fedora: sudo yum install sshpass 或 sudo dnf install sshpass",
"macos": "macOS: brew install hudochenkov/sshpass/sshpass",
"windows": "Windows: 使用 WSL 或考虑使用 SSH 密钥认证"
},
"sshServerConfigRequired": "SSH 服务器配置要求", "sshServerConfigRequired": "SSH 服务器配置要求",
"sshServerConfigDesc": "对于隧道连接SSH 服务器必须配置允许端口转发:", "sshServerConfigDesc": "对于隧道连接SSH 服务器必须配置允许端口转发:",
"gatewayPortsYes": "绑定远程端口到所有接口", "gatewayPortsYes": "绑定远程端口到所有接口",
"allowTcpForwardingYes": "启用端口转发", "allowTcpForwardingYes": "启用端口转发",
"permitRootLoginYes": "如果使用 root 用户进行隧道连接", "permitRootLoginYes": "如果使用 root 用户进行隧道连接",
"sshServerConfigReverse": "对于反向 SSH 隧道,端点 SSH 服务器必须允许:",
"gatewayPorts": "GatewayPorts yes绑定远程端口",
"allowTcpForwarding": "AllowTcpForwarding yes端口转发",
"permitRootLogin": "PermitRootLogin yes如果使用 root",
"editSshConfig": "编辑 /etc/ssh/sshd_config 并重启 SSH: sudo systemctl restart sshd", "editSshConfig": "编辑 /etc/ssh/sshd_config 并重启 SSH: sudo systemctl restart sshd",
"updateHost": "更新主机", "updateHost": "更新主机",
"hostUpdatedSuccessfully": "主机 \"{{name}}\" 更新成功!", "hostUpdatedSuccessfully": "主机 \"{{name}}\" 更新成功!",
@@ -758,7 +751,6 @@
"tunnel": "隧道", "tunnel": "隧道",
"fileManager": "文件管理器", "fileManager": "文件管理器",
"serverStats": "服务器统计", "serverStats": "服务器统计",
"hostViewer": "主机查看器",
"enableServerStats": "启用服务器统计", "enableServerStats": "启用服务器统计",
"enableServerStatsDesc": "启用/禁用此主机的服务器统计信息收集", "enableServerStatsDesc": "启用/禁用此主机的服务器统计信息收集",
"displayItems": "显示项目", "displayItems": "显示项目",
@@ -893,13 +885,21 @@
"searchServers": "搜索服务器...", "searchServers": "搜索服务器...",
"noServerFound": "未找到服务器", "noServerFound": "未找到服务器",
"jumpHostsOrder": "连接将按顺序进行:跳板主机 1 → 跳板主机 2 → ... → 目标服务器", "jumpHostsOrder": "连接将按顺序进行:跳板主机 1 → 跳板主机 2 → ... → 目标服务器",
"advancedAuthSettings": "高级身份验证设置" "advancedAuthSettings": "高级身份验证设置",
"addQuickAction": "添加 Quick Action",
"noSnippetFound": "没有 snippet found",
"quickActionName": "Action 名称",
"quickActions": "Quick Actions",
"quickActionsDescription": "Quick actions allow you to 创建 custom buttons that execute SSH snippets on this server. These buttons will appear at the top of the Server Stats page for quick access.",
"quickActionsList": "Quick Actions List",
"quickActionsOrder": "Quick action buttons will appear in the order listed above on the Server Stats page",
"sshpassRequiredDesc": "For 密码 认证 in tunnels, sshpass must be installed on the system."
}, },
"terminal": { "terminal": {
"title": "终端", "title": "终端",
"terminalTitle": "终端 - {{host}}", "terminalTitle": "终端 - {{host}}",
"terminalWithPath": "终端 - {{host}}:{{path}}", "terminalWithPath": "终端 - {{host}}:{{path}}",
"runTitle": "运行 {{command}} - {{host}}", "runTitle": "运行 {{command}} - {{name}}",
"totpRequired": "需要双因素认证", "totpRequired": "需要双因素认证",
"totpCodeLabel": "验证码", "totpCodeLabel": "验证码",
"totpPlaceholder": "000000", "totpPlaceholder": "000000",
@@ -931,7 +931,11 @@
"reconnecting": "重新连接中... ({{attempt}}/{{max}})", "reconnecting": "重新连接中... ({{attempt}}/{{max}})",
"reconnected": "重新连接成功", "reconnected": "重新连接成功",
"maxReconnectAttemptsReached": "已达到最大重连尝试次数", "maxReconnectAttemptsReached": "已达到最大重连尝试次数",
"connectionTimeout": "连接超时" "connectionTimeout": "连接超时",
"sudoPasswordPopupTitle": "插入密码?",
"sudoPasswordPopupHint": "按 Enter 插入Esc 取消",
"sudoPasswordPopupConfirm": "插入",
"sudoPasswordPopupDismiss": "取消"
}, },
"fileManager": { "fileManager": {
"title": "文件管理器", "title": "文件管理器",
@@ -1222,7 +1226,16 @@
"write": "写入", "write": "写入",
"execute": "执行", "execute": "执行",
"permissionsChangedSuccessfully": "权限修改成功", "permissionsChangedSuccessfully": "权限修改成功",
"failedToChangePermissions": "权限修改失败" "failedToChangePermissions": "权限修改失败",
"autoSaveFailed": "自动保存失败",
"delete": "删除",
"download": "下载",
"fileAutoSaved": "文件已自动保存",
"fileDownloadedSuccessfully": "文件 \"{{name}}\" 下载成功",
"fileSavedSuccessfully": "文件保存成功",
"path": "Path",
"permissions": "Permissions",
"size": "Size"
}, },
"tunnels": { "tunnels": {
"title": "SSH 隧道", "title": "SSH 隧道",
@@ -1272,7 +1285,8 @@
"endpointHostNotFound": "未找到端点主机", "endpointHostNotFound": "未找到端点主机",
"discord": "Discord", "discord": "Discord",
"githubIssue": "GitHub 问题", "githubIssue": "GitHub 问题",
"forHelp": "寻求帮助" "forHelp": "寻求帮助",
"unknownConnectionStatus": "Unk没有wn"
}, },
"serverStats": { "serverStats": {
"title": "服务器统计", "title": "服务器统计",
@@ -1314,8 +1328,6 @@
"totpRequired": "需要 TOTP 认证", "totpRequired": "需要 TOTP 认证",
"totpUnavailable": "启用了 TOTP 的服务器无法使用服务器统计功能", "totpUnavailable": "启用了 TOTP 的服务器无法使用服务器统计功能",
"load": "负载", "load": "负载",
"free": "空闲",
"available": "可用",
"editLayout": "编辑布局", "editLayout": "编辑布局",
"cancelEdit": "取消", "cancelEdit": "取消",
"addWidget": "添加小组件", "addWidget": "添加小组件",
@@ -1340,7 +1352,14 @@
"recentSuccessfulLogins": "最近成功登录", "recentSuccessfulLogins": "最近成功登录",
"recentFailedAttempts": "最近失败尝试", "recentFailedAttempts": "最近失败尝试",
"noRecentLoginData": "无最近登录数据", "noRecentLoginData": "无最近登录数据",
"from": "来自" "from": "来自",
"executeQuickAction": "执行 {{name}}",
"executingQuickAction": "执行中 {{name}}...",
"failedToFetchHomeData": "获取主页数据失败",
"quickActionError": "无法执行 {{name}}",
"quickActionFailed": "{{name}} 失败",
"quickActionSuccess": "{{name}} 完成成功",
"quickActions": "Quick Actions"
}, },
"auth": { "auth": {
"tagline": "SSH 终端管理器", "tagline": "SSH 终端管理器",
@@ -1440,7 +1459,17 @@
"sshTimeoutDescription": "身份验证尝试超时。请重试。", "sshTimeoutDescription": "身份验证尝试超时。请重试。",
"sshProvideCredentialsDescription": "请提供您的 SSH 凭据以连接到此服务器。", "sshProvideCredentialsDescription": "请提供您的 SSH 凭据以连接到此服务器。",
"sshPasswordDescription": "输入此 SSH 连接的密码。", "sshPasswordDescription": "输入此 SSH 连接的密码。",
"sshKeyPasswordDescription": "如果您的 SSH 密钥已加密,请在此处输入密码。" "sshKeyPasswordDescription": "如果您的 SSH 密钥已加密,请在此处输入密码。",
"authenticating": "Authenticating...",
"authenticationDisabled": "认证已禁用",
"authenticationDisabledDesc": "所有认证方式当前已禁用。请联系您的管理员。",
"desktopApp": "桌面应用",
"loadingServer": "加载服务器中...",
"loggingInToDesktopApp": "登录桌面应用",
"loggingInToDesktopAppViaWeb": "通过网页界面登录桌面应用",
"loggingInToMobileApp": "登录移动应用",
"mobileApp": "移动应用",
"redirectingToApp": "重定向到应用..."
}, },
"errors": { "errors": {
"notFound": "页面未找到", "notFound": "页面未找到",
@@ -1517,9 +1546,12 @@
"fileColorCodingDesc": "按类型对文件进行颜色编码:文件夹(红色)、文件(蓝色)、符号链接(绿色)", "fileColorCodingDesc": "按类型对文件进行颜色编码:文件夹(红色)、文件(蓝色)、符号链接(绿色)",
"commandAutocomplete": "命令自动补全", "commandAutocomplete": "命令自动补全",
"commandAutocompleteDesc": "启用基于命令历史记录的 Tab 键终端命令自动补全建议", "commandAutocompleteDesc": "启用基于命令历史记录的 Tab 键终端命令自动补全建议",
"defaultSnippetFoldersCollapsed": "默认折叠代码片段文件夹",
"defaultSnippetFoldersCollapsedDesc": "启用后,打开代码片段标签时所有文件夹将默认折叠",
"currentPassword": "当前密码", "currentPassword": "当前密码",
"passwordChangedSuccess": "密码修改成功!请重新登录。", "passwordChangedSuccess": "密码修改成功!请重新登录。",
"failedToChangePassword": "修改密码失败。请检查您当前的密码并重试。" "failedToChangePassword": "修改密码失败。请检查您当前的密码并重试。",
"externalAndLocal": "Dual Auth"
}, },
"user": { "user": {
"failedToLoadVersionInfo": "加载版本信息失败" "failedToLoadVersionInfo": "加载版本信息失败"
@@ -1549,7 +1581,7 @@
"redirectUrl": "https://your-provider.com/application/o/termix/", "redirectUrl": "https://your-provider.com/application/o/termix/",
"tokenUrl": "https://your-provider.com/application/o/token/", "tokenUrl": "https://your-provider.com/application/o/token/",
"userIdField": "sub", "userIdField": "sub",
"usernameField": "name", "usernameField": "名称",
"scopes": "openid email profile", "scopes": "openid email profile",
"userinfoUrl": "https://your-provider.com/application/o/userinfo/", "userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"enterUsername": "输入用户名以设为管理员", "enterUsername": "输入用户名以设为管理员",
@@ -1571,9 +1603,9 @@
"passwordRequired": "需要输入密码", "passwordRequired": "需要输入密码",
"failedToDeleteAccount": "删除账户失败", "failedToDeleteAccount": "删除账户失败",
"failedToMakeUserAdmin": "设为管理员失败", "failedToMakeUserAdmin": "设为管理员失败",
"userIsNowAdmin": "用户 {{username}} 现在是管理员", "userIsNowAdmin": "用户 {{用户名}} 现在是管理员",
"removeAdminConfirm": "确定要移除 {{username}} 的管理员权限吗?", "removeAdminConfirm": "确定要移除 {{用户名}} 的管理员权限吗?",
"deleteUserConfirm": "确定要删除用户 {{username}} 吗?此操作无法撤销。", "deleteUserConfirm": "确定要删除用户 {{用户名}} 吗?此操作无法撤销。",
"deleteAccount": "删除账户", "deleteAccount": "删除账户",
"closeDeleteAccount": "关闭删除账户", "closeDeleteAccount": "关闭删除账户",
"deleteAccountWarning": "此操作无法撤销。这将永久删除您的账户和所有相关数据。", "deleteAccountWarning": "此操作无法撤销。这将永久删除您的账户和所有相关数据。",
@@ -1620,7 +1652,76 @@
"failedToStartOidcLogin": "启动 OIDC 登录失败", "failedToStartOidcLogin": "启动 OIDC 登录失败",
"failedToGetUserInfoAfterOidc": "OIDC 登录后获取用户信息失败", "failedToGetUserInfoAfterOidc": "OIDC 登录后获取用户信息失败",
"loginWithExternalProvider": "使用外部提供者登录", "loginWithExternalProvider": "使用外部提供者登录",
"failedToStartTotpSetup": "启动 TOTP 设置失败" "failedToStartTotpSetup": "启动 TOTP 设置失败",
"addHost": "添加 主机",
"adding": "添加ing...",
"authentication": "认证",
"cannotDeleteAccount": "Can没有t 删除 Account",
"clickToSelectFile": "Click to 选择 a 文件",
"clientId": "Client ID",
"clientSecret": "Client Secret",
"closeDeleteAccount": "关闭 删除 Account",
"configureExternalProvider": "Configure external identity provider for",
"confirmPassword": "Confirm 密码",
"connected": "已连接",
"createNewFile": "创建 New 文件",
"createNewFolder": "创建 New 文件夹",
"defaultPath": "Default Path",
"deleteAccount": "删除 Account",
"deleteItem": "删除 Item",
"deleting": "删除中...",
"disconnected": "已断开",
"editHost": "编辑 主机",
"enableFileManager": "启用 文件 Manager",
"enableTerminal": "启用 终端",
"enableTunnel": "启用 隧道",
"endpointHostNotFound": "Endpoint host 未找到",
"external": "External",
"failedToCompletePasswordReset": "无法 完成 密码 reset",
"failedToDisableTotp": "无法 禁用 TOTP",
"failedToGenerateBackupCodes": "无法 generate 返回up codes",
"failedToInitiatePasswordReset": "无法 initiate 密码 reset",
"failedToMakeUserAdmin": "无法 make 用户 管理员",
"failedToUpdateOidcConfig": "无法 更新 OIDC 配置",
"failedToVerifyResetCode": "无法 verify reset code",
"invalidTotpCode": "Invalid TOTP code",
"invalidVerificationCode": "Invalid verification code",
"key": "密钥",
"keyPassword": "密钥 密码",
"keyType": "密钥 Type",
"keyTypeRequired": "密钥 Type 是必需的 when using 密钥 认证",
"loading": "加载中...",
"local": "Local",
"login": "Login",
"loginWithExternal": "Login with External Provider",
"makeAdmin": "Make 管理员",
"maxRetries": "Max Retries",
"newFile": "New 文件",
"newFolder": "New 文件夹",
"password": "密码",
"passwordRequired": "密码 是必需的 when using 密码 认证",
"refresh": "刷新",
"renameItem": "Re名称 Item",
"resetPassword": "Reset 密码",
"retryingConnection": "重试ing 连接",
"saveConfiguration": "保存 配置",
"saving": "保存中...",
"sendResetCode": "Send Reset Code",
"signUp": "Sign Up",
"sshHosts": "SSH 主机s",
"sshKeyRequired": "SSH Private 密钥 是必需的 when using 密钥 认证",
"sshPrivateKey": "SSH Private 密钥",
"tunnelConnections": "隧道 连接s",
"unknown": "Unk没有wn",
"unknownError": "Unk没有wn 错误",
"updateHost": "更新 主机",
"updateKey": "更新 密钥",
"upload": "上传",
"user": "用户",
"verifyAndEnable": "Verify and 启用",
"verifyCode": "Verify Code",
"waitingForRetry": "Waiting for 重试",
"warning": "警告"
}, },
"mobile": { "mobile": {
"selectHostToStart": "选择一个主机以开始您的终端会话", "selectHostToStart": "选择一个主机以开始您的终端会话",

109
src/types/guacamole-common-js.d.ts vendored Normal file
View File

@@ -0,0 +1,109 @@
declare module "guacamole-common-js" {
namespace Guacamole {
class Client {
constructor(tunnel: Tunnel);
connect(data?: string): void;
disconnect(): void;
getDisplay(): Display;
sendKeyEvent(pressed: number, keysym: number): void;
sendMouseState(state: Mouse.State): void;
setClipboard(stream: OutputStream, mimetype: string): void;
createClipboardStream(mimetype: string): OutputStream;
onstatechange: ((state: number) => void) | null;
onerror: ((error: Status) => void) | null;
onclipboard: ((stream: InputStream, mimetype: string) => void) | null;
}
class Display {
getElement(): HTMLElement;
getWidth(): number;
getHeight(): number;
scale(scale: number): void;
onresize: (() => void) | null;
}
class Tunnel {
onerror: ((status: Status) => void) | null;
onstatechange: ((state: number) => void) | null;
}
class WebSocketTunnel extends Tunnel {
constructor(url: string);
}
class Mouse {
constructor(element: HTMLElement);
onmousedown: ((state: Mouse.State) => void) | null;
onmouseup: ((state: Mouse.State) => void) | null;
onmousemove: ((state: Mouse.State) => void) | null;
onmouseout: ((state: Mouse.State) => void) | null;
}
namespace Mouse {
class State {
constructor(
x: number,
y: number,
left?: boolean,
middle?: boolean,
right?: boolean,
up?: boolean,
down?: boolean
);
constructor(state: {
x: number;
y: number;
left?: boolean;
middle?: boolean;
right?: boolean;
up?: boolean;
down?: boolean;
});
x: number;
y: number;
left: boolean;
middle: boolean;
right: boolean;
up: boolean;
down: boolean;
}
}
class Keyboard {
constructor(element: Document | HTMLElement);
onkeydown: ((keysym: number) => void) | null;
onkeyup: ((keysym: number) => void) | null;
}
class Status {
code: number;
message: string;
isError(): boolean;
}
class InputStream {
onblob: ((data: string) => void) | null;
onend: (() => void) | null;
}
class OutputStream {
sendBlob(data: string): void;
sendEnd(): void;
}
class StringReader {
constructor(stream: InputStream);
ontext: ((text: string) => void) | null;
onend: (() => void) | null;
}
class StringWriter {
constructor(stream: OutputStream);
sendText(text: string): void;
sendEnd(): void;
}
}
export default Guacamole;
}

View File

@@ -14,8 +14,113 @@ export interface QuickAction {
snippetId: number; snippetId: number;
} }
export interface DockerConfig {
connectionType: "socket" | "tcp" | "tls";
socketPath?: string;
host?: string;
port?: number;
tlsVerify?: boolean;
tlsCaCert?: string;
tlsCert?: string;
tlsKey?: string;
}
export type HostConnectionType = "ssh" | "rdp" | "vnc" | "telnet";
// Guacamole configuration for RDP/VNC/Telnet connections
export interface GuacamoleConfig {
// Display settings
colorDepth?: number;
width?: number;
height?: number;
dpi?: number;
resizeMethod?: string;
forceLossless?: boolean;
// Audio settings
disableAudio?: boolean;
enableAudioInput?: boolean;
// RDP Performance settings
enableWallpaper?: boolean;
enableTheming?: boolean;
enableFontSmoothing?: boolean;
enableFullWindowDrag?: boolean;
enableDesktopComposition?: boolean;
enableMenuAnimations?: boolean;
disableBitmapCaching?: boolean;
disableOffscreenCaching?: boolean;
disableGlyphCaching?: boolean;
disableGfx?: boolean;
// RDP Device redirection
enablePrinting?: boolean;
printerName?: string;
enableDrive?: boolean;
driveName?: string;
drivePath?: string;
createDrivePath?: boolean;
disableDownload?: boolean;
disableUpload?: boolean;
enableTouch?: boolean;
// RDP Session settings
clientName?: string;
console?: boolean;
initialProgram?: string;
serverLayout?: string;
timezone?: string;
// RDP Gateway settings
gatewayHostname?: string;
gatewayPort?: number;
gatewayUsername?: string;
gatewayPassword?: string;
gatewayDomain?: string;
// RDP RemoteApp settings
remoteApp?: string;
remoteAppDir?: string;
remoteAppArgs?: string;
// RDP Preconnection settings (Hyper-V)
preconnectionId?: number;
preconnectionBlob?: string;
// RDP Load balancing
loadBalanceInfo?: string;
// Clipboard settings
normalizeClipboard?: string;
disableCopy?: boolean;
disablePaste?: boolean;
// VNC specific settings
cursor?: string;
swapRedBlue?: boolean;
readOnly?: boolean;
// VNC Repeater settings
destHost?: string;
destPort?: number;
// VNC Reverse connection
reverseConnect?: boolean;
listenTimeout?: number;
// Common SFTP settings (for RDP/VNC file transfer)
enableSftp?: boolean;
sftpHostname?: string;
sftpPort?: number;
sftpUsername?: string;
sftpPassword?: string;
sftpPrivateKey?: string;
sftpDirectory?: string;
// Recording settings
recordingPath?: string;
recordingName?: string;
createRecordingPath?: boolean;
recordingExcludeOutput?: boolean;
recordingExcludeMouse?: boolean;
recordingIncludeKeys?: boolean;
// Wake-on-LAN settings
wolSendPacket?: boolean;
wolMacAddr?: string;
wolBroadcastAddr?: string;
wolUdpPort?: number;
wolWaitTime?: number;
}
export interface SSHHost { export interface SSHHost {
id: number; id: number;
connectionType: HostConnectionType;
name: string; name: string;
ip: string; ip: string;
port: number; port: number;
@@ -40,12 +145,20 @@ export interface SSHHost {
enableTerminal: boolean; enableTerminal: boolean;
enableTunnel: boolean; enableTunnel: boolean;
enableFileManager: boolean; enableFileManager: boolean;
enableDocker: boolean;
defaultPath: string; defaultPath: string;
tunnelConnections: TunnelConnection[]; tunnelConnections: TunnelConnection[];
jumpHosts?: JumpHost[]; jumpHosts?: JumpHost[];
quickActions?: QuickAction[]; quickActions?: QuickAction[];
statsConfig?: string; statsConfig?: string;
dockerConfig?: string;
terminalConfig?: TerminalConfig; terminalConfig?: TerminalConfig;
// RDP/VNC specific fields (basic)
domain?: string;
security?: string;
ignoreCert?: boolean;
// RDP/VNC extended configuration (stored as JSON)
guacamoleConfig?: GuacamoleConfig | string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }
@@ -60,6 +173,7 @@ export interface QuickActionData {
} }
export interface SSHHostData { export interface SSHHostData {
connectionType?: HostConnectionType;
name?: string; name?: string;
ip: string; ip: string;
port: number; port: number;
@@ -77,13 +191,21 @@ export interface SSHHostData {
enableTerminal?: boolean; enableTerminal?: boolean;
enableTunnel?: boolean; enableTunnel?: boolean;
enableFileManager?: boolean; enableFileManager?: boolean;
enableDocker?: boolean;
defaultPath?: string; defaultPath?: string;
forceKeyboardInteractive?: boolean; forceKeyboardInteractive?: boolean;
tunnelConnections?: TunnelConnection[]; tunnelConnections?: TunnelConnection[];
jumpHosts?: JumpHostData[]; jumpHosts?: JumpHostData[];
quickActions?: QuickActionData[]; quickActions?: QuickActionData[];
statsConfig?: string | Record<string, unknown>; statsConfig?: string | Record<string, unknown>;
dockerConfig?: DockerConfig | string;
terminalConfig?: TerminalConfig; terminalConfig?: TerminalConfig;
// RDP/VNC specific fields (basic)
domain?: string;
security?: string;
ignoreCert?: boolean;
// RDP/VNC extended configuration
guacamoleConfig?: GuacamoleConfig;
} }
export interface SSHFolder { export interface SSHFolder {
@@ -119,6 +241,28 @@ export interface Credential {
updatedAt: string; updatedAt: string;
} }
export interface CredentialBackend {
id: number;
userId: string;
name: string;
description: string | null;
folder: string | null;
tags: string;
authType: "password" | "key";
username: string;
password: string | null;
key: string;
private_key?: string;
public_key?: string;
key_password: string | null;
keyType?: string;
detectedKeyType: string;
usageCount: number;
lastUsed: string | null;
createdAt: string;
updatedAt: string;
}
export interface CredentialData { export interface CredentialData {
name: string; name: string;
description?: string; description?: string;
@@ -307,6 +451,7 @@ export interface TerminalConfig {
startupSnippetId: number | null; startupSnippetId: number | null;
autoMosh: boolean; autoMosh: boolean;
moshCommand: string; moshCommand: string;
sudoPasswordAutoFill: boolean;
} }
// ============================================================================ // ============================================================================
@@ -322,11 +467,30 @@ export interface TabContextTab {
| "server" | "server"
| "admin" | "admin"
| "file_manager" | "file_manager"
| "user_profile"; | "user_profile"
| "rdp"
| "vnc"
| "tunnel"
| "docker";
title: string; title: string;
hostConfig?: SSHHost; hostConfig?: SSHHost;
terminalRef?: any; terminalRef?: any;
initialTab?: string; initialTab?: string;
connectionConfig?: {
token: string;
protocol: "rdp" | "vnc" | "telnet";
type?: "rdp" | "vnc" | "telnet";
hostname?: string;
port?: number;
username?: string;
password?: string;
domain?: string;
security?: string;
"ignore-cert"?: boolean;
width?: number;
height?: number;
dpi?: number;
};
} }
export type SplitLayout = "2h" | "2v" | "3l" | "3r" | "3t" | "4grid"; export type SplitLayout = "2h" | "2v" | "3l" | "3r" | "3t" | "4grid";

View File

@@ -155,7 +155,11 @@ function AppContent() {
const showTerminalView = const showTerminalView =
currentTabData?.type === "terminal" || currentTabData?.type === "terminal" ||
currentTabData?.type === "server" || currentTabData?.type === "server" ||
currentTabData?.type === "file_manager"; currentTabData?.type === "file_manager" ||
currentTabData?.type === "rdp" ||
currentTabData?.type === "vnc" ||
currentTabData?.type === "tunnel" ||
currentTabData?.type === "docker";
const showHome = currentTabData?.type === "home"; const showHome = currentTabData?.type === "home";
const showSshManager = currentTabData?.type === "ssh_manager"; const showSshManager = currentTabData?.type === "ssh_manager";
const showAdmin = currentTabData?.type === "admin"; const showAdmin = currentTabData?.type === "admin";

View File

@@ -0,0 +1,126 @@
import React from "react";
import { useSidebar } from "@/components/ui/sidebar.tsx";
import { Separator } from "@/components/ui/separator.tsx";
import { useTranslation } from "react-i18next";
interface HostConfig {
id: number;
name: string;
ip: string;
username: string;
folder?: string;
enableFileManager?: boolean;
tunnelConnections?: unknown[];
[key: string]: unknown;
}
interface DockerManagerProps {
hostConfig?: HostConfig;
title?: string;
isVisible?: boolean;
isTopbarOpen?: boolean;
embedded?: boolean;
}
export function DockerManager({
hostConfig,
title,
isVisible = true,
isTopbarOpen = true,
embedded = false,
}: DockerManagerProps): React.ReactElement {
const { t } = useTranslation();
const { state: sidebarState } = useSidebar();
const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
React.useEffect(() => {
if (hostConfig?.id !== currentHostConfig?.id) {
setCurrentHostConfig(hostConfig);
}
}, [hostConfig?.id]);
React.useEffect(() => {
const fetchLatestHostConfig = async () => {
if (hostConfig?.id) {
try {
const { getSSHHosts } = await import("@/ui/main-axios.ts");
const hosts = await getSSHHosts();
const updatedHost = hosts.find((h) => h.id === hostConfig.id);
if (updatedHost) {
setCurrentHostConfig(updatedHost);
}
} catch {
// Silently handle error
}
}
};
fetchLatestHostConfig();
const handleHostsChanged = async () => {
if (hostConfig?.id) {
try {
const { getSSHHosts } = await import("@/ui/main-axios.ts");
const hosts = await getSSHHosts();
const updatedHost = hosts.find((h) => h.id === hostConfig.id);
if (updatedHost) {
setCurrentHostConfig(updatedHost);
}
} catch {
// Silently handle error
}
}
};
window.addEventListener("ssh-hosts:changed", handleHostsChanged);
return () =>
window.removeEventListener("ssh-hosts:changed", handleHostsChanged);
}, [hostConfig?.id]);
const topMarginPx = isTopbarOpen ? 74 : 16;
const leftMarginPx = sidebarState === "collapsed" ? 16 : 8;
const bottomMarginPx = 8;
const wrapperStyle: React.CSSProperties = embedded
? { opacity: isVisible ? 1 : 0, height: "100%", width: "100%" }
: {
opacity: isVisible ? 1 : 0,
marginLeft: leftMarginPx,
marginRight: 17,
marginTop: topMarginPx,
marginBottom: bottomMarginPx,
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
};
const containerClass = embedded
? "h-full w-full text-white overflow-hidden bg-transparent"
: "bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden";
return (
<div style={wrapperStyle} className={containerClass}>
<div className="h-full w-full flex flex-col">
<div className="flex flex-col sm:flex-row sm:items-center justify-between px-4 pt-3 pb-3 gap-3">
<div className="flex items-center gap-4 min-w-0">
<div className="min-w-0">
<h1 className="font-bold text-lg truncate">
{currentHostConfig?.folder} / {title}
</h1>
</div>
</div>
</div>
<Separator className="p-0.25 w-full" />
<div className="flex-1 overflow-hidden min-h-0 p-1">
{/* Empty body as requested */}
<div className="flex items-center justify-center h-full">
<div className="text-center">
<p className="text-gray-400 text-lg">
Docker management UI will be here.
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,386 @@
import {
useEffect,
useRef,
useState,
useImperativeHandle,
forwardRef,
useCallback,
} from "react";
import Guacamole from "guacamole-common-js";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { getCookie, isElectron } from "@/ui/main-axios.ts";
import { Loader2 } from "lucide-react";
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
export interface GuacamoleConnectionConfig {
// Pre-fetched token (preferred) - if provided, skip token fetch
token?: string;
protocol?: GuacamoleConnectionType;
// Legacy fields for backward compatibility (used if token not provided)
type?: GuacamoleConnectionType;
hostname?: string;
port?: number;
username?: string;
password?: string;
domain?: string;
// Display settings
width?: number;
height?: number;
dpi?: number;
// Additional protocol options
[key: string]: unknown;
}
export interface GuacamoleDisplayHandle {
disconnect: () => void;
sendKey: (keysym: number, pressed: boolean) => void;
sendMouse: (x: number, y: number, buttonMask: number) => void;
setClipboard: (data: string) => void;
}
interface GuacamoleDisplayProps {
connectionConfig: GuacamoleConnectionConfig;
isVisible: boolean;
onConnect?: () => void;
onDisconnect?: () => void;
onError?: (error: string) => void;
}
const isDev = import.meta.env.DEV;
export const GuacamoleDisplay = forwardRef<GuacamoleDisplayHandle, GuacamoleDisplayProps>(
function GuacamoleDisplay(
{ connectionConfig, isVisible, onConnect, onDisconnect, onError },
ref
) {
const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null); // Outer container for measuring size
const displayRef = useRef<HTMLDivElement>(null); // Inner div for guacamole canvas
const clientRef = useRef<Guacamole.Client | null>(null);
const scaleRef = useRef<number>(1); // Track current scale factor for mouse
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [connectionError, setConnectionError] = useState<string | null>(null);
useImperativeHandle(ref, () => ({
disconnect: () => {
if (clientRef.current) {
clientRef.current.disconnect();
}
},
sendKey: (keysym: number, pressed: boolean) => {
if (clientRef.current) {
clientRef.current.sendKeyEvent(pressed ? 1 : 0, keysym);
}
},
sendMouse: (x: number, y: number, buttonMask: number) => {
if (clientRef.current) {
clientRef.current.sendMouseState(
new Guacamole.Mouse.State({ x, y, left: !!(buttonMask & 1), middle: !!(buttonMask & 2), right: !!(buttonMask & 4) })
);
}
},
setClipboard: (data: string) => {
if (clientRef.current) {
const stream = clientRef.current.createClipboardStream("text/plain");
const writer = new Guacamole.StringWriter(stream);
writer.sendText(data);
writer.sendEnd();
}
},
}));
const getWebSocketUrl = useCallback(async (containerWidth: number, containerHeight: number): Promise<string | null> => {
try {
let token: string;
// If token is pre-fetched, use it directly
if (connectionConfig.token) {
token = connectionConfig.token;
} else {
// Otherwise, fetch token from backend (legacy behavior)
const jwtToken = getCookie("jwt");
if (!jwtToken) {
setConnectionError("Authentication required");
return null;
}
const baseUrl = isDev
? "http://localhost:30001"
: isElectron()
? (window as { configuredServerUrl?: string }).configuredServerUrl || "http://127.0.0.1:30001"
: `${window.location.origin}`;
const response = await fetch(`${baseUrl}/guacamole/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${jwtToken}`,
},
body: JSON.stringify(connectionConfig),
credentials: "include",
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error || "Failed to get connection token");
}
const data = await response.json();
token = data.token;
}
// Build WebSocket URL with width/height/dpi as query parameters
// These are passed as unencrypted settings to guacamole-lite
// Use actual container dimensions, fall back to 720p
const width = connectionConfig.width || containerWidth || 1280;
const height = connectionConfig.height || containerHeight || 720;
const dpi = connectionConfig.dpi || 96;
const wsBase = isDev
? `ws://localhost:30007`
: isElectron()
? (() => {
const base = (window as { configuredServerUrl?: string }).configuredServerUrl || "http://127.0.0.1:30001";
return `${base.startsWith("https://") ? "wss://" : "ws://"}${base.replace(/^https?:\/\//, "")}/guacamole/websocket/`;
})()
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/guacamole/websocket/`;
return `${wsBase}?token=${encodeURIComponent(token)}&width=${width}&height=${height}&dpi=${dpi}`;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
setConnectionError(errorMessage);
onError?.(errorMessage);
return null;
}
}, [connectionConfig, onError]);
const connect = useCallback(async () => {
if (isConnecting || isConnected) return;
setIsConnecting(true);
setConnectionError(null);
// Get container dimensions for the WebSocket URL
// Use the outer container ref which has h-full w-full
let containerWidth = containerRef.current?.clientWidth || 0;
let containerHeight = containerRef.current?.clientHeight || 0;
console.log(`[Guacamole] Container size: ${containerWidth}x${containerHeight}`);
// If container size is too small or unavailable, use 720p default
if (containerWidth < 100 || containerHeight < 100) {
console.log(`[Guacamole] Container too small, using 720p default`);
containerWidth = 1280;
containerHeight = 720;
}
const wsUrl = await getWebSocketUrl(containerWidth, containerHeight);
if (!wsUrl) {
setIsConnecting(false);
return;
}
const tunnel = new Guacamole.WebSocketTunnel(wsUrl);
const client = new Guacamole.Client(tunnel);
clientRef.current = client;
// Set up display
const display = client.getDisplay();
const displayElement = display.getElement();
if (displayRef.current) {
displayRef.current.innerHTML = "";
displayRef.current.appendChild(displayElement);
}
// Function to rescale display to fit container
const rescaleDisplay = () => {
if (!containerRef.current) return;
const cWidth = containerRef.current.clientWidth;
const cHeight = containerRef.current.clientHeight;
const displayWidth = display.getWidth();
const displayHeight = display.getHeight();
if (displayWidth > 0 && displayHeight > 0 && cWidth > 0 && cHeight > 0) {
const scale = Math.min(cWidth / displayWidth, cHeight / displayHeight);
scaleRef.current = scale;
display.scale(scale);
}
};
// Handle display sync (when frames arrive)
display.onresize = () => {
rescaleDisplay();
};
// Set up mouse input on the display element (not the container)
// We need to adjust mouse coordinates based on the current scale factor
const mouse = new Guacamole.Mouse(displayElement);
const sendMouseState = (state: Guacamole.Mouse.State) => {
// Adjust coordinates based on scale factor and round to integers
const scale = scaleRef.current;
const adjustedX = Math.round(state.x / scale);
const adjustedY = Math.round(state.y / scale);
// Create adjusted state - guacamole expects integer coordinates
const adjustedState = new Guacamole.Mouse.State(
adjustedX,
adjustedY,
state.left,
state.middle,
state.right,
state.up,
state.down
) as Guacamole.Mouse.State;
client.sendMouseState(adjustedState);
};
mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = sendMouseState;
// Set up keyboard input
const keyboard = new Guacamole.Keyboard(document);
keyboard.onkeydown = (keysym: number) => {
client.sendKeyEvent(1, keysym);
};
keyboard.onkeyup = (keysym: number) => {
client.sendKeyEvent(0, keysym);
};
// Handle client state changes
client.onstatechange = (state: number) => {
switch (state) {
case 0: // IDLE
break;
case 1: // CONNECTING
setIsConnecting(true);
break;
case 2: // WAITING
break;
case 3: // CONNECTED
setIsConnected(true);
setIsConnecting(false);
onConnect?.();
break;
case 4: // DISCONNECTING
break;
case 5: // DISCONNECTED
setIsConnected(false);
setIsConnecting(false);
keyboard.onkeydown = null;
keyboard.onkeyup = null;
onDisconnect?.();
break;
}
};
// Handle errors
client.onerror = (error: Guacamole.Status) => {
const errorMessage = error.message || "Connection error";
setConnectionError(errorMessage);
setIsConnecting(false);
onError?.(errorMessage);
toast.error(`${t("guacamole.connectionError")}: ${errorMessage}`);
};
// Handle clipboard from remote
client.onclipboard = (stream: Guacamole.InputStream, mimetype: string) => {
if (mimetype === "text/plain") {
const reader = new Guacamole.StringReader(stream);
let data = "";
reader.ontext = (text: string) => {
data += text;
};
reader.onend = () => {
navigator.clipboard.writeText(data).catch(() => {});
};
}
};
// Connect - the width/height/dpi are already in the WebSocket URL
client.connect();
}, [isConnecting, isConnected, getWebSocketUrl, connectionConfig, onConnect, onDisconnect, onError, t]);
// Track if we've initiated a connection to prevent re-triggering
const hasInitiatedRef = useRef(false);
useEffect(() => {
if (isVisible && !hasInitiatedRef.current) {
hasInitiatedRef.current = true;
connect();
}
}, [isVisible, connect]);
// Separate cleanup effect that only runs on unmount
useEffect(() => {
return () => {
if (clientRef.current) {
clientRef.current.disconnect();
}
};
}, []);
// Handle window resize - rescale display to fit container
useEffect(() => {
const handleResize = () => {
if (clientRef.current && containerRef.current) {
const display = clientRef.current.getDisplay();
const cWidth = containerRef.current.clientWidth;
const cHeight = containerRef.current.clientHeight;
const displayWidth = display.getWidth();
const displayHeight = display.getHeight();
if (displayWidth > 0 && displayHeight > 0 && cWidth > 0 && cHeight > 0) {
const scale = Math.min(cWidth / displayWidth, cHeight / displayHeight);
scaleRef.current = scale;
display.scale(scale);
}
}
};
window.addEventListener("resize", handleResize);
// Also trigger on initial render after a short delay
const initialTimeout = setTimeout(handleResize, 100);
return () => {
window.removeEventListener("resize", handleResize);
clearTimeout(initialTimeout);
};
}, []);
return (
<div
ref={containerRef}
className="h-full w-full relative bg-black flex items-center justify-center overflow-hidden"
>
<div
ref={displayRef}
className="relative"
style={{ cursor: isConnected ? "none" : "default" }}
/>
{isConnecting && (
<div className="absolute inset-0 flex items-center justify-center bg-black/80">
<div className="flex flex-col items-center gap-4">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
<span className="text-muted-foreground">
{t("guacamole.connecting", { type: (connectionConfig.protocol || connectionConfig.type || "remote").toUpperCase() })}
</span>
</div>
</div>
)}
{connectionError && !isConnecting && (
<div className="absolute inset-0 flex items-center justify-center bg-black/80">
<div className="flex flex-col items-center gap-4 text-center p-4">
<span className="text-destructive font-medium">{t("guacamole.connectionFailed")}</span>
<span className="text-muted-foreground text-sm">{connectionError}</span>
</div>
</div>
)}
</div>
);
}
);

View File

@@ -42,6 +42,15 @@ export function HostManager({
} }
}, [initialTab]); }, [initialTab]);
// Update editingHost when hostConfig changes
useEffect(() => {
if (hostConfig) {
setEditingHost(hostConfig);
setActiveTab("add_host");
lastProcessedHostIdRef.current = hostConfig.id;
}
}, [hostConfig?.id]);
const handleEditHost = (host: SSHHost) => { const handleEditHost = (host: SSHHost) => {
setEditingHost(host); setEditingHost(host);
setActiveTab("add_host"); setActiveTab("add_host");

File diff suppressed because it is too large Load Diff

View File

@@ -61,7 +61,10 @@ import {
HardDrive, HardDrive,
Globe, Globe,
FolderOpen, FolderOpen,
Monitor,
ScreenShare,
} from "lucide-react"; } from "lucide-react";
import { getGuacamoleToken } from "@/ui/main-axios.ts";
import type { import type {
SSHHost, SSHHost,
SSHFolder, SSHFolder,
@@ -1371,7 +1374,28 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
)} )}
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{host.enableTerminal && ( {/* Show connection type badge */}
{(host.connectionType === "rdp" || host.connectionType === "vnc") ? (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
{host.connectionType === "rdp" ? (
<Monitor className="h-2 w-2 mr-0.5" />
) : (
<ScreenShare className="h-2 w-2 mr-0.5" />
)}
{host.connectionType.toUpperCase()}
</Badge>
) : host.connectionType === "telnet" ? (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
<Terminal className="h-2 w-2 mr-0.5" />
Telnet
</Badge>
) : host.enableTerminal && (
<Badge <Badge
variant="outline" variant="outline"
className="text-xs px-1 py-0" className="text-xs px-1 py-0"
@@ -1450,30 +1474,72 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
</div> </div>
<div className="mt-3 pt-3 border-t border-border/50 flex items-center justify-center gap-1"> <div className="mt-3 pt-3 border-t border-border/50 flex items-center justify-center gap-1">
{host.enableTerminal && ( {/* Show connect button for SSH/Telnet if enableTerminal, or always for RDP/VNC */}
{(host.enableTerminal || host.connectionType === "rdp" || host.connectionType === "vnc") && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={(e) => { onClick={async (e) => {
e.stopPropagation(); e.stopPropagation();
const title = host.name?.trim() const title = host.name?.trim()
? host.name ? host.name
: `${host.username}@${host.ip}:${host.port}`; : `${host.username}@${host.ip}:${host.port}`;
const connectionType = host.connectionType || "ssh";
if (connectionType === "ssh" || connectionType === "telnet") {
addTab({ addTab({
type: "terminal", type: "terminal",
title, title,
hostConfig: host, hostConfig: host,
}); });
} else if (connectionType === "rdp" || connectionType === "vnc") {
try {
// Parse guacamoleConfig if it's a string
const guacConfig = typeof host.guacamoleConfig === "string"
? JSON.parse(host.guacamoleConfig)
: host.guacamoleConfig;
const tokenResponse = await getGuacamoleToken({
protocol: connectionType,
hostname: host.ip,
port: host.port,
username: host.username,
password: host.password || "",
domain: host.domain,
security: host.security,
ignoreCert: host.ignoreCert,
guacamoleConfig: guacConfig,
});
addTab({
type: connectionType,
title,
hostConfig: host,
connectionConfig: {
token: tokenResponse.token,
protocol: connectionType,
},
});
} catch (error) {
console.error(`Failed to get guacamole token for ${connectionType}:`, error);
toast.error(`Failed to connect to ${connectionType.toUpperCase()} host`);
}
}
}} }}
className="h-7 px-2 hover:bg-blue-500/10 hover:border-blue-500/50 flex-1" className="h-7 px-2 hover:bg-blue-500/10 hover:border-blue-500/50 flex-1"
> >
{host.connectionType === "rdp" ? (
<Monitor className="h-3.5 w-3.5" />
) : host.connectionType === "vnc" ? (
<ScreenShare className="h-3.5 w-3.5" />
) : (
<Terminal className="h-3.5 w-3.5" /> <Terminal className="h-3.5 w-3.5" />
)}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Open Terminal</p> <p>{host.connectionType === "rdp" ? "Open RDP" : host.connectionType === "vnc" ? "Open VNC" : "Open Terminal"}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)} )}

View File

@@ -3,7 +3,6 @@ import { useSidebar } from "@/components/ui/sidebar.tsx";
import { Status, StatusIndicator } from "@/components/ui/shadcn-io/status"; import { Status, StatusIndicator } from "@/components/ui/shadcn-io/status";
import { Separator } from "@/components/ui/separator.tsx"; import { Separator } from "@/components/ui/separator.tsx";
import { Button } from "@/components/ui/button.tsx"; import { Button } from "@/components/ui/button.tsx";
import { Tunnel } from "@/ui/desktop/apps/tunnel/Tunnel.tsx";
import { import {
getServerStatusById, getServerStatusById,
getServerMetricsById, getServerMetricsById,
@@ -64,7 +63,7 @@ interface ServerProps {
embedded?: boolean; embedded?: boolean;
} }
export function Server({ export function ServerStats({
hostConfig, hostConfig,
title, title,
isVisible = true, isVisible = true,
@@ -462,7 +461,7 @@ export function Server({
{(metricsEnabled && showStatsUI) || {(metricsEnabled && showStatsUI) ||
(currentHostConfig?.quickActions && (currentHostConfig?.quickActions &&
currentHostConfig.quickActions.length > 0) ? ( currentHostConfig.quickActions.length > 0) ? (
<div className="rounded-lg border-2 border-dark-border m-3 bg-dark-bg-darker p-4 overflow-y-auto relative flex-1 flex flex-col"> <div className="rounded-lg border-dark-border m-3 p-1 overflow-y-auto relative flex-1 flex flex-col">
{currentHostConfig?.quickActions && {currentHostConfig?.quickActions &&
currentHostConfig.quickActions.length > 0 && ( currentHostConfig.quickActions.length > 0 && (
<div className={metricsEnabled && showStatsUI ? "mb-4" : ""}> <div className={metricsEnabled && showStatsUI ? "mb-4" : ""}>
@@ -600,20 +599,6 @@ export function Server({
)} )}
</div> </div>
) : null} ) : null}
{currentHostConfig?.tunnelConnections &&
currentHostConfig.tunnelConnections.length > 0 && (
<div className="rounded-lg border-2 border-dark-border m-3 bg-dark-bg-darker h-[360px] overflow-hidden flex flex-col min-h-0">
<Tunnel
filterHostKey={
currentHostConfig?.name &&
currentHostConfig.name.trim() !== ""
? currentHostConfig.name
: `${currentHostConfig?.username}@${currentHostConfig?.ip}`
}
/>
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -30,7 +30,7 @@ export function CpuWidget({ metrics, metricsHistory }: CpuWidgetProps) {
}, [metricsHistory]); }, [metricsHistory]);
return ( return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden"> <div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3"> <div className="flex items-center gap-2 flex-shrink-0 mb-3">
<Cpu className="h-5 w-5 text-blue-400" /> <Cpu className="h-5 w-5 text-blue-400" />
<h3 className="font-semibold text-lg text-white"> <h3 className="font-semibold text-lg text-white">

View File

@@ -27,7 +27,7 @@ export function DiskWidget({ metrics }: DiskWidgetProps) {
}, [metrics]); }, [metrics]);
return ( return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden"> <div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3"> <div className="flex items-center gap-2 flex-shrink-0 mb-3">
<HardDrive className="h-5 w-5 text-orange-400" /> <HardDrive className="h-5 w-5 text-orange-400" />
<h3 className="font-semibold text-lg text-white"> <h3 className="font-semibold text-lg text-white">

View File

@@ -35,7 +35,7 @@ export function LoginStatsWidget({ metrics }: LoginStatsWidgetProps) {
const uniqueIPs = loginStats?.uniqueIPs || 0; const uniqueIPs = loginStats?.uniqueIPs || 0;
return ( return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden"> <div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3"> <div className="flex items-center gap-2 flex-shrink-0 mb-3">
<UserCheck className="h-5 w-5 text-green-400" /> <UserCheck className="h-5 w-5 text-green-400" />
<h3 className="font-semibold text-lg text-white"> <h3 className="font-semibold text-lg text-white">

View File

@@ -30,7 +30,7 @@ export function MemoryWidget({ metrics, metricsHistory }: MemoryWidgetProps) {
}, [metricsHistory]); }, [metricsHistory]);
return ( return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden"> <div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3"> <div className="flex items-center gap-2 flex-shrink-0 mb-3">
<MemoryStick className="h-5 w-5 text-green-400" /> <MemoryStick className="h-5 w-5 text-green-400" />
<h3 className="font-semibold text-lg text-white"> <h3 className="font-semibold text-lg text-white">

View File

@@ -24,7 +24,7 @@ export function NetworkWidget({ metrics }: NetworkWidgetProps) {
const interfaces = network?.interfaces || []; const interfaces = network?.interfaces || [];
return ( return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden"> <div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3"> <div className="flex items-center gap-2 flex-shrink-0 mb-3">
<Network className="h-5 w-5 text-indigo-400" /> <Network className="h-5 w-5 text-indigo-400" />
<h3 className="font-semibold text-lg text-white"> <h3 className="font-semibold text-lg text-white">

View File

@@ -28,7 +28,7 @@ export function ProcessesWidget({ metrics }: ProcessesWidgetProps) {
const topProcesses = processes?.top || []; const topProcesses = processes?.top || [];
return ( return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden"> <div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3"> <div className="flex items-center gap-2 flex-shrink-0 mb-3">
<List className="h-5 w-5 text-yellow-400" /> <List className="h-5 w-5 text-yellow-400" />
<h3 className="font-semibold text-lg text-white"> <h3 className="font-semibold text-lg text-white">

View File

@@ -21,7 +21,7 @@ export function SystemWidget({ metrics }: SystemWidgetProps) {
const system = metricsWithSystem?.system; const system = metricsWithSystem?.system;
return ( return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden"> <div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3"> <div className="flex items-center gap-2 flex-shrink-0 mb-3">
<Server className="h-5 w-5 text-purple-400" /> <Server className="h-5 w-5 text-purple-400" />
<h3 className="font-semibold text-lg text-white"> <h3 className="font-semibold text-lg text-white">

View File

@@ -20,7 +20,7 @@ export function UptimeWidget({ metrics }: UptimeWidgetProps) {
const uptime = metricsWithUptime?.uptime; const uptime = metricsWithUptime?.uptime;
return ( return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden"> <div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3"> <div className="flex items-center gap-2 flex-shrink-0 mb-3">
<Clock className="h-5 w-5 text-cyan-400" /> <Clock className="h-5 w-5 text-cyan-400" />
<h3 className="font-semibold text-lg text-white"> <h3 className="font-semibold text-lg text-white">

View File

@@ -0,0 +1,82 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { KeyRound } from "lucide-react";
import { Button } from "@/components/ui/button.tsx";
interface SudoPasswordPopupProps {
isOpen: boolean;
hostPassword: string;
backgroundColor: string;
onConfirm: (password: string) => void;
onDismiss: () => void;
}
export function SudoPasswordPopup({
isOpen,
hostPassword,
backgroundColor,
onConfirm,
onDismiss
}: SudoPasswordPopupProps) {
const { t } = useTranslation();
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
onConfirm(hostPassword);
} else if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
onDismiss();
}
};
window.addEventListener("keydown", handleKeyDown, true);
return () => window.removeEventListener("keydown", handleKeyDown, true);
}, [isOpen, onConfirm, onDismiss, hostPassword]);
if (!isOpen) return null;
return (
<div
className="absolute bottom-4 right-4 z-50 backdrop-blur-sm border border-border rounded-lg shadow-lg p-4 min-w-[280px]"
style={{ backgroundColor: backgroundColor }}
>
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-primary/10 rounded-full">
<KeyRound className="h-5 w-5 text-primary" />
</div>
<div>
<h4 className="font-medium text-sm">
{t("terminal.sudoPasswordPopupTitle", "Insert password?")}
</h4>
<p className="text-xs text-muted-foreground">
{t("terminal.sudoPasswordPopupHint", "Press Enter to insert, Esc to dismiss")}
</p>
</div>
</div>
<div className="flex gap-2 justify-end">
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
>
{t("terminal.sudoPasswordPopupDismiss", "Dismiss")}
</Button>
<Button
variant="default"
size="sm"
onClick={() => onConfirm(hostPassword)}
>
{t("terminal.sudoPasswordPopupConfirm", "Insert")}
</Button>
</div>
</div>
);
}

View File

@@ -32,6 +32,7 @@ import { useCommandHistory as useCommandHistoryHook } from "@/ui/hooks/useComman
import { useCommandHistory } from "@/ui/desktop/apps/terminal/command-history/CommandHistoryContext.tsx"; import { useCommandHistory } from "@/ui/desktop/apps/terminal/command-history/CommandHistoryContext.tsx";
import { CommandAutocomplete } from "./command-history/CommandAutocomplete.tsx"; import { CommandAutocomplete } from "./command-history/CommandAutocomplete.tsx";
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
import { SudoPasswordPopup } from "./SudoPasswordPopup.tsx";
interface HostConfig { interface HostConfig {
id?: number; id?: number;
@@ -173,6 +174,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
const [showHistoryDialog, setShowHistoryDialog] = useState(false); const [showHistoryDialog, setShowHistoryDialog] = useState(false);
const [commandHistory, setCommandHistory] = useState<string[]>([]); const [commandHistory, setCommandHistory] = useState<string[]>([]);
const [isLoadingHistory, setIsLoadingHistory] = useState(false); const [isLoadingHistory, setIsLoadingHistory] = useState(false);
const [showSudoPasswordPopup, setShowSudoPasswordPopup] = useState(false);
const setIsLoadingRef = useRef(commandHistoryContext.setIsLoading); const setIsLoadingRef = useRef(commandHistoryContext.setIsLoading);
const setCommandHistoryContextRef = useRef( const setCommandHistoryContextRef = useRef(
@@ -660,6 +662,11 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
if (msg.type === "data") { if (msg.type === "data") {
if (typeof msg.data === "string") { if (typeof msg.data === "string") {
terminal.write(msg.data); terminal.write(msg.data);
// Sudo password prompt detection
const sudoPasswordPattern = /(?:\[sudo\] password for \S+:|sudo: a password is required)/;
if (config.sudoPasswordAutoFill && sudoPasswordPattern.test(msg.data)) {
setShowSudoPasswordPopup(true);
}
} else { } else {
terminal.write(String(msg.data)); terminal.write(String(msg.data));
} }
@@ -1500,6 +1507,19 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
onSelect={handleAutocompleteSelect} onSelect={handleAutocompleteSelect}
/> />
<SudoPasswordPopup
isOpen={showSudoPasswordPopup}
hostPassword={hostConfig.password || ""}
backgroundColor={backgroundColor}
onConfirm={(password) => {
setShowSudoPasswordPopup(false);
if (webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN) {
webSocketRef.current.send(JSON.stringify({ type: "input", data: password + "\n" }));
}
}}
onDismiss={() => setShowSudoPasswordPopup(false)}
/>
<SimpleLoader <SimpleLoader
visible={isConnecting} visible={isConnecting}
message={t("terminal.connecting")} message={t("terminal.connecting")}

View File

@@ -197,9 +197,11 @@ export function SSHToolsSidebar({
); );
const [draggedSnippet, setDraggedSnippet] = useState<Snippet | null>(null); const [draggedSnippet, setDraggedSnippet] = useState<Snippet | null>(null);
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null); const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>( const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(() => {
new Set(), const shouldCollapse =
); localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false";
return shouldCollapse ? new Set() : new Set();
});
const [showFolderDialog, setShowFolderDialog] = useState(false); const [showFolderDialog, setShowFolderDialog] = useState(false);
const [editingFolder, setEditingFolder] = useState<SnippetFolder | null>( const [editingFolder, setEditingFolder] = useState<SnippetFolder | null>(
null, null,
@@ -351,6 +353,55 @@ export function SSHToolsSidebar({
} }
}, [isOpen, activeTab]); }, [isOpen, activeTab]);
useEffect(() => {
if (snippetFolders.length > 0) {
const shouldCollapse =
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false";
if (shouldCollapse) {
const allFolderNames = new Set(snippetFolders.map((f) => f.name));
const uncategorizedSnippets = snippets.filter(
(s) => !s.folder || s.folder === "",
);
if (uncategorizedSnippets.length > 0) {
allFolderNames.add("");
}
setCollapsedFolders(allFolderNames);
} else {
setCollapsedFolders(new Set());
}
}
}, [snippetFolders, snippets]);
useEffect(() => {
const handleSettingChange = () => {
const shouldCollapse =
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false";
if (shouldCollapse) {
const allFolderNames = new Set(snippetFolders.map((f) => f.name));
const uncategorizedSnippets = snippets.filter(
(s) => !s.folder || s.folder === "",
);
if (uncategorizedSnippets.length > 0) {
allFolderNames.add("");
}
setCollapsedFolders(allFolderNames);
} else {
setCollapsedFolders(new Set());
}
};
window.addEventListener(
"defaultSnippetFoldersCollapsedChanged",
handleSettingChange,
);
return () => {
window.removeEventListener(
"defaultSnippetFoldersCollapsedChanged",
handleSettingChange,
);
};
}, [snippetFolders, snippets]);
const handleMouseDown = (e: React.MouseEvent) => { const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
setIsResizing(true); setIsResizing(true);

View File

@@ -0,0 +1,143 @@
import React from "react";
import { useSidebar } from "@/components/ui/sidebar.tsx";
import { Separator } from "@/components/ui/separator.tsx";
import { Tunnel } from "@/ui/desktop/apps/tunnel/Tunnel.tsx";
import { useTranslation } from "react-i18next";
interface HostConfig {
id: number;
name: string;
ip: string;
username: string;
folder?: string;
enableFileManager?: boolean;
tunnelConnections?: unknown[];
[key: string]: unknown;
}
interface TunnelManagerProps {
hostConfig?: HostConfig;
title?: string;
isVisible?: boolean;
isTopbarOpen?: boolean;
embedded?: boolean;
}
export function TunnelManager({
hostConfig,
title,
isVisible = true,
isTopbarOpen = true,
embedded = false,
}: TunnelManagerProps): React.ReactElement {
const { t } = useTranslation();
const { state: sidebarState } = useSidebar();
const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
React.useEffect(() => {
if (hostConfig?.id !== currentHostConfig?.id) {
setCurrentHostConfig(hostConfig);
}
}, [hostConfig?.id]);
React.useEffect(() => {
const fetchLatestHostConfig = async () => {
if (hostConfig?.id) {
try {
const { getSSHHosts } = await import("@/ui/main-axios.ts");
const hosts = await getSSHHosts();
const updatedHost = hosts.find((h) => h.id === hostConfig.id);
if (updatedHost) {
setCurrentHostConfig(updatedHost);
}
} catch {
// Silently handle error
}
}
};
fetchLatestHostConfig();
const handleHostsChanged = async () => {
if (hostConfig?.id) {
try {
const { getSSHHosts } = await import("@/ui/main-axios.ts");
const hosts = await getSSHHosts();
const updatedHost = hosts.find((h) => h.id === hostConfig.id);
if (updatedHost) {
setCurrentHostConfig(updatedHost);
}
} catch {
// Silently handle error
}
}
};
window.addEventListener("ssh-hosts:changed", handleHostsChanged);
return () =>
window.removeEventListener("ssh-hosts:changed", handleHostsChanged);
}, [hostConfig?.id]);
const topMarginPx = isTopbarOpen ? 74 : 16;
const leftMarginPx = sidebarState === "collapsed" ? 16 : 8;
const bottomMarginPx = 8;
const wrapperStyle: React.CSSProperties = embedded
? { opacity: isVisible ? 1 : 0, height: "100%", width: "100%" }
: {
opacity: isVisible ? 1 : 0,
marginLeft: leftMarginPx,
marginRight: 17,
marginTop: topMarginPx,
marginBottom: bottomMarginPx,
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
};
const containerClass = embedded
? "h-full w-full text-white overflow-hidden bg-transparent"
: "bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden";
return (
<div style={wrapperStyle} className={containerClass}>
<div className="h-full w-full flex flex-col">
<div className="flex flex-col sm:flex-row sm:items-center justify-between px-4 pt-3 pb-3 gap-3">
<div className="flex items-center gap-4 min-w-0">
<div className="min-w-0">
<h1 className="font-bold text-lg truncate">
{currentHostConfig?.folder} / {title}
</h1>
</div>
</div>
</div>
<Separator className="p-0.25 w-full" />
<div className="flex-1 overflow-hidden min-h-0 p-1">
{currentHostConfig?.tunnelConnections &&
currentHostConfig.tunnelConnections.length > 0 ? (
<div className="rounded-lg h-full overflow-hidden flex flex-col min-h-0">
<Tunnel
filterHostKey={
currentHostConfig?.name &&
currentHostConfig.name.trim() !== ""
? currentHostConfig.name
: `${currentHostConfig?.username}@${currentHostConfig?.ip}`
}
/>
</div>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<p className="text-gray-400 text-lg">
{t("tunnel.noTunnelsConfigured")}
</p>
<p className="text-gray-500 text-sm mt-2">
{t("tunnel.configureTunnelsInHostSettings")}
</p>
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -43,11 +43,6 @@ export function TunnelViewer({
return ( return (
<div className="w-full h-full flex flex-col overflow-hidden p-3 min-h-0"> <div className="w-full h-full flex flex-col overflow-hidden p-3 min-h-0">
<div className="w-full flex-shrink-0 mb-2">
<h1 className="text-xl font-semibold text-foreground">
{t("tunnels.title")}
</h1>
</div>
<div className="min-h-0 flex-1 overflow-auto pr-1"> <div className="min-h-0 flex-1 overflow-auto pr-1">
<div className="grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-3 auto-rows-min content-start w-full"> <div className="grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-3 auto-rows-min content-start w-full">
{activeHost.tunnelConnections.map((t, idx) => ( {activeHost.tunnelConnections.map((t, idx) => (

View File

@@ -1,7 +1,13 @@
import React, { useEffect, useRef, useState, useMemo } from "react"; import React, { useEffect, useRef, useState, useMemo } from "react";
import { Terminal } from "@/ui/desktop/apps/terminal/Terminal.tsx"; import { Terminal } from "@/ui/desktop/apps/terminal/Terminal.tsx";
import { Server as ServerView } from "@/ui/desktop/apps/server/Server.tsx"; import { ServerStats as ServerView } from "@/ui/desktop/apps/server-stats/ServerStats.tsx";
import { FileManager } from "@/ui/desktop/apps/file-manager/FileManager.tsx"; import { FileManager } from "@/ui/desktop/apps/file-manager/FileManager.tsx";
import {
GuacamoleDisplay,
type GuacamoleConnectionConfig,
} from "@/ui/desktop/apps/guacamole/GuacamoleDisplay.tsx";
import { TunnelManager } from "@/ui/desktop/apps/tunnel/TunnelManager.tsx";
import { DockerManager } from "@/ui/desktop/apps/docker/DockerManager.tsx";
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
import { import {
ResizablePanelGroup, ResizablePanelGroup,
@@ -16,7 +22,6 @@ import {
TERMINAL_THEMES, TERMINAL_THEMES,
DEFAULT_TERMINAL_CONFIG, DEFAULT_TERMINAL_CONFIG,
} from "@/constants/terminal-themes"; } from "@/constants/terminal-themes";
import { SSHAuthDialog } from "@/ui/desktop/navigation/SSHAuthDialog.tsx";
interface TabData { interface TabData {
id: number; id: number;
@@ -30,6 +35,7 @@ interface TabData {
}; };
}; };
hostConfig?: any; hostConfig?: any;
connectionConfig?: GuacamoleConnectionConfig;
[key: string]: unknown; [key: string]: unknown;
} }
@@ -58,7 +64,11 @@ export function AppView({
(tab: TabData) => (tab: TabData) =>
tab.type === "terminal" || tab.type === "terminal" ||
tab.type === "server" || tab.type === "server" ||
tab.type === "file_manager", tab.type === "file_manager" ||
tab.type === "rdp" ||
tab.type === "vnc" ||
tab.type === "tunnel" ||
tab.type === "docker",
), ),
[tabs], [tabs],
); );
@@ -210,7 +220,10 @@ export function AppView({
const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab); const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab);
if (allSplitScreenTab.length === 0 && mainTab) { if (allSplitScreenTab.length === 0 && mainTab) {
const isFileManagerTab = mainTab.type === "file_manager"; const isFileManagerTab =
mainTab.type === "file_manager" ||
mainTab.type === "tunnel" ||
mainTab.type === "docker";
const newStyle = { const newStyle = {
position: "absolute" as const, position: "absolute" as const,
top: isFileManagerTab ? 0 : 4, top: isFileManagerTab ? 0 : 4,
@@ -257,9 +270,14 @@ export function AppView({
const isVisible = const isVisible =
hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab); hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab);
const effectiveVisible = isVisible;
const previousStyle = previousStylesRef.current[t.id]; const previousStyle = previousStylesRef.current[t.id];
const isFileManagerTab = t.type === "file_manager"; const isFileManagerTab =
t.type === "file_manager" ||
t.type === "tunnel" ||
t.type === "docker";
const standardStyle = { const standardStyle = {
position: "absolute" as const, position: "absolute" as const,
top: isFileManagerTab ? 0 : 4, top: isFileManagerTab ? 0 : 4,
@@ -270,6 +288,16 @@ export function AppView({
const finalStyle: React.CSSProperties = hasStyle const finalStyle: React.CSSProperties = hasStyle
? { ...styles[t.id], overflow: "hidden" } ? { ...styles[t.id], overflow: "hidden" }
: effectiveVisible
? {
...(previousStyle || standardStyle),
opacity: 1,
pointerEvents: "auto",
zIndex: 20,
display: "block",
transition: "opacity 150ms ease-in-out",
overflow: "hidden",
}
: ({ : ({
...(previousStyle || standardStyle), ...(previousStyle || standardStyle),
opacity: 0, opacity: 0,
@@ -279,8 +307,6 @@ export function AppView({
overflow: "hidden", overflow: "hidden",
} as React.CSSProperties); } as React.CSSProperties);
const effectiveVisible = isVisible;
const isTerminal = t.type === "terminal"; const isTerminal = t.type === "terminal";
const terminalConfig = { const terminalConfig = {
...DEFAULT_TERMINAL_CONFIG, ...DEFAULT_TERMINAL_CONFIG,
@@ -317,6 +343,35 @@ export function AppView({
isTopbarOpen={isTopbarOpen} isTopbarOpen={isTopbarOpen}
embedded embedded
/> />
) : t.type === "rdp" || t.type === "vnc" ? (
t.connectionConfig ? (
<GuacamoleDisplay
connectionConfig={t.connectionConfig}
isVisible={effectiveVisible}
onDisconnect={() => removeTab(t.id)}
onError={(err) => console.error("Guacamole error:", err)}
/>
) : (
<div className="flex items-center justify-center h-full text-red-500">
Missing connection configuration
</div>
)
) : t.type === "tunnel" ? (
<TunnelManager
hostConfig={t.hostConfig}
title={t.title}
isVisible={effectiveVisible}
isTopbarOpen={isTopbarOpen}
embedded
/>
) : t.type === "docker" ? (
<DockerManager
hostConfig={t.hostConfig}
title={t.title}
isVisible={effectiveVisible}
isTopbarOpen={isTopbarOpen}
embedded
/>
) : ( ) : (
<FileManager <FileManager
embedded embedded
@@ -636,6 +691,8 @@ export function AppView({
const currentTabData = tabs.find((tab: TabData) => tab.id === currentTab); const currentTabData = tabs.find((tab: TabData) => tab.id === currentTab);
const isFileManager = currentTabData?.type === "file_manager"; const isFileManager = currentTabData?.type === "file_manager";
const isTunnel = currentTabData?.type === "tunnel";
const isDocker = currentTabData?.type === "docker";
const isTerminal = currentTabData?.type === "terminal"; const isTerminal = currentTabData?.type === "terminal";
const isSplitScreen = allSplitScreenTab.length > 0; const isSplitScreen = allSplitScreenTab.length > 0;
@@ -653,7 +710,7 @@ export function AppView({
const bottomMarginPx = 8; const bottomMarginPx = 8;
let containerBackground = "var(--color-dark-bg)"; let containerBackground = "var(--color-dark-bg)";
if (isFileManager && !isSplitScreen) { if ((isFileManager || isTunnel || isDocker) && !isSplitScreen) {
containerBackground = "var(--color-dark-bg-darkest)"; containerBackground = "var(--color-dark-bg-darkest)";
} else if (isTerminal) { } else if (isTerminal) {
containerBackground = terminalBackgroundColor; containerBackground = terminalBackgroundColor;

View File

@@ -36,30 +36,7 @@ import { Button } from "@/components/ui/button.tsx";
import { FolderCard } from "@/ui/desktop/navigation/hosts/FolderCard.tsx"; import { FolderCard } from "@/ui/desktop/navigation/hosts/FolderCard.tsx";
import { getSSHHosts, getSSHFolders } from "@/ui/main-axios.ts"; import { getSSHHosts, getSSHFolders } from "@/ui/main-axios.ts";
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
import type { SSHFolder } from "@/types/index.ts"; import type { SSHFolder, SSHHost } from "@/types/index.ts";
interface SSHHost {
id: number;
name: string;
ip: string;
port: number;
username: string;
folder: string;
tags: string[];
pin: boolean;
authType: string;
password?: string;
key?: string;
keyPassword?: string;
keyType?: string;
enableTerminal: boolean;
enableTunnel: boolean;
enableFileManager: boolean;
defaultPath: string;
tunnelConnections: unknown[];
createdAt: string;
updatedAt: string;
}
interface SidebarProps { interface SidebarProps {
disabled?: boolean; disabled?: boolean;

View File

@@ -369,10 +369,15 @@ export function TopNavbar({
const isTerminal = tab.type === "terminal"; const isTerminal = tab.type === "terminal";
const isServer = tab.type === "server"; const isServer = tab.type === "server";
const isFileManager = tab.type === "file_manager"; const isFileManager = tab.type === "file_manager";
const isTunnel = tab.type === "tunnel";
const isDocker = tab.type === "docker";
const isSshManager = tab.type === "ssh_manager"; const isSshManager = tab.type === "ssh_manager";
const isAdmin = tab.type === "admin"; const isAdmin = tab.type === "admin";
const isUserProfile = tab.type === "user_profile"; const isUserProfile = tab.type === "user_profile";
const isSplittable = isTerminal || isServer || isFileManager; const isRdp = tab.type === "rdp";
const isVnc = tab.type === "vnc";
const isSplittable =
isTerminal || isServer || isFileManager || isTunnel || isDocker;
const disableSplit = !isSplittable; const disableSplit = !isSplittable;
const disableActivate = const disableActivate =
isSplit || isSplit ||
@@ -484,9 +489,13 @@ export function TopNavbar({
isTerminal || isTerminal ||
isServer || isServer ||
isFileManager || isFileManager ||
isTunnel ||
isDocker ||
isSshManager || isSshManager ||
isAdmin || isAdmin ||
isUserProfile isUserProfile ||
isRdp ||
isVnc
? () => handleTabClose(tab.id) ? () => handleTabClose(tab.id)
: undefined : undefined
} }
@@ -498,9 +507,13 @@ export function TopNavbar({
isTerminal || isTerminal ||
isServer || isServer ||
isFileManager || isFileManager ||
isTunnel ||
isDocker ||
isSshManager || isSshManager ||
isAdmin || isAdmin ||
isUserProfile isUserProfile ||
isRdp ||
isVnc
} }
disableActivate={disableActivate} disableActivate={disableActivate}
disableSplit={disableSplit} disableSplit={disableSplit}

View File

@@ -8,6 +8,10 @@ import {
Server, Server,
FolderOpen, FolderOpen,
Pencil, Pencil,
ArrowDownUp,
Container,
Monitor,
ScreenShare,
} from "lucide-react"; } from "lucide-react";
import { import {
DropdownMenu, DropdownMenu,
@@ -16,7 +20,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext";
import { getServerStatusById } from "@/ui/main-axios"; import { getServerStatusById, getGuacamoleToken } from "@/ui/main-axios";
import type { HostProps } from "../../../../types"; import type { HostProps } from "../../../../types";
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets"; import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets";
@@ -63,6 +67,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
}, [host.statsConfig]); }, [host.statsConfig]);
const shouldShowStatus = statsConfig.statusCheckEnabled !== false; const shouldShowStatus = statsConfig.statusCheckEnabled !== false;
const shouldShowMetrics = statsConfig.metricsEnabled !== false;
useEffect(() => { useEffect(() => {
if (!shouldShowStatus) { if (!shouldShowStatus) {
@@ -103,8 +108,49 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
}; };
}, [host.id, shouldShowStatus]); }, [host.id, shouldShowStatus]);
const handleTerminalClick = () => { const handleTerminalClick = async () => {
const connectionType = host.connectionType || "ssh";
if (connectionType === "ssh" || connectionType === "telnet") {
addTab({ type: "terminal", title, hostConfig: host }); addTab({ type: "terminal", title, hostConfig: host });
} else if (connectionType === "rdp" || connectionType === "vnc") {
try {
// Parse guacamoleConfig if it's a string
const guacConfig = typeof host.guacamoleConfig === "string"
? JSON.parse(host.guacamoleConfig)
: host.guacamoleConfig;
// Debug: log what guacamoleConfig we have
console.log("[Host.tsx] host.guacamoleConfig type:", typeof host.guacamoleConfig);
console.log("[Host.tsx] host.guacamoleConfig:", host.guacamoleConfig);
console.log("[Host.tsx] Parsed guacConfig:", guacConfig);
// Get guacamole token for RDP/VNC connection
const tokenResponse = await getGuacamoleToken({
protocol: connectionType,
hostname: host.ip,
port: host.port,
username: host.username,
password: host.password || "",
domain: host.domain,
security: host.security,
ignoreCert: host.ignoreCert,
guacamoleConfig: guacConfig,
});
addTab({
type: connectionType,
title,
hostConfig: host,
connectionConfig: {
token: tokenResponse.token,
protocol: connectionType,
},
});
} catch (error) {
console.error(`Failed to get guacamole token for ${connectionType}:`, error);
}
}
}; };
return ( return (
@@ -124,13 +170,20 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
</p> </p>
<ButtonGroup className="flex-shrink-0"> <ButtonGroup className="flex-shrink-0">
{host.enableTerminal && ( {/* Show connect button for SSH/Telnet if enableTerminal, or always for RDP/VNC */}
{(host.enableTerminal || host.connectionType === "rdp" || host.connectionType === "vnc") && (
<Button <Button
variant="outline" variant="outline"
className="!px-2 border-1 border-dark-border" className="!px-2 border-1 border-dark-border"
onClick={handleTerminalClick} onClick={handleTerminalClick}
> >
{host.connectionType === "rdp" ? (
<Monitor />
) : host.connectionType === "vnc" ? (
<ScreenShare />
) : (
<Terminal /> <Terminal />
)}
</Button> </Button>
)} )}
@@ -139,7 +192,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
<Button <Button
variant="outline" variant="outline"
className={`!px-2 border-1 border-dark-border ${ className={`!px-2 border-1 border-dark-border ${
host.enableTerminal ? "rounded-tl-none rounded-bl-none" : "" (host.enableTerminal || host.connectionType === "rdp" || host.connectionType === "vnc") ? "rounded-tl-none rounded-bl-none" : ""
}`} }`}
> >
<EllipsisVertical /> <EllipsisVertical />
@@ -151,6 +204,10 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
side="right" side="right"
className="w-56 bg-dark-bg border-dark-border text-white" className="w-56 bg-dark-bg border-dark-border text-white"
> >
{/* SSH-specific menu items */}
{(!host.connectionType || host.connectionType === "ssh") && (
<>
{shouldShowMetrics && (
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() =>
addTab({ type: "server", title, hostConfig: host }) addTab({ type: "server", title, hostConfig: host })
@@ -158,8 +215,10 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300" className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
> >
<Server className="h-4 w-4" /> <Server className="h-4 w-4" />
<span className="flex-1">Open Server Details</span> <span className="flex-1">Open Server Stats</span>
</DropdownMenuItem> </DropdownMenuItem>
)}
{host.enableFileManager && (
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() =>
addTab({ type: "file_manager", title, hostConfig: host }) addTab({ type: "file_manager", title, hostConfig: host })
@@ -169,6 +228,31 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
<FolderOpen className="h-4 w-4" /> <FolderOpen className="h-4 w-4" />
<span className="flex-1">Open File Manager</span> <span className="flex-1">Open File Manager</span>
</DropdownMenuItem> </DropdownMenuItem>
)}
{host.enableTunnel && (
<DropdownMenuItem
onClick={() =>
addTab({ type: "tunnel", title, hostConfig: host })
}
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
>
<ArrowDownUp className="h-4 w-4" />
<span className="flex-1">Open Tunnels</span>
</DropdownMenuItem>
)}
{host.enableDocker && (
<DropdownMenuItem
onClick={() =>
addTab({ type: "docker", title, hostConfig: host })
}
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
>
<Container className="h-4 w-4" />
<span className="flex-1">Open Docker</span>
</DropdownMenuItem>
)}
</>
)}
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() =>
addTab({ addTab({

View File

@@ -10,6 +10,9 @@ import {
Server as ServerIcon, Server as ServerIcon,
Folder as FolderIcon, Folder as FolderIcon,
User as UserIcon, User as UserIcon,
Monitor as MonitorIcon,
ArrowDownUp as TunnelIcon,
Container as DockerIcon,
} from "lucide-react"; } from "lucide-react";
interface TabProps { interface TabProps {
@@ -119,11 +122,18 @@ export function Tab({
tabType === "terminal" || tabType === "terminal" ||
tabType === "server" || tabType === "server" ||
tabType === "file_manager" || tabType === "file_manager" ||
tabType === "user_profile" tabType === "user_profile" ||
tabType === "rdp" ||
tabType === "vnc" ||
tabType === "tunnel" ||
tabType === "docker"
) { ) {
const isServer = tabType === "server"; const isServer = tabType === "server";
const isFileManager = tabType === "file_manager"; const isFileManager = tabType === "file_manager";
const isTunnel = tabType === "tunnel";
const isDocker = tabType === "docker";
const isUserProfile = tabType === "user_profile"; const isUserProfile = tabType === "user_profile";
const isRemoteDesktop = tabType === "rdp" || tabType === "vnc";
const displayTitle = const displayTitle =
title || title ||
@@ -131,8 +141,14 @@ export function Tab({
? t("nav.serverStats") ? t("nav.serverStats")
: isFileManager : isFileManager
? t("nav.fileManager") ? t("nav.fileManager")
: isTunnel
? t("nav.tunnels")
: isDocker
? t("nav.docker")
: isUserProfile : isUserProfile
? t("nav.userProfile") ? t("nav.userProfile")
: isRemoteDesktop
? tabType.toUpperCase()
: t("nav.terminal")); : t("nav.terminal"));
const { base, suffix } = splitTitle(displayTitle); const { base, suffix } = splitTitle(displayTitle);
@@ -151,8 +167,14 @@ export function Tab({
<ServerIcon className="h-4 w-4 flex-shrink-0" /> <ServerIcon className="h-4 w-4 flex-shrink-0" />
) : isFileManager ? ( ) : isFileManager ? (
<FolderIcon className="h-4 w-4 flex-shrink-0" /> <FolderIcon className="h-4 w-4 flex-shrink-0" />
) : isTunnel ? (
<TunnelIcon className="h-4 w-4 flex-shrink-0" />
) : isDocker ? (
<DockerIcon className="h-4 w-4 flex-shrink-0" />
) : isUserProfile ? ( ) : isUserProfile ? (
<UserIcon className="h-4 w-4 flex-shrink-0" /> <UserIcon className="h-4 w-4 flex-shrink-0" />
) : isRemoteDesktop ? (
<MonitorIcon className="h-4 w-4 flex-shrink-0" />
) : ( ) : (
<TerminalIcon className="h-4 w-4 flex-shrink-0" /> <TerminalIcon className="h-4 w-4 flex-shrink-0" />
)} )}

View File

@@ -76,6 +76,10 @@ export function TabProvider({ children }: TabProviderProps) {
? t("nav.serverStats") ? t("nav.serverStats")
: tabType === "file_manager" : tabType === "file_manager"
? t("nav.fileManager") ? t("nav.fileManager")
: tabType === "tunnel"
? t("nav.tunnels")
: tabType === "docker"
? t("nav.docker")
: t("nav.terminal"); : t("nav.terminal");
const baseTitle = (desiredTitle || defaultTitle).trim(); const baseTitle = (desiredTitle || defaultTitle).trim();
const match = baseTitle.match(/^(.*) \((\d+)\)$/); const match = baseTitle.match(/^(.*) \((\d+)\)$/);
@@ -137,7 +141,9 @@ export function TabProvider({ children }: TabProviderProps) {
const needsUniqueTitle = const needsUniqueTitle =
tabData.type === "terminal" || tabData.type === "terminal" ||
tabData.type === "server" || tabData.type === "server" ||
tabData.type === "file_manager"; tabData.type === "file_manager" ||
tabData.type === "tunnel" ||
tabData.type === "docker";
const effectiveTitle = needsUniqueTitle const effectiveTitle = needsUniqueTitle
? computeUniqueTitle(tabData.type, tabData.title) ? computeUniqueTitle(tabData.type, tabData.title)
: tabData.title || ""; : tabData.title || "";

View File

@@ -12,6 +12,8 @@ import {
Terminal as TerminalIcon, Terminal as TerminalIcon,
Server as ServerIcon, Server as ServerIcon,
Folder as FolderIcon, Folder as FolderIcon,
ArrowDownUp as TunnelIcon,
Container as DockerIcon,
Shield as AdminIcon, Shield as AdminIcon,
Network as SshManagerIcon, Network as SshManagerIcon,
User as UserIcon, User as UserIcon,
@@ -33,6 +35,10 @@ export function TabDropdown(): React.ReactElement {
return <ServerIcon className="h-4 w-4" />; return <ServerIcon className="h-4 w-4" />;
case "file_manager": case "file_manager":
return <FolderIcon className="h-4 w-4" />; return <FolderIcon className="h-4 w-4" />;
case "tunnel":
return <TunnelIcon className="h-4 w-4" />;
case "docker":
return <DockerIcon className="h-4 w-4" />;
case "user_profile": case "user_profile":
return <UserIcon className="h-4 w-4" />; return <UserIcon className="h-4 w-4" />;
case "ssh_manager": case "ssh_manager":
@@ -52,6 +58,10 @@ export function TabDropdown(): React.ReactElement {
return tab.title || t("nav.serverStats"); return tab.title || t("nav.serverStats");
case "file_manager": case "file_manager":
return tab.title || t("nav.fileManager"); return tab.title || t("nav.fileManager");
case "tunnel":
return tab.title || t("nav.tunnels");
case "docker":
return tab.title || t("nav.docker");
case "user_profile": case "user_profile":
return tab.title || t("nav.userProfile"); return tab.title || t("nav.userProfile");
case "ssh_manager": case "ssh_manager":

View File

@@ -20,6 +20,8 @@ const languages = [
}, },
{ code: "ru", name: "Russian", nativeName: "Русский" }, { code: "ru", name: "Russian", nativeName: "Русский" },
{ code: "fr", name: "French", nativeName: "Français" }, { code: "fr", name: "French", nativeName: "Français" },
{ code: "it", name: "Italian", nativeName: "Italiano" },
{ code: "ko", name: "Korean", nativeName: "한국어" },
]; ];
export function LanguageSwitcher() { export function LanguageSwitcher() {

View File

@@ -101,6 +101,10 @@ export function UserProfile({
const [commandAutocomplete, setCommandAutocomplete] = useState<boolean>( const [commandAutocomplete, setCommandAutocomplete] = useState<boolean>(
localStorage.getItem("commandAutocomplete") !== "false", localStorage.getItem("commandAutocomplete") !== "false",
); );
const [defaultSnippetFoldersCollapsed, setDefaultSnippetFoldersCollapsed] =
useState<boolean>(
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false",
);
useEffect(() => { useEffect(() => {
fetchUserInfo(); fetchUserInfo();
@@ -154,6 +158,12 @@ export function UserProfile({
localStorage.setItem("commandAutocomplete", enabled.toString()); localStorage.setItem("commandAutocomplete", enabled.toString());
}; };
const handleDefaultSnippetFoldersCollapsedToggle = (enabled: boolean) => {
setDefaultSnippetFoldersCollapsed(enabled);
localStorage.setItem("defaultSnippetFoldersCollapsed", enabled.toString());
window.dispatchEvent(new Event("defaultSnippetFoldersCollapsedChanged"));
};
const handleDeleteAccount = async (e: React.FormEvent) => { const handleDeleteAccount = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setDeleteLoading(true); setDeleteLoading(true);
@@ -391,6 +401,25 @@ export function UserProfile({
</div> </div>
</div> </div>
<div className="mt-6 pt-6 border-t border-dark-border">
<div className="flex items-center justify-between">
<div>
<Label className="text-gray-300">
{t("profile.defaultSnippetFoldersCollapsed")}
</Label>
<p className="text-sm text-gray-400 mt-1">
{t("profile.defaultSnippetFoldersCollapsedDesc")}
</p>
</div>
<Switch
checked={defaultSnippetFoldersCollapsed}
onCheckedChange={
handleDefaultSnippetFoldersCollapsedToggle
}
/>
</div>
</div>
<div className="mt-6 pt-6 border-t border-dark-border"> <div className="mt-6 pt-6 border-t border-dark-border">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>

View File

@@ -838,6 +838,7 @@ export async function getSSHHosts(): Promise<SSHHost[]> {
export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> { export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
try { try {
const submitData = { const submitData = {
connectionType: hostData.connectionType || "ssh",
name: hostData.name || "", name: hostData.name || "",
ip: hostData.ip, ip: hostData.ip,
port: parseInt(hostData.port.toString()) || 22, port: parseInt(hostData.port.toString()) || 22,
@@ -856,6 +857,7 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
enableTerminal: Boolean(hostData.enableTerminal), enableTerminal: Boolean(hostData.enableTerminal),
enableTunnel: Boolean(hostData.enableTunnel), enableTunnel: Boolean(hostData.enableTunnel),
enableFileManager: Boolean(hostData.enableFileManager), enableFileManager: Boolean(hostData.enableFileManager),
enableDocker: Boolean(hostData.enableDocker),
defaultPath: hostData.defaultPath || "/", defaultPath: hostData.defaultPath || "/",
tunnelConnections: hostData.tunnelConnections || [], tunnelConnections: hostData.tunnelConnections || [],
jumpHosts: hostData.jumpHosts || [], jumpHosts: hostData.jumpHosts || [],
@@ -865,8 +867,19 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
? hostData.statsConfig ? hostData.statsConfig
: JSON.stringify(hostData.statsConfig) : JSON.stringify(hostData.statsConfig)
: null, : null,
dockerConfig: hostData.dockerConfig
? typeof hostData.dockerConfig === "string"
? hostData.dockerConfig
: JSON.stringify(hostData.dockerConfig)
: null,
terminalConfig: hostData.terminalConfig || null, terminalConfig: hostData.terminalConfig || null,
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive), forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
// RDP/VNC specific fields
domain: hostData.domain || null,
security: hostData.security || null,
ignoreCert: Boolean(hostData.ignoreCert),
// Guacamole configuration for RDP/VNC
guacamoleConfig: hostData.guacamoleConfig || null,
}; };
if (!submitData.enableTunnel) { if (!submitData.enableTunnel) {
@@ -904,6 +917,7 @@ export async function updateSSHHost(
): Promise<SSHHost> { ): Promise<SSHHost> {
try { try {
const submitData = { const submitData = {
connectionType: hostData.connectionType || "ssh",
name: hostData.name || "", name: hostData.name || "",
ip: hostData.ip, ip: hostData.ip,
port: parseInt(hostData.port.toString()) || 22, port: parseInt(hostData.port.toString()) || 22,
@@ -922,6 +936,7 @@ export async function updateSSHHost(
enableTerminal: Boolean(hostData.enableTerminal), enableTerminal: Boolean(hostData.enableTerminal),
enableTunnel: Boolean(hostData.enableTunnel), enableTunnel: Boolean(hostData.enableTunnel),
enableFileManager: Boolean(hostData.enableFileManager), enableFileManager: Boolean(hostData.enableFileManager),
enableDocker: Boolean(hostData.enableDocker),
defaultPath: hostData.defaultPath || "/", defaultPath: hostData.defaultPath || "/",
tunnelConnections: hostData.tunnelConnections || [], tunnelConnections: hostData.tunnelConnections || [],
jumpHosts: hostData.jumpHosts || [], jumpHosts: hostData.jumpHosts || [],
@@ -931,8 +946,19 @@ export async function updateSSHHost(
? hostData.statsConfig ? hostData.statsConfig
: JSON.stringify(hostData.statsConfig) : JSON.stringify(hostData.statsConfig)
: null, : null,
dockerConfig: hostData.dockerConfig
? typeof hostData.dockerConfig === "string"
? hostData.dockerConfig
: JSON.stringify(hostData.dockerConfig)
: null,
terminalConfig: hostData.terminalConfig || null, terminalConfig: hostData.terminalConfig || null,
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive), forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
// RDP/VNC specific fields
domain: hostData.domain || null,
security: hostData.security || null,
ignoreCert: Boolean(hostData.ignoreCert),
// Guacamole configuration for RDP/VNC
guacamoleConfig: hostData.guacamoleConfig || null,
}; };
if (!submitData.enableTunnel) { if (!submitData.enableTunnel) {
@@ -3109,3 +3135,196 @@ export async function unlinkOIDCFromPasswordAccount(
throw handleApiError(error, "unlink OIDC from password account"); throw handleApiError(error, "unlink OIDC from password account");
} }
} }
// Guacamole API functions
export interface GuacamoleTokenRequest {
protocol: "rdp" | "vnc" | "telnet";
hostname: string;
port?: number;
username?: string;
password?: string;
domain?: string;
security?: string;
ignoreCert?: boolean;
// Extended guacamole configuration
guacamoleConfig?: {
// Display settings
colorDepth?: number;
width?: number;
height?: number;
dpi?: number;
resizeMethod?: string;
forceLossless?: boolean;
// Audio settings
disableAudio?: boolean;
enableAudioInput?: boolean;
// RDP Performance settings
enableWallpaper?: boolean;
enableTheming?: boolean;
enableFontSmoothing?: boolean;
enableFullWindowDrag?: boolean;
enableDesktopComposition?: boolean;
enableMenuAnimations?: boolean;
disableBitmapCaching?: boolean;
disableOffscreenCaching?: boolean;
disableGlyphCaching?: boolean;
disableGfx?: boolean;
// RDP Device redirection
enablePrinting?: boolean;
printerName?: string;
enableDrive?: boolean;
driveName?: string;
drivePath?: string;
createDrivePath?: boolean;
disableDownload?: boolean;
disableUpload?: boolean;
enableTouch?: boolean;
// RDP Session settings
clientName?: string;
console?: boolean;
initialProgram?: string;
serverLayout?: string;
timezone?: string;
// RDP Gateway settings
gatewayHostname?: string;
gatewayPort?: number;
gatewayUsername?: string;
gatewayPassword?: string;
gatewayDomain?: string;
// RDP RemoteApp settings
remoteApp?: string;
remoteAppDir?: string;
remoteAppArgs?: string;
// Clipboard settings
normalizeClipboard?: string;
disableCopy?: boolean;
disablePaste?: boolean;
// VNC specific settings
cursor?: string;
swapRedBlue?: boolean;
readOnly?: boolean;
// Recording settings
recordingPath?: string;
recordingName?: string;
createRecordingPath?: boolean;
recordingExcludeOutput?: boolean;
recordingExcludeMouse?: boolean;
recordingIncludeKeys?: boolean;
// Wake-on-LAN settings
wolSendPacket?: boolean;
wolMacAddr?: string;
wolBroadcastAddr?: string;
wolUdpPort?: number;
wolWaitTime?: number;
};
}
export interface GuacamoleTokenResponse {
token: string;
}
// Helper to convert camelCase to kebab-case for guacamole parameters
function toGuacamoleParams(config: GuacamoleTokenRequest["guacamoleConfig"]): Record<string, unknown> {
if (!config) return {};
const params: Record<string, unknown> = {};
// Map camelCase to guacamole's kebab-case parameter names
const mappings: Record<string, string> = {
colorDepth: "color-depth",
resizeMethod: "resize-method",
forceLossless: "force-lossless",
disableAudio: "disable-audio",
enableAudioInput: "enable-audio-input",
enableWallpaper: "enable-wallpaper",
enableTheming: "enable-theming",
enableFontSmoothing: "enable-font-smoothing",
enableFullWindowDrag: "enable-full-window-drag",
enableDesktopComposition: "enable-desktop-composition",
enableMenuAnimations: "enable-menu-animations",
disableBitmapCaching: "disable-bitmap-caching",
disableOffscreenCaching: "disable-offscreen-caching",
disableGlyphCaching: "disable-glyph-caching",
disableGfx: "disable-gfx",
enablePrinting: "enable-printing",
printerName: "printer-name",
enableDrive: "enable-drive",
driveName: "drive-name",
drivePath: "drive-path",
createDrivePath: "create-drive-path",
disableDownload: "disable-download",
disableUpload: "disable-upload",
enableTouch: "enable-touch",
clientName: "client-name",
initialProgram: "initial-program",
serverLayout: "server-layout",
gatewayHostname: "gateway-hostname",
gatewayPort: "gateway-port",
gatewayUsername: "gateway-username",
gatewayPassword: "gateway-password",
gatewayDomain: "gateway-domain",
remoteApp: "remote-app",
remoteAppDir: "remote-app-dir",
remoteAppArgs: "remote-app-args",
normalizeClipboard: "normalize-clipboard",
disableCopy: "disable-copy",
disablePaste: "disable-paste",
swapRedBlue: "swap-red-blue",
readOnly: "read-only",
recordingPath: "recording-path",
recordingName: "recording-name",
createRecordingPath: "create-recording-path",
recordingExcludeOutput: "recording-exclude-output",
recordingExcludeMouse: "recording-exclude-mouse",
recordingIncludeKeys: "recording-include-keys",
wolSendPacket: "wol-send-packet",
wolMacAddr: "wol-mac-addr",
wolBroadcastAddr: "wol-broadcast-addr",
wolUdpPort: "wol-udp-port",
wolWaitTime: "wol-wait-time",
};
for (const [key, value] of Object.entries(config)) {
if (value !== undefined && value !== null && value !== "") {
const paramName = mappings[key] || key;
// Guacamole expects boolean values as strings "true" or "false"
if (typeof value === "boolean") {
params[paramName] = value ? "true" : "false";
} else {
params[paramName] = value;
}
}
}
return params;
}
export async function getGuacamoleToken(
request: GuacamoleTokenRequest,
): Promise<GuacamoleTokenResponse> {
try {
// Convert guacamoleConfig to guacamole parameter format
const guacParams = toGuacamoleParams(request.guacamoleConfig);
// Debug: log guacamoleConfig and converted params
console.log("[Guacamole] Request guacamoleConfig:", request.guacamoleConfig);
console.log("[Guacamole] Converted params:", guacParams);
console.log("[Guacamole] Param count:", Object.keys(guacParams).length);
// Use authApi (port 30001 without /ssh prefix) since guacamole routes are at /guacamole
const response = await authApi.post("/guacamole/token", {
type: request.protocol,
hostname: request.hostname,
port: request.port,
username: request.username,
password: request.password,
domain: request.domain,
security: request.security,
"ignore-cert": request.ignoreCert,
...guacParams,
});
return response.data;
} catch (error) {
throw handleApiError(error, "get guacamole token");
}
}