Compare commits
33 Commits
main
...
starhound/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
247c1b5c0a | ||
|
|
776f581377 | ||
|
|
3ac7ad0bd7 | ||
|
|
bc6264bb50 | ||
|
|
5d61112a4e | ||
|
|
d047beab13 | ||
|
|
f2285b1abb | ||
|
|
48933e9b11 | ||
|
|
3248b2336b | ||
|
|
4b4bff4b29 | ||
|
|
2f092bd367 | ||
|
|
42e27e7389 | ||
|
|
aa1476fc13 | ||
|
|
7c9762562b | ||
|
|
a84eb5636e | ||
|
|
65466bc3f9 | ||
|
|
208110a433 | ||
|
|
a98359ebc1 | ||
|
|
05a1b3bfaf | ||
|
|
dfb9e7afe7 | ||
|
|
e405f8a6fa | ||
|
|
f8de3369c3 | ||
|
|
150d5796f8 | ||
|
|
18f31ade1e | ||
|
|
4863776f9b | ||
|
|
84c7b9f9fc | ||
|
|
2754585988 | ||
|
|
a06e62b81a | ||
|
|
69dfebab37 | ||
|
|
4da2b985ad | ||
|
|
b57cc52c94 | ||
|
|
757d0c246d | ||
|
|
7975a077ea |
404
.github/workflows/electron.yml
vendored
404
.github/workflows/electron.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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
49
docker/docker-compose.yml
Normal 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
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
12
flatpak/com.karmaa.termix.flatpakref
Normal file
12
flatpak/com.karmaa.termix.flatpakref
Normal 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
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
29
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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(
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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`),
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
108
src/backend/guacamole/guacamole-server.ts
Normal file
108
src/backend/guacamole/guacamole-server.ts
Normal 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 };
|
||||||
|
|
||||||
159
src/backend/guacamole/routes.ts
Normal file
159
src/backend/guacamole/routes.ts
Normal 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;
|
||||||
|
|
||||||
198
src/backend/guacamole/token-service.ts
Normal file
198
src/backend/guacamole/token-service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
3470
src/locales/it/translation.json
Normal file
3470
src/locales/it/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
1815
src/locales/ko/translation.json
Normal file
1815
src/locales/ko/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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-хостов",
|
||||||
|
|||||||
@@ -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
109
src/types/guacamole-common-js.d.ts
vendored
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
126
src/ui/desktop/apps/docker/DockerManager.tsx
Normal file
126
src/ui/desktop/apps/docker/DockerManager.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
386
src/ui/desktop/apps/guacamole/GuacamoleDisplay.tsx
Normal file
386
src/ui/desktop/apps/guacamole/GuacamoleDisplay.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
@@ -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
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
82
src/ui/desktop/apps/terminal/SudoPasswordPopup.tsx
Normal file
82
src/ui/desktop/apps/terminal/SudoPasswordPopup.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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")}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
143
src/ui/desktop/apps/tunnel/TunnelManager.tsx
Normal file
143
src/ui/desktop/apps/tunnel/TunnelManager.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 || "";
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user