diff --git a/.commitlintrc.json b/.commitlintrc.json index cba65ea51..d2572e133 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -1,21 +1,6 @@ { "extends": ["@commitlint/config-conventional"], "rules": { - "type-enum": [ - 2, - "always", - [ - "feat", - "fix", - "docs", - "style", - "refactor", - "perf", - "test", - "chore", - "revert" - ] - ], "subject-case": [0] } } diff --git a/.dockerignore b/.dockerignore index 06fae233e..353e19038 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,13 +5,22 @@ yarn-error.log* dist build -.next -.nuxt +coverage +release -.env.local -.env.development.local -.env.test.local -.env.production.local +# Local data, secrets and editor state +db +old_db +ssl +uploads +plugins +.env +.env.* +.mcp.json +.claude +CLAUDE.md +termix-plugin-signing.key +*.tmxplug.sig .vscode .idea @@ -29,64 +38,31 @@ Thumbs.db .git .gitignore +.husky README.md CONTRIBUTING.md +CHANGELOG.md LICENSE -docs/repo-images/ - -uploads/ - electron/ electron-builder.json +Casks +packaging *.log *.tmp *.temp - logs -*.log - pids *.pid *.seed *.pid.lock -coverage - .nyc_output - -jspm_packages/ - .npm - .eslintcache - -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -.node_repl_history - -*.tgz - -.yarn-integrity - .cache .parcel-cache - -.next - -.nuxt - -.vuepress/dist - -.serverless - -.fusebox/ - -.dynamodb/ - -.tern-port +*.tgz +.node_repl_history diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index f8aef6cbb..000000000 --- a/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -[*.{js,cjs,mjs,jsx,ts,tsx,json,css,scss,md,yml,yaml}] -indent_style = space -indent_size = 2 - -[*.md] -trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes index 140887259..8c3b50383 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,22 +1,5 @@ * text=auto eol=lf -*.js text eol=lf -*.cjs text eol=lf -*.mjs text eol=lf -*.jsx text eol=lf -*.ts text eol=lf -*.tsx text eol=lf -*.json text eol=lf -*.css text eol=lf -*.scss text eol=lf -*.html text eol=lf -*.md text eol=lf -*.yaml text eol=lf -*.yml text eol=lf - -*.sh text eol=lf -*.bash text eol=lf - *.bat text eol=crlf *.cmd text eol=crlf *.ps1 text eol=crlf @@ -25,10 +8,10 @@ *.jpg binary *.jpeg binary *.gif binary +*.webp binary *.ico binary -*.svg binary +*.icns binary *.woff binary *.woff2 binary *.ttf binary *.eot binary -*.icns binary diff --git a/.github/ISSUE_TEMPLATE/beta_feedback.yml b/.github/ISSUE_TEMPLATE/beta_feedback.yml new file mode 100644 index 000000000..d6c12f2dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/beta_feedback.yml @@ -0,0 +1,75 @@ +name: Beta feedback +description: A bug, rough edge or idea from a Termix beta +labels: [beta] +body: + - type: markdown + attributes: + value: | + Thanks for testing a beta. Short reports are fine. If you opened this from Termix, the version and setup are already filled in. + - type: input + id: termix-version + attributes: + label: Beta version + description: Shown in Settings under Updates, or your image tag. + placeholder: "e.g. 26.11.0-beta.2" + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: The bug, what felt off, or your idea. Screenshots help a lot. + validations: + required: true + - type: dropdown + id: regression + attributes: + label: Does this happen on the latest stable release? + options: + - "Not tested on stable" + - "Yes, it happens on stable too" + - "No, only on this beta" + - "Not a bug, just feedback" + validations: + required: false + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: As few steps as you can. + placeholder: | + 1. + 2. + 3. + validations: + required: false + - type: textarea + id: beta-plugins + attributes: + label: Plugins on a beta + description: Filled in by Termix. One plugin and version per line. + render: text + validations: + required: false + - type: textarea + id: environment + attributes: + label: Environment + description: Browser or desktop app, OS, and how the server is installed. + validations: + required: false + - type: textarea + id: logs + attributes: + label: Logs + description: Errors from the browser console, `docker logs termix`, or the error Termix shows. This is shown as code, so no backticks needed. + render: text + validations: + required: false + - type: checkboxes + id: checks + attributes: + label: Before you submit + options: + - label: I searched open issues and this is not a duplicate + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..828794780 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,107 @@ +name: Bug report +description: Something in Termix itself is broken +labels: [bug] +type: Bug +body: + - type: markdown + attributes: + value: | + Thanks for the report. A maintainer reads every issue, even when there is no reply right away. We usually reply once there is a fix or a workaround. + + If you came from **Report an issue** in Termix, your versions and environment are already filled in below. + + Is it about a plugin (terminal, file manager, Docker, remote desktop and so on)? Report it on that plugin's repo instead. **Report an issue** on the plugin's page in Termix takes you there with your versions filled in. + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Describe the bug. Screenshots help a lot. + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: As few steps as you can. + placeholder: | + 1. + 2. + 3. + validations: + required: true + - type: textarea + id: expected + attributes: + label: What did you expect? + description: What should have happened instead? + validations: + required: false + - type: input + id: termix-version + attributes: + label: Termix version + description: Shown in Settings under About, or in your User Profile. + placeholder: "e.g. 26.10.0" + validations: + required: true + - type: dropdown + id: platform + attributes: + label: Platform + description: Where do you see the problem? + options: + - "Web browser" + - "Desktop app (Windows)" + - "Desktop app (macOS)" + - "Desktop app (Linux)" + - "Mobile app (iOS)" + - "Mobile app (Android)" + - "Other" + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Server install + description: How is the Termix server installed? + options: + - "Docker" + - "Manual build" + - "Proxmox community script" + - "Desktop app (built-in server)" + - "Not sure" + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: Browser and OS, plus anything special about your setup (reverse proxy, database, SSO). + placeholder: | + Web, Chrome on Windows. Behind nginx, Postgres. + validations: + required: false + - type: textarea + id: logs + attributes: + label: Logs + description: Errors from the browser console, `docker logs termix`, or the error Termix shows. This is shown as code, so no backticks needed. + render: text + validations: + required: false + - type: textarea + id: context + attributes: + label: Anything else? + description: Links, config, or anything that might help. + validations: + required: false + - type: checkboxes + id: checks + attributes: + label: Before you submit + options: + - label: I searched open and closed issues and this is not a duplicate + required: true + - label: I am on the latest version, or I said which version I am on + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 570bbc396..b2d6ce27b 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,11 @@ blank_issues_enabled: false contact_links: - - name: Support Center - url: https://github.com/Termix-SSH/Support/issues - about: Report any feature requests or bugs in the support center - - name: Discord + - name: Problem with a plugin? + url: https://github.com/Termix-SSH/Termix#plugins + about: Each plugin has its own repo. Find it in this list, or use Report an issue on the plugin's page in Termix. + - name: Questions and chat url: https://discord.gg/jVQGdvHDrf - about: Official Termix Discord server for general discussion (not recommended for support) + about: Ask the community on Discord. Bugs and ideas belong in issues. + - name: Business or priority support + url: https://termix.site/contact + about: For teams that need a faster response. Contact us to discuss terms. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..b6eeaa3e3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,41 @@ +name: Feature request +description: Suggest an idea for Termix itself +labels: [feature] +type: Feature +body: + - type: markdown + attributes: + value: | + Is it about a plugin (terminal, file manager, Docker, remote desktop and so on)? Report it on that plugin's repo instead. **Report an issue** on the plugin's page in Termix takes you there with your versions filled in. + - type: markdown + attributes: + value: | + Thanks for the idea. Explain the problem first, then how you would solve it. + - type: textarea + id: problem + attributes: + label: What problem would this solve? + description: What are you trying to do, and what gets in the way today? + validations: + required: true + - type: textarea + id: solution + attributes: + label: What would you like? + description: Describe how it should work. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives + description: Anything you tried or considered instead. + validations: + required: false + - type: textarea + id: context + attributes: + label: Anything else? + description: Screenshots, mockups or links. + validations: + required: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index cb2a696d1..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,91 +0,0 @@ -version: 2 -updates: - # npm dependencies (single root package.json, no workspaces) - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - open-pull-requests-limit: 15 - labels: - - "dependencies" - - "npm" - commit-message: - prefix: "chore" - prefix-development: "chore" - include: "scope" - groups: - dev-patch-updates: - dependency-type: "development" - update-types: - - "patch" - dev-minor-updates: - dependency-type: "development" - update-types: - - "minor" - prod-patch-updates: - dependency-type: "production" - update-types: - - "patch" - prod-minor-updates: - dependency-type: "production" - update-types: - - "minor" - # Major bumps grouped so they land as a single reviewable PR instead of - # one-per-package noise. These often need manual follow-up (Electron, - # React, Vite, Tailwind, Express 5, etc.). - major-updates: - update-types: - - "major" - ignore: - # typescript-eslint declares `typescript: >=4.8.4 <6.1.0`, and TypeScript 7 - # removed `ts.Extension`, which @typescript-eslint/typescript-estree reads - # at import time. Bumping to 7 makes `eslint .` fail to load its own config, - # so `npm run lint` cannot run at all. Drop this once typescript-eslint - # supports TypeScript 7. - - dependency-name: "typescript" - update-types: ["version-update:semver-major"] - - # Docker base images (docker/Dockerfile + docker-compose / compose-dev) - - package-ecosystem: "docker" - directory: "/docker" - schedule: - interval: "weekly" - day: "monday" - open-pull-requests-limit: 10 - labels: - - "dependencies" - - "docker" - commit-message: - prefix: "chore" - include: "scope" - groups: - docker-patch-updates: - update-types: - - "patch" - docker-minor-updates: - update-types: - - "minor" - docker-major-updates: - update-types: - - "major" - - # GitHub Actions used across the workflows in .github/workflows - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - open-pull-requests-limit: 10 - labels: - - "dependencies" - - "github-actions" - commit-message: - prefix: "ci" - include: "scope" - groups: - github-actions: - update-types: - - "patch" - - "minor" - - "major" diff --git a/.github/funding.yml b/.github/funding.yml deleted file mode 100644 index c14d56027..000000000 --- a/.github/funding.yml +++ /dev/null @@ -1 +0,0 @@ -custom: https://donate.termix.site/ diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index d0b31d4d5..000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,32 +0,0 @@ -# Overview - -_Short summary of what this PR does_ - -- [ ] Added: ... -- [ ] Updated: ... -- [ ] Removed: ... -- [ ] Fixed: ... - -# Changes Made - -_Detailed explanation of changes (if needed)_ - -- ... - -# Related Issues - -_Link any issues this PR addresses_ - -- Closes #ISSUE_NUMBER -- Related to #ISSUE_NUMBER - -# Screenshots / Demos - -_(Optional: add before/after screenshots, GIFs, or console output)_ - -# Checklist - -- [ ] Code follows project style guidelines -- [ ] Supports mobile and desktop UI/app (if applicable) -- [ ] I have read [Contributing.md](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) -- [ ] This is not a translation request. See [docs](https://docs.termix.site/translations) diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml deleted file mode 100644 index 524818119..000000000 --- a/.github/workflows/beta-release.yml +++ /dev/null @@ -1,206 +0,0 @@ -name: Weekly Beta Release - -on: - schedule: - - cron: "15 6 * * 1" - workflow_dispatch: - inputs: - dry_run: - description: "Build and test but do not push images, upload installers, or publish a release" - required: false - default: false - type: boolean - -permissions: - contents: write - -jobs: - prep: - runs-on: blacksmith-2vcpu-ubuntu-2404 - outputs: - dev_branch: ${{ steps.dev.outputs.branch }} - beta_version: ${{ steps.dev.outputs.beta_version }} - sha: ${{ steps.dev.outputs.sha }} - steps: - - name: Checkout repository - uses: actions/checkout@v7 - with: - fetch-depth: 1 - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: ".nvmrc" - - - name: Resolve newest dev branch and compute beta version - id: dev - env: - GH_TOKEN: ${{ secrets.TERMIX_PAT }} - run: | - REFS=$(gh api "repos/${{ github.repository }}/branches" --paginate -q '.[].name') - if DEV_BRANCH=$(printf '%s\n' "$REFS" | node scripts/latest-dev-branch.cjs 2>/dev/null); then - echo "Newest dev branch: $DEV_BRANCH" - else - echo "No dev-X.Y.Z branch open; nothing to snapshot for this week's beta." - echo "branch=" >> "$GITHUB_OUTPUT" - exit 0 - fi - - BASE_VERSION=$(node scripts/parse-dev-branch.cjs "$DEV_BRANCH") - BETA_VERSION="${BASE_VERSION}-beta.$(date -u +%Y%m%d)" - SHA=$(gh api "repos/${{ github.repository }}/branches/$DEV_BRANCH" -q .commit.sha) - - echo "Beta version: $BETA_VERSION" - echo "branch=$DEV_BRANCH" >> "$GITHUB_OUTPUT" - echo "beta_version=$BETA_VERSION" >> "$GITHUB_OUTPUT" - echo "sha=$SHA" >> "$GITHUB_OUTPUT" - - verify: - needs: [prep] - if: ${{ needs.prep.outputs.dev_branch != '' }} - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - name: Checkout dev branch - uses: actions/checkout@v7 - with: - ref: ${{ needs.prep.outputs.dev_branch }} - fetch-depth: 1 - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: ".nvmrc" - cache: "npm" - - - name: Install dependencies - run: npm ci - - - name: Build plugin SDK - run: npm run build:sdk - - - name: Run ESLint - run: npx eslint . - - - name: Type check - run: npm run type-check - - # Before the tests: the plugin boot tests load the built bundles. - - name: Build - run: npm run build - - - name: Run unit tests - run: npm run test - - - name: Run plugin tests - run: npm run test:plugins - - create-release: - needs: [prep, verify] - if: ${{ needs.prep.outputs.dev_branch != '' && inputs.dry_run != true }} - runs-on: blacksmith-2vcpu-ubuntu-2404 - permissions: - contents: write - steps: - - name: Checkout dev branch - uses: actions/checkout@v7 - with: - ref: ${{ needs.prep.outputs.dev_branch }} - fetch-depth: 0 - - - name: Resolve previous beta commit - id: prev - env: - GH_TOKEN: ${{ secrets.TERMIX_PAT }} - run: | - SHA="${{ needs.prep.outputs.sha }}" - PREV_SHA=$(gh release view beta --repo ${{ github.repository }} --json targetCommitish -q .targetCommitish 2>/dev/null || true) - if [ -z "$PREV_SHA" ] || ! git cat-file -e "$PREV_SHA^{commit}" 2>/dev/null; then - PREV_SHA=$(git rev-parse -q --verify "refs/tags/beta^{commit}" || true) - fi - # The last beta may come from an older dev branch, so diff from where the two meet. - if [ -n "$PREV_SHA" ] && ! git merge-base --is-ancestor "$PREV_SHA" "$SHA"; then - PREV_SHA=$(git merge-base "$PREV_SHA" "$SHA" || true) - fi - echo "Previous beta commit: ${PREV_SHA:-none}" - echo "sha=$PREV_SHA" >> "$GITHUB_OUTPUT" - - - name: Generate rolling beta release notes - run: | - PREV="${{ steps.prev.outputs.sha }}" - SHA="${{ needs.prep.outputs.sha }}" - MAX=150 - if [ -z "$PREV" ]; then - CHANGES="- Could not find the previous beta commit." - else - LOG=$(git log --oneline --no-merges "$PREV..$SHA" -- . ':!package-lock.json') - COUNT=$(printf '%s' "$LOG" | grep -c . || true) - if [ "$COUNT" -eq 0 ]; then - CHANGES="- No new commits since the last beta." - else - CHANGES=$(printf '%s\n' "$LOG" | head -n "$MAX" | sed 's/^/- /') - if [ "$COUNT" -gt "$MAX" ]; then - CHANGES="$CHANGES - - ...and $((COUNT - MAX)) more" - fi - CHANGES="$CHANGES - - [Full diff](https://github.com/${{ github.repository }}/compare/$PREV...$SHA)" - fi - fi - - cat > BETA_RELEASE_BODY.md << EOF - > [!WARNING] - > This is an automated weekly beta build, snapshotted from the \`${{ needs.prep.outputs.dev_branch }}\` branch. It is not a stable release: it may contain unfinished features, regressions, or breaking changes, and this tag is overwritten every week. Do not run it in production. - > - > Found a bug? [Open a Beta Feedback report](https://github.com/Termix-SSH/Support/issues/new?template=beta_feedback.yml) and mention this build: \`${{ needs.prep.outputs.beta_version }}\`. - - **Snapshot of:** \`${{ needs.prep.outputs.dev_branch }}\` @ \`${{ needs.prep.outputs.sha }}\` - **Docker image:** \`ghcr.io/lukegus/termix:beta\` / \`docker.io/bugattiguy527/termix:beta\` (rolling), or pin to \`:beta-${{ needs.prep.outputs.beta_version }}\` for this exact build. - **Built:** $(date -u +"%Y-%m-%d %H:%M UTC") - - ### Changes since last beta - - $CHANGES - EOF - - - name: Create or update rolling beta release - env: - GH_TOKEN: ${{ secrets.TERMIX_PAT }} - run: | - TAG="beta" - TITLE="Beta (rolling) - ${{ needs.prep.outputs.beta_version }}" - if gh release view "$TAG" --repo ${{ github.repository }} >/dev/null 2>&1; then - # --target only applies to a new tag, so move the existing one by hand. - gh api -X PATCH "repos/${{ github.repository }}/git/refs/tags/$TAG" \ - -f sha="${{ needs.prep.outputs.sha }}" -F force=true >/dev/null - gh release edit "$TAG" --repo ${{ github.repository }} \ - --title "$TITLE" --notes-file BETA_RELEASE_BODY.md \ - --prerelease --target "${{ needs.prep.outputs.sha }}" - else - gh release create "$TAG" --repo ${{ github.repository }} \ - --title "$TITLE" --notes-file BETA_RELEASE_BODY.md \ - --prerelease --target "${{ needs.prep.outputs.sha }}" - fi - - docker: - needs: [prep, verify, create-release] - if: ${{ always() && needs.prep.outputs.dev_branch != '' && needs.verify.result == 'success' && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') }} - uses: ./.github/workflows/docker.yml - with: - version: ${{ needs.prep.outputs.beta_version }} - build_type: Beta - dry_run: ${{ inputs.dry_run == true }} - source_ref: ${{ needs.prep.outputs.sha }} - secrets: inherit - - electron-release: - needs: [prep, verify, create-release] - if: ${{ always() && needs.prep.outputs.dev_branch != '' && inputs.dry_run != true && needs.create-release.result == 'success' }} - uses: ./.github/workflows/electron.yml - with: - build_type: all - artifact_destination: release - release_tag: beta - version_override: ${{ needs.prep.outputs.beta_version }} - source_ref: ${{ needs.prep.outputs.sha }} - secrets: inherit diff --git a/.github/workflows/crowdin-sync.yml b/.github/workflows/crowdin-sync.yml deleted file mode 100644 index 653ed2e79..000000000 --- a/.github/workflows/crowdin-sync.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Crowdin Sync - -on: - schedule: - - cron: "0 6 * * *" - workflow_dispatch: - inputs: - branch: - description: "Branch to sync translations into" - required: false - type: string - -permissions: - contents: write - -jobs: - crowdin: - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - name: Resolve target branch - id: branch - run: | - BRANCH="${{ inputs.branch }}" - if [ -z "$BRANCH" ]; then - BRANCH="${{ github.event.repository.default_branch }}" - fi - echo "name=$BRANCH" >> "$GITHUB_OUTPUT" - - - name: Checkout branch - uses: actions/checkout@v7 - with: - ref: ${{ steps.branch.outputs.name }} - fetch-depth: 0 - token: ${{ secrets.TERMIX_PAT }} - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: ".nvmrc" - - - name: Upload sources to Crowdin - uses: crowdin/github-action@v3 - with: - upload_sources: true - upload_translations: false - download_translations: false - create_pull_request: false - push_translations: false - token: ${{ secrets.CROWDIN_API_KEY }} - project_id: "858252" - env: - CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_KEY }} - - - name: Pre-translate untranslated strings - env: - CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }} - run: node scripts/crowdin-pretranslate.cjs - - - name: Download translations from Crowdin - uses: crowdin/github-action@v3 - with: - upload_sources: false - upload_translations: false - download_translations: true - create_pull_request: false - push_translations: false - token: ${{ secrets.CROWDIN_API_KEY }} - project_id: "858252" - env: - CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_KEY }} - - - name: Commit translations - run: | - git config user.name "LukeGus" - git config user.email "bugattiguy527@gmail.com" - - git add src/ui/locales/translated plugins/*/locales/translated - if git diff --cached --quiet; then - echo "No translation changes to commit." - exit 0 - fi - git commit -m "chore: sync Crowdin translations" - git push origin HEAD:"${{ steps.branch.outputs.name }}" diff --git a/.github/workflows/dependabot-retarget.yml b/.github/workflows/dependabot-retarget.yml deleted file mode 100644 index 15f052275..000000000 --- a/.github/workflows/dependabot-retarget.yml +++ /dev/null @@ -1,157 +0,0 @@ -name: Retarget and Merge Dependabot PRs - -on: - schedule: - - cron: "0 6 * * *" - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - retarget-and-merge: - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - name: Checkout repository - uses: actions/checkout@v7 - with: - fetch-depth: 1 - - - name: Resolve newest dev branch - id: dev - env: - GH_TOKEN: ${{ secrets.TERMIX_PAT }} - run: | - REFS=$(gh api "repos/${{ github.repository }}/branches" --paginate -q '.[].name') - # The helper exits non-zero when no dev-X.Y.Z branch exists; treat that - # as "nothing to do" rather than a workflow failure. - if DEV_BRANCH=$(printf '%s\n' "$REFS" | node scripts/latest-dev-branch.cjs 2>/dev/null); then - echo "Newest dev branch: $DEV_BRANCH" - echo "branch=$DEV_BRANCH" >> "$GITHUB_OUTPUT" - else - echo "No dev-X.Y.Z branch open; nothing to retarget." - echo "branch=" >> "$GITHUB_OUTPUT" - fi - - - name: Retarget and merge Dependabot PRs - if: ${{ steps.dev.outputs.branch != '' }} - env: - GH_TOKEN: ${{ secrets.TERMIX_PAT }} - DEV_BRANCH: ${{ steps.dev.outputs.branch }} - REPO: ${{ github.repository }} - run: | - set -uo pipefail - - CONFLICT_LABEL="dependabot-rebase-requested" - - # Ensure the bookkeeping label exists (no-op if it already does). - gh label create "$CONFLICT_LABEL" --repo "$REPO" \ - --color "D93F0B" --description "Retarget workflow asked Dependabot to rebase a conflicting PR" \ - 2>/dev/null || true - - # True if the PR already carries the conflict label. - has_conflict_label() { - gh pr view "$1" --repo "$REPO" --json labels \ - -q '.labels[].name' | grep -qx "$CONFLICT_LABEL" - } - - # Wait until GitHub has a definite mergeable verdict for a PR (it - # returns UNKNOWN while recomputing after a base change or a push). - # Echoes " ". - wait_for_verdict() { - local pr="$1" mergeable state - for _ in $(seq 1 30); do - read -r mergeable state < <(gh pr view "$pr" --repo "$REPO" \ - --json mergeable,mergeStateStatus \ - -q '.mergeable + " " + .mergeStateStatus') - if [ "$mergeable" != "UNKNOWN" ] && [ "$state" != "UNKNOWN" ]; then - echo "$mergeable $state" - return 0 - fi - sleep 20 - done - echo "$mergeable $state" - } - - # Phase 1: retarget every open Dependabot PR from main onto the dev - # branch. This kicks off a Dependabot rebase for each. - PR_NUMBERS=$(gh pr list --repo "$REPO" \ - --author "app/dependabot" \ - --base main \ - --state open \ - --json number -q '.[].number') - - # Pick up PRs already sitting on the dev branch from a previous run too. - PR_NUMBERS="$PR_NUMBERS $(gh pr list --repo "$REPO" \ - --author "app/dependabot" \ - --base "$DEV_BRANCH" \ - --state open \ - --json number -q '.[].number')" - - PR_NUMBERS=$(printf '%s\n' $PR_NUMBERS | sort -un) - - if [ -z "$PR_NUMBERS" ]; then - echo "No open Dependabot PRs to process." - exit 0 - fi - - for PR in $PR_NUMBERS; do - BASE=$(gh pr view "$PR" --repo "$REPO" --json baseRefName -q .baseRefName) - if [ "$BASE" != "$DEV_BRANCH" ]; then - echo "Retargeting PR #$PR ($BASE -> $DEV_BRANCH)" - gh pr edit "$PR" --repo "$REPO" --base "$DEV_BRANCH" - fi - done - - # Phase 2: merge one at a time. Each merge can make the remaining npm - # PRs stale, so re-check immediately before merging and rebase stragglers. - for PR in $PR_NUMBERS; do - echo "::group::PR #$PR" - - read -r MERGEABLE STATE < <(wait_for_verdict "$PR") - echo " mergeable=$MERGEABLE mergeStateStatus=$STATE" - - # BEHIND = clean but needs the latest base; ask Dependabot to rebase - # and skip for now (next run merges it once it is up to date). - if [ "$STATE" = "BEHIND" ]; then - echo " PR #$PR is behind $DEV_BRANCH; asking Dependabot to rebase." - gh pr comment "$PR" --repo "$REPO" --body "@dependabot rebase" - echo "::endgroup::" - continue - fi - - # DIRTY / CONFLICTING = a real conflict. Try a rebase once (label it so - # we can tell next time); if it is STILL conflicting on a later run - # despite already being labelled, the rebase failed for good - close it - # so Dependabot reopens a fresh PR against the current dev branch. - if [ "$MERGEABLE" = "CONFLICTING" ] || [ "$STATE" = "DIRTY" ]; then - if has_conflict_label "$PR"; then - echo " PR #$PR still conflicts after a prior rebase request; closing so Dependabot reopens it fresh." - gh pr close "$PR" --repo "$REPO" --delete-branch \ - --comment "Closing: this PR still conflicts with $DEV_BRANCH after a rebase attempt (its changes are likely already merged). Dependabot will reopen a fresh PR computed against the current $DEV_BRANCH." - else - echo " PR #$PR conflicts with $DEV_BRANCH; requesting a rebase and labelling it." - gh pr edit "$PR" --repo "$REPO" --add-label "$CONFLICT_LABEL" - gh pr comment "$PR" --repo "$REPO" --body "@dependabot rebase" - fi - echo "::endgroup::" - continue - fi - - # A clean PR that was previously flagged has recovered - drop the label. - if has_conflict_label "$PR"; then - gh pr edit "$PR" --repo "$REPO" --remove-label "$CONFLICT_LABEL" || true - fi - - echo " Squash-merging PR #$PR" - if gh pr merge "$PR" --repo "$REPO" --squash --admin; then - echo " Merged PR #$PR" - # Give GitHub a moment to mark the now-stale siblings BEHIND. - sleep 15 - else - echo " Could not merge PR #$PR now; it will be retried next run." - fi - - echo "::endgroup::" - done diff --git a/.github/workflows/deploy-helm.yml b/.github/workflows/deploy-helm.yml deleted file mode 100644 index 27b44f206..000000000 --- a/.github/workflows/deploy-helm.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Deploy Helm - -on: - workflow_dispatch: - inputs: - namespace: - description: "Kubernetes namespace" - required: true - default: termix - release: - description: "Helm release name" - required: true - default: termix - values_file: - description: "Values file to use" - required: true - default: charts/termix/values-gitops-example.yaml - image_tag: - description: "Image tag to deploy" - required: false - default: "" - -permissions: - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - environment: production - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Set up Helm - uses: azure/setup-helm@v5 - with: - version: v3.15.4 - - - name: Configure kubeconfig - env: - KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }} - run: | - test -n "$KUBE_CONFIG" - echo "$KUBE_CONFIG" | base64 -d > "$RUNNER_TEMP/kubeconfig" - chmod 600 "$RUNNER_TEMP/kubeconfig" - - - name: Lint chart - run: helm lint charts/termix - - - name: Deploy - env: - KUBECONFIG: ${{ runner.temp }}/kubeconfig - IMAGE_TAG: ${{ inputs.image_tag }} - RELEASE_NAME: ${{ inputs.release }} - TARGET_NAMESPACE: ${{ inputs.namespace }} - VALUES_FILE: ${{ inputs.values_file }} - run: | - set -euo pipefail - [[ "$RELEASE_NAME" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]] - [[ "$TARGET_NAMESPACE" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]] - case "$VALUES_FILE" in - charts/termix/*.yaml) ;; - *) echo "values_file must be a YAML file under charts/termix" >&2; exit 1 ;; - esac - test -f "$VALUES_FILE" - ARGS=() - if [ -n "$IMAGE_TAG" ]; then - ARGS+=(--set "image.tag=$IMAGE_TAG") - fi - helm upgrade --install "$RELEASE_NAME" charts/termix \ - --namespace "$TARGET_NAMESPACE" \ - --create-namespace \ - --values "$VALUES_FILE" \ - --atomic \ - --timeout 10m \ - "${ARGS[@]}" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 299ce409d..d486744ce 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: "Version to build (e.g., 1.8.0)" + description: "Version to build (e.g., 26.10.0)" required: true build_type: description: "Build type" @@ -22,11 +22,11 @@ on: workflow_call: inputs: version: - description: "Version to build (e.g., 1.8.0)" + description: "Version to build (e.g., 26.10.0)" required: true type: string build_type: - description: "Build type (Development or Production)" + description: "Build type (Development, Beta or Production)" required: true type: string dry_run: @@ -52,7 +52,14 @@ jobs: fetch-depth: 1 - name: Resolve source revision - run: echo "SOURCE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" + run: | + echo "SOURCE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" + echo "BUILD_CREATED=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" + + # The image reads its version from package.json, so a beta has to say so. + - name: Stamp beta version + if: ${{ inputs.build_type == 'Beta' }} + run: node scripts/sync-version.cjs --version "${{ inputs.version }}" - name: Set up QEMU uses: docker/setup-qemu-action@v4 @@ -74,25 +81,23 @@ jobs: ALL_TAGS=() if [ "$BUILD_TYPE" = "Production" ]; then - TAGS+=("release-$VERSION" "$VERSION" "latest") - for tag in "${TAGS[@]}"; do - ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") - ALL_TAGS+=("docker.io/bugattiguy527/termix:$tag") - done + TAGS+=("$VERSION" "latest") elif [ "$BUILD_TYPE" = "Beta" ]; then - TAGS+=("beta" "beta-$VERSION") - for tag in "${TAGS[@]}"; do - ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") - ALL_TAGS+=("docker.io/bugattiguy527/termix:$tag") - done + TAGS+=("beta" "$VERSION") else TAGS+=("dev-$VERSION") - for tag in "${TAGS[@]}"; do - ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") - done fi + for tag in "${TAGS[@]}"; do + ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") + ALL_TAGS+=("ghcr.io/termix-ssh/termix:$tag") + if [ "$BUILD_TYPE" != "Development" ]; then + ALL_TAGS+=("docker.io/bugattiguy527/termix:$tag") + fi + done + echo "ALL_TAGS=$(IFS=,; echo "${ALL_TAGS[*]}")" >> $GITHUB_ENV + echo "TAGS=${TAGS[*]}" >> $GITHUB_ENV - name: Login to GHCR if: ${{ !inputs.dry_run }} @@ -107,7 +112,7 @@ jobs: uses: docker/login-action@v4 with: username: bugattiguy527 - password: ${{ secrets.DOCKERHUB_TOKEN }} + password: ${{ secrets.DOCKERHUB_BUGATTIGUY527_TOKEN }} - name: Build and push multi-arch image uses: useblacksmith/build-push-action@v2 @@ -122,9 +127,29 @@ jobs: labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.revision=${{ env.SOURCE_SHA }} - org.opencontainers.image.created=${{ github.run_id }} + org.opencontainers.image.created=${{ env.BUILD_CREATED }} outputs: ${{ inputs.dry_run && 'type=cacheonly' || 'type=registry,compression=zstd' }} + - name: Login to Docker Hub as termixssh + id: termix_hub + if: ${{ (inputs.build_type == 'Production' || inputs.build_type == 'Beta') && !inputs.dry_run }} + env: + TOKEN: ${{ secrets.DOCKERHUB_TERMIXSSH_TOKEN }} + run: | + if [ -z "$TOKEN" ]; then + echo "DOCKERHUB_TERMIXSSH_TOKEN is not set, skipping docker.io/termixssh" + exit 0 + fi + echo "$TOKEN" | docker login docker.io --username termixssh --password-stdin + echo "ready=true" >> "$GITHUB_OUTPUT" + + - name: Copy image to docker.io/termixssh + if: ${{ steps.termix_hub.outputs.ready == 'true' }} + run: | + for tag in $TAGS; do + docker buildx imagetools create --tag "docker.io/termixssh/termix:$tag" "ghcr.io/termix-ssh/termix:$tag" + done + - name: Cleanup Docker if: always() run: | diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index 576af8533..a3e29608c 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -117,6 +117,11 @@ jobs: - name: Install dependencies run: npm ci + # A beta build has to know it is a beta, or it reports the stable version. + - name: Stamp beta version + if: inputs.version_override != '' + run: node scripts/sync-version.cjs --version "${{ inputs.version_override }}" + - name: Get version id: package-version run: | @@ -220,6 +225,11 @@ jobs: npm install --force @rollup/rollup-linux-arm64-gnu npm install --force @rollup/rollup-linux-arm-gnueabihf + # A beta build has to know it is a beta, or it reports the stable version. + - name: Stamp beta version + if: inputs.version_override != '' + run: node scripts/sync-version.cjs --version "${{ inputs.version_override }}" + - name: Build Linux x64 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -355,8 +365,8 @@ jobs: 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|https://github.com/Termix-SSH/Termix/releases/download/vVERSION_PLACEHOLDER/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/vVERSION_PLACEHOLDER/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 @@ -382,7 +392,7 @@ jobs: run: | VERSION="${{ steps.flatpak-version.outputs.version }}" cp packaging/flatpak/com.karmaa.termix.flatpakref release/ - sed -i "s|VERSION_PLACEHOLDER|release-${VERSION}-tag|g" release/com.karmaa.termix.flatpakref + sed -i "s|VERSION_PLACEHOLDER|v${VERSION}|g" release/com.karmaa.termix.flatpakref - name: Upload Flatpak bundle uses: actions/upload-artifact@v7 @@ -429,6 +439,11 @@ jobs: npm install dmg-license node scripts/install-macos-sharp.cjs + # A beta build has to know it is a beta, or it reports the stable version. + - name: Stamp beta version + if: inputs.version_override != '' + run: node scripts/sync-version.cjs --version "${{ inputs.version_override }}" + - name: Check for Code Signing Certificates id: check_certs run: | @@ -468,14 +483,15 @@ jobs: security find-identity -v -p codesigning $KEYCHAIN_PATH + # Betas never go to the App Store, so they skip the MAS package. - name: Build macOS App Store Package - if: steps.check_certs.outputs.has_certs == 'true' + id: mas_build + if: steps.check_certs.outputs.has_certs == 'true' && inputs.version_override == '' env: ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=4096 run: | - CURRENT_VERSION=$(node -p "require('./package.json').version") BUILD_VERSION="${{ github.run_number }}" npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" @@ -534,7 +550,7 @@ jobs: NODE_OPTIONS: --max-old-space-size=4096 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - if [ "${{ steps.check_certs.outputs.has_certs }}" != "true" ]; then + if [ "${{ steps.mas_build.outcome }}" != "success" ]; then npm run build fi npx electron-builder --mac dmg --universal --x64 --arm64 --publish never @@ -555,7 +571,7 @@ jobs: echo "path=$MAS_PKG" >> "$GITHUB_OUTPUT" - name: Upload macOS MAS PKG - if: steps.check_certs.outputs.has_certs == 'true' && steps.locate_mas_pkg.outputs.path != '' && (inputs.artifact_destination == 'file' || inputs.artifact_destination == 'release' || inputs.artifact_destination == 'submit') + if: steps.mas_build.outcome == 'success' && steps.locate_mas_pkg.outputs.path != '' && (inputs.artifact_destination == 'file' || inputs.artifact_destination == 'release' || inputs.artifact_destination == 'submit') uses: actions/upload-artifact@v7 with: name: termix_macos_universal_mas @@ -618,7 +634,7 @@ jobs: 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 + sed -i '' "s|download/[^/]*/termix_macos|download/v$VERSION/termix_macos|g" homebrew-generated/termix.rb - name: Upload Homebrew Cask as artifact uses: actions/upload-artifact@v7 @@ -634,7 +650,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION="${{ steps.homebrew-version.outputs.version }}" - RELEASE_TAG="release-$VERSION-tag" + RELEASE_TAG="v$VERSION" gh release list --repo ${{ github.repository }} --limit 100 | grep -q "$RELEASE_TAG" || { echo "Release $RELEASE_TAG not found" @@ -673,7 +689,7 @@ jobs: run: | $VERSION = "${{ steps.package-version.outputs.version }}" $MSI_NAME = "termix_windows_x64_msi.msi" - $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$($VERSION)-tag/$($MSI_NAME)" + $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/v$($VERSION)/$($MSI_NAME)" Write-Host "Downloading from $DOWNLOAD_URL" New-Item -ItemType Directory -Force -Path "release_asset" @@ -696,7 +712,7 @@ jobs: $CHECKSUM = "${{ steps.msi-info.outputs.checksum }}" $MSI_NAME = "${{ steps.msi-info.outputs.msi_name }}" - $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$MSI_NAME" + $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/v$VERSION/$MSI_NAME" New-Item -ItemType Directory -Force -Path "choco-build" Copy-Item -Path "packaging\chocolatey\*" -Destination "choco-build" -Recurse -Force @@ -722,7 +738,7 @@ jobs: choco pack termix-ssh.nuspec if ($LASTEXITCODE -ne 0) { - throw "Chocolatey push failed with exit code $LASTEXITCODE" + throw "Chocolatey pack failed with exit code $LASTEXITCODE" } - name: Check for Chocolatey API Key @@ -784,7 +800,7 @@ jobs: mkdir -p release_assets APPIMAGE_X64_NAME="termix_linux_x64_appimage.AppImage" - URL_X64="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_X64_NAME" + URL_X64="https://github.com/Termix-SSH/Termix/releases/download/v$VERSION/$APPIMAGE_X64_NAME" PATH_X64="release_assets/$APPIMAGE_X64_NAME" echo "Downloading x64 AppImage from $URL_X64" curl -L -o "$PATH_X64" "$URL_X64" @@ -792,7 +808,7 @@ jobs: CHECKSUM_X64=$(sha256sum "$PATH_X64" | awk '{print $1}') APPIMAGE_ARM64_NAME="termix_linux_arm64_appimage.AppImage" - URL_ARM64="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_ARM64_NAME" + URL_ARM64="https://github.com/Termix-SSH/Termix/releases/download/v$VERSION/$APPIMAGE_ARM64_NAME" PATH_ARM64="release_assets/$APPIMAGE_ARM64_NAME" echo "Downloading arm64 AppImage from $URL_ARM64" curl -L -o "$PATH_ARM64" "$URL_ARM64" @@ -918,7 +934,7 @@ jobs: run: | VERSION="${{ steps.package-version.outputs.version }}" DMG_NAME="termix_macos_universal_dmg.dmg" - URL="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$DMG_NAME" + URL="https://github.com/Termix-SSH/Termix/releases/download/v$VERSION/$DMG_NAME" mkdir -p release_asset DOWNLOAD_PATH="release_asset/$DMG_NAME" @@ -952,8 +968,8 @@ jobs: cp Casks/termix.rb homebrew-submission/Casks/t/termix.rb - sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb - sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb + sed -i '' "s|version \".*\"|version \"$VERSION\"|g" homebrew-submission/Casks/t/termix.rb + sed -i '' "s|sha256 \".*\"|sha256 \"$CHECKSUM\"|g" homebrew-submission/Casks/t/termix.rb - name: Verify Cask syntax run: | @@ -1191,7 +1207,7 @@ jobs: META_DIR="$RUNNER_TEMP/asc_metadata" rm -rf "$META_DIR" node scripts/generate-appstore-notes.cjs \ - --notes RELEASE_NOTES.md \ + --changelog CHANGELOG.md \ --out-dir "$META_DIR" \ --locales "en-US" echo "metadata_path=$META_DIR" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml deleted file mode 100644 index 4f8e3c66e..000000000 --- a/.github/workflows/helm.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Helm - -on: - pull_request: - paths: - - "charts/**" - - ".github/workflows/helm.yml" - push: - branches: [main] - paths: - - "charts/**" - - ".github/workflows/helm.yml" - workflow_dispatch: - inputs: - publish: - description: "Publish chart to GHCR OCI registry" - required: true - type: boolean - default: false - -permissions: - contents: read - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Set up Helm - uses: azure/setup-helm@v5 - with: - version: v3.15.4 - - - name: Lint chart - run: helm lint charts/termix - - - name: Render default chart - run: helm template termix charts/termix --namespace termix - - - name: Render GitOps example - run: helm template termix charts/termix --values charts/termix/values-gitops-example.yaml --namespace termix - - - name: Render Traefik example - run: helm template termix charts/termix --values charts/termix/values-traefik.yaml --namespace termix - - publish: - needs: lint - if: github.event_name == 'workflow_dispatch' && inputs.publish - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Set up Helm - uses: azure/setup-helm@v5 - with: - version: v3.15.4 - - - name: Log in to GHCR - run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username "${{ github.actor }}" --password-stdin - - - name: Package chart - run: helm package charts/termix --destination .helm-packages - - - name: Push chart - run: | - OWNER="$(echo "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')" - helm push .helm-packages/termix-*.tgz "oci://ghcr.io/$OWNER/charts" diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml deleted file mode 100644 index 54f1486fd..000000000 --- a/.github/workflows/openapi.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Generate OpenAPI Specification - -on: - workflow_dispatch: - -jobs: - generate-openapi: - name: Generate OpenAPI JSON - runs-on: blacksmith-2vcpu-ubuntu-2404 - - steps: - - name: Checkout repository - uses: actions/checkout@v7 - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: ".nvmrc" - cache: "npm" - - - name: Install dependencies - run: npm ci - - - name: Generate OpenAPI specification - run: npm run generate:openapi - - - name: Upload OpenAPI artifact - uses: actions/upload-artifact@v7 - with: - name: openapi-spec - path: openapi.json - retention-days: 90 diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index cee0b3135..8809af565 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -28,8 +28,6 @@ jobs: run: npm run build:sdk - name: Lint - # npm run lint, not npx eslint — the script also checks that the - # generated dialect schemas match schema.ts, which eslint cannot see. run: npm run lint - name: Run Prettier check @@ -44,16 +42,10 @@ jobs: - name: Run unit tests run: npm run test - - name: Run plugin tests - run: npm run test:plugins - database-dialects: name: Postgres and MySQL runs-on: blacksmith-2vcpu-ubuntu-2404 - # The test suite only ever sees SQLite. Everything that differs per engine — - # the RETURNING replacements, the read-then-write transactions, the - # migrations themselves — is only covered here, against real servers. services: postgres: image: postgres:16 @@ -100,17 +92,12 @@ jobs: - name: Build plugin SDK run: npm run build:sdk - # Each run applies the migrations to an empty database first, so a - # migration that does not apply cleanly fails the build. - name: Verify Postgres run: npm run verify:dialect -- postgres://termix:termix@127.0.0.1:5432/termix_test - name: Verify MySQL run: npm run verify:dialect -- mysql://termix:termix@127.0.0.1:3306/termix_test - # The same repository suite the SQLite run executes, pointed at each - # engine. This is where a dialect difference in a query shows up as a - # failing assertion rather than as a bug report. - name: Repository tests on Postgres env: TEST_DIALECT: postgres diff --git a/.github/workflows/redirect-issues.yml b/.github/workflows/redirect-issues.yml deleted file mode 100644 index 7a23ce1f2..000000000 --- a/.github/workflows/redirect-issues.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Redirect issues -on: - issues: - types: [opened] - -permissions: - issues: write - -jobs: - close: - if: github.event.issue.author_association != 'OWNER' && github.event.issue.author_association != 'COLLABORATOR' - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - uses: actions/github-script@v9 - with: - script: | - const n = context.issue.number; - await github.rest.issues.createComment({ - ...context.repo, issue_number: n, - body: "Do not create issues here. Use the dedicated support repo and issue template system: https://github.com/Termix-SSH/Support" - }); - await github.rest.issues.update({ ...context.repo, issue_number: n, state: "closed", state_reason: "not_planned" }); - await github.rest.issues.lock({ ...context.repo, issue_number: n, lock_reason: "off-topic" }); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc60a618a..9cedee366 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,24 @@ name: Release +# Stable: run by hand from a dev-X.Y.Z branch. Beta: run by hand from a dev +# branch, or let the Tuesday and Friday schedule snapshot the newest dev +# branch when it moved since the last beta. + on: + schedule: + - cron: "0 15 * * 2,5" workflow_dispatch: inputs: + channel: + description: "Stable release or beta" + required: true + default: Stable + type: choice + options: + - Stable + - Beta mode: - description: "Release mode" + description: "Release mode (Overwrite release is stable only)" required: true default: Everything type: choice @@ -17,23 +31,45 @@ on: permissions: contents: write +concurrency: + group: release + cancel-in-progress: false + jobs: prep: runs-on: blacksmith-2vcpu-ubuntu-2404 outputs: - version: ${{ steps.info.outputs.version }} - release_tag: ${{ steps.info.outputs.release_tag }} + channel: ${{ steps.mode.outputs.channel }} + mode: ${{ steps.mode.outputs.mode }} + skip: ${{ steps.beta.outputs.skip || 'false' }} + version: ${{ steps.info.outputs.version || steps.beta.outputs.version }} + release_tag: ${{ steps.info.outputs.release_tag || steps.beta.outputs.release_tag }} mobile_version: ${{ steps.info.outputs.mobile_version }} - dev_branch: ${{ steps.info.outputs.dev_branch }} + mobile_tag: ${{ steps.info.outputs.mobile_tag }} + dev_branch: ${{ steps.info.outputs.dev_branch || steps.beta.outputs.dev_branch }} + sha: ${{ steps.beta.outputs.sha }} + prev_ref: ${{ steps.beta.outputs.prev_ref }} steps: + - name: Resolve channel and mode + id: mode + run: | + CHANNEL="${{ inputs.channel || 'Beta' }}" + MODE="${{ inputs.mode || 'Everything' }}" + echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT" + echo "mode=$MODE" >> "$GITHUB_OUTPUT" + if [ "$CHANNEL" = "Beta" ] && [ "$MODE" = "Overwrite release" ]; then + echo "Overwrite release is for stable releases only." + exit 1 + fi + - name: Guard branch (release modes) - if: ${{ inputs.mode != 'Overwrite release' && !startsWith(github.ref, 'refs/heads/dev-') }} + if: ${{ github.event_name == 'workflow_dispatch' && steps.mode.outputs.mode != 'Overwrite release' && !startsWith(github.ref, 'refs/heads/dev-') }} run: | echo "This mode must be run from a dev branch (got ${{ github.ref }})." exit 1 - name: Guard branch (overwrite mode) - if: ${{ inputs.mode == 'Overwrite release' && github.ref != 'refs/heads/main' }} + if: ${{ steps.mode.outputs.mode == 'Overwrite release' && github.ref != 'refs/heads/main' }} run: | echo "Overwrite release must be run from main (got ${{ github.ref }})." exit 1 @@ -50,10 +86,11 @@ jobs: - name: Resolve versions id: info + if: ${{ steps.mode.outputs.channel == 'Stable' }} env: GH_TOKEN: ${{ github.token }} run: | - if [ "${{ inputs.mode }}" = "Overwrite release" ]; then + if [ "${{ steps.mode.outputs.mode }}" = "Overwrite release" ]; then DEV_BRANCH="" VERSION=$(node -p "require('./package.json').version") else @@ -62,26 +99,106 @@ jobs: fi echo "dev_branch=$DEV_BRANCH" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "release_tag=release-$VERSION-tag" >> "$GITHUB_OUTPUT" + echo "release_tag=v$VERSION" >> "$GITHUB_OUTPUT" MOBILE_RAW=$(gh release view -R Termix-SSH/Mobile --json tagName -q .tagName) - MOBILE_VERSION=$(echo "$MOBILE_RAW" | sed -E 's/^release-//; s/-tag$//') + MOBILE_VERSION=$(echo "$MOBILE_RAW" | sed -E 's/^release-//; s/-tag$//; s/^v//') if [ -z "$MOBILE_VERSION" ]; then echo "Failed to resolve the latest Termix-SSH/Mobile release version." exit 1 fi echo "mobile_version=$MOBILE_VERSION" >> "$GITHUB_OUTPUT" + echo "mobile_tag=$MOBILE_RAW" >> "$GITHUB_OUTPUT" - - name: Validate release notes + - name: Check CHANGELOG.md has notes for this version + if: ${{ steps.mode.outputs.channel == 'Stable' }} run: | node scripts/generate-release-body.cjs \ --version "${{ steps.info.outputs.version }}" \ --mobile-version "${{ steps.info.outputs.mobile_version }}" \ - --notes RELEASE_NOTES.md > /dev/null + --mobile-tag "${{ steps.info.outputs.mobile_tag }}" > /dev/null + + - name: Resolve beta version + id: beta + if: ${{ steps.mode.outputs.channel == 'Beta' }} + env: + GH_TOKEN: ${{ secrets.TERMIX_PAT }} + run: | + if [ "${{ github.event_name }}" = "schedule" ]; then + REFS=$(gh api "repos/${{ github.repository }}/branches" --paginate -q '.[].name') + if ! DEV_BRANCH=$(printf '%s\n' "$REFS" | node scripts/latest-dev-branch.cjs 2>/dev/null); then + echo "No dev-X.Y.Z branch is open, so no beta today." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + else + DEV_BRANCH="${GITHUB_REF#refs/heads/}" + fi + + BASE=$(node scripts/parse-dev-branch.cjs "$DEV_BRANCH") + SHA=$(gh api "repos/${{ github.repository }}/branches/$DEV_BRANCH" -q .commit.sha) + TAGS=$(gh api "repos/${{ github.repository }}/git/matching-refs/tags/v$BASE-beta." -q '.[].ref' | sed 's#^refs/tags/##') + LATEST=$(printf '%s\n' "$TAGS" | node scripts/beta-version.cjs "$BASE" --latest) + VERSION=$(printf '%s\n' "$TAGS" | node scripts/beta-version.cjs "$BASE") + + if [ -n "$LATEST" ]; then + LATEST_SHA=$(gh api "repos/${{ github.repository }}/commits/$LATEST" -q .sha) + if [ "$LATEST_SHA" = "$SHA" ] && [ "${{ github.event_name }}" = "schedule" ]; then + echo "$DEV_BRANCH has not moved since $LATEST, so no beta today." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + PREV="$LATEST" + else + PREV=$(gh release view --repo ${{ github.repository }} --json tagName -q .tagName 2>/dev/null || true) + fi + + echo "Beta $VERSION from $DEV_BRANCH @ $SHA, changes since ${PREV:-nothing}" + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "dev_branch=$DEV_BRANCH" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "release_tag=v$VERSION" >> "$GITHUB_OUTPUT" + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "prev_ref=$PREV" >> "$GITHUB_OUTPUT" + + verify-beta: + needs: [prep] + if: ${{ needs.prep.outputs.channel == 'Beta' && needs.prep.outputs.skip != 'true' }} + runs-on: blacksmith-2vcpu-ubuntu-2404 + steps: + - name: Checkout beta commit + uses: actions/checkout@v7 + with: + ref: ${{ needs.prep.outputs.sha }} + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version-file: ".nvmrc" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build plugin SDK + run: npm run build:sdk + + - name: Lint + run: npm run lint + + - name: Type check + run: npm run type-check + + - name: Build + run: npm run build + + - name: Run unit tests + run: npm run test normalize: needs: [prep] - if: ${{ inputs.mode != 'Overwrite release' }} + if: ${{ needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Overwrite release' }} runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - name: Checkout dev branch @@ -110,16 +227,9 @@ jobs: npm run format npm run lint - # The plugin boot tests load the built bundles. - - name: Build plugins - run: npm run build:plugins - - name: Run unit tests run: npm run test - - name: Run plugin tests - run: npm run test:plugins - - name: Sync version run: node scripts/sync-version.cjs --version "${{ needs.prep.outputs.version }}" @@ -133,7 +243,7 @@ jobs: exit 0 fi - if [ "${{ inputs.mode }}" = "Dry run" ]; then + if [ "${{ needs.prep.outputs.mode }}" = "Dry run" ]; then echo "DRY RUN: would commit and push the following changes to ${{ needs.prep.outputs.dev_branch }}:" git --no-pager diff --stat exit 0 @@ -143,9 +253,9 @@ jobs: git commit -m "chore: lint, format, and bump version to ${{ needs.prep.outputs.version }}" git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}" - crowdin: + sync-main: needs: [prep, normalize] - if: ${{ inputs.mode != 'Dry run' && inputs.mode != 'Overwrite release' }} + if: ${{ needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Dry run' && needs.prep.outputs.mode != 'Overwrite release' }} runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - name: Checkout dev branch @@ -155,79 +265,27 @@ jobs: fetch-depth: 0 token: ${{ secrets.TERMIX_PAT }} - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: ".nvmrc" - - name: Merge main into dev branch run: | git config user.name "LukeGus" git config user.email "bugattiguy527@gmail.com" git fetch origin main - git merge origin/main -X ours --no-edit || true - git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}" - - - name: Clean up stale Crowdin git integration branch - continue-on-error: true - env: - GH_TOKEN: ${{ secrets.TERMIX_PAT }} - run: | - PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head i18n_translate --state open --json number -q '.[0].number' || true) - if [ -n "$PR_NUMBER" ]; then - gh pr close "$PR_NUMBER" --repo ${{ github.repository }} --delete-branch || true + # -X ours leaves modify/delete conflicts, so settle those to the dev side too + if ! git merge origin/main -X ours --no-edit; then + git diff --name-only --diff-filter=U | while read -r f; do + if git cat-file -e "HEAD:$f" 2>/dev/null; then + git checkout --ours -- "$f" && git add -- "$f" + else + git rm -q -- "$f" + fi + done + git commit --no-edit fi - git push origin --delete i18n_translate || true - - - name: Upload sources to Crowdin - uses: crowdin/github-action@v3 - with: - upload_sources: true - upload_translations: false - download_translations: false - create_pull_request: false - push_translations: false - token: ${{ secrets.CROWDIN_API_KEY }} - project_id: "858252" - env: - CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_KEY }} - - # Off until the machine translation credits are topped up. - - name: Pre-translate untranslated strings - if: false - env: - CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }} - run: node scripts/crowdin-pretranslate.cjs - - - name: Download translations from Crowdin - uses: crowdin/github-action@v3 - with: - upload_sources: false - upload_translations: false - download_translations: true - create_pull_request: false - push_translations: false - token: ${{ secrets.CROWDIN_API_KEY }} - project_id: "858252" - env: - CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_KEY }} - - - name: Commit translations to dev branch - run: | - git config user.name "LukeGus" - git config user.email "bugattiguy527@gmail.com" - - git add src/ui/locales/translated plugins/*/locales/translated - if git diff --cached --quiet; then - echo "No translation changes to commit." - exit 0 - fi - git commit -m "chore: sync Crowdin translations for ${{ needs.prep.outputs.version }}" git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}" merge-to-main: - needs: [prep, normalize, crowdin] - if: ${{ always() && needs.prep.result == 'success' && (needs.normalize.result == 'success' || needs.normalize.result == 'skipped') && (needs.crowdin.result == 'success' || needs.crowdin.result == 'skipped') }} + needs: [prep, normalize, sync-main] + if: ${{ always() && needs.prep.result == 'success' && needs.prep.outputs.channel == 'Stable' && (needs.normalize.result == 'success' || needs.normalize.result == 'skipped') && (needs.sync-main.result == 'success' || needs.sync-main.result == 'skipped') }} runs-on: blacksmith-2vcpu-ubuntu-2404 outputs: main_sha: ${{ steps.merge.outputs.main_sha }} @@ -249,7 +307,7 @@ jobs: node scripts/generate-release-body.cjs \ --version "${{ needs.prep.outputs.version }}" \ --mobile-version "${{ needs.prep.outputs.mobile_version }}" \ - --notes RELEASE_NOTES.md > PR_BODY.md + --mobile-tag "${{ needs.prep.outputs.mobile_tag }}" > PR_BODY.md - name: Open and squash-merge PR to main id: merge @@ -257,16 +315,16 @@ jobs: GH_TOKEN: ${{ secrets.TERMIX_PAT }} run: | DEV_BRANCH="${{ needs.prep.outputs.dev_branch }}" - TITLE="release-${{ needs.prep.outputs.version }}" + TITLE="v${{ needs.prep.outputs.version }}" - if [ "${{ inputs.mode }}" = "Overwrite release" ]; then + if [ "${{ needs.prep.outputs.mode }}" = "Overwrite release" ]; then echo "OVERWRITE: no merge; building from main as-is." echo "build_ref=main" >> "$GITHUB_OUTPUT" echo "main_sha=$(gh api repos/${{ github.repository }}/commits/main -q .sha)" >> "$GITHUB_OUTPUT" exit 0 fi - if [ "${{ inputs.mode }}" = "Dry run" ]; then + if [ "${{ needs.prep.outputs.mode }}" = "Dry run" ]; then echo "DRY RUN: would open and squash-merge a PR from $DEV_BRANCH into main." echo "DRY RUN: downstream jobs will build from $DEV_BRANCH instead of main." echo "build_ref=$DEV_BRANCH" >> "$GITHUB_OUTPUT" @@ -295,18 +353,19 @@ jobs: echo "build_ref=$MAIN_SHA" >> "$GITHUB_OUTPUT" docker: - needs: [prep, merge-to-main] + needs: [prep, verify-beta, merge-to-main] + if: ${{ always() && needs.prep.result == 'success' && needs.prep.outputs.skip != 'true' && (needs.merge-to-main.result == 'success' || needs.verify-beta.result == 'success') }} uses: ./.github/workflows/docker.yml with: version: ${{ needs.prep.outputs.version }} - build_type: Production - dry_run: ${{ inputs.mode == 'Dry run' }} - source_ref: ${{ needs.merge-to-main.outputs.build_ref }} + build_type: ${{ needs.prep.outputs.channel == 'Beta' && 'Beta' || 'Production' }} + dry_run: ${{ needs.prep.outputs.mode == 'Dry run' }} + source_ref: ${{ needs.merge-to-main.outputs.build_ref || needs.prep.outputs.sha }} secrets: inherit create-release: needs: [prep, merge-to-main, docker] - if: ${{ inputs.mode != 'Dry run' }} + if: ${{ always() && needs.prep.outputs.channel == 'Stable' && needs.docker.result == 'success' && needs.prep.outputs.mode != 'Dry run' }} runs-on: blacksmith-2vcpu-ubuntu-2404 permissions: contents: write @@ -323,7 +382,7 @@ jobs: node-version-file: ".nvmrc" - name: Clear existing release artifacts (overwrite mode) - if: ${{ inputs.mode == 'Overwrite release' }} + if: ${{ needs.prep.outputs.mode == 'Overwrite release' }} env: GH_TOKEN: ${{ secrets.TERMIX_PAT }} run: | @@ -338,40 +397,83 @@ jobs: done - name: Generate release body - if: ${{ inputs.mode != 'Overwrite release' }} + if: ${{ needs.prep.outputs.mode != 'Overwrite release' }} run: | node scripts/generate-release-body.cjs \ --version "${{ needs.prep.outputs.version }}" \ --mobile-version "${{ needs.prep.outputs.mobile_version }}" \ - --notes RELEASE_NOTES.md > RELEASE_BODY.md + --mobile-tag "${{ needs.prep.outputs.mobile_tag }}" > RELEASE_BODY.md - name: Create or update GitHub release - if: ${{ inputs.mode != 'Overwrite release' }} + if: ${{ needs.prep.outputs.mode != 'Overwrite release' }} env: GH_TOKEN: ${{ secrets.TERMIX_PAT }} run: | TAG="${{ needs.prep.outputs.release_tag }}" - TITLE="release-${{ needs.prep.outputs.version }}" + TITLE="Termix v${{ needs.prep.outputs.version }}" if gh release view "$TAG" --repo ${{ github.repository }} >/dev/null 2>&1; then gh release edit "$TAG" --repo ${{ github.repository }} --title "$TITLE" --notes-file RELEASE_BODY.md else gh release create "$TAG" --repo ${{ github.repository }} --title "$TITLE" --notes-file RELEASE_BODY.md --target "${{ needs.merge-to-main.outputs.main_sha }}" fi + create-beta-release: + needs: [prep, docker] + if: ${{ always() && needs.prep.outputs.channel == 'Beta' && needs.docker.result == 'success' && needs.prep.outputs.mode != 'Dry run' }} + runs-on: blacksmith-2vcpu-ubuntu-2404 + permissions: + contents: write + steps: + - name: Checkout beta commit + uses: actions/checkout@v7 + with: + ref: ${{ needs.prep.outputs.sha }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version-file: ".nvmrc" + + - name: Generate beta release body + run: | + node scripts/generate-beta-body.cjs \ + --version "${{ needs.prep.outputs.version }}" \ + --branch "${{ needs.prep.outputs.dev_branch }}" \ + --sha "${{ needs.prep.outputs.sha }}" \ + --prev "${{ needs.prep.outputs.prev_ref }}" > BETA_BODY.md + + - name: Create beta release + env: + GH_TOKEN: ${{ secrets.TERMIX_PAT }} + run: | + VERSION="${{ needs.prep.outputs.version }}" + TITLE="Termix v${VERSION%-beta.*} Beta ${VERSION##*-beta.}" + TAG="${{ needs.prep.outputs.release_tag }}" + if gh release view "$TAG" --repo ${{ github.repository }} >/dev/null 2>&1; then + gh release edit "$TAG" --repo ${{ github.repository }} \ + --title "$TITLE" --notes-file BETA_BODY.md --prerelease + else + gh release create "$TAG" --repo ${{ github.repository }} \ + --title "$TITLE" --notes-file BETA_BODY.md \ + --prerelease --target "${{ needs.prep.outputs.sha }}" + fi + electron-release: - needs: [prep, merge-to-main, create-release] - if: ${{ always() && needs.merge-to-main.result == 'success' && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') }} + needs: [prep, merge-to-main, docker, create-release, create-beta-release] + if: ${{ always() && needs.prep.outputs.skip != 'true' && needs.docker.result == 'success' && needs.create-release.result != 'failure' && needs.create-beta-release.result != 'failure' }} uses: ./.github/workflows/electron.yml with: build_type: all - artifact_destination: ${{ inputs.mode == 'Dry run' && 'file' || 'release' }} + artifact_destination: ${{ needs.prep.outputs.mode == 'Dry run' && 'file' || 'release' }} release_tag: ${{ needs.prep.outputs.release_tag }} - source_ref: ${{ needs.merge-to-main.outputs.build_ref }} + version_override: ${{ needs.prep.outputs.channel == 'Beta' && needs.prep.outputs.version || '' }} + source_ref: ${{ needs.merge-to-main.outputs.build_ref || needs.prep.outputs.sha }} secrets: inherit electron-submit: needs: [prep, merge-to-main, electron-release] - if: ${{ inputs.mode != 'Dry run' && inputs.mode != 'Skip submit' }} + if: ${{ needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Dry run' && needs.prep.outputs.mode != 'Skip submit' }} uses: ./.github/workflows/electron.yml with: build_type: all @@ -381,7 +483,7 @@ jobs: cask-commit-back: needs: [prep, electron-release] - if: ${{ inputs.mode != 'Dry run' }} + if: ${{ needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Dry run' }} runs-on: blacksmith-2vcpu-ubuntu-2404 permissions: contents: write @@ -421,142 +523,37 @@ jobs: git commit -m "chore: bump Homebrew cask to $VERSION" git push origin HEAD:main - docs: - needs: [prep, merge-to-main] - if: ${{ always() && needs.merge-to-main.result == 'success' && inputs.mode != 'Overwrite release' }} + prune-betas: + needs: [prep, create-release] + if: ${{ always() && needs.prep.outputs.channel == 'Stable' && needs.create-release.result == 'success' && needs.prep.outputs.mode != 'Overwrite release' }} runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - - name: Checkout Termix - uses: actions/checkout@v7 - with: - ref: ${{ needs.merge-to-main.outputs.build_ref }} - fetch-depth: 1 - path: termix - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: "termix/.nvmrc" - cache: "npm" - cache-dependency-path: termix/package-lock.json - - - name: Generate OpenAPI spec - working-directory: termix - run: | - npm ci - npm run generate:openapi - - - name: Checkout Docs repository - uses: actions/checkout@v7 - with: - repository: Termix-SSH/Docs - ref: main - fetch-depth: 0 - token: ${{ secrets.TERMIX_PAT }} - path: docs-repo - - - name: Create docs release branch - working-directory: docs-repo - run: | - BRANCH="dev-${{ needs.prep.outputs.version }}" - if git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null 2>&1; then - echo "Reusing existing docs branch $BRANCH." - git checkout -B "$BRANCH" "origin/$BRANCH" - else - git checkout -B "$BRANCH" - fi - - - name: Overwrite OpenAPI spec and regenerate API docs - working-directory: docs-repo - run: | - cp ../termix/openapi.json static/openapi.json - npm ci - npm run docusaurus clean-api-docs termix - npm run docusaurus gen-api-docs termix - - - name: Commit and push docs branch - working-directory: docs-repo - run: | - git config user.name "LukeGus" - git config user.email "bugattiguy527@gmail.com" - - if git diff --quiet; then - echo "No docs changes to commit." - exit 0 - fi - - if [ "${{ inputs.mode }}" = "Dry run" ]; then - echo "DRY RUN: would commit and push the following docs changes to dev-${{ needs.prep.outputs.version }}:" - git --no-pager diff --stat - exit 0 - fi - - git add -A - git commit -m "feat: update API docs for ${{ needs.prep.outputs.version }}" - git push origin "dev-${{ needs.prep.outputs.version }}" - - - name: Open and squash-merge docs PR - if: ${{ inputs.mode != 'Dry run' }} - working-directory: docs-repo + - name: Delete this version's beta releases (tags stay) env: GH_TOKEN: ${{ secrets.TERMIX_PAT }} run: | - BRANCH="dev-${{ needs.prep.outputs.version }}" - if ! git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null 2>&1; then - echo "No docs branch pushed (nothing changed); skipping PR." - exit 0 - fi + PREFIX="v${{ needs.prep.outputs.version }}-beta." + gh release list --repo ${{ github.repository }} --limit 200 --json tagName -q '.[].tagName' \ + | grep -F "$PREFIX" | while read -r TAG; do + echo "Deleting release $TAG" + gh release delete "$TAG" --repo ${{ github.repository }} --yes || true + done - PR_NUMBER=$(gh pr list --repo Termix-SSH/Docs --head "$BRANCH" --base main --state open --json number -q '.[0].number' || true) - if [ -z "$PR_NUMBER" ]; then - PR_URL=$(gh pr create --repo Termix-SSH/Docs \ - --base main --head "$BRANCH" \ - --title "release-${{ needs.prep.outputs.version }}" \ - --body "API docs for ${{ needs.prep.outputs.version }}") - PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') - fi - - if [ -z "$PR_NUMBER" ]; then - echo "Failed to find or create a PR for $BRANCH." - exit 1 - fi - - gh pr merge "$PR_NUMBER" --repo Termix-SSH/Docs --squash --admin - - publish-youtube: + # The docs site reads the env catalog, API spec and download list from the + # newest stable release. + docs-refresh: needs: [prep, electron-release] - if: ${{ always() && inputs.mode != 'Dry run' && inputs.mode != 'Overwrite release' && needs.electron-release.result == 'success' }} + if: ${{ always() && needs.prep.outputs.channel == 'Stable' && needs.electron-release.result == 'success' && needs.prep.outputs.mode != 'Dry run' }} runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - - name: Checkout main - uses: actions/checkout@v7 - with: - ref: main - fetch-depth: 1 - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: ".nvmrc" - - - name: Set release video to public + - name: Tell the docs site env: - YOUTUBE_CLIENT_ID: ${{ secrets.YOUTUBE_CLIENT_ID }} - YOUTUBE_CLIENT_SECRET: ${{ secrets.YOUTUBE_CLIENT_SECRET }} - YOUTUBE_REFRESH_TOKEN: ${{ secrets.YOUTUBE_REFRESH_TOKEN }} - run: node scripts/publish-youtube.cjs + GH_TOKEN: ${{ secrets.TERMIX_PAT }} + run: gh api repos/Termix-SSH/Docs/dispatches -f event_type=core-released cleanup: - needs: - [ - prep, - merge-to-main, - electron-release, - cask-commit-back, - docs, - publish-youtube, - ] - if: ${{ always() && (inputs.mode == 'Everything' || inputs.mode == 'Skip submit') && needs.merge-to-main.result == 'success' && needs.docs.result == 'success' }} + needs: [prep, merge-to-main, electron-release, cask-commit-back] + if: ${{ always() && needs.prep.outputs.channel == 'Stable' && (needs.prep.outputs.mode == 'Everything' || needs.prep.outputs.mode == 'Skip submit') && needs.merge-to-main.result == 'success' }} runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - name: Checkout repository @@ -571,10 +568,3 @@ jobs: run: | git push https://x-access-token:${{ secrets.TERMIX_PAT }}@github.com/${{ github.repository }}.git \ --delete "${{ needs.prep.outputs.dev_branch }}" || true - - - name: Delete dev branch in Docs - env: - GH_TOKEN: ${{ secrets.TERMIX_PAT }} - run: | - git push https://x-access-token:${{ secrets.TERMIX_PAT }}@github.com/Termix-SSH/Docs.git \ - --delete "dev-${{ needs.prep.outputs.version }}" || true diff --git a/.gitignore b/.gitignore index 458fe27ac..b2496f533 100644 --- a/.gitignore +++ b/.gitignore @@ -15,16 +15,15 @@ coverage # Plugin build output. termix-plugin build writes it, the packaged server # gets its copy under dist/plugins. -/plugins/*/dist/ *.tmxplug *.tmxplug.sig termix-plugin-signing.key /src/**/*.js -.vscode/* -!.vscode/settings.json -.idea +.vscode/ +.idea/ .DS_Store +Thumbs.db *.suo *.ntvs* *.njsproj diff --git a/.prettierignore b/.prettierignore index 9d9f404b7..11fe2e1de 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,25 +1,14 @@ build coverage dist +node_modules +package-lock.json + dist-ssr release - -node_modules -src/mcp-server/node_modules -package-lock.json -pnpm-lock.yaml -yarn.lock - db - -.env - *.min.js *.min.css openapi.json - -# Generated by drizzle-kit; formatting is the tool's own drizzle/ - -# Helm templates contain Go template syntax, not plain YAML -charts/*/templates/ +src/ui/settings/settings-search-index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index dff3ab474..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "prettier.prettierPath": "./node_modules/prettier", - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - }, - "eslint.validate": [ - "javascript", - "javascriptreact", - "typescript", - "typescriptreact" - ], - "files.eol": "\n", - "files.insertFinalNewline": true, - "files.trimTrailingWhitespace": true, - "[markdown]": { - "files.trimTrailingWhitespace": false - }, - "typescript.tsdk": "node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true -} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..ec8a614dc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +## 26.10.0 + +### Added + +- Plugins. Every feature beyond hosts, credentials and sharing is now its own plugin with its own repo, and you pick which ones you want +- A Plugins tab to install, update, pin, turn off and remove plugins from the official registry, with release notes and a beta channel per plugin +- Developer mode to install a plugin from a file, and a plugin SDK on npm to build your own +- The community plugin registry is open for submissions. Termix can install community plugins in a later update +- Redesigned UI with a Manage tab for hosts and credentials and a Settings tab with search +- New full screen onboarding where you choose which plugins to start with +- Beta channel for Termix itself in Settings > Updates +- Docs links on every settings page, in the Plugins tab, in plugin settings and in the command palette +- Plugins can link their own docs and list the environment variables they read, shown on their page in the Plugins tab +- Report a bug links that open a prefilled issue in the right repo +- `TRUSTED_PROXIES` to choose which proxies nginx takes client IPs from + +### Changed + +- Termix now uses calendar versions (year, month, patch), starting with 26.10.0 +- Redesigned host defaults with admin, user and folder levels +- Simpler host status: online or offline, kept live without a refresh +- Bug reports and feature requests now go to each repo instead of the Support repo + +### Fixed + +- A lot of bugs across the app, the desktop app and every plugin +- Several security issues reported through GitHub advisories + +### Removed + +- Downgrading to 2.8 is no longer supported. A 2.8 install has to start 2.9 once before upgrading +- The donation reminder popup diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9db7959d3..0e97834e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,10 @@ # Contributing +Termix is a small core plus plugins. This repo is the core: hosts, credentials, users, sharing, sessions, the app shell and the plugin runtime. Every other feature lives in its own `Plugin-` repo. If your change is about the terminal, file manager, Docker or any other plugin feature, open it in that plugin's repo instead. + ## Prerequisites -- [Node.js](https://nodejs.org/en/download/) (built with v24) +- [Node.js](https://nodejs.org/en/download/) 22.12 or newer (built with v24) - [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) - [Git](https://git-scm.com/downloads) @@ -19,88 +21,55 @@ ## Running the development server -Run the following commands: - ```sh npm run dev -npm run dev:backend ``` -This will start the backend and the frontend Vite server. You can access Termix by going to `http://localhost:5174/`. +This builds everything, starts the backend and then the Vite server at `http://localhost:5173/`. Backend changes restart the backend, and plugin changes in `../Termix-Plugins` rebuild and reload that plugin. `npm run dev:electron` does the same with the desktop app, and `npm run dev -- --help` lists the other options. -## Contributing +## Useful commands -1. **Fork the repository**: Click the "Fork" button at the top right of - the [repository page](https://github.com/Termix-SSH/Termix). -2. **Create a new branch**: +```sh +npm run test # run the test suite +npm run type-check # type-check the core +npm run lint # lint and the other repo checks +npm run format # format the code with Prettier +``` + +## Making a change + +1. **Fork the repository**: Click "Fork" at the top right of the [repository page](https://github.com/Termix-SSH/Termix). +2. **Create a branch**: ```sh git checkout -b feature/my-new-feature ``` -3. **Make your changes**: Implement your feature, fix, or improvement. +3. **Make your changes** and add or update tests for them. 4. **Commit your changes**: ```sh - git commit -m "Feature request my new feature" + git commit -m "feat: add my new feature" ``` 5. **Push to your fork**: ```sh - git push origin feature/my-feature-request + git push origin feature/my-new-feature ``` -6. **Open a pull request**: Go to the original repository and create a PR with a clear description. +6. **Open a pull request** against the current `dev-*` branch with a clear description. ## Guidelines -- Follow the existing code style. Use Tailwind CSS with shadcn components. -- Use the below color scheme with the respective CSS variable placed in the `className` of a div/component. -- Place all API routes in the `main-axios.ts` file. Updating the `openapi.json` is unneeded. -- Include meaningful commit messages. -- Link related issues when applicable. -- `MobileApp.tsx` renders when the users screen width is less than 768px, otherwise it loads the usual `DesktopApp.tsx`. +- Follow the existing code style. The frontend uses React, Tailwind CSS and shadcn components in `src/ui/`. +- Never hardcode text. Use `t()` and add the English string to `src/ui/locales/en.json`. That's the only locale file you need to update. Other languages are translated automatically, and if an automatic translation is wrong you can fix it in that language's file. +- Keep the UI flat and square. Avoid rounded corners on panels, cards and dialogs. +- Use the theme variables (like `var(--bg-base)` and `var(--text-primary)`) instead of fixed colors. +- There is one component tree for desktop and mobile. Use `src/ui/hooks/use-mobile.ts` for layout changes. +- Core never names a plugin. If core needs something from a plugin, add a slot, action or component id that the plugin fills. +- Tests live in `src/backend/tests/` and `src/ui/tests/`, mirroring the source tree. `npm run test` must pass. +- Every API route gets an OpenAPI JSDoc comment like the routes around it. +- Use short commit messages that start with `feat:`, `fix:` or `chore:`, and link related issues. -## Color Scheme +## Building a plugin -### Background Colors - -| CSS Variable | Color Value | Usage | Description | -| ----------------------------- | ----------- | --------------------------- | ---------------------------------------- | -| `--color-dark-bg` | `#18181b` | Main dark background | Primary dark background color | -| `--color-dark-bg-darker` | `#0e0e10` | Darker backgrounds | Darker variant for panels and containers | -| `--color-dark-bg-darkest` | `#09090b` | Darkest backgrounds | Darkest background (terminal) | -| `--color-dark-bg-light` | `#141416` | Light dark backgrounds | Lighter variant of dark background | -| `--color-dark-bg-very-light` | `#101014` | Very light dark backgrounds | Very light variant of dark background | -| `--color-dark-bg-panel` | `#1b1b1e` | Panel backgrounds | Background for panels and cards | -| `--color-dark-bg-panel-hover` | `#232327` | Panel hover states | Background for panels on hover | - -### Element-Specific Backgrounds - -| CSS Variable | Color Value | Usage | Description | -| ------------------------ | ----------- | ------------------ | --------------------------------------------- | -| `--color-dark-bg-input` | `#222225` | Input fields | Background for input fields and form elements | -| `--color-dark-bg-button` | `#23232a` | Button backgrounds | Background for buttons and clickable elements | -| `--color-dark-bg-active` | `#1d1d1f` | Active states | Background for active/selected elements | -| `--color-dark-bg-header` | `#131316` | Header backgrounds | Background for headers and navigation bars | - -### Border Colors - -| CSS Variable | Color Value | Usage | Description | -| ---------------------------- | ----------- | --------------- | ---------------------------------------- | -| `--color-dark-border` | `#303032` | Default borders | Standard border color | -| `--color-dark-border-active` | `#2d2d30` | Active borders | Border color for active elements | -| `--color-dark-border-hover` | `#434345` | Hover borders | Border color on hover states | -| `--color-dark-border-light` | `#5a5a5d` | Light borders | Lighter border color for subtle elements | -| `--color-dark-border-medium` | `#373739` | Medium borders | Medium weight border color | -| `--color-dark-border-panel` | `#222224` | Panel borders | Border color for panels and cards | - -### Interactive States - -| CSS Variable | Color Value | Usage | Description | -| ------------------------ | ----------- | ----------------- | --------------------------------------------- | -| `--color-dark-hover` | `#2d2d30` | Hover states | Background color for hover effects | -| `--color-dark-active` | `#2a2a2c` | Active states | Background color for active elements | -| `--color-dark-pressed` | `#1a1a1c` | Pressed states | Background color for pressed/clicked elements | -| `--color-dark-hover-alt` | `#2a2a2d` | Alternative hover | Alternative hover state color | +Start from the [plugin template](https://github.com/Termix-SSH/Termix-Plugin-Template). It talks to Termix only through `@termix-ssh/plugin-sdk` and has its own build, test and release setup. ## Support -If you need help or want to request a feature with Termix, visit the [Issues](https://github.com/Termix-SSH/Support/issues) page, log in, and press `New Issue`. -Please be as detailed as possible in your issue, preferably written in English. You can also join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support -channel, however, response times may be longer. +Bugs and ideas for Termix itself go in [this repo](https://github.com/Termix-SSH/Termix/issues/new/choose). Plugin problems go in that plugin's own `Plugin-` repo. Please be as detailed as possible, preferably in English. You can also ask in the [Discord](https://discord.gg/jVQGdvHDrf) server. diff --git a/Casks/termix.rb b/Casks/termix.rb index 17b04a230..e32216bb3 100644 --- a/Casks/termix.rb +++ b/Casks/termix.rb @@ -2,9 +2,9 @@ cask "termix" do version "2.9.2" sha256 "808d876b37ab802c511e964c232619b1ba2949ec39ccd2b1f97e264544739172" - url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg" + url "https://github.com/Termix-SSH/Termix/releases/download/v#{version}/termix_macos_universal_dmg.dmg" name "Termix" - desc "Web-based server management platform with SSH terminal, tunneling, and file editing" + desc "Self-hosted, plugin-based server management" homepage "https://github.com/Termix-SSH/Termix" livecheck do diff --git a/FUNDING.json b/FUNDING.json deleted file mode 100644 index 6ec36892b..000000000 --- a/FUNDING.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "drips": { - "ethereum": { - "ownedBy": "0x67e0C779119D9BcC2187564A66B80a58767d05d1" - } - } -} diff --git a/LICENSE b/LICENSE index 1f0bd1d66..2cc094d20 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,203 @@ Copyright 2026 Luke Gustafson -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - http://www.apache.org/licenses/LICENSE-2.0 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index e8e01fa88..43975fd02 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,7 @@

Termix

-

Self-hosted server management, from SSH and remote desktop to automations

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

+

Self-hosted, plugin-based server management

@@ -35,233 +18,81 @@ Donations this month

-
- -Termix is free and open source. If you find it useful, consider [donating](https://donate.termix.site/) to help cover server costs and development time. - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- Achieved on September 1st, 2025 -

-
## Overview -Termix is a free, open source, self-hosted platform for managing your servers. It puts SSH terminals, remote desktops (RDP, VNC, Telnet), file transfers, tunnels, Docker, metrics, and automations in one place, on web, desktop, and mobile. It is a self-hosted alternative to Termius that stays free forever. +Termix is a free, open source, self-hosted platform for managing your servers, built on plugins. The core is small. It keeps your hosts, credentials, users, sharing and sessions. Everything else, like the SSH terminal, remote desktop, file manager, Docker and metrics, is a plugin you can turn on, turn off or remove. + +It runs on web, desktop and mobile, and it is a self-hosted alternative to Termius that stays free forever.
-## Features +## Try the Demo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**SSH Terminal:** -A full terminal with browser-like tabs and split screen, up to 6 panels at once. Pick your theme, font, and colors. A toolbar sits above each session with live CPU, memory, and disk, plus quick links to that host's files, Docker, tunnels, and metrics. - - - -**Remote Desktop:** -RDP, VNC, and Telnet in the browser, in tabs and split screen like any other session. Includes a file browser for RDP drives and drag-and-drop upload. On Windows desktop you can also open a host in the native RDP client. - -
- -**SSH Tunnels:** -Local, remote, and dynamic SOCKS forwarding with auto reconnect and health checks. Client-to-server tunnels on the desktop app are stored on that machine, and you can save presets to the server to move a setup to another client. - - - -**File Manager:** -Browse, edit, upload, download, rename, move, and delete files over SFTP, with sudo support. View and edit code, images, audio, and video. Copy files straight from one server to another in a dedicated transfer tab, with the fastest route picked for you and transfers checked for integrity. The desktop app also has a side-by-side local and remote view. - -
- -**Docker and Podman:** -Start, stop, pause, and remove containers, watch their stats, and open a shell inside one. Works with both Docker and Podman. It is not meant to replace Portainer or Dockge, just to manage containers you already have. - - - -**Host Manager:** -Save and organize hosts with tags and nested folders you can name and color. Reuse saved credentials across hosts, deploy SSH keys automatically, group hosts under a parent host, bulk edit and export, and use Quick Connect for one-off connections you do not want to save. - -
- -**Host Metrics:** -CPU, memory, disk, network, temperature, NVIDIA GPU, uptime, processes, ports, logins, and system info on most Linux servers, with history graphs. Manager cards let you handle services, cron jobs, packages, users, firewall rules, WireGuard, Tailscale, SSL certs, logs, and health checks without leaving Termix. - - - -**Automations:** -Pick a trigger, then say what should happen. Triggers include a metric crossing a threshold, a host going up or down, a health check changing, a schedule, a container event, or an incoming webhook. Steps can run commands and snippets, control containers and tunnels, wake a host, call a URL, wait, branch on a condition, run another automation, and notify you over ntfy, Discord, or a webhook. Test runs let you try it safely first. - -
- -**Fleets:** -Group hosts into a fleet by picking them or with tag rules, so new hosts join on their own. Run one command on every host at once, push and pull files across all of them, install packages, and collect an inventory of OS, kernel, arch, and uptime. - - - -**AI Assistant:** -Optional, and off until you turn it on. Connect OpenAI, Anthropic, Gemini, Ollama, or any OpenAI compatible endpoint and ask about your setup. It reads hosts, fleets, snippets, and alerts, and proposes changes for you to approve instead of making them. It can never touch credentials, users, or settings. Admins can leave it off for the whole instance, and you can hide it during setup. - -
- -**Login and Users:** -Local accounts plus OIDC, LDAP, GitHub, and Google sign-in, with 2FA (TOTP), passkeys (WebAuthn), and trusted devices. Admins can manage users, map OIDC groups to roles, see every active session across platforms, and revoke them. Link your local and OIDC accounts together, and read the audit log of what everyone did. - - - -**Roles and Sharing:** -Create roles and share hosts with users or roles at four levels: connect, view, edit, and manage. Works with every auth type and every protocol, and you can override the credentials used for a shared host. - -
- -**Alerts:** -Set rules on host metrics like CPU, memory, and disk, and get notified over ntfy, Discord, or a webhook when they fire. See firing and resolved alerts in a history log, and dismiss the ones you do not care about. - - - -**Homepage:** -A drag-and-drop widget grid you build yourself. Widgets for host status, pings, service links, bookmarks, search, clocks, calendars, countdowns, notes, RSS, weather, images, iframes, Docker, tunnels, metrics charts, custom APIs, and even a live terminal. - -
- -**Snippets and Tools:** -Save commands you run often and fire them off in one click, with variables for the host and your own inputs. Run a single command across every open terminal, and search your command history with autocomplete. - - - -**Session Sharing:** -Share a live terminal, RDP, VNC, or Telnet session in real time. Send a link anyone can join without an account, or share with a specific Termix user, in read-only or read-write mode. Shares can expire on their own or be revoked, and can be turned off globally or per host. - -
- -**Session Recording and Logs:** -Record terminal, RDP, and VNC sessions and play them back later. Download plain text logs of a session, and check the connection log to see exactly what happened during a connection. - - - -**Serial Connections:** -Talk to serial devices like routers, switches, and microcontrollers from the browser or desktop app. Set baud rate, data bits, stop bits, and parity. Uses the Web Serial API in supported browsers, or a native backend in the desktop app. - -
- -**Tailscale:** -Pull devices from your tailnet to add them as hosts in a couple of clicks, and connect with Tailscale SSH so your tailnet ACLs handle access and no credentials are stored. Headscale and custom endpoints work too. - - - -**Proxmox:** -Import hosts straight from a Proxmox instance, and watch node and guest stats, including CPU, memory, and storage, in their own tab. - -
- -**Workspaces and Tabs:** -Save a set of tabs with their split layout and reopen the whole thing in one click. Termix also remembers your last session, so your tabs come back across refreshes and devices. - - - -**Command Line Interface:** -A `termix` CLI for your shell and your scripts. Open terminals, run a command on one host or a whole fleet, move files over SFTP, and manage hosts, snippets, and credentials. Install with `npm install -g @termix-ssh/cli` or grab a standalone binary. See the [CLI docs](https://docs.termix.site/cli). - -
- -**Security:** -Passwords, keys, and other secrets are encrypted per user, and the database files themselves can be encrypted on disk. See the [docs](https://docs.termix.site/features/authentication/security/) for how it works. - - - -**Languages:** -Around 30 languages built in, managed through [Crowdin](https://docs.termix.site/translations). - -
+Want to see it before you install it? Try the demo at [demo.termix.site](https://demo.termix.site/). Any username and password works.
-
-More features +## Core + +What every Termix install has, with or without plugins: + +- **Hosts:** Save hosts in nested folders with tags and parent hosts, bulk edit, import and export, and use Quick Connect for one-off connections +- **Credentials:** Reuse saved passwords and keys across hosts, with host defaults you can set for the server, a user or a folder +- **Sharing and Roles:** Share hosts, credentials and folders with users or roles at four levels: connect, view, edit and manage +- **Accounts:** Local accounts, sessions you can see and revoke, trusted devices, API keys and an audit log +- **Security:** Passwords, keys and other secrets are encrypted per user, and the database can be encrypted on disk +- **Desktop Sync:** The desktop app runs on its own and can link to a Termix server to keep an offline copy of your account +- **Interface:** Tabs and split screen, a command palette (double tap left shift), rebindable shortcuts, themes, branding and around 30 languages +- **Databases:** SQLite by default, with PostgreSQL and MySQL supported too +
-- **Dashboard** - Your servers at a glance, with cards you arrange yourself -- **Network Graph** - See your homelab drawn out from your hosts, with live status -- **Tmux Monitor** - Browse tmux sessions, windows, and panes, with previews and search -- **API Keys** - User-scoped keys with expiry dates for scripts and CI -- **Export and Import** - Move hosts, credentials, and file manager data in and out -- **Automatic SSL** - Certificates generated and renewed for you, with HTTPS redirects, or bring your own -- **Databases** - SQLite by default, with PostgreSQL and MySQL supported too -- **Modern UI** - Clean React interface that works on desktop and mobile, with themes like light, dark, and Dracula. Any connection can open full screen from a URL -- **Command Palette** - Double tap left shift to jump to a host from the keyboard -- **Keyboard Shortcuts** - Move between tabs, close tabs, and more, all rebindable -- **Wake-on-LAN** - Wake a machine from Termix or from an automation step -- **Trusted Proxy Auth** - Let a reverse proxy handle sign-in and pass the user through -- **White Label** - Admins can rebrand the instance with their own name, logo, and colors -- **Web Endpoints** - Open a host's own web UI, like a router admin page, embedded inside Termix instead of a separate tab -- **Collaboration Rooms** - Persistent rooms where a group can jump between sessions together, with a presenter stage and invites -- **SSH Feature Rich** - Jump hosts, Warpgate, TOTP prompts, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, terminal logging, agent forwarding, Bitwarden SSH agent, HashiCorp Vault SSH signing, Step CA, 1Password Connect, and more -- **Termix ID** - A built-in take on sshid.io. Claim a handle, publish your public keys at a resolver URL, and issue SSH certificates from the built-in CA -- **Command History Autosuggestions** - Inline suggestions in the terminal as you type, based on your command history -- **Desktop Standalone and Sync** - The desktop app runs on its own with a local backend and database, and can optionally sync with a Termix server +## Plugins -
+Plugins add everything else. The official ones ship with Termix, and admins can install, update or remove plugins from the Plugins tab. Each plugin has its own repo. + +| Plugin | What it does | +| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| [ACME Certificates](https://github.com/Termix-SSH/Plugin-ACME-SSL) | Gets the Termix HTTPS certificate from Let's Encrypt or any ACME provider and renews it for you. | +| [AI Assistant](https://github.com/Termix-SSH/Plugin-AI) | An AI assistant that reads your setup and proposes changes for you to approve. | +| [Alerts](https://github.com/Termix-SSH/Plugin-Alerts) | One inbox for every alert in Termix, with delivery to webhooks, ntfy, Discord and email. | +| [Automations](https://github.com/Termix-SSH/Plugin-Automations) | Run steps on your hosts on a schedule or when something happens. | +| [Docker](https://github.com/Termix-SSH/Plugin-Docker) | Manage Docker and Podman containers on your hosts over SSH, with logs, stats and a console. | +| [File Manager](https://github.com/Termix-SSH/Plugin-File-Manager) | Browse, edit and move files over SFTP, including straight from one server to another. | +| [Fleets](https://github.com/Termix-SSH/Plugin-Fleets) | Group hosts into fleets and run commands, package actions and file transfers on all of them at once. | +| [HashiCorp Vault](https://github.com/Termix-SSH/Plugin-Vault) | Connect to hosts with short-lived SSH certificates signed by HashiCorp Vault. | +| [Homepage](https://github.com/Termix-SSH/Plugin-Homepage) | A drag and drop widget page for your servers and services, plus service links for the dashboard. | +| [Host Metrics](https://github.com/Termix-SSH/Plugin-Host-Metrics) | Live CPU, memory, disk, network and GPU stats, plus tools to manage your hosts. | +| [LDAP](https://github.com/Termix-SSH/Plugin-LDAP) | Sign in with your LDAP or Active Directory username and password. | +| [Network Topology](https://github.com/Termix-SSH/Plugin-Network-Topology) | Draws your hosts and the links between them as a live, interactive graph. | +| [OPKSSH](https://github.com/Termix-SSH/Plugin-OPKSSH) | Connect to hosts with short-lived OpenPubkey SSH certificates after signing in with your identity provider. | +| [Passkeys](https://github.com/Termix-SSH/Plugin-WebAuthn) | Sign in with a passkey or security key instead of a password. | +| [Proxmox](https://github.com/Termix-SSH/Plugin-Proxmox) | Import Proxmox VE guests as hosts, keep them in sync and watch node stats. | +| [Remote Desktop](https://github.com/Termix-SSH/Plugin-Remote-Desktop) | RDP, VNC and Telnet sessions in your browser, with clipboard, file transfer, sharing and recording. | +| [Secret Sources](https://github.com/Termix-SSH/Plugin-Secret-Sources) | Pull host passwords and keys from 1Password when you connect instead of storing them in Termix. | +| [Serial](https://github.com/Termix-SSH/Plugin-Serial) | Open a serial console to a router, switch or other device plugged into your computer. | +| [Session Recording](https://github.com/Termix-SSH/Plugin-Session-Recording) | Record terminal and remote desktop sessions and play them back or download them later. | +| [Session Sharing](https://github.com/Termix-SSH/Plugin-Session-Sharing) | Share live sessions by link or with another user, and present sessions to a group in meeting rooms. | +| [Single sign-on](https://github.com/Termix-SSH/Plugin-SSO) | Sign in with any OpenID Connect provider, GitHub or Google. | +| [Snippets](https://github.com/Termix-SSH/Plugin-Snippets) | Save the commands you run often and run them in a terminal or on many hosts in one click. | +| [SSH Terminal](https://github.com/Termix-SSH/Plugin-SSH-Terminal) | SSH terminal sessions with tabs, split screen, history, macros and a local terminal in the desktop app. | +| [Step CA](https://github.com/Termix-SSH/Plugin-Step-CA) | Connect to hosts with short-lived SSH certificates from a smallstep step-ca server. | +| [Tailscale](https://github.com/Termix-SSH/Plugin-Tailscale) | Add tailnet devices as hosts and connect with Tailscale SSH. | +| [Termix Identity](https://github.com/Termix-SSH/Plugin-Termix-Identity) | Publish your SSH public keys under a public handle and run your own SSH certificate authority. | +| [Tmux Monitor](https://github.com/Termix-SSH/Plugin-Tmux-Monitor) | Browse and control tmux sessions, windows and panes across your hosts. | +| [TOTP](https://github.com/Termix-SSH/Plugin-TOTP) | Two-factor sign in with a code from an authenticator app, with backup codes. | +| [Tunnels](https://github.com/Termix-SSH/Plugin-Tunnels) | SSH port forwarding that reconnects on its own, plus client tunnels from the desktop app. | +| [Usage Statistics](https://github.com/Termix-SSH/Plugin-Telemetry) | Sends a small anonymous daily report so we can see how Termix is used. | +| [Wake-on-LAN](https://github.com/Termix-SSH/Plugin-Wake-On-LAN) | Wake sleeping machines by sending a Wake-on-LAN packet to their MAC address. | +| [Warpgate](https://github.com/Termix-SSH/Plugin-Warpgate) | Connect to hosts through a Warpgate SSH bastion. | +| [Web Endpoint](https://github.com/Termix-SSH/Plugin-Web-Endpoint) | Open a host's web UI inside Termix, directly or over an SSH tunnel. | +| [Workspaces](https://github.com/Termix-SSH/Plugin-Workspaces) | Save your open tabs and split layouts and reopen them in one click. | + +Want to build your own? Start from the [plugin template](https://github.com/Termix-SSH/Termix-Plugin-Template). Plugins you can install are listed in the [registry](https://github.com/Termix-SSH/Termix-Registry). Its community registry is open for submissions now, and Termix will be able to install community plugins in a later update. See [submitting a plugin](https://docs.termix.site/develop/community-registry).
@@ -278,14 +109,14 @@ Around 30 languages built in, managed through [Crowdin](https://docs.termix.site Windows x64/ia32 -Portable · MSI Installer · Chocolatey +Portable · EXE and MSI Installer · Chocolatey -Linux x64/ia32 +Linux x64/arm64/armv7l Portable · AUR · AppImage · Deb · Flatpak -macOS x64/ia32, v12.0+ +macOS Universal/x64/arm64, v12.0+ Apple App Store · DMG · Homebrew @@ -302,18 +133,14 @@ Around 30 languages built in, managed through [Crowdin](https://docs.termix.site ## Installation -Visit the [Termix Docs](https://docs.termix.site/install) for full installation instructions across all platforms. +Visit the [Termix Docs](https://docs.termix.site/install) for full install steps on every platform. -Deploying to Kubernetes? The Helm chart is in `charts/termix`, and setup instructions -covering Ingress, Traefik, Argo CD, GitHub Actions, and GitLab CI are at -[docs.termix.site/install/server/kubernetes](https://docs.termix.site/install/server/kubernetes). - -Sample Docker Compose file (you can omit `guacd` and the network if you don't plan on using remote desktop features): +Sample Docker Compose file (you can leave out `guacd` and the network if you don't plan to use the Remote Desktop plugin): ```yaml services: termix: - image: ghcr.io/lukegus/termix:latest + image: ghcr.io/termix-ssh/termix:latest container_name: termix restart: unless-stopped ports: @@ -322,6 +149,10 @@ services: - termix-data:/app/data environment: PORT: "8080" + GUACD_HOST: "guacd" + GUACD_TUNNEL_HOST: "termix" + GUACD_RECORDING_PATH: "/termix-data/session_recordings/guacamole" + GUACD_DRIVE_PATH: "/termix-data/rdp-drive" depends_on: - guacd networks: @@ -331,8 +162,8 @@ services: image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped - ports: - - "4822:4822" + volumes: + - termix-data:/termix-data networks: - termix-net @@ -345,37 +176,17 @@ networks: driver: bridge ``` -### Command Line Interface - -Termix also has a CLI, so you can manage your servers from a terminal and use Termix in your own scripts. - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -It can open terminals, run a command on one host or a whole fleet, move files over SFTP, and manage hosts, snippets and credentials. Full documentation is at [docs.termix.site/cli](https://docs.termix.site/cli). - ### Cloud Hosting -You can run the Termix server on a VPS instead of inside your own network. If Termix runs on the network it manages, an outage takes Termix down with it, right when you need it to fix things. Running it elsewhere keeps it reachable, gives you a static IP, and lets you get in from anywhere without a VPN or port forward. +You can run Termix on a VPS instead of inside your own network. If Termix runs on the network it manages, an outage takes Termix down with it, right when you need it to fix things. Running it somewhere else keeps it reachable, gives you a static IP and lets you get in from anywhere without a VPN or port forward. -[GINERNET](https://docs.termix.site/install/ginernet) sponsors Termix, and the docs have a step by step guide for deploying to their VPS platform. - -
- -## Telemetry - -Termix sends a small anonymous report once a day so I can see how many instances are running and which features get used. It contains a random instance ID, the app version, and how many users and hosts you have. It can also include your platform (OS, architecture, Node.js version, database type, and whether you run Docker, the desktop app or a plain server), how often each kind of tab was opened and how many SSH logins happened, and which built-in features are running. It never contains usernames, hostnames, IP addresses, credentials, or anything else that identifies you or your servers. - -It is on by default. Turn it off, or choose what it includes, in Admin Settings under Usage Statistics, where you can also preview the exact report. Each user can leave out their own feature usage in their profile. Setting `ENABLE_TELEMETRY=false` turns it off and locks the switch. +[GINERNET](https://docs.termix.site/install/ginernet) sponsors Termix, and the docs have a step by step guide for their VPS platform.
## Donate -Termix is free and open source with no subscriptions or paid plans. If you find it useful, consider donating to help cover server costs, domains, and development time. Donations also help fund the time to research and learn what's needed to build features like SAML, Kubernetes, and Agent support. Track progress and donate below. +Termix is free and open source with no subscriptions or paid plans. If you find it useful, consider donating to help cover server costs, domains and development time. [Donate](https://donate.termix.site/) @@ -435,70 +246,16 @@ Interested in a paid placement to support development? Email [mail@termix.site]( ## Support -Need help or want to request a feature? Open a [new issue](https://github.com/Termix-SSH/Support/issues) and add as much detail as you can, in English if possible. You can also ask in the support channel on [Discord](https://discord.gg/jVQGdvHDrf), though replies there can take longer. +Bugs and ideas for Termix itself (login, hosts, credentials, sharing, sync, the app and desktop app) go in this repo: [report a bug](https://github.com/Termix-SSH/Termix/issues/new?template=bug_report.yml) or [request a feature](https://github.com/Termix-SSH/Termix/issues/new?template=feature_request.yml). -
+Every plugin has its own repo, so a problem with the terminal, file manager, Docker, remote desktop or any other plugin goes in that plugin's repo (see the table above, or use Report an issue on the plugin's page in the app, which fills in your versions for you). Not sure where it goes? Open it here and it will be moved. -## Screenshots +You need to be logged in to GitHub. Please be as detailed as possible, preferably in English. -
- -
- -[![YouTube](./docs/repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -Watch update overviews on YouTube - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -Some videos and images may be out of date or may not perfectly showcase features. - -
- -
- -## Planned Features - -See [Projects](https://github.com/orgs/Termix-SSH/projects/5) for all planned features. If you are looking to contribute, see [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). +For discussions and questions, join the [Discord](https://discord.gg/jVQGdvHDrf) server. Update videos are on [YouTube](https://www.youtube.com/@TermixSSH/videos).
## License -Distributed under the Apache License Version 2.0. See `LICENSE` for more information. +Distributed under the Apache License Version 2.0. See [LICENSE](LICENSE) for more information. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md deleted file mode 100644 index 24476a7a3..000000000 --- a/RELEASE_NOTES.md +++ /dev/null @@ -1,26 +0,0 @@ - - -An urgent patch for 2.9.1 that fixes the server writing huge amounts of data to disk, which could add up to 100 GB every few hours. Update as soon as you can. - - - - - -https://youtu.be/lngaePO96tM - - - - - -- Background database saves now happen at most once every 30 seconds, so disk writes no longer grow with the number of hosts -- Plugin database and storage reads are now logged to the audit log once per start instead of on every read -- On first start, 2.9.2 clears the plugin read entries that 2.9.1 piled up in the audit log - - - - - -- The server rewriting its whole database to disk many times a second, causing very high disk writes (up to 100 GB every few hours) on 2.9.1 -- The audit log filling up with plugin entries and pushing out real ones - - diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index dcb11d2a1..000000000 --- a/SECURITY.md +++ /dev/null @@ -1,23 +0,0 @@ -# Security Policy - -## Reporting a Vulnerability - -Please report any vulnerabilities to [GitHub Security](https://github.com/Termix-SSH/Termix/security/advisories). - -## External secret storage - -By default, a single-container installation generates its keys in the Termix -data directory for ease of recovery. Production deployments that keep backups -or database files outside a trusted encrypted volume should set -`TERMIX_REQUIRE_EXTERNAL_SECRETS=true` and provide all four keys through a -secret manager: - -- `JWT_SECRET` (at least 64 characters) -- `DATABASE_KEY` (64 hexadecimal characters) -- `ENCRYPTION_KEY` (64 hexadecimal characters) -- `INTERNAL_AUTH_TOKEN` (at least 32 characters) - -Each value can instead be mounted as a Docker or Kubernetes secret and supplied -with its corresponding `_FILE` variable, such as `ENCRYPTION_KEY_FILE`. -Hardened mode fails closed instead of writing a replacement key beside the -encrypted database. diff --git a/charts/termix/.helmignore b/charts/termix/.helmignore deleted file mode 100644 index e4a6797e2..000000000 --- a/charts/termix/.helmignore +++ /dev/null @@ -1,6 +0,0 @@ -.git/ -.github/ -node_modules/ -dist/ -coverage/ -*.tgz diff --git a/charts/termix/Chart.yaml b/charts/termix/Chart.yaml deleted file mode 100644 index 5fd44049a..000000000 --- a/charts/termix/Chart.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v2 -name: termix -description: Self-hosted SSH and remote desktop management for Kubernetes. -type: application -version: 0.1.0 -appVersion: 2.7.0 -home: https://github.com/Termix-SSH/Termix -sources: - - https://github.com/Termix-SSH/Termix -maintainers: - - name: Termix maintainers -keywords: - - ssh - - remote-desktop - - guacamole - - terminal diff --git a/charts/termix/templates/NOTES.txt b/charts/termix/templates/NOTES.txt deleted file mode 100644 index c2eea984e..000000000 --- a/charts/termix/templates/NOTES.txt +++ /dev/null @@ -1,17 +0,0 @@ -Termix has been installed. - -Service: - {{ include "termix.fullname" . }}:{{ .Values.service.port }} - -{{- if .Values.ingress.enabled }} -Ingress hosts: -{{- range .Values.ingress.hosts }} - - {{ .host }} -{{- end }} -{{- else if .Values.traefik.ingressRoute.enabled }} -Traefik IngressRoute: - https://{{ .Values.traefik.ingressRoute.host }}{{ .Values.traefik.ingressRoute.pathPrefix }} -{{- else }} -Port-forward for local testing: - kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "termix.fullname" . }} 8080:{{ .Values.service.port }} -{{- end }} diff --git a/charts/termix/templates/_helpers.tpl b/charts/termix/templates/_helpers.tpl deleted file mode 100644 index 65b38907b..000000000 --- a/charts/termix/templates/_helpers.tpl +++ /dev/null @@ -1,45 +0,0 @@ -{{- define "termix.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{- define "termix.fullname" -}} -{{- if .Values.fullnameOverride -}} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- if contains $name .Release.Name -}} -{{- .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} -{{- end -}} - -{{- define "termix.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{- define "termix.labels" -}} -helm.sh/chart: {{ include "termix.chart" . }} -app.kubernetes.io/name: {{ include "termix.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end -}} - -{{- define "termix.selectorLabels" -}} -app.kubernetes.io/name: {{ include "termix.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end -}} - -{{- define "termix.serviceAccountName" -}} -{{- if .Values.serviceAccount.create -}} -{{- default (include "termix.fullname" .) .Values.serviceAccount.name -}} -{{- else -}} -{{- default "default" .Values.serviceAccount.name -}} -{{- end -}} -{{- end -}} - -{{- define "termix.secretName" -}} -{{- default (printf "%s-secret" (include "termix.fullname" .)) .Values.secrets.name -}} -{{- end -}} diff --git a/charts/termix/templates/deployment.yaml b/charts/termix/templates/deployment.yaml deleted file mode 100644 index 52487425d..000000000 --- a/charts/termix/templates/deployment.yaml +++ /dev/null @@ -1,148 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "termix.fullname" . }} - labels: - {{- include "termix.labels" . | nindent 4 }} -spec: - {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} - {{- end }} - strategy: - {{- toYaml .Values.strategy | nindent 4 }} - selector: - matchLabels: - {{- include "termix.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - {{- include "termix.selectorLabels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - serviceAccountName: {{ include "termix.serviceAccountName" . }} - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: termix - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: http - containerPort: {{ .Values.service.port }} - protocol: TCP - env: - {{- range $key, $value := .Values.env }} - - name: {{ $key }} - value: {{ $value | quote }} - {{- end }} - {{- if .Values.guacd.enabled }} - - name: GUACD_HOST - value: "127.0.0.1" - {{- else if .Values.guacd.host }} - - name: GUACD_HOST - value: {{ .Values.guacd.host | quote }} - {{- end }} - - name: GUACD_TUNNEL_HOST - value: {{ include "termix.fullname" . | quote }} - {{- if ne .Values.database.dialect "sqlite" }} - - name: DATABASE_DIALECT - value: {{ .Values.database.dialect | quote }} - {{- end }} - {{- if .Values.database.existingSecret.name }} - - name: DATABASE_URL - valueFrom: - secretKeyRef: - name: {{ .Values.database.existingSecret.name }} - key: {{ .Values.database.existingSecret.urlKey }} - {{- end }} - {{- if .Values.secrets.create }} - {{- range $key, $value := .Values.secrets.data }} - {{- if $value }} - - name: {{ $key }} - valueFrom: - secretKeyRef: - name: {{ include "termix.secretName" $ }} - key: {{ $key }} - {{- end }} - {{- end }} - {{- end }} - {{- with .Values.extraEnv }} - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.extraEnvFrom }} - envFrom: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- if .Values.probes.liveness.enabled }} - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} - periodSeconds: {{ .Values.probes.liveness.periodSeconds }} - timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }} - failureThreshold: {{ .Values.probes.liveness.failureThreshold }} - {{- end }} - {{- if .Values.probes.readiness.enabled }} - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} - periodSeconds: {{ .Values.probes.readiness.periodSeconds }} - timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }} - failureThreshold: {{ .Values.probes.readiness.failureThreshold }} - {{- end }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - volumeMounts: - - name: data - mountPath: /app/data - {{- if .Values.guacd.enabled }} - - name: guacd - image: "{{ .Values.guacd.image.repository }}:{{ .Values.guacd.image.tag }}" - imagePullPolicy: {{ .Values.guacd.image.pullPolicy }} - securityContext: - {{- toYaml .Values.guacd.securityContext | nindent 12 }} - ports: - - name: guacd - containerPort: {{ .Values.guacd.service.port }} - protocol: TCP - resources: - {{- toYaml .Values.guacd.resources | nindent 12 }} - volumeMounts: - - name: data - mountPath: /termix-data - {{- end }} - volumes: - - name: data - {{- if .Values.persistence.enabled }} - persistentVolumeClaim: - claimName: {{ default (printf "%s-data" (include "termix.fullname" .)) .Values.persistence.existingClaim }} - {{- else }} - emptyDir: {} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/charts/termix/templates/hpa.yaml b/charts/termix/templates/hpa.yaml deleted file mode 100644 index 016a9e87c..000000000 --- a/charts/termix/templates/hpa.yaml +++ /dev/null @@ -1,22 +0,0 @@ -{{- if .Values.autoscaling.enabled -}} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "termix.fullname" . }} - labels: - {{- include "termix.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "termix.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} -{{- end }} diff --git a/charts/termix/templates/ingress.yaml b/charts/termix/templates/ingress.yaml deleted file mode 100644 index 6c6b1fbe8..000000000 --- a/charts/termix/templates/ingress.yaml +++ /dev/null @@ -1,35 +0,0 @@ -{{- if .Values.ingress.enabled -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ include "termix.fullname" . }} - labels: - {{- include "termix.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if .Values.ingress.className }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- toYaml .Values.ingress.tls | nindent 4 }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - pathType: {{ .pathType }} - backend: - service: - name: {{ include "termix.fullname" $ }} - port: - name: http - {{- end }} - {{- end }} -{{- end }} diff --git a/charts/termix/templates/networkpolicy.yaml b/charts/termix/templates/networkpolicy.yaml deleted file mode 100644 index c0e508be5..000000000 --- a/charts/termix/templates/networkpolicy.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if .Values.networkPolicy.enabled -}} -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: {{ include "termix.fullname" . }} - labels: - {{- include "termix.labels" . | nindent 4 }} -spec: - podSelector: - matchLabels: - {{- include "termix.selectorLabels" . | nindent 6 }} - policyTypes: - - Ingress - - Egress - ingress: - {{- toYaml .Values.networkPolicy.ingress | nindent 4 }} - egress: - {{- toYaml .Values.networkPolicy.egress | nindent 4 }} -{{- end }} diff --git a/charts/termix/templates/pdb.yaml b/charts/termix/templates/pdb.yaml deleted file mode 100644 index fc22ede1c..000000000 --- a/charts/termix/templates/pdb.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.podDisruptionBudget.enabled -}} -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: {{ include "termix.fullname" . }} - labels: - {{- include "termix.labels" . | nindent 4 }} -spec: - minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} - selector: - matchLabels: - {{- include "termix.selectorLabels" . | nindent 6 }} -{{- end }} diff --git a/charts/termix/templates/pvc.yaml b/charts/termix/templates/pvc.yaml deleted file mode 100644 index ac7a11a48..000000000 --- a/charts/termix/templates/pvc.yaml +++ /dev/null @@ -1,21 +0,0 @@ -{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) -}} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ include "termix.fullname" . }}-data - labels: - {{- include "termix.labels" . | nindent 4 }} - {{- with .Values.persistence.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - accessModes: - {{- toYaml .Values.persistence.accessModes | nindent 4 }} - resources: - requests: - storage: {{ .Values.persistence.size | quote }} - {{- if .Values.persistence.storageClass }} - storageClassName: {{ .Values.persistence.storageClass | quote }} - {{- end }} -{{- end }} diff --git a/charts/termix/templates/secret.yaml b/charts/termix/templates/secret.yaml deleted file mode 100644 index 278119ee9..000000000 --- a/charts/termix/templates/secret.yaml +++ /dev/null @@ -1,15 +0,0 @@ -{{- if .Values.secrets.create -}} -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "termix.secretName" . }} - labels: - {{- include "termix.labels" . | nindent 4 }} -type: Opaque -stringData: - {{- range $key, $value := .Values.secrets.data }} - {{- if $value }} - {{ $key }}: {{ $value | quote }} - {{- end }} - {{- end }} -{{- end }} diff --git a/charts/termix/templates/service.yaml b/charts/termix/templates/service.yaml deleted file mode 100644 index c2d2f4dba..000000000 --- a/charts/termix/templates/service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "termix.fullname" . }} - labels: - {{- include "termix.labels" . | nindent 4 }} - {{- with .Values.service.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "termix.selectorLabels" . | nindent 4 }} diff --git a/charts/termix/templates/serviceaccount.yaml b/charts/termix/templates/serviceaccount.yaml deleted file mode 100644 index 365c382b3..000000000 --- a/charts/termix/templates/serviceaccount.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "termix.serviceAccountName" . }} - labels: - {{- include "termix.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automount }} -{{- end }} diff --git a/charts/termix/templates/traefik-ingressroute.yaml b/charts/termix/templates/traefik-ingressroute.yaml deleted file mode 100644 index 4d5f29a7f..000000000 --- a/charts/termix/templates/traefik-ingressroute.yaml +++ /dev/null @@ -1,34 +0,0 @@ -{{- if .Values.traefik.ingressRoute.enabled -}} -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: {{ include "termix.fullname" . }} - labels: - {{- include "termix.labels" . | nindent 4 }} - {{- with .Values.traefik.ingressRoute.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - entryPoints: - {{- toYaml .Values.traefik.ingressRoute.entryPoints | nindent 4 }} - routes: - - match: Host(`{{ .Values.traefik.ingressRoute.host }}`) && PathPrefix(`{{ .Values.traefik.ingressRoute.pathPrefix }}`) - kind: Rule - services: - - name: {{ include "termix.fullname" . }} - port: {{ .Values.service.port }} - {{- with .Values.traefik.ingressRoute.middlewares }} - middlewares: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- if .Values.traefik.ingressRoute.tls.enabled }} - tls: - {{- if .Values.traefik.ingressRoute.tls.secretName }} - secretName: {{ .Values.traefik.ingressRoute.tls.secretName }} - {{- end }} - {{- if .Values.traefik.ingressRoute.tls.certResolver }} - certResolver: {{ .Values.traefik.ingressRoute.tls.certResolver }} - {{- end }} - {{- end }} -{{- end }} diff --git a/charts/termix/templates/validate.yaml b/charts/termix/templates/validate.yaml deleted file mode 100644 index 2faf999a4..000000000 --- a/charts/termix/templates/validate.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if and .Values.ingress.enabled .Values.traefik.ingressRoute.enabled -}} -{{- fail "ingress and traefik.ingressRoute cannot both be enabled" -}} -{{- end -}} -{{- if and (eq .Values.database.dialect "sqlite") (or (gt (int .Values.replicaCount) 1) .Values.autoscaling.enabled) -}} -{{- fail "SQLite supports only one replica; use Postgres or MySQL before enabling replicas or autoscaling" -}} -{{- end -}} -{{- if and (not .Values.guacd.enabled) (not .Values.guacd.host) -}} -{{- fail "guacd.host is required when the bundled guacd sidecar is disabled" -}} -{{- end -}} -{{- if and .Values.database.existingSecret.name .Values.secrets.create .Values.secrets.data.DATABASE_URL -}} -{{- fail "DATABASE_URL must come from either database.existingSecret or secrets.data, not both" -}} -{{- end -}} diff --git a/charts/termix/values-gitops-example.yaml b/charts/termix/values-gitops-example.yaml deleted file mode 100644 index 9f3d769ba..000000000 --- a/charts/termix/values-gitops-example.yaml +++ /dev/null @@ -1,28 +0,0 @@ -image: - repository: ghcr.io/termix-ssh/termix - tag: "2.7.0" - -ingress: - enabled: true - className: nginx - hosts: - - host: termix.example.com - paths: - - path: / - pathType: Prefix - tls: - - secretName: termix-tls - hosts: - - termix.example.com - -persistence: - enabled: true - size: 20Gi - -secrets: - create: false - -database: - dialect: sqlite - -extraEnvFrom: [] diff --git a/charts/termix/values-traefik.yaml b/charts/termix/values-traefik.yaml deleted file mode 100644 index d4b799428..000000000 --- a/charts/termix/values-traefik.yaml +++ /dev/null @@ -1,32 +0,0 @@ -ingress: - enabled: false - -traefik: - ingressRoute: - enabled: true - entryPoints: - - websecure - host: termix.example.com - pathPrefix: / - tls: - enabled: true - certResolver: letsencrypt - -persistence: - enabled: true - size: 20Gi - -resources: - requests: - cpu: 250m - memory: 512Mi - limits: - memory: 2Gi - -guacd: - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - memory: 512Mi diff --git a/charts/termix/values.yaml b/charts/termix/values.yaml deleted file mode 100644 index 55cf77613..000000000 --- a/charts/termix/values.yaml +++ /dev/null @@ -1,151 +0,0 @@ -replicaCount: 1 - -image: - repository: ghcr.io/termix-ssh/termix - pullPolicy: IfNotPresent - tag: "" - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - create: true - automount: false - annotations: {} - name: "" - -podAnnotations: {} -podLabels: {} - -podSecurityContext: - fsGroup: 1000 - fsGroupChangePolicy: OnRootMismatch - -securityContext: {} - -service: - type: ClusterIP - port: 8080 - annotations: {} - -ingress: - enabled: false - className: "" - annotations: {} - hosts: - - host: termix.example.com - paths: - - path: / - pathType: Prefix - tls: [] - -traefik: - ingressRoute: - enabled: false - entryPoints: - - websecure - host: termix.example.com - pathPrefix: / - tls: - enabled: true - secretName: "" - certResolver: "" - middlewares: [] - annotations: {} - -resources: {} - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 3 - targetCPUUtilizationPercentage: 80 - -persistence: - enabled: true - accessModes: - - ReadWriteOnce - size: 10Gi - storageClass: "" - annotations: {} - existingClaim: "" - -env: - PORT: "8080" - DATA_DIR: /app/data - NODE_ENV: production - TERMIX_REQUIRE_EXTERNAL_SECRETS: "false" - GUACD_RECORDING_PATH: /termix-data/session_recordings/guacamole - GUACD_RECORDING_BACKEND_PATH: /app/data/session_recordings/guacamole - -extraEnv: [] -extraEnvFrom: [] - -secrets: - create: false - name: "" - data: - JWT_SECRET: "" - DATABASE_KEY: "" - ENCRYPTION_KEY: "" - INTERNAL_AUTH_TOKEN: "" - DATABASE_URL: "" - GUACAMOLE_ENCRYPTION_KEY: "" - -database: - dialect: sqlite - existingSecret: - name: "" - urlKey: DATABASE_URL - -guacd: - enabled: true - host: "" - image: - repository: guacamole/guacd - tag: 1.6.0 - pullPolicy: IfNotPresent - resources: {} - podAnnotations: {} - podLabels: {} - securityContext: - runAsNonRoot: true - runAsUser: 1000 - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - service: - port: 4822 - annotations: {} - -nodeSelector: {} -tolerations: [] -affinity: {} - -strategy: - type: Recreate - -probes: - liveness: - enabled: true - initialDelaySeconds: 60 - periodSeconds: 30 - timeoutSeconds: 10 - failureThreshold: 3 - readiness: - enabled: true - initialDelaySeconds: 15 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 6 - -podDisruptionBudget: - enabled: false - minAvailable: 1 - -networkPolicy: - enabled: false - ingress: [] - egress: [] diff --git a/components.json b/components.json index 26b31d574..10e27bb21 100644 --- a/components.json +++ b/components.json @@ -5,7 +5,7 @@ "tsx": true, "tailwind": { "config": "", - "css": "src/index.css", + "css": "src/ui/index.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" @@ -15,7 +15,7 @@ "aliases": { "components": "@/components", "utils": "@/lib/utils", - "ui": "@/components/ui", + "ui": "@/components", "lib": "@/lib", "hooks": "@/hooks" }, diff --git a/crowdin.yml b/crowdin.yml deleted file mode 100644 index 8a88aa9aa..000000000 --- a/crowdin.yml +++ /dev/null @@ -1,11 +0,0 @@ -project_id: "858252" -api_token: "env:CROWDIN_API_TOKEN" -preserve_hierarchy: true - -files: - # Core has always lived at /en.json in Crowdin; moving it would orphan its translations. - - source: /src/ui/locales/en.json - dest: /en.json - translation: /src/ui/locales/translated/%locale_with_underscore%.json - - source: /plugins/*/locales/en.json - translation: /%original_path%/translated/%locale_with_underscore%.json diff --git a/docker/Dockerfile b/docker/Dockerfile index 473ffbd47..9159f321a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,23 +1,13 @@ -# Stage 0: every plugin's package.json and manifest.json and nothing else. -# npm ci needs each workspace package.json in place or it silently skips it, -# and a glob COPY flattens paths, so the tree is trimmed here instead. The -# deps layer below stays cached until one of these files changes. -FROM node:26-slim AS workspace-manifests -WORKDIR /src -COPY plugins ./plugins -RUN find plugins -mindepth 2 -maxdepth 2 ! -name package.json ! -name manifest.json -exec rm -rf {} + - # Stage 1: Install dependencies # The builders only produce JS, so they run on the build machine's own arch # once instead of under QEMU per target. Rolldown hangs under arm64 emulation. -FROM --platform=$BUILDPLATFORM node:26-slim AS deps +FROM --platform=$BUILDPLATFORM node:24-slim AS deps WORKDIR /app RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* COPY package*.json ./ COPY packages/plugin-sdk/package.json ./packages/plugin-sdk/package.json -COPY --from=workspace-manifests /src/plugins ./plugins COPY .npmrc ./ COPY vendor ./vendor @@ -47,7 +37,7 @@ COPY . . RUN npm run build:backend # Stage 4: Download OPKSSH binary for the target platform so the image works offline -FROM node:26-slim AS opkssh-downloader +FROM node:24-slim AS opkssh-downloader ARG TARGETARCH ARG OPKSSH_VERSION=v0.16.0 ARG OPKSSH_SHA256_AMD64=c018c3e7baf98612b923e742dd87be38650bf61e3b755fb2bc90de177568b1bf @@ -68,7 +58,7 @@ RUN case "$TARGETARCH" in \ echo -n "$OPKSSH_VERSION" > version.txt # Stage 5: Production dependencies only -FROM node:26-slim AS production-deps +FROM node:24-slim AS production-deps WORKDIR /app RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* @@ -80,15 +70,9 @@ COPY packages/plugin-sdk/package.json ./packages/plugin-sdk/package.json COPY .npmrc ./ COPY vendor ./vendor -COPY scripts/patch-ssh2-agent.cjs scripts/install-native-plugin-deps.cjs ./scripts/ -# Plugin manifests only, for their nativeDependencies. Outside plugins/ so npm -# ci does not treat them as workspaces: everything else a plugin uses is -# already inside its bundle. -COPY --from=workspace-manifests /src/plugins ./plugin-manifests +COPY scripts/patch-ssh2-agent.cjs ./scripts/ RUN npm ci --omit=dev --ignore-scripts && \ - node scripts/install-native-plugin-deps.cjs plugin-manifests && \ - rm -rf plugin-manifests && \ node scripts/patch-ssh2-agent.cjs && \ rm -rf node_modules/better-sqlite3/prebuilds && \ npm run build-release --prefix node_modules/better-sqlite3 && \ @@ -97,7 +81,7 @@ RUN npm ci --omit=dev --ignore-scripts && \ npm cache clean --force # Stage 6: Final optimized image -FROM node:26-slim +FROM node:24-slim WORKDIR /app # The release the opkssh-downloader stage baked in, so the plugin trusts that @@ -128,10 +112,10 @@ COPY --chown=node:node --from=frontend-builder /app/dist /app/html COPY --chown=node:node --from=production-deps /app/node_modules /app/node_modules COPY --chown=node:node --from=backend-builder /app/dist/backend ./dist/backend -# Bundled first-party plugins, resolved as dist/plugins by +# Plugins pinned in docker/bundled-plugins.json, resolved as dist/plugins by # getBundledPluginsDir(). COPY --chown=node:node --from=backend-builder /app/dist/plugins ./dist/plugins -# Target of node_modules/@termix/plugin-sdk, which core and the plugins import. +# Target of node_modules/@termix-ssh/plugin-sdk, which core and the plugins import. COPY --chown=node:node --from=backend-builder /app/packages/plugin-sdk/package.json ./packages/plugin-sdk/package.json COPY --chown=node:node --from=backend-builder /app/packages/plugin-sdk/dist ./packages/plugin-sdk/dist # Pre-baked OPKSSH binary. The opkssh plugin uses it when its checksum diff --git a/docker/bundled-plugins.json b/docker/bundled-plugins.json index 2728154af..2e507c4f0 100644 --- a/docker/bundled-plugins.json +++ b/docker/bundled-plugins.json @@ -2,139 +2,264 @@ "plugins": [ { "id": "acme-ssl", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-ACME-SSL/releases/download/v1.0.0/acme-ssl-1.0.0.tmxplug", + "sha256": "34aa172b2f6bc6a773ebdff2ec0f590d411ce258fd43176bdddb4c30d1fe30da" }, { "id": "ai", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-AI/releases/download/v1.0.0/ai-1.0.0.tmxplug", + "sha256": "ad291a2c6f6f01b72ebd275e8d202b78f55bc13a1e1a40f419d04e71b2fbd06b" }, { "id": "alerts", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Alerts/releases/download/v1.0.0/alerts-1.0.0.tmxplug", + "sha256": "3fe008f9d2a32e2b7111399054a2264ea5cf5d71264cc31e85cdab3e909cc62f", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "automations", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Automations/releases/download/v1.0.0/automations-1.0.0.tmxplug", + "sha256": "76d4cff6e78d40ec67e899f0c7b2aca43d35a47b9a15071d0a8f5a8d939af9a9" }, { "id": "docker", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Docker/releases/download/v1.0.0/docker-1.0.0.tmxplug", + "sha256": "641e564ba915a72efed4236272d63f6fff43904309833870f3554d3e9bd28dfa", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "file-manager", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-File-Manager/releases/download/v1.0.0/file-manager-1.0.0.tmxplug", + "sha256": "1cafccfe86a29e2f318026ca7bc20629df94331c81209607976dafd7feac0ca4", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "fleets", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Fleets/releases/download/v1.0.0/fleets-1.0.0.tmxplug", + "sha256": "1ebeccd6b2a8e9510ab56658374d0b87f652149101d14956752a696ca920f611" }, { "id": "homepage", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Homepage/releases/download/v1.0.0/homepage-1.0.0.tmxplug", + "sha256": "65cd21802f6a71f3b3c582bc0007bfc91160abc161b2390bce715fd2ed987f99" }, { "id": "host-metrics", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Host-Metrics/releases/download/v1.0.0/host-metrics-1.0.0.tmxplug", + "sha256": "1afce5489038e5e30b55210e8e43ba93e0446fe087e2952af75a402dbf809ea9", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "ldap", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-LDAP/releases/download/v1.0.0/ldap-1.0.0.tmxplug", + "sha256": "1df08a931457043ba500689a73b4b5ab2bfb43630f377557e0fb13280b996334" }, { "id": "network-topology", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Network-Topology/releases/download/v1.0.0/network-topology-1.0.0.tmxplug", + "sha256": "dc3ad943c1753b352b4d80c09836a293a8767293061ddf2e641575bae97124e9" }, { "id": "opkssh", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-OPKSSH/releases/download/v1.0.0/opkssh-1.0.0.tmxplug", + "sha256": "91cc1575aa1ff84632e0b6a4da68df3a7df4b8e18431b9c0378d314b886ed55d" }, { "id": "proxmox", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Proxmox/releases/download/v1.0.0/proxmox-1.0.0.tmxplug", + "sha256": "09891a3ab67a5b6589d8ab9e0fc88d8c63ff9d4e52ed8e7cbd0801a5e64e593f" }, { "id": "remote-desktop", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Remote-Desktop/releases/download/v1.0.0/remote-desktop-1.0.0.tmxplug", + "sha256": "7b24691953550458ee1609372443eeb5580d20091b02c95011c0427268350efc", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "secret-sources", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Secret-Sources/releases/download/v1.0.0/secret-sources-1.0.0.tmxplug", + "sha256": "04e9cfca70ffafc41546ddf0f1f6411743a2e0e9835b26ad5c0b91ac11028485" }, { "id": "serial", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Serial/releases/download/v1.0.0/serial-1.0.0.tmxplug", + "sha256": "2470acbd6c4c343c3d89dada972051f1ee7aace88af1e6719b92f19a4a6e46ea", + "onboarding": { + "recommended": "desktop", + "consent": false + } }, { "id": "session-recording", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Session-Recording/releases/download/v1.0.0/session-recording-1.0.0.tmxplug", + "sha256": "6b630ca0e08d79be7cb49366c898586ee3416a518f8e200157a6191c2c7c9ab7" }, { "id": "session-sharing", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Session-Sharing/releases/download/v1.0.0/session-sharing-1.0.0.tmxplug", + "sha256": "0c0d3f2d63184d4a4b2a90b11a1ee247ddc937abba76af74eb39e27048b412f9" }, { "id": "snippets", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Snippets/releases/download/v1.0.0/snippets-1.0.0.tmxplug", + "sha256": "47872d08cdf4a15b50c8f90ca125276a92579147a27cb6b5639a84a01160abe5", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "ssh-terminal", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-SSH-Terminal/releases/download/v1.0.0/ssh-terminal-1.0.0.tmxplug", + "sha256": "5b5a357bbf9a503c6b68725245712a99fa8ea0eec0baff64319be28950acac79", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "sso", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-SSO/releases/download/v1.0.0/sso-1.0.0.tmxplug", + "sha256": "d9f69f7f52b778d1b144c58e5a65006780498d6fa171b5aeb18e15b7b033ca6b", + "onboarding": { + "recommended": false, + "consent": false, + "enabledByEnv": ["OIDC_CLIENT_ID"] + } }, { "id": "step-ca", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Step-CA/releases/download/v1.0.0/step-ca-1.0.0.tmxplug", + "sha256": "05209afbaeb104ff77fecb5cf26a7823b9ed66031eac7557d3216fc3b861f7a6" }, { "id": "tailscale", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Tailscale/releases/download/v1.0.0/tailscale-1.0.0.tmxplug", + "sha256": "e9840bddef696b24354b9f04f2c97e164c3879f3fd1b337347de728db522874e" }, { "id": "telemetry", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Telemetry/releases/download/v1.0.0/telemetry-1.0.0.tmxplug", + "sha256": "b41210d638b93470891ab238e3d1180a0e0d0f0c697b1c2079156416dec05966", + "onboarding": { + "recommended": true, + "consent": true + } }, { "id": "termix-identity", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Termix-Identity/releases/download/v1.0.0/termix-identity-1.0.0.tmxplug", + "sha256": "facd9f1c6036202e7c6b1020408ce49c7ba4bc687a8f8b93ccf60e97f473f897" }, { "id": "tmux-monitor", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Tmux-Monitor/releases/download/v1.0.0/tmux-monitor-1.0.0.tmxplug", + "sha256": "d453e0c5f1bf54160615d5df4e12eac0d97fea3c78c0aad5ebe8552e1f2de42c" }, { "id": "totp", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-TOTP/releases/download/v1.0.0/totp-1.0.0.tmxplug", + "sha256": "a2d389048d16e31c089cf211fe2fb37fcfb33369497b58ee445e8626e9ec494d", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "tunnels", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Tunnels/releases/download/v1.0.0/tunnels-1.0.0.tmxplug", + "sha256": "dc6c2942490c07ec1f9a4065850288cbb00c42dae64397937d2551d689314939", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "vault", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Vault/releases/download/v1.0.0/vault-1.0.0.tmxplug", + "sha256": "bdad88ed576ed30b464c9cbddab4d9bb60696581dc1b815b76e1b4a08500f870" }, { "id": "wake-on-lan", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Wake-On-LAN/releases/download/v1.0.0/wake-on-lan-1.0.0.tmxplug", + "sha256": "3e7d4c8a9a20cde95d8594d4ae8e1fa0469d0751be9f00249f6431b10ada9ea9" }, { "id": "warpgate", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Warpgate/releases/download/v1.0.0/warpgate-1.0.0.tmxplug", + "sha256": "18ed1c8238ecf57b5776ad93c6533d7040b988ae765012c5da591b85faa02d2e" }, { "id": "web-endpoint", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Web-Endpoint/releases/download/v1.0.0/web-endpoint-1.0.0.tmxplug", + "sha256": "7f69e608eccd8cc6fa2436bffb3e2c3c8aae6cebebf7341913f15e7f9be58e42" }, { "id": "webauthn", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-WebAuthn/releases/download/v1.0.0/webauthn-1.0.0.tmxplug", + "sha256": "dafcd7bfffc39b60a14333164dfd8ca70986da1390285359669d0293cd050121", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "workspaces", - "source": "workspace" + "source": "tmxplug", + "url": "https://github.com/Termix-SSH/Plugin-Workspaces/releases/download/v1.0.0/workspaces-1.0.0.tmxplug", + "sha256": "1ee3a07bd8d61b46b6fbe118d2a424f8027575282287622b467e83a4356e6412", + "onboarding": { + "recommended": true, + "consent": false + } } ] } diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index dd671bcd2..601ae1808 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,6 +1,6 @@ services: termix: - image: ghcr.io/lukegus/termix:latest + image: ghcr.io/termix-ssh/termix:latest container_name: termix restart: unless-stopped ports: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 7a7742d2b..39e810eb4 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -68,6 +68,25 @@ fi mkdir -p /tmp/nginx envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /tmp/nginx/nginx.conf +# Which proxies may set the client IP through X-Forwarded-For. Unset keeps +# the private ranges in the template. +if [ -n "$TRUSTED_PROXIES" ]; then + REAL_IP_LINES="" + for proxy in $(echo "$TRUSTED_PROXIES" | tr ',' ' '); do + case "$proxy" in + *[!0-9a-fA-F:./]*) echo "Ignoring invalid TRUSTED_PROXIES entry: $proxy" ;; + *) REAL_IP_LINES="${REAL_IP_LINES} set_real_ip_from ${proxy};\n" ;; + esac + done + echo "Trusting X-Forwarded-For from: $TRUSTED_PROXIES" + awk -v lines="$REAL_IP_LINES" ' + /# BEGIN TRUSTED_PROXIES/ { printf "%s", lines; skip = 1; next } + /# END TRUSTED_PROXIES/ { skip = 0; next } + !skip + ' /tmp/nginx/nginx.conf > /tmp/nginx/nginx.conf.tmp + mv /tmp/nginx/nginx.conf.tmp /tmp/nginx/nginx.conf +fi + if [ "$ENABLE_SSL" = "true" ] && [ "$PORT" = "$SSL_PORT" ]; then echo "HTTP and HTTPS use port $SSL_PORT; disabling the HTTP redirect listener" sed -i '/# BEGIN HTTP_REDIRECT_SERVER/,/# END HTTP_REDIRECT_SERVER/d' /tmp/nginx/nginx.conf diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index 5c48ac194..0684f3ed0 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -41,10 +41,12 @@ http { image/svg+xml; client_header_timeout 300s; + # BEGIN TRUSTED_PROXIES set_real_ip_from 127.0.0.1; set_real_ip_from 10.0.0.0/8; set_real_ip_from 172.16.0.0/12; set_real_ip_from 192.168.0.0/16; + # END TRUSTED_PROXIES real_ip_header X-Forwarded-For; real_ip_recursive on; @@ -174,7 +176,7 @@ http { root /app/html; index index.html index.htm; expires off; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-gQLhWUEDVfLtBiv83VAckuSAVmhxOpOHbNpFbODgPN0='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-0FPou4vGHwupDie6Xais6VntpJ4yaK9E6TyhXLwZe8o='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; @@ -291,33 +293,6 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } - # 2.8 URLs that something outside Termix still calls: the Vault OIDC - # callback, the Termix ID resolver and automation webhooks. The - # backend redirects each to the plugin that serves it now (see - # contributes.http in each plugin's manifest). New plugins never add - # a block here. - location = /vault/oidc/callback { - proxy_pass http://127.0.0.1:30001; - proxy_http_version 1.1; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - } - - location ^~ /termix-id/u/ { - proxy_pass http://127.0.0.1:30001; - proxy_http_version 1.1; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location ^~ /automations/webhook/ { - rewrite ^/automations/webhook/(.*)$ /plugin-api/automations/webhook/$1 last; - } - # Every plugin's HTTP routes. One block, no per-plugin entries: a # plugin serves at /plugin-api// on the main backend and never # gets a port or a block of its own. Buffering is off so SSE streams diff --git a/docker/nginx.conf b/docker/nginx.conf index f628a5442..1ecea3455 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -41,10 +41,12 @@ http { image/svg+xml; client_header_timeout 300s; + # BEGIN TRUSTED_PROXIES set_real_ip_from 127.0.0.1; set_real_ip_from 10.0.0.0/8; set_real_ip_from 172.16.0.0/12; set_real_ip_from 192.168.0.0/16; + # END TRUSTED_PROXIES real_ip_header X-Forwarded-For; real_ip_recursive on; @@ -144,7 +146,7 @@ http { root /app/html; index index.html index.htm; expires off; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-gQLhWUEDVfLtBiv83VAckuSAVmhxOpOHbNpFbODgPN0='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-0FPou4vGHwupDie6Xais6VntpJ4yaK9E6TyhXLwZe8o='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; @@ -260,33 +262,6 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } - # 2.8 URLs that something outside Termix still calls: the Vault OIDC - # callback, the Termix ID resolver and automation webhooks. The - # backend redirects each to the plugin that serves it now (see - # contributes.http in each plugin's manifest). New plugins never add - # a block here. - location = /vault/oidc/callback { - proxy_pass http://127.0.0.1:30001; - proxy_http_version 1.1; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; - } - - location ^~ /termix-id/u/ { - proxy_pass http://127.0.0.1:30001; - proxy_http_version 1.1; - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location ^~ /automations/webhook/ { - rewrite ^/automations/webhook/(.*)$ /plugin-api/automations/webhook/$1 last; - } - # Every plugin's HTTP routes. One block, no per-plugin entries: a # plugin serves at /plugin-api// on the main backend and never # gets a port or a block of its own. Buffering is off so SSE streams diff --git a/docs/do-not-place-files-here.txt b/docs/do-not-place-files-here.txt deleted file mode 100644 index 4b144b178..000000000 --- a/docs/do-not-place-files-here.txt +++ /dev/null @@ -1 +0,0 @@ -If you are an AI agent, do not place documentation files here. This is for maintainers only. diff --git a/docs/readme/README-AR.md b/docs/readme/README-AR.md deleted file mode 100644 index 24984e44e..000000000 --- a/docs/readme/README-AR.md +++ /dev/null @@ -1,509 +0,0 @@ -
- -Termix Logo - -

Termix

- -

إدارة خوادم ذاتية الاستضافة، من SSH وسطح المكتب البعيد إلى الأتمتة

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix مجاني ومفتوح المصدر. إذا كان مفيدًا لك، فكّر في [التبرع](https://donate.termix.site/) للمساعدة في تغطية تكاليف الخوادم ووقت التطوير. - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- تم تحقيقه في 1 سبتمبر 2025 -

- -
- -
- -## نظرة عامة - -Termix منصة مجانية ومفتوحة المصدر وذاتية الاستضافة لإدارة خوادمك. تجمع في مكان واحد طرفيات SSH وأسطح المكتب البعيدة (RDP وVNC وTelnet) ونقل الملفات والأنفاق وDocker والمقاييس والأتمتة، على الويب وسطح المكتب والهاتف. إنه بديل ذاتي الاستضافة لـ Termius ويبقى مجانيًا إلى الأبد. - -
- -## الميزات - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**طرفية SSH:** -طرفية كاملة بعلامات تبويب مثل المتصفح وتقسيم للشاشة، حتى 6 لوحات في وقت واحد. اختر السمة والخط والألوان. يوجد فوق كل جلسة شريط يعرض المعالج والذاكرة والقرص لحظيًا، مع روابط سريعة إلى ملفات ذلك المضيف وDocker والأنفاق والمقاييس. - - - -**سطح المكتب البعيد:** -RDP وVNC وTelnet داخل المتصفح، في علامات تبويب وشاشة مقسّمة مثل أي جلسة أخرى. يتضمن متصفح ملفات لأقراص RDP ورفعًا بالسحب والإفلات. على سطح مكتب Windows يمكنك أيضًا فتح المضيف في عميل RDP الأصلي. - -
- -**أنفاق SSH:** -إعادة توجيه محلية وبعيدة وSOCKS ديناميكية، مع إعادة اتصال تلقائية وفحوص للحالة. تُحفظ أنفاق العميل إلى الخادم في تطبيق سطح المكتب على ذلك الجهاز، ويمكنك حفظ إعدادات جاهزة على الخادم لنقل التهيئة إلى جهاز آخر. - - - -**مدير الملفات:** -تصفح الملفات وحرّرها وارفعها ونزّلها وأعد تسميتها وانقلها واحذفها عبر SFTP، مع دعم sudo. اعرض وحرّر الشيفرة والصور والصوت والفيديو. انسخ الملفات مباشرة من خادم إلى آخر، مع اختيار أسرع مسار تلقائيًا والتحقق من سلامة النقل. - -
- -**Docker وPodman:** -شغّل الحاويات وأوقفها وعلّقها واحذفها، وتابع إحصاءاتها، وافتح طرفية داخل إحداها. يعمل مع Docker وPodman معًا. ليس بديلاً عن Portainer أو Dockge، بل وسيلة لإدارة الحاويات الموجودة لديك. - - - -**مدير المضيفات:** -احفظ مضيفاتك ونظّمها بالوسوم ومجلدات متداخلة يمكنك تسميتها وتلوينها. أعد استخدام بيانات الدخول المحفوظة عبر عدة مضيفات، وانشر مفاتيح SSH تلقائيًا، واجمع المضيفات تحت مضيف رئيسي، وحرّر وصدّر دفعة واحدة، واستخدم الاتصال السريع للاتصالات العابرة التي لا تريد حفظها. - -
- -**مقاييس المضيف:** -المعالج والذاكرة والقرص والشبكة والحرارة ومدة التشغيل والعمليات والمنافذ وتسجيلات الدخول ومعلومات النظام على معظم خوادم Linux، مع رسوم بيانية للسجل. تتيح لك بطاقات الإدارة التعامل مع الخدمات ومهام cron والحزم والمستخدمين وقواعد الجدار الناري وWireGuard وTailscale وشهادات SSL والسجلات وفحوص السلامة دون مغادرة Termix. - - - -**الأتمتة:** -اختر مُشغِّلًا ثم حدّد ما ينبغي أن يحدث. تشمل المشغّلات تجاوز مقياس لحد معين، أو مضيفًا يسقط أو يعود، أو تغيّر فحص السلامة، أو جدولًا زمنيًا، أو حدث حاوية، أو webhook واردًا. يمكن للخطوات تشغيل أوامر ومقتطفات، والتحكم في الحاويات والأنفاق، وإيقاظ مضيف، واستدعاء رابط، والانتظار، والتفرع حسب شرط، وتشغيل أتمتة أخرى، وإشعارك عبر ntfy أو Discord أو webhook. تتيح لك عمليات التشغيل التجريبي التجربة بأمان أولًا. - -
- -**الأساطيل:** -اجمع المضيفات في أسطول باختيارها يدويًا أو بقواعد الوسوم، لتنضم المضيفات الجديدة تلقائيًا. شغّل أمرًا واحدًا على كل المضيفات دفعة واحدة، وادفع الملفات واسحبها من جميعها، وثبّت الحزم، واجمع جردًا بنظام التشغيل والنواة والمعمارية ومدة التشغيل. - - - -**مساعد الذكاء الاصطناعي:** -ميزة اختيارية ومعطلة حتى تفعّلها بنفسك. اربط OpenAI أو Anthropic أو Gemini أو Ollama أو أي نقطة وصول متوافقة مع OpenAI واسأل عن إعداداتك. يمكنه قراءة المضيفات والأساطيل والمقتطفات والتنبيهات، ويقترح التغييرات لتوافق عليها بدلًا من تنفيذها بنفسه. لا يمكنه أبدًا الوصول إلى بيانات الدخول أو المستخدمين أو الإعدادات. يستطيع المسؤولون تعطيله للنظام بالكامل، ويمكنك إخفاؤه أثناء الإعداد. - -
- -**تسجيل الدخول والمستخدمون:** -حسابات محلية إضافة إلى تسجيل الدخول عبر OIDC وLDAP وGitHub وGoogle، مع التحقق بخطوتين (TOTP) ومفاتيح المرور (WebAuthn) والأجهزة الموثوقة. يستطيع المسؤولون إدارة المستخدمين وربط مجموعات OIDC بالأدوار ورؤية كل الجلسات النشطة على جميع المنصات وإلغاؤها. اربط حسابك المحلي بحساب OIDC، واطّلع على سجل التدقيق لما فعله الجميع. - - - -**الأدوار والمشاركة:** -أنشئ أدوارًا وشارك المضيفات مع المستخدمين أو الأدوار على أربعة مستويات: الاتصال والعرض والتحرير والإدارة. يعمل مع جميع أنواع المصادقة وجميع البروتوكولات، ويمكنك تجاوز بيانات الدخول المستخدمة لمضيف مشترك. - -
- -**التنبيهات:** -ضع قواعد على مقاييس المضيف مثل المعالج والذاكرة والقرص، وتلقَّ إشعارًا عبر ntfy أو Discord أو webhook عند تفعيلها. اطّلع على التنبيهات النشطة والمنتهية في سجل، وتجاهل ما لا يهمك منها. - - - -**الصفحة الرئيسية:** -شبكة عناصر تبنيها بنفسك بالسحب والإفلات. هناك عناصر لحالة المضيفات وnping وروابط الخدمات والإشارات المرجعية والبحث والساعات والتقويمات والعد التنازلي والملاحظات وRSS والطقس والصور والإطارات المضمّنة وDocker والأنفاق ورسوم المقاييس وواجهات API الخاصة بك، وحتى طرفية حية. - -
- -**المقتطفات والأدوات:** -احفظ الأوامر التي تستخدمها كثيرًا وشغّلها بنقرة واحدة، مع متغيرات للمضيف ولمدخلاتك الخاصة. شغّل أمرًا واحدًا على كل الطرفيات المفتوحة، وابحث في سجل أوامرك مع الإكمال التلقائي. - - - -**مشاركة الجلسة:** -شارك جلسة طرفية أو RDP أو VNC أو Telnet مباشرة. أرسل رابطًا يمكن لأي شخص الانضمام إليه دون حساب، أو شارك مع مستخدم Termix محدد، للقراءة فقط أو مع صلاحية الكتابة. يمكن أن تنتهي المشاركات تلقائيًا أو تُلغى في أي وقت، ويمكن إيقافها كليًا أو لكل مضيف على حدة. - -
- -**تسجيل الجلسات والسجلات:** -سجّل جلسات الطرفية وRDP وVNC وشاهدها لاحقًا. نزّل سجلات نصية للجلسة، واطّلع على سجل الاتصال لترى بالضبط ما جرى أثناء الاتصال. - - - -**الاتصالات التسلسلية:** -تواصل مع الأجهزة التسلسلية مثل الموجّهات والمبدّلات والمتحكمات الدقيقة من المتصفح أو تطبيق سطح المكتب. اضبط معدل الباود وبتات البيانات وبتات التوقف والتماثل. يستخدم واجهة Web Serial في المتصفحات المدعومة، أو خلفية أصلية في تطبيق سطح المكتب. - -
- -**Tailscale:** -اسحب الأجهزة من شبكة tailnet لإضافتها كمضيفات ببضع نقرات، واتصل عبر Tailscale SSH لتتولى قوائم صلاحيات tailnet أمر الوصول دون تخزين بيانات دخول. يعمل أيضًا مع Headscale ونقاط الوصول المخصصة. - - - -**Proxmox:** -استورد المضيفات مباشرة من نسخة Proxmox، وتابع إحصاءات العقد والأنظمة الضيفة، بما فيها المعالج والذاكرة والتخزين، في تبويب خاص بها. - -
- -**مساحات العمل وعلامات التبويب:** -احفظ مجموعة من علامات التبويب بتقسيمها، وأعد فتحها كلها بنقرة واحدة. يتذكر Termix أيضًا جلستك الأخيرة، فتعود علامات التبويب بعد التحديث وعلى الأجهزة الأخرى. - - - -**إعداد موجَّه:** -إعداد قصير يرشدك إلى اختيار نمط الواجهة والسمة والميزات التي تريدها وأول مضيف لك. يخفي الوضع البسيط ما لا تستخدمه، ويمكنك إعادة الإعداد أو تغيير النمط في أي وقت. - -
- -**تطبيق سطح المكتب المستقل والمزامنة:** -يعمل تطبيق سطح المكتب بمفرده بخلفية وقاعدة بيانات محلية، دون حاجة إلى خادم. يمكنك أيضًا ربطه بخادم Termix لمزامنة المضيفات وبيانات الدخول والمقتطفات وغيرها في الاتجاهين، واختيار ما إذا كانت الاتصالات تبدأ من جهازك أم عبر الخادم. - - - -**سطر الأوامر:** -أداة `termix` لسطر الأوامر تعمل في الطرفية وفي سكربتاتك. افتح الطرفيات، ونفّذ أمرًا على مضيف واحد أو على أسطول كامل، وانقل الملفات عبر SFTP، وأدر المضيفات والمقتطفات وبيانات الدخول. ثبّتها عبر `npm install -g @termix-ssh/cli` أو استخدم ملفًا تنفيذيًا مستقلًا. راجع [وثائق سطر الأوامر](https://docs.termix.site/cli). - -
- -**الأمان:** -تُشفَّر كلمات المرور والمفاتيح والأسرار الأخرى لكل مستخدم على حدة، ويمكن تشفير ملفات قاعدة البيانات نفسها على القرص. راجع [الوثائق](https://docs.termix.site/features/authentication/security/) لمعرفة آلية العمل. - - - -**اللغات:** -نحو 30 لغة مدمجة، تُدار عبر [Crowdin](https://docs.termix.site/translations). - -
- -
- -
-ميزات أخرى -
- -- **لوحة المعلومات** - خوادمك في لمحة واحدة، ببطاقات ترتّبها بنفسك -- **رسم الشبكة** - مختبرك المنزلي مرسومًا انطلاقًا من مضيفاتك، مع الحالة لحظيًا -- **مراقب tmux** - تصفح جلسات tmux ونوافذه ولوحاته، مع معاينة وبحث -- **مفاتيح API** - مفاتيح خاصة بكل مستخدم لها تاريخ انتهاء، للسكربتات وأنظمة CI -- **التصدير والاستيراد** - انقل المضيفات وبيانات الدخول وبيانات مدير الملفات إلى الداخل والخارج -- **SSL تلقائي** - تُنشأ الشهادات وتُجدَّد نيابة عنك، مع إعادة التوجيه إلى HTTPS، أو استخدم شهاداتك الخاصة -- **قواعد البيانات** - SQLite افتراضيًا، مع دعم PostgreSQL وMySQL أيضًا -- **واجهة حديثة** - واجهة React أنيقة تعمل على سطح المكتب والهاتف، بسمات مثل الفاتح والداكن وDracula. يمكن فتح أي اتصال بملء الشاشة من رابط -- **لوحة الأوامر** - اضغط مفتاح Shift الأيسر مرتين للانتقال إلى مضيف من لوحة المفاتيح -- **اختصارات لوحة المفاتيح** - التنقل بين علامات التبويب وإغلاقها وغير ذلك، وكلها قابلة لإعادة التعيين -- **Wake-on-LAN** - أيقظ جهازًا من Termix أو من خطوة في الأتمتة -- **مصادقة الوكيل الموثوق** - دع وكيلًا عكسيًا يتولى تسجيل الدخول ويمرّر المستخدم -- **SSH بإمكانات واسعة** - مضيفات وسيطة وWarpgate وطلبات TOTP وSOCKS5 والتحقق من مفاتيح المضيف والتعبئة التلقائية لكلمات المرور و[OPKSSH](https://github.com/openpubkey/opkssh) وtmux وport knocking وسجلات الطرفية وتمرير الوكيل ووكيل SSH من Bitwarden وتوقيع SSH عبر HashiCorp Vault وغيرها -- **Termix ID** - نسخة مدمجة على غرار sshid.io. احجز معرّفًا، وانشر مفاتيحك العامة على رابط محلِّل، وأصدر شهادات SSH من سلطة التصديق المدمجة - -
- -
- -## المنصات المدعومة - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
المنصةطريقة التوزيع
Webأي متصفح حديث (Chrome وSafari وFirefox) · يدعم PWA
Windows x64/ia32نسخة محمولة · مثبّت MSI · Chocolatey
Linux x64/ia32نسخة محمولة · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## التثبيت - -راجع [وثائق Termix](https://docs.termix.site/install) للاطلاع على تعليمات التثبيت الكاملة لجميع المنصات. - -مثال على ملف Docker Compose (يمكنك حذف `guacd` والشبكة إذا كنت لا تنوي استخدام سطح المكتب البعيد): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### سطر الأوامر - -يوفر Termix أيضًا أداة سطر أوامر، لتدير خوادمك من الطرفية وتستخدم Termix داخل سكربتاتك. - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -تستطيع فتح الطرفيات، وتنفيذ أمر على مضيف واحد أو على أسطول كامل، ونقل الملفات عبر SFTP، وإدارة المضيفات والمقتطفات وبيانات الدخول. الوثائق الكاملة على [docs.termix.site/cli](https://docs.termix.site/cli). - -### الاستضافة السحابية - -يمكنك تشغيل خادم Termix على VPS بدلًا من داخل شبكتك. إذا كان Termix يعمل داخل الشبكة التي يديرها، فأي عطل سيأخذه معه، تحديدًا حين تحتاج إليه لإصلاح الأمور. تشغيله في الخارج يبقيه متاحًا، ويمنحك عنوان IP ثابتًا، ويتيح لك الدخول من أي مكان دون VPN أو فتح منافذ. - -ترعى [GINERNET](https://docs.termix.site/install/ginernet) مشروع Termix، وتتضمن الوثائق دليلًا خطوة بخطوة للنشر على منصة الخوادم الافتراضية الخاصة بهم. - -
- -## بيانات الاستخدام - -يرسل Termix تقريرًا صغيرًا مجهول الهوية مرة واحدة يوميًا حتى أعرف عدد النسخ العاملة والميزات المستخدمة. يحتوي على معرّف عشوائي للنسخة، وإصدار التطبيق، وعدد المستخدمين والأجهزة لديك. ويمكن أن يتضمن أيضًا منصتك (نظام التشغيل، والمعمارية، وإصدار Node.js، ونوع قاعدة البيانات، وهل تستخدم Docker أو تطبيق سطح المكتب أو خادمًا عاديًا)، وعدد مرات فتح كل نوع من التبويبات وعدد عمليات تسجيل الدخول عبر SSH، والميزات المدمجة التي تعمل. لا يحتوي أبدًا على أسماء المستخدمين أو أسماء الأجهزة أو عناوين IP أو بيانات الدخول أو أي شيء آخر يعرّف بك أو بخوادمك. - -وهي مفعّلة افتراضيًا. يمكنك إيقافها أو اختيار ما تتضمنه من إعدادات المسؤول ضمن قسم Usage Statistics، وهناك يمكنك أيضًا معاينة التقرير كما سيُرسل تمامًا. ويمكن لكل مستخدم استبعاد استخدامه الشخصي للميزات من ملفه الشخصي. ضبط `ENABLE_TELEMETRY=false` يوقفها ويقفل المفتاح. - -
- -## التبرع - -Termix مجاني ومفتوح المصدر، بلا اشتراكات ولا خطط مدفوعة. إذا كان مفيدًا لك، فكّر في التبرع للمساعدة في تغطية الخوادم والنطاقات ووقت التطوير. تموّل التبرعات أيضًا وقت البحث والتعلّم اللازم لبناء ميزات مثل SAML وKubernetes ودعم الوكلاء. تابع التقدم وتبرع من الرابط أدناه. - -[تبرّع](https://donate.termix.site/) - -
- -## الرعاة - -هل تهتم بمساحة إعلانية مدفوعة لدعم التطوير؟ راسلنا على [mail@termix.site](mailto:mail@termix.site). - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## الدعم - -تحتاج مساعدة أو تريد طلب ميزة؟ افتح [مشكلة جديدة](https://github.com/Termix-SSH/Support/issues) واذكر أكبر قدر ممكن من التفاصيل، بالإنجليزية إن أمكن. يمكنك أيضًا السؤال في قناة الدعم على [Discord](https://discord.gg/jVQGdvHDrf)، وإن كانت الردود هناك قد تتأخر. - -
- -## لقطات الشاشة - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -شاهد عروض التحديثات على YouTube - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -قد تكون بعض المقاطع والصور قديمة أو لا تعرض الميزات على أفضل وجه. - -
- -
- -## الميزات المخططة - -جميع الميزات المخططة موجودة في [Projects](https://github.com/orgs/Termix-SSH/projects/5). إذا أردت المساهمة، راجع [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). - -
- -## الترخيص - -يوزَّع بموجب ترخيص Apache الإصدار 2.0. راجع ملف `LICENSE` لمزيد من المعلومات. diff --git a/docs/readme/README-CN.md b/docs/readme/README-CN.md deleted file mode 100644 index b3f646890..000000000 --- a/docs/readme/README-CN.md +++ /dev/null @@ -1,504 +0,0 @@ -
- -Termix Logo - -

Termix

- -

自托管服务器管理,从 SSH 和远程桌面到自动化

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix 免费且开源。如果您觉得它有用,请考虑[捐赠](https://donate.termix.site/)以帮助支付服务器费用和开发时间。 - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- 获得于 2025年9月1日 -

- -
- -
- -## 概览 - -Termix 是一个免费、开源、自托管的服务器管理平台。它把 SSH 终端、远程桌面(RDP、VNC、Telnet)、文件传输、隧道、Docker、指标和自动化集中在一个地方,支持网页端、桌面端和移动端。它是 Termius 的自托管替代品,并且永久免费。 - -
- -## 功能 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**SSH 终端:** -功能齐全的终端,配有类似浏览器的标签页和分屏,最多同时显示 6 个面板。可以选择主题、字体和配色。每个会话上方都有一个工具栏,显示实时的 CPU、内存和磁盘,并提供指向该主机文件、Docker、隧道和指标的快捷入口。 - - - -**远程桌面:** -在浏览器中使用 RDP、VNC 和 Telnet,和其他会话一样支持标签页和分屏。包含 RDP 驱动器的文件浏览器和拖放上传。在 Windows 桌面端,你还可以用原生 RDP 客户端打开主机。 - -
- -**SSH 隧道:** -支持本地、远程和动态 SOCKS 转发,可自动重连并进行健康检查。桌面端的客户端到服务器隧道保存在本机,你也可以把预设保存到服务器,以便迁移到另一台客户端。 - - - -**文件管理器:** -通过 SFTP 浏览、编辑、上传、下载、重命名、移动和删除文件,支持 sudo。可以查看和编辑代码、图片、音频和视频。在专属的传输标签页里把文件直接从一台服务器复制到另一台,系统会自动选择最快的路径并校验传输完整性。桌面端还提供本地和远程并排显示的视图。 - -
- -**Docker 和 Podman:** -启动、停止、暂停和删除容器,查看它们的状态,并在容器内打开一个终端。同时支持 Docker 和 Podman。它不是要取代 Portainer 或 Dockge,只是用来管理你已有的容器。 - - - -**主机管理:** -用标签和可命名、可配色的嵌套文件夹来整理主机。在多台主机之间复用已保存的凭据,自动部署 SSH 密钥,把主机归到父主机下,批量编辑和导出,还可以用快速连接处理那些不想保存的一次性连接。 - -
- -**主机指标:** -在大多数 Linux 服务器上查看 CPU、内存、磁盘、网络、温度、NVIDIA GPU、运行时间、进程、端口、登录记录和系统信息,并附带历史曲线图。管理卡片让你无需离开 Termix 就能处理服务、定时任务、软件包、用户、防火墙规则、WireGuard、Tailscale、SSL 证书、日志和健康检查。 - - - -**自动化:** -先选一个触发条件,再决定要做什么。触发条件包括指标超过阈值、主机上线或下线、健康检查状态变化、定时计划、容器事件,或者一个传入的 Webhook。步骤可以执行命令和代码片段、控制容器和隧道、唤醒主机、调用某个网址、等待、按条件分支、运行另一个自动化,并通过 ntfy、Discord 或 Webhook 通知你。测试运行让你先安全地试一遍。 - -
- -**机群:** -通过手动挑选或标签规则把主机编成一个机群,新主机可以自动加入。一次在所有主机上执行同一条命令,向全部主机推送和拉取文件,安装软件包,并收集系统、内核、架构和运行时间的清单。 - - - -**AI 助手:** -可选功能,默认关闭,需要你手动开启。接入 OpenAI、Anthropic、Gemini、Ollama 或任何兼容 OpenAI 的接口,向它询问你的配置。它可以读取主机、机群、代码片段和告警,并把改动作为建议提交给你确认,而不会自行修改。它永远无法接触凭据、用户和设置。管理员可以对整个实例关闭它,你也可以在初始设置时把它隐藏。 - -
- -**登录与用户:** -支持本地账户,以及 OIDC、LDAP、GitHub 和 Google 登录,还有两步验证(TOTP)、通行密钥(WebAuthn)和受信任设备。管理员可以管理用户、把 OIDC 群组映射到角色、查看所有平台上的活动会话并将其吊销。你可以把本地账户和 OIDC 账户关联起来,并查看记录所有人操作的审计日志。 - - - -**角色与共享:** -创建角色,并按四个级别把主机共享给用户或角色:连接、查看、编辑和管理。适用于所有认证方式和所有协议,并且可以覆盖共享主机所使用的凭据。 - -
- -**告警:** -为 CPU、内存、磁盘等主机指标设置规则,触发时通过 ntfy、Discord 或 Webhook 通知你。在历史记录中查看正在触发和已恢复的告警,并忽略你不关心的那些。 - - - -**主页:** -一个由你自己搭建的拖放小组件网格。小组件包括主机状态、Ping、服务链接、书签、搜索、时钟、日历、倒计时、便签、RSS、天气、图片、内嵌网页、Docker、隧道、指标图表、自定义 API,甚至还有一个实时终端。 - -
- -**代码片段与工具:** -保存常用命令,一键执行,并支持主机变量和自定义输入。可以在所有已打开的终端中同时运行一条命令,也可以带自动补全地搜索命令历史。 - - - -**会话共享:** -实时共享终端、RDP、VNC 或 Telnet 会话。可以发送一个无需账户即可加入的链接,也可以共享给指定的 Termix 用户,并选择只读或可读写。共享可以自动过期或随时撤销,也可以全局或按主机关闭。 - -
- -**会话录制与日志:** -录制终端、RDP 和 VNC 会话,之后可以回放。可以下载会话的纯文本日志,也可以查看连接日志,了解连接过程中究竟发生了什么。 - - - -**串口连接:** -从浏览器或桌面应用连接路由器、交换机和单片机等串口设备。可设置波特率、数据位、停止位和校验位。在支持的浏览器中使用 Web Serial API,在桌面应用中使用原生后端。 - -
- -**Tailscale:** -从你的 tailnet 中拉取设备,点几下就能把它们添加为主机,并使用 Tailscale SSH 连接,由 tailnet ACL 负责访问控制,无需保存任何凭据。也支持 Headscale 和自定义接口地址。 - - - -**Proxmox:** -直接从 Proxmox 实例导入主机,并在专属标签页中查看节点和虚拟机的状态,包括 CPU、内存和存储。 - -
- -**工作区与标签页:** -保存一组标签页及其分屏布局,一键就能把整套重新打开。Termix 还会记住你上次的会话,所以刷新页面或换设备后标签页都会回来。 - - - -**命令行工具:** -`termix` 命令行工具,可用于你的终端和脚本。打开终端、在单台主机或整个机群上执行命令、通过 SFTP 传输文件,以及管理主机、代码片段和凭据。用 `npm install -g @termix-ssh/cli` 安装,或者直接下载独立的可执行文件。详见 [CLI 文档](https://docs.termix.site/cli)。 - -
- -**安全:** -密码、密钥和其他机密按用户加密,数据库文件本身也可以在磁盘上加密。具体原理请查看[文档](https://docs.termix.site/features/authentication/security/)。 - - - -**多语言:** -内置约 30 种语言,通过 [Crowdin](https://docs.termix.site/translations) 管理。 - -
- -
- -
-更多功能 -
- -- **仪表盘** - 一眼看清你的服务器,卡片由你自己排布 -- **网络拓扑图** - 根据你的主机绘制出你的家庭实验室,并显示实时状态 -- **Tmux 监视器** - 浏览 tmux 的会话、窗口和面板,支持预览和搜索 -- **API 密钥** - 面向用户的密钥,带有效期,可用于脚本和 CI -- **导出与导入** - 把主机、凭据和文件管理器数据导入导出 -- **自动 SSL** - 自动签发和续期证书,并配置 HTTPS 跳转,也可以使用你自己的证书 -- **数据库** - 默认使用 SQLite,同时支持 PostgreSQL 和 MySQL -- **现代界面** - 简洁的 React 界面,桌面和移动端都适用,提供浅色、深色和 Dracula 等主题。任何连接都能通过网址全屏打开 -- **命令面板** - 双击左 Shift,用键盘直接跳到某台主机 -- **键盘快捷键** - 在标签页之间切换、关闭标签页等,全部可以重新绑定 -- **网络唤醒** - 从 Termix 或自动化步骤中唤醒一台机器 -- **受信任代理认证** - 由反向代理完成登录,并把用户信息传递进来 -- **白标定制** - 管理员可以用自己的名称、图标和配色为实例重新品牌化 -- **Web 端点** - 在 Termix 内嵌打开主机自己的网页界面,比如路由器的管理页面,而不是另开一个标签页 -- **协作房间** - 持久化的房间,一群人可以一起在会话之间跳转,有主持人舞台和邀请功能 -- **丰富的 SSH 功能** - 跳板机、Warpgate、TOTP 验证、SOCKS5、主机密钥验证、密码自动填充、[OPKSSH](https://github.com/openpubkey/opkssh)、tmux、端口敲门、终端日志、代理转发、Bitwarden SSH 代理、HashiCorp Vault SSH 签名、Step CA、1Password Connect 等等 -- **Termix ID** - 内置的 sshid.io 式功能。认领一个用户名,在解析地址上发布你的公钥,并用内置 CA 签发 SSH 证书 -- **命令历史自动建议** - 在终端中输入时,根据你的命令历史给出行内建议 -- **桌面独立运行与同步** - 桌面应用自带本地后端和数据库可以独立运行,也可以选择和 Termix 服务器同步 - -
- -
- -## 平台支持 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
平台发行方式
Web任何现代浏览器(Chrome、Safari、Firefox)· 支持 PWA
Windows x64/ia32便携版 · MSI 安装程序 · Chocolatey
Linux x64/ia32便携版 · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## 安装 - -访问 [Termix 文档](https://docs.termix.site/install) 查看所有平台的完整安装说明。 - -要部署到 Kubernetes?Helm chart 在 `charts/termix` 目录下,涵盖 Ingress、Traefik、Argo CD、GitHub Actions 和 GitLab CI 的完整搭建说明见 -[docs.termix.site/install/server/kubernetes](https://docs.termix.site/install/server/kubernetes)。 - -Docker Compose 示例(如果你不打算使用远程桌面功能,可以省略 `guacd` 和相关网络配置): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### 命令行工具 - -Termix 还提供命令行工具,让你可以在终端里管理服务器,也可以把 Termix 用在自己的脚本中。 - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -它可以打开终端、在单台主机或整个机群上执行命令、通过 SFTP 传输文件,以及管理主机、代码片段和凭据。完整文档见 [docs.termix.site/cli](https://docs.termix.site/cli)。 - -### 云端部署 - -你也可以把 Termix 服务端跑在 VPS 上,而不是自己的内网里。如果 Termix 就运行在它所管理的网络中,一旦网络出问题,Termix 也会跟着一起挂掉,而这恰恰是你最需要它的时候。放在外面运行可以保证它始终可达,还能获得固定 IP,不用 VPN 或端口转发就能从任何地方接入。 - -[GINERNET](https://docs.termix.site/install/ginernet) 是 Termix 的赞助商,文档里有部署到他们 VPS 平台的分步指南。 - -
- -## 遥测 - -Termix 每天会发送一次匿名的小型报告,让我知道有多少实例在运行、哪些功能被使用。报告包含一个随机的实例 ID、应用版本,以及你的用户数和主机数。它还可以包含你的平台信息(操作系统、架构、Node.js 版本、数据库类型,以及你是用 Docker、桌面应用还是普通服务器运行)、每种标签页被打开的次数和 SSH 登录次数,以及正在运行的内置功能。它绝不会包含用户名、主机名、IP 地址、凭据或任何能识别你或你的服务器的信息。 - -该功能默认开启。你可以在管理设置的"Usage Statistics"中关闭它或选择要包含的内容,还可以在那里预览将要发送的完整报告。每个用户都可以在个人资料中排除自己的功能使用数据。设置 `ENABLE_TELEMETRY=false` 会关闭它并锁定开关。 - -
- -## 捐赠 - -Termix 免费且开源,没有订阅也没有付费方案。如果你觉得它有用,可以考虑捐赠,帮忙分担服务器、域名和开发时间的成本。捐赠还能支持研究和学习 SAML、Kubernetes、Agent 等功能所需的时间。可以在下方查看进展并捐赠。 - -[捐赠](https://donate.termix.site/) - -
- -## 赞助商 - -有意通过付费展示位支持开发吗?请发邮件到 [mail@termix.site](mailto:mail@termix.site)。 - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## 支持 - -需要帮助或想提功能建议?可以[新建一个 issue](https://github.com/Termix-SSH/Support/issues),尽量写清楚细节,如果方便请用英文。你也可以在 [Discord](https://discord.gg/jVQGdvHDrf) 的支持频道提问,不过那边回复可能会慢一些。 - -
- -## 展示 - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -在 YouTube 上观看版本更新介绍 - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -部分视频和图片可能已经过时,或者不能完整展示功能。 - -
- -
- -## 计划功能 - -所有计划中的功能都在 [Projects](https://github.com/orgs/Termix-SSH/projects/5) 里。如果你想参与贡献,请查看[贡献指南](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)。 - -
- -## 许可证 - -基于 Apache License 2.0 发布。详见 `LICENSE`。 - diff --git a/docs/readme/README-DE.md b/docs/readme/README-DE.md deleted file mode 100644 index 15a25558d..000000000 --- a/docs/readme/README-DE.md +++ /dev/null @@ -1,504 +0,0 @@ -
- -Termix Logo - -

Termix

- -

Selbst gehostete Serververwaltung, von SSH und Remotedesktop bis zu Automatisierungen

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix ist kostenlos und quelloffen. Wenn es dir hilft, denk über eine [Spende](https://donate.termix.site/) nach, um Serverkosten und Entwicklungszeit zu decken. - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- Erreicht am 1. September 2025 -

- -
- -
- -## Überblick - -Termix ist eine kostenlose, quelloffene und selbst gehostete Plattform zur Verwaltung deiner Server. Sie bringt SSH-Terminals, Remotedesktops (RDP, VNC, Telnet), Dateiübertragungen, Tunnel, Docker, Metriken und Automatisierungen an einem Ort zusammen, im Browser, auf dem Desktop und auf dem Handy. Eine selbst gehostete Alternative zu Termius, die dauerhaft kostenlos bleibt. - -
- -## Funktionen - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**SSH-Terminal:** -Ein vollwertiges Terminal mit Tabs wie im Browser und geteiltem Bildschirm, bis zu 6 Bereiche gleichzeitig. Thema, Schrift und Farben wählst du selbst. Über jeder Sitzung sitzt eine Leiste mit CPU, Speicher und Festplatte in Echtzeit sowie Verknüpfungen zu Dateien, Docker, Tunneln und Metriken dieses Hosts. - - - -**Remotedesktop:** -RDP, VNC und Telnet im Browser, in Tabs und geteiltem Bildschirm wie jede andere Sitzung. Mit Dateibrowser für RDP-Laufwerke und Hochladen per Drag-and-drop. Auf dem Windows-Desktop kannst du einen Host auch im nativen RDP-Client öffnen. - -
- -**SSH-Tunnel:** -Lokale, entfernte und dynamische SOCKS-Weiterleitung mit automatischem Neuverbinden und Statusprüfungen. Client-zu-Server-Tunnel der Desktop-App bleiben auf diesem Rechner, und du kannst Voreinstellungen auf dem Server speichern, um eine Konfiguration auf einen anderen Rechner zu übernehmen. - - - -**Dateimanager:** -Dateien über SFTP durchsuchen, bearbeiten, hochladen, herunterladen, umbenennen, verschieben und löschen, auch mit sudo. Code, Bilder, Audio und Video ansehen und bearbeiten. Dateien direkt von einem Server zum anderen kopieren, wobei der schnellste Weg für dich gewählt und die Übertragung auf Fehler geprüft wird. - -
- -**Docker und Podman:** -Container starten, stoppen, pausieren und entfernen, ihre Auslastung ansehen und eine Shell darin öffnen. Funktioniert mit Docker und mit Podman. Es soll Portainer oder Dockge nicht ersetzen, sondern nur die Container verwalten, die du schon hast. - - - -**Hostverwaltung:** -Hosts mit Tags und verschachtelten Ordnern ordnen, die du benennen und einfärben kannst. Gespeicherte Zugangsdaten für mehrere Hosts wiederverwenden, SSH-Schlüssel automatisch verteilen, Hosts unter einem übergeordneten Host gruppieren, in großen Mengen bearbeiten und exportieren. Für einmalige Verbindungen, die du nicht speichern willst, gibt es Schnellverbindung. - -
- -**Host-Metriken:** -CPU, Speicher, Festplatte, Netzwerk, Temperatur, NVIDIA-GPU, Laufzeit, Prozesse, Ports, Anmeldungen und Systeminfos auf den meisten Linux-Servern, mit Verlaufsgrafiken. Über Verwaltungskarten kümmerst du dich um Dienste, Cronjobs, Pakete, Benutzer, Firewallregeln, WireGuard, Tailscale, SSL-Zertifikate, Logs und Statusprüfungen, ohne Termix zu verlassen. - - - -**Automatisierungen:** -Wähle einen Auslöser und lege fest, was passieren soll. Auslöser sind unter anderem eine Metrik über einem Schwellwert, ein Host der hoch- oder runtergeht, eine geänderte Statusprüfung, ein Zeitplan, ein Container-Ereignis oder ein eingehender Webhook. Schritte können Befehle und Snippets ausführen, Container und Tunnel steuern, einen Host aufwecken, eine URL aufrufen, warten, sich nach einer Bedingung verzweigen, eine andere Automatisierung starten und dich über ntfy, Discord oder einen Webhook benachrichtigen. Mit Testläufen probierst du alles gefahrlos aus. - -
- -**Flotten:** -Fasse Hosts zu einer Flotte zusammen, entweder von Hand oder über Tag-Regeln, damit neue Hosts von selbst dazukommen. Führe einen Befehl auf allen Hosts gleichzeitig aus, schiebe und hole Dateien auf allen, installiere Pakete und sammle eine Übersicht über Betriebssystem, Kernel, Architektur und Laufzeit. - - - -**KI-Assistent:** -Optional und aus, bis du ihn einschaltest. Verbinde OpenAI, Anthropic, Gemini, Ollama oder einen beliebigen OpenAI-kompatiblen Endpunkt und frag ihn zu deiner Umgebung. Er liest Hosts, Flotten, Snippets und Warnungen und schlägt Änderungen vor, die du bestätigst, statt sie selbst vorzunehmen. An Zugangsdaten, Benutzer und Einstellungen kommt er nie heran. Administratoren können ihn für die ganze Instanz auslassen, und du kannst ihn schon bei der Einrichtung ausblenden. - -
- -**Anmeldung und Benutzer:** -Lokale Konten sowie Anmeldung über OIDC, LDAP, GitHub und Google, dazu Zwei-Faktor-Authentifizierung (TOTP), Passkeys (WebAuthn) und vertrauenswürdige Geräte. Administratoren können Benutzer verwalten, OIDC-Gruppen auf Rollen abbilden, alle aktiven Sitzungen über alle Plattformen hinweg sehen und beenden. Verknüpfe dein lokales Konto mit deinem OIDC-Konto und lies im Prüfprotokoll nach, wer was gemacht hat. - - - -**Rollen und Freigaben:** -Lege Rollen an und teile Hosts mit Benutzern oder Rollen auf vier Stufen: Verbinden, Ansehen, Bearbeiten und Verwalten. Das funktioniert mit jeder Authentifizierungsart und jedem Protokoll, und du kannst die Zugangsdaten für einen geteilten Host überschreiben. - -
- -**Warnungen:** -Lege Regeln für Host-Metriken wie CPU, Speicher und Festplatte fest und lass dich über ntfy, Discord oder einen Webhook benachrichtigen, wenn sie greifen. Sieh dir aktive und wieder behobene Warnungen im Verlauf an und blende aus, was dich nicht interessiert. - - - -**Startseite:** -Ein Raster aus Widgets, das du selbst per Drag-and-drop zusammenstellst. Widgets für Hoststatus, Pings, Dienstlinks, Lesezeichen, Suche, Uhren, Kalender, Countdowns, Notizen, RSS, Wetter, Bilder, Iframes, Docker, Tunnel, Metrikdiagramme, eigene APIs und sogar ein laufendes Terminal. - -
- -**Snippets und Werkzeuge:** -Speichere Befehle, die du oft brauchst, und starte sie mit einem Klick, mit Variablen für den Host und eigene Eingaben. Führe einen Befehl in allen offenen Terminals zugleich aus und durchsuche deinen Befehlsverlauf mit Autovervollständigung. - - - -**Sitzungsfreigabe:** -Teile eine laufende Terminal-, RDP-, VNC- oder Telnet-Sitzung in Echtzeit. Verschicke einen Link, dem jeder ohne Konto beitreten kann, oder teile mit einem bestimmten Termix-Benutzer, nur lesend oder mit Schreibrechten. Freigaben können von selbst ablaufen oder zurückgezogen werden und lassen sich global oder pro Host abschalten. - -
- -**Sitzungsaufzeichnung und Protokolle:** -Zeichne Terminal-, RDP- und VNC-Sitzungen auf und spiel sie später ab. Lade einfache Textprotokolle einer Sitzung herunter und sieh im Verbindungsprotokoll nach, was während einer Verbindung genau passiert ist. - - - -**Serielle Verbindungen:** -Sprich mit seriellen Geräten wie Routern, Switches und Mikrocontrollern, aus dem Browser oder der Desktop-App. Stelle Baudrate, Datenbits, Stoppbits und Parität ein. Nutzt die Web-Serial-API in passenden Browsern oder ein natives Backend in der Desktop-App. - -
- -**Tailscale:** -Hol Geräte aus deinem Tailnet, um sie mit ein paar Klicks als Hosts anzulegen, und verbinde dich per Tailscale SSH, damit deine Tailnet-ACLs den Zugriff regeln und keine Zugangsdaten gespeichert werden. Headscale und eigene Endpunkte gehen auch. - - - -**Proxmox:** -Importiere Hosts direkt aus einer Proxmox-Instanz und beobachte Knoten- und Gastwerte wie CPU, Speicher und Storage in einem eigenen Tab. - -
- -**Arbeitsbereiche und Tabs:** -Speichere eine Reihe von Tabs samt Aufteilung und öffne alles mit einem Klick wieder. Termix merkt sich auch deine letzte Sitzung, sodass deine Tabs nach einem Neuladen und auf anderen Geräten wieder da sind. - - - -**Kommandozeile:** -Ein `termix`-CLI für deine Shell und deine Skripte. Terminals öffnen, einen Befehl auf einem Host oder einer ganzen Flotte ausführen, Dateien per SFTP verschieben und Hosts, Snippets und Zugangsdaten verwalten. Installiere es mit `npm install -g @termix-ssh/cli` oder nimm eine eigenständige Binärdatei. Siehe die [CLI-Dokumentation](https://docs.termix.site/cli). - -
- -**Sicherheit:** -Passwörter, Schlüssel und andere Geheimnisse werden pro Benutzer verschlüsselt, und die Datenbankdateien selbst lassen sich auf der Festplatte verschlüsseln. Wie das funktioniert, steht in der [Dokumentation](https://docs.termix.site/features/authentication/security/). - - - -**Sprachen:** -Rund 30 Sprachen sind eingebaut, verwaltet über [Crowdin](https://docs.termix.site/translations). - -
- -
- -
-Weitere Funktionen -
- -- **Dashboard** - Deine Server auf einen Blick, mit Karten, die du selbst anordnest -- **Netzwerkgrafik** - Dein Homelab aus deinen Hosts gezeichnet, mit Live-Status -- **Tmux-Monitor** - tmux-Sitzungen, Fenster und Bereiche durchsehen, mit Vorschau und Suche -- **API-Schlüssel** - Benutzerbezogene Schlüssel mit Ablaufdatum für Skripte und CI -- **Export und Import** - Hosts, Zugangsdaten und Dateimanager-Daten rein- und rausholen -- **Automatisches SSL** - Zertifikate werden für dich erstellt und erneuert, samt HTTPS-Weiterleitung, oder du bringst eigene mit -- **Datenbanken** - Standardmäßig SQLite, dazu PostgreSQL und MySQL -- **Moderne Oberfläche** - Aufgeräumte React-Oberfläche für Desktop und Handy, mit Themen wie Hell, Dunkel und Dracula. Jede Verbindung lässt sich über eine URL im Vollbild öffnen -- **Befehlspalette** - Zweimal linke Umschalttaste, um per Tastatur zu einem Host zu springen -- **Tastenkürzel** - Zwischen Tabs wechseln, Tabs schließen und mehr, alles neu belegbar -- **Wake-on-LAN** - Einen Rechner aus Termix heraus oder aus einem Automatisierungsschritt aufwecken -- **Vertrauenswürdiger Proxy** - Einen Reverse Proxy die Anmeldung erledigen und den Benutzer durchreichen lassen -- **White Label** - Administratoren können die Instanz mit eigenem Namen, Logo und Farben versehen -- **Web-Endpunkte** - Öffne die eigene Weboberfläche eines Hosts, etwa die Verwaltungsseite eines Routers, eingebettet in Termix statt in einem eigenen Tab -- **Kollaborationsräume** - Feste Räume, in denen eine Gruppe gemeinsam zwischen Sitzungen wechselt, mit Präsentationsbühne und Einladungen -- **Viele SSH-Funktionen** - Sprunghosts, Warpgate, TOTP-Abfragen, SOCKS5, Prüfung von Hostschlüsseln, automatisches Ausfüllen von Passwörtern, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, Port Knocking, Terminalprotokolle, Agent-Weiterleitung, Bitwarden SSH-Agent, SSH-Signierung über HashiCorp Vault, Step CA, 1Password Connect und mehr -- **Termix ID** - Eine eingebaute Variante von sshid.io. Sichere dir einen Namen, veröffentliche deine öffentlichen Schlüssel unter einer Resolver-URL und stelle SSH-Zertifikate über die eingebaute CA aus -- **Autovervollständigung im Befehlsverlauf** - Vorschläge direkt im Terminal beim Tippen, basierend auf deinem Befehlsverlauf -- **Desktop eigenständig und Synchronisierung** - Die Desktop-App läuft eigenständig mit lokalem Backend und eigener Datenbank und kann sich optional mit einem Termix-Server synchronisieren - -
- -
- -## Unterstützte Plattformen - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PlattformBezugsquelle
WebJeder moderne Browser (Chrome, Safari, Firefox) · PWA-fähig
Windows x64/ia32Portabel · MSI-Installer · Chocolatey
Linux x64/ia32Portabel · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## Installation - -In der [Termix-Dokumentation](https://docs.termix.site/install) findest du die vollständigen Installationsanleitungen für alle Plattformen. - -Willst du auf Kubernetes bereitstellen? Das Helm-Chart liegt in `charts/termix`, und die Einrichtung -für Ingress, Traefik, Argo CD, GitHub Actions und GitLab CI steht unter -[docs.termix.site/install/server/kubernetes](https://docs.termix.site/install/server/kubernetes). - -Beispiel für eine Docker-Compose-Datei (`guacd` und das Netzwerk kannst du weglassen, wenn du keinen Remotedesktop brauchst): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### Kommandozeile - -Termix hat auch ein CLI, damit du deine Server vom Terminal aus verwalten und Termix in eigenen Skripten nutzen kannst. - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -Es kann Terminals öffnen, einen Befehl auf einem Host oder einer ganzen Flotte ausführen, Dateien per SFTP verschieben und Hosts, Snippets und Zugangsdaten verwalten. Die vollständige Dokumentation steht auf [docs.termix.site/cli](https://docs.termix.site/cli). - -### Cloud-Hosting - -Du kannst den Termix-Server auf einem VPS laufen lassen statt im eigenen Netz. Läuft Termix in dem Netz, das es verwaltet, reißt eine Störung es mit sich, und zwar genau dann, wenn du es zum Reparieren bräuchtest. Woanders bleibt es erreichbar, du bekommst eine feste IP und kommst von überall heran, ohne VPN und ohne Portfreigabe. - -[GINERNET](https://docs.termix.site/install/ginernet) sponsert Termix, und in der Dokumentation steht eine Schritt-für-Schritt-Anleitung für die Bereitstellung auf deren VPS-Plattform. - -
- -## Telemetrie - -Termix schickt einmal am Tag einen kleinen anonymen Bericht, damit ich sehe, wie viele Instanzen laufen und welche Funktionen genutzt werden. Er enthält eine zufällige Instanz-ID, die App-Version und wie viele Benutzer und Hosts du hast. Er kann außerdem deine Plattform enthalten (Betriebssystem, Architektur, Node.js-Version, Datenbanktyp und ob du Docker, die Desktop-App oder einen normalen Server nutzt), wie oft jede Art von Tab geöffnet wurde und wie viele SSH-Logins es gab, sowie welche eingebauten Funktionen laufen. Er enthält nie Benutzernamen, Hostnamen, IP-Adressen, Zugangsdaten oder sonst etwas, das dich oder deine Server erkennbar macht. - -Es ist standardmäßig an. Schalte es in den Administrationseinstellungen unter Usage Statistics aus oder wähle dort, was gesendet wird. Dort kannst du dir auch den genauen Bericht vorab ansehen. Jeder Benutzer kann seine eigene Funktionsnutzung in seinem Profil ausschließen. `ENABLE_TELEMETRY=false` schaltet es aus und sperrt den Schalter. - -
- -## Spenden - -Termix ist kostenlos und quelloffen, ohne Abo und ohne Bezahlmodell. Wenn es dir hilft, denk über eine Spende nach, um Server, Domains und Entwicklungszeit zu decken. Spenden finanzieren auch die Zeit, um Funktionen wie SAML, Kubernetes und Agent-Unterstützung zu erarbeiten. Unten kannst du den Fortschritt verfolgen und spenden. - -[Spenden](https://donate.termix.site/) - -
- -## Sponsoren - -Interesse an einer bezahlten Platzierung zur Unterstützung der Entwicklung? Schreib an [mail@termix.site](mailto:mail@termix.site). - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## Support - -Brauchst du Hilfe oder möchtest du eine Funktion vorschlagen? Erstelle ein [neues Issue](https://github.com/Termix-SSH/Support/issues) und beschreibe es so genau wie möglich, nach Möglichkeit auf Englisch. Du kannst auch im Support-Kanal auf [Discord](https://discord.gg/jVQGdvHDrf) fragen, dort dauern Antworten aber manchmal länger. - -
- -## Screenshots - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -Übersichten zu Updates auf YouTube ansehen - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -Manche Videos und Bilder sind vielleicht veraltet oder zeigen die Funktionen nicht perfekt. - -
- -
- -## Geplante Funktionen - -Alle geplanten Funktionen stehen unter [Projects](https://github.com/orgs/Termix-SSH/projects/5). Wenn du mitarbeiten möchtest, sieh dir [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) an. - -
- -## Lizenz - -Veröffentlicht unter der Apache-Lizenz Version 2.0. Mehr dazu in `LICENSE`. diff --git a/docs/readme/README-ES.md b/docs/readme/README-ES.md deleted file mode 100644 index fe3c072ed..000000000 --- a/docs/readme/README-ES.md +++ /dev/null @@ -1,504 +0,0 @@ -
- -Termix Logo - -

Termix

- -

Gestión de servidores autoalojada, desde SSH y escritorio remoto hasta automatizaciones

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix es gratuito y de código abierto. Si te resulta útil, considera [donar](https://donate.termix.site/) para ayudar a cubrir los costes de servidor y el tiempo de desarrollo. - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- Conseguido el 1 de septiembre de 2025 -

- -
- -
- -## Descripción general - -Termix es una plataforma gratuita, de código abierto y autoalojada para gestionar tus servidores. Reúne en un solo sitio terminales SSH, escritorios remotos (RDP, VNC, Telnet), transferencias de archivos, túneles, Docker, métricas y automatizaciones, en web, escritorio y móvil. Es una alternativa autoalojada a Termius que seguirá siendo gratuita. - -
- -## Características - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**Terminal SSH:** -Un terminal completo con pestañas como las del navegador y pantalla dividida, hasta 6 paneles a la vez. Elige tu tema, tu fuente y tus colores. Sobre cada sesión hay una barra con CPU, memoria y disco en vivo, además de accesos rápidos a los archivos, Docker, túneles y métricas de ese host. - - - -**Escritorio remoto:** -RDP, VNC y Telnet en el navegador, en pestañas y pantalla dividida como cualquier otra sesión. Incluye un explorador de archivos para las unidades RDP y subida arrastrando y soltando. En el escritorio de Windows también puedes abrir un host en el cliente RDP nativo. - -
- -**Túneles SSH:** -Reenvío local, remoto y SOCKS dinámico, con reconexión automática y comprobaciones de estado. Los túneles de cliente a servidor de la aplicación de escritorio se guardan en ese equipo, y puedes guardar ajustes en el servidor para llevarte una configuración a otro equipo. - - - -**Gestor de archivos:** -Navega, edita, sube, descarga, renombra, mueve y borra archivos por SFTP, con soporte para sudo. Mira y edita código, imágenes, audio y vídeo. Copia archivos directamente de un servidor a otro, con la ruta más rápida elegida por ti y las transferencias verificadas. - -
- -**Docker y Podman:** -Arranca, para, pausa y elimina contenedores, mira sus estadísticas y abre una consola dentro de uno. Funciona con Docker y con Podman. No pretende sustituir a Portainer ni a Dockge, solo gestionar los contenedores que ya tienes. - - - -**Gestor de hosts:** -Guarda y organiza hosts con etiquetas y carpetas anidadas que puedes nombrar y colorear. Reutiliza credenciales guardadas entre hosts, despliega claves SSH automáticamente, agrupa hosts bajo un host padre, edita y exporta en lote, y usa la conexión rápida para conexiones puntuales que no quieres guardar. - -
- -**Métricas de host:** -CPU, memoria, disco, red, temperatura, GPU NVIDIA, tiempo encendido, procesos, puertos, inicios de sesión e información del sistema en la mayoría de servidores Linux, con gráficas de histórico. Las tarjetas de gestión te dejan manejar servicios, tareas cron, paquetes, usuarios, reglas del cortafuegos, WireGuard, Tailscale, certificados SSL, registros y comprobaciones de estado sin salir de Termix. - - - -**Automatizaciones:** -Elige un disparador y luego di qué debe pasar. Los disparadores incluyen una métrica que supera un umbral, un host que se cae o vuelve, una comprobación de estado que cambia, una programación, un evento de contenedor o un webhook entrante. Los pasos pueden ejecutar comandos y fragmentos, controlar contenedores y túneles, despertar un host, llamar a una URL, esperar, ramificarse según una condición, ejecutar otra automatización y avisarte por ntfy, Discord o un webhook. Las ejecuciones de prueba te dejan probarlo sin riesgo. - -
- -**Flotas:** -Agrupa hosts en una flota eligiéndolos o con reglas de etiquetas, para que los nuevos entren solos. Ejecuta un comando en todos los hosts a la vez, envía y recoge archivos de todos ellos, instala paquetes y reúne un inventario del sistema, el kernel, la arquitectura y el tiempo encendido. - - - -**Asistente de IA:** -Es opcional y está apagado hasta que tú lo enciendas. Conecta OpenAI, Anthropic, Gemini, Ollama o cualquier punto de acceso compatible con OpenAI y pregúntale sobre tu instalación. Puede leer hosts, flotas, fragmentos y alertas, y propone cambios para que los apruebes en lugar de hacerlos él. Nunca puede tocar credenciales, usuarios ni ajustes. Los administradores pueden dejarlo apagado para toda la instancia, y tú puedes ocultarlo durante la configuración. - -
- -**Acceso y usuarios:** -Cuentas locales más inicio de sesión con OIDC, LDAP, GitHub y Google, con doble factor (TOTP), llaves de acceso (WebAuthn) y dispositivos de confianza. Los administradores pueden gestionar usuarios, asignar grupos de OIDC a roles, ver todas las sesiones activas en cualquier plataforma y revocarlas. Enlaza tu cuenta local con la de OIDC y consulta el registro de auditoría de lo que ha hecho cada uno. - - - -**Roles y compartición:** -Crea roles y comparte hosts con usuarios o roles en cuatro niveles: conectar, ver, editar y gestionar. Funciona con todos los tipos de autenticación y todos los protocolos, y puedes cambiar las credenciales que se usan en un host compartido. - -
- -**Alertas:** -Pon reglas sobre métricas de host como CPU, memoria y disco, y recibe avisos por ntfy, Discord o un webhook cuando salten. Consulta las alertas activas y resueltas en un histórico y descarta las que no te importan. - - - -**Página de inicio:** -Una rejilla de widgets que montas tú mismo arrastrando y soltando. Hay widgets para el estado de los hosts, pings, enlaces a servicios, marcadores, búsqueda, relojes, calendarios, cuentas atrás, notas, RSS, tiempo, imágenes, iframes, Docker, túneles, gráficas de métricas, APIs propias e incluso un terminal en vivo. - -
- -**Fragmentos y herramientas:** -Guarda los comandos que usas a menudo y lánzalos con un clic, con variables para el host y para lo que tú escribas. Ejecuta un mismo comando en todos los terminales abiertos y busca en tu historial con autocompletado. - - - -**Compartir sesión:** -Comparte en directo una sesión de terminal, RDP, VNC o Telnet. Manda un enlace al que cualquiera puede entrar sin cuenta, o compártela con un usuario concreto de Termix, en solo lectura o con escritura. Las comparticiones pueden caducar solas o revocarse, y se pueden desactivar globalmente o por host. - -
- -**Grabación y registros de sesión:** -Graba sesiones de terminal, RDP y VNC y reprodúcelas después. Descarga registros de texto de una sesión y mira el registro de conexión para ver exactamente qué pasó durante ella. - - - -**Conexiones serie:** -Habla con dispositivos serie como routers, switches y microcontroladores desde el navegador o la aplicación de escritorio. Ajusta velocidad, bits de datos, bits de parada y paridad. Usa la API Web Serial en los navegadores compatibles, o un backend nativo en la aplicación de escritorio. - -
- -**Tailscale:** -Trae dispositivos de tu tailnet para añadirlos como hosts en un par de clics, y conéctate con Tailscale SSH para que las ACL de tu tailnet controlen el acceso sin guardar credenciales. También funcionan Headscale y los puntos de acceso personalizados. - - - -**Proxmox:** -Importa hosts directamente desde una instancia de Proxmox y observa las estadísticas de nodos e invitados, incluidas CPU, memoria y almacenamiento, en su propia pestaña. - -
- -**Espacios de trabajo y pestañas:** -Guarda un conjunto de pestañas con su distribución dividida y reábrelo entero con un clic. Termix también recuerda tu última sesión, así que tus pestañas vuelven tras recargar y en otros dispositivos. - - - -**Línea de comandos:** -Un CLI `termix` para tu shell y tus scripts. Abre terminales, ejecuta un comando en un host o en una flota entera, mueve archivos por SFTP y gestiona hosts, fragmentos y credenciales. Instálalo con `npm install -g @termix-ssh/cli` o coge un binario independiente. Consulta la [documentación del CLI](https://docs.termix.site/cli). - -
- -**Seguridad:** -Las contraseñas, las claves y otros secretos se cifran por usuario, y los propios archivos de la base de datos se pueden cifrar en disco. Mira la [documentación](https://docs.termix.site/features/authentication/security/) para saber cómo funciona. - - - -**Idiomas:** -Unos 30 idiomas incluidos, gestionados a través de [Crowdin](https://docs.termix.site/translations). - -
- -
- -
-Más características -
- -- **Panel** - Tus servidores de un vistazo, con tarjetas que colocas tú -- **Gráfico de red** - Tu homelab dibujado a partir de tus hosts, con estado en vivo -- **Monitor de tmux** - Revisa sesiones, ventanas y paneles de tmux, con vista previa y búsqueda -- **Claves de API** - Claves por usuario con fecha de caducidad para scripts y CI -- **Exportar e importar** - Mueve hosts, credenciales y datos del gestor de archivos -- **SSL automático** - Certificados generados y renovados por ti, con redirección a HTTPS, o usa los tuyos -- **Bases de datos** - SQLite por defecto, y también PostgreSQL y MySQL -- **Interfaz moderna** - Una interfaz React limpia que funciona en escritorio y móvil, con temas como claro, oscuro y Dracula. Cualquier conexión se puede abrir a pantalla completa desde una URL -- **Paleta de comandos** - Pulsa dos veces Mayús izquierda para ir a un host desde el teclado -- **Atajos de teclado** - Moverte entre pestañas, cerrarlas y más, todo reasignable -- **Wake-on-LAN** - Enciende una máquina desde Termix o desde un paso de automatización -- **Proxy de confianza** - Deja que un proxy inverso gestione el acceso y pase al usuario -- **Marca blanca** - Los administradores pueden personalizar la instancia con su propio nombre, logo y colores -- **Puntos de acceso web** - Abre la propia interfaz web de un host, como la página de administración de un router, integrada dentro de Termix en lugar de en una pestaña aparte -- **Salas de colaboración** - Salas persistentes donde un grupo salta entre sesiones junto, con un escenario para presentador e invitaciones -- **SSH muy completo** - Hosts de salto, Warpgate, peticiones TOTP, SOCKS5, verificación de claves de host, autorrelleno de contraseñas, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, registro del terminal, reenvío de agente, agente SSH de Bitwarden, firma SSH con HashiCorp Vault, Step CA, 1Password Connect y más -- **Termix ID** - Una versión integrada de sshid.io. Reserva un identificador, publica tus claves públicas en una URL de resolución y emite certificados SSH desde la CA integrada -- **Autosugerencias del historial de comandos** - Sugerencias en línea en el terminal mientras escribes, basadas en tu historial de comandos -- **Escritorio independiente y sincronización** - La aplicación de escritorio funciona sola, con su propio backend y base de datos locales, y puede sincronizarse opcionalmente con un servidor Termix - -
- -
- -## Plataformas compatibles - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PlataformaDistribución
WebCualquier navegador moderno (Chrome, Safari, Firefox) · Compatible con PWA
Windows x64/ia32Portable · Instalador MSI · Chocolatey
Linux x64/ia32Portable · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## Instalación - -Visita la [documentación de Termix](https://docs.termix.site/install) para ver las instrucciones completas de instalación en todas las plataformas. - -¿Vas a desplegar en Kubernetes? El chart de Helm está en `charts/termix`, y las instrucciones -de configuración para Ingress, Traefik, Argo CD, GitHub Actions y GitLab CI están en -[docs.termix.site/install/server/kubernetes](https://docs.termix.site/install/server/kubernetes). - -Ejemplo de archivo Docker Compose (puedes quitar `guacd` y la red si no piensas usar el escritorio remoto): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### Línea de comandos - -Termix también tiene un CLI, para que gestiones tus servidores desde un terminal y uses Termix en tus propios scripts. - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -Puede abrir terminales, ejecutar un comando en un host o en una flota entera, mover archivos por SFTP y gestionar hosts, fragmentos y credenciales. La documentación completa está en [docs.termix.site/cli](https://docs.termix.site/cli). - -### Alojamiento en la nube - -Puedes ejecutar el servidor de Termix en un VPS en lugar de dentro de tu propia red. Si Termix corre en la red que gestiona, una caída se lo lleva por delante justo cuando lo necesitas para arreglar las cosas. Fuera se mantiene accesible, te da una IP fija y puedes entrar desde cualquier sitio sin VPN ni abrir puertos. - -[GINERNET](https://docs.termix.site/install/ginernet) patrocina Termix, y la documentación tiene una guía paso a paso para desplegar en su plataforma de VPS. - -
- -## Telemetría - -Termix envía una vez al día un pequeño informe anónimo para que pueda ver cuántas instancias hay funcionando y qué funciones se usan. Incluye un ID de instancia aleatorio, la versión de la app y cuántos usuarios y hosts tienes. También puede incluir tu plataforma (sistema operativo, arquitectura, versión de Node.js, tipo de base de datos y si usas Docker, la app de escritorio o un servidor normal), cuántas veces se abrió cada tipo de pestaña y cuántos inicios de sesión SSH hubo, y qué funciones integradas están activas. Nunca incluye nombres de usuario, nombres de host, direcciones IP, credenciales ni nada que te identifique a ti o a tus servidores. - -Viene activado. Puedes desactivarlo o elegir qué incluye en los ajustes de administración, en Usage Statistics, donde también puedes ver el informe exacto antes de enviarlo. Cada usuario puede excluir su propio uso de funciones desde su perfil. Poner `ENABLE_TELEMETRY=false` lo desactiva y bloquea el interruptor. - -
- -## Donar - -Termix es gratuito y de código abierto, sin suscripciones ni planes de pago. Si te resulta útil, considera donar para ayudar con los servidores, los dominios y el tiempo de desarrollo. Las donaciones también financian el tiempo de investigar y aprender lo necesario para funciones como SAML, Kubernetes y el soporte de agentes. Sigue el progreso y dona abajo. - -[Donar](https://donate.termix.site/) - -
- -## Patrocinadores - -¿Te interesa un espacio de pago para apoyar el desarrollo? Escribe a [mail@termix.site](mailto:mail@termix.site). - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## Soporte - -¿Necesitas ayuda o quieres pedir una función? Abre una [nueva incidencia](https://github.com/Termix-SSH/Support/issues) y añade todo el detalle que puedas, en inglés si te es posible. También puedes preguntar en el canal de soporte de [Discord](https://discord.gg/jVQGdvHDrf), aunque allí las respuestas pueden tardar más. - -
- -## Capturas de pantalla - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -Mira los resúmenes de las actualizaciones en YouTube - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -Algunos vídeos e imágenes pueden estar desactualizados o no mostrar del todo bien las funciones. - -
- -
- -## Características planeadas - -Todas las funciones planeadas están en [Projects](https://github.com/orgs/Termix-SSH/projects/5). Si quieres colaborar, consulta [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). - -
- -## Licencia - -Distribuido bajo la Licencia Apache versión 2.0. Consulta `LICENSE` para más información. diff --git a/docs/readme/README-FR.md b/docs/readme/README-FR.md deleted file mode 100644 index 017d9f51a..000000000 --- a/docs/readme/README-FR.md +++ /dev/null @@ -1,502 +0,0 @@ -
- -Termix Logo - -

Termix

- -

Gestion de serveurs auto-hébergée, du SSH au bureau à distance jusqu'aux automatisations

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix est gratuit et open source. S'il vous est utile, pensez à [faire un don](https://donate.termix.site/) pour aider à payer les serveurs et le temps de développement. - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- Obtenu le 1er septembre 2025 -

- -
- -
- -## Présentation - -Termix est une plateforme gratuite, open source et auto-hébergée pour gérer vos serveurs. Elle réunit au même endroit les terminaux SSH, les bureaux à distance (RDP, VNC, Telnet), les transferts de fichiers, les tunnels, Docker, les métriques et les automatisations, sur le web, le bureau et le mobile. C'est une alternative auto-hébergée à Termius, gratuite pour toujours. - -
- -## Fonctionnalités - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**Terminal SSH:** -Un vrai terminal avec des onglets façon navigateur et un écran divisé, jusqu'à 6 panneaux à la fois. Choisissez votre thème, votre police et vos couleurs. Une barre d'outils au-dessus de chaque session affiche le CPU, la mémoire et le disque en direct, avec des raccourcis vers les fichiers, Docker, les tunnels et les métriques de cet hôte. - - - -**Bureau à distance:** -RDP, VNC et Telnet dans le navigateur, en onglets et en écran divisé comme n'importe quelle autre session. Comprend un explorateur de fichiers pour les lecteurs RDP et l'envoi par glisser-déposer. Sur le bureau Windows, vous pouvez aussi ouvrir un hôte dans le client RDP natif. - -
- -**Tunnels SSH:** -Redirection locale, distante et SOCKS dynamique, avec reconnexion automatique et vérification de l'état. Les tunnels client vers serveur de l'application de bureau restent sur cette machine, et vous pouvez enregistrer des préréglages sur le serveur pour reprendre une configuration sur un autre poste. - - - -**Gestionnaire de fichiers:** -Parcourez, modifiez, envoyez, téléchargez, renommez, déplacez et supprimez des fichiers en SFTP, avec sudo. Affichez et modifiez du code, des images, de l'audio et de la vidéo. Copiez des fichiers directement d'un serveur à l'autre : le chemin le plus rapide est choisi pour vous et l'intégrité des transferts est vérifiée. - -
- -**Docker et Podman:** -Démarrez, arrêtez, mettez en pause et supprimez des conteneurs, suivez leurs statistiques et ouvrez un shell à l'intérieur. Fonctionne avec Docker comme avec Podman. Le but n'est pas de remplacer Portainer ou Dockge, juste de gérer les conteneurs que vous avez déjà. - - - -**Gestionnaire d'hôtes:** -Rangez vos hôtes avec des étiquettes et des dossiers imbriqués que vous pouvez nommer et colorer. Réutilisez des identifiants enregistrés sur plusieurs hôtes, déployez des clés SSH automatiquement, regroupez des hôtes sous un hôte parent, modifiez et exportez en lot, et utilisez la connexion rapide pour les connexions ponctuelles que vous ne voulez pas garder. - -
- -**Métriques des hôtes:** -CPU, mémoire, disque, réseau, température, GPU NVIDIA, temps de fonctionnement, processus, ports, connexions et informations système sur la plupart des serveurs Linux, avec des graphiques d'historique. Les cartes de gestion vous permettent de gérer les services, les tâches cron, les paquets, les utilisateurs, les règles de pare-feu, WireGuard, Tailscale, les certificats SSL, les journaux et les vérifications d'état sans quitter Termix. - - - -**Automatisations:** -Choisissez un déclencheur, puis dites ce qui doit se passer. Les déclencheurs peuvent être une métrique qui dépasse un seuil, un hôte qui tombe ou revient, une vérification d'état qui change, un horaire, un événement de conteneur ou un webhook entrant. Les étapes peuvent lancer des commandes et des extraits, piloter des conteneurs et des tunnels, réveiller un hôte, appeler une URL, attendre, se diviser selon une condition, lancer une autre automatisation et vous prévenir via ntfy, Discord ou un webhook. Les essais à blanc vous permettent de tester sans risque. - -
- -**Flottes:** -Regroupez des hôtes dans une flotte en les choisissant ou avec des règles d'étiquettes, pour que les nouveaux hôtes s'ajoutent tout seuls. Lancez une commande sur tous les hôtes d'un coup, envoyez et récupérez des fichiers sur l'ensemble, installez des paquets et collectez un inventaire de l'OS, du noyau, de l'architecture et du temps de fonctionnement. - - - -**Assistant IA:** -Optionnel, et désactivé tant que vous ne l'activez pas. Connectez OpenAI, Anthropic, Gemini, Ollama ou n'importe quel point d'accès compatible OpenAI et posez des questions sur votre installation. Il lit les hôtes, les flottes, les extraits et les alertes, et propose des changements que vous validez au lieu de les appliquer lui-même. Il ne peut jamais toucher aux identifiants, aux utilisateurs ni aux réglages. Les administrateurs peuvent le laisser désactivé pour toute l'instance, et vous pouvez le masquer pendant la configuration. - -
- -**Connexion et utilisateurs:** -Comptes locaux ainsi que connexion OIDC, LDAP, GitHub et Google, avec double authentification (TOTP), clés d'accès (WebAuthn) et appareils de confiance. Les administrateurs peuvent gérer les utilisateurs, associer les groupes OIDC aux rôles, voir toutes les sessions actives sur toutes les plateformes et les révoquer. Reliez vos comptes local et OIDC, et consultez le journal d'audit de ce que chacun a fait. - - - -**Rôles et partage:** -Créez des rôles et partagez des hôtes avec des utilisateurs ou des rôles selon quatre niveaux : connexion, lecture, modification et gestion. Cela fonctionne avec tous les types d'authentification et tous les protocoles, et vous pouvez remplacer les identifiants utilisés pour un hôte partagé. - -
- -**Alertes:** -Définissez des règles sur les métriques des hôtes comme le CPU, la mémoire et le disque, et recevez une notification via ntfy, Discord ou un webhook quand elles se déclenchent. Consultez les alertes en cours et résolues dans un historique, et écartez celles qui ne vous intéressent pas. - - - -**Page d'accueil:** -Une grille de widgets en glisser-déposer que vous construisez vous-même. Des widgets pour l'état des hôtes, les pings, les liens de services, les favoris, la recherche, les horloges, les calendriers, les comptes à rebours, les notes, les flux RSS, la météo, les images, les iframes, Docker, les tunnels, les graphiques de métriques, les API personnalisées et même un terminal en direct. - -
- -**Extraits et outils:** -Enregistrez les commandes que vous lancez souvent et exécutez-les en un clic, avec des variables pour l'hôte et vos propres saisies. Lancez une même commande dans tous les terminaux ouverts, et cherchez dans votre historique avec la complétion automatique. - - - -**Partage de session:** -Partagez en direct une session terminal, RDP, VNC ou Telnet. Envoyez un lien que n'importe qui peut rejoindre sans compte, ou partagez avec un utilisateur Termix précis, en lecture seule ou en lecture-écriture. Les partages peuvent expirer d'eux-mêmes ou être révoqués, et se désactivent globalement ou hôte par hôte. - -
- -**Enregistrement et journaux de session:** -Enregistrez les sessions terminal, RDP et VNC pour les revoir plus tard. Téléchargez les journaux d'une session en texte simple, et consultez le journal de connexion pour voir exactement ce qui s'est passé pendant une connexion. - - - -**Connexions série:** -Dialoguez avec des appareils série comme des routeurs, des commutateurs et des microcontrôleurs depuis le navigateur ou l'application de bureau. Réglez la vitesse, les bits de données, les bits d'arrêt et la parité. Utilise l'API Web Serial dans les navigateurs compatibles, ou un backend natif dans l'application de bureau. - -
- -**Tailscale:** -Récupérez les appareils de votre tailnet pour les ajouter comme hôtes en quelques clics, et connectez-vous avec Tailscale SSH pour que les ACL de votre tailnet gèrent les accès, sans stocker d'identifiants. Headscale et les points d'accès personnalisés fonctionnent aussi. - - - -**Proxmox:** -Importez des hôtes directement depuis une instance Proxmox, et suivez les statistiques des nœuds et des invités, dont le CPU, la mémoire et le stockage, dans un onglet dédié. - -
- -**Espaces de travail et onglets:** -Enregistrez un ensemble d'onglets avec leur disposition en écran divisé et rouvrez le tout en un clic. Termix retient aussi votre dernière session, donc vos onglets reviennent après un rafraîchissement ou sur un autre appareil. - - - -**Ligne de commande:** -Un CLI `termix` pour votre shell et vos scripts. Ouvrez des terminaux, lancez une commande sur un hôte ou une flotte entière, déplacez des fichiers en SFTP et gérez hôtes, extraits et identifiants. Installez-le avec `npm install -g @termix-ssh/cli` ou récupérez un binaire autonome. Voir la [documentation du CLI](https://docs.termix.site/cli). - -
- -**Sécurité:** -Les mots de passe, les clés et les autres secrets sont chiffrés par utilisateur, et les fichiers de base de données eux-mêmes peuvent être chiffrés sur le disque. Voir la [documentation](https://docs.termix.site/features/authentication/security/) pour le détail. - - - -**Langues:** -Une trentaine de langues intégrées, gérées via [Crowdin](https://docs.termix.site/translations). - -
- -
- -
-Plus de fonctionnalités -
- -- **Tableau de bord** - Vos serveurs en un coup d'œil, avec des cartes que vous rangez vous-même -- **Graphe réseau** - Votre homelab dessiné à partir de vos hôtes, avec l'état en direct -- **Moniteur tmux** - Parcourez les sessions, fenêtres et panneaux tmux, avec aperçus et recherche -- **Clés API** - Des clés par utilisateur avec date d'expiration, pour vos scripts et votre CI -- **Export et import** - Faites entrer et sortir hôtes, identifiants et données du gestionnaire de fichiers -- **SSL automatique** - Certificats générés et renouvelés pour vous, avec redirection HTTPS, ou apportez les vôtres -- **Bases de données** - SQLite par défaut, PostgreSQL et MySQL également pris en charge -- **Interface moderne** - Une interface React soignée qui marche sur ordinateur et mobile, avec des thèmes clair, sombre et Dracula. Chaque connexion peut s'ouvrir en plein écran depuis une URL -- **Palette de commandes** - Double appui sur Maj gauche pour rejoindre un hôte au clavier -- **Raccourcis clavier** - Naviguer entre les onglets, les fermer et plus encore, tout est reconfigurable -- **Wake-on-LAN** - Réveillez une machine depuis Termix ou depuis une étape d'automatisation -- **Authentification par proxy de confiance** - Laissez un reverse proxy gérer la connexion et transmettre l'utilisateur -- **Marque blanche** - Les administrateurs peuvent personnaliser l'instance avec leur propre nom, logo et couleurs -- **Points d'accès web** - Ouvrez l'interface web d'un hôte, comme la page d'administration d'un routeur, directement dans Termix plutôt que dans un onglet séparé -- **Salles de collaboration** - Des salles persistantes où un groupe peut passer d'une session à l'autre ensemble, avec une scène pour le présentateur et des invitations -- **SSH complet** - Hôtes de rebond, Warpgate, demandes TOTP, SOCKS5, vérification des clés d'hôte, remplissage automatique des mots de passe, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, journalisation du terminal, transfert d'agent, agent SSH Bitwarden, signature SSH HashiCorp Vault, Step CA, 1Password Connect et plus encore -- **Termix ID** - Une version intégrée de sshid.io. Réservez un identifiant, publiez vos clés publiques sur une URL de résolution et émettez des certificats SSH depuis l'autorité intégrée -- **Suggestions automatiques d'historique de commandes** - Des suggestions en ligne dans le terminal pendant que vous tapez, basées sur votre historique de commandes -- **Application de bureau autonome et synchronisation** - L'application de bureau fonctionne toute seule, avec son propre backend et sa base de données, et peut en option se synchroniser avec un serveur Termix - -
- -
- -## Plateformes prises en charge - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PlateformeDistribution
WebTout navigateur récent (Chrome, Safari, Firefox) · Compatible PWA
Windows x64/ia32Portable · Installeur MSI · Chocolatey
Linux x64/ia32Portable · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## Installation - -Consultez la [documentation Termix](https://docs.termix.site/install) pour les instructions d'installation complètes sur toutes les plateformes. - -Vous déployez sur Kubernetes ? Le chart Helm se trouve dans `charts/termix`, et les instructions de configuration couvrant Ingress, Traefik, Argo CD, GitHub Actions et GitLab CI sont sur [docs.termix.site/install/server/kubernetes](https://docs.termix.site/install/server/kubernetes). - -Exemple de fichier Docker Compose (vous pouvez retirer `guacd` et le réseau si vous ne comptez pas utiliser le bureau à distance) : - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### Ligne de commande - -Termix propose aussi un CLI, pour gérer vos serveurs depuis un terminal et utiliser Termix dans vos propres scripts. - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -Il peut ouvrir des terminaux, lancer une commande sur un hôte ou une flotte entière, déplacer des fichiers en SFTP et gérer hôtes, extraits et identifiants. La documentation complète est sur [docs.termix.site/cli](https://docs.termix.site/cli). - -### Hébergement cloud - -Vous pouvez faire tourner le serveur Termix sur un VPS plutôt que dans votre propre réseau. Si Termix tourne sur le réseau qu'il gère, une panne l'emporte avec elle, juste au moment où vous en avez besoin pour réparer. Ailleurs, il reste joignable, vous avez une IP fixe et vous pouvez y accéder de partout sans VPN ni redirection de port. - -[GINERNET](https://docs.termix.site/install/ginernet) sponsorise Termix, et la documentation contient un guide pas à pas pour déployer sur leur plateforme VPS. - -
- -## Télémétrie - -Termix envoie une fois par jour un petit rapport anonyme, pour que je puisse voir combien d'instances tournent et quelles fonctionnalités sont utilisées. Il contient un identifiant d'instance aléatoire, la version de l'application et le nombre d'utilisateurs et d'hôtes. Il peut aussi inclure votre plateforme (système, architecture, version de Node.js, type de base de données, et si vous utilisez Docker, l'application de bureau ou un simple serveur), combien de fois chaque type d'onglet a été ouvert et combien de connexions SSH ont eu lieu, ainsi que les fonctionnalités intégrées actives. Il ne contient jamais de noms d'utilisateur, de noms d'hôte, d'adresses IP, d'identifiants ni quoi que ce soit qui permette de vous identifier, vous ou vos serveurs. - -C'est activé par défaut. Désactivez-le ou choisissez ce qu'il contient dans les paramètres d'administration, section Usage Statistics, où vous pouvez aussi voir le rapport exact avant l'envoi. Chaque utilisateur peut exclure sa propre utilisation dans son profil. Définir `ENABLE_TELEMETRY=false` le désactive et verrouille l'interrupteur. - -
- -## Faire un don - -Termix est gratuit et open source, sans abonnement ni offre payante. S'il vous est utile, pensez à faire un don pour aider à couvrir les serveurs, les noms de domaine et le temps de développement. Les dons financent aussi le temps de recherche nécessaire pour construire des fonctionnalités comme SAML, Kubernetes et le support des agents. Suivez l'avancement et faites un don ci-dessous. - -[Faire un don](https://donate.termix.site/) - -
- -## Sponsors - -Intéressé par un emplacement payant pour soutenir le développement ? Écrivez à [mail@termix.site](mailto:mail@termix.site). - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## Support - -Besoin d'aide ou envie de proposer une fonctionnalité ? Ouvrez un [nouveau ticket](https://github.com/Termix-SSH/Support/issues) avec le plus de détails possible, en anglais si vous le pouvez. Vous pouvez aussi demander dans le canal support sur [Discord](https://discord.gg/jVQGdvHDrf), même si les réponses y prennent parfois plus de temps. - -
- -## Captures d'écran - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -Regarder les présentations des mises à jour sur YouTube - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -Certaines vidéos et images peuvent être dépassées ou ne pas montrer parfaitement les fonctionnalités. - -
- -
- -## Fonctionnalités prévues - -Toutes les fonctionnalités prévues sont dans [Projects](https://github.com/orgs/Termix-SSH/projects/5). Si vous souhaitez contribuer, voir [Contribuer](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). - -
- -## Licence - -Distribué sous licence Apache version 2.0. Voir `LICENSE` pour plus d'informations. diff --git a/docs/readme/README-HI.md b/docs/readme/README-HI.md deleted file mode 100644 index 9652c12f4..000000000 --- a/docs/readme/README-HI.md +++ /dev/null @@ -1,509 +0,0 @@ -
- -Termix Logo - -

Termix

- -

सेल्फ-होस्टेड सर्वर प्रबंधन, SSH और रिमोट डेस्कटॉप से लेकर ऑटोमेशन तक

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix मुफ़्त और ओपन सोर्स है। अगर यह आपके काम आता है, तो सर्वर की लागत और विकास के समय में मदद के लिए [दान](https://donate.termix.site/) करने पर विचार करें। - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- 1 सितंबर 2025 को हासिल किया गया -

- -
- -
- -## अवलोकन - -Termix आपके सर्वर संभालने के लिए एक मुफ़्त, ओपन सोर्स, सेल्फ-होस्टेड प्लेटफ़ॉर्म है। यह SSH टर्मिनल, रिमोट डेस्कटॉप (RDP, VNC, Telnet), फ़ाइल ट्रांसफ़र, टनल, Docker, मेट्रिक्स और ऑटोमेशन को एक ही जगह लाता है, वेब, डेस्कटॉप और मोबाइल पर। यह Termius का सेल्फ-होस्टेड विकल्प है जो हमेशा मुफ़्त रहेगा। - -
- -## विशेषताएँ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**SSH टर्मिनल:** -ब्राउज़र जैसे टैब और स्प्लिट स्क्रीन वाला पूरा टर्मिनल, एक साथ 6 पैनल तक। थीम, फ़ॉन्ट और रंग आप खुद चुनें। हर सत्र के ऊपर एक टूलबार रहता है जिसमें CPU, मेमोरी और डिस्क लाइव दिखते हैं, साथ ही उस होस्ट की फ़ाइलों, Docker, टनल और मेट्रिक्स तक जाने के शॉर्टकट भी। - - - -**रिमोट डेस्कटॉप:** -ब्राउज़र में RDP, VNC और Telnet, बाकी सत्रों की तरह टैब और स्प्लिट स्क्रीन में। इसमें RDP ड्राइव के लिए फ़ाइल ब्राउज़र और खींचकर छोड़ने वाला अपलोड भी है। Windows डेस्कटॉप पर आप होस्ट को सिस्टम के अपने RDP क्लाइंट में भी खोल सकते हैं। - -
- -**SSH टनल:** -लोकल, रिमोट और डायनामिक SOCKS फ़ॉरवर्डिंग, अपने आप दोबारा जुड़ने और स्थिति जाँच के साथ। डेस्कटॉप ऐप के क्लाइंट से सर्वर वाले टनल उसी मशीन पर रहते हैं, और आप सेटिंग सर्वर पर सहेजकर उसे दूसरी मशीन पर ले जा सकते हैं। - - - -**फ़ाइल मैनेजर:** -SFTP से फ़ाइलें देखें, संपादित करें, अपलोड और डाउनलोड करें, नाम बदलें, हटाएँ और खिसकाएँ, sudo के साथ भी। कोड, तस्वीरें, ऑडियो और वीडियो देखें और बदलें। फ़ाइलें सीधे एक सर्वर से दूसरे पर कॉपी करें, सबसे तेज़ रास्ता अपने आप चुना जाता है और ट्रांसफ़र की जाँच भी होती है। - -
- -**Docker और Podman:** -कंटेनर चालू करें, रोकें, थामें और हटाएँ, उनके आँकड़े देखें, और किसी एक के अंदर शेल खोलें। Docker और Podman दोनों के साथ चलता है। यह Portainer या Dockge की जगह लेने के लिए नहीं है, सिर्फ़ आपके पहले से मौजूद कंटेनर संभालने के लिए है। - - - -**होस्ट मैनेजर:** -टैग और नाम व रंग वाले नेस्टेड फ़ोल्डर से होस्ट सहेजें और व्यवस्थित करें। सहेजे गए क्रेडेंशियल कई होस्ट पर दोबारा इस्तेमाल करें, SSH कुंजियाँ अपने आप भेजें, होस्ट को किसी मुख्य होस्ट के नीचे रखें, एक साथ कई में बदलाव करें और निर्यात करें, और जिन कनेक्शनों को सहेजना नहीं चाहते उनके लिए क्विक कनेक्ट इस्तेमाल करें। - -
- -**होस्ट मेट्रिक्स:** -ज़्यादातर Linux सर्वर पर CPU, मेमोरी, डिस्क, नेटवर्क, तापमान, अपटाइम, प्रोसेस, पोर्ट, लॉगिन और सिस्टम जानकारी, पुराने आँकड़ों के ग्राफ़ के साथ। मैनेजर कार्ड से आप सर्विस, cron कार्य, पैकेज, उपयोगकर्ता, फ़ायरवॉल नियम, WireGuard, Tailscale, SSL प्रमाणपत्र, लॉग और हेल्थ चेक Termix छोड़े बिना संभाल सकते हैं। - - - -**ऑटोमेशन:** -पहले एक ट्रिगर चुनें, फिर बताएँ कि क्या होना चाहिए। ट्रिगर में किसी मेट्रिक का तय सीमा पार करना, होस्ट का बंद होना या वापस आना, हेल्थ चेक का बदलना, कोई तय समय, कंटेनर की कोई घटना, या आने वाला webhook शामिल है। कदमों में कमांड और स्निपेट चलाना, कंटेनर और टनल संभालना, मशीन जगाना, कोई URL बुलाना, इंतज़ार करना, शर्त के हिसाब से रास्ता बदलना, दूसरा ऑटोमेशन चलाना, और ntfy, Discord या webhook से आपको बताना शामिल है। टेस्ट रन से आप पहले सुरक्षित तरीके से आज़मा सकते हैं। - -
- -**फ़्लीट:** -होस्ट चुनकर या टैग नियमों से एक फ़्लीट बनाएँ, ताकि नए होस्ट अपने आप जुड़ जाएँ। एक ही कमांड सभी होस्ट पर एक साथ चलाएँ, सब पर फ़ाइलें भेजें और उनसे लाएँ, पैकेज इंस्टॉल करें, और OS, कर्नेल, आर्किटेक्चर और अपटाइम की सूची इकट्ठा करें। - - - -**AI सहायक:** -यह वैकल्पिक है और जब तक आप खुद चालू न करें, बंद रहता है। OpenAI, Anthropic, Gemini, Ollama या OpenAI के अनुरूप कोई भी एंडपॉइंट जोड़ें और अपने सेटअप के बारे में पूछें। यह होस्ट, फ़्लीट, स्निपेट और अलर्ट पढ़ सकता है, और बदलाव खुद करने के बजाय आपकी मंज़ूरी के लिए सुझाता है। यह क्रेडेंशियल, उपयोगकर्ताओं या सेटिंग्स तक कभी नहीं पहुँच सकता। एडमिन इसे पूरे इंस्टेंस के लिए बंद रख सकते हैं, और आप इसे शुरुआती सेटअप में ही छिपा सकते हैं। - -
- -**लॉगिन और उपयोगकर्ता:** -लोकल खातों के साथ OIDC, LDAP, GitHub और Google से लॉगिन, और दो चरणों वाला सत्यापन (TOTP), पासकी (WebAuthn) तथा भरोसेमंद डिवाइस। एडमिन उपयोगकर्ताओं को संभाल सकते हैं, OIDC समूहों को भूमिकाओं से जोड़ सकते हैं, हर प्लेटफ़ॉर्म पर चालू सत्र देख और रद्द कर सकते हैं। अपने लोकल और OIDC खाते आपस में जोड़ें, और ऑडिट लॉग में देखें कि किसने क्या किया। - - - -**भूमिकाएँ और साझाकरण:** -भूमिकाएँ बनाएँ और होस्ट को उपयोगकर्ताओं या भूमिकाओं के साथ चार स्तरों पर साझा करें: कनेक्ट, देखना, बदलना और प्रबंधन। यह हर तरह के प्रमाणीकरण और हर प्रोटोकॉल के साथ चलता है, और साझा होस्ट के लिए इस्तेमाल होने वाले क्रेडेंशियल आप बदल भी सकते हैं। - -
- -**अलर्ट:** -CPU, मेमोरी और डिस्क जैसी होस्ट मेट्रिक्स पर नियम लगाएँ, और उनके चलने पर ntfy, Discord या webhook से सूचना पाएँ। चल रहे और ठीक हो चुके अलर्ट इतिहास में देखें, और जो आपके काम के नहीं उन्हें हटा दें। - - - -**होमपेज:** -खींचकर छोड़ने वाला विजेट ग्रिड जिसे आप खुद बनाते हैं। होस्ट की स्थिति, पिंग, सर्विस लिंक, बुकमार्क, खोज, घड़ियाँ, कैलेंडर, उलटी गिनती, नोट्स, RSS, मौसम, तस्वीरें, iframe, Docker, टनल, मेट्रिक्स के ग्राफ़, अपने API और यहाँ तक कि चालू टर्मिनल तक के विजेट मौजूद हैं। - -
- -**स्निपेट और उपकरण:** -जो कमांड आप बार-बार चलाते हैं उन्हें सहेजें और एक क्लिक में चलाएँ, होस्ट और अपने इनपुट के लिए वेरिएबल के साथ। एक ही कमांड सभी खुले टर्मिनलों पर चलाएँ, और अपने कमांड इतिहास में ऑटो-कंप्लीट के साथ खोजें। - - - -**सत्र साझा करना:** -चालू टर्मिनल, RDP, VNC या Telnet सत्र लाइव साझा करें। ऐसा लिंक भेजें जिससे कोई भी बिना खाते के जुड़ सके, या किसी खास Termix उपयोगकर्ता के साथ साझा करें, सिर्फ़ देखने या लिखने की अनुमति के साथ। साझाकरण अपने आप खत्म हो सकता है या कभी भी रद्द किया जा सकता है, और इसे पूरी तरह या हर होस्ट के लिए अलग से बंद किया जा सकता है। - -
- -**सत्र रिकॉर्डिंग और लॉग:** -टर्मिनल, RDP और VNC सत्र रिकॉर्ड करें और बाद में देखें। सत्र के सादे टेक्स्ट लॉग डाउनलोड करें, और कनेक्शन लॉग देखकर जानें कि जुड़ते समय असल में क्या हुआ। - - - -**सीरियल कनेक्शन:** -राउटर, स्विच और माइक्रोकंट्रोलर जैसे सीरियल उपकरणों से ब्राउज़र या डेस्कटॉप ऐप से बात करें। बॉड रेट, डेटा बिट, स्टॉप बिट और पैरिटी सेट करें। सहयोगी ब्राउज़रों में Web Serial API और डेस्कटॉप ऐप में मूल बैकएंड इस्तेमाल होता है। - -
- -**Tailscale:** -अपने tailnet से डिवाइस लाकर कुछ ही क्लिक में होस्ट के रूप में जोड़ें, और Tailscale SSH से जुड़ें ताकि पहुँच का काम आपके tailnet के ACL संभालें और कोई क्रेडेंशियल सहेजना न पड़े। Headscale और अपने एंडपॉइंट भी चलते हैं। - - - -**Proxmox:** -होस्ट सीधे किसी Proxmox इंस्टेंस से लाएँ, और नोड तथा गेस्ट के आँकड़े, जिनमें CPU, मेमोरी और स्टोरेज शामिल हैं, अलग टैब में देखें। - -
- -**वर्कस्पेस और टैब:** -टैब का एक सेट उनके स्प्लिट लेआउट के साथ सहेजें और पूरा का पूरा एक क्लिक में फिर खोलें। Termix आपका पिछला सत्र भी याद रखता है, इसलिए पेज रीफ़्रेश करने पर और दूसरे डिवाइस पर भी आपके टैब लौट आते हैं। - - - -**निर्देशित सेटअप:** -एक छोटा सा सेटअप आपको इंटरफ़ेस प्रीसेट, थीम, मनचाही सुविधाएँ और पहला होस्ट चुनने में मदद करता है। सरल मोड वह सब छिपा देता है जो आप इस्तेमाल नहीं करते, और आप सेटअप दोबारा चला सकते हैं या प्रीसेट कभी भी बदल सकते हैं। - -
- -**स्वतंत्र डेस्कटॉप ऐप और सिंक:** -डेस्कटॉप ऐप अपने लोकल बैकएंड और डेटाबेस के साथ बिना किसी सर्वर के अकेले चलता है। आप इसे किसी Termix सर्वर से जोड़कर होस्ट, क्रेडेंशियल, स्निपेट वगैरह दोनों तरफ़ सिंक कर सकते हैं, और चुन सकते हैं कि कनेक्शन आपकी मशीन से शुरू हों या सर्वर के रास्ते। - - - -**कमांड लाइन:** -आपके शेल और स्क्रिप्ट के लिए `termix` CLI। टर्मिनल खोलें, किसी एक होस्ट या पूरे फ़्लीट पर कमांड चलाएँ, SFTP से फ़ाइलें भेजें, और होस्ट, स्निपेट व क्रेडेंशियल संभालें। `npm install -g @termix-ssh/cli` से इंस्टॉल करें या अलग बाइनरी लें। [CLI दस्तावेज़](https://docs.termix.site/cli) देखें। - -
- -**सुरक्षा:** -पासवर्ड, कुंजियाँ और बाकी गोपनीय जानकारी हर उपयोगकर्ता के लिए अलग से एन्क्रिप्ट होती है, और डेटाबेस फ़ाइलें भी डिस्क पर एन्क्रिप्ट की जा सकती हैं। यह कैसे काम करता है, यह [दस्तावेज़](https://docs.termix.site/features/authentication/security/) में देखें। - - - -**भाषाएँ:** -लगभग 30 भाषाएँ पहले से मौजूद हैं, जिन्हें [Crowdin](https://docs.termix.site/translations) से संभाला जाता है। - -
- -
- -
-और भी विशेषताएँ -
- -- **डैशबोर्ड** - आपके सर्वर एक नज़र में, ऐसे कार्ड के साथ जिन्हें आप खुद जमाते हैं -- **नेटवर्क ग्राफ़** - आपके होस्ट से बना आपका होमलैब का नक्शा, लाइव स्थिति के साथ -- **tmux मॉनिटर** - tmux के सत्र, विंडो और पैन देखें, झलक और खोज के साथ -- **API कुंजियाँ** - स्क्रिप्ट और CI के लिए, समाप्ति तिथि वाली उपयोगकर्ता-विशिष्ट कुंजियाँ -- **निर्यात और आयात** - होस्ट, क्रेडेंशियल और फ़ाइल मैनेजर का डेटा अंदर-बाहर ले जाएँ -- **अपने आप SSL** - प्रमाणपत्र आपके लिए बनते और नवीनीकृत होते हैं, HTTPS रीडायरेक्ट के साथ, या अपने खुद के लगाएँ -- **डेटाबेस** - डिफ़ॉल्ट रूप से SQLite, साथ में PostgreSQL और MySQL भी -- **आधुनिक इंटरफ़ेस** - साफ़ सुथरा React इंटरफ़ेस जो डेस्कटॉप और मोबाइल दोनों पर चलता है, लाइट, डार्क और Dracula जैसी थीम के साथ। कोई भी कनेक्शन URL से पूरी स्क्रीन में खुल सकता है -- **कमांड पैलेट** - बाईं Shift दो बार दबाकर कीबोर्ड से सीधे किसी होस्ट पर जाएँ -- **कीबोर्ड शॉर्टकट** - टैब बदलना, बंद करना और बहुत कुछ, सब दोबारा तय किए जा सकते हैं -- **Wake-on-LAN** - किसी मशीन को Termix से या ऑटोमेशन के किसी कदम से जगाएँ -- **भरोसेमंद प्रॉक्सी से लॉगिन** - रिवर्स प्रॉक्सी को लॉगिन संभालने दें और उपयोगकर्ता की जानकारी आगे भेजने दें -- **भरपूर SSH सुविधाएँ** - जंप होस्ट, Warpgate, TOTP पूछना, SOCKS5, होस्ट कुंजी की जाँच, पासवर्ड अपने आप भरना, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, पोर्ट नॉकिंग, टर्मिनल लॉग, एजेंट फ़ॉरवर्डिंग, Bitwarden SSH एजेंट, HashiCorp Vault से SSH हस्ताक्षर और भी बहुत कुछ -- **Termix ID** - sshid.io जैसा अपना बना हुआ इंतज़ाम। एक नाम लें, अपनी सार्वजनिक कुंजियाँ एक रिज़ॉल्वर URL पर रखें, और अंदर मौजूद CA से SSH प्रमाणपत्र जारी करें - -
- -
- -## प्लेटफ़ॉर्म सपोर्ट - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
प्लेटफ़ॉर्मवितरण
Webकोई भी आधुनिक ब्राउज़र (Chrome, Safari, Firefox) · PWA सपोर्ट
Windows x64/ia32पोर्टेबल · MSI इंस्टॉलर · Chocolatey
Linux x64/ia32पोर्टेबल · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## इंस्टॉलेशन - -सभी प्लेटफ़ॉर्म पर पूरी इंस्टॉलेशन जानकारी के लिए [Termix दस्तावेज़](https://docs.termix.site/install) देखें। - -Docker Compose फ़ाइल का नमूना (अगर आप रिमोट डेस्कटॉप इस्तेमाल नहीं करने वाले तो `guacd` और नेटवर्क वाला हिस्सा हटा सकते हैं): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### कमांड लाइन - -Termix में CLI भी है, ताकि आप टर्मिनल से अपने सर्वर संभाल सकें और Termix को अपनी स्क्रिप्ट में इस्तेमाल कर सकें। - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -यह टर्मिनल खोल सकता है, एक होस्ट या पूरे फ़्लीट पर कमांड चला सकता है, SFTP से फ़ाइलें ले जा सकता है, और होस्ट, स्निपेट व क्रेडेंशियल संभाल सकता है। पूरा दस्तावेज़ [docs.termix.site/cli](https://docs.termix.site/cli) पर है। - -### क्लाउड होस्टिंग - -आप Termix सर्वर को अपने नेटवर्क के बजाय किसी VPS पर भी चला सकते हैं। अगर Termix उसी नेटवर्क पर चल रहा है जिसे वह संभालता है, तो गड़बड़ी होने पर वह भी साथ ही बंद हो जाएगा, ठीक उसी वक्त जब आपको उसे ठीक करने के लिए चाहिए। बाहर चलाने पर वह हमेशा पहुँच में रहता है, एक स्थिर IP देता है, और बिना VPN या पोर्ट फ़ॉरवर्ड के कहीं से भी पहुँच मिलती है। - -[GINERNET](https://docs.termix.site/install/ginernet) Termix को प्रायोजित करता है, और दस्तावेज़ में उनके VPS प्लेटफ़ॉर्म पर तैनाती की कदम-दर-कदम गाइड मौजूद है। - -
- -## टेलीमेट्री - -Termix दिन में एक बार एक छोटी गुमनाम रिपोर्ट भेजता है ताकि मैं देख सकूं कि कितने इंस्टेंस चल रहे हैं और कौन से फ़ीचर इस्तेमाल हो रहे हैं। इसमें एक रैंडम इंस्टेंस ID, ऐप का वर्शन, और आपके यूज़र्स और होस्ट्स की संख्या होती है। इसमें आपका प्लेटफ़ॉर्म (OS, आर्किटेक्चर, Node.js वर्शन, डेटाबेस का प्रकार, और आप Docker, डेस्कटॉप ऐप या सामान्य सर्वर इस्तेमाल करते हैं), हर तरह का टैब कितनी बार खोला गया और कितने SSH लॉगिन हुए, और कौन से बिल्ट-इन फ़ीचर चल रहे हैं, यह भी हो सकता है। इसमें कभी भी यूज़रनेम, होस्टनेम, IP पते, क्रेडेंशियल या ऐसी कोई चीज़ नहीं होती जिससे आपकी या आपके सर्वर की पहचान हो। - -यह डिफ़ॉल्ट रूप से चालू रहता है। इसे एडमिन सेटिंग्स में Usage Statistics के अंतर्गत बंद करें या चुनें कि इसमें क्या शामिल हो, और वहीं भेजी जाने वाली रिपोर्ट पहले से देख भी सकते हैं। हर यूज़र अपनी प्रोफ़ाइल में अपना फ़ीचर इस्तेमाल हटा सकता है। `ENABLE_TELEMETRY=false` सेट करने से यह बंद हो जाता है और स्विच लॉक हो जाता है। - -
- -## दान करें - -Termix मुफ़्त और ओपन सोर्स है, न कोई सदस्यता है न कोई पेड प्लान। अगर यह आपके काम आता है, तो सर्वर, डोमेन और विकास के समय में मदद के लिए दान करने पर विचार करें। दान से SAML, Kubernetes और एजेंट सपोर्ट जैसी सुविधाएँ बनाने के लिए ज़रूरी शोध और सीखने का समय भी मिलता है। नीचे प्रगति देखें और दान करें। - -[दान करें](https://donate.termix.site/) - -
- -## प्रायोजक - -विकास में सहयोग के लिए पेड प्लेसमेंट में रुचि है? [mail@termix.site](mailto:mail@termix.site) पर ईमेल करें। - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## सहायता - -मदद चाहिए या कोई सुविधा माँगनी है? एक [नया issue](https://github.com/Termix-SSH/Support/issues) खोलें और जितना हो सके विस्तार से लिखें, हो सके तो अंग्रेज़ी में। आप [Discord](https://discord.gg/jVQGdvHDrf) के सपोर्ट चैनल में भी पूछ सकते हैं, हालाँकि वहाँ जवाब आने में ज़्यादा समय लग सकता है। - -
- -## स्क्रीनशॉट - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -YouTube पर अपडेट की जानकारी देखें - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -कुछ वीडियो और तस्वीरें पुरानी हो सकती हैं या सुविधाओं को पूरी तरह नहीं दिखा पातीं। - -
- -
- -## नियोजित विशेषताएँ - -सभी नियोजित सुविधाएँ [Projects](https://github.com/orgs/Termix-SSH/projects/5) में हैं। अगर आप योगदान देना चाहते हैं, तो [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) देखें। - -
- -## लाइसेंस - -Apache License संस्करण 2.0 के तहत वितरित। अधिक जानकारी के लिए `LICENSE` देखें। diff --git a/docs/readme/README-IT.md b/docs/readme/README-IT.md deleted file mode 100644 index 5044739a7..000000000 --- a/docs/readme/README-IT.md +++ /dev/null @@ -1,506 +0,0 @@ -
- -Termix Logo - -

Termix

- -

Gestione dei server self-hosted, da SSH e desktop remoto fino alle automazioni

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix è gratuito e open source. Se ti è utile, valuta una [donazione](https://donate.termix.site/) per aiutare a coprire i costi dei server e il tempo di sviluppo. - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- Ottenuto il 1 settembre 2025 -

- -
- -
- -## Panoramica - -Termix è una piattaforma gratuita, open source e self-hosted per gestire i tuoi server. Mette in un unico posto terminali SSH, desktop remoti (RDP, VNC, Telnet), trasferimenti di file, tunnel, Docker, metriche e automazioni, su web, desktop e mobile. È un'alternativa self-hosted a Termius che resta gratuita per sempre. - -
- -## Funzionalità - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**Terminale SSH:** -Un terminale completo con schede come quelle del browser e schermo diviso, fino a 6 pannelli insieme. Scegli tema, carattere e colori. Sopra ogni sessione c'è una barra con CPU, memoria e disco in tempo reale, più scorciatoie ai file, a Docker, ai tunnel e alle metriche di quell'host. - - - -**Desktop remoto:** -RDP, VNC e Telnet nel browser, in schede e schermo diviso come qualsiasi altra sessione. Include un browser dei file per le unità RDP e il caricamento trascinando i file. Sul desktop Windows puoi anche aprire un host nel client RDP nativo. - -
- -**Tunnel SSH:** -Inoltro locale, remoto e SOCKS dinamico, con riconnessione automatica e controlli di stato. I tunnel da client a server dell'app desktop restano su quella macchina, e puoi salvare delle preimpostazioni sul server per portare una configurazione su un altro client. - - - -**Gestore file:** -Sfoglia, modifica, carica, scarica, rinomina, sposta ed elimina file via SFTP, anche con sudo. Guarda e modifica codice, immagini, audio e video. Copia i file direttamente da un server all'altro in una scheda dedicata di trasferimento, con il percorso più veloce scelto per te e i trasferimenti controllati per integrità. L'app desktop ha anche una vista affiancata di locale e remoto. - -
- -**Docker e Podman:** -Avvia, ferma, metti in pausa ed elimina i container, guarda le loro statistiche e apri una shell dentro uno di essi. Funziona sia con Docker sia con Podman. Non vuole sostituire Portainer o Dockge, serve solo a gestire i container che hai già. - - - -**Gestore host:** -Salva e organizza gli host con etichette e cartelle annidate a cui puoi dare nome e colore. Riutilizza le credenziali salvate su più host, distribuisci le chiavi SSH in automatico, raggruppa gli host sotto un host padre, modifica ed esporta in blocco, e usa la connessione rapida per i collegamenti una tantum che non vuoi salvare. - -
- -**Metriche host:** -CPU, memoria, disco, rete, temperatura, GPU NVIDIA, tempo di accensione, processi, porte, accessi e informazioni di sistema sulla maggior parte dei server Linux, con grafici storici. Le schede di gestione ti fanno seguire servizi, cron, pacchetti, utenti, regole del firewall, WireGuard, Tailscale, certificati SSL, log e controlli di stato senza uscire da Termix. - - - -**Automazioni:** -Scegli un evento che fa partire tutto, poi decidi cosa deve succedere. Gli eventi possono essere una metrica che supera una soglia, un host che cade o torna su, un controllo di stato che cambia, una pianificazione, un evento di un container o un webhook in arrivo. I passaggi possono eseguire comandi e frammenti, gestire container e tunnel, accendere un host, chiamare un URL, aspettare, seguire una condizione, avviare un'altra automazione e avvisarti via ntfy, Discord o webhook. Le prove a vuoto ti permettono di provare senza rischi. - -
- -**Flotte:** -Raggruppa gli host in una flotta scegliendoli o con regole sulle etichette, così i nuovi host entrano da soli. Esegui un comando su tutti gli host in una volta, invia e recupera file su tutti quanti, installa pacchetti e raccogli un inventario di sistema operativo, kernel, architettura e tempo di accensione. - - - -**Assistente IA:** -È opzionale e resta spento finché non lo accendi tu. Collega OpenAI, Anthropic, Gemini, Ollama o qualsiasi endpoint compatibile con OpenAI e fai domande sulla tua installazione. Legge host, flotte, frammenti e avvisi, e propone modifiche da approvare invece di farle da solo. Non può mai toccare credenziali, utenti o impostazioni. Gli amministratori possono lasciarlo spento per tutta l'istanza, e tu puoi nasconderlo già durante la configurazione. - -
- -**Accesso e utenti:** -Account locali più accesso con OIDC, LDAP, GitHub e Google, con doppia autenticazione (TOTP), passkey (WebAuthn) e dispositivi fidati. Gli amministratori possono gestire gli utenti, collegare i gruppi OIDC ai ruoli, vedere tutte le sessioni attive su ogni piattaforma e revocarle. Collega il tuo account locale a quello OIDC e consulta il registro di controllo di quello che ha fatto ognuno. - - - -**Ruoli e condivisione:** -Crea ruoli e condividi gli host con utenti o ruoli su quattro livelli: connessione, visualizzazione, modifica e gestione. Funziona con ogni tipo di autenticazione e ogni protocollo, e puoi cambiare le credenziali usate per un host condiviso. - -
- -**Avvisi:** -Imposta regole sulle metriche degli host come CPU, memoria e disco, e ricevi una notifica via ntfy, Discord o webhook quando scattano. Guarda gli avvisi attivi e quelli rientrati in uno storico, e scarta quelli che non ti interessano. - - - -**Pagina iniziale:** -Una griglia di widget che costruisci tu trascinandoli. Ci sono widget per stato degli host, ping, collegamenti ai servizi, segnalibri, ricerca, orologi, calendari, conti alla rovescia, note, RSS, meteo, immagini, iframe, Docker, tunnel, grafici delle metriche, API personalizzate e perfino un terminale dal vivo. - -
- -**Frammenti e strumenti:** -Salva i comandi che usi spesso e lanciali con un clic, con variabili per l'host e per quello che scrivi tu. Esegui uno stesso comando su tutti i terminali aperti e cerca nella cronologia con il completamento automatico. - - - -**Condivisione sessione:** -Condividi dal vivo una sessione di terminale, RDP, VNC o Telnet. Manda un link a cui chiunque può accedere senza account, oppure condividi con un utente Termix preciso, in sola lettura o anche in scrittura. Le condivisioni possono scadere da sole o essere revocate, e si possono spegnere per tutti o per singolo host. - -
- -**Registrazione e log delle sessioni:** -Registra le sessioni di terminale, RDP e VNC e riguardale dopo. Scarica i log di testo di una sessione e consulta il registro delle connessioni per vedere esattamente cosa è successo durante una connessione. - - - -**Connessioni seriali:** -Parla con dispositivi seriali come router, switch e microcontrollori dal browser o dall'app desktop. Imposta velocità, bit di dati, bit di stop e parità. Usa l'API Web Serial nei browser compatibili, oppure un backend nativo nell'app desktop. - -
- -**Tailscale:** -Prendi i dispositivi dalla tua tailnet per aggiungerli come host in pochi clic, e collegati con Tailscale SSH così gli ACL della tailnet gestiscono l'accesso senza salvare credenziali. Funzionano anche Headscale e gli endpoint personalizzati. - - - -**Proxmox:** -Importa gli host direttamente da un'istanza Proxmox e segui le statistiche di nodi e macchine ospiti, comprese CPU, memoria e spazio, in una scheda dedicata. - -
- -**Spazi di lavoro e schede:** -Salva un insieme di schede con la loro disposizione divisa e riapri tutto con un clic. Termix ricorda anche l'ultima sessione, così le schede tornano dopo un ricaricamento e su altri dispositivi. - - - -**Riga di comando:** -Una CLI `termix` per la tua shell e i tuoi script. Apri terminali, esegui un comando su un host o su un'intera flotta, sposta file via SFTP e gestisci host, frammenti e credenziali. Installala con `npm install -g @termix-ssh/cli` oppure prendi un binario autonomo. Vedi la [documentazione della CLI](https://docs.termix.site/cli). - -
- -**Sicurezza:** -Password, chiavi e altri segreti sono cifrati per ogni utente, e gli stessi file del database possono essere cifrati su disco. Guarda la [documentazione](https://docs.termix.site/features/authentication/security/) per capire come funziona. - - - -**Lingue:** -Circa 30 lingue incluse, gestite tramite [Crowdin](https://docs.termix.site/translations). - -
- -
- -
-Altre funzionalità -
- -- **Dashboard** - I tuoi server a colpo d'occhio, con schede che disponi tu -- **Grafico di rete** - Il tuo homelab disegnato a partire dagli host, con stato in tempo reale -- **Monitor tmux** - Sfoglia sessioni, finestre e pannelli di tmux, con anteprime e ricerca -- **Chiavi API** - Chiavi per singolo utente con scadenza, per script e CI -- **Esporta e importa** - Sposta host, credenziali e dati del gestore file dentro e fuori -- **SSL automatico** - Certificati generati e rinnovati per te, con reindirizzamento a HTTPS, oppure usa i tuoi -- **Database** - SQLite di base, con supporto anche per PostgreSQL e MySQL -- **Interfaccia moderna** - Interfaccia React pulita che funziona su desktop e mobile, con temi come chiaro, scuro e Dracula. Ogni connessione si può aprire a schermo intero da un URL -- **Palette comandi** - Premi due volte Maiusc sinistro per saltare a un host da tastiera -- **Scorciatoie da tastiera** - Spostarsi tra le schede, chiuderle e altro, tutto riassegnabile -- **Wake-on-LAN** - Accendi una macchina da Termix o da un passaggio di un'automazione -- **Autenticazione tramite proxy fidato** - Lascia che un reverse proxy gestisca l'accesso e passi l'utente -- **White label** - Gli amministratori possono rimarchiare l'istanza con nome, logo e colori propri -- **Endpoint web** - Apri l'interfaccia web di un host, come la pagina di amministrazione di un router, incorporata dentro Termix invece che in una scheda separata -- **Stanze di collaborazione** - Stanze persistenti dove un gruppo può passare insieme da una sessione all'altra, con un palco per il presentatore e inviti -- **SSH molto completo** - Host di salto, Warpgate, richieste TOTP, SOCKS5, verifica delle chiavi host, riempimento automatico della password, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, log del terminale, inoltro dell'agente, agente SSH di Bitwarden, firma SSH con HashiCorp Vault, Step CA, 1Password Connect e altro -- **Termix ID** - Una versione integrata di sshid.io. Prendi un identificativo, pubblica le tue chiavi pubbliche su un URL di risoluzione ed emetti certificati SSH dalla CA integrata -- **Suggerimenti automatici nella cronologia comandi** - Suggerimenti in linea nel terminale mentre scrivi, basati sulla tua cronologia comandi -- **Desktop autonomo e sincronizzazione** - L'app desktop funziona da sola, con backend e database locali, e può facoltativamente sincronizzarsi con un server Termix - -
- -
- -## Piattaforme supportate - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PiattaformaDistribuzione
WebQualsiasi browser recente (Chrome, Safari, Firefox) · Supporto PWA
Windows x64/ia32Portatile · Installer MSI · Chocolatey
Linux x64/ia32Portatile · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## Installazione - -Vai alla [documentazione di Termix](https://docs.termix.site/install) per le istruzioni complete di installazione su tutte le piattaforme. - -Vuoi installare su Kubernetes? Il chart Helm è in `charts/termix`, e le istruzioni di configurazione -per Ingress, Traefik, Argo CD, GitHub Actions e GitLab CI sono su -[docs.termix.site/install/server/kubernetes](https://docs.termix.site/install/server/kubernetes). - -Esempio di file Docker Compose (puoi togliere `guacd` e la rete se non pensi di usare il desktop remoto): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### Riga di comando - -Termix ha anche una CLI, così puoi gestire i tuoi server dal terminale e usare Termix nei tuoi script. - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -Può aprire terminali, eseguire un comando su un host o su un'intera flotta, spostare file via SFTP e gestire host, frammenti e credenziali. La documentazione completa è su [docs.termix.site/cli](https://docs.termix.site/cli). - -### Hosting in cloud - -Puoi far girare il server Termix su un VPS invece che dentro la tua rete. Se Termix gira sulla rete che gestisce, un guasto se lo porta via proprio quando ti servirebbe per sistemare le cose. Farlo girare altrove lo mantiene raggiungibile, ti dà un IP fisso e ti permette di entrare da ovunque senza VPN né porte aperte. - -[GINERNET](https://docs.termix.site/install/ginernet) sponsorizza Termix, e nella documentazione c'è una guida passo passo per il rilascio sulla loro piattaforma VPS. - -
- -## Telemetria - -Termix invia una volta al giorno un piccolo report anonimo, così posso vedere quante istanze sono attive e quali funzioni vengono usate. Contiene un ID di istanza casuale, la versione dell'app e quanti utenti e host hai. Può includere anche la tua piattaforma (sistema operativo, architettura, versione di Node.js, tipo di database e se usi Docker, l'app desktop o un server normale), quante volte è stato aperto ogni tipo di scheda e quanti login SSH ci sono stati, e quali funzioni integrate sono attive. Non contiene mai nomi utente, nomi host, indirizzi IP, credenziali o altro che identifichi te o i tuoi server. - -È attivo di base. Puoi spegnerlo o scegliere cosa include nelle impostazioni di amministrazione, sezione Usage Statistics, dove puoi anche vedere il report esatto prima dell'invio. Ogni utente può escludere il proprio uso delle funzioni dal suo profilo. Impostare `ENABLE_TELEMETRY=false` lo spegne e blocca l'interruttore. - -
- -## Dona - -Termix è gratuito e open source, senza abbonamenti né piani a pagamento. Se ti è utile, valuta una donazione per aiutare con server, domini e tempo di sviluppo. Le donazioni finanziano anche il tempo per studiare quello che serve a costruire funzionalità come SAML, Kubernetes e il supporto agli agenti. Segui i progressi e dona qui sotto. - -[Dona](https://donate.termix.site/) - -
- -## Sponsor - -Ti interessa uno spazio a pagamento per sostenere lo sviluppo? Scrivi a [mail@termix.site](mailto:mail@termix.site). - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## Supporto - -Ti serve aiuto o vuoi proporre una funzionalità? Apri una [nuova issue](https://github.com/Termix-SSH/Support/issues) con più dettagli possibile, in inglese se ci riesci. Puoi anche chiedere nel canale di supporto su [Discord](https://discord.gg/jVQGdvHDrf), anche se lì le risposte possono richiedere più tempo. - -
- -## Screenshot - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -Guarda le panoramiche degli aggiornamenti su YouTube - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -Alcuni video e immagini possono essere datati o non mostrare al meglio le funzionalità. - -
- -
- -## Funzionalità pianificate - -Tutte le funzionalità pianificate sono su [Projects](https://github.com/orgs/Termix-SSH/projects/5). Se vuoi contribuire, guarda [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). - -
- -## Licenza - -Distribuito con licenza Apache versione 2.0. Vedi `LICENSE` per maggiori informazioni. - - diff --git a/docs/readme/README-JA.md b/docs/readme/README-JA.md deleted file mode 100644 index 0a9ec9ede..000000000 --- a/docs/readme/README-JA.md +++ /dev/null @@ -1,505 +0,0 @@ -
- -Termix Logo - -

Termix

- -

SSH やリモートデスクトップから自動化まで、セルフホストのサーバー管理

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix は無料でオープンソースです。役に立ったと感じたら、サーバー費用と開発時間を支えるために[寄付](https://donate.termix.site/)をご検討ください。 - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- 2025年9月1日 達成 -

- -
- -
- -## 概要 - -Termix は無料でオープンソースの、セルフホスト型サーバー管理プラットフォームです。SSH ターミナル、リモートデスクトップ(RDP、VNC、Telnet)、ファイル転送、トンネル、Docker、メトリクス、自動化をひとつにまとめ、ウェブ、デスクトップ、モバイルで使えます。ずっと無料で使える、セルフホスト版の Termius 代替です。 - -
- -## 機能 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**SSH ターミナル:** -ブラウザのようなタブと分割画面を備えた本格的なターミナルで、最大 6 分割まで同時に表示できます。テーマ、フォント、配色は自由に選べます。各セッションの上のツールバーには CPU、メモリ、ディスクの状況がリアルタイムで表示され、そのホストのファイル、Docker、トンネル、メトリクスへすぐ移動できます。 - - - -**リモートデスクトップ:** -RDP、VNC、Telnet をブラウザから利用でき、他のセッションと同じようにタブや分割画面で扱えます。RDP ドライブ用のファイルブラウザとドラッグ&ドロップのアップロードも使えます。Windows のデスクトップ版では、ホストをネイティブの RDP クライアントで開くこともできます。 - -
- -**SSH トンネル:** -ローカル、リモート、ダイナミック SOCKS の転送に対応し、自動再接続とヘルスチェックが付いています。デスクトップ版のクライアント間トンネルはその端末に保存され、プリセットをサーバーに保存しておけば別の端末に設定を移せます。 - - - -**ファイルマネージャー:** -SFTP でファイルの閲覧、編集、アップロード、ダウンロード、名前変更、移動、削除ができ、sudo にも対応しています。コード、画像、音声、動画を表示・編集できます。専用の転送タブでサーバー間から直接ファイルをコピーでき、最速の経路が自動で選ばれ、転送の整合性も検証されます。デスクトップ版にはローカルとリモートを並べて表示する画面もあります。 - -
- -**Docker と Podman:** -コンテナの起動、停止、一時停止、削除ができ、状態を確認したり、中でシェルを開いたりできます。Docker と Podman のどちらでも動きます。Portainer や Dockge を置き換えるためのものではなく、すでにあるコンテナを扱うためのものです。 - - - -**ホスト管理:** -タグと、名前や色を付けられる入れ子のフォルダでホストを整理できます。保存した認証情報を複数のホストで使い回し、SSH 鍵を自動で配布し、ホストを親ホストの下にまとめ、一括編集やエクスポートができます。保存したくない一度きりの接続にはクイック接続が使えます。 - -
- -**ホストメトリクス:** -たいていの Linux サーバーで CPU、メモリ、ディスク、ネットワーク、温度、NVIDIA GPU、稼働時間、プロセス、ポート、ログイン、システム情報を履歴グラフ付きで確認できます。マネージャーカードを使えば、サービス、cron、パッケージ、ユーザー、ファイアウォール、WireGuard、Tailscale、SSL 証明書、ログ、ヘルスチェックを Termix から離れずに扱えます。 - - - -**自動化:** -きっかけを選んで、何をするかを決めるだけです。きっかけには、メトリクスがしきい値を超えたとき、ホストが上がったり落ちたりしたとき、ヘルスチェックの状態が変わったとき、スケジュール、コンテナのイベント、外部からの Webhook があります。ステップではコマンドやスニペットの実行、コンテナやトンネルの操作、ホストの起動、URL の呼び出し、待機、条件分岐、別の自動化の実行ができ、ntfy、Discord、Webhook で通知できます。テスト実行で安全に試せます。 - -
- -**フリート:** -ホストを選ぶか、タグのルールを決めてフリートにまとめると、新しいホストは自動で入ります。すべてのホストで同じコマンドを一度に実行し、全台にファイルを配ったり集めたり、パッケージを入れたり、OS、カーネル、アーキテクチャ、稼働時間の一覧を集められます。 - - - -**AI アシスタント:** -任意の機能で、自分で有効にするまでは動きません。OpenAI、Anthropic、Gemini、Ollama、または OpenAI 互換のエンドポイントにつないで、自分の環境について質問できます。ホスト、フリート、スニペット、アラートを読み取れますが、変更は自分で行わず、承認してもらうための提案として出します。認証情報、ユーザー、設定には決して触れられません。管理者はインスタンス全体で無効にでき、初期設定で非表示にすることもできます。 - -
- -**ログインとユーザー:** -ローカルアカウントに加えて OIDC、LDAP、GitHub、Google でのサインインに対応し、2 要素認証(TOTP)、パスキー(WebAuthn)、信頼済みデバイスも使えます。管理者はユーザーの管理、OIDC グループとロールの対応付け、全プラットフォームのアクティブなセッションの確認と失効ができます。ローカルと OIDC のアカウントを連携でき、誰が何をしたかは監査ログで確認できます。 - - - -**ロールと共有:** -ロールを作り、接続、閲覧、編集、管理という 4 段階でホストをユーザーやロールに共有できます。すべての認証方式とすべてのプロトコルで使え、共有したホストで使う認証情報を上書きすることもできます。 - -
- -**アラート:** -CPU、メモリ、ディスクなどのホストメトリクスにルールを設定し、発報したら ntfy、Discord、Webhook で通知を受け取れます。発報中と解消済みのアラートは履歴で確認でき、気にしないものは消しておけます。 - - - -**ホームページ:** -自分で組み立てるドラッグ&ドロップのウィジェット画面です。ホストの状態、Ping、サービスリンク、ブックマーク、検索、時計、カレンダー、カウントダウン、メモ、RSS、天気、画像、iframe、Docker、トンネル、メトリクスのグラフ、独自 API、さらにはライブのターミナルまでウィジェットとして置けます。 - -
- -**スニペットとツール:** -よく使うコマンドを保存して、ワンクリックで実行できます。ホストの値や自分で入力する値を変数として使えます。開いているすべてのターミナルで同じコマンドをまとめて実行でき、コマンド履歴も補完付きで検索できます。 - - - -**セッション共有:** -ターミナル、RDP、VNC、Telnet のセッションをリアルタイムで共有できます。アカウントなしで参加できるリンクを送るか、特定の Termix ユーザーと共有し、閲覧のみか操作可能かを選べます。共有は自動で期限切れにも、いつでも取り消しにもでき、全体またはホストごとにオフにできます。 - -
- -**セッション録画とログ:** -ターミナル、RDP、VNC のセッションを録画して、あとから再生できます。セッションのテキストログをダウンロードでき、接続ログを見れば接続中に何が起きたかがそのまま分かります。 - - - -**シリアル接続:** -ルーター、スイッチ、マイコンなどのシリアル機器に、ブラウザやデスクトップアプリから接続できます。ボーレート、データビット、ストップビット、パリティを設定できます。対応ブラウザでは Web Serial API を、デスクトップアプリではネイティブのバックエンドを使います。 - -
- -**Tailscale:** -tailnet から端末を取り込んで数クリックでホストとして追加でき、Tailscale SSH で接続すればアクセス制御は tailnet の ACL に任せられ、認証情報を保存する必要がありません。Headscale や独自のエンドポイントにも対応しています。 - - - -**Proxmox:** -Proxmox のインスタンスからそのままホストを取り込めます。ノードやゲストの CPU、メモリ、ストレージなどの状態を専用のタブで確認できます。 - -
- -**ワークスペースとタブ:** -タブと分割レイアウトのセットを保存して、ワンクリックでまるごと開き直せます。Termix は前回のセッションも覚えているので、再読み込みしても端末を変えてもタブは戻ってきます。 - - - -**コマンドラインツール:** -シェルやスクリプトから使える `termix` CLI です。ターミナルを開き、ホスト 1 台またはフリート全体でコマンドを実行し、SFTP でファイルを移動し、ホストやスニペット、認証情報を管理できます。`npm install -g @termix-ssh/cli` で入れるか、単体のバイナリを使ってください。詳しくは [CLI ドキュメント](https://docs.termix.site/cli)をご覧ください。 - -
- -**セキュリティ:** -パスワードや鍵などの秘密情報はユーザーごとに暗号化され、データベースのファイル自体もディスク上で暗号化できます。仕組みは[ドキュメント](https://docs.termix.site/features/authentication/security/)をご覧ください。 - - - -**多言語対応:** -約 30 言語に対応しており、[Crowdin](https://docs.termix.site/translations) で管理しています。 - -
- -
- -
-その他の機能 -
- -- **ダッシュボード** - 自分で並べたカードでサーバーの状況をひと目で把握 -- **ネットワーク図** - ホストからホームラボを図にして、状態をリアルタイム表示 -- **tmux モニター** - tmux のセッション、ウィンドウ、ペインをプレビューと検索付きで一覧 -- **API キー** - スクリプトや CI 用の、有効期限付きユーザー単位のキー -- **エクスポートとインポート** - ホスト、認証情報、ファイルマネージャーのデータを出し入れ -- **自動 SSL** - 証明書の発行と更新、HTTPS へのリダイレクトを自動で。自前の証明書も使えます -- **データベース** - 標準は SQLite、PostgreSQL と MySQL にも対応 -- **モダンな UI** - デスクトップでもモバイルでも使える React の画面。ライト、ダーク、Dracula などのテーマ付き。どの接続も URL からフルスクリーンで開けます -- **コマンドパレット** - 左 Shift の 2 回押しで、キーボードからホストへ移動 -- **キーボードショートカット** - タブの移動や閉じる操作など、すべて割り当て変更可能 -- **Wake-on-LAN** - Termix からでも自動化のステップからでもマシンを起動 -- **信頼済みプロキシ認証** - リバースプロキシにサインインを任せ、ユーザー情報を引き継ぎ -- **ホワイトラベル** - 管理者が自分の名前、ロゴ、配色でインスタンスをブランド変更できます -- **Web エンドポイント** - ルーターの管理画面のようなホスト自身の Web UI を、別タブではなく Termix の中に埋め込んで開けます -- **コラボレーションルーム** - グループでセッションを行き来できる常設ルーム。プレゼンターの表示や招待機能もあります -- **充実した SSH 機能** - 踏み台ホスト、Warpgate、TOTP の入力、SOCKS5、ホスト鍵の検証、パスワードの自動入力、[OPKSSH](https://github.com/openpubkey/opkssh)、tmux、ポートノッキング、ターミナルのログ、エージェント転送、Bitwarden SSH エージェント、HashiCorp Vault の SSH 署名、Step CA、1Password Connect など -- **Termix ID** - sshid.io のような仕組みを内蔵。ハンドルを取得し、公開鍵をリゾルバー URL で公開し、内蔵 CA から SSH 証明書を発行できます -- **コマンド履歴の自動候補** - ターミナルに入力すると、コマンド履歴をもとにしたインライン候補が表示されます -- **デスクトップ単体利用と同期** - デスクトップアプリはローカルのバックエンドとデータベースを持ち単体でも動き、Termix サーバーと同期することもできます - -
- -
- -## プラットフォーム対応 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
プラットフォーム配布形式
Web最近のブラウザ全般(Chrome、Safari、Firefox)· PWA 対応
Windows x64/ia32ポータブル · MSI インストーラー · Chocolatey
Linux x64/ia32ポータブル · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## インストール - -すべてのプラットフォーム向けの詳しいインストール手順は [Termix ドキュメント](https://docs.termix.site/install)をご覧ください。 - -Kubernetes にデプロイしますか?Helm chart は `charts/termix` にあり、 -Ingress、Traefik、Argo CD、GitHub Actions、GitLab CI を扱うセットアップ手順は -[docs.termix.site/install/server/kubernetes](https://docs.termix.site/install/server/kubernetes) にあります。 - -Docker Compose の例です(リモートデスクトップ機能を使わないなら `guacd` とネットワークの部分は省略できます): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### コマンドラインツール - -Termix には CLI もあるので、ターミナルからサーバーを管理したり、自分のスクリプトに組み込んだりできます。 - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -ターミナルを開き、ホスト 1 台またはフリート全体でコマンドを実行し、SFTP でファイルを移動し、ホストやスニペット、認証情報を管理できます。詳しい説明は [docs.termix.site/cli](https://docs.termix.site/cli) にあります。 - -### クラウドでの運用 - -Termix のサーバーは自分のネットワーク内ではなく、VPS で動かすこともできます。管理対象のネットワーク上で動かしていると、障害が起きたときに Termix も一緒に落ちてしまい、直したいときに限って使えなくなります。外で動かしておけばいつでも届きますし、固定 IP も手に入り、VPN やポート開放なしでどこからでも入れます。 - -[GINERNET](https://docs.termix.site/install/ginernet) は Termix のスポンサーで、同社の VPS へデプロイする手順はドキュメントに詳しく載っています。 - -
- -## テレメトリー - -Termix は 1 日に 1 回、小さな匿名レポートを送信します。これで、どれだけのインスタンスが動いていて、どの機能が使われているかを把握しています。内容はランダムなインスタンス ID、アプリのバージョン、ユーザー数とホスト数です。さらに、プラットフォーム(OS、アーキテクチャ、Node.js のバージョン、データベースの種類、Docker・デスクトップアプリ・通常のサーバーのどれで動いているか)、各種タブが開かれた回数と SSH ログイン数、動作中の組み込み機能を含めることもできます。ユーザー名、ホスト名、IP アドレス、認証情報など、あなたやサーバーを特定できる情報は一切含みません。 - -初期状態では有効です。管理設定の「Usage Statistics」から止めたり、含める内容を選んだりでき、送信されるレポートをそのまま事前に確認することもできます。各ユーザーはプロフィールで自分の機能利用データを除外できます。`ENABLE_TELEMETRY=false` を設定すると無効になり、スイッチもロックされます。 - -
- -## 寄付 - -Termix は無料でオープンソースで、サブスクリプションも有料プランもありません。役に立っていると感じたら、サーバー代、ドメイン、開発時間を支えるための寄付をご検討ください。寄付は SAML、Kubernetes、エージェント対応といった機能を作るための調査や学習の時間にもあてられます。進み具合の確認と寄付は下記からどうぞ。 - -[寄付する](https://donate.termix.site/) - -
- -## スポンサー - -有料掲載で開発を支援することにご興味がありますか。[mail@termix.site](mailto:mail@termix.site) までご連絡ください。 - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## サポート - -困ったときや機能の要望があるときは、[新しい issue](https://github.com/Termix-SSH/Support/issues) を作って、できるだけ詳しく、できれば英語で書いてください。[Discord](https://discord.gg/jVQGdvHDrf) のサポートチャンネルでも質問できますが、返信には時間がかかることがあります。 - -
- -## スクリーンショット - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -YouTube でアップデートの紹介を見る - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -動画や画像は古くなっていたり、機能を十分に伝えられていない場合があります。 - -
- -
- -## 予定されている機能 - -予定されている機能はすべて [Projects](https://github.com/orgs/Termix-SSH/projects/5) にあります。貢献をお考えの方は[コントリビューションガイド](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)をご覧ください。 - -
- -## ライセンス - -Apache License 2.0 のもとで配布しています。詳しくは `LICENSE` をご覧ください。 - diff --git a/docs/readme/README-KO.md b/docs/readme/README-KO.md deleted file mode 100644 index 53ac1d4d4..000000000 --- a/docs/readme/README-KO.md +++ /dev/null @@ -1,502 +0,0 @@ -
- -Termix Logo - -

Termix

- -

SSH와 원격 데스크톱부터 자동화까지, 셀프 호스팅 서버 관리

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix는 무료이며 오픈 소스입니다. 유용하게 쓰고 계시다면 서버 비용과 개발 시간에 보탬이 되도록 [후원](https://donate.termix.site/)을 고려해 주세요. - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- 2025년 9월 1일 달성 -

- -
- -
- -## 개요 - -Termix는 무료 오픈 소스 셀프 호스팅 서버 관리 플랫폼입니다. SSH 터미널, 원격 데스크톱(RDP, VNC, Telnet), 파일 전송, 터널, Docker, 지표, 자동화를 한곳에 모아 웹과 데스크톱, 모바일에서 쓸 수 있습니다. 계속 무료로 쓸 수 있는 셀프 호스팅 Termius 대안입니다. - -
- -## 기능 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**SSH 터미널:** -브라우저 같은 탭과 분할 화면을 갖춘 제대로 된 터미널로, 한 번에 최대 6개 패널까지 띄울 수 있습니다. 테마와 글꼴, 색을 골라 쓸 수 있습니다. 각 세션 위의 툴바에는 CPU, 메모리, 디스크가 실시간으로 표시되고, 해당 호스트의 파일과 Docker, 터널, 지표로 바로 갈 수 있습니다. - - - -**원격 데스크톱:** -브라우저에서 RDP와 VNC, Telnet을 쓸 수 있고 다른 세션과 똑같이 탭과 분할 화면으로 다룰 수 있습니다. RDP 드라이브용 파일 브라우저와 끌어다 놓기 업로드도 있습니다. Windows 데스크톱에서는 호스트를 기본 RDP 클라이언트로 열 수도 있습니다. - -
- -**SSH 터널:** -로컬과 원격, 동적 SOCKS 포워딩을 지원하며 자동 재연결과 상태 확인이 붙어 있습니다. 데스크톱 앱의 클라이언트 대 서버 터널은 그 컴퓨터에 저장되고, 프리셋을 서버에 저장해 두면 다른 컴퓨터로 설정을 옮길 수 있습니다. - - - -**파일 관리자:** -SFTP로 파일을 살펴보고 편집하고 올리고 내려받고 이름을 바꾸고 옮기고 지울 수 있으며 sudo도 됩니다. 코드와 이미지, 오디오, 비디오를 보고 편집할 수 있습니다. 서버에서 서버로 파일을 바로 복사할 수 있는데, 가장 빠른 경로가 자동으로 선택되고 전송 무결성도 확인합니다. - -
- -**Docker와 Podman:** -컨테이너를 시작하고 멈추고 일시 정지하고 지울 수 있으며, 상태를 보거나 안에서 셸을 열 수 있습니다. Docker와 Podman 모두에서 동작합니다. Portainer나 Dockge를 대신하려는 것이 아니라, 이미 있는 컨테이너를 다루기 위한 것입니다. - - - -**호스트 관리:** -태그와, 이름과 색을 붙일 수 있는 중첩 폴더로 호스트를 정리합니다. 저장한 자격 증명을 여러 호스트에서 다시 쓰고, SSH 키를 자동으로 배포하고, 호스트를 상위 호스트 아래로 묶고, 한꺼번에 편집하거나 내보낼 수 있습니다. 저장하고 싶지 않은 일회성 연결에는 빠른 연결을 쓰면 됩니다. - -
- -**호스트 지표:** -대부분의 리눅스 서버에서 CPU, 메모리, 디스크, 네트워크, 온도, NVIDIA GPU, 가동 시간, 프로세스, 포트, 로그인, 시스템 정보를 기록 그래프와 함께 볼 수 있습니다. 관리 카드로 서비스와 cron 작업, 패키지, 사용자, 방화벽 규칙, WireGuard, Tailscale, SSL 인증서, 로그, 상태 확인을 Termix 안에서 처리할 수 있습니다. - - - -**자동화:** -먼저 조건을 고르고, 무슨 일이 일어날지 정하면 됩니다. 조건에는 지표가 기준을 넘을 때, 호스트가 올라오거나 내려갈 때, 상태 확인 결과가 바뀔 때, 정해진 일정, 컨테이너 이벤트, 들어오는 웹훅이 있습니다. 각 단계에서 명령과 스니펫을 실행하고, 컨테이너와 터널을 조작하고, 호스트를 깨우고, URL을 호출하고, 기다리고, 조건에 따라 갈라지고, 다른 자동화를 실행하고, ntfy나 Discord, 웹훅으로 알릴 수 있습니다. 테스트 실행으로 먼저 안전하게 시험해 볼 수 있습니다. - -
- -**플릿:** -호스트를 직접 고르거나 태그 규칙으로 플릿에 묶으면 새 호스트는 알아서 들어옵니다. 모든 호스트에서 같은 명령을 한 번에 실행하고, 전부에 파일을 보내고 가져오고, 패키지를 설치하고, OS와 커널, 아키텍처, 가동 시간 목록을 모을 수 있습니다. - - - -**AI 어시스턴트:** -선택 기능이며 직접 켜기 전까지는 꺼져 있습니다. OpenAI, Anthropic, Gemini, Ollama 또는 OpenAI 호환 엔드포인트를 연결해 내 환경에 대해 물어볼 수 있습니다. 호스트와 플릿, 스니펫, 알림을 읽을 수 있지만 직접 바꾸지 않고 승인받을 제안으로 내놓습니다. 자격 증명과 사용자, 설정에는 절대 접근할 수 없습니다. 관리자는 인스턴스 전체에서 꺼 둘 수 있고, 초기 설정에서 아예 숨길 수도 있습니다. - -
- -**로그인과 사용자:** -로컬 계정과 함께 OIDC, LDAP, GitHub, Google 로그인을 지원하고 2단계 인증(TOTP), 패스키(WebAuthn), 신뢰할 수 있는 기기도 쓸 수 있습니다. 관리자는 사용자를 관리하고, OIDC 그룹을 역할에 연결하고, 모든 플랫폼의 활성 세션을 보고 해지할 수 있습니다. 로컬 계정과 OIDC 계정을 연결할 수 있고, 누가 무엇을 했는지는 감사 로그에서 확인합니다. - - - -**역할과 공유:** -역할을 만들고 연결, 보기, 편집, 관리라는 네 단계로 호스트를 사용자나 역할에 공유할 수 있습니다. 모든 인증 방식과 모든 프로토콜에서 동작하며, 공유한 호스트에 쓸 자격 증명을 따로 지정할 수도 있습니다. - -
- -**알림:** -CPU와 메모리, 디스크 같은 호스트 지표에 규칙을 걸어 두고 조건이 걸리면 ntfy나 Discord, 웹훅으로 알림을 받습니다. 발생 중인 알림과 해제된 알림을 기록에서 보고, 신경 쓰지 않을 것은 지워 둘 수 있습니다. - - - -**홈페이지:** -직접 꾸미는 끌어다 놓기 위젯 화면입니다. 호스트 상태, 핑, 서비스 링크, 북마크, 검색, 시계, 달력, 카운트다운, 메모, RSS, 날씨, 이미지, iframe, Docker, 터널, 지표 차트, 사용자 API, 심지어 살아 있는 터미널까지 위젯으로 놓을 수 있습니다. - -
- -**스니펫과 도구:** -자주 쓰는 명령을 저장해 두고 한 번에 실행할 수 있으며, 호스트 값이나 직접 넣는 값을 변수로 쓸 수 있습니다. 열려 있는 모든 터미널에서 같은 명령을 한꺼번에 실행할 수 있고, 명령 기록도 자동 완성으로 찾을 수 있습니다. - - - -**세션 공유:** -터미널과 RDP, VNC, Telnet 세션을 실시간으로 공유합니다. 계정 없이 들어올 수 있는 링크를 보내거나 특정 Termix 사용자와 공유할 수 있고, 보기만 할지 조작까지 할지 고를 수 있습니다. 공유는 알아서 만료되게 하거나 언제든 취소할 수 있고, 전체 또는 호스트별로 꺼 둘 수 있습니다. - -
- -**세션 녹화와 로그:** -터미널과 RDP, VNC 세션을 녹화해 두었다가 나중에 다시 볼 수 있습니다. 세션의 텍스트 로그를 내려받을 수 있고, 연결 로그를 보면 연결하는 동안 무슨 일이 있었는지 그대로 알 수 있습니다. - - - -**시리얼 연결:** -라우터와 스위치, 마이크로컨트롤러 같은 시리얼 장치에 브라우저나 데스크톱 앱에서 접속할 수 있습니다. 보드레이트와 데이터 비트, 스톱 비트, 패리티를 설정할 수 있습니다. 지원되는 브라우저에서는 Web Serial API를, 데스크톱 앱에서는 네이티브 백엔드를 씁니다. - -
- -**Tailscale:** -tailnet에서 기기를 가져와 몇 번의 클릭으로 호스트로 추가하고, Tailscale SSH로 접속하면 접근 권한은 tailnet ACL이 처리하므로 자격 증명을 저장할 필요가 없습니다. Headscale과 사용자 지정 엔드포인트도 됩니다. - - - -**Proxmox:** -Proxmox 인스턴스에서 호스트를 바로 가져오고, 노드와 게스트의 CPU와 메모리, 스토리지 상태를 전용 탭에서 볼 수 있습니다. - -
- -**워크스페이스와 탭:** -탭과 분할 배치를 통째로 저장해 두고 한 번에 다시 열 수 있습니다. Termix는 마지막 세션도 기억하기 때문에 새로 고침을 하거나 기기를 바꿔도 탭이 그대로 돌아옵니다. - - - -**명령줄 도구:** -셸과 스크립트에서 쓰는 `termix` CLI입니다. 터미널을 열고, 호스트 하나나 플릿 전체에서 명령을 실행하고, SFTP로 파일을 옮기고, 호스트와 스니펫, 자격 증명을 관리할 수 있습니다. `npm install -g @termix-ssh/cli`로 설치하거나 단독 실행 파일을 받으면 됩니다. [CLI 문서](https://docs.termix.site/cli)를 참고하세요. - -
- -**보안:** -비밀번호와 키를 비롯한 비밀 정보는 사용자별로 암호화되고, 데이터베이스 파일 자체도 디스크에서 암호화할 수 있습니다. 어떻게 동작하는지는 [문서](https://docs.termix.site/features/authentication/security/)에서 볼 수 있습니다. - - - -**언어:** -약 30개 언어가 기본으로 들어 있으며 [Crowdin](https://docs.termix.site/translations)으로 관리합니다. - -
- -
- -
-더 많은 기능 -
- -- **대시보드** - 직접 배치한 카드로 서버 상태를 한눈에 -- **네트워크 그래프** - 호스트를 바탕으로 홈랩을 그려 주고 상태를 실시간 표시 -- **tmux 모니터** - tmux 세션과 창, 페인을 미리보기와 검색으로 살펴보기 -- **API 키** - 스크립트와 CI용, 만료일이 있는 사용자별 키 -- **내보내기와 가져오기** - 호스트와 자격 증명, 파일 관리자 데이터를 옮기기 -- **자동 SSL** - 인증서 발급과 갱신, HTTPS 리다이렉트를 알아서. 직접 만든 인증서도 쓸 수 있습니다 -- **데이터베이스** - 기본은 SQLite, PostgreSQL과 MySQL도 지원 -- **현대적인 UI** - 데스크톱과 모바일에서 모두 쓸 수 있는 깔끔한 React 화면. 라이트와 다크, Dracula 같은 테마 제공. 어떤 연결이든 URL로 전체 화면에서 열 수 있습니다 -- **명령 팔레트** - 왼쪽 Shift를 두 번 눌러 키보드로 호스트로 이동 -- **키보드 단축키** - 탭 이동과 닫기 등, 모두 다시 지정할 수 있습니다 -- **Wake-on-LAN** - Termix에서도, 자동화 단계에서도 컴퓨터를 켜기 -- **신뢰할 수 있는 프록시 인증** - 리버스 프록시가 로그인을 처리하고 사용자 정보를 넘겨주기 -- **화이트 라벨** - 관리자가 인스턴스를 자기 이름과 로고, 색으로 다시 꾸밀 수 있습니다 -- **웹 엔드포인트** - 라우터 관리 페이지 같은 호스트의 웹 UI를 별도 탭이 아니라 Termix 안에 그대로 띄우기 -- **협업 룸** - 발표자 화면과 초대 기능을 갖춘, 여러 명이 함께 세션을 오가는 상시 룸 -- **풍부한 SSH 기능** - 점프 호스트, Warpgate, TOTP 입력, SOCKS5, 호스트 키 확인, 비밀번호 자동 입력, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, 포트 노킹, 터미널 로그, 에이전트 포워딩, Bitwarden SSH 에이전트, HashiCorp Vault SSH 서명, Step CA, 1Password Connect 등 -- **Termix ID** - sshid.io 같은 기능을 내장했습니다. 핸들을 등록하고 리졸버 URL에 공개 키를 올리고 내장 CA에서 SSH 인증서를 발급할 수 있습니다 -- **명령 기록 자동 완성** - 입력하는 동안 명령 기록을 바탕으로 터미널에 인라인 제안을 보여줍니다 -- **데스크톱 단독 실행과 동기화** - 데스크톱 앱은 자체 백엔드와 데이터베이스로 서버 없이 혼자 돌아가며, Termix 서버와 선택적으로 동기화할 수 있습니다 - -
- -
- -## 플랫폼 지원 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
플랫폼배포 형태
Web최신 브라우저 전반(Chrome, Safari, Firefox) · PWA 지원
Windows x64/ia32포터블 · MSI 설치 파일 · Chocolatey
Linux x64/ia32포터블 · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## 설치 - -모든 플랫폼의 자세한 설치 방법은 [Termix 문서](https://docs.termix.site/install)를 참고하세요. - -쿠버네티스에 배포하시나요? Helm 차트는 `charts/termix`에 있고, Ingress와 Traefik, Argo CD, GitHub Actions, GitLab CI를 다루는 설정 안내는 [docs.termix.site/install/server/kubernetes](https://docs.termix.site/install/server/kubernetes)에 있습니다. - -Docker Compose 예시입니다(원격 데스크톱 기능을 쓰지 않는다면 `guacd`와 네트워크 부분은 빼도 됩니다): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### 명령줄 도구 - -Termix에는 CLI도 있어서 터미널에서 서버를 관리하거나 Termix를 자기 스크립트에 넣어 쓸 수 있습니다. - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -터미널을 열고, 호스트 하나나 플릿 전체에서 명령을 실행하고, SFTP로 파일을 옮기고, 호스트와 스니펫, 자격 증명을 관리할 수 있습니다. 전체 문서는 [docs.termix.site/cli](https://docs.termix.site/cli)에 있습니다. - -### 클라우드 호스팅 - -Termix 서버는 집 안 네트워크가 아니라 VPS에서 돌릴 수도 있습니다. 관리 대상 네트워크 위에서 돌아가면 장애가 났을 때 Termix도 같이 멈춰서, 정작 고쳐야 할 때 쓸 수 없게 됩니다. 밖에서 돌리면 언제든 접속할 수 있고 고정 IP도 생기며, VPN이나 포트 포워딩 없이 어디서나 들어갈 수 있습니다. - -[GINERNET](https://docs.termix.site/install/ginernet)은 Termix를 후원하고 있으며, 문서에 이 회사 VPS에 배포하는 단계별 안내가 있습니다. - -
- -## 텔레메트리 - -Termix는 하루에 한 번 작은 익명 보고서를 보내서 몇 개의 인스턴스가 실행 중이고 어떤 기능이 쓰이는지 알 수 있게 합니다. 보고서에는 무작위 인스턴스 ID, 앱 버전, 사용자 수와 호스트 수가 들어 있습니다. 또한 플랫폼 정보(OS, 아키텍처, Node.js 버전, 데이터베이스 종류, Docker·데스크톱 앱·일반 서버 중 무엇으로 실행하는지), 탭 종류별로 열린 횟수와 SSH 로그인 수, 실행 중인 내장 기능도 포함될 수 있습니다. 사용자 이름, 호스트 이름, IP 주소, 자격 증명 등 당신이나 서버를 식별할 수 있는 정보는 절대 포함되지 않습니다. - -기본으로 켜져 있습니다. 관리 설정의 Usage Statistics에서 끄거나 포함할 내용을 고를 수 있고, 보낼 보고서를 미리 그대로 볼 수도 있습니다. 각 사용자는 자신의 프로필에서 본인의 기능 사용 기록을 뺄 수 있습니다. `ENABLE_TELEMETRY=false`를 지정하면 꺼지고 스위치가 잠깁니다. - -
- -## 후원 - -Termix는 무료 오픈 소스이고 구독이나 유료 요금제가 없습니다. 유용하게 쓰고 계시다면 서버 비용과 도메인, 개발 시간에 보탬이 되도록 후원을 고려해 주세요. 후원은 SAML과 Kubernetes, 에이전트 지원 같은 기능을 만들기 위해 알아보고 배우는 시간에도 쓰입니다. 진행 상황을 보고 후원하시려면 아래를 눌러 주세요. - -[후원하기](https://donate.termix.site/) - -
- -## 스폰서 - -유료 게재로 개발을 지원하고 싶으신가요? [mail@termix.site](mailto:mail@termix.site)로 메일을 보내 주세요. - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## 지원 - -도움이 필요하거나 기능을 제안하고 싶으신가요? [새 이슈](https://github.com/Termix-SSH/Support/issues)를 올리면서 되도록 자세히, 가능하면 영어로 적어 주세요. [Discord](https://discord.gg/jVQGdvHDrf) 지원 채널에서 물어봐도 되지만 답변이 늦을 수 있습니다. - -
- -## 스크린샷 - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -YouTube에서 업데이트 소개 보기 - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -일부 영상과 이미지는 오래되었거나 기능을 제대로 보여 주지 못할 수 있습니다. - -
- -
- -## 계획된 기능 - -계획된 기능은 모두 [Projects](https://github.com/orgs/Termix-SSH/projects/5)에 있습니다. 기여하고 싶다면 [기여 안내](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)를 봐 주세요. - -
- -## 라이선스 - -Apache License 2.0에 따라 배포합니다. 자세한 내용은 `LICENSE`를 참고하세요. diff --git a/docs/readme/README-PT.md b/docs/readme/README-PT.md deleted file mode 100644 index 8ad8aa429..000000000 --- a/docs/readme/README-PT.md +++ /dev/null @@ -1,500 +0,0 @@ -
- -Termix Logo - -

Termix

- -

Gestão de servidores auto-hospedada, do SSH e do desktop remoto às automações

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -O Termix é gratuito e de código aberto. Se ele te ajuda, considera [doar](https://donate.termix.site/) para ajudar a cobrir os custos de servidor e o tempo de desenvolvimento. - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- Conquistado em 1 de setembro de 2025 -

- -
- -
- -## Visão geral - -O Termix é uma plataforma gratuita, de código aberto e auto-hospedada para gerenciar os teus servidores. Ele junta num só lugar terminais SSH, desktops remotos (RDP, VNC, Telnet), transferência de arquivos, túneis, Docker, métricas e automações, na web, no desktop e no celular. É uma alternativa auto-hospedada ao Termius que continua gratuita para sempre. - -
- -## Funcionalidades - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**Terminal SSH:** -Um terminal completo com abas como as do navegador e tela dividida, até 6 painéis ao mesmo tempo. Escolhe o teu tema, a fonte e as cores. Acima de cada sessão há uma barra com CPU, memória e disco ao vivo, além de atalhos para os arquivos, o Docker, os túneis e as métricas daquele host. - - - -**Desktop remoto:** -RDP, VNC e Telnet no navegador, em abas e tela dividida como qualquer outra sessão. Inclui um navegador de arquivos para as unidades RDP e envio arrastando e soltando. No desktop Windows também dá para abrir um host no cliente RDP nativo. - -
- -**Túneis SSH:** -Encaminhamento local, remoto e SOCKS dinâmico, com reconexão automática e verificação de estado. Os túneis de cliente para servidor do aplicativo de desktop ficam naquela máquina, e dá para salvar predefinições no servidor para levar uma configuração para outro computador. - - - -**Gerenciador de arquivos:** -Navega, edita, envia, baixa, renomeia, move e apaga arquivos por SFTP, com suporte a sudo. Vê e edita código, imagens, áudio e vídeo. Copia arquivos direto de um servidor para outro, com o caminho mais rápido escolhido para ti e a integridade das transferências verificada. - -
- -**Docker e Podman:** -Inicia, para, pausa e remove containers, acompanha as estatísticas e abre um shell dentro de um deles. Funciona tanto com Docker quanto com Podman. Não é para substituir o Portainer ou o Dockge, só para gerenciar os containers que já tens. - - - -**Gerenciador de hosts:** -Salva e organiza hosts com etiquetas e pastas aninhadas que podes nomear e colorir. Reaproveita credenciais salvas entre hosts, distribui chaves SSH automaticamente, agrupa hosts sob um host pai, edita e exporta em lote, e usa a conexão rápida para conexões pontuais que não queres guardar. - -
- -**Métricas de host:** -CPU, memória, disco, rede, temperatura, tempo ligado, processos, portas, logins e informações do sistema na maioria dos servidores Linux, com gráficos de histórico. Os cartões de gerenciamento deixam cuidar de serviços, tarefas cron, pacotes, usuários, regras de firewall, WireGuard, Tailscale, certificados SSL, logs e verificações de saúde sem sair do Termix. - - - -**Automações:** -Escolhe um gatilho e depois diz o que deve acontecer. Os gatilhos incluem uma métrica passando de um limite, um host caindo ou voltando, uma verificação de saúde mudando, um agendamento, um evento de container ou um webhook recebido. Os passos podem rodar comandos e trechos, controlar containers e túneis, acordar um host, chamar uma URL, esperar, seguir por uma condição, rodar outra automação e te avisar por ntfy, Discord ou webhook. As execuções de teste deixam experimentar com segurança primeiro. - -
- -**Frotas:** -Junta hosts numa frota escolhendo um a um ou com regras de etiquetas, para que os novos entrem sozinhos. Roda um comando em todos os hosts de uma vez, envia e busca arquivos em todos eles, instala pacotes e reúne um inventário do sistema, do kernel, da arquitetura e do tempo ligado. - - - -**Assistente de IA:** -É opcional e fica desligado até tu ligares. Conecta OpenAI, Anthropic, Gemini, Ollama ou qualquer endpoint compatível com OpenAI e pergunta sobre a tua instalação. Ele lê hosts, frotas, trechos e alertas, e propõe mudanças para tu aprovares em vez de fazer sozinho. Nunca consegue mexer em credenciais, usuários ou configurações. Os administradores podem deixar desligado para toda a instância, e dá para escondê-lo já na configuração inicial. - -
- -**Login e usuários:** -Contas locais além de login por OIDC, LDAP, GitHub e Google, com dois fatores (TOTP), chaves de acesso (WebAuthn) e dispositivos confiáveis. Os administradores podem gerenciar usuários, ligar grupos do OIDC a papéis, ver todas as sessões ativas em qualquer plataforma e encerrá-las. Liga a tua conta local com a do OIDC e consulta o registro de auditoria do que cada um fez. - - - -**Papéis e compartilhamento:** -Cria papéis e compartilha hosts com usuários ou papéis em quatro níveis: conectar, ver, editar e gerenciar. Funciona com todos os tipos de autenticação e todos os protocolos, e dá para trocar as credenciais usadas num host compartilhado. - -
- -**Alertas:** -Define regras em métricas de host como CPU, memória e disco, e recebe aviso por ntfy, Discord ou webhook quando elas disparam. Vê os alertas ativos e resolvidos num histórico e dispensa os que não te interessam. - - - -**Página inicial:** -Uma grade de widgets que tu mesmo montas arrastando e soltando. Tem widget para status de host, ping, links de serviços, favoritos, busca, relógios, calendários, contagens regressivas, notas, RSS, previsão do tempo, imagens, iframes, Docker, túneis, gráficos de métricas, APIs próprias e até um terminal ao vivo. - -
- -**Trechos e ferramentas:** -Salva os comandos que usas sempre e dispara com um clique, com variáveis para o host e para o que tu digitares. Roda um mesmo comando em todos os terminais abertos e pesquisa o teu histórico com preenchimento automático. - - - -**Compartilhar sessão:** -Compartilha ao vivo uma sessão de terminal, RDP, VNC ou Telnet. Manda um link que qualquer um entra sem conta, ou compartilha com um usuário específico do Termix, em somente leitura ou com escrita. Os compartilhamentos podem expirar sozinhos ou ser revogados, e dá para desligar tudo de uma vez ou por host. - -
- -**Gravação e registros de sessão:** -Grava sessões de terminal, RDP e VNC e reproduz depois. Baixa registros de texto de uma sessão e olha o registro de conexão para ver exatamente o que aconteceu durante ela. - - - -**Conexões seriais:** -Fala com dispositivos seriais como roteadores, switches e microcontroladores pelo navegador ou pelo aplicativo de desktop. Define taxa de transmissão, bits de dados, bits de parada e paridade. Usa a API Web Serial nos navegadores compatíveis, ou um backend nativo no aplicativo de desktop. - -
- -**Tailscale:** -Traz dispositivos da tua tailnet para adicionar como hosts em poucos cliques, e conecta com Tailscale SSH para que as ACLs da tailnet cuidem do acesso sem guardar credenciais. Headscale e endpoints personalizados também funcionam. - - - -**Proxmox:** -Importa hosts direto de uma instância Proxmox e acompanha as estatísticas de nós e convidados, incluindo CPU, memória e armazenamento, numa aba própria. - -
- -**Áreas de trabalho e abas:** -Salva um conjunto de abas com a divisão da tela e reabre tudo com um clique. O Termix também lembra da tua última sessão, então as abas voltam depois de recarregar e em outros dispositivos. - - - -**Linha de comando:** -Um CLI `termix` para o teu shell e os teus scripts. Abre terminais, roda um comando num host ou numa frota inteira, move arquivos por SFTP e gerencia hosts, trechos e credenciais. Instala com `npm install -g @termix-ssh/cli` ou pega um binário independente. Vê a [documentação do CLI](https://docs.termix.site/cli). - -
- -**Segurança:** -Senhas, chaves e outros segredos são criptografados por usuário, e os próprios arquivos do banco de dados podem ser criptografados em disco. Vê a [documentação](https://docs.termix.site/features/authentication/security/) para entender como funciona. - - - -**Idiomas:** -Cerca de 30 idiomas incluídos, gerenciados pelo [Crowdin](https://docs.termix.site/translations). - -
- -
- -
-Mais funcionalidades -
- -- **Painel** - Os teus servidores num relance, com cartões que tu mesmo organizas -- **Gráfico de rede** - O teu homelab desenhado a partir dos teus hosts, com status ao vivo -- **Monitor tmux** - Percorre sessões, janelas e painéis do tmux, com prévia e busca -- **Chaves de API** - Chaves por usuário com data de validade para scripts e CI -- **Exportar e importar** - Leva e traz hosts, credenciais e dados do gerenciador de arquivos -- **SSL automático** - Certificados gerados e renovados para ti, com redirecionamento para HTTPS, ou usa os teus -- **Bancos de dados** - SQLite por padrão, com PostgreSQL e MySQL também suportados -- **Interface moderna** - Interface React limpa que funciona no desktop e no celular, com temas como claro, escuro e Dracula. Qualquer conexão abre em tela cheia por uma URL -- **Paleta de comandos** - Toca duas vezes no Shift esquerdo para ir a um host pelo teclado -- **Atalhos de teclado** - Trocar de aba, fechar abas e mais, tudo remapeável -- **Wake-on-LAN** - Liga uma máquina pelo Termix ou por um passo de automação -- **Autenticação por proxy confiável** - Deixa um proxy reverso cuidar do login e repassar o usuário -- **Marca própria** - Os administradores podem rebatizar a instância com nome, logo e cores próprios -- **Endpoints web** - Abre a interface web do próprio host, como a página de administração de um roteador, embutida no Termix em vez de numa aba separada -- **Salas de colaboração** - Salas persistentes onde um grupo pode passar de uma sessão para outra junto, com um palco de apresentação e convites -- **SSH bem completo** - Hosts de salto, Warpgate, pedidos de TOTP, SOCKS5, verificação de chave de host, preenchimento automático de senha, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, registro do terminal, encaminhamento de agente, agente SSH do Bitwarden, assinatura SSH com HashiCorp Vault, Step CA, 1Password Connect e mais -- **Termix ID** - Uma versão embutida do sshid.io. Registra um identificador, publica as tuas chaves públicas numa URL de resolução e emite certificados SSH pela CA embutida -- **Sugestões automáticas de histórico de comandos** - Sugestões dentro do terminal enquanto tu digitas, baseadas no teu histórico de comandos -- **Desktop independente e sincronização** - O aplicativo de desktop roda sozinho, com backend e banco de dados locais, e pode opcionalmente sincronizar com um servidor Termix - -
- -
- -## Plataformas suportadas - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PlataformaDistribuição
WebQualquer navegador moderno (Chrome, Safari, Firefox) · Suporte a PWA
Windows x64/ia32Portátil · Instalador MSI · Chocolatey
Linux x64/ia32Portátil · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## Instalação - -Vê a [documentação do Termix](https://docs.termix.site/install) para as instruções completas de instalação em todas as plataformas. - -Exemplo de arquivo Docker Compose (dá para tirar o `guacd` e a rede se não pretendes usar o desktop remoto): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### Linha de comando - -O Termix também tem um CLI, para gerenciares os teus servidores pelo terminal e usares o Termix nos teus próprios scripts. - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -Ele abre terminais, roda um comando num host ou numa frota inteira, move arquivos por SFTP e gerencia hosts, trechos e credenciais. A documentação completa está em [docs.termix.site/cli](https://docs.termix.site/cli). - -### Hospedagem na nuvem - -Dá para rodar o servidor do Termix num VPS em vez de dentro da tua própria rede. Se o Termix roda na rede que ele gerencia, uma queda leva ele junto, bem na hora em que precisas dele para resolver. Rodando fora ele continua acessível, te dá um IP fixo e dá para entrar de qualquer lugar sem VPN nem abrir portas. - -A [GINERNET](https://docs.termix.site/install/ginernet) patrocina o Termix, e a documentação tem um guia passo a passo para publicar na plataforma de VPS deles. - -
- -## Telemetria - -O Termix envia uma vez por dia um pequeno relatório anónimo, para eu ver quantas instâncias estão a correr e que funcionalidades são usadas. Contém um ID de instância aleatório, a versão da app e quantos utilizadores e hosts tens. Também pode incluir a tua plataforma (sistema operativo, arquitetura, versão do Node.js, tipo de base de dados e se usas Docker, a app de desktop ou um servidor normal), quantas vezes cada tipo de separador foi aberto e quantos logins SSH houve, e que funcionalidades integradas estão ativas. Nunca contém nomes de utilizador, nomes de host, endereços IP, credenciais ou qualquer outra coisa que te identifique a ti ou aos teus servidores. - -Vem ligado por padrão. Podes desligar ou escolher o que inclui nas configurações de administração, em Usage Statistics, onde também podes ver o relatório exato antes de ser enviado. Cada utilizador pode excluir o seu próprio uso de funcionalidades no perfil. Definir `ENABLE_TELEMETRY=false` desliga-o e bloqueia o interruptor. - -
- -## Doar - -O Termix é gratuito e de código aberto, sem assinaturas nem planos pagos. Se ele te ajuda, considera doar para ajudar com servidores, domínios e tempo de desenvolvimento. As doações também custeiam o tempo de pesquisar e aprender o necessário para funcionalidades como SAML, Kubernetes e suporte a agentes. Acompanha o progresso e doa abaixo. - -[Doar](https://donate.termix.site/) - -
- -## Patrocinadores - -Tens interesse num espaço pago para apoiar o desenvolvimento? Escreve para [mail@termix.site](mailto:mail@termix.site). - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## Suporte - -Precisas de ajuda ou queres pedir uma funcionalidade? Abre uma [nova issue](https://github.com/Termix-SSH/Support/issues) com o máximo de detalhes possível, em inglês se der. Também podes perguntar no canal de suporte do [Discord](https://discord.gg/jVQGdvHDrf), embora as respostas por lá possam demorar mais. - -
- -## Capturas de tela - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -Vê as apresentações das atualizações no YouTube - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -Alguns vídeos e imagens podem estar desatualizados ou não mostrar as funcionalidades perfeitamente. - -
- -
- -## Funcionalidades planejadas - -Todas as funcionalidades planejadas estão em [Projects](https://github.com/orgs/Termix-SSH/projects/5). Se queres contribuir, vê [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). - -
- -## Licença - -Distribuído sob a Licença Apache versão 2.0. Vê `LICENSE` para mais informações. diff --git a/docs/readme/README-RU.md b/docs/readme/README-RU.md deleted file mode 100644 index 01462bc30..000000000 --- a/docs/readme/README-RU.md +++ /dev/null @@ -1,500 +0,0 @@ -
- -Termix Logo - -

Termix

- -

Управление серверами на своём хостинге, от SSH и удалённого рабочего стола до автоматизаций

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix бесплатен и с открытым исходным кодом. Если он вам пригодился, подумайте о [пожертвовании](https://donate.termix.site/), чтобы помочь покрыть расходы на серверы и время на разработку. - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- Получено 1 сентября 2025 года -

- -
- -
- -## Обзор - -Termix это бесплатная платформа с открытым исходным кодом для управления серверами на своём хостинге. Она собирает в одном месте SSH-терминалы, удалённые рабочие столы (RDP, VNC, Telnet), передачу файлов, туннели, Docker, метрики и автоматизации, в браузере, на компьютере и на телефоне. Это self-hosted замена Termius, которая остаётся бесплатной навсегда. - -
- -## Возможности - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**SSH-терминал:** -Полноценный терминал с вкладками как в браузере и разделением экрана, до 6 панелей одновременно. Тему, шрифт и цвета выбираете вы. Над каждой сессией есть панель с текущей загрузкой процессора, памяти и диска, а также быстрые ссылки на файлы, Docker, туннели и метрики этого хоста. - - - -**Удалённый рабочий стол:** -RDP, VNC и Telnet прямо в браузере, во вкладках и с разделением экрана, как и любая другая сессия. Есть просмотр файлов на дисках RDP и загрузка перетаскиванием. В версии для Windows хост можно открыть и в обычном клиенте RDP. - -
- -**SSH-туннели:** -Локальная, удалённая и динамическая переадресация SOCKS с автоматическим переподключением и проверкой состояния. Туннели от клиента к серверу в настольном приложении хранятся на этом компьютере, а наборы настроек можно сохранить на сервере, чтобы перенести конфигурацию на другую машину. - - - -**Файловый менеджер:** -Просматривайте, редактируйте, загружайте, скачивайте, переименовывайте, перемещайте и удаляйте файлы по SFTP, в том числе через sudo. Смотрите и правьте код, изображения, аудио и видео. Копируйте файлы напрямую с одного сервера на другой: самый быстрый маршрут подбирается сам, а целостность передачи проверяется. - -
- -**Docker и Podman:** -Запускайте, останавливайте, ставьте на паузу и удаляйте контейнеры, смотрите их нагрузку и открывайте оболочку внутри. Работает и с Docker, и с Podman. Это не замена Portainer или Dockge, а способ управлять теми контейнерами, что у вас уже есть. - - - -**Менеджер хостов:** -Храните и упорядочивайте хосты с помощью меток и вложенных папок, которым можно задать имя и цвет. Используйте сохранённые учётные данные на нескольких хостах, разворачивайте SSH-ключи автоматически, группируйте хосты под родительским, редактируйте и выгружайте пакетно, а для разовых подключений, которые не хочется сохранять, есть быстрое подключение. - -
- -**Метрики хостов:** -Процессор, память, диск, сеть, температура, время работы, процессы, порты, входы в систему и сведения о системе на большинстве серверов Linux, с графиками за прошлые периоды. Карточки управления позволяют работать со службами, задачами cron, пакетами, пользователями, правилами брандмауэра, WireGuard, Tailscale, сертификатами SSL, журналами и проверками состояния, не выходя из Termix. - - - -**Автоматизации:** -Выберите событие, а затем опишите, что должно произойти. Событием может быть превышение порога метрикой, хост, который упал или вернулся, изменение проверки состояния, расписание, событие контейнера или входящий webhook. Шаги умеют выполнять команды и сниппеты, управлять контейнерами и туннелями, будить хост, обращаться по адресу, ждать, ветвиться по условию, запускать другую автоматизацию и присылать уведомления через ntfy, Discord или webhook. Тестовый запуск позволяет всё безопасно проверить. - -
- -**Флоты:** -Объединяйте хосты во флот вручную или по правилам меток, чтобы новые хосты попадали туда сами. Выполняйте одну команду сразу на всех хостах, отправляйте и забирайте файлы со всех, устанавливайте пакеты и собирайте сводку по системе, ядру, архитектуре и времени работы. - - - -**ИИ-помощник:** -Необязательная возможность, выключенная до тех пор, пока вы сами её не включите. Подключите OpenAI, Anthropic, Gemini, Ollama или любой совместимый с OpenAI адрес и спрашивайте о своей системе. Он читает хосты, флоты, сниппеты и оповещения и предлагает изменения на ваше утверждение, а не вносит их сам. До учётных данных, пользователей и настроек он не доберётся никогда. Администраторы могут оставить его выключенным для всей установки, а вы можете скрыть его ещё при первичной настройке. - -
- -**Вход и пользователи:** -Локальные учётные записи, а также вход через OIDC, LDAP, GitHub и Google, с двухфакторной проверкой (TOTP), ключами доступа (WebAuthn) и доверенными устройствами. Администраторы могут управлять пользователями, сопоставлять группы OIDC с ролями, видеть все активные сессии на всех платформах и завершать их. Свяжите локальную учётную запись с OIDC и смотрите журнал аудита действий каждого. - - - -**Роли и общий доступ:** -Создавайте роли и делитесь хостами с пользователями или ролями на четырёх уровнях: подключение, просмотр, изменение и управление. Работает со всеми способами аутентификации и всеми протоколами, а учётные данные для общего хоста можно переопределить. - -
- -**Оповещения:** -Задайте правила по метрикам хостов, например процессору, памяти и диску, и получайте уведомления через ntfy, Discord или webhook, когда они срабатывают. Смотрите активные и уже снятые оповещения в журнале и убирайте те, что вам не нужны. - - - -**Домашняя страница:** -Сетка виджетов, которую вы собираете сами перетаскиванием. Есть виджеты для состояния хостов, пингов, ссылок на сервисы, закладок, поиска, часов, календарей, обратного отсчёта, заметок, RSS, погоды, изображений, встроенных страниц, Docker, туннелей, графиков метрик, своих API и даже живого терминала. - -
- -**Сниппеты и инструменты:** -Сохраняйте команды, которые часто набираете, и запускайте их одним нажатием, с переменными для хоста и для собственного ввода. Выполняйте одну команду сразу во всех открытых терминалах и ищите по истории команд с автодополнением. - - - -**Общий доступ к сессии:** -Делитесь живой сессией терминала, RDP, VNC или Telnet. Отправьте ссылку, по которой можно подключиться без учётной записи, или поделитесь с конкретным пользователем Termix, только для просмотра или с правом ввода. Доступ может истекать сам или отзываться в любой момент, и его можно отключить полностью или для отдельного хоста. - -
- -**Запись сессий и журналы:** -Записывайте сессии терминала, RDP и VNC и просматривайте их позже. Скачивайте текстовые журналы сессии и заглядывайте в журнал подключения, чтобы увидеть, что именно происходило во время соединения. - - - -**Последовательные подключения:** -Общайтесь с последовательными устройствами вроде маршрутизаторов, коммутаторов и микроконтроллеров из браузера или настольного приложения. Настраивайте скорость, биты данных, стоповые биты и чётность. В подходящих браузерах используется Web Serial API, а в настольном приложении собственный бэкенд. - -
- -**Tailscale:** -Подтягивайте устройства из своей tailnet, чтобы добавить их как хосты в пару нажатий, и подключайтесь через Tailscale SSH: доступом займутся правила tailnet, а учётные данные хранить не придётся. Headscale и свои адреса тоже работают. - - - -**Proxmox:** -Импортируйте хосты прямо из установки Proxmox и следите за показателями узлов и гостевых машин, включая процессор, память и хранилище, на отдельной вкладке. - -
- -**Рабочие пространства и вкладки:** -Сохраните набор вкладок вместе с разделением экрана и откройте всё это одним нажатием. Termix помнит и последнюю сессию, поэтому вкладки возвращаются после обновления страницы и на других устройствах. - - - -**Командная строка:** -CLI `termix` для вашей оболочки и ваших скриптов. Открывайте терминалы, выполняйте команду на одном хосте или на целом флоте, перемещайте файлы по SFTP и управляйте хостами, сниппетами и учётными данными. Установите через `npm install -g @termix-ssh/cli` или возьмите отдельный исполняемый файл. Смотрите [документацию CLI](https://docs.termix.site/cli). - -
- -**Безопасность:** -Пароли, ключи и другие секреты шифруются для каждого пользователя, а сами файлы базы данных можно зашифровать на диске. Как это устроено, описано в [документации](https://docs.termix.site/features/authentication/security/). - - - -**Языки:** -Около 30 встроенных языков, которые ведутся через [Crowdin](https://docs.termix.site/translations). - -
- -
- -
-Другие возможности -
- -- **Панель** - Ваши серверы одним взглядом, карточки расставляете вы сами -- **Схема сети** - Ваша домашняя лаборатория, нарисованная по хостам, с состоянием в реальном времени -- **Монитор tmux** - Просмотр сессий, окон и панелей tmux с предпросмотром и поиском -- **Ключи API** - Ключи для конкретного пользователя со сроком действия, для скриптов и CI -- **Экспорт и импорт** - Перенос хостов, учётных данных и данных файлового менеджера -- **Автоматический SSL** - Сертификаты выпускаются и обновляются за вас, с переходом на HTTPS, либо используйте свои -- **Базы данных** - По умолчанию SQLite, поддерживаются также PostgreSQL и MySQL -- **Современный интерфейс** - Аккуратный интерфейс на React для компьютера и телефона, с темами вроде светлой, тёмной и Dracula. Любое подключение открывается на весь экран по ссылке -- **Палитра команд** - Двойное нажатие левого Shift, чтобы перейти к хосту с клавиатуры -- **Сочетания клавиш** - Переход между вкладками, их закрытие и другое, всё можно переназначить -- **Wake-on-LAN** - Разбудите машину из Termix или из шага автоматизации -- **Доверенный прокси** - Пусть обратный прокси возьмёт вход на себя и передаст пользователя -- **Собственный бренд** - Администраторы могут переоформить установку под своё имя, логотип и цвета -- **Веб-эндпоинты** - Открывайте собственный веб-интерфейс хоста, например админку роутера, прямо внутри Termix вместо отдельной вкладки -- **Комнаты для совместной работы** - Постоянные комнаты, где группа может вместе переключаться между сессиями, с режимом докладчика и приглашениями -- **Богатые возможности SSH** - Промежуточные хосты, Warpgate, запросы TOTP, SOCKS5, проверка ключей хоста, автозаполнение паролей, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, журналы терминала, проброс агента, SSH-агент Bitwarden, подпись SSH через HashiCorp Vault, Step CA, 1Password Connect и другое -- **Termix ID** - Встроенный аналог sshid.io. Займите имя, опубликуйте открытые ключи по адресу распознавателя и выпускайте SSH-сертификаты через встроенный центр сертификации -- **Автоподсказки истории команд** - Подсказки прямо в терминале по мере ввода, на основе истории ваших команд -- **Автономный клиент и синхронизация** - Настольное приложение работает само по себе, со своим бэкендом и локальной базой данных, и может по желанию синхронизироваться с сервером Termix - -
- -
- -## Поддержка платформ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ПлатформаСпособ установки
WebЛюбой современный браузер (Chrome, Safari, Firefox) · Поддержка PWA
Windows x64/ia32Портативная версия · Установщик MSI · Chocolatey
Linux x64/ia32Портативная версия · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## Установка - -Полные инструкции по установке для всех платформ смотрите в [документации Termix](https://docs.termix.site/install). - -Пример файла Docker Compose (`guacd` и сеть можно убрать, если удалённый рабочий стол вам не нужен): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### Командная строка - -У Termix есть и CLI, так что серверами можно управлять из терминала и использовать Termix в своих скриптах. - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -Он умеет открывать терминалы, выполнять команду на одном хосте или на целом флоте, перемещать файлы по SFTP и управлять хостами, сниппетами и учётными данными. Полная документация есть на [docs.termix.site/cli](https://docs.termix.site/cli). - -### Размещение в облаке - -Сервер Termix можно держать на VPS, а не внутри своей сети. Если Termix работает в той же сети, которой управляет, при сбое он упадёт вместе с ней, как раз тогда, когда нужен для починки. Снаружи он остаётся доступным, даёт постоянный IP и позволяет зайти откуда угодно без VPN и проброса портов. - -[GINERNET](https://docs.termix.site/install/ginernet) спонсирует Termix, и в документации есть пошаговое руководство по развёртыванию на их площадке VPS. - -
- -## Телеметрия - -Раз в день Termix отправляет небольшой анонимный отчёт, чтобы я видел, сколько установок работает и какими функциями пользуются. В нём есть случайный ID установки, версия приложения и количество пользователей и хостов. Он также может включать вашу платформу (ОС, архитектуру, версию Node.js, тип базы данных и то, как запущен Termix: в Docker, как настольное приложение или как обычный сервер), сколько раз открывались вкладки каждого типа и сколько было входов по SSH, а также какие встроенные функции работают. В нём никогда нет имён пользователей, имён хостов, IP-адресов, учётных данных или чего-либо ещё, что может указать на вас или ваши серверы. - -По умолчанию он включён. Выключить его или выбрать, что в него входит, можно в настройках администратора в разделе Usage Statistics, там же можно заранее посмотреть точный отчёт. Каждый пользователь может исключить своё использование функций в своём профиле. `ENABLE_TELEMETRY=false` выключает его и блокирует переключатель. - -
- -## Пожертвования - -Termix бесплатен и открыт, без подписок и платных тарифов. Если он вам полезен, подумайте о пожертвовании: оно помогает с серверами, доменами и временем на разработку. Пожертвования также оплачивают время на изучение того, что нужно для таких возможностей, как SAML, Kubernetes и поддержка агентов. Следить за ходом работ и поддержать можно по ссылке ниже. - -[Поддержать](https://donate.termix.site/) - -
- -## Спонсоры - -Интересует платное размещение в поддержку разработки? Напишите на [mail@termix.site](mailto:mail@termix.site). - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## Поддержка - -Нужна помощь или хотите предложить функцию? Создайте [новое обращение](https://github.com/Termix-SSH/Support/issues) и опишите всё как можно подробнее, по возможности на английском. Ещё можно спросить в канале поддержки в [Discord](https://discord.gg/jVQGdvHDrf), хотя там ответа иногда приходится ждать дольше. - -
- -## Скриншоты - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -Смотрите обзоры обновлений на YouTube - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -Некоторые видео и изображения могут устареть или не полностью показывать возможности. - -
- -
- -## Запланированные функции - -Все запланированные функции собраны в [Projects](https://github.com/orgs/Termix-SSH/projects/5). Если хотите поучаствовать, посмотрите [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). - -
- -## Лицензия - -Распространяется по лицензии Apache версии 2.0. Подробности в файле `LICENSE`. diff --git a/docs/readme/README-TR.md b/docs/readme/README-TR.md deleted file mode 100644 index 931d47a31..000000000 --- a/docs/readme/README-TR.md +++ /dev/null @@ -1,500 +0,0 @@ -
- -Termix Logo - -

Termix

- -

Kendi sunucunuzda çalışan sunucu yönetimi, SSH ve uzak masaüstünden otomasyonlara kadar

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix ücretsiz ve açık kaynaklıdır. İşinize yarıyorsa, sunucu masraflarına ve geliştirme süresine katkı için [bağış yapmayı](https://donate.termix.site/) düşünün. - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- 1 Eylül 2025 tarihinde kazanıldı -

- -
- -
- -## Genel bakış - -Termix, sunucularınızı yönetmek için ücretsiz, açık kaynaklı ve kendi sunucunuzda çalışan bir platformdur. SSH terminallerini, uzak masaüstlerini (RDP, VNC, Telnet), dosya aktarımlarını, tünelleri, Docker'ı, ölçümleri ve otomasyonları tek yerde toplar; web, masaüstü ve mobilde çalışır. Sonsuza dek ücretsiz kalan, kendi sunucunuzda çalışan bir Termius alternatifidir. - -
- -## Özellikler - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**SSH terminali:** -Tarayıcı gibi sekmeleri ve bölünmüş ekranı olan tam donanımlı bir terminal, aynı anda 6 panele kadar. Temanızı, yazı tipinizi ve renklerinizi seçin. Her oturumun üstünde anlık CPU, bellek ve disk bilgisi gösteren bir araç çubuğu ile o sunucunun dosyalarına, Docker'ına, tünellerine ve ölçümlerine giden kısayollar bulunur. - - - -**Uzak masaüstü:** -Tarayıcıda RDP, VNC ve Telnet; diğer oturumlar gibi sekmelerde ve bölünmüş ekranda. RDP sürücüleri için dosya tarayıcısı ve sürükle bırak yükleme içerir. Windows masaüstünde bir sunucuyu yerel RDP istemcisinde de açabilirsiniz. - -
- -**SSH tünelleri:** -Yerel, uzak ve dinamik SOCKS yönlendirmesi; otomatik yeniden bağlanma ve durum kontrolleriyle. Masaüstü uygulamasındaki istemciden sunucuya tüneller o makinede saklanır, ayarları sunucuya kaydederek bir kurulumu başka bir makineye taşıyabilirsiniz. - - - -**Dosya yöneticisi:** -SFTP üzerinden dosyalara göz atın, düzenleyin, yükleyin, indirin, yeniden adlandırın, taşıyın ve silin; sudo da kullanılabilir. Kod, görsel, ses ve videoyu görüntüleyip düzenleyin. Dosyaları doğrudan bir sunucudan diğerine kopyalayın; en hızlı yol sizin için seçilir ve aktarımların bütünlüğü doğrulanır. - -
- -**Docker ve Podman:** -Kapsayıcıları başlatın, durdurun, duraklatın ve silin, durumlarını izleyin ve içlerinde bir kabuk açın. Hem Docker hem Podman ile çalışır. Portainer ya da Dockge'nin yerini almak için değil, hâlihazırdaki kapsayıcılarınızı yönetmek için tasarlandı. - - - -**Sunucu yöneticisi:** -Sunucularınızı etiketlerle ve isim ve renk verebileceğiniz iç içe klasörlerle düzenleyin. Kayıtlı kimlik bilgilerini birden çok sunucuda kullanın, SSH anahtarlarını otomatik dağıtın, sunucuları bir üst sunucunun altında toplayın, toplu düzenleyip dışa aktarın ve kaydetmek istemediğiniz tek seferlik bağlantılar için hızlı bağlantıyı kullanın. - -
- -**Sunucu ölçümleri:** -Çoğu Linux sunucusunda CPU, bellek, disk, ağ, sıcaklık, çalışma süresi, süreçler, portlar, oturum açmalar ve sistem bilgisi; geçmiş grafikleriyle birlikte. Yönetim kartları sayesinde servisleri, cron görevlerini, paketleri, kullanıcıları, güvenlik duvarı kurallarını, WireGuard'ı, Tailscale'i, SSL sertifikalarını, günlükleri ve sağlık kontrollerini Termix'ten çıkmadan yönetirsiniz. - - - -**Otomasyonlar:** -Bir tetikleyici seçin, sonra ne olacağını söyleyin. Tetikleyiciler arasında bir ölçümün eşiği aşması, bir sunucunun düşmesi veya geri gelmesi, sağlık kontrolünün değişmesi, bir zamanlama, bir kapsayıcı olayı ya da gelen bir webhook var. Adımlar komut ve parçacık çalıştırabilir, kapsayıcı ve tünelleri yönetebilir, bir makineyi uyandırabilir, bir adrese istek atabilir, bekleyebilir, koşula göre dallanabilir, başka bir otomasyonu çalıştırabilir ve ntfy, Discord ya da webhook ile size haber verebilir. Deneme çalıştırmaları ile önce güvenle test edersiniz. - -
- -**Filolar:** -Sunucuları tek tek seçerek ya da etiket kurallarıyla bir filoda toplayın; yeni sunucular kendiliğinden katılsın. Tek bir komutu tüm sunucularda aynı anda çalıştırın, hepsine dosya gönderip hepsinden dosya alın, paket kurun ve işletim sistemi, çekirdek, mimari ve çalışma süresi dökümünü toplayın. - - - -**Yapay zekâ asistanı:** -İsteğe bağlıdır ve siz açana kadar kapalıdır. OpenAI, Anthropic, Gemini, Ollama ya da OpenAI uyumlu herhangi bir uç noktayı bağlayın ve kurulumunuz hakkında sorular sorun. Sunucuları, filoları, parçacıkları ve uyarıları okuyabilir; değişiklikleri kendisi yapmak yerine onayınıza sunar. Kimlik bilgilerine, kullanıcılara ve ayarlara asla erişemez. Yöneticiler tüm kurulum için kapalı bırakabilir, siz de kurulum sırasında gizleyebilirsiniz. - -
- -**Giriş ve kullanıcılar:** -Yerel hesapların yanında OIDC, LDAP, GitHub ve Google ile giriş; iki adımlı doğrulama (TOTP), geçiş anahtarları (WebAuthn) ve güvenilir cihazlar. Yöneticiler kullanıcıları yönetebilir, OIDC gruplarını rollerle eşleştirebilir, tüm platformlardaki etkin oturumları görüp sonlandırabilir. Yerel ve OIDC hesaplarınızı birbirine bağlayın ve herkesin ne yaptığını denetim günlüğünden okuyun. - - - -**Roller ve paylaşım:** -Roller oluşturun ve sunucuları kullanıcılar veya rollerle dört düzeyde paylaşın: bağlanma, görüntüleme, düzenleme ve yönetme. Tüm kimlik doğrulama türleri ve tüm protokollerle çalışır, paylaşılan bir sunucuda kullanılan kimlik bilgilerini değiştirebilirsiniz. - -
- -**Uyarılar:** -CPU, bellek ve disk gibi sunucu ölçümlerine kurallar koyun ve tetiklendiklerinde ntfy, Discord veya webhook ile haberdar olun. Devam eden ve çözülen uyarıları geçmişte görün, ilgilenmediklerinizi kapatın. - - - -**Ana sayfa:** -Kendi kurduğunuz, sürükle bırak çalışan bir bileşen ızgarası. Sunucu durumu, ping, servis bağlantıları, yer imleri, arama, saatler, takvimler, geri sayımlar, notlar, RSS, hava durumu, görseller, gömülü sayfalar, Docker, tüneller, ölçüm grafikleri, kendi API'leriniz ve hatta canlı bir terminal için bileşenler var. - -
- -**Parçacıklar ve araçlar:** -Sık kullandığınız komutları kaydedin ve tek tıkla çalıştırın; sunucu için ve kendi girdileriniz için değişkenler kullanabilirsiniz. Aynı komutu açık olan tüm terminallerde çalıştırın, komut geçmişinizde tamamlamayla arama yapın. - - - -**Oturum paylaşımı:** -Canlı bir terminal, RDP, VNC veya Telnet oturumunu paylaşın. Hesap gerekmeden katılınabilen bir bağlantı gönderin ya da belirli bir Termix kullanıcısıyla, salt okunur veya yazma yetkili olarak paylaşın. Paylaşımlar kendiliğinden sona erebilir veya istediğiniz an iptal edilebilir; tümüyle ya da sunucu bazında kapatılabilir. - -
- -**Oturum kaydı ve günlükler:** -Terminal, RDP ve VNC oturumlarını kaydedin ve sonra izleyin. Bir oturumun düz metin günlüğünü indirin, bağlantı günlüğüne bakarak bağlantı sırasında tam olarak ne olduğunu görün. - - - -**Seri bağlantılar:** -Yönlendirici, anahtar ve mikrodenetleyici gibi seri cihazlarla tarayıcıdan veya masaüstü uygulamasından konuşun. Baud hızını, veri bitlerini, dur bitlerini ve pariteyi ayarlayın. Destekleyen tarayıcılarda Web Serial API'yi, masaüstü uygulamasında yerel bir arka ucu kullanır. - -
- -**Tailscale:** -Tailnet'inizdeki cihazları çekip birkaç tıkla sunucu olarak ekleyin ve Tailscale SSH ile bağlanın; erişimi tailnet kurallarınız yönetsin, kimlik bilgisi saklamanız gerekmesin. Headscale ve özel uç noktalar da çalışır. - - - -**Proxmox:** -Sunucuları doğrudan bir Proxmox kurulumundan içe aktarın; düğüm ve misafir makinelerin CPU, bellek ve depolama dahil durumlarını kendi sekmesinde izleyin. - -
- -**Çalışma alanları ve sekmeler:** -Bir sekme grubunu bölünmüş düzeniyle birlikte kaydedin ve hepsini tek tıkla yeniden açın. Termix son oturumunuzu da hatırlar, böylece sayfayı yenileseniz de başka cihaza geçseniz de sekmeleriniz geri gelir. - - - -**Komut satırı arayüzü:** -Kabuğunuz ve betikleriniz için bir `termix` CLI'ı. Terminal açın, tek bir sunucuda veya tüm filoda komut çalıştırın, SFTP ile dosya taşıyın ve sunucuları, parçacıkları ve kimlik bilgilerini yönetin. `npm install -g @termix-ssh/cli` ile kurun ya da bağımsız bir çalıştırılabilir dosya edinin. [CLI belgelerine](https://docs.termix.site/cli) bakın. - -
- -**Güvenlik:** -Parolalar, anahtarlar ve diğer gizli bilgiler kullanıcı bazında şifrelenir, veritabanı dosyalarının kendisi de diskte şifrelenebilir. Nasıl çalıştığı için [belgelere](https://docs.termix.site/features/authentication/security/) bakın. - - - -**Diller:** -Yaklaşık 30 dil yerleşik olarak gelir, [Crowdin](https://docs.termix.site/translations) üzerinden yönetilir. - -
- -
- -
-Daha fazla özellik -
- -- **Kontrol paneli** - Kendi dizdiğiniz kartlarla sunucularınıza tek bakışta göz atın -- **Ağ grafiği** - Ev laboratuvarınız sunucularınızdan çizilir, durum anlık gösterilir -- **Tmux izleyici** - tmux oturumlarına, pencerelerine ve panellerine önizleme ve aramayla göz atın -- **API anahtarları** - Betikler ve CI için, son kullanma tarihli kullanıcıya özel anahtarlar -- **Dışa ve içe aktarma** - Sunucuları, kimlik bilgilerini ve dosya yöneticisi verilerini taşıyın -- **Otomatik SSL** - Sertifikalar sizin için oluşturulur ve yenilenir, HTTPS yönlendirmesiyle birlikte; ya da kendi sertifikanızı kullanın -- **Veritabanları** - Varsayılan SQLite, ayrıca PostgreSQL ve MySQL desteklenir -- **Modern arayüz** - Masaüstü ve mobilde çalışan sade bir React arayüzü; açık, koyu ve Dracula gibi temalarla. Her bağlantı bir adresten tam ekran açılabilir -- **Komut paleti** - Sol Shift'e iki kez basarak klavyeden bir sunucuya atlayın -- **Klavye kısayolları** - Sekmeler arasında geçiş, sekme kapatma ve dahası, hepsi yeniden atanabilir -- **Wake-on-LAN** - Bir makineyi Termix'ten ya da bir otomasyon adımından uyandırın -- **Güvenilir vekil doğrulaması** - Girişi ters vekil sunucu halletsin ve kullanıcıyı aktarsın -- **Beyaz etiket** - Yöneticiler kurulumu kendi adı, logosu ve renkleriyle yeniden markalayabilir -- **Web uç noktaları** - Bir sunucunun kendi web arayüzünü, örneğin bir yönlendirici yönetim sayfasını, ayrı bir sekme yerine Termix içine gömülü açın -- **İşbirliği odaları** - Bir grubun oturumlar arasında birlikte geçiş yapabildiği kalıcı odalar; sunum kürsüsü ve davetlerle -- **Zengin SSH desteği** - Atlama sunucuları, Warpgate, TOTP istekleri, SOCKS5, sunucu anahtarı doğrulama, parola otomatik doldurma, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, terminal günlüğü, aracı yönlendirme, Bitwarden SSH aracısı, HashiCorp Vault ile SSH imzalama, Step CA, 1Password Connect ve dahası -- **Termix ID** - sshid.io'nun yerleşik hali. Bir kullanıcı adı alın, açık anahtarlarınızı bir çözümleyici adresinde yayımlayın ve yerleşik CA ile SSH sertifikaları çıkarın -- **Komut geçmişi önerileri** - Yazarken komut geçmişinize dayalı satır içi öneriler -- **Bağımsız masaüstü ve eşitleme** - Masaüstü uygulaması kendi arka ucu ve veritabanıyla tek başına çalışır, isteğe bağlı olarak bir Termix sunucusuyla eşitlenebilir - -
- -
- -## Platform desteği - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PlatformDağıtım
WebGüncel her tarayıcı (Chrome, Safari, Firefox) · PWA desteği
Windows x64/ia32Taşınabilir · MSI kurulumu · Chocolatey
Linux x64/ia32Taşınabilir · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## Kurulum - -Tüm platformlar için ayrıntılı kurulum yönergelerini [Termix belgelerinde](https://docs.termix.site/install) bulabilirsiniz. - -Örnek Docker Compose dosyası (uzak masaüstünü kullanmayacaksanız `guacd` ve ağ kısmını çıkarabilirsiniz): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### Komut satırı - -Termix'in bir CLI'ı da var; sunucularınızı terminalden yönetebilir ve Termix'i kendi betiklerinizde kullanabilirsiniz. - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -Terminal açabilir, tek bir sunucuda veya tüm filoda komut çalıştırabilir, SFTP ile dosya taşıyabilir ve sunucuları, parçacıkları ve kimlik bilgilerini yönetebilir. Belgelerin tamamı [docs.termix.site/cli](https://docs.termix.site/cli) adresinde. - -### Bulutta barındırma - -Termix sunucusunu kendi ağınız yerine bir VPS üzerinde de çalıştırabilirsiniz. Termix yönettiği ağın içinde çalışıyorsa, bir kesinti onu da beraberinde götürür; hem de tam onu tamir için kullanmanız gereken anda. Dışarıda çalıştırmak erişilebilir kalmasını sağlar, sabit bir IP verir ve VPN ya da port yönlendirme olmadan her yerden girmenize izin verir. - -[GINERNET](https://docs.termix.site/install/ginernet) Termix'e sponsor oluyor ve belgelerde onların VPS platformuna kurulum için adım adım bir rehber var. - -
- -## Telemetri - -Termix, kaç kurulumun çalıştığını ve hangi özelliklerin kullanıldığını görebilmem için günde bir kez küçük ve anonim bir rapor gönderir. Raporda rastgele bir kurulum kimliği, uygulama sürümü ve kaç kullanıcı ile sunucunuz olduğu bulunur. Ayrıca platformunuzu (işletim sistemi, mimari, Node.js sürümü, veritabanı türü ve Docker, masaüstü uygulaması ya da normal bir sunucu kullanıp kullanmadığınız), her sekme türünün kaç kez açıldığını ve kaç SSH girişi yapıldığını, hangi yerleşik özelliklerin çalıştığını da içerebilir. Kullanıcı adları, sunucu adları, IP adresleri, kimlik bilgileri ya da sizi veya sunucularınızı tanıtacak hiçbir şey asla gönderilmez. - -Varsayılan olarak açıktır. Yönetici ayarlarında Usage Statistics bölümünden kapatabilir veya neleri içereceğini seçebilir, gönderilecek raporu olduğu gibi önizleyebilirsiniz. Her kullanıcı kendi özellik kullanımını profilinden hariç tutabilir. `ENABLE_TELEMETRY=false` tanımlamak onu kapatır ve anahtarı kilitler. - -
- -## Bağış - -Termix ücretsiz ve açık kaynaklıdır; abonelik ya da ücretli plan yoktur. İşinize yarıyorsa, sunucu, alan adı ve geliştirme süresi masraflarına katkı için bağış yapmayı düşünün. Bağışlar ayrıca SAML, Kubernetes ve aracı desteği gibi özellikler için gereken araştırma ve öğrenme süresini karşılar. İlerlemeyi aşağıdan izleyip bağış yapabilirsiniz. - -[Bağış yap](https://donate.termix.site/) - -
- -## Sponsorlar - -Geliştirmeyi desteklemek için ücretli bir yerleşim ilginizi çeker mi? [mail@termix.site](mailto:mail@termix.site) adresine yazın. - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## Destek - -Yardıma mı ihtiyacınız var ya da bir özellik mi istiyorsunuz? [Yeni bir konu](https://github.com/Termix-SSH/Support/issues) açın ve olabildiğince ayrıntı ekleyin, mümkünse İngilizce yazın. [Discord](https://discord.gg/jVQGdvHDrf) üzerindeki destek kanalında da sorabilirsiniz, ancak oradaki yanıtlar daha uzun sürebilir. - -
- -## Ekran görüntüleri - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -Güncelleme tanıtımlarını YouTube'da izleyin - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -Bazı videolar ve görseller güncelliğini yitirmiş ya da özellikleri tam olarak göstermiyor olabilir. - -
- -
- -## Planlanan özellikler - -Planlanan tüm özellikler [Projects](https://github.com/orgs/Termix-SSH/projects/5) sayfasında. Katkıda bulunmak isterseniz [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md) dosyasına bakın. - -
- -## Lisans - -Apache Lisansı Sürüm 2.0 ile dağıtılır. Ayrıntılar için `LICENSE` dosyasına bakın. diff --git a/docs/readme/README-VI.md b/docs/readme/README-VI.md deleted file mode 100644 index 97913e49c..000000000 --- a/docs/readme/README-VI.md +++ /dev/null @@ -1,500 +0,0 @@ -
- -Termix Logo - -

Termix

- -

Quản lý máy chủ tự lưu trữ, từ SSH và máy tính từ xa cho đến tự động hoá

- -

- English · - 中文 · - 日本語 · - 한국어 · - Français · - Deutsch · - Español · - Português · - Русский · - العربية · - हिन्दी · - Türkçe · - Tiếng Việt · - Italiano -

- -

- - - - Discord - Donate -

- -

- Donations this month -

- -
- -Termix miễn phí và mã nguồn mở. Nếu bạn thấy hữu ích, hãy cân nhắc [quyên góp](https://donate.termix.site/) để giúp trang trải chi phí máy chủ và thời gian phát triển. - -
- -Termix Banner - -
-
- -

- Repo of the Day Achievement -
- Đạt được vào ngày 1 tháng 9 năm 2025 -

- -
- -
- -## Tổng quan - -Termix là nền tảng miễn phí, mã nguồn mở, tự lưu trữ để quản lý máy chủ của bạn. Nó gom vào một chỗ terminal SSH, máy tính từ xa (RDP, VNC, Telnet), truyền tệp, tunnel, Docker, số liệu và tự động hoá, trên web, máy tính và điện thoại. Đây là bản thay thế tự lưu trữ cho Termius và sẽ miễn phí mãi mãi. - -
- -## Tính năng - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -**Terminal SSH:** -Một terminal đầy đủ với các thẻ giống trình duyệt và chia đôi màn hình, tối đa 6 khung cùng lúc. Bạn tự chọn giao diện, phông chữ và màu sắc. Phía trên mỗi phiên có một thanh công cụ hiện CPU, bộ nhớ và ổ đĩa theo thời gian thực, kèm lối tắt tới tệp, Docker, tunnel và số liệu của máy chủ đó. - - - -**Máy tính từ xa:** -RDP, VNC và Telnet ngay trong trình duyệt, dùng thẻ và chia đôi màn hình như mọi phiên khác. Có trình duyệt tệp cho ổ đĩa RDP và tải lên bằng cách kéo thả. Trên máy tính Windows, bạn còn có thể mở máy chủ bằng ứng dụng RDP của hệ điều hành. - -
- -**Tunnel SSH:** -Chuyển tiếp cục bộ, từ xa và SOCKS động, có tự kết nối lại và kiểm tra tình trạng. Tunnel từ máy khách tới máy chủ trong ứng dụng máy tính được lưu ngay trên máy đó, và bạn có thể lưu cấu hình sẵn lên máy chủ để mang sang máy khác. - - - -**Trình quản lý tệp:** -Duyệt, sửa, tải lên, tải xuống, đổi tên, di chuyển và xoá tệp qua SFTP, có hỗ trợ sudo. Xem và sửa mã nguồn, hình ảnh, âm thanh và video. Sao chép tệp thẳng từ máy chủ này sang máy chủ khác, hệ thống tự chọn đường nhanh nhất và kiểm tra tính toàn vẹn khi truyền. - -
- -**Docker và Podman:** -Khởi động, dừng, tạm dừng và xoá container, xem thông số của chúng và mở một shell bên trong. Chạy được với cả Docker lẫn Podman. Nó không nhằm thay thế Portainer hay Dockge, chỉ để quản lý những container bạn đã có. - - - -**Quản lý máy chủ:** -Lưu và sắp xếp máy chủ bằng thẻ và thư mục lồng nhau mà bạn có thể đặt tên và tô màu. Dùng lại thông tin đăng nhập đã lưu cho nhiều máy chủ, tự động triển khai khoá SSH, gom máy chủ dưới một máy chủ cha, sửa và xuất hàng loạt, và dùng kết nối nhanh cho những lần kết nối một lần mà bạn không muốn lưu. - -
- -**Số liệu máy chủ:** -CPU, bộ nhớ, ổ đĩa, mạng, nhiệt độ, thời gian hoạt động, tiến trình, cổng, lượt đăng nhập và thông tin hệ thống trên hầu hết máy chủ Linux, kèm biểu đồ lịch sử. Các thẻ quản lý cho phép bạn xử lý dịch vụ, tác vụ cron, gói phần mềm, người dùng, luật tường lửa, WireGuard, Tailscale, chứng chỉ SSL, nhật ký và kiểm tra tình trạng mà không cần rời Termix. - - - -**Tự động hoá:** -Chọn một điều kiện kích hoạt, rồi nói bạn muốn điều gì xảy ra. Điều kiện gồm một số liệu vượt ngưỡng, một máy chủ sập hoặc sống lại, kiểm tra tình trạng thay đổi, một lịch định sẵn, một sự kiện container, hoặc một webhook gửi đến. Các bước có thể chạy lệnh và đoạn lệnh, điều khiển container và tunnel, đánh thức máy chủ, gọi một địa chỉ, chờ, rẽ nhánh theo điều kiện, chạy một tự động hoá khác, và báo cho bạn qua ntfy, Discord hoặc webhook. Chạy thử giúp bạn kiểm tra an toàn trước. - -
- -**Nhóm máy chủ:** -Gom máy chủ vào một nhóm bằng cách tự chọn hoặc theo luật thẻ, để máy chủ mới tự vào nhóm. Chạy một lệnh trên mọi máy chủ cùng lúc, đẩy và lấy tệp trên tất cả, cài gói phần mềm, và thu thập danh sách hệ điều hành, nhân, kiến trúc và thời gian hoạt động. - - - -**Trợ lý AI:** -Là tuỳ chọn, và tắt cho đến khi bạn tự bật. Kết nối OpenAI, Anthropic, Gemini, Ollama hoặc bất kỳ endpoint tương thích OpenAI nào rồi hỏi về hệ thống của bạn. Nó đọc được máy chủ, nhóm máy chủ, đoạn lệnh và cảnh báo, và đề xuất thay đổi để bạn duyệt chứ không tự làm. Nó không bao giờ chạm được vào thông tin đăng nhập, người dùng hay thiết lập. Quản trị viên có thể tắt hẳn cho cả hệ thống, còn bạn có thể ẩn nó ngay khi cài đặt ban đầu. - -
- -**Đăng nhập và người dùng:** -Tài khoản cục bộ cùng với đăng nhập qua OIDC, LDAP, GitHub và Google, kèm xác thực hai bước (TOTP), passkey (WebAuthn) và thiết bị tin cậy. Quản trị viên có thể quản lý người dùng, ánh xạ nhóm OIDC sang vai trò, xem mọi phiên đang hoạt động trên mọi nền tảng và thu hồi chúng. Bạn có thể liên kết tài khoản cục bộ với tài khoản OIDC, và xem nhật ký kiểm toán về những gì mọi người đã làm. - - - -**Vai trò và chia sẻ:** -Tạo vai trò và chia sẻ máy chủ với người dùng hoặc vai trò theo bốn mức: kết nối, xem, sửa và quản lý. Hoạt động với mọi kiểu xác thực và mọi giao thức, và bạn có thể thay thông tin đăng nhập dùng cho một máy chủ được chia sẻ. - -
- -**Cảnh báo:** -Đặt luật cho các số liệu máy chủ như CPU, bộ nhớ và ổ đĩa, rồi nhận thông báo qua ntfy, Discord hoặc webhook khi chúng kích hoạt. Xem cảnh báo đang bật và đã hết trong nhật ký, và bỏ qua những cái bạn không quan tâm. - - - -**Trang chủ:** -Một lưới tiện ích kéo thả do bạn tự dựng. Có tiện ích cho tình trạng máy chủ, ping, liên kết dịch vụ, dấu trang, tìm kiếm, đồng hồ, lịch, đếm ngược, ghi chú, RSS, thời tiết, hình ảnh, iframe, Docker, tunnel, biểu đồ số liệu, API riêng, và cả một terminal đang chạy. - -
- -**Đoạn lệnh và công cụ:** -Lưu những lệnh bạn hay dùng và chạy chỉ với một cú nhấp, có biến cho máy chủ và cho phần bạn tự nhập. Chạy một lệnh trên tất cả terminal đang mở, và tìm trong lịch sử lệnh với gợi ý tự động. - - - -**Chia sẻ phiên:** -Chia sẻ trực tiếp một phiên terminal, RDP, VNC hoặc Telnet. Gửi một liên kết mà ai cũng vào được không cần tài khoản, hoặc chia sẻ với một người dùng Termix cụ thể, ở chế độ chỉ xem hoặc cho phép thao tác. Chia sẻ có thể tự hết hạn hoặc bị thu hồi bất cứ lúc nào, và có thể tắt toàn bộ hoặc theo từng máy chủ. - -
- -**Ghi phiên và nhật ký:** -Ghi lại phiên terminal, RDP và VNC rồi xem lại sau. Tải nhật ký dạng văn bản của một phiên, và xem nhật ký kết nối để biết chính xác chuyện gì đã xảy ra trong lúc kết nối. - - - -**Kết nối serial:** -Làm việc với thiết bị serial như router, switch và vi điều khiển từ trình duyệt hoặc ứng dụng máy tính. Đặt tốc độ baud, bit dữ liệu, bit dừng và bit chẵn lẻ. Dùng Web Serial API trên các trình duyệt hỗ trợ, hoặc backend gốc trong ứng dụng máy tính. - -
- -**Tailscale:** -Lấy thiết bị từ tailnet của bạn để thêm làm máy chủ chỉ với vài cú nhấp, và kết nối bằng Tailscale SSH để ACL của tailnet lo phần quyền truy cập, không cần lưu thông tin đăng nhập. Headscale và endpoint tuỳ chỉnh cũng dùng được. - - - -**Proxmox:** -Nhập máy chủ thẳng từ một hệ thống Proxmox, và theo dõi số liệu của node và máy ảo, gồm CPU, bộ nhớ và dung lượng, trong một thẻ riêng. - -
- -**Không gian làm việc và thẻ:** -Lưu một bộ thẻ cùng cách chia màn hình rồi mở lại toàn bộ chỉ với một cú nhấp. Termix cũng nhớ phiên gần nhất, nên các thẻ của bạn quay lại sau khi tải lại trang và trên thiết bị khác. - - - -**Dòng lệnh:** -Công cụ `termix` cho shell và các script của bạn. Mở terminal, chạy một lệnh trên một máy chủ hoặc cả một nhóm, chuyển tệp qua SFTP, và quản lý máy chủ, đoạn lệnh và thông tin đăng nhập. Cài bằng `npm install -g @termix-ssh/cli` hoặc tải bản chạy độc lập. Xem [tài liệu CLI](https://docs.termix.site/cli). - -
- -**Bảo mật:** -Mật khẩu, khoá và các thông tin bí mật khác được mã hoá theo từng người dùng, và bản thân các tệp cơ sở dữ liệu cũng có thể mã hoá trên ổ đĩa. Xem [tài liệu](https://docs.termix.site/features/authentication/security/) để biết cách hoạt động. - - - -**Ngôn ngữ:** -Có sẵn khoảng 30 ngôn ngữ, quản lý qua [Crowdin](https://docs.termix.site/translations). - -
- -
- -
-Thêm tính năng khác -
- -- **Bảng điều khiển** - Nhìn nhanh toàn bộ máy chủ, với các thẻ do bạn tự sắp xếp -- **Sơ đồ mạng** - Vẽ homelab của bạn từ danh sách máy chủ, kèm trạng thái theo thời gian thực -- **Theo dõi tmux** - Xem các phiên, cửa sổ và khung tmux, có xem trước và tìm kiếm -- **Khoá API** - Khoá theo từng người dùng có ngày hết hạn, dùng cho script và CI -- **Xuất và nhập** - Chuyển máy chủ, thông tin đăng nhập và dữ liệu trình quản lý tệp ra vào -- **SSL tự động** - Chứng chỉ được tạo và gia hạn giúp bạn, kèm chuyển hướng HTTPS, hoặc dùng chứng chỉ của riêng bạn -- **Cơ sở dữ liệu** - Mặc định là SQLite, đồng thời hỗ trợ PostgreSQL và MySQL -- **Giao diện hiện đại** - Giao diện React gọn gàng chạy tốt trên máy tính và điện thoại, với các chủ đề như sáng, tối và Dracula. Mọi kết nối đều mở toàn màn hình được từ một địa chỉ -- **Bảng lệnh** - Nhấn hai lần phím Shift trái để nhảy tới một máy chủ bằng bàn phím -- **Phím tắt** - Chuyển giữa các thẻ, đóng thẻ và nhiều thao tác khác, đều gán lại được -- **Wake-on-LAN** - Đánh thức một máy từ Termix hoặc từ một bước tự động hoá -- **Xác thực qua proxy tin cậy** - Để reverse proxy lo phần đăng nhập rồi chuyển thông tin người dùng vào -- **Nhãn trắng** - Quản trị viên có thể đổi thương hiệu hệ thống với tên, logo và màu sắc riêng -- **Điểm cuối web** - Mở giao diện web riêng của một máy chủ, chẳng hạn trang quản trị router, ngay trong Termix thay vì mở một thẻ riêng -- **Phòng cộng tác** - Phòng lâu dài để cả nhóm cùng chuyển qua lại giữa các phiên, có sân khấu người trình bày và lời mời -- **SSH nhiều tính năng** - Máy chủ trung gian, Warpgate, hỏi mã TOTP, SOCKS5, kiểm tra khoá máy chủ, tự điền mật khẩu, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, ghi nhật ký terminal, chuyển tiếp agent, SSH agent của Bitwarden, ký SSH bằng HashiCorp Vault, Step CA, 1Password Connect và nhiều thứ khác -- **Termix ID** - Bản dựng sẵn theo kiểu sshid.io. Đăng ký một tên, công bố khoá công khai của bạn tại một địa chỉ phân giải, và cấp chứng chỉ SSH từ CA tích hợp -- **Gợi ý lịch sử lệnh** - Gợi ý ngay trong terminal khi bạn gõ, dựa trên lịch sử lệnh của bạn -- **Ứng dụng máy tính độc lập và đồng bộ** - Ứng dụng máy tính chạy độc lập với backend và cơ sở dữ liệu riêng, có thể đồng bộ tuỳ chọn với một máy chủ Termix - -
- -
- -## Nền tảng hỗ trợ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nền tảngBản phân phối
WebMọi trình duyệt hiện đại (Chrome, Safari, Firefox) · Hỗ trợ PWA
Windows x64/ia32Bản chạy ngay · Bộ cài MSI · Chocolatey
Linux x64/ia32Bản chạy ngay · AUR · AppImage · Deb · Flatpak
macOS x64/ia32, v12.0+Apple App Store · DMG · Homebrew
iOS/iPadOS v15.1+Apple App Store · IPA
Android v7.0+Google Play Store · APK
- -
- -## Cài đặt - -Xem [tài liệu Termix](https://docs.termix.site/install) để có hướng dẫn cài đặt đầy đủ trên mọi nền tảng. - -Tệp Docker Compose mẫu (bạn có thể bỏ `guacd` và phần mạng nếu không định dùng máy tính từ xa): - -```yaml -services: - termix: - image: ghcr.io/lukegus/termix:latest - container_name: termix - restart: unless-stopped - ports: - - "8080:8080" - volumes: - - termix-data:/app/data - environment: - PORT: "8080" - depends_on: - - guacd - networks: - - termix-net - - guacd: - image: guacamole/guacd:1.6.0 - container_name: guacd - restart: unless-stopped - ports: - - "4822:4822" - networks: - - termix-net - -volumes: - termix-data: - driver: local - -networks: - termix-net: - driver: bridge -``` - -### Dòng lệnh - -Termix cũng có CLI, để bạn quản lý máy chủ từ terminal và dùng Termix trong script của mình. - -```bash -npm install -g @termix-ssh/cli -termix login --url https://termix.example.com -termix ssh 1 -``` - -Nó mở được terminal, chạy lệnh trên một máy chủ hoặc cả một nhóm, chuyển tệp qua SFTP, và quản lý máy chủ, đoạn lệnh và thông tin đăng nhập. Tài liệu đầy đủ ở [docs.termix.site/cli](https://docs.termix.site/cli). - -### Chạy trên cloud - -Bạn có thể chạy máy chủ Termix trên VPS thay vì trong mạng của mình. Nếu Termix chạy ngay trong mạng mà nó quản lý, một sự cố sẽ kéo nó sập theo, đúng lúc bạn cần nó để sửa. Chạy ở ngoài thì nó luôn truy cập được, cho bạn một IP cố định và vào được từ bất cứ đâu mà không cần VPN hay mở cổng. - -[GINERNET](https://docs.termix.site/install/ginernet) là nhà tài trợ của Termix, và tài liệu có hướng dẫn từng bước để triển khai trên nền tảng VPS của họ. - -
- -## Dữ liệu sử dụng - -Mỗi ngày một lần, Termix gửi một báo cáo ẩn danh nhỏ để tôi biết có bao nhiêu bản cài đặt đang chạy và tính năng nào được dùng. Báo cáo gồm một ID ngẫu nhiên của bản cài đặt, phiên bản ứng dụng, và số người dùng cùng số máy chủ của bạn. Nó cũng có thể gồm nền tảng của bạn (hệ điều hành, kiến trúc, phiên bản Node.js, loại cơ sở dữ liệu, và bạn chạy bằng Docker, ứng dụng máy tính hay máy chủ thường), số lần mỗi loại tab được mở và số lần đăng nhập SSH, cùng các tính năng tích hợp đang chạy. Nó không bao giờ chứa tên người dùng, tên máy chủ, địa chỉ IP, thông tin đăng nhập hay bất cứ thứ gì có thể nhận ra bạn hoặc máy chủ của bạn. - -Mặc định là bật. Bạn có thể tắt hoặc chọn nội dung gửi đi trong phần Cài đặt quản trị, mục Usage Statistics, và xem trước chính xác báo cáo sẽ được gửi. Mỗi người dùng có thể loại phần sử dụng tính năng của riêng mình trong hồ sơ. Đặt `ENABLE_TELEMETRY=false` sẽ tắt nó và khóa công tắc. - -
- -## Quyên góp - -Termix miễn phí và mã nguồn mở, không có gói thuê bao hay bản trả phí. Nếu bạn thấy hữu ích, hãy cân nhắc quyên góp để giúp trang trải máy chủ, tên miền và thời gian phát triển. Quyên góp cũng giúp có thời gian tìm hiểu những thứ cần thiết cho các tính năng như SAML, Kubernetes và hỗ trợ agent. Theo dõi tiến độ và quyên góp ở bên dưới. - -[Quyên góp](https://donate.termix.site/) - -
- -## Nhà tài trợ - -Bạn muốn đặt quảng cáo trả phí để ủng hộ việc phát triển? Gửi thư tới [mail@termix.site](mailto:mail@termix.site). - - - -
- -
- - - DigitalOcean - -    - - Crowdin - -    - - Blacksmith - -    - - Cloudflare - -    - - Akamai - -    - - AWS - -    - - Rack Genius - -    - - Ginernet - -    - - Hetzner - - -
- - - -
- -## Hỗ trợ - -Cần giúp đỡ hoặc muốn đề xuất tính năng? Hãy mở một [issue mới](https://github.com/Termix-SSH/Support/issues) và mô tả càng chi tiết càng tốt, bằng tiếng Anh nếu được. Bạn cũng có thể hỏi trong kênh hỗ trợ trên [Discord](https://discord.gg/jVQGdvHDrf), tuy nhiên ở đó có thể lâu được trả lời hơn. - -
- -## Ảnh chụp màn hình - -
- -
- -[![YouTube](../repo-images/YouTube.png)](https://www.youtube.com/@TermixSSH/videos) - -Xem giới thiệu các bản cập nhật trên YouTube - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Termix Screenshot 1Termix Screenshot 2
Termix Screenshot 3Termix Screenshot 4
Termix Screenshot 5Termix Screenshot 6
Termix Screenshot 7Termix Screenshot 8
Termix Screenshot 9Termix Screenshot 10
Termix Screenshot 11Termix Screenshot 12
Termix Screenshot 13Termix Screenshot 14
Termix Screenshot 15Termix Screenshot 16
- -Một số video và hình ảnh có thể đã cũ hoặc chưa thể hiện đầy đủ tính năng. - -
- -
- -## Tính năng dự kiến - -Toàn bộ tính năng dự kiến nằm ở [Projects](https://github.com/orgs/Termix-SSH/projects/5). Nếu bạn muốn đóng góp, xem [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). - -
- -## Giấy phép - -Phát hành theo Giấy phép Apache phiên bản 2.0. Xem `LICENSE` để biết thêm chi tiết. diff --git a/docs/repo-images/Image 1.png b/docs/repo-images/Image 1.png deleted file mode 100644 index 0a31a3177..000000000 Binary files a/docs/repo-images/Image 1.png and /dev/null differ diff --git a/docs/repo-images/Image 10.png b/docs/repo-images/Image 10.png deleted file mode 100644 index 218bbd05c..000000000 Binary files a/docs/repo-images/Image 10.png and /dev/null differ diff --git a/docs/repo-images/Image 11.png b/docs/repo-images/Image 11.png deleted file mode 100644 index 0952dcb73..000000000 Binary files a/docs/repo-images/Image 11.png and /dev/null differ diff --git a/docs/repo-images/Image 12.png b/docs/repo-images/Image 12.png deleted file mode 100644 index 11047553a..000000000 Binary files a/docs/repo-images/Image 12.png and /dev/null differ diff --git a/docs/repo-images/Image 13.png b/docs/repo-images/Image 13.png deleted file mode 100644 index 37c7d418a..000000000 Binary files a/docs/repo-images/Image 13.png and /dev/null differ diff --git a/docs/repo-images/Image 14.png b/docs/repo-images/Image 14.png deleted file mode 100644 index a4779bc50..000000000 Binary files a/docs/repo-images/Image 14.png and /dev/null differ diff --git a/docs/repo-images/Image 15.png b/docs/repo-images/Image 15.png deleted file mode 100644 index 4180e3ba2..000000000 Binary files a/docs/repo-images/Image 15.png and /dev/null differ diff --git a/docs/repo-images/Image 16.png b/docs/repo-images/Image 16.png deleted file mode 100644 index 8837bb97d..000000000 Binary files a/docs/repo-images/Image 16.png and /dev/null differ diff --git a/docs/repo-images/Image 2.png b/docs/repo-images/Image 2.png deleted file mode 100644 index 56dd4bc27..000000000 Binary files a/docs/repo-images/Image 2.png and /dev/null differ diff --git a/docs/repo-images/Image 3.png b/docs/repo-images/Image 3.png deleted file mode 100644 index d8bab6220..000000000 Binary files a/docs/repo-images/Image 3.png and /dev/null differ diff --git a/docs/repo-images/Image 4.png b/docs/repo-images/Image 4.png deleted file mode 100644 index bb0819079..000000000 Binary files a/docs/repo-images/Image 4.png and /dev/null differ diff --git a/docs/repo-images/Image 5.png b/docs/repo-images/Image 5.png deleted file mode 100644 index 164700669..000000000 Binary files a/docs/repo-images/Image 5.png and /dev/null differ diff --git a/docs/repo-images/Image 6.png b/docs/repo-images/Image 6.png deleted file mode 100644 index c4bdc37ac..000000000 Binary files a/docs/repo-images/Image 6.png and /dev/null differ diff --git a/docs/repo-images/Image 7.png b/docs/repo-images/Image 7.png deleted file mode 100644 index bd6c08e9e..000000000 Binary files a/docs/repo-images/Image 7.png and /dev/null differ diff --git a/docs/repo-images/Image 8.png b/docs/repo-images/Image 8.png deleted file mode 100644 index 46d72226a..000000000 Binary files a/docs/repo-images/Image 8.png and /dev/null differ diff --git a/docs/repo-images/Image 9.png b/docs/repo-images/Image 9.png deleted file mode 100644 index 6427e02e5..000000000 Binary files a/docs/repo-images/Image 9.png and /dev/null differ diff --git a/docs/repo-images/Repo of the Day.png b/docs/repo-images/Repo of the Day.png deleted file mode 100644 index e50a1b6bc..000000000 Binary files a/docs/repo-images/Repo of the Day.png and /dev/null differ diff --git a/docs/repo-images/Termix Header.png b/docs/repo-images/Termix Header.png deleted file mode 100644 index 249637ead..000000000 Binary files a/docs/repo-images/Termix Header.png and /dev/null differ diff --git a/docs/repo-images/YouTube.png b/docs/repo-images/YouTube.png deleted file mode 100644 index 372eaeab2..000000000 Binary files a/docs/repo-images/YouTube.png and /dev/null differ diff --git a/drizzle/mysql/0066_retire_2_8_columns.sql b/drizzle/mysql/0066_retire_2_8_columns.sql new file mode 100644 index 000000000..29454b4f3 --- /dev/null +++ b/drizzle/mysql/0066_retire_2_8_columns.sql @@ -0,0 +1,28 @@ +ALTER TABLE `ssh_folders` DROP FOREIGN KEY `ssh_folders_credential_id_ssh_credentials_id_fk`; +--> statement-breakpoint +ALTER TABLE `ssh_data` DROP COLUMN `terminal_config`;--> statement-breakpoint +ALTER TABLE `ssh_data` DROP COLUMN `quick_actions`;--> statement-breakpoint +ALTER TABLE `sessions` DROP COLUMN `oidc_sub`;--> statement-breakpoint +ALTER TABLE `sessions` DROP COLUMN `oidc_sid`;--> statement-breakpoint +ALTER TABLE `sessions` DROP COLUMN `sso_provider_id`;--> statement-breakpoint +ALTER TABLE `ssh_folders` DROP COLUMN `credential_id`;--> statement-breakpoint +ALTER TABLE `user_preferences` DROP COLUMN `command_autocomplete`;--> statement-breakpoint +ALTER TABLE `user_preferences` DROP COLUMN `confirm_snippet_execution`;--> statement-breakpoint +ALTER TABLE `user_preferences` DROP COLUMN `custom_themes`;--> statement-breakpoint +ALTER TABLE `user_preferences` DROP COLUMN `terminal_defaults`;--> statement-breakpoint +ALTER TABLE `user_preferences` DROP COLUMN `terminal_macros`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `is_oidc`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `oidc_identifier`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `sso_provider_id`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `client_id`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `client_secret`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `issuer_url`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `authorization_url`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `token_url`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `identifier_path`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `name_path`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `scopes`;--> statement-breakpoint +DELETE FROM `settings` WHERE `key` IN ('host_defaults', 'analytics_enabled', 'analytics_instance_id');--> statement-breakpoint +UPDATE `ssh_data` SET `username` = REPLACE(`username`, '$oidc.preferred_username', '$external.username') WHERE `username` LIKE '%$oidc.preferred_username%';--> statement-breakpoint +UPDATE `ssh_credentials` SET `username` = REPLACE(`username`, '$oidc.preferred_username', '$external.username') WHERE `username` LIKE '%$oidc.preferred_username%';--> statement-breakpoint +UPDATE `host_defaults` SET `value` = REPLACE(`value`, '$oidc.preferred_username', '$external.username') WHERE `value` LIKE '%$oidc.preferred_username%'; \ No newline at end of file diff --git a/drizzle/mysql/0067_external_session_ref.sql b/drizzle/mysql/0067_external_session_ref.sql new file mode 100644 index 000000000..f8b8402a8 --- /dev/null +++ b/drizzle/mysql/0067_external_session_ref.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD `external_session_ref` text; \ No newline at end of file diff --git a/drizzle/mysql/0068_plugin_pinned_version.sql b/drizzle/mysql/0068_plugin_pinned_version.sql new file mode 100644 index 000000000..91a5c68cf --- /dev/null +++ b/drizzle/mysql/0068_plugin_pinned_version.sql @@ -0,0 +1 @@ +ALTER TABLE `plugins` ADD `pinned_version` text; \ No newline at end of file diff --git a/drizzle/mysql/0069_plugin_channel.sql b/drizzle/mysql/0069_plugin_channel.sql new file mode 100644 index 000000000..62a5bbcc4 --- /dev/null +++ b/drizzle/mysql/0069_plugin_channel.sql @@ -0,0 +1 @@ +ALTER TABLE `plugins` ADD `channel` text DEFAULT ('stable') NOT NULL; \ No newline at end of file diff --git a/drizzle/mysql/0070_drop_donation_modal.sql b/drizzle/mysql/0070_drop_donation_modal.sql new file mode 100644 index 000000000..710110f62 --- /dev/null +++ b/drizzle/mysql/0070_drop_donation_modal.sql @@ -0,0 +1 @@ +ALTER TABLE `users` DROP COLUMN `donation_modal_dismissed`; \ No newline at end of file diff --git a/drizzle/mysql/meta/0066_snapshot.json b/drizzle/mysql/meta/0066_snapshot.json new file mode 100644 index 000000000..146cbe3da --- /dev/null +++ b/drizzle/mysql/meta/0066_snapshot.json @@ -0,0 +1,4731 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "2fdeddc6-f532-43a0-8dfd-51791cc3cf46", + "prevId": "d8cbd86b-0ab0-46ac-bab2-b47ddfa2a4fb", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('use')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_access_id": { + "name": "credential_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "folder_access_id": { + "name": "folder_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "enum('admin','user','folder')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_defaults_id": { + "name": "host_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('direct')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_protocol_auth_id": { + "name": "host_protocol_auth_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('aggregate-telemetry')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_install_counts_id": { + "name": "plugin_install_counts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_migrations_id": { + "name": "plugin_migrations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('admin')" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_permission_grants_id": { + "name": "plugin_permission_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_registries_id": { + "name": "plugin_registries_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "enum('admin','user','host','secret')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_settings_id": { + "name": "plugin_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_storage_id": { + "name": "plugin_storage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('available')" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('disabled')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugins_id": { + "name": "plugins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_applied_defaults_id": { + "name": "rbac_applied_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_known_permissions_id": { + "name": "rbac_known_permissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('password')" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_credential_secrets_id": { + "name": "shared_credential_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_conflicts_id": { + "name": "sync_conflicts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_link": { + "name": "sync_link", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('idle')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_link_id": { + "name": "sync_link_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_records": { + "name": "sync_records", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_records_id": { + "name": "sync_records_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_external_identities_id": { + "name": "user_external_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_second_factors_id": { + "name": "user_second_factors_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0067_snapshot.json b/drizzle/mysql/meta/0067_snapshot.json new file mode 100644 index 000000000..5d628221c --- /dev/null +++ b/drizzle/mysql/meta/0067_snapshot.json @@ -0,0 +1,4738 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "b91f5f72-d0db-46df-915c-f8bce033cd9a", + "prevId": "2fdeddc6-f532-43a0-8dfd-51791cc3cf46", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('use')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_access_id": { + "name": "credential_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "folder_access_id": { + "name": "folder_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "enum('admin','user','folder')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_defaults_id": { + "name": "host_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('direct')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_protocol_auth_id": { + "name": "host_protocol_auth_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('aggregate-telemetry')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_install_counts_id": { + "name": "plugin_install_counts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_migrations_id": { + "name": "plugin_migrations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('admin')" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_permission_grants_id": { + "name": "plugin_permission_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_registries_id": { + "name": "plugin_registries_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "enum('admin','user','host','secret')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_settings_id": { + "name": "plugin_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_storage_id": { + "name": "plugin_storage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('available')" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('disabled')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugins_id": { + "name": "plugins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_applied_defaults_id": { + "name": "rbac_applied_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_known_permissions_id": { + "name": "rbac_known_permissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('password')" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_credential_secrets_id": { + "name": "shared_credential_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_conflicts_id": { + "name": "sync_conflicts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_link": { + "name": "sync_link", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('idle')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_link_id": { + "name": "sync_link_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_records": { + "name": "sync_records", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_records_id": { + "name": "sync_records_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_external_identities_id": { + "name": "user_external_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_second_factors_id": { + "name": "user_second_factors_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0068_snapshot.json b/drizzle/mysql/meta/0068_snapshot.json new file mode 100644 index 000000000..ba7bb0608 --- /dev/null +++ b/drizzle/mysql/meta/0068_snapshot.json @@ -0,0 +1,4745 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "5b26c0bd-97f5-4351-859b-b28056467f20", + "prevId": "b91f5f72-d0db-46df-915c-f8bce033cd9a", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('use')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_access_id": { + "name": "credential_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "folder_access_id": { + "name": "folder_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "enum('admin','user','folder')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_defaults_id": { + "name": "host_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('direct')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_protocol_auth_id": { + "name": "host_protocol_auth_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('aggregate-telemetry')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_install_counts_id": { + "name": "plugin_install_counts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_migrations_id": { + "name": "plugin_migrations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('admin')" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_permission_grants_id": { + "name": "plugin_permission_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_registries_id": { + "name": "plugin_registries_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "enum('admin','user','host','secret')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_settings_id": { + "name": "plugin_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_storage_id": { + "name": "plugin_storage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('available')" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('disabled')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugins_id": { + "name": "plugins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_applied_defaults_id": { + "name": "rbac_applied_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_known_permissions_id": { + "name": "rbac_known_permissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('password')" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_credential_secrets_id": { + "name": "shared_credential_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_conflicts_id": { + "name": "sync_conflicts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_link": { + "name": "sync_link", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('idle')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_link_id": { + "name": "sync_link_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_records": { + "name": "sync_records", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_records_id": { + "name": "sync_records_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_external_identities_id": { + "name": "user_external_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_second_factors_id": { + "name": "user_second_factors_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0069_snapshot.json b/drizzle/mysql/meta/0069_snapshot.json new file mode 100644 index 000000000..69ab3503e --- /dev/null +++ b/drizzle/mysql/meta/0069_snapshot.json @@ -0,0 +1,4753 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "defefebd-f15d-4754-bf1b-0be8dd7be872", + "prevId": "5b26c0bd-97f5-4351-859b-b28056467f20", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('use')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_access_id": { + "name": "credential_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "folder_access_id": { + "name": "folder_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "enum('admin','user','folder')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_defaults_id": { + "name": "host_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('direct')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_protocol_auth_id": { + "name": "host_protocol_auth_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('aggregate-telemetry')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_install_counts_id": { + "name": "plugin_install_counts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_migrations_id": { + "name": "plugin_migrations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('admin')" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_permission_grants_id": { + "name": "plugin_permission_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_registries_id": { + "name": "plugin_registries_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "enum('admin','user','host','secret')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_settings_id": { + "name": "plugin_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_storage_id": { + "name": "plugin_storage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('available')" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('disabled')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('stable')" + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugins_id": { + "name": "plugins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_applied_defaults_id": { + "name": "rbac_applied_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_known_permissions_id": { + "name": "rbac_known_permissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('password')" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_credential_secrets_id": { + "name": "shared_credential_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_conflicts_id": { + "name": "sync_conflicts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_link": { + "name": "sync_link", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('idle')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_link_id": { + "name": "sync_link_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_records": { + "name": "sync_records", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_records_id": { + "name": "sync_records_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_external_identities_id": { + "name": "user_external_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_second_factors_id": { + "name": "user_second_factors_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/0070_snapshot.json b/drizzle/mysql/meta/0070_snapshot.json new file mode 100644 index 000000000..bf80530d0 --- /dev/null +++ b/drizzle/mysql/meta/0070_snapshot.json @@ -0,0 +1,4745 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "dd028966-4309-4238-8385-f189bf1e5778", + "prevId": "defefebd-f15d-4754-bf1b-0be8dd7be872", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('use')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_access_id": { + "name": "credential_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "folder_access_id": { + "name": "folder_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "enum('admin','user','folder')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_defaults_id": { + "name": "host_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('direct')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_protocol_auth_id": { + "name": "host_protocol_auth_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('aggregate-telemetry')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_install_counts_id": { + "name": "plugin_install_counts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_migrations_id": { + "name": "plugin_migrations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('admin')" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_permission_grants_id": { + "name": "plugin_permission_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_registries_id": { + "name": "plugin_registries_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "enum('admin','user','host','secret')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_settings_id": { + "name": "plugin_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_storage_id": { + "name": "plugin_storage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('available')" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('disabled')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('stable')" + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugins_id": { + "name": "plugins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_applied_defaults_id": { + "name": "rbac_applied_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_known_permissions_id": { + "name": "rbac_known_permissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('password')" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_credential_secrets_id": { + "name": "shared_credential_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_conflicts_id": { + "name": "sync_conflicts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_link": { + "name": "sync_link", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('idle')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_link_id": { + "name": "sync_link_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_records": { + "name": "sync_records", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_records_id": { + "name": "sync_records_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_external_identities_id": { + "name": "user_external_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_second_factors_id": { + "name": "user_second_factors_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/_journal.json b/drizzle/mysql/meta/_journal.json index ebf03aab8..59685b8cd 100644 --- a/drizzle/mysql/meta/_journal.json +++ b/drizzle/mysql/meta/_journal.json @@ -463,6 +463,41 @@ "when": 1790742086934, "tag": "0065_ambiguous_agent_zero", "breakpoints": true + }, + { + "idx": 66, + "version": "5", + "when": 1791209297104, + "tag": "0066_retire_2_8_columns", + "breakpoints": true + }, + { + "idx": 67, + "version": "5", + "when": 1791209311841, + "tag": "0067_external_session_ref", + "breakpoints": true + }, + { + "idx": 68, + "version": "5", + "when": 1791256341049, + "tag": "0068_plugin_pinned_version", + "breakpoints": true + }, + { + "idx": 69, + "version": "5", + "when": 1791388888997, + "tag": "0069_plugin_channel", + "breakpoints": true + }, + { + "idx": 70, + "version": "5", + "when": 1791550497926, + "tag": "0070_drop_donation_modal", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/postgres/0066_retire_2_8_columns.sql b/drizzle/postgres/0066_retire_2_8_columns.sql new file mode 100644 index 000000000..068f9b61c --- /dev/null +++ b/drizzle/postgres/0066_retire_2_8_columns.sql @@ -0,0 +1,28 @@ +ALTER TABLE "ssh_folders" DROP CONSTRAINT "ssh_folders_credential_id_ssh_credentials_id_fk"; +--> statement-breakpoint +ALTER TABLE "ssh_data" DROP COLUMN "terminal_config";--> statement-breakpoint +ALTER TABLE "ssh_data" DROP COLUMN "quick_actions";--> statement-breakpoint +ALTER TABLE "sessions" DROP COLUMN "oidc_sub";--> statement-breakpoint +ALTER TABLE "sessions" DROP COLUMN "oidc_sid";--> statement-breakpoint +ALTER TABLE "sessions" DROP COLUMN "sso_provider_id";--> statement-breakpoint +ALTER TABLE "ssh_folders" DROP COLUMN "credential_id";--> statement-breakpoint +ALTER TABLE "user_preferences" DROP COLUMN "command_autocomplete";--> statement-breakpoint +ALTER TABLE "user_preferences" DROP COLUMN "confirm_snippet_execution";--> statement-breakpoint +ALTER TABLE "user_preferences" DROP COLUMN "custom_themes";--> statement-breakpoint +ALTER TABLE "user_preferences" DROP COLUMN "terminal_defaults";--> statement-breakpoint +ALTER TABLE "user_preferences" DROP COLUMN "terminal_macros";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "is_oidc";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "oidc_identifier";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "sso_provider_id";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "client_id";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "client_secret";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "issuer_url";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "authorization_url";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "token_url";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "identifier_path";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "name_path";--> statement-breakpoint +ALTER TABLE "users" DROP COLUMN "scopes";--> statement-breakpoint +DELETE FROM "settings" WHERE "key" IN ('host_defaults', 'analytics_enabled', 'analytics_instance_id');--> statement-breakpoint +UPDATE "ssh_data" SET "username" = REPLACE("username", '$oidc.preferred_username', '$external.username') WHERE "username" LIKE '%$oidc.preferred_username%';--> statement-breakpoint +UPDATE "ssh_credentials" SET "username" = REPLACE("username", '$oidc.preferred_username', '$external.username') WHERE "username" LIKE '%$oidc.preferred_username%';--> statement-breakpoint +UPDATE "host_defaults" SET "value" = REPLACE("value", '$oidc.preferred_username', '$external.username') WHERE "value" LIKE '%$oidc.preferred_username%'; \ No newline at end of file diff --git a/drizzle/postgres/0067_external_session_ref.sql b/drizzle/postgres/0067_external_session_ref.sql new file mode 100644 index 000000000..896b7b1c3 --- /dev/null +++ b/drizzle/postgres/0067_external_session_ref.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "external_session_ref" text; \ No newline at end of file diff --git a/drizzle/postgres/0068_plugin_pinned_version.sql b/drizzle/postgres/0068_plugin_pinned_version.sql new file mode 100644 index 000000000..4acdc2f16 --- /dev/null +++ b/drizzle/postgres/0068_plugin_pinned_version.sql @@ -0,0 +1 @@ +ALTER TABLE "plugins" ADD COLUMN "pinned_version" text; \ No newline at end of file diff --git a/drizzle/postgres/0069_plugin_channel.sql b/drizzle/postgres/0069_plugin_channel.sql new file mode 100644 index 000000000..11bbc13e5 --- /dev/null +++ b/drizzle/postgres/0069_plugin_channel.sql @@ -0,0 +1 @@ +ALTER TABLE "plugins" ADD COLUMN "channel" text DEFAULT 'stable' NOT NULL; \ No newline at end of file diff --git a/drizzle/postgres/0070_drop_donation_modal.sql b/drizzle/postgres/0070_drop_donation_modal.sql new file mode 100644 index 000000000..fae895342 --- /dev/null +++ b/drizzle/postgres/0070_drop_donation_modal.sql @@ -0,0 +1 @@ +ALTER TABLE "users" DROP COLUMN "donation_modal_dismissed"; \ No newline at end of file diff --git a/drizzle/postgres/meta/0066_snapshot.json b/drizzle/postgres/meta/0066_snapshot.json new file mode 100644 index 000000000..bd12fc436 --- /dev/null +++ b/drizzle/postgres/meta/0066_snapshot.json @@ -0,0 +1,4758 @@ +{ + "id": "63dca749-99fe-403c-8a34-84c4ac5cdab3", + "prevId": "772f1576-e033-4d2b-8e40-c7b3d499058e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_access": { + "name": "credential_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder_access": { + "name": "folder_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_defaults": { + "name": "host_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_protocol_auth": { + "name": "host_protocol_auth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_install_counts": { + "name": "plugin_install_counts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_permission_grants": { + "name": "plugin_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_registries": { + "name": "plugin_registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_settings": { + "name": "plugin_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + { + "expression": "role_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_known_permissions": { + "name": "rbac_known_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_credential_secrets": { + "name": "shared_credential_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + { + "expression": "credential_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_conflicts": { + "name": "sync_conflicts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_link": { + "name": "sync_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_records": { + "name": "sync_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sync_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_external_identities": { + "name": "user_external_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_second_factors": { + "name": "user_second_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "factor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0067_snapshot.json b/drizzle/postgres/meta/0067_snapshot.json new file mode 100644 index 000000000..c0aa8dcd6 --- /dev/null +++ b/drizzle/postgres/meta/0067_snapshot.json @@ -0,0 +1,4764 @@ +{ + "id": "0e009e23-d22a-4b65-a142-0887e2f243a3", + "prevId": "63dca749-99fe-403c-8a34-84c4ac5cdab3", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_access": { + "name": "credential_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder_access": { + "name": "folder_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_defaults": { + "name": "host_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_protocol_auth": { + "name": "host_protocol_auth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_install_counts": { + "name": "plugin_install_counts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_permission_grants": { + "name": "plugin_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_registries": { + "name": "plugin_registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_settings": { + "name": "plugin_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + { + "expression": "role_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_known_permissions": { + "name": "rbac_known_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_credential_secrets": { + "name": "shared_credential_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + { + "expression": "credential_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_conflicts": { + "name": "sync_conflicts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_link": { + "name": "sync_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_records": { + "name": "sync_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sync_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_external_identities": { + "name": "user_external_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_second_factors": { + "name": "user_second_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "factor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0068_snapshot.json b/drizzle/postgres/meta/0068_snapshot.json new file mode 100644 index 000000000..89d6bf20f --- /dev/null +++ b/drizzle/postgres/meta/0068_snapshot.json @@ -0,0 +1,4770 @@ +{ + "id": "6d2db0b8-e68a-4537-9b6e-1e232c591629", + "prevId": "0e009e23-d22a-4b65-a142-0887e2f243a3", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_access": { + "name": "credential_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder_access": { + "name": "folder_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_defaults": { + "name": "host_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_protocol_auth": { + "name": "host_protocol_auth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_install_counts": { + "name": "plugin_install_counts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_permission_grants": { + "name": "plugin_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_registries": { + "name": "plugin_registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_settings": { + "name": "plugin_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + { + "expression": "role_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_known_permissions": { + "name": "rbac_known_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_credential_secrets": { + "name": "shared_credential_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + { + "expression": "credential_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_conflicts": { + "name": "sync_conflicts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_link": { + "name": "sync_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_records": { + "name": "sync_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sync_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_external_identities": { + "name": "user_external_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_second_factors": { + "name": "user_second_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "factor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0069_snapshot.json b/drizzle/postgres/meta/0069_snapshot.json new file mode 100644 index 000000000..e54f8c821 --- /dev/null +++ b/drizzle/postgres/meta/0069_snapshot.json @@ -0,0 +1,4777 @@ +{ + "id": "6f40b539-787c-4649-be7d-5e59d30e5e43", + "prevId": "6d2db0b8-e68a-4537-9b6e-1e232c591629", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_access": { + "name": "credential_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder_access": { + "name": "folder_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_defaults": { + "name": "host_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_protocol_auth": { + "name": "host_protocol_auth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_install_counts": { + "name": "plugin_install_counts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_permission_grants": { + "name": "plugin_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_registries": { + "name": "plugin_registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_settings": { + "name": "plugin_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stable'" + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + { + "expression": "role_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_known_permissions": { + "name": "rbac_known_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_credential_secrets": { + "name": "shared_credential_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + { + "expression": "credential_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_conflicts": { + "name": "sync_conflicts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_link": { + "name": "sync_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_records": { + "name": "sync_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sync_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_external_identities": { + "name": "user_external_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_second_factors": { + "name": "user_second_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "factor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/0070_snapshot.json b/drizzle/postgres/meta/0070_snapshot.json new file mode 100644 index 000000000..a1c42a307 --- /dev/null +++ b/drizzle/postgres/meta/0070_snapshot.json @@ -0,0 +1,4770 @@ +{ + "id": "1ac7b3b1-fa09-4500-807f-70086a63950d", + "prevId": "6f40b539-787c-4649-be7d-5e59d30e5e43", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_access": { + "name": "credential_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder_access": { + "name": "folder_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_defaults": { + "name": "host_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_protocol_auth": { + "name": "host_protocol_auth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_install_counts": { + "name": "plugin_install_counts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_permission_grants": { + "name": "plugin_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_registries": { + "name": "plugin_registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_settings": { + "name": "plugin_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stable'" + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + { + "expression": "role_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_known_permissions": { + "name": "rbac_known_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_credential_secrets": { + "name": "shared_credential_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + { + "expression": "credential_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_conflicts": { + "name": "sync_conflicts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_link": { + "name": "sync_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_records": { + "name": "sync_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sync_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_external_identities": { + "name": "user_external_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_second_factors": { + "name": "user_second_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "factor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/_journal.json b/drizzle/postgres/meta/_journal.json index d60309508..60f7988d8 100644 --- a/drizzle/postgres/meta/_journal.json +++ b/drizzle/postgres/meta/_journal.json @@ -463,6 +463,41 @@ "when": 1790742086298, "tag": "0065_regular_joystick", "breakpoints": true + }, + { + "idx": 66, + "version": "7", + "when": 1791209295939, + "tag": "0066_retire_2_8_columns", + "breakpoints": true + }, + { + "idx": 67, + "version": "7", + "when": 1791209310758, + "tag": "0067_external_session_ref", + "breakpoints": true + }, + { + "idx": 68, + "version": "7", + "when": 1791256340388, + "tag": "0068_plugin_pinned_version", + "breakpoints": true + }, + { + "idx": 69, + "version": "7", + "when": 1791388888397, + "tag": "0069_plugin_channel", + "breakpoints": true + }, + { + "idx": 70, + "version": "7", + "when": 1791550496661, + "tag": "0070_drop_donation_modal", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/sqlite/0061_retire_2_8_columns.sql b/drizzle/sqlite/0061_retire_2_8_columns.sql new file mode 100644 index 000000000..6fcef5ed3 --- /dev/null +++ b/drizzle/sqlite/0061_retire_2_8_columns.sql @@ -0,0 +1,46 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_ssh_folders` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `color` text, + `icon` text, + `sort_order` integer, + `sync_id` text, + `local_only` integer DEFAULT false NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_ssh_folders`("id", "user_id", "name", "color", "icon", "sort_order", "sync_id", "local_only", "created_at", "updated_at") SELECT "id", "user_id", "name", "color", "icon", "sort_order", "sync_id", "local_only", "created_at", "updated_at" FROM `ssh_folders`;--> statement-breakpoint +DROP TABLE `ssh_folders`;--> statement-breakpoint +ALTER TABLE `__new_ssh_folders` RENAME TO `ssh_folders`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `ssh_folders_sync_id_unique` ON `ssh_folders` (`sync_id`);--> statement-breakpoint +CREATE INDEX `idx_ssh_folders_user_id` ON `ssh_folders` (`user_id`);--> statement-breakpoint +ALTER TABLE `ssh_data` DROP COLUMN `terminal_config`;--> statement-breakpoint +ALTER TABLE `ssh_data` DROP COLUMN `quick_actions`;--> statement-breakpoint +ALTER TABLE `sessions` DROP COLUMN `oidc_sub`;--> statement-breakpoint +ALTER TABLE `sessions` DROP COLUMN `oidc_sid`;--> statement-breakpoint +ALTER TABLE `sessions` DROP COLUMN `sso_provider_id`;--> statement-breakpoint +ALTER TABLE `user_preferences` DROP COLUMN `command_autocomplete`;--> statement-breakpoint +ALTER TABLE `user_preferences` DROP COLUMN `confirm_snippet_execution`;--> statement-breakpoint +ALTER TABLE `user_preferences` DROP COLUMN `custom_themes`;--> statement-breakpoint +ALTER TABLE `user_preferences` DROP COLUMN `terminal_defaults`;--> statement-breakpoint +ALTER TABLE `user_preferences` DROP COLUMN `terminal_macros`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `is_oidc`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `oidc_identifier`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `sso_provider_id`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `client_id`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `client_secret`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `issuer_url`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `authorization_url`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `token_url`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `identifier_path`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `name_path`;--> statement-breakpoint +ALTER TABLE `users` DROP COLUMN `scopes`;--> statement-breakpoint +DELETE FROM `settings` WHERE `key` IN ('host_defaults', 'analytics_enabled', 'analytics_instance_id');--> statement-breakpoint +UPDATE `ssh_data` SET `username` = REPLACE(`username`, '$oidc.preferred_username', '$external.username') WHERE `username` LIKE '%$oidc.preferred_username%';--> statement-breakpoint +UPDATE `ssh_credentials` SET `username` = REPLACE(`username`, '$oidc.preferred_username', '$external.username') WHERE `username` LIKE '%$oidc.preferred_username%';--> statement-breakpoint +UPDATE `host_defaults` SET `value` = REPLACE(`value`, '$oidc.preferred_username', '$external.username') WHERE `value` LIKE '%$oidc.preferred_username%'; \ No newline at end of file diff --git a/drizzle/sqlite/0062_external_session_ref.sql b/drizzle/sqlite/0062_external_session_ref.sql new file mode 100644 index 000000000..f8b8402a8 --- /dev/null +++ b/drizzle/sqlite/0062_external_session_ref.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD `external_session_ref` text; \ No newline at end of file diff --git a/drizzle/sqlite/0063_plugin_pinned_version.sql b/drizzle/sqlite/0063_plugin_pinned_version.sql new file mode 100644 index 000000000..91a5c68cf --- /dev/null +++ b/drizzle/sqlite/0063_plugin_pinned_version.sql @@ -0,0 +1 @@ +ALTER TABLE `plugins` ADD `pinned_version` text; \ No newline at end of file diff --git a/drizzle/sqlite/0064_plugin_channel.sql b/drizzle/sqlite/0064_plugin_channel.sql new file mode 100644 index 000000000..db92bca12 --- /dev/null +++ b/drizzle/sqlite/0064_plugin_channel.sql @@ -0,0 +1 @@ +ALTER TABLE `plugins` ADD `channel` text DEFAULT 'stable' NOT NULL; \ No newline at end of file diff --git a/drizzle/sqlite/0065_drop_donation_modal.sql b/drizzle/sqlite/0065_drop_donation_modal.sql new file mode 100644 index 000000000..710110f62 --- /dev/null +++ b/drizzle/sqlite/0065_drop_donation_modal.sql @@ -0,0 +1 @@ +ALTER TABLE `users` DROP COLUMN `donation_modal_dismissed`; \ No newline at end of file diff --git a/drizzle/sqlite/meta/0061_snapshot.json b/drizzle/sqlite/meta/0061_snapshot.json new file mode 100644 index 000000000..22d34e0bd --- /dev/null +++ b/drizzle/sqlite/meta/0061_snapshot.json @@ -0,0 +1,4452 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6c43dde1-c5eb-4ff0-8c71-b4c5e6010c66", + "prevId": "d81ae027-ff86-4354-9705-ead08a426d8f", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_link": { + "name": "sync_link", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_records": { + "name": "sync_records", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0062_snapshot.json b/drizzle/sqlite/meta/0062_snapshot.json new file mode 100644 index 000000000..df8ccf836 --- /dev/null +++ b/drizzle/sqlite/meta/0062_snapshot.json @@ -0,0 +1,4459 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "dd7d607e-a0a7-4fa9-833b-d333ceed8cee", + "prevId": "6c43dde1-c5eb-4ff0-8c71-b4c5e6010c66", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_link": { + "name": "sync_link", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_records": { + "name": "sync_records", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0063_snapshot.json b/drizzle/sqlite/meta/0063_snapshot.json new file mode 100644 index 000000000..9a83fbebc --- /dev/null +++ b/drizzle/sqlite/meta/0063_snapshot.json @@ -0,0 +1,4466 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f30d235a-ff96-490c-a84a-4787012caa8a", + "prevId": "dd7d607e-a0a7-4fa9-833b-d333ceed8cee", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_link": { + "name": "sync_link", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_records": { + "name": "sync_records", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0064_snapshot.json b/drizzle/sqlite/meta/0064_snapshot.json new file mode 100644 index 000000000..8f24c5f4d --- /dev/null +++ b/drizzle/sqlite/meta/0064_snapshot.json @@ -0,0 +1,4474 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "64d849e8-d93d-4bf3-a385-4703f6e80a11", + "prevId": "f30d235a-ff96-490c-a84a-4787012caa8a", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'stable'" + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_link": { + "name": "sync_link", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_records": { + "name": "sync_records", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/0065_snapshot.json b/drizzle/sqlite/meta/0065_snapshot.json new file mode 100644 index 000000000..70e86bba5 --- /dev/null +++ b/drizzle/sqlite/meta/0065_snapshot.json @@ -0,0 +1,4466 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "3bc2344b-9f1a-4b8f-bdb3-8d62b5654ee9", + "prevId": "64d849e8-d93d-4bf3-a385-4703f6e80a11", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_options": { + "name": "ssh_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'stable'" + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "local_only": { + "name": "local_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_link": { + "name": "sync_link", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_records": { + "name": "sync_records", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json index 5291f32be..b865c8bc7 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -428,6 +428,41 @@ "when": 1790742085632, "tag": "0060_fine_cerise", "breakpoints": true + }, + { + "idx": 61, + "version": "6", + "when": 1791209294736, + "tag": "0061_retire_2_8_columns", + "breakpoints": true + }, + { + "idx": 62, + "version": "6", + "when": 1791209309626, + "tag": "0062_external_session_ref", + "breakpoints": true + }, + { + "idx": 63, + "version": "6", + "when": 1791256339684, + "tag": "0063_plugin_pinned_version", + "breakpoints": true + }, + { + "idx": 64, + "version": "6", + "when": 1791388887785, + "tag": "0064_plugin_channel", + "breakpoints": true + }, + { + "idx": 65, + "version": "6", + "when": 1791550495357, + "tag": "0065_drop_donation_modal", + "breakpoints": true } ] } \ No newline at end of file diff --git a/electron-builder.json b/electron-builder.json index b580d9ce0..8d2cd7207 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -93,7 +93,7 @@ "desktop": { "entry": { "Name": "Termix", - "Comment": "Self-hosted SSH and remote desktop management. ", + "Comment": "Self-hosted, plugin-based server management.", "Keywords": "docker;ssh;terminal;telnet;self-hosted;rdp;file-management;vnc;ssh-tunnel;server-stats;termix", "StartupWMClass": "termix" } @@ -123,7 +123,7 @@ "entitlements": "packaging/build/entitlements.mac.plist", "entitlementsInherit": "packaging/build/entitlements.mac.inherit.plist", "type": "distribution", - "minimumSystemVersion": "10.15", + "minimumSystemVersion": "12.0", "mergeASARs": false, "singleArchFiles": "**/*.{node,bare,dylib}", "x64ArchFiles": "**/{*.{node,bare,dylib},node-pty/prebuilds/*/spawn-helper}" diff --git a/electron/local-shell.cjs b/electron/local-shell.cjs index ac633fec7..bbf965fb2 100644 --- a/electron/local-shell.cjs +++ b/electron/local-shell.cjs @@ -1,4 +1,16 @@ -function resolveLocalShell(platform, requestedShell, env = process.env) { +const fs = require("node:fs"); +const path = require("node:path"); + +function isUsableShell(file, exists) { + return !!file && (!path.isAbsolute(file) || exists(file)); +} + +function resolveLocalShell( + platform, + requestedShell, + env = process.env, + exists = fs.existsSync, +) { if (platform === "win32") { if (requestedShell === "wsl") { return { file: "wsl.exe", args: [] }; @@ -9,13 +21,47 @@ function resolveLocalShell(platform, requestedShell, env = process.env) { }; } - return { - file: - env.TERMIX_LOCAL_SHELL || - env.SHELL || - (platform === "darwin" ? "/bin/zsh" : "/bin/bash"), - args: ["-l"], - }; + // A SHELL left pointing at an uninstalled shell makes the spawn fail. + const fallbacks = + platform === "darwin" + ? ["/bin/zsh", "/bin/bash", "/bin/sh"] + : ["/bin/bash", "/bin/sh"]; + const file = + [env.TERMIX_LOCAL_SHELL, env.SHELL, ...fallbacks].find((candidate) => + isUsableShell(candidate, exists), + ) || fallbacks[0]; + return { file, args: ["-l"] }; } -module.exports = { resolveLocalShell }; +/** + * node-pty starts every shell through its spawn-helper binary, and a packaged + * copy that lost its execute bit fails with "posix_spawnp failed". Returns + * how many helpers were fixed. + */ +function repairSpawnHelpers(ptyDir, fsImpl = fs) { + const dir = ptyDir.replace(/app\.asar(?!\.unpacked)/, "app.asar.unpacked"); + const candidates = [ + path.join(dir, "build", "Release", "spawn-helper"), + path.join( + dir, + "prebuilds", + `${process.platform}-${process.arch}`, + "spawn-helper", + ), + ]; + let fixed = 0; + for (const helper of candidates) { + try { + const mode = fsImpl.statSync(helper).mode; + if ((mode & 0o111) === 0) { + fsImpl.chmodSync(helper, mode | 0o755); + fixed += 1; + } + } catch { + // Missing or read-only; nothing to fix here. + } + } + return fixed; +} + +module.exports = { resolveLocalShell, repairSpawnHelpers }; diff --git a/electron/main.cjs b/electron/main.cjs index 01e53fb0d..05ebbdd1f 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -33,7 +33,7 @@ const { launchNativeRdp } = require("./native-rdp.cjs"); const { isCloseActiveTabInput } = require("./keyboard-shortcuts.cjs"); const { quitApp } = require("./app-quit.cjs"); const { selectLinuxPasswordStore } = require("./linux-password-store.cjs"); -const { resolveLocalShell } = require("./local-shell.cjs"); +const { resolveLocalShell, repairSpawnHelpers } = require("./local-shell.cjs"); const { registerLocalFileHandlers } = require("./local-files.cjs"); const localTerminalSessions = new Map(); @@ -644,17 +644,30 @@ const BACKEND_REQUEST_HANDLERS = { }, }; +function isDevRunner() { + return ( + process.env.TERMIX_DEV_RUNNER === "true" && + typeof process.send === "function" + ); +} + async function handleBackendRequest(msg) { if (!msg || msg.type !== "backend-request") return; const { id, channel, payload } = msg; const handler = BACKEND_REQUEST_HANDLERS[channel]; + // Under npm run dev:electron the runner owns the backend and relays these messages. + const target = + backendProcess && !backendProcess.killed + ? backendProcess + : isDevRunner() + ? process + : null; const reply = (response) => { - if (backendProcess && !backendProcess.killed) { - try { - backendProcess.send({ type: "backend-response", id, ...response }); - } catch (error) { - logToFile("Failed to reply to backend request:", error.message); - } + if (!target) return; + try { + target.send({ type: "backend-response", id, ...response }); + } catch (error) { + logToFile("Failed to reply to backend request:", error.message); } }; if (!handler) { @@ -1028,7 +1041,7 @@ function getBackendPaths() { backendCwd: backendDir, }; } - // fork() does not go through Electron's asar redirector — use the unpacked path. + // fork() does not go through Electron's asar redirector, use the unpacked path. // On macOS multi-arch builds (mergeASARs: false), electron-builder names the ASAR // app-arm64.asar / app-x64.asar instead of app.asar, so match all variants. const unpackedRoot = getUnpackedAppRoot(appRoot); @@ -1290,7 +1303,7 @@ function createTray() { try { const { nativeImage } = require("electron"); - // Native APIs (Tray, nativeImage) can't load files from inside app.asar — + // Native APIs (Tray, nativeImage) can't load files from inside app.asar, // use the unpacked path so the OS sees a real file. const publicRoot = isDev ? path.join(appRoot, "public") @@ -1449,6 +1462,14 @@ function createWindow() { details.requestHeaders["User-Agent"] = customUserAgent; + // YouTube refuses embeds without a Referer, which file:// never sends. + if ( + details.url.startsWith("https://www.youtube-nocookie.com/embed/") && + !details.requestHeaders.Referer + ) { + details.requestHeaders.Referer = "https://termix.site/"; + } + const rememberedJwt = getRememberedElectronAuthCookie("jwt", details.url); if (rememberedJwt) { setCookieHeaderValue( @@ -2166,11 +2187,6 @@ async function openC2SRelay( targetHost, targetPort, }); - setC2STunnelStatus(tunnelName, { - connected: false, - status: "CONNECTING", - reason: `Opening relay to ${targetHost}:${targetPort}`, - }); const ws = new WebSocket( relayUrl, getWebSocketOptions(relayUrl, { headers }), @@ -2205,13 +2221,16 @@ async function openC2SRelay( socket.on("data", sendChunk); socket.on("close", cleanup); + // One connection failing must not stop the tunnel: the listener refuses + // new connections while the tunnel is not connected, and a browser opens + // many at once. socket.on("error", (error) => { - setC2STunnelError(tunnelName, error.message || "Local socket error"); + logToFile(`[c2s] ${tunnelName} local socket error:`, error.message); cleanup(); }); ws.on("close", cleanup); ws.on("error", (error) => { - setC2STunnelError(tunnelName, error.message || "Relay connection failed"); + logToFile(`[c2s] ${tunnelName} relay error:`, error.message); cleanup(); }); @@ -2238,10 +2257,6 @@ async function openC2SRelay( if (message.type === "ready") { ready = true; logToFile(`[c2s] relay ready for ${tunnelName}`); - setC2STunnelStatus(tunnelName, { - connected: true, - status: "CONNECTED", - }); if (initialData?.length) { ws.send(initialData); } @@ -2254,12 +2269,13 @@ async function openC2SRelay( "Relay rejected the client tunnel", ); logToFile("[c2s] relay error:", relayError); - setC2STunnelError(tunnelName, relayError); + if (relayError === C2S_REMOTE_SESSION_EXPIRED_ERROR) { + setC2STunnelError(tunnelName, relayError); + } cleanup(); } } catch (error) { logToFile("[c2s] invalid relay message:", error.message); - setC2STunnelError(tunnelName, error.message || "Invalid relay response"); cleanup(); } }); @@ -2485,7 +2501,9 @@ function handleC2SLocalConnection(tunnel, socket, authToken) { "Local relay failed", ); logToFile("[c2s] local relay failed:", message); - setC2STunnelError(tunnelName, message); + if (message === C2S_REMOTE_SESSION_EXPIRED_ERROR) { + setC2STunnelError(tunnelName, message); + } socket.destroy(); }); } @@ -3222,17 +3240,32 @@ ipcMain.handle("local-terminal-start", (event, dimensions = {}) => { const rows = Math.min(300, Math.max(1, Number(dimensions.rows) || 24)); const sessionId = crypto.randomUUID(); const shellConfig = resolveLocalShell(process.platform, dimensions.shell); - const child = pty.spawn(shellConfig.file, shellConfig.args, { - name: "xterm-256color", - cols, - rows, - cwd: os.homedir(), - env: { - ...process.env, - TERM: "xterm-256color", - COLORTERM: "truecolor", - }, - }); + const spawnShell = () => + pty.spawn(shellConfig.file, shellConfig.args, { + name: "xterm-256color", + cols, + rows, + cwd: os.homedir(), + env: { + ...process.env, + TERM: "xterm-256color", + COLORTERM: "truecolor", + }, + }); + let child; + try { + child = spawnShell(); + } catch (error) { + const ptyDir = path.dirname(require.resolve("node-pty/package.json")); + if ( + !String(error?.message).includes("posix_spawnp") || + repairSpawnHelpers(ptyDir) === 0 + ) { + throw error; + } + logToFile("[local-terminal] restored the spawn-helper execute bit"); + child = spawnShell(); + } const ownerId = event.sender.id; const session = { ownerId, process: child, ready: false, buffered: "" }; localTerminalSessions.set(sessionId, session); @@ -3762,8 +3795,11 @@ app.whenReady().then(async () => { logToFile("startBackendServer result:", result); } else { logToFile( - "Skipping embedded backend (isDev=true) - expecting separate dev:backend process", + "Skipping embedded backend (isDev=true) - expecting the dev runner's backend", ); + if (isDevRunner()) { + process.on("message", (msg) => void handleBackendRequest(msg)); + } } createTray(); diff --git a/electron/preload.js b/electron/preload.js index 133119513..d7b88ad3f 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -83,9 +83,6 @@ contextBridge.exposeInMainWorld("electronAPI", { externalBrowserLogin: (authUrl, callbackPort) => ipcRenderer.invoke("external-browser-login", authUrl, callbackPort), - // 2.8 name, read by older server login pages. Remove in 3.0.0. - oidcSystemBrowserAuth: (authUrl, callbackPort) => - ipcRenderer.invoke("external-browser-login", authUrl, callbackPort), openExternalEditor: (fileData) => ipcRenderer.invoke("open-external-editor", fileData), diff --git a/env.catalog.json b/env.catalog.json new file mode 100644 index 000000000..4b7d0ed28 --- /dev/null +++ b/env.catalog.json @@ -0,0 +1,460 @@ +{ + "groups": { + "server": "Server", + "https": "HTTPS", + "database": "Database", + "secrets": "Secrets", + "auth": "Sign in", + "proxy": "Proxies and origins", + "logging": "Logs and audit", + "plugins": "Plugins", + "desktop": "Desktop app", + "internal": "Internal" + }, + "vars": [ + { + "name": "PORT", + "group": "server", + "default": "8080", + "description": "Port the web UI listens on in the Docker image." + }, + { + "name": "DATA_DIR", + "group": "server", + "default": "/app/data", + "description": "Where Termix keeps its database, keys, plugins and uploads. Mount a volume here. Outside Docker the default is ./db/data." + }, + { + "name": "ALLOW_EMPTY_DATA_DIR", + "group": "server", + "default": "false", + "description": "Start even when DATA_DIR is empty but an older data folder exists elsewhere. Only set this if you really want a fresh install." + }, + { + "name": "PUID", + "group": "server", + "default": "1000", + "docker": true, + "description": "User id the container runs as. Match it to the owner of your data volume." + }, + { + "name": "PGID", + "group": "server", + "default": "1000", + "docker": true, + "description": "Group id the container runs as." + }, + { + "name": "BASE_PATH", + "group": "server", + "description": "Serve Termix under a sub path, like /termix. Set the same path in your reverse proxy." + }, + { + "name": "NODE_ENV", + "group": "server", + "default": "production", + "description": "Set by the image. Leave it alone." + }, + { + "name": "VERSION", + "group": "server", + "description": "Set by the image to the Termix version. Leave it alone." + }, + { + "name": "SETTINGS_CACHE_REFRESH_SECONDS", + "group": "server", + "default": "30", + "description": "How often each server instance reloads admin settings from the database. 0 turns the refresh off." + }, + { + "name": "SSH_AUTH_SOCK", + "group": "server", + "description": "SSH agent socket used for hosts set to agent auth when they do not name their own socket." + }, + + { + "name": "ENABLE_SSL", + "group": "https", + "default": "false", + "description": "Serve HTTPS. Termix makes a self-signed certificate on first boot unless you give it one." + }, + { + "name": "SSL_PORT", + "group": "https", + "default": "8443", + "description": "Port for HTTPS." + }, + { + "name": "SSL_CERT_PATH", + "group": "https", + "default": "/app/data/ssl/termix.crt", + "description": "Certificate file for HTTPS." + }, + { + "name": "SSL_KEY_PATH", + "group": "https", + "default": "/app/data/ssl/termix.key", + "description": "Private key file for HTTPS." + }, + { + "name": "SSL_DOMAIN", + "group": "https", + "default": "localhost", + "description": "Domain put in the self-signed certificate." + }, + { + "name": "TERMIX_SSL_TERMINATED_BY_NGINX", + "group": "https", + "internal": true, + "description": "Set by the image when nginx serves HTTPS in front of the backend." + }, + + { + "name": "DATABASE_DIALECT", + "group": "database", + "default": "sqlite", + "description": "sqlite, postgres or mysql." + }, + { + "name": "DATABASE_URL", + "group": "database", + "description": "Connection URL for postgres or mysql, like postgres://user:pass@db:5432/termix." + }, + { + "name": "DATABASE_POOL_MAX", + "group": "database", + "default": "10", + "description": "Most connections Termix opens to postgres or mysql." + }, + { + "name": "DATABASE_SSL", + "group": "database", + "description": "TLS for postgres or mysql: require, no-verify or disable." + }, + { + "name": "DB_FILE_ENCRYPTION", + "group": "database", + "default": "true", + "description": "Encrypt the SQLite file at rest. Set false only if something else already encrypts the disk." + }, + { + "name": "DB_FILE_KEY", + "group": "database", + "secret": true, + "description": "Only used to open SQLite files encrypted by very old versions. You almost never need it." + }, + { + "name": "DATABASE_LAYER_SKIP_PREUPGRADE_BACKUP", + "group": "database", + "default": "false", + "description": "Skip the backup Termix takes of the data folder before an upgrade changes the database." + }, + { + "name": "DATABASE_LAYER_PREUPGRADE_BACKUP_KEEP", + "group": "database", + "default": "3", + "description": "How many of those upgrade backups to keep." + }, + { + "name": "DRIZZLE_MIGRATIONS_DIR", + "group": "database", + "internal": true, + "description": "Where database migrations are read from. Set by the image." + }, + + { + "name": "JWT_SECRET", + "group": "secrets", + "secret": true, + "description": "Signs sign-in sessions. At least 64 characters. Made for you and saved in DATA_DIR/.env if not set. Also read from JWT_SECRET_FILE." + }, + { + "name": "DATABASE_KEY", + "group": "secrets", + "secret": true, + "description": "Key for the encrypted SQLite file, 64 hex characters. Made for you if not set. Also read from DATABASE_KEY_FILE." + }, + { + "name": "ENCRYPTION_KEY", + "group": "secrets", + "secret": true, + "description": "Wraps the data keys that encrypt passwords and SSH keys, 64 hex characters. Made for you if not set. Also read from ENCRYPTION_KEY_FILE." + }, + { + "name": "INTERNAL_AUTH_TOKEN", + "group": "secrets", + "secret": true, + "description": "Token the server uses to call itself. Made for you if not set. Also read from INTERNAL_AUTH_TOKEN_FILE." + }, + { + "name": "TERMIX_REQUIRE_EXTERNAL_SECRETS", + "group": "secrets", + "default": "false", + "description": "Refuse to start unless the four secrets above come from env vars or _FILE files, so none are written to disk." + }, + { + "name": "OIDC_SYSTEM_SECRET", + "group": "secrets", + "secret": true, + "description": "Only read to move data from installs older than 2.5. Never change it on an install that has it." + }, + { + "name": "WEBAUTHN_SYSTEM_SECRET", + "group": "secrets", + "secret": true, + "description": "Only read to move data from installs older than 2.5. Never change it on an install that has it." + }, + + { + "name": "ALLOW_PASSWORD_LOGIN", + "group": "auth", + "description": "true or false. Overrides the admin setting for signing in with a username and password." + }, + { + "name": "ALLOW_REGISTRATION", + "group": "auth", + "description": "true or false. Overrides the admin setting for creating new accounts." + }, + { + "name": "ALLOW_PASSWORD_RESET", + "group": "auth", + "description": "true or false. Overrides the admin setting for resetting a password from the sign in page." + }, + { + "name": "EXTERNAL_ALLOW_REGISTRATION", + "group": "auth", + "description": "true or false. Overrides the admin setting for creating accounts the first time someone signs in with SSO or LDAP." + }, + { + "name": "EXTERNAL_FORCE_HTTPS", + "group": "auth", + "default": "false", + "description": "Build sign in callback URLs with https even when the request came in over http. Use it behind a proxy that does not send X-Forwarded-Proto." + }, + { + "name": "OIDC_ALLOW_REGISTRATION", + "group": "auth", + "deprecated": "EXTERNAL_ALLOW_REGISTRATION", + "description": "Old name for EXTERNAL_ALLOW_REGISTRATION. Still read in 26.10." + }, + { + "name": "OIDC_FORCE_HTTPS", + "group": "auth", + "deprecated": "EXTERNAL_FORCE_HTTPS", + "description": "Old name for EXTERNAL_FORCE_HTTPS. Still read in 26.10." + }, + { + "name": "OIDC_CLIENT_ID", + "group": "auth", + "internal": true, + "description": "Read by core only to move a 2.8 OIDC setup into the sso plugin. See the sso plugin for the real variables." + }, + { + "name": "OIDC_ENV_OVERRIDE", + "group": "auth", + "internal": true, + "description": "Read by core only to move a 2.8 OIDC setup into the sso plugin. See the sso plugin for the real variables." + }, + { + "name": "TRUSTED_PROXY_AUTH_ENABLED", + "group": "auth", + "default": "false", + "description": "Let a proxy like Authelia or Authentik sign people in by sending their username in a header." + }, + { + "name": "TRUSTED_PROXY_AUTH_TRUSTED_PROXIES", + "group": "auth", + "description": "Comma separated IPs or CIDRs of the proxies allowed to send the headers. Required when trusted proxy login is on." + }, + { + "name": "TRUSTED_PROXY_AUTH_USERNAME_HEADER", + "group": "auth", + "default": "x-forwarded-username", + "description": "Header that holds the username." + }, + { + "name": "TRUSTED_PROXY_AUTH_ROLE_HEADER", + "group": "auth", + "default": "x-forwarded-role", + "description": "Header that holds the user's groups or roles." + }, + { + "name": "TRUSTED_PROXY_AUTH_ROLE_MAP", + "group": "auth", + "description": "JSON map from proxy roles to Termix roles, like {\"admins\":[\"admin\"],\"staff\":[\"user\"]}. Required when trusted proxy login is on." + }, + + { + "name": "TRUSTED_PROXIES", + "group": "proxy", + "docker": true, + "description": "Comma separated IPs or CIDRs nginx takes the client IP from (X-Forwarded-For). Set it to your reverse proxy." + }, + { + "name": "CORS_ALLOWED_ORIGINS", + "group": "proxy", + "description": "Comma separated extra origins allowed to call the API from a browser." + }, + { + "name": "TERMIX_ALLOWED_ORIGINS", + "group": "proxy", + "description": "Comma separated extra origins allowed to open WebSockets." + }, + { + "name": "HTTP_PROXY", + "group": "proxy", + "description": "Proxy for outbound HTTP requests, like registry downloads and webhooks." + }, + { + "name": "HTTPS_PROXY", + "group": "proxy", + "description": "Proxy for outbound HTTPS requests." + }, + { + "name": "NO_PROXY", + "group": "proxy", + "description": "Comma separated hosts that skip the proxy." + }, + + { + "name": "LOG_LEVEL", + "group": "logging", + "default": "info", + "description": "debug, info, warn or error." + }, + { + "name": "LOG_TIMESTAMP_FORMAT", + "group": "logging", + "description": "iso or 24h. Unset uses the local time format." + }, + { + "name": "AUDIT_LOG_RETENTION_DAYS", + "group": "logging", + "description": "Delete audit entries older than this many days. Unset keeps them until the entry cap." + }, + { + "name": "AUDIT_LOG_MAX_ENTRIES", + "group": "logging", + "default": "10000", + "description": "Most audit entries kept. The oldest go first." + }, + { + "name": "AUDIT_LOG_FORWARD_URL", + "group": "logging", + "description": "Also POST every audit entry as JSON to this URL. Overrides the admin setting." + }, + { + "name": "AUDIT_LOG_FORWARD_TOKEN", + "group": "logging", + "secret": true, + "description": "Bearer token sent with forwarded audit entries." + }, + { + "name": "SESSION_RECORDING_RETENTION_DAYS", + "group": "logging", + "deprecated": "the session-recording plugin setting", + "description": "Only read once to carry a 2.8 value into the session-recording plugin." + }, + + { + "name": "TERMIX_PLUGIN_REGISTRY_URL", + "group": "plugins", + "description": "Use another plugin index instead of the official registry." + }, + { + "name": "TERMIX_PLUGIN_STATS_URL", + "group": "plugins", + "description": "Where install counts are read from. Defaults to stats.json next to the registry index." + }, + { + "name": "TERMIX_REQUIRE_SIGNED_PLUGINS", + "group": "plugins", + "default": "false", + "description": "Only load plugins signed by a trusted key. Blocks uploads of unsigned files even in developer mode." + }, + { + "name": "TERMIX_BUNDLED_PLUGINS_DIR", + "group": "plugins", + "description": "Where the plugins shipped with Termix are read from. Set by the image." + }, + { + "name": "TERMIX_PLUGIN_PUBLIC_RATE_LIMIT", + "group": "plugins", + "default": "120", + "description": "Requests per minute one IP may make to a plugin's public routes." + }, + { + "name": "PLUGIN_MAX_KV_KEYS", + "group": "plugins", + "default": "10000", + "description": "Most key value entries one plugin may store." + }, + { + "name": "TERMIX_DEV_RELOAD", + "group": "plugins", + "internal": true, + "description": "Set by npm run dev so plugin rebuilds reload without a restart." + }, + { + "name": "TERMIX_DEV_RUNNER", + "group": "plugins", + "internal": true, + "description": "Set by npm run dev." + }, + + { + "name": "ENABLE_INSECURE_MODE", + "group": "desktop", + "default": "false", + "description": "Desktop app only. Skip TLS certificate checks for the linked server. Only for testing." + }, + { + "name": "ELECTRON_DISABLE_GPU", + "group": "desktop", + "description": "Desktop app only. Set to 1 to turn off GPU acceleration if the window is blank or flickers." + }, + { + "name": "TERMIX_LOCAL_SHELL", + "group": "desktop", + "description": "Desktop app only. Shell the local terminal opens, like /bin/zsh or pwsh.exe." + }, + { + "name": "SHELL", + "group": "desktop", + "internal": true, + "description": "Your login shell, used by the local terminal when TERMIX_LOCAL_SHELL is not set." + }, + { + "name": "ELECTRON_EMBEDDED", + "group": "desktop", + "internal": true, + "description": "Set by the desktop app when it runs its own backend." + }, + { + "name": "TERMIX_DATA_DIR", + "group": "desktop", + "internal": true, + "description": "Set by the desktop app to its data folder." + }, + { + "name": "XDG_CURRENT_DESKTOP", + "group": "desktop", + "internal": true, + "description": "Read on Linux to pick the password store and window behavior." + }, + + { + "name": "DESKTOP_SESSION", + "group": "desktop", + "internal": true, + "description": "Read on Linux to pick the password store." + }, + + { + "name": "VITE_BASE_PATH", + "group": "internal", + "internal": true, + "description": "Base path baked in at build time." + } + ] +} diff --git a/eslint.config.mjs b/eslint.config.mjs index a8717f6d3..1e4fad2b4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,64 +5,6 @@ import reactRefresh from "eslint-plugin-react-refresh"; import unusedImports from "eslint-plugin-unused-imports"; import tseslint from "typescript-eslint"; import { globalIgnores } from "eslint/config"; -import path from "node:path"; - -// A plugin may import @termix/plugin-sdk, npm packages and its own files. -// A relative path that climbs out of plugins// reaches core or another -// plugin, and "@/..." is core's alias. -const pluginBoundary = { - rules: { - "stay-inside": { - meta: { - type: "problem", - messages: { - core: "A plugin reaches core only through @termix/plugin-sdk. Use an SDK API, or add one.", - outside: - "A plugin cannot import files outside its own folder. Use the SDK, ctx.services, events, actions or slots.", - }, - }, - create(context) { - const file = context.filename.replace(/\\/g, "/"); - const root = /^(.*\/plugins\/[^/]+)\//.exec(file)?.[1]; - const check = (node, source) => { - if (typeof source !== "string") return; - if (source === "@" || source.startsWith("@/")) { - context.report({ node, messageId: "core" }); - return; - } - if (!root || !source.startsWith(".")) return; - const target = path.posix.normalize( - path.posix.join(path.posix.dirname(file), source), - ); - if (target !== root && !target.startsWith(`${root}/`)) { - context.report({ node, messageId: "outside" }); - } - }; - return { - ImportDeclaration: (node) => check(node, node.source.value), - ExportNamedDeclaration: (node) => check(node, node.source?.value), - ExportAllDeclaration: (node) => check(node, node.source.value), - ImportExpression: (node) => check(node, node.source.value), - // vi.mock("@/x") and require("../../x") name a module too. - CallExpression: (node) => { - const callee = node.callee; - const named = - (callee.type === "Identifier" && callee.name === "require") || - (callee.type === "MemberExpression" && - callee.object.type === "Identifier" && - callee.object.name === "vi" && - callee.property.type === "Identifier" && - ["mock", "doMock", "importActual", "unmock"].includes( - callee.property.name, - )); - const first = node.arguments[0]; - if (named && first?.type === "Literal") check(node, first.value); - }, - }; - }, - }, - }, -}; export default tseslint.config([ globalIgnores([ @@ -111,7 +53,7 @@ export default tseslint.config([ }, { // MySQL has no RETURNING clause, and drizzle's mysql-core does not expose - // the method at all — a bare .returning() is a TypeError there, not a bad + // the method at all. A bare .returning() is a TypeError there, not a bad // query, and it only fails on the engine no test in this repo runs against. // // 175 call sites were migrated off it. This is what stops number 176. @@ -143,14 +85,14 @@ export default tseslint.config([ { // Postgres and SQLite spell it ON CONFLICT; MySQL spells it ON // DUPLICATE KEY and names no columns, so drizzle's mysql-core has no - // onConflictDoUpdate at all — another TypeError, not a bad query. + // onConflictDoUpdate at all, another TypeError, not a bad query. selector: "CallExpression[callee.property.name='onConflictDoUpdate']", message: "MySQL has no ON CONFLICT. Use upsert() from ./returning.js.", }, { // better-sqlite3 puts these on a write result; node-postgres and - // mysql2 do not, so reading them directly yields undefined — and + // mysql2 do not, so reading them directly yields undefined, and // Number(undefined) is NaN, which reaches the database as the string // "NaN" and fails an integer column. Three call sites did exactly // this and only broke on Postgres. @@ -162,75 +104,4 @@ export default tseslint.config([ ], }, }, - { - // Core must not reach into a plugin. A plugin can be disabled, upgraded - // or removed, so an import from core turns "disabled" into a broken - // build rather than a missing feature. Core talks to plugins through the - // runtime in src/backend/plugins/, which handles absence. Core tests only - // use fixture plugins. - files: ["src/**/*.{ts,tsx}"], - rules: { - "no-restricted-imports": [ - "error", - { - patterns: [ - { - group: ["**/plugins/*/src/backend/**"], - message: - "Core must not import a plugin backend. Go through the plugin runtime, so disabling the plugin degrades cleanly.", - }, - ], - }, - ], - }, - }, - { - // The shell never imports a plugin. It reaches plugins only through the - // registries their frontends fill via the app object (A7). This block - // also carries the backend rule for src/ui files, because flat config - // replaces a rule's options rather than merging them. - files: ["src/ui/**/*.{ts,tsx}", "src/main.tsx"], - rules: { - "no-restricted-imports": [ - "error", - { - patterns: [ - { - group: ["**/plugins/*/src/**", "**/plugins/*/dist/**"], - message: - "The shell does not import plugins. Register the surface through the app object and read it from a registry.", - }, - ], - }, - ], - }, - }, - { - // The other direction. A plugin reaches core only through - // @termix/plugin-sdk, and never reaches into another plugin's source. - files: ["plugins/*/src/**/*.{ts,tsx,mjs}", "plugins/*/tests/**/*.{ts,tsx}"], - plugins: { "termix-plugins": pluginBoundary }, - rules: { - "termix-plugins/stay-inside": "error", - }, - }, - { - // react-i18next's hook reads core's namespace, so plugin strings show as raw keys. - files: ["plugins/*/src/**/*.{ts,tsx}"], - rules: { - "no-restricted-imports": [ - "error", - { - paths: [ - { - name: "react-i18next", - importNames: ["useTranslation", "Trans", "withTranslation"], - message: - "Use useTranslation from @termix/plugin-sdk/frontend so keys resolve in this plugin's namespace.", - }, - ], - }, - ], - }, - }, ]); diff --git a/index.html b/index.html index 9de5da6e8..09e886cdd 100644 --- a/index.html +++ b/index.html @@ -20,35 +20,6 @@ Termix - - -
-

${heading}

-

${message}

- ${detailsBlock} - ${requestIdBlock} -
- -`; -} - -/** - * Points every URL in a page OPKSSH served at its proxied path, so the - * browser keeps talking to Termix instead of this server's localhost. - * `basePath` is the proxied prefix, e.g. "/plugin-api/opkssh/chooser/". - */ -export function rewriteOpksshHtml(html: string, basePath: string): string { - const localHostPattern = "(?:localhost|127\\.0\\.0\\.1)"; - const attrPatterns = ["action", "href", "src", "formaction"]; - for (const attr of attrPatterns) { - html = html.replace( - new RegExp(`${attr}="(/[^"]*)`, "g"), - `${attr}="${basePath}$1`, - ); - html = html.replace( - new RegExp(`${attr}='(/[^']*)`, "g"), - `${attr}='${basePath}$1`, - ); - } - - for (const attr of ["href", "action", "src", "formaction"]) { - html = html.replace( - new RegExp( - `${attr}=["']?http:\\/\\/${localHostPattern}:\\d+\\/([^"'\\s]*)`, - "g", - ), - `${attr}="${basePath}/$1`, - ); - } - - html = html.replace( - new RegExp( - `(window\\.location\\.href\\s*=\\s*["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, - "g", - ), - `$1${basePath}/$2$3`, - ); - html = html.replace( - new RegExp( - `(window\\.location\\s*=\\s*["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, - "g", - ), - `$1${basePath}/$2$3`, - ); - html = html.replace( - new RegExp( - `(fetch\\(["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, - "g", - ), - `$1${basePath}/$2$3`, - ); - - html = html.replace( - new RegExp( - `(location\\.assign\\(["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["']\\))`, - "g", - ), - `$1${basePath}/$2$3`, - ); - html = html.replace( - new RegExp( - `(location\\.replace\\(["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["']\\))`, - "g", - ), - `$1${basePath}/$2$3`, - ); - - // XMLHttpRequest.open("GET", "http://localhost:PORT/path", ...) - html = html.replace( - new RegExp( - `(\\.open\\(["']\\w+["']\\s*,\\s*["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, - "g", - ), - `$1${basePath}/$2$3`, - ); - - html = html.replace( - new RegExp( - `(]+http-equiv=["']refresh["'][^>]+content=["'][^;]+;\\s*url=)http:\\/\\/${localHostPattern}:\\d+\\/([^"']+)(["'][^>]*>)`, - "gi", - ), - `$1${basePath}/$2$3`, - ); - - html = html.replace( - new RegExp( - `(data-[\\w-]+=["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, - "g", - ), - `$1${basePath}/$2$3`, - ); - - const baseTag = ``; - - if (html.includes("]*>/i, baseTag); - } else if (html.includes("")) { - html = html.replace(//i, `${baseTag}`); - } else { - html = `${baseTag}${html}`; - } - - return html; -} diff --git a/plugins/opkssh/src/backend/index.ts b/plugins/opkssh/src/backend/index.ts deleted file mode 100644 index 5bd79bd78..000000000 --- a/plugins/opkssh/src/backend/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Router } from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { tokens as tokensTable } from "./tables.js"; -import { createTokenStore } from "./token-store.js"; -import { createAuthSessions } from "./auth-session.js"; -import { binarySpec } from "./binary.js"; -import { createOpksshProvider } from "./provider.js"; -import { PUBLIC_PATHS, registerRoutes } from "./routes.js"; - -export async function activate(ctx: PluginContext) { - const table = await ctx.db.define(tokensTable); - const tokens = createTokenStore(ctx, table); - - // Fetched once per activation, in the background: the Docker image ships a - // copy, anywhere else the first boot downloads it. A sign-in waits for it. - let binary: Promise | null = null; - const binaryPath = () => { - if (!binary) { - binary = ctx.process.ensureBinary(binarySpec()).catch((error) => { - binary = null; - throw error; - }); - } - return binary; - }; - binaryPath().catch((error) => - ctx.log.warn( - `OPKSSH binary is not available yet: ${error instanceof Error ? error.message : String(error)}`, - ), - ); - - const sessions = createAuthSessions(ctx, { tokens, binaryPath }); - ctx.disposables.add(() => sessions.closeAll()); - - ctx.auth.registerSshAuthProvider(createOpksshProvider(ctx, tokens, sessions)); - - registerRoutes( - ctx.http.router({ public: PUBLIC_PATHS }), - ctx, - sessions, - tokens, - ); -} - -export async function deactivate() { - // Everything above was registered through ctx and is disposed by core. -} diff --git a/plugins/opkssh/src/backend/provider.ts b/plugins/opkssh/src/backend/provider.ts deleted file mode 100644 index 07ef5bcf5..000000000 --- a/plugins/opkssh/src/backend/provider.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { - PluginContext, - PluginSshAuthProvider, -} from "@termix/plugin-sdk/backend"; -import { applyCertificateAuth } from "@termix/plugin-sdk/ssh-certs"; -import type { AuthSessions, SignInSocket } from "./auth-session.js"; -import type { TokenStore } from "./token-store.js"; - -export const AUTH_TYPE = "opkssh"; - -const REQUIRED_MESSAGE = - "OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours."; - -const AUTH_FAILED_PATTERN = /All configured authentication methods failed/i; - -/** - * Hosts with auth type "opkssh" connect with a cached certificate. Without - * one, the terminal is asked for a browser sign-in ("opkssh_auth_required"), - * which startInteraction runs. - */ -export function createOpksshProvider( - ctx: PluginContext, - tokens: TokenStore, - sessions: AuthSessions, -): PluginSshAuthProvider { - return { - type: AUTH_TYPE, - labelKey: "hosts.filterAuthOpkssh", - needsUserInteraction: true, - supportsBackground: false, - interaction: AUTH_TYPE, - prepare: async (config, host, env) => { - const certificate = await tokens.get(env.userId, env.hostId); - if (!certificate) { - env.log("info", "No valid certificate found, requesting sign-in"); - return { - status: "interaction-required", - interaction: AUTH_TYPE, - message: REQUIRED_MESSAGE, - flag: "requiresOPKSSHAuth", - }; - } - try { - await applyCertificateAuth( - config as never, - env.client as never, - { - privateKey: certificate.privateKey, - certificate: certificate.sshCert, - }, - host.username, - ); - } catch (error) { - return { - status: "error", - code: "failed", - message: - "OPKSSH authentication failed: " + - (error instanceof Error ? error.message : String(error)), - }; - } - env.log("info", "Using cached SSH certificate"); - return { status: "ready" }; - }, - onAuthFailed: (_host, env, context) => { - if (!AUTH_FAILED_PATTERN.test(context.error.message)) return undefined; - ctx.log.warn("OPKSSH authentication failed, forgetting the certificate"); - void tokens.remove(env.userId, env.hostId).catch(() => undefined); - return { - status: "interaction-required", - interaction: AUTH_TYPE, - message: - "OPKSSH authentication failed or expired. Please authenticate again.", - }; - }, - startInteraction: async (request) => { - await sessions.start({ - userId: request.userId, - hostId: request.hostId, - socket: request.socket as SignInSocket, - requestOrigin: request.requestOrigin, - }); - }, - cancelInteraction: async (request) => { - if (request.requestId) await sessions.cancel(request.requestId); - }, - }; -} diff --git a/plugins/opkssh/src/backend/routes.ts b/plugins/opkssh/src/backend/routes.ts deleted file mode 100644 index f9f1ec0f3..000000000 --- a/plugins/opkssh/src/backend/routes.ts +++ /dev/null @@ -1,622 +0,0 @@ -import axios from "axios"; -import type { Request, Response, Router } from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { - normalizeSelectOpParam, - renderErrorPage, - rewriteOpksshHtml, -} from "./html.js"; -import { - PLUGIN_PATH, - type AuthSession, - type AuthSessions, -} from "./auth-session.js"; -import type { TokenStore } from "./token-store.js"; - -/** - * The browser reaches these without a Termix session: the sign-in may finish - * in a different browser, and the identity provider redirects to /callback. - * Each is matched to its sign-in by the request id, the OAuth state or the - * opkssh_request_id cookie. - */ -export const PUBLIC_PATHS = [ - "/chooser/:requestId", - "/chooser/:requestId/*", - "/callback", - "/callback/:requestId", - "/callback/:requestId/*", -]; - -const REQUEST_COOKIE = "opkssh_request_id"; -const MAX_HOPS = 4; - -function isLocalHostname(host: string): boolean { - const bare = host.split(":")[0]; - return bare === "127.0.0.1" || bare === "localhost" || bare === "[::1]"; -} - -function readCookie(req: Request, name: string): string | undefined { - const header = req.headers.cookie; - if (!header) return undefined; - for (const part of header.split(";")) { - const [key, ...rest] = part.trim().split("="); - if (key === name) return decodeURIComponent(rest.join("=")); - } - return undefined; -} - -function param(value: string | string[] | undefined): string { - return Array.isArray(value) ? (value[0] ?? "") : (value ?? ""); -} - -export function registerRoutes( - router: Router, - ctx: PluginContext, - sessions: AuthSessions, - tokens: TokenStore, -): void { - /** The plugin's public path prefix on this install, base path included. */ - const prefix = (req: Request): string => { - const base = new URL(ctx.http.baseUrl(req)).pathname.replace(/\/$/, ""); - return `${base}${PLUGIN_PATH}`; - }; - - const pathAfter = (req: Request, marker: string): string => { - const full = req.originalUrl || req.url; - const index = full.indexOf(marker); - return index === -1 ? "" : full.slice(index + marker.length); - }; - - const sessionNotFound = (res: Response, requestId: string) => { - res.status(404).send( - renderErrorPage({ - title: "Session Not Found", - heading: "Session Not Found", - message: - "This authentication session has expired or is invalid. Please close this window and try again.", - requestId, - }), - ); - }; - - async function actingUserWithAccess( - req: Request, - res: Response, - ): Promise<{ userId: string; hostId: number } | null> { - const userId = ctx.currentActor(); - const hostId = Number(param(req.params.hostId)); - if (!userId) { - res.status(401).json({ error: "Authentication required" }); - return null; - } - if (!Number.isInteger(hostId) || hostId <= 0) { - res.status(400).json({ error: "Invalid host id" }); - return null; - } - const access = await ctx.hosts.checkAccess(hostId, "connect"); - if (!access.hasAccess) { - res.status(404).json({ error: "Host not found" }); - return null; - } - return { userId, hostId }; - } - - /** - * @openapi - * /plugin-api/opkssh/token/{hostId}: - * get: - * summary: Get the cached OPKSSH certificate status for a host - * description: Whether the current user has a valid cached certificate for this host, and when it expires. - * tags: [OPKSSH] - * parameters: - * - name: hostId - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Certificate status. - * 400: - * description: Invalid host id. - * 404: - * description: Host not found or not accessible. - */ - router.get("/token/:hostId", async (req: Request, res: Response) => { - const target = await actingUserWithAccess(req, res); - if (!target) return; - res.json(await tokens.status(target.userId, target.hostId)); - }); - - /** - * @openapi - * /plugin-api/opkssh/token/{hostId}: - * delete: - * summary: Forget the cached OPKSSH certificate for a host - * description: The next connection to the host asks for a browser sign-in again. - * tags: [OPKSSH] - * parameters: - * - name: hostId - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Certificate removed, or there was none. - * 400: - * description: Invalid host id. - * 404: - * description: Host not found or not accessible. - */ - router.delete("/token/:hostId", async (req: Request, res: Response) => { - const target = await actingUserWithAccess(req, res); - if (!target) return; - await tokens.remove(target.userId, target.hostId); - res.json({ success: true }); - }); - - /** - * Follows OPKSSH's /select hops until it names the identity provider: - * /select to /select/ on the chooser, then /login on the callback listener, - * then the provider's authorize URL. - */ - async function proxySelect( - req: Request, - res: Response, - session: AuthSession, - targetPath: string, - ): Promise { - const requestId = session.requestId; - const rawQs = targetPath.includes("?") - ? targetPath.slice(targetPath.indexOf("?")) - : ""; - let qs = rawQs; - if (rawQs) { - try { - const params = new URLSearchParams(rawQs.replace(/^\?/, "")); - const rawOp = params.get("op"); - if (rawOp) { - const mapped = normalizeSelectOpParam(rawOp, session.providers); - if (mapped !== rawOp) { - params.set("op", mapped); - qs = `?${params.toString()}`; - } - } - } catch { - // keep the raw query - } - } - - const chooserHost = `127.0.0.1:${session.localPort}`; - const startUrl = `http://${chooserHost}/select/${qs}`; - - const fetchUpstream = async (url: string) => { - let hostHeader = chooserHost; - try { - hostHeader = new URL(url).host; - } catch { - // keep the chooser host - } - const response = await axios({ - method: "GET", - url, - maxRedirects: 0, - validateStatus: () => true, - timeout: 10000, - responseType: "text", - transformResponse: (value) => value, - headers: { host: hostHeader }, - }); - const location = response.headers["location"]; - const contentType = response.headers["content-type"]; - return { - status: response.status, - location: typeof location === "string" ? location : undefined, - contentType: typeof contentType === "string" ? contentType : "", - body: - typeof response.data === "string" - ? response.data - : String(response.data ?? ""), - targetUrl: url, - }; - }; - - try { - let response = await fetchUpstream(startUrl); - for (let hop = 0; hop < MAX_HOPS; hop++) { - if (response.status < 300 || response.status >= 400) break; - const location = response.location; - if (!location) break; - - if (location.startsWith("/")) { - let currentHost = chooserHost; - try { - currentHost = new URL(response.targetUrl).host; - } catch { - // keep the chooser host - } - response = await fetchUpstream(`http://${currentHost}${location}`); - continue; - } - - if (/^https?:\/\//i.test(location)) { - const parsed = new URL(location); - if (isLocalHostname(parsed.host)) { - if (!session.callbackPort) { - const port = parseInt(parsed.port, 10); - if (!Number.isNaN(port)) session.callbackPort = port; - } - // Send the browser through the proxy so it gets the state cookie - // OPKSSH sets on /login; following it here would swallow it. - res.redirect( - 302, - `${prefix(req)}/chooser/${requestId}${parsed.pathname}${parsed.search}`, - ); - return; - } - } - break; - } - - let external: URL | null = null; - if ( - response.status >= 300 && - response.status < 400 && - response.location && - /^https?:\/\//i.test(response.location) - ) { - try { - const parsed = new URL(response.location); - if (!isLocalHostname(parsed.host)) external = parsed; - } catch { - external = null; - } - } - - if (external) { - const state = external.searchParams.get("state"); - if (state) sessions.registerOAuthState(state, requestId); - res.redirect(302, external.toString()); - return; - } - - const bodyPreview = response.body.slice(0, 512); - ctx.log.error( - `OPKSSH /select did not produce an OAuth redirect (status ${response.status})`, - ); - res.status(502).send( - renderErrorPage({ - title: "OPKSSH error", - heading: "Failed to get OAuth redirect", - message: - "OPKSSH did not return an external OAuth provider URL. " + - "This typically indicates a configuration mismatch between the provider's redirect_uris " + - "and the Termix callback path. Check the server log for the OPKSSH response body.", - details: [ - `Upstream: ${response.targetUrl}`, - `Status: ${response.status}`, - response.location ? `Location: ${response.location}` : "", - `Content-Type: ${response.contentType || "(none)"}`, - "", - bodyPreview - ? `Body (first 512 chars):\n${bodyPreview}` - : "Body: (empty)", - ] - .filter((line, index) => line || index === 4) - .join("\n"), - requestId, - }), - ); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - ctx.log.error(`Error proxying OPKSSH /select: ${message}`); - res.status(502).send( - renderErrorPage({ - title: "OPKSSH error", - heading: "Failed to reach OPKSSH service", - message: - "Termix could not connect to the local OPKSSH authentication service. " + - "The OPKSSH process may have exited or is not listening yet.", - details: `Upstream: ${startUrl}\nError: ${message}`, - requestId, - }), - ); - } - } - - /** - * @openapi - * /plugin-api/opkssh/chooser/{requestId}: - * get: - * summary: Proxy the OPKSSH provider chooser page and its resources - * description: Public. Serves OPKSSH's local chooser for one sign-in, rewriting its URLs to this path. Any subpath is proxied too. - * tags: [OPKSSH] - * parameters: - * - name: requestId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chooser page content. - * 302: - * description: Redirect to the identity provider or the callback listener. - * 404: - * description: Sign-in not found. - * 502: - * description: OPKSSH did not answer as expected. - */ - router.use("/chooser/:requestId", async (req: Request, res: Response) => { - const requestId = param(req.params.requestId); - const session = sessions.get(requestId); - if (!session) { - sessionNotFound(res, requestId); - return; - } - if (!session.localPort) { - res.status(500).send( - renderErrorPage({ - title: "Error", - heading: "Authentication Error", - message: - "Failed to load authentication page. OPKSSH process may not be ready yet. Please try again.", - requestId, - }), - ); - return; - } - - const base = prefix(req); - const chooserBase = `${base}/chooser/${requestId}`; - const callbackBase = `${base}/callback/${requestId}`; - const targetPath = pathAfter(req, `/chooser/${requestId}`) || "/chooser"; - - if (targetPath.startsWith("/select")) { - await proxySelect(req, res, session, targetPath); - return; - } - - // The browser is sent here for OPKSSH's login listener so it receives - // the listener's cookies. - const onCallbackListener = - /^\/login(?:\?|$)/.test(targetPath) || - /^\/login-callback(?:\?|$)/.test(targetPath); - const upstreamPort = - onCallbackListener && session.callbackPort - ? session.callbackPort - : session.localPort; - - try { - const response = await axios({ - method: req.method, - url: `http://127.0.0.1:${upstreamPort}${targetPath}`, - headers: { ...req.headers, host: `127.0.0.1:${upstreamPort}` }, - data: req.body, - timeout: 10000, - validateStatus: () => true, - maxRedirects: 0, - responseType: "arraybuffer", - }); - - for (const [key, value] of Object.entries(response.headers)) { - const lower = key.toLowerCase(); - if (lower === "transfer-encoding") continue; - if (lower === "location") { - const location = String(value); - if (location.startsWith("/")) { - res.setHeader(key, `${chooserBase}${location}`); - continue; - } - const local = location.match( - /^http:\/\/(?:localhost|127\.0\.0\.1):(\d+)(\/.*)?$/, - ); - if (local) { - const port = parseInt(local[1], 10); - const path = local[2] || "/"; - const toCallback = - (session.callbackPort && port === session.callbackPort) || - (port !== session.localPort && - (path.includes("login") || path.includes("callback"))); - res.setHeader( - key, - `${toCallback ? callbackBase : chooserBase}${path}`, - ); - continue; - } - try { - const state = new URL(location).searchParams.get("state"); - if (state) sessions.registerOAuthState(state, requestId); - } catch { - // not a URL - } - res.setHeader(key, location); - } else if (lower === "set-cookie") { - // Scope OPKSSH's cookies to the proxied callback, where the state - // cookie set by /login has to arrive. - const cookies = Array.isArray(value) ? value : [String(value)]; - res.setHeader( - key, - cookies.map((cookie) => - /;\s*path=/i.test(cookie) - ? cookie - .replace(/;\s*domain=[^;]*/gi, "") - .replace(/;\s*path=[^;]*/gi, `; Path=${base}/callback/`) - : `${cookie.replace(/;\s*domain=[^;]*/gi, "")}; Path=${base}/callback/`, - ), - ); - } else if (value !== undefined) { - res.setHeader(key, value as string | string[]); - } - } - - // Survives the identity provider round trip, for when OPKSSH - // redirects with script and no state was captured. - res.cookie(REQUEST_COOKIE, requestId, { - path: `${base}/`, - httpOnly: true, - sameSite: "lax", - maxAge: 5 * 60 * 1000, - }); - - const contentType = String(response.headers["content-type"] || ""); - const data = Buffer.from(response.data as ArrayBuffer); - if (contentType.includes("text/html")) { - res - .status(response.status) - .send(rewriteOpksshHtml(data.toString("utf-8"), chooserBase)); - } else { - res.status(response.status).send(data); - } - } catch (error) { - ctx.log.error( - `Error proxying OPKSSH chooser: ${error instanceof Error ? error.message : String(error)}`, - ); - res.status(500).send( - renderErrorPage({ - title: "Error", - heading: "Error", - message: "Failed to load authentication page. Please try again.", - requestId, - }), - ); - } - }); - - /** - * @openapi - * /plugin-api/opkssh/callback: - * get: - * summary: OAuth callback from the identity provider for an OPKSSH sign-in - * description: Public. The redirect URI registered with the identity provider. Finds the sign-in by OAuth state or the opkssh_request_id cookie and forwards to its proxied callback listener. - * tags: [OPKSSH] - * responses: - * 302: - * description: Forwarded to the sign-in's callback listener. - * 401: - * description: No sign-in matches this callback. - * 503: - * description: OPKSSH's callback listener is not ready yet. - */ - router.get("/callback", (req: Request, res: Response) => { - const base = prefix(req); - const state = typeof req.query.state === "string" ? req.query.state : ""; - let session = state ? sessions.takeByOAuthState(state) : undefined; - if (!session) { - const cookieId = readCookie(req, REQUEST_COOKIE); - if (cookieId) { - session = sessions.get(cookieId); - if (session) res.clearCookie(REQUEST_COOKIE, { path: `${base}/` }); - } - } - if (!session) { - ctx.log.warn("OPKSSH callback matched no sign-in"); - res - .status(401) - .send("Authentication callback failed: unable to identify session"); - return; - } - if (!session.callbackPort) { - res.status(503).send("OPKSSH callback listener not ready yet"); - return; - } - const query = req.url.includes("?") - ? req.url.slice(req.url.indexOf("?")) - : ""; - res.redirect(302, `${base}/callback/${session.requestId}${query}`); - }); - - /** - * @openapi - * /plugin-api/opkssh/callback/{requestId}: - * get: - * summary: Proxy OPKSSH's local callback listener for one sign-in - * description: Public. Forwards to OPKSSH's login-callback listener, which prints the key and certificate once the code exchange succeeds. Any subpath is proxied too. - * tags: [OPKSSH] - * parameters: - * - name: requestId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The listener's page. - * 404: - * description: Sign-in not found. - * 500: - * description: The listener is not ready. - */ - router.use("/callback/:requestId", async (req: Request, res: Response) => { - const requestId = param(req.params.requestId); - const session = sessions.get(requestId); - if (!session) { - sessionNotFound(res, requestId); - return; - } - if (!session.callbackPort) { - res.status(500).send( - renderErrorPage({ - title: "Error", - heading: "Callback Error", - message: - "OPKSSH callback listener not ready. Please try authenticating again.", - requestId, - }), - ); - return; - } - - const callbackBase = `${prefix(req)}/callback/${requestId}`; - const after = pathAfter(req, `/callback/${requestId}`); - // The listener serves /login-callback whatever the public path was. - const targetPath = - after === "" || after.startsWith("?") ? `/login-callback${after}` : after; - - try { - const response = await axios({ - method: req.method, - url: `http://127.0.0.1:${session.callbackPort}${targetPath}`, - headers: { ...req.headers, host: `127.0.0.1:${session.callbackPort}` }, - data: req.body, - timeout: 10000, - validateStatus: () => true, - maxRedirects: 0, - responseType: "arraybuffer", - }); - - for (const [key, value] of Object.entries(response.headers)) { - const lower = key.toLowerCase(); - if (lower === "transfer-encoding" || value === undefined) continue; - if (lower === "location" && String(value).startsWith("/")) { - res.setHeader(key, `${callbackBase}${String(value)}`); - } else { - res.setHeader(key, value as string | string[]); - } - } - - const contentType = String(response.headers["content-type"] || ""); - const data = Buffer.from(response.data as ArrayBuffer); - if (contentType.includes("text/html")) { - res - .status(response.status) - .send(rewriteOpksshHtml(data.toString("utf-8"), callbackBase)); - } else { - res.status(response.status).send(data); - } - } catch (error) { - ctx.log.error( - `Error handling OPKSSH callback: ${error instanceof Error ? error.message : String(error)}`, - ); - res.status(500).send( - renderErrorPage({ - title: "Error", - heading: "Error", - message: "An unexpected error occurred. Please try again.", - requestId, - }), - ); - } - }); -} diff --git a/plugins/opkssh/src/backend/tables.ts b/plugins/opkssh/src/backend/tables.ts deleted file mode 100644 index 4404a6983..000000000 --- a/plugins/opkssh/src/backend/tables.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - adoptLegacyTable, - defineTable, - id, - refHost, - refUser, - text, - varchar, -} from "@termix/plugin-sdk/db"; - -/** - * One cached certificate per user and host. Adopted from core's - * opkssh_tokens, so the column and index names are the legacy ones. - * - * sshCert and privateKey are sealed with ctx.secrets.seal. Rows written by - * 2.8 were encrypted with the user's data key instead; they do not unseal and - * are treated as expired, which costs one browser sign-in. - */ -export const tokens = adoptLegacyTable( - "opkssh_tokens", - defineTable( - "tokens", - { - id: id(), - userId: refUser(), - hostId: refHost(), - sshCert: text().notNull(), - privateKey: text().notNull(), - email: text(), - sub: text(), - issuer: text(), - audience: text(), - createdAt: text().notNull().defaultNow(), - expiresAt: varchar().notNull(), - lastUsed: text(), - }, - { - uniques: [ - { - name: "idx_opkssh_tokens_user_host", - columns: ["userId", "hostId"], - }, - ], - }, - ), -); - -export const tables = [tokens]; diff --git a/plugins/opkssh/src/backend/token-store.ts b/plugins/opkssh/src/backend/token-store.ts deleted file mode 100644 index 125cdaf70..000000000 --- a/plugins/opkssh/src/backend/token-store.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { and, eq } from "drizzle-orm"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; - -/* eslint-disable @typescript-eslint/no-explicit-any */ -// ctx.db.define hands the table back untyped and the drizzle handle is the -// server's own, so both are typed at this module's edge. -type Table = any; -type Drizzle = any; -/* eslint-enable @typescript-eslint/no-explicit-any */ - -/** How long a certificate is cached after sign-in. */ -export const TOKEN_LIFETIME_MS = 24 * 60 * 60 * 1000; - -export interface StoredCertificate { - sshCert: string; - privateKey: string; -} - -export interface CertificateIdentity { - email?: string; - sub?: string; - issuer?: string; - audience?: string; -} - -export interface TokenStatus { - exists: boolean; - expiresAt?: string; - email?: string | null; -} - -export type TokenStore = ReturnType; - -export function createTokenStore(ctx: PluginContext, table: Table) { - const client = () => ctx.db.client(); - const byUserAndHost = (userId: string, hostId: number) => - and(eq(table.userId, userId), eq(table.hostId, hostId)); - - async function find(userId: string, hostId: number) { - const rows = await ( - await client() - ) - .select() - .from(table) - .where(byUserAndHost(userId, hostId)) - .limit(1); - return rows[0] ?? null; - } - - async function remove(userId: string, hostId: number): Promise { - const existing = await find(userId, hostId); - if (!existing) return false; - await (await client()).delete(table).where(byUserAndHost(userId, hostId)); - await ctx.db.persist(); - return true; - } - - function expired(row: { expiresAt: string }): boolean { - const expiresAt = new Date(row.expiresAt).getTime(); - return !Number.isFinite(expiresAt) || expiresAt <= Date.now(); - } - - return { - async save( - userId: string, - hostId: number, - certificate: StoredCertificate, - identity: CertificateIdentity, - ): Promise { - const expiresAt = new Date(Date.now() + TOKEN_LIFETIME_MS).toISOString(); - const values = { - sshCert: await ctx.secrets.seal(certificate.sshCert), - privateKey: await ctx.secrets.seal(certificate.privateKey), - email: identity.email ?? null, - sub: identity.sub ?? null, - issuer: identity.issuer ?? null, - audience: identity.audience ?? null, - createdAt: new Date().toISOString(), - expiresAt, - lastUsed: null, - }; - if (await find(userId, hostId)) { - await ( - await client() - ) - .update(table) - .set(values) - .where(byUserAndHost(userId, hostId)); - } else { - await ( - await client() - ) - .insert(table) - .values({ userId, hostId, ...values }); - } - await ctx.db.persist(); - return expiresAt; - }, - - /** - * The usable certificate, or null. An expired row, or one that does not - * unseal (written by 2.8), is deleted. - */ - async get( - userId: string, - hostId: number, - ): Promise { - const row = await find(userId, hostId); - if (!row) return null; - if (expired(row)) { - await remove(userId, hostId); - return null; - } - const sshCert = await ctx.secrets.unseal(row.sshCert); - const privateKey = await ctx.secrets.unseal(row.privateKey); - if (!sshCert || !privateKey) { - await remove(userId, hostId); - return null; - } - await ( - await client() - ) - .update(table) - .set({ lastUsed: new Date().toISOString() }) - .where(byUserAndHost(userId, hostId)); - await ctx.db.persist({ lazy: true }); - return { sshCert, privateKey }; - }, - - async status(userId: string, hostId: number): Promise { - const row = await find(userId, hostId); - if (!row) return { exists: false }; - if (expired(row)) { - await remove(userId, hostId); - return { exists: false }; - } - return { exists: true, expiresAt: row.expiresAt, email: row.email }; - }, - - remove, - }; -} diff --git a/plugins/opkssh/src/frontend/OpksshAuthEditor.tsx b/plugins/opkssh/src/frontend/OpksshAuthEditor.tsx deleted file mode 100644 index 719af72cc..000000000 --- a/plugins/opkssh/src/frontend/OpksshAuthEditor.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useTranslation } from "@termix/plugin-sdk/frontend"; - -const DOCS_URL = "https://docs.termix.site/features/authentication/opkssh"; - -/** The host editor's "opkssh" auth method: nothing to fill in, just a pointer. */ -export function OpksshAuthEditor() { - const { t } = useTranslation(); - return ( -
-
- - {t("editor.label")} - - - {t("editor.docs")} - -
-

- {t("editor.description")} -

-
- ); -} diff --git a/plugins/opkssh/src/frontend/OpksshDialog.tsx b/plugins/opkssh/src/frontend/OpksshDialog.tsx deleted file mode 100644 index 66b8be8bd..000000000 --- a/plugins/opkssh/src/frontend/OpksshDialog.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { Shield, ExternalLink, Loader2, AlertCircle } from "lucide-react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { Button } from "@termix/plugin-sdk/ui"; - -export type SignInStage = - "chooser" | "waiting" | "authenticating" | "completed" | "error"; - -interface OpksshDialogProps { - authUrl: string; - stage: SignInStage; - error?: string; - providers?: Array<{ alias: string; issuer: string }>; - onCancel: () => void; - onOpenUrl: () => void; - onSelectProvider?: (alias: string) => void; - backgroundColor?: string; -} - -/** The browser sign-in the terminal shows while OPKSSH waits. */ -export function OpksshDialog({ - authUrl, - stage, - error, - providers, - onCancel, - onOpenUrl, - onSelectProvider, - backgroundColor, -}: OpksshDialogProps) { - const { t } = useTranslation(); - - return ( -
-
-
-
-
- -

- {t("dialog.title")} -

-
- {stage === "chooser" && ( -

- {t("dialog.description")} -

- )} -
-
- {stage === "chooser" && ( - <> - {providers && providers.length > 0 && onSelectProvider ? ( -
- {providers.map((provider) => ( - - ))} -
- ) : authUrl ? ( - - ) : null} -
- -
- - )} - - {(stage === "waiting" || stage === "authenticating") && ( - <> -
- -

- {stage === "waiting" - ? t("dialog.waiting") - : t("dialog.authenticating")} -

-
-
- -
- - )} - - {stage === "error" && error && ( - <> -
- -
-

- {t("common.error")} -

-

- {error} -

-
-
-
- -
- - )} -
-
-
- ); -} diff --git a/plugins/opkssh/src/frontend/OpksshOverlay.tsx b/plugins/opkssh/src/frontend/OpksshOverlay.tsx deleted file mode 100644 index ce18064e3..000000000 --- a/plugins/opkssh/src/frontend/OpksshOverlay.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { OpksshDialog, type SignInStage } from "./OpksshDialog"; - -/** What the ssh-terminal plugin hands a "terminal.overlay" component. */ -export interface TerminalOverlayProps { - host: { id: number | string; [key: string]: unknown } | undefined; - backgroundColor?: string; - subscribe: ( - listener: (message: { type: string; [key: string]: unknown }) => void, - ) => () => void; - holdConnectTimeout: (held: boolean) => void; - fail: (message: string) => void; - disconnect: () => void; - send: (type: string, data?: unknown) => void; - connectPayload: () => Record; -} - -interface DialogState { - authUrl: string; - requestId: string; - stage: SignInStage; - error?: string; - providers?: Array<{ alias: string; issuer: string }>; -} - -/** How long the sign-in dialog waits before giving up on the connection. */ -const DIALOG_TIMEOUT_MS = 300_000; - -/** - * Runs the OPKSSH browser sign-in for a terminal: asks the server to start - * it, shows the provider chooser, and reconnects once the certificate is - * stored. - */ -export function OpksshOverlay({ - subscribe, - holdConnectTimeout, - fail, - disconnect, - send, - connectPayload, - backgroundColor, -}: TerminalOverlayProps) { - const { t } = useTranslation(); - const [dialog, setDialog] = useState(null); - const timerRef = useRef | null>(null); - const heldRef = useRef(false); - // A second "auth required" after a sign-in means the certificate did not work. - const failedRef = useRef(false); - - useEffect(() => { - const clearTimer = () => { - if (timerRef.current) clearTimeout(timerRef.current); - timerRef.current = null; - }; - const hold = (held: boolean) => { - if (heldRef.current === held) return; - heldRef.current = held; - holdConnectTimeout(held); - }; - const showError = (requestId: unknown, error: string) => { - clearTimer(); - setDialog((current) => - current - ? { ...current, stage: "error", error } - : { - authUrl: "", - requestId: typeof requestId === "string" ? requestId : "", - stage: "error", - error, - }, - ); - }; - - const unsubscribe = subscribe((message) => { - switch (message.type) { - case "opkssh_auth_required": - hold(true); - if (failedRef.current) { - clearTimer(); - setDialog(null); - hold(false); - fail(t("errors.authFailed")); - } else { - failedRef.current = true; - send("opkssh_start_auth", { hostId: message.hostId }); - } - break; - case "opkssh_status": - if (message.stage === "chooser") { - setDialog({ - authUrl: typeof message.url === "string" ? message.url : "", - requestId: - typeof message.requestId === "string" ? message.requestId : "", - stage: "chooser", - providers: Array.isArray(message.providers) - ? (message.providers as DialogState["providers"]) - : undefined, - }); - clearTimer(); - timerRef.current = setTimeout(() => { - setDialog(null); - hold(false); - disconnect(); - }, DIALOG_TIMEOUT_MS); - } else { - setDialog((current) => - current - ? { ...current, stage: message.stage as SignInStage } - : null, - ); - } - break; - case "opkssh_completed": - clearTimer(); - setDialog(null); - send("opkssh_auth_completed", connectPayload()); - break; - case "opkssh_error": - failedRef.current = true; - showError(message.requestId, String(message.error ?? "")); - break; - case "opkssh_timeout": - failedRef.current = true; - showError(message.requestId, t("errors.timeout")); - break; - case "opkssh_config_error": - showError( - message.requestId, - String(message.instructions || message.error || ""), - ); - break; - case "connected": - case "sessionAttached": - failedRef.current = false; - hold(false); - break; - case "session_closed": - clearTimer(); - setDialog(null); - hold(false); - break; - } - }); - - return () => { - unsubscribe(); - clearTimer(); - if (heldRef.current) { - heldRef.current = false; - holdConnectTimeout(false); - } - }; - }, [ - subscribe, - holdConnectTimeout, - fail, - disconnect, - send, - connectPayload, - t, - ]); - - if (!dialog) return null; - - const close = () => { - if (timerRef.current) clearTimeout(timerRef.current); - timerRef.current = null; - if (dialog.requestId) { - send("opkssh_cancel", { requestId: dialog.requestId }); - } - setDialog(null); - if (heldRef.current) { - heldRef.current = false; - holdConnectTimeout(false); - } - }; - - return ( - { - window.open(dialog.authUrl, "_blank"); - }} - onSelectProvider={(alias) => { - if (!dialog.authUrl) return; - window.open( - `${dialog.authUrl}/select?op=${encodeURIComponent(alias)}`, - "_blank", - ); - setDialog((current) => - current ? { ...current, stage: "waiting" } : null, - ); - }} - backgroundColor={backgroundColor} - /> - ); -} diff --git a/plugins/opkssh/src/frontend/RedirectUriSetting.tsx b/plugins/opkssh/src/frontend/RedirectUriSetting.tsx deleted file mode 100644 index c96a3804e..000000000 --- a/plugins/opkssh/src/frontend/RedirectUriSetting.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { toast } from "sonner"; -import { - useTranslation, - type SettingsComponentProps, -} from "@termix/plugin-sdk/frontend"; -import { Button, Input, copyToClipboard } from "@termix/plugin-sdk/ui"; - -const CALLBACK_PATH = "plugin-api/opkssh/callback"; -const LEGACY_CALLBACK_PATH = "host/opkssh-callback"; - -/** The public callback URI to register with the identity provider. */ -export function redirectUri( - legacy: boolean, - baseUri = document.baseURI, -): string { - return new URL( - legacy ? LEGACY_CALLBACK_PATH : CALLBACK_PATH, - baseUri, - ).toString(); -} - -export function RedirectUriSetting({ values }: SettingsComponentProps) { - const { t } = useTranslation(); - const uri = redirectUri(values.legacyCallback === true); - - return ( -
- - {t("settings.redirectUri.label")} - -
- - -
-

- {t("settings.redirectUri.description")} -

-
- ); -} diff --git a/plugins/opkssh/src/frontend/index.tsx b/plugins/opkssh/src/frontend/index.tsx deleted file mode 100644 index eb8b4542c..000000000 --- a/plugins/opkssh/src/frontend/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { ComponentType } from "react"; -import type { TermixApp } from "@termix/plugin-sdk/frontend"; -import { OpksshAuthEditor } from "./OpksshAuthEditor"; -import { OpksshOverlay } from "./OpksshOverlay"; -import { RedirectUriSetting } from "./RedirectUriSetting"; - -export function activate(app: TermixApp): void { - app.registerSshAuthEditor({ - authType: "opkssh", - titleKey: "hosts.filterAuthOpkssh", - component: OpksshAuthEditor, - }); - - // The browser sign-in while a terminal connects to an OPKSSH host. - app.registerSlotContribution("terminal.overlay", { - actionId: "opkssh.signIn", - titleKey: "dialog.title", - kind: "component", - component: OpksshOverlay as unknown as ComponentType< - Record - >, - }); - - app.registerSettingsComponent("redirectUri", RedirectUriSetting); -} diff --git a/plugins/opkssh/tests/backend/helpers-pure.test.ts b/plugins/opkssh/tests/backend/helpers-pure.test.ts deleted file mode 100644 index 4dffd09c2..000000000 --- a/plugins/opkssh/tests/backend/helpers-pure.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { - binaryName, - binarySpec, - DEFAULT_VERSION, -} from "../../src/backend/binary.js"; -import { - parseProviders, - validateRedirectUris, -} from "../../src/backend/config.js"; -import { - normalizeSelectOpParam, - rewriteOpksshHtml, -} from "../../src/backend/html.js"; - -describe("config", () => { - it("reads providers and strips the issuer scheme", () => { - expect( - parseProviders(`providers: - - alias: google - issuer: https://accounts.google.com - redirect_uris: - - http://localhost:3000/login-callback - - alias: broken -`), - ).toEqual([ - { - alias: "google", - issuer: "accounts.google.com", - redirectUris: ["http://localhost:3000/login-callback"], - }, - ]); - }); - - it("accepts localhost redirect_uris only, naming the public callback", () => { - const callback = "https://termix.test/plugin-api/opkssh/callback"; - expect( - validateRedirectUris( - [ - { - alias: "a", - issuer: "x", - redirectUris: ["http://127.0.0.1:10001/login-callback"], - }, - ], - callback, - ), - ).toEqual({ ok: true }); - const refused = validateRedirectUris( - [{ alias: "a", issuer: "x", redirectUris: [callback] }], - callback, - ); - expect(refused.ok).toBe(false); - expect(refused.ok === false && refused.message).toContain(callback); - }); -}); - -describe("html", () => { - it("sends the page's links through the proxied path", () => { - const html = rewriteOpksshHtml( - 'g
', - "/plugin-api/opkssh/chooser/req-1", - ); - expect(html).toContain( - 'href="/plugin-api/opkssh/chooser/req-1/select?op=google"', - ); - expect(html).toContain('action="/plugin-api/opkssh/chooser/req-1/login"'); - expect(html).toContain(''); - }); - - it("maps a provider alias to the name OPKSSH's chooser uses", () => { - const providers = [{ alias: "work", issuer: "accounts.google.com" }]; - expect(normalizeSelectOpParam("work", providers)).toBe("google"); - expect(normalizeSelectOpParam("google", providers)).toBe("google"); - expect(normalizeSelectOpParam("other", providers)).toBe("other"); - }); -}); - -describe("binary", () => { - it("names the release asset per platform", () => { - expect(binaryName("linux", "x64")).toBe("opkssh-linux-amd64"); - expect(binaryName("darwin", "arm64")).toBe("opkssh-osx-arm64"); - expect(binaryName("win32", "x64")).toBe("opkssh-windows-amd64.exe"); - }); - - it("pins the default release and checks the Docker copy first", () => { - const spec = binarySpec({}, "linux", "x64", "/app"); - expect(spec).toMatchObject({ - name: "opkssh-linux-amd64", - version: DEFAULT_VERSION, - url: `https://github.com/openpubkey/opkssh/releases/download/${DEFAULT_VERSION}/opkssh-linux-amd64`, - prebuilt: [path.join("/app", "opkssh-bundled", "opkssh-linux-amd64")], - }); - expect(spec.sha256).toMatch(/^[0-9a-f]{64}$/); - expect( - binarySpec({ OPKSSH_BUNDLED_DIR: "/srv/opk" }, "linux", "x64").prebuilt, - ).toEqual([path.join("/srv/opk", "opkssh-linux-amd64")]); - }); - - it("needs a checksum for any other release", () => { - expect(() => binarySpec({ OPKSSH_VERSION: "v9" }, "linux", "x64")).toThrow( - /no trusted SHA-256/, - ); - const sha = "a".repeat(64); - expect( - binarySpec({ OPKSSH_VERSION: "v9", OPKSSH_SHA256: sha }, "linux", "x64"), - ).toMatchObject({ version: "v9", sha256: sha }); - }); - - it("reads the per-architecture checksum the Docker image passes on", () => { - const amd = "b".repeat(64); - const arm = "c".repeat(64); - const env = { - OPKSSH_VERSION: "v9", - OPKSSH_SHA256_AMD64: amd, - OPKSSH_SHA256_ARM64: arm, - }; - expect(binarySpec(env, "linux", "x64").sha256).toBe(amd); - expect(binarySpec(env, "linux", "arm64").sha256).toBe(arm); - }); -}); diff --git a/plugins/opkssh/tests/backend/helpers.ts b/plugins/opkssh/tests/backend/helpers.ts deleted file mode 100644 index 4c2396c15..000000000 --- a/plugins/opkssh/tests/backend/helpers.ts +++ /dev/null @@ -1,193 +0,0 @@ -import http from "node:http"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import type { AddressInfo } from "node:net"; -import { fileURLToPath } from "node:url"; -import express, { type Router } from "express"; -import { - createFakeProcess, - createMockCtx, - createTestDb, - type FakeProcess, - type MockPluginContext, - type TestDb, -} from "@termix/plugin-sdk/testing"; -import type { - PluginHostSummary, - PluginSshAuthProvider, -} from "@termix/plugin-sdk/backend"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import manifestJson from "../../manifest.json"; - -export const pluginDir = fileURLToPath(new URL("../..", import.meta.url)); -export const manifest = manifestJson as unknown as PluginManifest; - -/** Core's 2.8 opkssh_tokens, as db/index.ts created it. */ -export const LEGACY_DDL = ` - CREATE TABLE IF NOT EXISTS opkssh_tokens ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL, - host_id INTEGER NOT NULL, - ssh_cert TEXT NOT NULL, - private_key TEXT NOT NULL, - email TEXT, - sub TEXT, - issuer TEXT, - audience TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TEXT NOT NULL, - last_used TEXT, - UNIQUE(user_id, host_id), - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE - ); -`; - -export const PROVIDERS_CONFIG = `providers: - - alias: google - issuer: https://accounts.google.com - client_id: abc - scopes: openid email -`; - -/** A fake terminal socket that records what it was sent. */ -export function fakeSocket() { - const sent: Array> = []; - const closers: Array<() => void> = []; - return { - sent, - socket: { - send: (data: string) => sent.push(JSON.parse(data)), - on: (_event: "close", listener: () => void) => closers.push(listener), - }, - close: () => closers.forEach((listener) => listener()), - }; -} - -export interface TestServer { - db: TestDb; - mock: MockPluginContext; - dataDir: string; - processes: FakeProcess[]; - provider: () => PluginSshAuthProvider; - request: ( - method: string, - path: string, - options?: { user?: string; headers?: Record }, - // Route bodies vary per test; asserting on them is the point. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ) => Promise<{ status: number; body: any; location: string | null }>; - close: () => Promise; -} - -export async function startServer( - options: { - capabilities?: string[]; - hosts?: PluginHostSummary[]; - settings?: Record; - before?: (sqlite: TestDb["sqlite"]) => void; - } = {}, -): Promise { - const db = await createTestDb(pluginDir, { before: options.before }); - db.sqlite - .prepare("INSERT OR IGNORE INTO users (id) VALUES (?)") - .run("user-1"); - db.sqlite.prepare("INSERT OR IGNORE INTO ssh_data (id) VALUES (?)").run(7); - const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opkssh-")); - const processes: FakeProcess[] = []; - - let router: Router | null = null; - const mock = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: options.capabilities ?? manifest.capabilities, - db: db.database, - router: () => (router = express.Router()), - hosts: options.hosts ?? [ - { - id: 7, - userId: "user-1", - name: "box", - ip: "10.0.0.7", - port: 22, - username: "root", - } as PluginHostSummary, - ], - settings: options.settings, - baseUrl: "https://termix.test", - process: { - run: () => { - const child = createFakeProcess(); - processes.push(child); - return child; - }, - ensureBinary: async (spec) => `/opt/${spec.name}`, - }, - }); - // The plugin's data folder, somewhere the test can write. - (mock.ctx as unknown as { files: unknown }).files = { - dataDir: async () => dataDir, - }; - - const { activate } = await import("../../src/backend/index.js"); - await activate(mock.ctx); - - const app = express(); - app.use(express.json()); - app.use((req, _res, next) => { - mock.setActor(req.header("x-test-user") || undefined); - next(); - }); - app.use((req, res, next) => router!(req, res, next)); - - const server = http.createServer(app); - await new Promise((resolve) => server.listen(0, resolve)); - const { port } = server.address() as AddressInfo; - - return { - db, - mock, - dataDir, - processes, - provider: () => - mock.auth.sshAuthProviders.find( - (provider) => provider.type === "opkssh", - )!, - async request(method, requestPath, { user = "user-1", headers = {} } = {}) { - const response = await fetch(`http://127.0.0.1:${port}${requestPath}`, { - method, - redirect: "manual", - headers: { ...(user ? { "x-test-user": user } : {}), ...headers }, - }); - const text = await response.text(); - let parsed: unknown = null; - try { - parsed = text ? JSON.parse(text) : null; - } catch { - parsed = text; - } - return { - status: response.status, - body: parsed, - location: response.headers.get("location"), - }; - }, - async close() { - await new Promise((resolve) => server.close(() => resolve())); - db.close(); - await fs.rm(dataDir, { recursive: true, force: true }); - }, - }; -} - -export const env = (hostId = 7) => ({ - client: {}, - userId: "user-1", - hostId, - purpose: "terminal", - interactive: true, - log: () => {}, -}); - -export const HOST = { id: 7, ip: "10.0.0.7", port: 22, username: "root" }; diff --git a/plugins/opkssh/tests/backend/legacy-urls.test.ts b/plugins/opkssh/tests/backend/legacy-urls.test.ts deleted file mode 100644 index 0c5ece68d..000000000 --- a/plugins/opkssh/tests/backend/legacy-urls.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { validateManifest } from "@termix/plugin-sdk/manifest"; -import manifest from "../../manifest.json"; - -// Old URLs something outside Termix still calls keep reaching the plugin. -const redirects = manifest.contributes.http.legacyRedirects as unknown[]; - -describe("legacy URLs", () => { - it("declares them in a manifest that validates", () => { - expect(validateManifest(manifest)).toEqual([]); - }); - - it("redirects /host/opkssh-callback, which OPKSSH identity providers were set up with before 2.9", () => { - expect(redirects).toContainEqual({ - from: "/host/opkssh-callback", - to: "/callback", - }); - }); -}); diff --git a/plugins/opkssh/tests/backend/opkssh.test.ts b/plugins/opkssh/tests/backend/opkssh.test.ts deleted file mode 100644 index 1b7ff22b0..000000000 --- a/plugins/opkssh/tests/backend/opkssh.test.ts +++ /dev/null @@ -1,341 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { PluginCapabilityError } from "@termix/plugin-sdk/backend"; -import { createMockCtx, createTestDb } from "@termix/plugin-sdk/testing"; -import { - HOST, - LEGACY_DDL, - PROVIDERS_CONFIG, - env, - fakeSocket, - manifest, - pluginDir, - startServer, - type TestServer, -} from "./helpers.js"; - -const certs = vi.hoisted(() => ({ applyCertificateAuth: vi.fn() })); -vi.mock("@termix/plugin-sdk/ssh-certs", () => certs); - -const KEY = `-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAA ------END OPENSSH PRIVATE KEY-----`; -const CERT = "ssh-ed25519-cert-v01@openssh.com AAAAC3NzaC1lZDI1NTE5"; - -let server: TestServer | null = null; - -beforeEach(() => { - certs.applyCertificateAuth.mockReset(); -}); - -afterEach(async () => { - await server?.close(); - server = null; -}); - -async function writeConfig(content = PROVIDERS_CONFIG) { - await fs.writeFile(path.join(server!.dataDir, "config.yml"), content); -} - -/** Starts a sign-in and returns its request id and what the socket saw. */ -async function startSignIn(origin = "https://termix.test") { - const socket = fakeSocket(); - await server!.provider().startInteraction!({ - userId: "user-1", - hostId: 7, - host: { name: "box", ip: "10.0.0.7", username: "root" }, - socket: socket.socket, - requestOrigin: origin, - payload: {}, - }); - return socket; -} - -describe("adopting opkssh_tokens", () => { - it("keeps the 2.8 rows and the unique index", async () => { - const db = await createTestDb(pluginDir, { - before: (sqlite) => { - sqlite.exec("INSERT INTO users (id) VALUES ('user-1')"); - sqlite.exec("INSERT INTO ssh_data (id) VALUES (7)"); - sqlite.exec(LEGACY_DDL); - sqlite - .prepare( - "INSERT INTO opkssh_tokens (user_id, host_id, ssh_cert, private_key, expires_at) VALUES (?, ?, ?, ?, ?)", - ) - .run("user-1", 7, "cert", "key", "2099-01-01T00:00:00.000Z"); - }, - }); - const rows = db.sqlite.prepare("SELECT * FROM p_opkssh_tokens").all(); - expect(rows).toHaveLength(1); - expect(() => - db.sqlite - .prepare( - "INSERT INTO p_opkssh_tokens (user_id, host_id, ssh_cert, private_key, expires_at) VALUES (?, ?, ?, ?, ?)", - ) - .run("user-1", 7, "c", "k", "2099-01-01T00:00:00.000Z"), - ).toThrow(/UNIQUE/); - expect( - db.sqlite - .prepare("SELECT name FROM sqlite_master WHERE name = 'opkssh_tokens'") - .get(), - ).toBeUndefined(); - db.close(); - }); - - it("creates the table on a fresh install", async () => { - const db = await createTestDb(pluginDir); - expect( - db.sqlite - .prepare( - "SELECT name FROM sqlite_master WHERE name = 'p_opkssh_tokens'", - ) - .get(), - ).toBeTruthy(); - db.close(); - }); -}); - -describe("activate", () => { - it("registers the opkssh auth type and its routes", async () => { - server = await startServer(); - expect(server.provider()).toMatchObject({ - type: "opkssh", - interaction: "opkssh", - supportsBackground: false, - }); - expect(server.mock.httpRouters).toHaveLength(1); - expect(server.mock.binaries[0]?.name).toMatch(/^opkssh-/); - }); - - it.each(["db:own", "auth:provide", "network:serve"])( - "fails closed without %s", - async (capability) => { - const mock = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: manifest.capabilities.filter((c) => c !== capability), - db: (await createTestDb(pluginDir)).database, - }); - const { activate } = await import("../../src/backend/index.js"); - await expect(activate(mock.ctx)).rejects.toThrow(PluginCapabilityError); - }, - ); -}); - -describe("the opkssh provider", () => { - it("asks for a sign-in without a cached certificate", async () => { - server = await startServer(); - const outcome = await server.provider().prepare({}, HOST, env()); - expect(outcome).toMatchObject({ - status: "interaction-required", - interaction: "opkssh", - flag: "requiresOPKSSHAuth", - }); - }); - - it("connects with the certificate a sign-in stored", async () => { - server = await startServer(); - await writeConfig(); - await startSignIn(); - server.processes[0].emitStdout(`${KEY}\n${CERT}\n`); - await vi.waitFor(() => - expect( - server!.db.sqlite.prepare("SELECT * FROM p_opkssh_tokens").all(), - ).toHaveLength(1), - ); - - const config: Record = {}; - const outcome = await server.provider().prepare(config, HOST, env()); - expect(outcome).toEqual({ status: "ready" }); - expect(certs.applyCertificateAuth).toHaveBeenCalledWith( - config, - {}, - { privateKey: KEY, certificate: CERT }, - "root", - ); - const row = server.db.sqlite - .prepare("SELECT ssh_cert FROM p_opkssh_tokens") - .get() as { ssh_cert: string }; - expect(row.ssh_cert).not.toContain("cert-v01"); - }); - - it("treats a 2.8 row it cannot unseal as expired", async () => { - server = await startServer({ - before: (sqlite) => { - sqlite.exec("INSERT INTO users (id) VALUES ('user-1')"); - sqlite.exec("INSERT INTO ssh_data (id) VALUES (7)"); - sqlite.exec(LEGACY_DDL); - sqlite - .prepare( - "INSERT INTO opkssh_tokens (user_id, host_id, ssh_cert, private_key, expires_at) VALUES (?, ?, ?, ?, ?)", - ) - .run("user-1", 7, "v2:old", "v2:old", "2099-01-01T00:00:00.000Z"); - }, - }); - const outcome = await server.provider().prepare({}, HOST, env()); - expect(outcome.status).toBe("interaction-required"); - expect( - server.db.sqlite.prepare("SELECT * FROM p_opkssh_tokens").all(), - ).toHaveLength(0); - }); - - it("forgets the certificate when the server refuses it", async () => { - server = await startServer(); - await writeConfig(); - await startSignIn(); - server.processes[0].emitStdout(`${KEY}\n${CERT}\n`); - await vi.waitFor(() => - expect( - server!.db.sqlite.prepare("SELECT * FROM p_opkssh_tokens").all(), - ).toHaveLength(1), - ); - const followUp = server.provider().onAuthFailed!(HOST, env(), { - error: new Error("All configured authentication methods failed"), - retries: 0, - canRetry: true, - methodNotAvailable: false, - }); - expect(followUp).toMatchObject({ status: "interaction-required" }); - await vi.waitFor(() => - expect( - server!.db.sqlite.prepare("SELECT * FROM p_opkssh_tokens").all(), - ).toHaveLength(0), - ); - }); -}); - -describe("the browser sign-in", () => { - it("runs opkssh with the plugin's config and the public callback", async () => { - server = await startServer(); - await writeConfig(); - const socket = await startSignIn(); - const run = server.mock.processRuns[0]; - expect(run.file).toMatch(/opkssh-/); - expect(run.args).toContain( - `--config-path=${path.join(server.dataDir, "config.yml")}`, - ); - expect(run.args).toContain( - "--remote-redirect-uri=https://termix.test/plugin-api/opkssh/callback", - ); - - server.processes[0].emitStderr( - "Opening browser to http://localhost:4567/chooser\n", - ); - const status = socket.sent.find((m) => m.type === "opkssh_status"); - expect(status).toMatchObject({ - stage: "chooser", - url: expect.stringMatching( - /^https:\/\/termix\.test\/plugin-api\/opkssh\/chooser\/[\w-]+$/, - ), - }); - }); - - it("keeps the old callback for an install upgraded from 2.8", async () => { - server = await startServer({ settings: { legacyCallback: true } }); - await writeConfig(); - await startSignIn(); - expect(server.mock.processRuns[0].args).toContain( - "--remote-redirect-uri=https://termix.test/host/opkssh-callback", - ); - }); - - it("writes a template and reports a missing config", async () => { - server = await startServer(); - const socket = await startSignIn(); - expect(socket.sent[0]).toMatchObject({ type: "opkssh_config_error" }); - expect( - await fs.readFile(path.join(server.dataDir, "config.yml"), "utf8"), - ).toContain("OPKSSH Configuration"); - expect(server.mock.processRuns).toHaveLength(0); - }); - - it("refuses redirect_uris that are not localhost", async () => { - server = await startServer(); - await writeConfig( - `${PROVIDERS_CONFIG} redirect_uris:\n - https://termix.example/host/opkssh-callback\n`, - ); - const socket = await startSignIn(); - expect(socket.sent[0]).toMatchObject({ type: "opkssh_config_error" }); - expect(String(socket.sent[0].error)).toMatch(/must only contain localhost/); - }); - - it("kills the process when the terminal goes away", async () => { - server = await startServer(); - await writeConfig(); - const socket = await startSignIn(); - socket.close(); - await vi.waitFor(() => - expect(server!.processes[0].killed).toContain("SIGTERM"), - ); - }); -}); - -describe("routes", () => { - it("reports and forgets a cached certificate", async () => { - server = await startServer(); - expect((await server.request("GET", "/token/7")).body).toEqual({ - exists: false, - }); - await writeConfig(); - await startSignIn(); - server.processes[0].emitStdout(`${KEY}\n${CERT}\n`); - await vi.waitFor(async () => - expect((await server!.request("GET", "/token/7")).body.exists).toBe(true), - ); - expect((await server.request("DELETE", "/token/7")).status).toBe(200); - expect((await server.request("GET", "/token/7")).body.exists).toBe(false); - }); - - it("refuses a host the user cannot reach, and anonymous callers", async () => { - server = await startServer(); - expect((await server.request("GET", "/token/99")).status).toBe(404); - expect((await server.request("GET", "/token/7", { user: "" })).status).toBe( - 401, - ); - }); - - it("forwards the identity provider's callback to the sign-in it belongs to", async () => { - server = await startServer(); - await writeConfig(); - const socket = await startSignIn(); - server.processes[0].emitStderr( - "Opening browser to http://localhost:4567/chooser\n", - ); - const requestId = String( - socket.sent.find((m) => m.type === "opkssh_status")?.requestId, - ); - const cookie = { cookie: `opkssh_request_id=${requestId}` }; - - // OPKSSH's callback listener is not up yet. - expect( - ( - await server.request("GET", "/callback?code=x", { - user: "", - headers: cookie, - }) - ).status, - ).toBe(503); - - server.processes[0].emitStderr("listening on http://127.0.0.1:5555/\n"); - const forwarded = await server.request("GET", "/callback?code=x&state=s", { - user: "", - headers: cookie, - }); - expect(forwarded.status).toBe(302); - expect(forwarded.location).toBe( - `/plugin-api/opkssh/callback/${requestId}?code=x&state=s`, - ); - - expect( - (await server.request("GET", "/callback?code=x", { user: "" })).status, - ).toBe(401); - }); - - it("answers an unknown sign-in with a not-found page", async () => { - server = await startServer(); - const response = await server.request("GET", "/chooser/nope", { user: "" }); - expect(response.status).toBe(404); - expect(String(response.body)).toContain("Session Not Found"); - }); -}); diff --git a/plugins/opkssh/tests/frontend/activate.test.tsx b/plugins/opkssh/tests/frontend/activate.test.tsx deleted file mode 100644 index e9760b226..000000000 --- a/plugins/opkssh/tests/frontend/activate.test.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { act, fireEvent, render, screen } from "@testing-library/react"; -import { - renderWithApp, - type RenderedPluginApp, -} from "@termix/plugin-sdk/testing"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import * as plugin from "../../src/frontend/index"; -import { OpksshOverlay } from "../../src/frontend/OpksshOverlay"; -import { redirectUri } from "../../src/frontend/RedirectUriSetting"; -import manifestJson from "../../manifest.json"; -import locales from "../../locales/en.json"; - -vi.mock("@termix/plugin-sdk/frontend", async (importOriginal) => ({ - ...(await importOriginal()), - useTranslation: () => ({ t: (key: string) => key }), -})); - -const manifest = manifestJson as unknown as PluginManifest; - -let rendered: RenderedPluginApp | null = null; - -afterEach(async () => { - await rendered?.deactivate(); - rendered = null; -}); - -describe("opkssh activate", () => { - it("registers the auth editor, the terminal overlay and the settings field", async () => { - rendered = await renderWithApp(plugin, { manifest, locales }); - expect(rendered.registered.settingsComponents()).toEqual(["redirectUri"]); - expect(rendered.registered.slot("terminal.overlay")).toEqual([ - "opkssh.signIn", - ]); - await rendered.deactivate(); - expect(rendered.registered.settingsComponents()).toEqual([]); - expect(rendered.registered.slot("terminal.overlay")).toEqual([]); - rendered = null; - }); -}); - -describe("redirectUri", () => { - it("names the plugin callback, or the 2.8 one", () => { - expect(redirectUri(false, "https://t.example/app/")).toBe( - "https://t.example/app/plugin-api/opkssh/callback", - ); - expect(redirectUri(true, "https://t.example/")).toBe( - "https://t.example/host/opkssh-callback", - ); - }); -}); - -describe("OpksshOverlay", () => { - function setup() { - let listener: (message: { - type: string; - [key: string]: unknown; - }) => void = () => {}; - const props = { - host: { id: 7 }, - subscribe: vi.fn((next: typeof listener) => { - listener = next; - return () => {}; - }), - holdConnectTimeout: vi.fn(), - fail: vi.fn(), - disconnect: vi.fn(), - send: vi.fn(), - connectPayload: vi.fn(() => ({ hostId: 7, cols: 80, rows: 24 })), - }; - render(); - const emit = (message: { type: string; [key: string]: unknown }) => - act(() => listener(message)); - return { props, emit }; - } - - it("starts the sign-in once, then reports a second request as a failure", () => { - const { props, emit } = setup(); - emit({ type: "opkssh_auth_required", hostId: 7 }); - expect(props.send).toHaveBeenCalledWith("opkssh_start_auth", { hostId: 7 }); - expect(props.holdConnectTimeout).toHaveBeenCalledWith(true); - - emit({ type: "opkssh_auth_required", hostId: 7 }); - expect(props.fail).toHaveBeenCalledWith("errors.authFailed"); - }); - - it("shows the chooser, cancels it, and reconnects once signed in", () => { - const { props, emit } = setup(); - emit({ type: "opkssh_auth_required", hostId: 7 }); - emit({ - type: "opkssh_status", - stage: "chooser", - requestId: "req-1", - url: "https://termix.test/plugin-api/opkssh/chooser/req-1", - providers: [{ alias: "google", issuer: "accounts.google.com" }], - }); - expect(screen.getByText("dialog.title")).toBeTruthy(); - - emit({ type: "opkssh_completed", requestId: "req-1" }); - expect(props.send).toHaveBeenCalledWith("opkssh_auth_completed", { - hostId: 7, - cols: 80, - rows: 24, - }); - expect(screen.queryByText("dialog.title")).toBeNull(); - - emit({ - type: "opkssh_status", - stage: "chooser", - requestId: "req-2", - url: "https://termix.test/plugin-api/opkssh/chooser/req-2", - }); - fireEvent.click(screen.getByText("common.cancel")); - expect(props.send).toHaveBeenCalledWith("opkssh_cancel", { - requestId: "req-2", - }); - }); - - it("shows a config error", () => { - const { emit } = setup(); - emit({ - type: "opkssh_config_error", - requestId: "", - error: "OPKSSH configuration not found.", - }); - expect(screen.getByText("OPKSSH configuration not found.")).toBeTruthy(); - }); -}); diff --git a/plugins/opkssh/tsconfig.json b/plugins/opkssh/tsconfig.json deleted file mode 100644 index 7b35314c3..000000000 --- a/plugins/opkssh/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@termix/plugin-sdk/tsconfig.plugin.json", - "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] -} diff --git a/plugins/opkssh/vitest.config.ts b/plugins/opkssh/vitest.config.ts deleted file mode 100644 index 31ba004fc..000000000 --- a/plugins/opkssh/vitest.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; - -export default pluginVitestConfig(import.meta.url); diff --git a/plugins/package.json b/plugins/package.json deleted file mode 100644 index 3dbc1ca59..000000000 --- a/plugins/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/plugins/proxmox/CHANGELOG.md b/plugins/proxmox/CHANGELOG.md deleted file mode 100644 index 95d41f13e..000000000 --- a/plugins/proxmox/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -## 1.0.0 - -First release, bundled with Termix 2.9.0. - -Finds the VMs and containers on a Proxmox VE node through an SSH host and imports them as Termix hosts. Can keep them in sync on a schedule and shows node stats. diff --git a/plugins/proxmox/README.md b/plugins/proxmox/README.md deleted file mode 100644 index 4df68d975..000000000 --- a/plugins/proxmox/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Proxmox - -Find Proxmox VE guests through an SSH host, import them as hosts and keep them in sync. - -## Features - -- Find the VMs and containers on a Proxmox VE node through an SSH host you already have. -- Import them as Termix hosts with the login type you choose. -- Keep imported hosts in sync on a schedule. -- Show node and guest stats in the Proxmox tab. - -## Settings - -### Host - -- Proxmox: mark this host as a Proxmox node and set how guests are imported. -- Default Auth Type: the login type given to imported guests. -- Auto sync guests and Sync interval (minutes): check the node for changes on a schedule. The minimum is 5 minutes. -- Proxmox stats: show the stats tab for this host. - -## Development - -```bash -npm run build # build into dist/ -npm run test # run this plugin's tests -npm run typecheck # type-check this plugin -``` diff --git a/plugins/proxmox/locales/en.json b/plugins/proxmox/locales/en.json deleted file mode 100644 index b4f5d48be..000000000 --- a/plugins/proxmox/locales/en.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox Stats" - }, - "proxmoxStats": { - "tabTitle": "Proxmox Stats", - "cpuUsage": "CPU Usage", - "memoryUsage": "Memory Usage", - "diskUsage": "Disk Usage", - "networkInterfaces": "Network Interfaces", - "uptime": "Uptime", - "systemInfo": "System Information", - "guestsSummary": "Guests", - "storagePools": "Storage Pools", - "clusterHealth": "Cluster Health", - "cpuCores_one": "{{count}} Core", - "cpuCores_other": "{{count}} Cores", - "used": "Used", - "total": "Total", - "hostname": "Hostname", - "kernel": "Kernel", - "pveVersion": "PVE Version", - "name": "Name", - "status": "Status", - "type": "Type", - "cpu": "CPU", - "mem": "Mem", - "disk": "Disk", - "running": "Running", - "stopped": "Stopped", - "guestCounts": "{{running}}/{{total}} running", - "inactive": "Inactive", - "notClustered": "Standalone node (not part of a cluster)", - "quorate": "Quorate", - "notQuorate": "Not quorate", - "nodeOnline": "Online", - "nodeOffline": "Offline", - "noHostSelected": "No host selected", - "pveshNotFound": "pvesh not found - is this a Proxmox node?", - "nodeDetectionFailed": "Unable to determine a valid Proxmox node name", - "connecting": "Connecting...", - "connectionFailed": "Failed to connect to Proxmox node", - "refresh": "Refresh", - "searchGuests": "Search guests...", - "filterAll": "All", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "No VMs or LXC containers found on this node.", - "noGuestsMatch": "No guests match the current filters." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Enable Proxmox Stats", - "enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.", - "proxmoxStatsPollInterval": "Poll interval (seconds)", - "proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.", - "proxmoxStatsNodeOverride": "Node name override", - "proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).", - "proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname", - "authTypeStepca": "Step CA", - "proxmoxIntegration": "Proxmox Integration", - "enableProxmox": "Enable Proxmox", - "enableProxmoxDesc": "Mark this host as a Proxmox node. Enables guest discovery and import directly from this host.", - "proxmoxDefaultAuthType": "Default Auth Type", - "proxmoxDefaultAuthTypeDesc": "Authentication method applied to imported guest hosts. Choose the auth type that matches how you connect to your guests.", - "authTypePassword": "Password", - "authTypeKey": "SSH Key", - "authTypeCredential": "Credential", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "None", - "proxmoxDefaultCredential": "Default Credential", - "proxmoxDefaultCredentialDesc": "Credential used for imported guest hosts when auth type is set to Credential.", - "proxmoxWindowsDetection": "Windows / RDP detection", - "proxmoxWindowsDetectionDesc": "Comma-separated name patterns that trigger RDP instead of SSH (case-insensitive)", - "proxmoxDockerDetection": "Docker detection", - "proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests", - "proxmoxPreferredRanges": "Preferred IP ranges", - "proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces", - "proxmoxAutoSync": "Auto sync guests", - "proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.", - "proxmoxSyncInterval": "Sync interval (minutes)", - "proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.", - "proxmoxMarkMissing": "Mark missing guests", - "proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.", - "proxmoxLastSync": "Last sync", - "proxmoxLastSyncNever": "Not synced yet", - "proxmoxLastSyncNoResult": "No result details", - "proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped", - "proxmoxLastSyncStatus": { - "error": "Failed", - "pending": "Pending", - "success": "Success" - }, - "proxmoxDiscoverAction": "Discover & import Proxmox guests", - "proxmoxImportTitle": "Import from Proxmox", - "proxmoxSelectHost": "Select a Proxmox host…", - "proxmoxDiscover": "Discover", - "proxmoxDiscovering": "Discovering…", - "proxmoxDiscoverGuests": "Discover guests", - "proxmoxSelectAll": "Select all", - "proxmoxDeselectAll": "Deselect all", - "proxmoxNoGuests": "No guests found on this Proxmox node.", - "proxmoxResultImported": "{{count}} imported", - "proxmoxResultUpdated": "{{count}} updated", - "proxmoxResultFailed": "{{count}} failed", - "proxmoxImportComplete": "Proxmox import complete: {{summary}}", - "proxmoxDiscoveryFailed": "Discovery failed", - "proxmoxImportFailed": "Import failed", - "docsLink": "View docs", - "hostNotFound": "Host not found", - "loadingHost": "Loading host...", - "none": "None", - "proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected", - "proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected", - "proxmoxImportButton_one": "Import {{count}} host", - "proxmoxImportButton_other": "Import {{count}} hosts" - }, - "terminal": { - "connected": "Connected" - } -} diff --git a/plugins/proxmox/locales/translated/af_ZA.json b/plugins/proxmox/locales/translated/af_ZA.json deleted file mode 100644 index 07975deba..000000000 --- a/plugins/proxmox/locales/translated/af_ZA.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox-statistieke" - }, - "proxmoxStats": { - "tabTitle": "Proxmox-statistieke", - "cpuUsage": "SVE-gebruik", - "memoryUsage": "Geheuegebruik", - "diskUsage": "Skyfgebruik", - "networkInterfaces": "Netwerkkoppelvlakke", - "uptime": "Optyd", - "systemInfo": "Stelselinligting", - "guestsSummary": "Gaste", - "storagePools": "Bergingspoele", - "clusterHealth": "Klustergesondheid", - "cpuCores_one": "{{count}} Kern", - "cpuCores_other": "{{count}} Kerne", - "used": "Gebruik", - "total": "Totaal", - "hostname": "Gasheernaam", - "kernel": "Kern", - "pveVersion": "PVE-weergawe", - "name": "Naam", - "status": "Status", - "type": "Tipe", - "cpu": "SVE", - "mem": "Mem", - "disk": "Skyf", - "running": "Hardloop", - "stopped": "Gestop", - "guestCounts": "{{running}}/{{total}} hardloop", - "inactive": "Onaktief", - "notClustered": "Losstaande nodus (nie deel van 'n groep nie)", - "quorate": "Kworate", - "notQuorate": "Nie kworum nie", - "nodeOnline": "Aanlyn", - "nodeOffline": "Vanlyn", - "noHostSelected": "Geen gasheer gekies nie", - "pveshNotFound": "pvesh nie gevind nie - is dit 'n Proxmox-node?", - "nodeDetectionFailed": "Kan nie 'n geldige Proxmox-nodusnaam bepaal nie", - "connecting": "Verbind...", - "connectionFailed": "Kon nie aan die Proxmox-nodus koppel nie", - "refresh": "Verfris", - "searchGuests": "Soek gaste...", - "filterAll": "Alles", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "Geen VM'e of LXC-houers is op hierdie node gevind nie.", - "noGuestsMatch": "Geen gaste stem ooreen met die huidige filters nie." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Aktiveer Proxmox-statistieke", - "enableProxmoxStatsDesc": "Wys 'n Proxmox Statistiek-oortjie vir hierdie gasheer met lewendige nodus-, gas-, bergings- en groepstatistieke wat oor SSH versamel is.", - "proxmoxStatsPollInterval": "Stembusinterval (sekondes)", - "proxmoxStatsPollIntervalDesc": "Minimum 15 sekondes. Hoe gereeld om Proxmox-nodusstatistieke te verfris.", - "proxmoxStatsNodeOverride": "Nodenaam-oorskrywing", - "proxmoxStatsNodeOverrideDesc": "Ignoreer outomatiese opsporing van die Proxmox-nodusnaam (standaard opgespoor via gasheernaam).", - "proxmoxStatsNodeOverridePlaceholder": "Outomaties opgespoor vanaf gasheernaam", - "authTypeStepca": "Stap CA", - "proxmoxIntegration": "Proxmox-integrasie", - "enableProxmox": "Aktiveer Proxmox", - "enableProxmoxDesc": "Merk hierdie gasheer as 'n Proxmox-nodus. Aktiveer gasontdekking en invoer direk vanaf hierdie gasheer.", - "proxmoxDefaultAuthType": "Standaardmagtigingtipe", - "proxmoxDefaultAuthTypeDesc": "Verifikasiemetode toegepas op ingevoerde gashere. Kies die verifikasietipe wat ooreenstem met hoe jy met jou gaste skakel.", - "authTypePassword": "Wagwoord", - "authTypeKey": "SSH-sleutel", - "authTypeCredential": "Geloofsbrief", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Geen", - "proxmoxDefaultCredential": "Standaard geloofsbriewe", - "proxmoxDefaultCredentialDesc": "Geloofsbrief wat gebruik word vir ingevoerde gashere wanneer magtigingstipe op Geloofsbrief gestel is.", - "proxmoxWindowsDetection": "Windows / RDP-opsporing", - "proxmoxWindowsDetectionDesc": "Komma-geskeide naampatrone wat RDP in plaas van SSH aktiveer (ongesensitief vir hoofletters)", - "proxmoxDockerDetection": "Docker-opsporing", - "proxmoxDockerDetectionDesc": "Komma-geskeide naampatrone wat Docker in staat stel om gaste te pas", - "proxmoxPreferredRanges": "Voorkeur-IP-reekse", - "proxmoxPreferredRangesDesc": "Komma-geskeide voorvoegsels in prioriteitsvolgorde vir IP-keuse wanneer 'n gas verskeie koppelvlakke het", - "proxmoxAutoSync": "Outomatiese sinkronisering van gaste", - "proxmoxAutoSyncDesc": "Ontdek hierdie Proxmox-nodus gereeld en skep of werk ingevoerde gashere op terwyl jou sessie ontsluit is.", - "proxmoxSyncInterval": "Sinkronisasie-interval (minute)", - "proxmoxSyncIntervalDesc": "Minimum 5 minute. Die skeduleerder slaan geslote gebruikerssessies oor.", - "proxmoxMarkMissing": "Merk ontbrekende gaste", - "proxmoxMarkMissingDesc": "Voeg 'n proxmox-missing-etiket by wanneer 'n voorheen ingevoerde gas verdwyn in plaas daarvan om dit te verwyder.", - "proxmoxLastSync": "Laaste sinkronisasie", - "proxmoxLastSyncNever": "Nog nie gesinkroniseer nie", - "proxmoxLastSyncNoResult": "Geen resultaatbesonderhede nie", - "proxmoxLastSyncSummary": "{{created}} geskep, {{updated}} opgedateer, {{markedMissing}} ontbreek, {{skipped}} oorgeslaan", - "proxmoxLastSyncStatus": { - "error": "Misluk", - "pending": "Hangende", - "success": "Sukses" - }, - "proxmoxDiscoverAction": "Ontdek en invoer Proxmox-gaste", - "proxmoxImportTitle": "Invoer vanaf Proxmox", - "proxmoxSelectHost": "Kies 'n Proxmox-gasheer…", - "proxmoxDiscover": "Ontdek", - "proxmoxDiscovering": "Ontdek…", - "proxmoxDiscoverGuests": "Ontdek gaste", - "proxmoxSelectAll": "Kies alles", - "proxmoxDeselectAll": "Deselekteer alles", - "proxmoxNoGuests": "Geen gaste gevind op hierdie Proxmox-nodus nie.", - "proxmoxResultImported": "{{count}} ingevoer", - "proxmoxResultUpdated": "{{count}} opgedateer", - "proxmoxResultFailed": "{{count}} het misluk", - "proxmoxImportComplete": "Proxmox-invoer voltooi: {{summary}}", - "proxmoxDiscoveryFailed": "Ontdekking het misluk", - "proxmoxImportFailed": "Invoer het misluk", - "docsLink": "Bekyk dokumente", - "hostNotFound": "Gasheer nie gevind nie", - "loadingHost": "Laai gasheer...", - "none": "Geen", - "proxmoxGuestsSelected_one": "{{count}} gas, {{selected}} gekies", - "proxmoxGuestsSelected_other": "{{count}} gaste, {{selected}} gekies", - "proxmoxImportButton_one": "Voer {{count}} gasheer in", - "proxmoxImportButton_other": "Voer {{count}} gashere in" - }, - "terminal": { - "connected": "Verbonde" - } -} diff --git a/plugins/proxmox/locales/translated/ar_SA.json b/plugins/proxmox/locales/translated/ar_SA.json deleted file mode 100644 index 4f1dfb878..000000000 --- a/plugins/proxmox/locales/translated/ar_SA.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "إحصائيات Proxmox" - }, - "proxmoxStats": { - "tabTitle": "إحصائيات Proxmox", - "cpuUsage": "استخدام CPU", - "memoryUsage": "استخدام الذاكرة", - "diskUsage": "استخدام القرص", - "networkInterfaces": "واجهات الشبكة", - "uptime": "مدة التشغيل", - "systemInfo": "معلومات النظام", - "guestsSummary": "الضيوف", - "storagePools": "أحواض التخزين", - "clusterHealth": "الصحة العنقودية", - "cpuCores_one": "{{count}} نواة", - "cpuCores_other": "{{count}} نوى", - "used": "مستخدم", - "total": "المجموع", - "hostname": "اسم المضيف", - "kernel": "النواة", - "pveVersion": "إصدار لاعب ضد البيئة", - "name": "الاسم", - "status": "الحالة", - "type": "النوع", - "cpu": "المعالج", - "mem": "ميم", - "disk": "القرص", - "running": "قيد التشغيل", - "stopped": "متوقف", - "guestCounts": "{{running}}/{{total}} قيد التشغيل", - "inactive": "غير نشط", - "notClustered": "عقدة مستقلة (ليست جزءًا من مجموعة)", - "quorate": "اقتبس", - "notQuorate": "غير مكتمل النصاب", - "nodeOnline": "متصل", - "nodeOffline": "غير متصل", - "noHostSelected": "لم يتم تحديد أي جهاز مضيف", - "pveshNotFound": "لم يتم العثور على pvesh - هل هذه عقدة Proxmox؟", - "nodeDetectionFailed": "تعذر تحديد اسم عقدة Proxmox صالح", - "connecting": "جارٍ الاتصال...", - "connectionFailed": "فشل الاتصال بعقدة Proxmox", - "refresh": "تحديث", - "searchGuests": "ابحث عن الضيوف...", - "filterAll": "الكل", - "filterVm": "آلة افتراضية", - "filterLxc": "LXC", - "noGuests": "لم يتم العثور على أي أجهزة افتراضية أو حاويات LXC على هذه العقدة.", - "noGuestsMatch": "لا يوجد ضيوف يطابقون المعايير الحالية." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "تفعيل إحصائيات Proxmox", - "enableProxmoxStatsDesc": "اعرض علامة تبويب إحصائيات Proxmox لهذا المضيف مع إحصائيات العقدة والضيف والتخزين والمجموعة التي تم جمعها عبر SSH.", - "proxmoxStatsPollInterval": "فترة الاستطلاع (بالثواني)", - "proxmoxStatsPollIntervalDesc": "الحد الأدنى 15 ثانية. عدد مرات تحديث إحصائيات عقدة Proxmox.", - "proxmoxStatsNodeOverride": "تجاوز اسم العقدة", - "proxmoxStatsNodeOverrideDesc": "تجاوز الكشف التلقائي عن اسم عقدة Proxmox (يتم الكشف عنه عبر اسم المضيف افتراضيًا).", - "proxmoxStatsNodeOverridePlaceholder": "تم اكتشافه تلقائيًا من اسم المضيف", - "authTypeStepca": "الخطوة ج", - "proxmoxIntegration": "تكامل Proxmox", - "enableProxmox": "تمكين Proxmox", - "enableProxmoxDesc": "تعليم هذا المضيف كعقدة Proxmox. يُمكّن اكتشاف الضيوف واستيرادهم مباشرة من هذا المضيف.", - "proxmoxDefaultAuthType": "نوع المصادقة الافتراضي", - "proxmoxDefaultAuthTypeDesc": "طريقة المصادقة المطبقة على المضيفين الضيوف المستوردين. اختر نوع المصادقة الذي يتوافق مع طريقة اتصالك بالضيوف.", - "authTypePassword": "كلمة مرور", - "authTypeKey": "مفتاح SSH", - "authTypeCredential": "بيانات اعتماد", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "بلا", - "proxmoxDefaultCredential": "بيانات الاعتماد الافتراضية", - "proxmoxDefaultCredentialDesc": "بيانات الاعتماد المُستخدمة للأجهزة الضيفة المستوردة عند ضبط نوع المصادقة على 'بيانات الاعتماد'.", - "proxmoxWindowsDetection": "اكتشاف Windows / RDP", - "proxmoxWindowsDetectionDesc": "أنماط الأسماء مفصولة بفواصل تُشغّل RDP بدلاً من SSH (غير حسّاسة لحالة الأحرف)", - "proxmoxDockerDetection": "اكتشاف Docker", - "proxmoxDockerDetectionDesc": "أنماط الأسماء مفصولة بفواصل تُمكّن Docker للأجهزة الضيفة المطابقة", - "proxmoxPreferredRanges": "نطاقات IP المُفضّلة", - "proxmoxPreferredRangesDesc": "بادئات مفصولة بفواصل بترتيب الأولوية لاختيار IP عندما يكون للجهاز الضيف واجهات متعددة", - "proxmoxAutoSync": "مزامنة تلقائية للأجهزة الضيفة", - "proxmoxAutoSyncDesc": "اكتشاف عقدة Proxmox هذه بشكل دوري وإنشاء أو تحديث الأجهزة الضيفة المستوردة أثناء فتح جلستك.", - "proxmoxSyncInterval": "فترة المزامنة (بالدقائق)", - "proxmoxSyncIntervalDesc": "الحد الأدنى 5 دقائق. يتخطّى المجدول جلسات المستخدم المقفلة.", - "proxmoxMarkMissing": "تعليم الأجهزة الضيفة المفقودة", - "proxmoxMarkMissingDesc": "أضف وسم proxmox-missing عندما يختفي جهاز ضيف تم استيراده مسبقاً بدلاً من حذفه.", - "proxmoxLastSync": "آخر مزامنة", - "proxmoxLastSyncNever": "لم تتم المزامنة بعد", - "proxmoxLastSyncNoResult": "لا توجد تفاصيل للنتائج", - "proxmoxLastSyncSummary": "{{created}} مُنشأ، {{updated}} مُحدَّث، {{markedMissing}} مفقود، {{skipped}} مُتخطّى", - "proxmoxLastSyncStatus": { - "error": "فشل", - "pending": "قيد الانتظار", - "success": "نجاح" - }, - "proxmoxDiscoverAction": "اكتشاف واستيراد أجهزة Proxmox الضيفة", - "proxmoxImportTitle": "استيراد من Proxmox", - "proxmoxSelectHost": "اختر مضيف Proxmox…", - "proxmoxDiscover": "اكتشف", - "proxmoxDiscovering": "جارٍ الاكتشاف…", - "proxmoxDiscoverGuests": "اكتشف الأجهزة الضيفة", - "proxmoxSelectAll": "تحديد الكل", - "proxmoxDeselectAll": "إلغاء تحديد الكل", - "proxmoxNoGuests": "لم يتم العثور على أجهزة ضيفة على عقدة Proxmox هذه.", - "proxmoxResultImported": "{{count}} تم استيرادها", - "proxmoxResultUpdated": "{{count}} تم تحديثها", - "proxmoxResultFailed": "{{count}} فشلت", - "proxmoxImportComplete": "اكتمل استيراد Proxmox: {{summary}}", - "proxmoxDiscoveryFailed": "فشل الاكتشاف", - "proxmoxImportFailed": "فشل الاستيراد", - "docsLink": "عرض المستندات", - "hostNotFound": "المضيف غير موجود", - "loadingHost": "جارٍ تحميل المضيف...", - "none": "لا يوجد", - "proxmoxGuestsSelected_one": "{{count}} ضيف، {{selected}} مُختار", - "proxmoxGuestsSelected_other": "{{count}} ضيوف، {{selected}} مختارون", - "proxmoxImportButton_one": "استيراد {{count}} مضيف", - "proxmoxImportButton_other": "استيراد {{count}} مضيفين" - }, - "terminal": { - "connected": "متصل" - } -} diff --git a/plugins/proxmox/locales/translated/bg_BG.json b/plugins/proxmox/locales/translated/bg_BG.json deleted file mode 100644 index 103e76249..000000000 --- a/plugins/proxmox/locales/translated/bg_BG.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Статистика на Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Статистика на Proxmox", - "cpuUsage": "Използване на процесора", - "memoryUsage": "Използване на паметта", - "diskUsage": "Използване на диска", - "networkInterfaces": "Мрежови интерфейси", - "uptime": "Време на работа", - "systemInfo": "Системна информация", - "guestsSummary": "Гости", - "storagePools": "Пулове за съхранение", - "clusterHealth": "Здраве на клъстера", - "cpuCores_one": "{{count}} Ядро", - "cpuCores_other": "{{count}} Ядра", - "used": "Използван", - "total": "Общо", - "hostname": "Име на хост", - "kernel": "Ядро", - "pveVersion": "PVE версия", - "name": "Име", - "status": "Статус", - "type": "Тип", - "cpu": "Процесор", - "mem": "Мем", - "disk": "Диск", - "running": "Бягане", - "stopped": "Спряно", - "guestCounts": "{{running}}/{{total}} бягане", - "inactive": "Неактивен", - "notClustered": "Самостоятелен възел (не е част от клъстер)", - "quorate": "Кворат", - "notQuorate": "Няма кворум", - "nodeOnline": "Онлайн", - "nodeOffline": "Офлайн", - "noHostSelected": "Няма избран хост", - "pveshNotFound": "pvesh не е намерен - това Proxmox възел ли е?", - "nodeDetectionFailed": "Не може да се определи валидно име на възел Proxmox", - "connecting": "Свързване...", - "connectionFailed": "Неуспешно свързване с възел Proxmox", - "refresh": "Обновяване", - "searchGuests": "Търсене на гости...", - "filterAll": "Всички", - "filterVm": "ВМ", - "filterLxc": "LXC", - "noGuests": "На този възел не са намерени виртуални машини или LXC контейнери.", - "noGuestsMatch": "Няма гости, които да отговарят на текущите филтри." - }, - "hosts": { - "tabProxmox": "Проксмокс", - "enableProxmoxStats": "Активиране на Proxmox Stats", - "enableProxmoxStatsDesc": "Покажете раздела „Статистика на Proxmox“ за този хост с актуални статистически данни за възлите, госта, хранилището и клъстера, събрани през SSH.", - "proxmoxStatsPollInterval": "Интервал на анкетата (секунди)", - "proxmoxStatsPollIntervalDesc": "Минимум 15 секунди. Колко често да се обновяват статистическите данни за възлите на Proxmox.", - "proxmoxStatsNodeOverride": "Замяна на името на възела", - "proxmoxStatsNodeOverrideDesc": "Игнориране на автоматичното откриване на името на възела Proxmox (по подразбиране се открива чрез името на хоста).", - "proxmoxStatsNodeOverridePlaceholder": "Автоматично разпознато от името на хоста", - "authTypeStepca": "Стъпка CA", - "proxmoxIntegration": "Интеграция с Proxmox", - "enableProxmox": "Активиране на Proxmox", - "enableProxmoxDesc": "Маркирайте този хост като Proxmox възел. Активирайте откриването на гост и импортирането директно от този хост.", - "proxmoxDefaultAuthType": "Тип удостоверяване по подразбиране", - "proxmoxDefaultAuthTypeDesc": "Методът за удостоверяване е приложен към импортираните гост-хостове. Изберете типа удостоверяване, който съответства на начина, по който се свързвате с гостите си.", - "authTypePassword": "Парола", - "authTypeKey": "SSH ключ", - "authTypeCredential": "Пълномощия", - "authTypeOpkssh": "ОПКСШ", - "authTypeNone": "Няма", - "proxmoxDefaultCredential": "Подразбиращи се идентификационни данни", - "proxmoxDefaultCredentialDesc": "Идентификационни данни, използвани за импортирани гост хостове, когато типът удостоверяване е зададен на Идентификационни данни.", - "proxmoxWindowsDetection": "Откриване на Windows / RDP", - "proxmoxWindowsDetectionDesc": "Модели на имена, разделени със запетаи, които задействат RDP вместо SSH (без значение от главни и малки букви)", - "proxmoxDockerDetection": "Откриване на Докер", - "proxmoxDockerDetectionDesc": "Модели за имена, разделени със запетаи, които позволяват на Docker да съпоставя гости", - "proxmoxPreferredRanges": "Предпочитани IP диапазони", - "proxmoxPreferredRangesDesc": "Префикси, разделени със запетаи, подредени по приоритет за избор на IP адрес, когато гост има множество интерфейси", - "proxmoxAutoSync": "Автоматично синхронизиране на гости", - "proxmoxAutoSyncDesc": "Периодично откривайте този Proxmox възел и създавайте или актуализирайте импортирани гост хостове, докато сесията ви е отключена.", - "proxmoxSyncInterval": "Интервал на синхронизиране (минути)", - "proxmoxSyncIntervalDesc": "Минимум 5 минути. Планировчикът пропуска заключените потребителски сесии.", - "proxmoxMarkMissing": "Маркиране на липсващи гости", - "proxmoxMarkMissingDesc": "Добавете етикет proxmox-missing, когато предварително импортиран гост изчезне, вместо да го изтриете.", - "proxmoxLastSync": "Последна синхронизация", - "proxmoxLastSyncNever": "Все още не е синхронизирано", - "proxmoxLastSyncNoResult": "Няма подробности за резултата", - "proxmoxLastSyncSummary": "{{created}} създадено, {{updated}} актуализирано, {{markedMissing}} липсващо, {{skipped}} пропуснато", - "proxmoxLastSyncStatus": { - "error": "Неуспешно", - "pending": "В очакване", - "success": "Успех" - }, - "proxmoxDiscoverAction": "Откриване и импортиране на гости на Proxmox", - "proxmoxImportTitle": "Импортиране от Proxmox", - "proxmoxSelectHost": "Изберете хост на Proxmox…", - "proxmoxDiscover": "Открийте", - "proxmoxDiscovering": "Откриване…", - "proxmoxDiscoverGuests": "Открийте гостите", - "proxmoxSelectAll": "Избери всички", - "proxmoxDeselectAll": "Премахване на избора от всички", - "proxmoxNoGuests": "Няма намерени гости на този Proxmox възел.", - "proxmoxResultImported": "{{count}} импортирано", - "proxmoxResultUpdated": "{{count}} актуализирано", - "proxmoxResultFailed": "{{count}} неуспешно", - "proxmoxImportComplete": "Импортирането на Proxmox е завършено: {{summary}}", - "proxmoxDiscoveryFailed": "Откриването не беше успешно", - "proxmoxImportFailed": "Импортирането не бе успешно", - "docsLink": "Преглед на документи", - "hostNotFound": "Хостът не е намерен", - "loadingHost": "Зареждане на хоста...", - "none": "Няма", - "proxmoxGuestsSelected_one": "{{count}} гост, {{selected}} избран", - "proxmoxGuestsSelected_other": "{{count}} гости, {{selected}} избрани", - "proxmoxImportButton_one": "Импортиране на хост {{count}}", - "proxmoxImportButton_other": "Импортиране на {{count}} хостове" - }, - "terminal": { - "connected": "Свързан" - } -} diff --git a/plugins/proxmox/locales/translated/bn_BD.json b/plugins/proxmox/locales/translated/bn_BD.json deleted file mode 100644 index 9863a1c62..000000000 --- a/plugins/proxmox/locales/translated/bn_BD.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "প্রক্সমক্স পরিসংখ্যান" - }, - "proxmoxStats": { - "tabTitle": "প্রক্সমক্স পরিসংখ্যান", - "cpuUsage": "সিপিইউ ব্যবহার", - "memoryUsage": "মেমরি ব্যবহার", - "diskUsage": "ডিস্ক ব্যবহার", - "networkInterfaces": "নেটওয়ার্ক ইন্টারফেস", - "uptime": "আপটাইম", - "systemInfo": "সিস্টেমের তথ্য", - "guestsSummary": "অতিথিরা", - "storagePools": "স্টোরেজ পুল", - "clusterHealth": "ক্লাস্টার স্বাস্থ্য", - "cpuCores_one": "{{count}} কোর", - "cpuCores_other": "{{count}} কোর", - "used": "ব্যবহৃত", - "total": "মোট", - "hostname": "হোস্টনাম", - "kernel": "কার্নেল", - "pveVersion": "PVE সংস্করণ", - "name": "নাম", - "status": "অবস্থা", - "type": "প্রকার", - "cpu": "সিপিইউ", - "mem": "মেম", - "disk": "ডিস্ক", - "running": "দৌড়ানো", - "stopped": "থামানো হয়েছে", - "guestCounts": "{{running}}/{{total}} চলমান", - "inactive": "নিষ্ক্রিয়", - "notClustered": "স্বতন্ত্র নোড (ক্লাস্টারের অংশ নয়)", - "quorate": "কোরাট", - "notQuorate": "কোরাম পূর্ণ নয়", - "nodeOnline": "অনলাইন", - "nodeOffline": "অফলাইন", - "noHostSelected": "কোন হোস্ট নির্বাচন করা হয়নি", - "pveshNotFound": "pvesh খুঁজে পাওয়া যায়নি - এটি কি একটি Proxmox নোড?", - "nodeDetectionFailed": "একটি বৈধ Proxmox নোডের নাম নির্ধারণ করা সম্ভব হয়নি।", - "connecting": "সংযোগ স্থাপন...", - "connectionFailed": "Proxmox নোডের সাথে সংযোগ করতে ব্যর্থ হয়েছে", - "refresh": "রিফ্রেশ", - "searchGuests": "অতিথিদের অনুসন্ধান করুন...", - "filterAll": "সব", - "filterVm": "ভিএম", - "filterLxc": "এলএক্সসি", - "noGuests": "এই নোডে কোনো ভিএম বা এলএক্সসি কন্টেইনার খুঁজে পাওয়া যায়নি।", - "noGuestsMatch": "বর্তমান ফিল্টারগুলোর সাথে কোনো অতিথি মেলে না।" - }, - "hosts": { - "tabProxmox": "প্রক্সমক্স", - "enableProxmoxStats": "Proxmox Stats সক্রিয় করুন", - "enableProxmoxStatsDesc": "এই হোস্টের জন্য একটি Proxmox Stats ট্যাব দেখান, যেখানে SSH-এর মাধ্যমে সংগৃহীত লাইভ নোড, গেস্ট, স্টোরেজ এবং ক্লাস্টারের পরিসংখ্যান থাকবে।", - "proxmoxStatsPollInterval": "ভোটের ব্যবধান (সেকেন্ড)", - "proxmoxStatsPollIntervalDesc": "সর্বনিম্ন ১৫ সেকেন্ড। কত ঘন ঘন প্রক্সমক্স নোড স্ট্যাটস রিফ্রেশ করতে হবে।", - "proxmoxStatsNodeOverride": "নোডের নাম ওভাররাইড", - "proxmoxStatsNodeOverrideDesc": "প্রক্সমক্স নোডের নামের স্বয়ংক্রিয় সনাক্তকরণকে অগ্রাহ্য করুন (যা ডিফল্টরূপে হোস্টনেমের মাধ্যমে সনাক্ত করা হয়)।", - "proxmoxStatsNodeOverridePlaceholder": "হোস্টনেম থেকে স্বয়ংক্রিয়ভাবে শনাক্ত করা হয়েছে", - "authTypeStepca": "স্টেপ সিএ", - "proxmoxIntegration": "প্রক্সমক্স ইন্টিগ্রেশন", - "enableProxmox": "প্রক্সমক্স সক্রিয় করুন", - "enableProxmoxDesc": "এই হোস্টটিকে একটি প্রক্সমক্স নোড হিসেবে চিহ্নিত করুন। এর ফলে সরাসরি এই হোস্ট থেকে গেস্ট ডিসকভারি এবং ইম্পোর্ট করা যায়।", - "proxmoxDefaultAuthType": "ডিফল্ট প্রমাণীকরণ ধরণ", - "proxmoxDefaultAuthTypeDesc": "আমদানিকৃত গেস্ট হোস্টদের জন্য প্রযোজ্য প্রমাণীকরণ পদ্ধতি। আপনি আপনার গেস্টদের সাথে যেভাবে সংযোগ স্থাপন করেন, তার সাথে মেলে এমন প্রমাণীকরণের ধরণটি বেছে নিন।", - "authTypePassword": "পাসওয়ার্ড", - "authTypeKey": "SSH কী", - "authTypeCredential": "শংসাপত্র", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "কোনোটিই না", - "proxmoxDefaultCredential": "ডিফল্ট পরিচয়পত্র", - "proxmoxDefaultCredentialDesc": "যখন অথোরাইজেশন টাইপ 'ক্রেডেনশিয়াল' হিসেবে সেট করা থাকে, তখন ইম্পোর্ট করা গেস্ট হোস্টগুলির জন্য ব্যবহৃত ক্রেডেনশিয়াল।", - "proxmoxWindowsDetection": "উইন্ডোজ / আরডিপি সনাক্তকরণ", - "proxmoxWindowsDetectionDesc": "কমা দ্বারা পৃথক করা নামের প্যাটার্ন যা SSH-এর পরিবর্তে RDP চালু করে (কেস-ইনসেনসিটিভ)", - "proxmoxDockerDetection": "ডকার সনাক্তকরণ", - "proxmoxDockerDetectionDesc": "কমা দ্বারা পৃথক করা নামের প্যাটার্ন যা ডকারকে গেস্টদের মেলানোর জন্য সক্ষম করে", - "proxmoxPreferredRanges": "পছন্দের আইপি রেঞ্জ", - "proxmoxPreferredRangesDesc": "যখন কোনো গেস্টের একাধিক ইন্টারফেস থাকে, তখন আইপি নির্বাচনের জন্য অগ্রাধিকার ক্রমে কমা দ্বারা পৃথক করা প্রিফিক্সগুলো ব্যবহৃত হয়।", - "proxmoxAutoSync": "অতিথিদের স্বয়ংক্রিয়ভাবে সিঙ্ক করুন", - "proxmoxAutoSyncDesc": "আপনার সেশন আনলক থাকা অবস্থায় পর্যায়ক্রমে এই প্রক্সমক্স নোডটি আবিষ্কার করুন এবং ইম্পোর্ট করা গেস্ট হোস্টগুলি তৈরি বা আপডেট করুন।", - "proxmoxSyncInterval": "সিঙ্ক ব্যবধান (মিনিট)", - "proxmoxSyncIntervalDesc": "সর্বনিম্ন ৫ মিনিট। শিডিউলারটি লক করা ব্যবহারকারীর সেশনগুলো এড়িয়ে যায়।", - "proxmoxMarkMissing": "অনুপস্থিত অতিথিদের চিহ্নিত করুন", - "proxmoxMarkMissingDesc": "পূর্বে ইম্পোর্ট করা কোনো গেস্ট অদৃশ্য হয়ে গেলে, সেটিকে ডিলিট না করে একটি proxmox-missing ট্যাগ যোগ করুন।", - "proxmoxLastSync": "শেষ সিঙ্ক", - "proxmoxLastSyncNever": "এখনো সিঙ্ক হয়নি", - "proxmoxLastSyncNoResult": "ফলাফলের কোনো বিবরণ নেই", - "proxmoxLastSyncSummary": "{{created}} তৈরি করা হয়েছে, {{updated}} আপডেট করা হয়েছে, {{markedMissing}} অনুপস্থিত, {{skipped}} বাদ দেওয়া হয়েছে", - "proxmoxLastSyncStatus": { - "error": "ব্যর্থ", - "pending": "বিচারাধীন", - "success": "সাফল্য" - }, - "proxmoxDiscoverAction": "Proxmox গেস্টদের আবিষ্কার ও আমদানি করুন", - "proxmoxImportTitle": "প্রক্সমক্স থেকে আমদানি করুন", - "proxmoxSelectHost": "একটি প্রক্সমক্স হোস্ট নির্বাচন করুন…", - "proxmoxDiscover": "আবিষ্কার করুন", - "proxmoxDiscovering": "… আবিষ্কার করা", - "proxmoxDiscoverGuests": "অতিথিদের আবিষ্কার করুন", - "proxmoxSelectAll": "সবগুলো নির্বাচন করুন", - "proxmoxDeselectAll": "সবগুলো নির্বাচন বাতিল করুন", - "proxmoxNoGuests": "এই প্রক্সমক্স নোডে কোনো অতিথি পাওয়া যায়নি।", - "proxmoxResultImported": "{{count}} আমদানি করা হয়েছে", - "proxmoxResultUpdated": "{{count}} আপডেট করা হয়েছে", - "proxmoxResultFailed": "{{count}} ব্যর্থ হয়েছে", - "proxmoxImportComplete": "প্রক্সমক্স আমদানি সম্পন্ন: {{summary}}", - "proxmoxDiscoveryFailed": "আবিষ্কার ব্যর্থ হয়েছে", - "proxmoxImportFailed": "আমদানি ব্যর্থ হয়েছে", - "docsLink": "নথি দেখুন", - "hostNotFound": "হোস্ট খুঁজে পাওয়া যায়নি", - "loadingHost": "হোস্ট লোড হচ্ছে...", - "none": "কোনোটিই না", - "proxmoxGuestsSelected_one": "{{count}} অতিথি, {{selected}} নির্বাচিত", - "proxmoxGuestsSelected_other": "{{count}} জন অতিথি, {{selected}} জন নির্বাচিত", - "proxmoxImportButton_one": "{{count}} হোস্ট আমদানি করুন", - "proxmoxImportButton_other": "{{count}} হোস্ট আমদানি করুন" - }, - "terminal": { - "connected": "সংযুক্ত" - } -} diff --git a/plugins/proxmox/locales/translated/ca_ES.json b/plugins/proxmox/locales/translated/ca_ES.json deleted file mode 100644 index 95314e63c..000000000 --- a/plugins/proxmox/locales/translated/ca_ES.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Estadístiques de Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Estadístiques de Proxmox", - "cpuUsage": "Ús de la CPU", - "memoryUsage": "Ús de memòria", - "diskUsage": "Ús del disc", - "networkInterfaces": "Interfícies de xarxa", - "uptime": "Temps de funcionament", - "systemInfo": "Informació del sistema", - "guestsSummary": "Convidats", - "storagePools": "Piscines d'emmagatzematge", - "clusterHealth": "Salut del clúster", - "cpuCores_one": "{{count}} Nucli", - "cpuCores_other": "{{count}} Nuclis", - "used": "Usat", - "total": "Total", - "hostname": "Nom d'amfitrió", - "kernel": "Nucli", - "pveVersion": "Versió PVE", - "name": "Nom", - "status": "Estat", - "type": "Tipus", - "cpu": "CPU", - "mem": "Memòria", - "disk": "Disc", - "running": "Córrer", - "stopped": "Aturat", - "guestCounts": "{{running}}/{{total}} en execució", - "inactive": "Inactiu", - "notClustered": "Node autònom (no forma part d'un clúster)", - "quorate": "Quota", - "notQuorate": "Sense quòrum", - "nodeOnline": "En línia", - "nodeOffline": "Fora de línia", - "noHostSelected": "No s'ha seleccionat cap amfitrió", - "pveshNotFound": "No s'ha trobat pvesh: és un node de Proxmox?", - "nodeDetectionFailed": "No s'ha pogut determinar un nom de node Proxmox vàlid", - "connecting": "Connectant...", - "connectionFailed": "No s'ha pogut connectar al node de Proxmox.", - "refresh": "Actualitza", - "searchGuests": "Cerca convidats...", - "filterAll": "Tot", - "filterVm": "màquina virtual", - "filterLxc": "LXC", - "noGuests": "No s'han trobat màquines virtuals ni contenidors LXC en aquest node.", - "noGuestsMatch": "No hi ha convidats que coincideixin amb els filtres actuals." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Activa les estadístiques de Proxmox", - "enableProxmoxStatsDesc": "Mostra una pestanya Estadístiques de Proxmox per a aquest amfitrió amb estadístiques en directe de node, convidat, emmagatzematge i clúster recollides a través de SSH.", - "proxmoxStatsPollInterval": "Interval de sondeig (segons)", - "proxmoxStatsPollIntervalDesc": "Mínim 15 segons. Amb quina freqüència s'han d'actualitzar les estadístiques del node Proxmox.", - "proxmoxStatsNodeOverride": "Substitució del nom del node", - "proxmoxStatsNodeOverrideDesc": "Anul·la la detecció automàtica del nom del node Proxmox (detectat per defecte mitjançant el nom de l'amfitrió).", - "proxmoxStatsNodeOverridePlaceholder": "Detectat automàticament des del nom d'amfitrió", - "authTypeStepca": "Pas CA", - "proxmoxIntegration": "Integració de Proxmox", - "enableProxmox": "Activa Proxmox", - "enableProxmoxDesc": "Marca aquest amfitrió com a node de Proxmox. Activa la detecció de convidats i la importació directament des d'aquest amfitrió.", - "proxmoxDefaultAuthType": "Tipus d'autorització per defecte", - "proxmoxDefaultAuthTypeDesc": "Mètode d'autenticació aplicat als amfitrions convidats importats. Trieu el tipus d'autenticació que coincideixi amb la manera com us connecteu als vostres convidats.", - "authTypePassword": "Contrasenya", - "authTypeKey": "Clau SSH", - "authTypeCredential": "Credencial", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Cap", - "proxmoxDefaultCredential": "Credencial predeterminada", - "proxmoxDefaultCredentialDesc": "Credencial utilitzada per als amfitrions convidats importats quan el tipus d'autenticació està definit com a Credencial.", - "proxmoxWindowsDetection": "Detecció de Windows / RDP", - "proxmoxWindowsDetectionDesc": "Patrons de noms separats per comes que activen RDP en lloc de SSH (sense distinció entre majúscules i minúscules)", - "proxmoxDockerDetection": "Detecció de Docker", - "proxmoxDockerDetectionDesc": "Patrons de noms separats per comes que permeten a Docker utilitzar convidats coincidents", - "proxmoxPreferredRanges": "Intervals d'IP preferits", - "proxmoxPreferredRangesDesc": "Prefixos separats per comes en ordre de prioritat per a la selecció d'IP quan un convidat té diverses interfícies", - "proxmoxAutoSync": "Sincronitza automàticament els convidats", - "proxmoxAutoSyncDesc": "Descobreix periòdicament aquest node de Proxmox i crea o actualitza els hosts convidats importats mentre la sessió està desbloquejada.", - "proxmoxSyncInterval": "Interval de sincronització (minuts)", - "proxmoxSyncIntervalDesc": "Mínim 5 minuts. El planificador omet les sessions d'usuari bloquejades.", - "proxmoxMarkMissing": "Marca els convidats que falten", - "proxmoxMarkMissingDesc": "Afegeix una etiqueta proxmox-missing quan un convidat importat prèviament desapareix en comptes d'eliminar-lo.", - "proxmoxLastSync": "Darrera sincronització", - "proxmoxLastSyncNever": "Encara no sincronitzat", - "proxmoxLastSyncNoResult": "Sense detalls del resultat", - "proxmoxLastSyncSummary": "{{created}} creat, {{updated}} actualitzat, {{markedMissing}} no hi és, {{skipped}} omès", - "proxmoxLastSyncStatus": { - "error": "Fallat", - "pending": "Pendent", - "success": "Èxit" - }, - "proxmoxDiscoverAction": "Descobreix i importa convidats de Proxmox", - "proxmoxImportTitle": "Importar des de Proxmox", - "proxmoxSelectHost": "Seleccioneu un amfitrió Proxmox…", - "proxmoxDiscover": "Descobreix", - "proxmoxDiscovering": "Descobrint…", - "proxmoxDiscoverGuests": "Descobreix convidats", - "proxmoxSelectAll": "Selecciona-ho tot", - "proxmoxDeselectAll": "Desselecciona-ho tot", - "proxmoxNoGuests": "No s'han trobat convidats en aquest node de Proxmox.", - "proxmoxResultImported": "{{count}} importat", - "proxmoxResultUpdated": "{{count}} actualitzat", - "proxmoxResultFailed": "{{count}} ha fallat", - "proxmoxImportComplete": "Importació de Proxmox completada: {{summary}}", - "proxmoxDiscoveryFailed": "El descobriment ha fallat", - "proxmoxImportFailed": "La importació ha fallat", - "docsLink": "Veure documents", - "hostNotFound": "No s'ha trobat l'amfitrió", - "loadingHost": "S'està carregant l'amfitrió...", - "none": "Cap", - "proxmoxGuestsSelected_one": "{{count}} convidat, {{selected}} seleccionat", - "proxmoxGuestsSelected_other": "{{count}} convidats, {{selected}} seleccionats", - "proxmoxImportButton_one": "Importa l'amfitrió {{count}}", - "proxmoxImportButton_other": "Importa els amfitrions {{count}}" - }, - "terminal": { - "connected": "Connectat" - } -} diff --git a/plugins/proxmox/locales/translated/cs_CZ.json b/plugins/proxmox/locales/translated/cs_CZ.json deleted file mode 100644 index 8899cd93e..000000000 --- a/plugins/proxmox/locales/translated/cs_CZ.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Statistiky Proxmoxu" - }, - "proxmoxStats": { - "tabTitle": "Statistiky Proxmoxu", - "cpuUsage": "Využití CPU", - "memoryUsage": "Využití paměti", - "diskUsage": "Využití disku", - "networkInterfaces": "Síťová rozhraní", - "uptime": "Doba provozuschopnosti", - "systemInfo": "Informace o systému", - "guestsSummary": "Hosté", - "storagePools": "Úložné fondy", - "clusterHealth": "Zdraví klastru", - "cpuCores_one": "{{count}} Jádro", - "cpuCores_other": "{{count}} Jádra", - "used": "Použitý", - "total": "Celkový", - "hostname": "Název hostitele", - "kernel": "Jádro", - "pveVersion": "PVE verze", - "name": "Jméno", - "status": "Postavení", - "type": "Typ", - "cpu": "Procesor", - "mem": "Paměť", - "disk": "Disk", - "running": "Běh", - "stopped": "Zastaveno", - "guestCounts": "{{running}}/{{total}} běží", - "inactive": "Neaktivní", - "notClustered": "Samostatný uzel (není součástí clusteru)", - "quorate": "Kvorát", - "notQuorate": "Není usnášeníschopný", - "nodeOnline": "Online", - "nodeOffline": "Offline", - "noHostSelected": "Nebyl vybrán žádný hostitel", - "pveshNotFound": "pvesh nenalezen - je to uzel Proxmox?", - "nodeDetectionFailed": "Nelze určit platný název uzlu Proxmox.", - "connecting": "Spojovací...", - "connectionFailed": "Nepodařilo se připojit k uzlu Proxmox", - "refresh": "Obnovit", - "searchGuests": "Hledat hosty...", - "filterAll": "Vše", - "filterVm": "virtuální počítač", - "filterLxc": "LXC", - "noGuests": "Na tomto uzlu nebyly nalezeny žádné virtuální počítače ani kontejnery LXC.", - "noGuestsMatch": "Aktuálním filtrům neodpovídají žádní hosté." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Povolit statistiky Proxmoxu", - "enableProxmoxStatsDesc": "Zobrazit kartu Statistiky Proxmoxu pro tohoto hostitele s aktivními statistikami uzlů, hostů, úložišť a clusterů shromážděnými přes SSH.", - "proxmoxStatsPollInterval": "Interval dotazování (sekundy)", - "proxmoxStatsPollIntervalDesc": "Minimálně 15 sekund. Jak často aktualizovat statistiky uzlu Proxmox.", - "proxmoxStatsNodeOverride": "Přepsání názvu uzlu", - "proxmoxStatsNodeOverrideDesc": "Přepsat automatickou detekci názvu uzlu Proxmox (ve výchozím nastavení detekováno pomocí názvu hostitele).", - "proxmoxStatsNodeOverridePlaceholder": "Automaticky detekováno z názvu hostitele", - "authTypeStepca": "Krok CA", - "proxmoxIntegration": "Integrace Proxmoxu", - "enableProxmox": "Povolit Proxmox", - "enableProxmoxDesc": "Označit tento hostitel jako uzel Proxmox. Umožňuje vyhledávání hostů a import přímo z tohoto hostitele.", - "proxmoxDefaultAuthType": "Výchozí typ ověření", - "proxmoxDefaultAuthTypeDesc": "Metoda ověřování použitá na importované hostující hostitele. Vyberte typ ověřování, který odpovídá způsobu, jakým se připojujete k hostům.", - "authTypePassword": "Heslo", - "authTypeKey": "Klíč SSH", - "authTypeCredential": "Přihlašovací údaj", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Žádný", - "proxmoxDefaultCredential": "Výchozí přihlašovací údaje", - "proxmoxDefaultCredentialDesc": "Přihlašovací údaje používané pro importované hostující hostitele, pokud je typ ověření nastaven na Přihlašovací údaje.", - "proxmoxWindowsDetection": "Detekce Windows / RDP", - "proxmoxWindowsDetectionDesc": "Vzory názvů oddělené čárkami, které spouštějí RDP místo SSH (bez rozlišení velkých a malých písmen)", - "proxmoxDockerDetection": "Detekce Dockeru", - "proxmoxDockerDetectionDesc": "Vzory názvů oddělených čárkami, které Dockeru umožňují vyhledávání hostů", - "proxmoxPreferredRanges": "Preferované rozsahy IP adres", - "proxmoxPreferredRangesDesc": "Prefixy oddělené čárkami v pořadí priority pro výběr IP adresy, když má host více rozhraní", - "proxmoxAutoSync": "Automatická synchronizace hostů", - "proxmoxAutoSyncDesc": "Pravidelně vyhledávejte tento uzel Proxmox a vytvářejte nebo aktualizujte importované hostující hostitele, když je vaše relace odemčená.", - "proxmoxSyncInterval": "Interval synchronizace (minuty)", - "proxmoxSyncIntervalDesc": "Minimálně 5 minut. Plánovač přeskakuje uzamčené uživatelské relace.", - "proxmoxMarkMissing": "Označit chybějící hosty", - "proxmoxMarkMissingDesc": "Přidat tag proxmox-missing, když dříve importovaný host zmizí, místo aby byl smazán.", - "proxmoxLastSync": "Poslední synchronizace", - "proxmoxLastSyncNever": "Zatím nesynchronizováno", - "proxmoxLastSyncNoResult": "Žádné podrobnosti o výsledcích", - "proxmoxLastSyncSummary": "{{created}} vytvořeno, {{updated}} aktualizováno, {{markedMissing}} chybí, {{skipped}} přeskočeno", - "proxmoxLastSyncStatus": { - "error": "Selhalo", - "pending": "Čeká na vyřízení", - "success": "Úspěch" - }, - "proxmoxDiscoverAction": "Objevte a importujte hosty Proxmoxu", - "proxmoxImportTitle": "Import z Proxmoxu", - "proxmoxSelectHost": "Vyberte hostitele Proxmox…", - "proxmoxDiscover": "Objevit", - "proxmoxDiscovering": "Objevování…", - "proxmoxDiscoverGuests": "Objevte hosty", - "proxmoxSelectAll": "Vybrat vše", - "proxmoxDeselectAll": "Zrušit výběr všeho", - "proxmoxNoGuests": "Na tomto uzlu Proxmox nebyli nalezeni žádní hosté.", - "proxmoxResultImported": "{{count}} importováno", - "proxmoxResultUpdated": "{{count}} aktualizováno", - "proxmoxResultFailed": "{{count}} selhalo", - "proxmoxImportComplete": "Import Proxmoxu dokončen: {{summary}}", - "proxmoxDiscoveryFailed": "Objev selhal", - "proxmoxImportFailed": "Import se nezdařil", - "docsLink": "Zobrazit dokumenty", - "hostNotFound": "Hostitel nenalezen", - "loadingHost": "Načítání hostitele...", - "none": "Žádný", - "proxmoxGuestsSelected_one": "{{count}} host, {{selected}} vybráno", - "proxmoxGuestsSelected_other": "{{count}} hostů, {{selected}} vybráno", - "proxmoxImportButton_one": "Importovat hostitele {{count}}", - "proxmoxImportButton_other": "Importovat hostitele {{count}}" - }, - "terminal": { - "connected": "Připojeno" - } -} diff --git a/plugins/proxmox/locales/translated/da_DK.json b/plugins/proxmox/locales/translated/da_DK.json deleted file mode 100644 index a954b04d7..000000000 --- a/plugins/proxmox/locales/translated/da_DK.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox-statistik" - }, - "proxmoxStats": { - "tabTitle": "Proxmox-statistik", - "cpuUsage": "CPU-forbrug", - "memoryUsage": "Hukommelsesforbrug", - "diskUsage": "Diskforbrug", - "networkInterfaces": "Netværksgrænseflader", - "uptime": "Oppetid", - "systemInfo": "Systemoplysninger", - "guestsSummary": "Gæster", - "storagePools": "Opbevaringsbassiner", - "clusterHealth": "Klyngesundhed", - "cpuCores_one": "{{count}} Kerne", - "cpuCores_other": "{{count}} Kerner", - "used": "Brugt", - "total": "Total", - "hostname": "Værtsnavn", - "kernel": "Kernen", - "pveVersion": "PVE-version", - "name": "Navn", - "status": "Status", - "type": "Type", - "cpu": "CPU", - "mem": "Mem", - "disk": "Disk", - "running": "Løb", - "stopped": "Stoppet", - "guestCounts": "{{running}}/{{total}} løber", - "inactive": "Inaktiv", - "notClustered": "Selvstændig node (ikke en del af en klynge)", - "quorate": "Kvorat", - "notQuorate": "Ikke quorable", - "nodeOnline": "Online", - "nodeOffline": "Offline", - "noHostSelected": "Ingen vært valgt", - "pveshNotFound": "pvesh ikke fundet - er dette en Proxmox-node?", - "nodeDetectionFailed": "Kan ikke bestemme et gyldigt Proxmox-nodenavn", - "connecting": "Forbinder...", - "connectionFailed": "Kunne ikke oprette forbindelse til Proxmox-noden", - "refresh": "Opfriske", - "searchGuests": "Søg gæster...", - "filterAll": "Alle", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "Der blev ikke fundet nogen VM'er eller LXC-containere på denne node.", - "noGuestsMatch": "Ingen gæster matcher de nuværende filtre." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Aktivér Proxmox-statistik", - "enableProxmoxStatsDesc": "Vis en Proxmox Stats-faneblad for denne vært med live node-, gæst-, lager- og klyngestatistikker indsamlet via SSH.", - "proxmoxStatsPollInterval": "Afstemningsinterval (sekunder)", - "proxmoxStatsPollIntervalDesc": "Minimum 15 sekunder. Hvor ofte skal Proxmox-nodestatistikker opdateres.", - "proxmoxStatsNodeOverride": "Tilsidesættelse af nodenavn", - "proxmoxStatsNodeOverrideDesc": "Tilsidesæt automatisk detektion af Proxmox-nodenavnet (registreres som standard via værtsnavn).", - "proxmoxStatsNodeOverridePlaceholder": "Automatisk registreret fra værtsnavn", - "authTypeStepca": "Trin CA", - "proxmoxIntegration": "Proxmox-integration", - "enableProxmox": "Aktivér Proxmox", - "enableProxmoxDesc": "Marker denne vært som en Proxmox-node. Aktiverer gæsteopdagelse og import direkte fra denne vært.", - "proxmoxDefaultAuthType": "Standardgodkendelsestype", - "proxmoxDefaultAuthTypeDesc": "Godkendelsesmetode anvendt på importerede gæsteværter. Vælg den godkendelsestype, der matcher den måde, du opretter forbindelse til dine gæster.", - "authTypePassword": "Adgangskode", - "authTypeKey": "SSH-nøgle", - "authTypeCredential": "Legitimationsoplysninger", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Ingen", - "proxmoxDefaultCredential": "Standardlegitimationsoplysninger", - "proxmoxDefaultCredentialDesc": "Legitimationsoplysninger, der bruges til importerede gæsteværter, når godkendelsestypen er indstillet til Legitimationsoplysninger.", - "proxmoxWindowsDetection": "Windows/RDP-detektion", - "proxmoxWindowsDetectionDesc": "Kommaseparerede navnemønstre, der udløser RDP i stedet for SSH (ikke-sanskel på store og små bogstaver)", - "proxmoxDockerDetection": "Docker-detektion", - "proxmoxDockerDetectionDesc": "Kommaseparerede navnemønstre, der gør det muligt for Docker at matche gæster", - "proxmoxPreferredRanges": "Foretrukne IP-intervaller", - "proxmoxPreferredRangesDesc": "Kommaseparerede præfikser i prioriteret rækkefølge for IP-valg, når en gæst har flere grænseflader", - "proxmoxAutoSync": "Automatisk synkronisering af gæster", - "proxmoxAutoSyncDesc": "Opdag periodisk denne Proxmox-node og opret eller opdater importerede gæsteværter, mens din session er låst op.", - "proxmoxSyncInterval": "Synkroniseringsinterval (minutter)", - "proxmoxSyncIntervalDesc": "Minimum 5 minutter. Planlæggeren springer låste brugersessioner over.", - "proxmoxMarkMissing": "Markér manglende gæster", - "proxmoxMarkMissingDesc": "Tilføj et proxmox-missing-tag, når en tidligere importeret gæst forsvinder, i stedet for at slette den.", - "proxmoxLastSync": "Sidste synkronisering", - "proxmoxLastSyncNever": "Ikke synkroniseret endnu", - "proxmoxLastSyncNoResult": "Ingen resultatdetaljer", - "proxmoxLastSyncSummary": "{{created}} oprettet, {{updated}} opdateret, {{markedMissing}} mangler, {{skipped}} sprunget over", - "proxmoxLastSyncStatus": { - "error": "Mislykkedes", - "pending": "Indtil", - "success": "Succes" - }, - "proxmoxDiscoverAction": "Opdag og importer Proxmox-gæster", - "proxmoxImportTitle": "Importer fra Proxmox", - "proxmoxSelectHost": "Vælg en Proxmox-vært…", - "proxmoxDiscover": "Opdage", - "proxmoxDiscovering": "Opdager…", - "proxmoxDiscoverGuests": "Opdag gæster", - "proxmoxSelectAll": "Vælg alle", - "proxmoxDeselectAll": "Fravælg alle", - "proxmoxNoGuests": "Ingen gæster fundet på denne Proxmox-node.", - "proxmoxResultImported": "{{count}} importeret", - "proxmoxResultUpdated": "{{count}} opdateret", - "proxmoxResultFailed": "{{count}} mislykkedes", - "proxmoxImportComplete": "Proxmox-import fuldført: {{summary}}", - "proxmoxDiscoveryFailed": "Opdagelsen mislykkedes", - "proxmoxImportFailed": "Importen mislykkedes", - "docsLink": "Se dokumenter", - "hostNotFound": "Værten blev ikke fundet", - "loadingHost": "Indlæser vært...", - "none": "Ingen", - "proxmoxGuestsSelected_one": "{{count}} gæst, {{selected}} valgt", - "proxmoxGuestsSelected_other": "{{count}} gæster, {{selected}} valgt", - "proxmoxImportButton_one": "Importér {{count}} vært", - "proxmoxImportButton_other": "Importér {{count}} værter" - }, - "terminal": { - "connected": "Forbundet" - } -} diff --git a/plugins/proxmox/locales/translated/de_DE.json b/plugins/proxmox/locales/translated/de_DE.json deleted file mode 100644 index e712e4327..000000000 --- a/plugins/proxmox/locales/translated/de_DE.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox-Statistiken" - }, - "proxmoxStats": { - "tabTitle": "Proxmox-Statistiken", - "cpuUsage": "CPU-Auslastung", - "memoryUsage": "Speicherauslastung", - "diskUsage": "Festplattenauslastung", - "networkInterfaces": "Netzwerkschnittstellen", - "uptime": "Betriebszeit", - "systemInfo": "Systeminformationen", - "guestsSummary": "Gäste", - "storagePools": "Speicherbecken", - "clusterHealth": "Cluster Health", - "cpuCores_one": "{{count}} Kern", - "cpuCores_other": "{{count}} Kerne", - "used": "Belegt", - "total": "Gesamt", - "hostname": "Hostname", - "kernel": "Kernel", - "pveVersion": "PVE-Version", - "name": "Name", - "status": "Status", - "type": "Typ", - "cpu": "CPU", - "mem": "Mitglied", - "disk": "Festplatte", - "running": "Läuft", - "stopped": "Gestoppt", - "guestCounts": "{{running}}/{{total}} läuft", - "inactive": "Inaktiv", - "notClustered": "Eigenständiger Knoten (nicht Teil eines Clusters)", - "quorate": "Quorum", - "notQuorate": "Nicht beschlussfähig", - "nodeOnline": "Online", - "nodeOffline": "Offline", - "noHostSelected": "Kein Host ausgewählt", - "pveshNotFound": "pvesh nicht gefunden - handelt es sich um einen Proxmox-Knoten?", - "nodeDetectionFailed": "Es konnte kein gültiger Proxmox-Knotenname ermittelt werden.", - "connecting": "Verbindung wird hergestellt...", - "connectionFailed": "Verbindung zum Proxmox-Knoten fehlgeschlagen.", - "refresh": "Aktualisieren", - "searchGuests": "Gäste suchen...", - "filterAll": "Alle", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "Auf diesem Knoten wurden keine VMs oder LXC-Container gefunden.", - "noGuestsMatch": "Es gibt keine Gäste, die den aktuellen Filtern entsprechen." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Proxmox-Statistiken aktivieren", - "enableProxmoxStatsDesc": "Zeigen Sie für diesen Host eine Registerkarte „Proxmox Stats“ mit Live-Statistiken zu Knoten, Gastsystemen, Speicher und Cluster an, die über SSH erfasst werden.", - "proxmoxStatsPollInterval": "Abfrageintervall (Sekunden)", - "proxmoxStatsPollIntervalDesc": "Mindestens 15 Sekunden. Wie oft sollen die Proxmox-Knotenstatistiken aktualisiert werden?", - "proxmoxStatsNodeOverride": "Knotennamen überschreiben", - "proxmoxStatsNodeOverrideDesc": "Die automatische Erkennung des Proxmox-Knotennamens (standardmäßig über den Hostnamen ermittelt) kann deaktiviert werden.", - "proxmoxStatsNodeOverridePlaceholder": "Automatisch anhand des Hostnamens erkannt", - "authTypeStepca": "Schritt CA", - "proxmoxIntegration": "Proxmox-Integration", - "enableProxmox": "Proxmox aktivieren", - "enableProxmoxDesc": "Diesen Host als Proxmox-Knoten markieren. Ermöglicht die Erkennung und den Import von Gästen direkt von diesem Host.", - "proxmoxDefaultAuthType": "Standard-Authentifizierungstyp", - "proxmoxDefaultAuthTypeDesc": "Authentifizierungsmethode, die auf importierte Gast-Hosts angewendet wird. Wählen Sie den Authentifizierungstyp, der Ihrer Verbindungsmethode zu den Gästen entspricht.", - "authTypePassword": "Passwort", - "authTypeKey": "SSH-Schlüssel", - "authTypeCredential": "Anmeldedaten", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Keine", - "proxmoxDefaultCredential": "Standard-Anmeldedaten", - "proxmoxDefaultCredentialDesc": "Anmeldedaten, die für importierte Gastsysteme verwendet werden, wenn der Authentifizierungstyp auf Anmeldedaten gesetzt ist.", - "proxmoxWindowsDetection": "Windows-/RDP-Erkennung", - "proxmoxWindowsDetectionDesc": "Kommagetrennte Namensmuster, die RDP statt SSH auslösen (Groß-/Kleinschreibung wird nicht beachtet).", - "proxmoxDockerDetection": "Docker-Erkennung", - "proxmoxDockerDetectionDesc": "Kommagetrennte Namensmuster, die Docker für passende Gastsysteme aktivieren.", - "proxmoxPreferredRanges": "Bevorzugte IP-Bereiche", - "proxmoxPreferredRangesDesc": "Kommagetrennte Präfixe in Prioritätsreihenfolge für die IP-Auswahl, wenn ein Gastsystem mehrere Schnittstellen hat.", - "proxmoxAutoSync": "Gastsysteme automatisch synchronisieren", - "proxmoxAutoSyncDesc": "Diesen Proxmox-Knoten periodisch erkennen und importierte Gastsysteme erstellen oder aktualisieren, während Ihre Sitzung entsperrt ist.", - "proxmoxSyncInterval": "Synchronisierungsintervall (Minuten)", - "proxmoxSyncIntervalDesc": "Mindestens 5 Minuten. Der Scheduler überspringt gesperrte Benutzersitzungen.", - "proxmoxMarkMissing": "Fehlende Gastsysteme markieren", - "proxmoxMarkMissingDesc": "Ein proxmox-missing-Tag hinzufügen, wenn ein zuvor importiertes Gastsystem verschwindet, anstatt es zu löschen.", - "proxmoxLastSync": "Letzte Synchronisierung", - "proxmoxLastSyncNever": "Noch nicht synchronisiert", - "proxmoxLastSyncNoResult": "Keine Ergebnisdetails", - "proxmoxLastSyncSummary": "{{created}} erstellt, {{updated}} aktualisiert, {{markedMissing}} fehlend, {{skipped}} übersprungen", - "proxmoxLastSyncStatus": { - "error": "Fehlgeschlagen", - "pending": "Ausstehend", - "success": "Erfolg" - }, - "proxmoxDiscoverAction": "Proxmox-Gastsysteme erkennen & importieren", - "proxmoxImportTitle": "Von Proxmox importieren", - "proxmoxSelectHost": "Proxmox-Host auswählen…", - "proxmoxDiscover": "Erkennen", - "proxmoxDiscovering": "Erkennen…", - "proxmoxDiscoverGuests": "Gastsysteme erkennen", - "proxmoxSelectAll": "Alle auswählen", - "proxmoxDeselectAll": "Alle abwählen", - "proxmoxNoGuests": "Keine Gastsysteme auf diesem Proxmox-Knoten gefunden.", - "proxmoxResultImported": "{{count}} importiert", - "proxmoxResultUpdated": "{{count}} aktualisiert", - "proxmoxResultFailed": "{{count}} fehlgeschlagen", - "proxmoxImportComplete": "Proxmox-Import abgeschlossen: {{summary}}", - "proxmoxDiscoveryFailed": "Erkennung fehlgeschlagen", - "proxmoxImportFailed": "Import fehlgeschlagen", - "docsLink": "Dokumentation anzeigen", - "hostNotFound": "Host nicht gefunden", - "loadingHost": "Host wird geladen...", - "none": "Keine", - "proxmoxGuestsSelected_one": "{{count}} Gast, {{selected}} ausgewählt", - "proxmoxGuestsSelected_other": "{{count}} Gäste, {{selected}} ausgewählt", - "proxmoxImportButton_one": "{{count}} Host importieren", - "proxmoxImportButton_other": "{{count}} Hosts importieren" - }, - "terminal": { - "connected": "Verbunden" - } -} diff --git a/plugins/proxmox/locales/translated/el_GR.json b/plugins/proxmox/locales/translated/el_GR.json deleted file mode 100644 index bc90cd2a5..000000000 --- a/plugins/proxmox/locales/translated/el_GR.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Στατιστικά Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Στατιστικά Proxmox", - "cpuUsage": "Χρήση CPU", - "memoryUsage": "Χρήση μνήμης", - "diskUsage": "Χρήση δίσκου", - "networkInterfaces": "Διεπαφές δικτύου", - "uptime": "Χρόνος λειτουργίας", - "systemInfo": "Πληροφορίες συστήματος", - "guestsSummary": "Επισκέπτες", - "storagePools": "Πισίνες αποθήκευσης", - "clusterHealth": "Υγεία συμπλέγματος", - "cpuCores_one": "{{count}} Πυρήνας", - "cpuCores_other": "{{count}} Πυρήνες", - "used": "Μεταχειρισμένος", - "total": "Σύνολο", - "hostname": "Όνομα κεντρικού υπολογιστή", - "kernel": "Πυρήνας", - "pveVersion": "Έκδοση PVE", - "name": "Ονομα", - "status": "Κατάσταση", - "type": "Τύπος", - "cpu": "ΚΜΕ", - "mem": "Μνήμη", - "disk": "Δίσκος", - "running": "Τρέξιμο", - "stopped": "Σταμάτησε", - "guestCounts": "{{running}}/{{total}} σε λειτουργία", - "inactive": "Αδρανής", - "notClustered": "Αυτόνομος κόμβος (δεν αποτελεί μέρος συμπλέγματος)", - "quorate": "Απαρτίζω", - "notQuorate": "Δεν υπάρχει απαρτία", - "nodeOnline": "Διαδικτυακά", - "nodeOffline": "Εκτός σύνδεσης", - "noHostSelected": "Δεν έχει επιλεγεί κεντρικός υπολογιστής", - "pveshNotFound": "Το pvesh δεν βρέθηκε - είναι αυτός κόμβος Proxmox;", - "nodeDetectionFailed": "Δεν είναι δυνατός ο προσδιορισμός ενός έγκυρου ονόματος κόμβου Proxmox", - "connecting": "Συνδετικός...", - "connectionFailed": "Αποτυχία σύνδεσης με τον κόμβο Proxmox", - "refresh": "Φρεσκάρω", - "searchGuests": "Αναζήτηση επισκεπτών...", - "filterAll": "Ολοι", - "filterVm": "Εικονική Μηχανή", - "filterLxc": "LXC", - "noGuests": "Δεν βρέθηκαν εικονικές μηχανές ή κοντέινερ LXC σε αυτόν τον κόμβο.", - "noGuestsMatch": "Δεν υπάρχουν επισκέπτες που να αντιστοιχούν στα τρέχοντα φίλτρα." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Ενεργοποίηση στατιστικών Proxmox", - "enableProxmoxStatsDesc": "Εμφάνιση μιας καρτέλας Proxmox Stats για αυτόν τον κεντρικό υπολογιστή με στατιστικά στοιχεία ζωντανού κόμβου, επισκέπτη, χώρου αποθήκευσης και συμπλέγματος που συλλέγονται μέσω SSH.", - "proxmoxStatsPollInterval": "Διάστημα δημοσκόπησης (δευτερόλεπτα)", - "proxmoxStatsPollIntervalDesc": "Ελάχιστο 15 δευτερόλεπτα. Πόσο συχνά θα ανανεώνονται τα στατιστικά στοιχεία του κόμβου Proxmox.", - "proxmoxStatsNodeOverride": "Παράκαμψη ονόματος κόμβου", - "proxmoxStatsNodeOverrideDesc": "Παράκαμψη αυτόματης ανίχνευσης του ονόματος κόμβου Proxmox (εντοπίζεται μέσω του ονόματος κεντρικού υπολογιστή από προεπιλογή).", - "proxmoxStatsNodeOverridePlaceholder": "Αυτόματη ανίχνευση από το όνομα κεντρικού υπολογιστή", - "authTypeStepca": "Βήμα CA", - "proxmoxIntegration": "Ενσωμάτωση Proxmox", - "enableProxmox": "Ενεργοποίηση Proxmox", - "enableProxmoxDesc": "Σημειώστε αυτόν τον κεντρικό υπολογιστή ως κόμβο Proxmox. Επιτρέπει την ανακάλυψη επισκεπτών και την απευθείας εισαγωγή από αυτόν τον κεντρικό υπολογιστή.", - "proxmoxDefaultAuthType": "Προεπιλεγμένος τύπος εξουσιοδότησης", - "proxmoxDefaultAuthTypeDesc": "Μέθοδος ελέγχου ταυτότητας που εφαρμόστηκε στους εισαγόμενους φιλοξενούμενους οικοδεσπότες. Επιλέξτε τον τύπο ελέγχου ταυτότητας που ταιριάζει με τον τρόπο που συνδέεστε με τους επισκέπτες σας.", - "authTypePassword": "Σύνθημα", - "authTypeKey": "Κλειδί SSH", - "authTypeCredential": "Πιστοποιητικό", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Κανένας", - "proxmoxDefaultCredential": "Προεπιλεγμένα διαπιστευτήρια", - "proxmoxDefaultCredentialDesc": "Διαπιστευτήριο που χρησιμοποιείται για εισαγόμενους φιλοξενούμενους οικοδεσπότες όταν ο τύπος εξουσιοδότησης έχει οριστεί σε Διαπιστευτήριο.", - "proxmoxWindowsDetection": "Ανίχνευση Windows / RDP", - "proxmoxWindowsDetectionDesc": "Μοτίβα ονομάτων διαχωρισμένα με κόμματα που ενεργοποιούν το RDP αντί για SSH (χωρίς διάκριση πεζών-κεφαλαίων)", - "proxmoxDockerDetection": "Ανίχνευση Docker", - "proxmoxDockerDetectionDesc": "Μοτίβα ονομάτων διαχωρισμένα με κόμματα που επιτρέπουν στο Docker να αντιστοιχίζει επισκέπτες", - "proxmoxPreferredRanges": "Προτιμώμενα εύρη IP", - "proxmoxPreferredRangesDesc": "Προθέματα διαχωρισμένα με κόμμα σε σειρά προτεραιότητας για την επιλογή IP όταν ένας επισκέπτης έχει πολλαπλές διεπαφές", - "proxmoxAutoSync": "Αυτόματος συγχρονισμός επισκεπτών", - "proxmoxAutoSyncDesc": "Εντοπίζετε περιοδικά αυτόν τον κόμβο Proxmox και δημιουργείτε ή ενημερώνετε εισαγόμενους φιλοξενούμενους υπολογιστές όσο η συνεδρία σας είναι ξεκλείδωτη.", - "proxmoxSyncInterval": "Διάστημα συγχρονισμού (λεπτά)", - "proxmoxSyncIntervalDesc": "Ελάχιστο 5 λεπτά. Ο προγραμματιστής παραλείπει τις κλειδωμένες συνεδρίες χρήστη.", - "proxmoxMarkMissing": "Ο Μαρκ λείπουν επισκέπτες.", - "proxmoxMarkMissingDesc": "Προσθέστε μια ετικέτα proxmox-missing όταν εξαφανιστεί ένας προηγουμένως εισαγόμενος επισκέπτης αντί να τον διαγράψετε.", - "proxmoxLastSync": "Τελευταίος συγχρονισμός", - "proxmoxLastSyncNever": "Δεν έχει συγχρονιστεί ακόμα", - "proxmoxLastSyncNoResult": "Δεν υπάρχουν λεπτομέρειες για το αποτέλεσμα", - "proxmoxLastSyncSummary": "{{created}} δημιουργήθηκε, {{updated}} ενημερώθηκε, {{markedMissing}} λείπει, {{skipped}} παραλείφθηκε", - "proxmoxLastSyncStatus": { - "error": "Αποτυχημένος", - "pending": "Εκκρεμής", - "success": "Επιτυχία" - }, - "proxmoxDiscoverAction": "Ανακάλυψη και εισαγωγή επισκεπτών Proxmox", - "proxmoxImportTitle": "Εισαγωγή από το Proxmox", - "proxmoxSelectHost": "Επιλέξτε έναν κεντρικό υπολογιστή Proxmox…", - "proxmoxDiscover": "Ανακαλύπτω", - "proxmoxDiscovering": "Ανακαλύπτοντας…", - "proxmoxDiscoverGuests": "Ανακαλύψτε επισκέπτες", - "proxmoxSelectAll": "Επιλογή όλων", - "proxmoxDeselectAll": "Αποεπιλογή όλων", - "proxmoxNoGuests": "Δεν βρέθηκαν επισκέπτες σε αυτόν τον κόμβο Proxmox.", - "proxmoxResultImported": "{{count}} εισήχθη", - "proxmoxResultUpdated": "{{count}} ενημερώθηκε", - "proxmoxResultFailed": "{{count}} απέτυχε", - "proxmoxImportComplete": "Η εισαγωγή Proxmox ολοκληρώθηκε: {{summary}}", - "proxmoxDiscoveryFailed": "Η ανακάλυψη απέτυχε", - "proxmoxImportFailed": "Η εισαγωγή απέτυχε.", - "docsLink": "Προβολή εγγράφων", - "hostNotFound": "Δεν βρέθηκε κεντρικός υπολογιστής", - "loadingHost": "Φόρτωση κεντρικού υπολογιστή...", - "none": "Κανένας", - "proxmoxGuestsSelected_one": "{{count}} επισκέπτης, {{selected}} επιλεγμένος", - "proxmoxGuestsSelected_other": "{{count}} επισκέπτες, {{selected}} επιλεγμένοι", - "proxmoxImportButton_one": "Εισαγωγή κεντρικού υπολογιστή {{count}}", - "proxmoxImportButton_other": "Εισαγωγή {{count}} κεντρικών υπολογιστών" - }, - "terminal": { - "connected": "Συνδεδεμένος" - } -} diff --git a/plugins/proxmox/locales/translated/es_ES.json b/plugins/proxmox/locales/translated/es_ES.json deleted file mode 100644 index b87796172..000000000 --- a/plugins/proxmox/locales/translated/es_ES.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Estadísticas de Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Estadísticas de Proxmox", - "cpuUsage": "Uso de CPU", - "memoryUsage": "Uso de memoria", - "diskUsage": "Uso del disco", - "networkInterfaces": "Interfaces de red", - "uptime": "Tiempo de actividad", - "systemInfo": "Información del sistema", - "guestsSummary": "Huéspedes", - "storagePools": "Piscinas de almacenamiento", - "clusterHealth": "Salud en grupo", - "cpuCores_one": "{{count}} núcleo", - "cpuCores_other": "{{count}} núcleos", - "used": "Usado", - "total": "Total", - "hostname": "Nombre de host", - "kernel": "Kernel", - "pveVersion": "Versión PVE", - "name": "Nombre", - "status": "Estado", - "type": "Tipo", - "cpu": "CPU", - "mem": "Memoria", - "disk": "Disco", - "running": "En ejecución", - "stopped": "Detenido", - "guestCounts": "{{running}}/{{total}} corriendo", - "inactive": "Inactivo", - "notClustered": "Nodo independiente (no forma parte de un clúster)", - "quorate": "Con quórum", - "notQuorate": "No hay quórum", - "nodeOnline": "En línea", - "nodeOffline": "Desconectado", - "noHostSelected": "Ningún host seleccionado", - "pveshNotFound": "pvesh no encontrado - ¿es este un nodo Proxmox?", - "nodeDetectionFailed": "No se pudo determinar un nombre de nodo Proxmox válido.", - "connecting": "Conectando...", - "connectionFailed": "No se pudo conectar con el nodo Proxmox.", - "refresh": "Actualizar", - "searchGuests": "Buscar huéspedes...", - "filterAll": "Revocar todo", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "No se encontraron máquinas virtuales ni contenedores LXC en este nodo.", - "noGuestsMatch": "No hay huéspedes que coincidan con los filtros actuales." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Habilitar estadísticas de Proxmox", - "enableProxmoxStatsDesc": "Muestra una pestaña de estadísticas de Proxmox para este host con estadísticas en tiempo real del nodo, invitado, almacenamiento y clúster recopiladas a través de SSH.", - "proxmoxStatsPollInterval": "Intervalo de sondeo (segundos)", - "proxmoxStatsPollIntervalDesc": "Mínimo 15 segundos. Con qué frecuencia se actualizan las estadísticas del nodo Proxmox.", - "proxmoxStatsNodeOverride": "Anulación del nombre del nodo", - "proxmoxStatsNodeOverrideDesc": "Anular la detección automática del nombre del nodo Proxmox (detectado por defecto mediante el nombre de host).", - "proxmoxStatsNodeOverridePlaceholder": "Detectado automáticamente a partir del nombre de host.", - "authTypeStepca": "Paso CA", - "proxmoxIntegration": "Integración con Proxmox", - "enableProxmox": "Activar Proxmox", - "enableProxmoxDesc": "Marcar este host como un nodo Proxmox. Permite la detección e importación de invitados directamente desde este host.", - "proxmoxDefaultAuthType": "Tipo de autenticación predeterminado", - "proxmoxDefaultAuthTypeDesc": "Método de autenticación aplicado a los hosts invitados importados. Elija el tipo de autenticación que coincida con cómo se conecta a sus invitados.", - "authTypePassword": "Contraseña", - "authTypeKey": "Clave SSH", - "authTypeCredential": "Credencial", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Ninguno", - "proxmoxDefaultCredential": "Credencial predeterminada", - "proxmoxDefaultCredentialDesc": "Credencial utilizada para los hosts invitados importados cuando el tipo de autenticación está configurado como Credencial.", - "proxmoxWindowsDetection": "Detección de Windows / RDP", - "proxmoxWindowsDetectionDesc": "Patrones de nombres separados por comas que activan RDP en lugar de SSH (sin distinción de mayúsculas/minúsculas)", - "proxmoxDockerDetection": "Detección de Docker", - "proxmoxDockerDetectionDesc": "Patrones de nombres separados por comas que habilitan Docker para los invitados coincidentes", - "proxmoxPreferredRanges": "Rangos de IP preferidos", - "proxmoxPreferredRangesDesc": "Prefijos separados por comas en orden de prioridad para la selección de IP cuando un invitado tiene múltiples interfaces", - "proxmoxAutoSync": "Sincronización automática de invitados", - "proxmoxAutoSyncDesc": "Descubrir periódicamente este nodo Proxmox y crear o actualizar los hosts invitados importados mientras la sesión está desbloqueada.", - "proxmoxSyncInterval": "Intervalo de sincronización (minutos)", - "proxmoxSyncIntervalDesc": "Mínimo 5 minutos. El planificador omite las sesiones de usuario bloqueadas.", - "proxmoxMarkMissing": "Marcar invitados faltantes", - "proxmoxMarkMissingDesc": "Añadir una etiqueta proxmox-missing cuando un invitado previamente importado desaparece en lugar de eliminarlo.", - "proxmoxLastSync": "Última sincronización", - "proxmoxLastSyncNever": "Aún no sincronizado", - "proxmoxLastSyncNoResult": "Sin detalles del resultado", - "proxmoxLastSyncSummary": "{{created}} creados, {{updated}} actualizados, {{markedMissing}} faltantes, {{skipped}} omitidos", - "proxmoxLastSyncStatus": { - "error": "Fallido", - "pending": "Pendiente", - "success": "Correcto" - }, - "proxmoxDiscoverAction": "Descubrir e importar invitados Proxmox", - "proxmoxImportTitle": "Importar desde Proxmox", - "proxmoxSelectHost": "Seleccionar un host Proxmox…", - "proxmoxDiscover": "Descubrir", - "proxmoxDiscovering": "Descubriendo…", - "proxmoxDiscoverGuests": "Descubrir invitados", - "proxmoxSelectAll": "Seleccionar todos", - "proxmoxDeselectAll": "Deseleccionar todos", - "proxmoxNoGuests": "No se encontraron invitados en este nodo Proxmox.", - "proxmoxResultImported": "{{count}} importados", - "proxmoxResultUpdated": "{{count}} actualizados", - "proxmoxResultFailed": "{{count}} fallidos", - "proxmoxImportComplete": "Importación de Proxmox completada: {{summary}}", - "proxmoxDiscoveryFailed": "Descubrimiento fallido", - "proxmoxImportFailed": "Importación fallida", - "docsLink": "Ver documentación", - "hostNotFound": "Host no encontrado", - "loadingHost": "Cargando host...", - "none": "Ninguno", - "proxmoxGuestsSelected_one": "{{count}} invitado, {{selected}} seleccionado", - "proxmoxGuestsSelected_other": "{{count}} invitados, {{selected}} seleccionados", - "proxmoxImportButton_one": "Importar {{count}} host", - "proxmoxImportButton_other": "Importar {{count}} hosts" - }, - "terminal": { - "connected": "Conectado" - } -} diff --git a/plugins/proxmox/locales/translated/fi_FI.json b/plugins/proxmox/locales/translated/fi_FI.json deleted file mode 100644 index 387087ac0..000000000 --- a/plugins/proxmox/locales/translated/fi_FI.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox-tilastot" - }, - "proxmoxStats": { - "tabTitle": "Proxmox-tilastot", - "cpuUsage": "Suorittimen käyttö", - "memoryUsage": "Muistin käyttö", - "diskUsage": "Levyn käyttö", - "networkInterfaces": "Verkkoliitännät", - "uptime": "Käyttöaika", - "systemInfo": "Järjestelmätiedot", - "guestsSummary": "Vieraat", - "storagePools": "Varastoaltaat", - "clusterHealth": "Klusterin terveys", - "cpuCores_one": "{{count}} Ydin", - "cpuCores_other": "{{count}} Ytimet", - "used": "Käytetty", - "total": "Kokonais", - "hostname": "Isäntänimi", - "kernel": "Ydin", - "pveVersion": "PVE-versio", - "name": "Nimi", - "status": "Status", - "type": "Tyyppi", - "cpu": "Suoritin", - "mem": "Mem", - "disk": "Levy", - "running": "Juoksu", - "stopped": "Pysähtynyt", - "guestCounts": "{{running}}/{{total}} käynnissä", - "inactive": "Passiivinen", - "notClustered": "Itsenäinen solmu (ei osa klusteria)", - "quorate": "Päätösvaltainen", - "notQuorate": "Ei päätöslauselma", - "nodeOnline": "Verkossa", - "nodeOffline": "Offline-tilassa", - "noHostSelected": "Ei valittua isäntää", - "pveshNotFound": "pvesh-solmua ei löytynyt - onko tämä Proxmox-solmu?", - "nodeDetectionFailed": "Kelvollista Proxmox-solmun nimeä ei voitu määrittää.", - "connecting": "Yhdistetään...", - "connectionFailed": "Yhteyden muodostaminen Proxmox-solmuun epäonnistui", - "refresh": "Päivitä", - "searchGuests": "Hae vieraita...", - "filterAll": "Kaikki", - "filterVm": "Virtuaalikone", - "filterLxc": "LXC", - "noGuests": "Tältä solmulta ei löytynyt virtuaalikoneita tai LXC-säilöjä.", - "noGuestsMatch": "Yksikään vieras ei vastaa nykyisiä suodattimia." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Ota Proxmox-tilastot käyttöön", - "enableProxmoxStatsDesc": "Näytä tämän isännän Proxmox-tilastot-välilehti, joka sisältää SSH:n kautta kerätyt reaaliaikaiset solmu-, vieras-, tallennus- ja klusteritilastot.", - "proxmoxStatsPollInterval": "Kyselyväli (sekuntia)", - "proxmoxStatsPollIntervalDesc": "Vähintään 15 sekuntia. Kuinka usein Proxmox-solmun tilastot päivitetään.", - "proxmoxStatsNodeOverride": "Solmun nimen ohitus", - "proxmoxStatsNodeOverrideDesc": "Ohita Proxmox-solmun nimen automaattinen tunnistus (tunnistetaan oletusarvoisesti isäntänimen kautta).", - "proxmoxStatsNodeOverridePlaceholder": "Automaattisesti tunnistettu isäntänimestä", - "authTypeStepca": "Vaihe CA", - "proxmoxIntegration": "Proxmox-integraatio", - "enableProxmox": "Ota Proxmox käyttöön", - "enableProxmoxDesc": "Merkitse tämä isäntä Proxmox-solmuksi. Mahdollistaa vieraslaitteiden etsinnän ja tuonnin suoraan tästä isännästä.", - "proxmoxDefaultAuthType": "Oletustodennustyyppi", - "proxmoxDefaultAuthTypeDesc": "Tuotuihin vieraspalvelimiin sovellettu todennustapa. Valitse todennustyyppi, joka vastaa tapaa, jolla muodostat yhteyden vieraisiin palvelimiin.", - "authTypePassword": "Salasana", - "authTypeKey": "SSH-avain", - "authTypeCredential": "Valtakirja", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Ei mitään", - "proxmoxDefaultCredential": "Oletustunnistetiedot", - "proxmoxDefaultCredentialDesc": "Tuotujen vieraspalvelimien tunnistetiedot, kun todennustyypiksi on asetettu Tunnistetiedot.", - "proxmoxWindowsDetection": "Windowsin / RDP-tunnistus", - "proxmoxWindowsDetectionDesc": "Pilkuilla erotetut nimimallit, jotka käynnistävät RDP:n SSH:n sijaan (kirjainkokoa ei erotella)", - "proxmoxDockerDetection": "Docker-tunnistus", - "proxmoxDockerDetectionDesc": "Pilkuilla erotetut nimimallit, joiden avulla Docker löytää vieraat yhteensopiviksi", - "proxmoxPreferredRanges": "Suositellut IP-alueet", - "proxmoxPreferredRangesDesc": "Pilkuilla erotetut etuliitteet prioriteettijärjestyksessä IP-osoitteen valinnassa, kun vieraalla on useita rajapintoja", - "proxmoxAutoSync": "Synkronoi vieraat automaattisesti", - "proxmoxAutoSyncDesc": "Etsi tämä Proxmox-solmu säännöllisesti ja luo tai päivitä tuotuja vieraskomanteja istunnon ollessa lukitsematon.", - "proxmoxSyncInterval": "Synkronointiväli (minuuttia)", - "proxmoxSyncIntervalDesc": "Vähintään 5 minuuttia. Ajastin ohittaa lukitut käyttäjäistunnot.", - "proxmoxMarkMissing": "Merkitse puuttuvat vieraat", - "proxmoxMarkMissingDesc": "Lisää proxmox-missing-tunniste, kun aiemmin tuotu vieras katoaa sen poistamisen sijaan.", - "proxmoxLastSync": "Viimeisin synkronointi", - "proxmoxLastSyncNever": "Ei vielä synkronoitu", - "proxmoxLastSyncNoResult": "Ei tulostietoja", - "proxmoxLastSyncSummary": "{{created}} luotu, {{updated}} päivitetty, {{markedMissing}} puuttuu, {{skipped}} ohitettu", - "proxmoxLastSyncStatus": { - "error": "Epäonnistui", - "pending": "Odottaa", - "success": "Menestys" - }, - "proxmoxDiscoverAction": "Löydä ja tuo Proxmox-vieraita", - "proxmoxImportTitle": "Tuo Proxmoxista", - "proxmoxSelectHost": "Valitse Proxmox-isäntä…", - "proxmoxDiscover": "Tutustu", - "proxmoxDiscovering": "Löytöretkellä…", - "proxmoxDiscoverGuests": "Tutustu vieraisiin", - "proxmoxSelectAll": "Valitse kaikki", - "proxmoxDeselectAll": "Poista kaikkien valinta", - "proxmoxNoGuests": "Tältä Proxmox-solmulta ei löytynyt vieraita.", - "proxmoxResultImported": "{{count}} tuotu", - "proxmoxResultUpdated": "{{count}} päivitetty", - "proxmoxResultFailed": "{{count}} epäonnistui", - "proxmoxImportComplete": "Proxmox-tuonti valmis: {{summary}}", - "proxmoxDiscoveryFailed": "Löytö epäonnistui", - "proxmoxImportFailed": "Tuonti epäonnistui", - "docsLink": "Näytä dokumentit", - "hostNotFound": "Isäntää ei löydy", - "loadingHost": "Ladataan isäntää...", - "none": "Ei mitään", - "proxmoxGuestsSelected_one": "{{count}} vieras, {{selected}} valittu", - "proxmoxGuestsSelected_other": "{{count}} vierasta, {{selected}} valittu", - "proxmoxImportButton_one": "Tuo {{count}} isäntä", - "proxmoxImportButton_other": "Tuo {{count}} isäntiä" - }, - "terminal": { - "connected": "Yhdistetty" - } -} diff --git a/plugins/proxmox/locales/translated/fr_FR.json b/plugins/proxmox/locales/translated/fr_FR.json deleted file mode 100644 index c32ee9bd2..000000000 --- a/plugins/proxmox/locales/translated/fr_FR.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Statistiques Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Statistiques Proxmox", - "cpuUsage": "Utilisation CPU", - "memoryUsage": "Utilisation mémoire", - "diskUsage": "Utilisation disque", - "networkInterfaces": "Interfaces réseau", - "uptime": "Disponibilité", - "systemInfo": "Informations système", - "guestsSummary": "Les invités", - "storagePools": "Piscines de stockage", - "clusterHealth": "Cluster Health", - "cpuCores_one": "{{count}} cœur", - "cpuCores_other": "{{count}} cœurs", - "used": "Utilisé", - "total": "Total", - "hostname": "Nom d'hôte", - "kernel": "Noyau", - "pveVersion": "Version JcE", - "name": "Nom", - "status": "État", - "type": "Type", - "cpu": "CPU", - "mem": "Mem", - "disk": "Disque", - "running": "En cours d’exécution", - "stopped": "Arrêté", - "guestCounts": "{{running}}/{{total}} en cours", - "inactive": "Inactif", - "notClustered": "Nœud autonome (ne faisant pas partie d'un cluster)", - "quorate": "Quaire", - "notQuorate": "Pas de quorum", - "nodeOnline": "En ligne", - "nodeOffline": "Hors ligne", - "noHostSelected": "Aucun hôte sélectionné", - "pveshNotFound": "pvesh introuvable - s'agit-il d'un nœud Proxmox ?", - "nodeDetectionFailed": "Impossible de déterminer un nom de nœud Proxmox valide", - "connecting": "Connexion...", - "connectionFailed": "Échec de la connexion au nœud Proxmox", - "refresh": "Actualiser", - "searchGuests": "Rechercher des invités...", - "filterAll": "Tout", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "Aucune machine virtuelle ni conteneur LXC n'a été trouvé sur ce nœud.", - "noGuestsMatch": "Aucun invité ne correspond aux filtres actuels." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Activer les statistiques Proxmox", - "enableProxmoxStatsDesc": "Afficher un onglet Statistiques Proxmox pour cet hôte avec des statistiques en direct sur les nœuds, les invités, le stockage et le cluster collectées via SSH.", - "proxmoxStatsPollInterval": "Intervalle d'interrogation (secondes)", - "proxmoxStatsPollIntervalDesc": "Minimum 15 secondes. Fréquence d'actualisation des statistiques du nœud Proxmox.", - "proxmoxStatsNodeOverride": "Remplacement du nom du nœud", - "proxmoxStatsNodeOverrideDesc": "Désactiver la détection automatique du nom du nœud Proxmox (détecté par défaut via le nom d'hôte).", - "proxmoxStatsNodeOverridePlaceholder": "Détection automatique à partir du nom d'hôte", - "authTypeStepca": "Étape CA", - "proxmoxIntegration": "Intégration Proxmox", - "enableProxmox": "Activer Proxmox", - "enableProxmoxDesc": "Marquer cet hôte comme un nœud Proxmox. Permet la découverte et l'importation des invités directement depuis cet hôte.", - "proxmoxDefaultAuthType": "Type d'authentification par défaut", - "proxmoxDefaultAuthTypeDesc": "Méthode d'authentification appliquée aux hôtes invités importés. Choisissez le type d'authentification correspondant à votre méthode de connexion aux invités.", - "authTypePassword": "Mot de passe", - "authTypeKey": "Clé SSH", - "authTypeCredential": "Identifiants", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Aucun", - "proxmoxDefaultCredential": "Identifiant par défaut", - "proxmoxDefaultCredentialDesc": "Identifiant utilisé pour les hôtes invités importés lorsque le type d'authentification est défini sur Identifiant.", - "proxmoxWindowsDetection": "Détection Windows / RDP", - "proxmoxWindowsDetectionDesc": "Liste de motifs de noms, séparés par des virgules, qui déclenchent RDP au lieu de SSH (insensible à la casse).", - "proxmoxDockerDetection": "Détection Docker", - "proxmoxDockerDetectionDesc": "Liste de motifs de noms, séparés par des virgules, qui activent Docker pour les invités correspondants.", - "proxmoxPreferredRanges": "Plages IP préférées", - "proxmoxPreferredRangesDesc": "Préfixes séparés par des virgules, par ordre de priorité, pour la sélection d'adresse IP lorsqu'un invité possède plusieurs interfaces.", - "proxmoxAutoSync": "Synchronisation automatique des invités", - "proxmoxAutoSyncDesc": "Découvre périodiquement ce nœud Proxmox et crée ou met à jour les hôtes invités importés tant que votre session est déverrouillée.", - "proxmoxSyncInterval": "Intervalle de synchronisation (minutes)", - "proxmoxSyncIntervalDesc": "Minimum 5 minutes. Le planificateur ignore les sessions utilisateur verrouillées.", - "proxmoxMarkMissing": "Marquer les invités manquants", - "proxmoxMarkMissingDesc": "Ajoute une étiquette proxmox-missing lorsqu'un invité précédemment importé disparaît, au lieu de le supprimer.", - "proxmoxLastSync": "Dernière synchronisation", - "proxmoxLastSyncNever": "Pas encore synchronisé", - "proxmoxLastSyncNoResult": "Aucun détail de résultat", - "proxmoxLastSyncSummary": "{{created}} créé(s), {{updated}} mis à jour, {{markedMissing}} manquant(s), {{skipped}} ignoré(s)", - "proxmoxLastSyncStatus": { - "error": "Échec", - "pending": "En attente", - "success": "Succès" - }, - "proxmoxDiscoverAction": "Découvrir et importer les invités Proxmox", - "proxmoxImportTitle": "Importer depuis Proxmox", - "proxmoxSelectHost": "Sélectionner un hôte Proxmox…", - "proxmoxDiscover": "Découvrir", - "proxmoxDiscovering": "Découverte en cours…", - "proxmoxDiscoverGuests": "Découvrir les invités", - "proxmoxSelectAll": "Tout sélectionner", - "proxmoxDeselectAll": "Tout désélectionner", - "proxmoxNoGuests": "Aucun invité trouvé sur ce nœud Proxmox.", - "proxmoxResultImported": "{{count}} importés", - "proxmoxResultUpdated": "{{count}} mis à jour", - "proxmoxResultFailed": "{{count}} échoués", - "proxmoxImportComplete": "Importation Proxmox terminée : {{summary}}", - "proxmoxDiscoveryFailed": "Échec de la découverte", - "proxmoxImportFailed": "Échec de l'importation", - "docsLink": "Voir la documentation", - "hostNotFound": "Hôte introuvable", - "loadingHost": "Chargement de l'hôte...", - "none": "Aucun", - "proxmoxGuestsSelected_one": "{{count}} invité, {{selected}} sélectionné", - "proxmoxGuestsSelected_other": "{{count}} invités, {{selected}} sélectionnés", - "proxmoxImportButton_one": "Importer {{count}} hôte", - "proxmoxImportButton_other": "Importer {{count}} hôtes" - }, - "terminal": { - "connected": "Connecté" - } -} diff --git a/plugins/proxmox/locales/translated/he_IL.json b/plugins/proxmox/locales/translated/he_IL.json deleted file mode 100644 index a2921dbd8..000000000 --- a/plugins/proxmox/locales/translated/he_IL.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "סטטיסטיקות פרוקסמוקס" - }, - "proxmoxStats": { - "tabTitle": "סטטיסטיקות פרוקסמוקס", - "cpuUsage": "שימוש במעבד", - "memoryUsage": "שימוש בזיכרון", - "diskUsage": "שימוש בדיסק", - "networkInterfaces": "ממשקי רשת", - "uptime": "זמן פעולה", - "systemInfo": "מידע מערכת", - "guestsSummary": "אורחים", - "storagePools": "בריכות אחסון", - "clusterHealth": "בריאות אשכול", - "cpuCores_one": "{{count}} ליבה", - "cpuCores_other": "{{count}} ליבות", - "used": "מְשׁוּמָשׁ", - "total": "סַך הַכֹּל", - "hostname": "שם מארח", - "kernel": "גַרעִין", - "pveVersion": "גרסת PVE", - "name": "שֵׁם", - "status": "סטָטוּס", - "type": "סוּג", - "cpu": "מעבד", - "mem": "מ", - "disk": "דִיסק", - "running": "רִיצָה", - "stopped": "נֶעצָר", - "guestCounts": "ריצה {{running}}/ ריצה{{total}}", - "inactive": "לֹא פָּעִיל", - "notClustered": "צומת עצמאי (לא חלק מאשכול)", - "quorate": "קוורט", - "notQuorate": "לא נמצא מניין חוקי", - "nodeOnline": "באינטרנט", - "nodeOffline": "לא מקוון", - "noHostSelected": "לא נבחר מארח", - "pveshNotFound": "pvesh לא נמצא - האם זהו צומת Proxmox?", - "nodeDetectionFailed": "לא ניתן לקבוע שם צומת Proxmox תקין", - "connecting": "מְקַשֵׁר...", - "connectionFailed": "נכשלה ההתחברות לצומת Proxmox", - "refresh": "לְרַעֲנֵן", - "searchGuests": "חיפוש אורחים...", - "filterAll": "כֹּל", - "filterVm": "מכונה וירטואלית", - "filterLxc": "LXC", - "noGuests": "לא נמצאו מכונות וירטואליות או מכולות LXC בצומת זה.", - "noGuestsMatch": "אין אורחים התואמים את המסננים הנוכחיים." - }, - "hosts": { - "tabProxmox": "פרוקסמוקס", - "enableProxmoxStats": "הפעל סטטיסטיקות Proxmox", - "enableProxmoxStatsDesc": "הצג כרטיסיית סטטיסטיקות Proxmox עבור מארח זה עם סטטיסטיקות של צומת, אורח, אחסון ואשכול שנאספו דרך SSH.", - "proxmoxStatsPollInterval": "מרווח זמן בין סקר (שניות)", - "proxmoxStatsPollIntervalDesc": "מינימום 15 שניות. באיזו תדירות לרענן את סטטיסטיקות הצומת של Proxmox.", - "proxmoxStatsNodeOverride": "עקיפת שם הצומת", - "proxmoxStatsNodeOverrideDesc": "עקיפת זיהוי אוטומטי של שם הצומת Proxmox (מזוהה דרך שם המארח כברירת מחדל).", - "proxmoxStatsNodeOverridePlaceholder": "זוהה אוטומטית משם המארח", - "authTypeStepca": "שלב CA", - "proxmoxIntegration": "אינטגרציה של פרוקסמוקס", - "enableProxmox": "הפעל את Proxmox", - "enableProxmoxDesc": "סמן מארח זה כצומת Proxmox. מאפשר גילוי אורחים וייבוא ישירות ממארח זה.", - "proxmoxDefaultAuthType": "סוג אימות ברירת מחדל", - "proxmoxDefaultAuthTypeDesc": "שיטת אימות הוחלת על מארחי אורחים מיובאים. בחר את סוג האימות שמתאים לאופן שבו אתה מתחבר לאורחים שלך.", - "authTypePassword": "סִיסמָה", - "authTypeKey": "מפתח SSH", - "authTypeCredential": "תְעוּדָה", - "authTypeOpkssh": "אופקש", - "authTypeNone": "אַף לֹא אֶחָד", - "proxmoxDefaultCredential": "אישור ברירת מחדל", - "proxmoxDefaultCredentialDesc": "אישור המשמש עבור מארחים אורחים מיובאים כאשר סוג האימות מוגדר לאישור.", - "proxmoxWindowsDetection": "זיהוי Windows / RDP", - "proxmoxWindowsDetectionDesc": "תבניות שמות מופרדות בפסיקים המפעילות RDP במקום SSH (לא תלויות רישיות)", - "proxmoxDockerDetection": "זיהוי דוקר", - "proxmoxDockerDetectionDesc": "תבניות שמות מופרדות בפסיקים המאפשרות ל-Docker להתאים אורחים", - "proxmoxPreferredRanges": "טווחי IP מועדפים", - "proxmoxPreferredRangesDesc": "קידומות מופרדות בפסיקים בסדר עדיפות לבחירת IP כאשר לאורח יש ממשקים מרובים", - "proxmoxAutoSync": "סנכרון אוטומטי של אורחים", - "proxmoxAutoSyncDesc": "גלה מעת לעת את צומת Proxmox הזה וצור או עדכן מארחי אורחים מיובאים בזמן שההפעלה שלך אינה נעולה.", - "proxmoxSyncInterval": "מרווח סנכרון (דקות)", - "proxmoxSyncIntervalDesc": "מינימום 5 דקות. המתזמן מדלג על סשנים נעולים של משתמשים.", - "proxmoxMarkMissing": "סמן אורחים חסרים", - "proxmoxMarkMissingDesc": "הוסף תג proxmox-missing כאשר אורח שיובא בעבר נעלם במקום למחוק אותו.", - "proxmoxLastSync": "סנכרון אחרון", - "proxmoxLastSyncNever": "עדיין לא מסונכרן", - "proxmoxLastSyncNoResult": "אין פרטי תוצאה", - "proxmoxLastSyncSummary": "{{created}} נוצר, {{updated}} עודכן, {{markedMissing}} חסר, {{skipped}} דילג", - "proxmoxLastSyncStatus": { - "error": "נִכשָׁל", - "pending": "תָלוּי וְעוֹמֵד", - "success": "הַצלָחָה" - }, - "proxmoxDiscoverAction": "גלה וייבא אורחי Proxmox", - "proxmoxImportTitle": "ייבוא מ-Proxmox", - "proxmoxSelectHost": "בחר מארח Proxmox…", - "proxmoxDiscover": "לְגַלוֹת", - "proxmoxDiscovering": "גילוי…", - "proxmoxDiscoverGuests": "גלה אורחים", - "proxmoxSelectAll": "בחר הכל", - "proxmoxDeselectAll": "בטל את הבחירה של הכל", - "proxmoxNoGuests": "לא נמצאו אורחים בצומת Proxmox זה.", - "proxmoxResultImported": "{{count}} מיובא", - "proxmoxResultUpdated": "{{count}} updated", - "proxmoxResultFailed": "{{count}} נכשל", - "proxmoxImportComplete": "ייבוא Proxmox הושלם: {{summary}}", - "proxmoxDiscoveryFailed": "הגילוי נכשל", - "proxmoxImportFailed": "הייבוא נכשל", - "docsLink": "הצג מסמכים", - "hostNotFound": "המארח לא נמצא", - "loadingHost": "טוען מארח...", - "none": "אַף לֹא אֶחָד", - "proxmoxGuestsSelected_one": "{{count}} אורח, {{selected}} נבחר", - "proxmoxGuestsSelected_other": "{{count}} אורחים, {{selected}} נבחרו", - "proxmoxImportButton_one": "ייבוא מארח {{count}}", - "proxmoxImportButton_other": "ייבוא מארחים {{count}}" - }, - "terminal": { - "connected": "מְחוּבָּר" - } -} diff --git a/plugins/proxmox/locales/translated/hi_IN.json b/plugins/proxmox/locales/translated/hi_IN.json deleted file mode 100644 index 47af863f0..000000000 --- a/plugins/proxmox/locales/translated/hi_IN.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "प्रॉक्समॉक्स आँकड़े" - }, - "proxmoxStats": { - "tabTitle": "प्रॉक्समॉक्स आँकड़े", - "cpuUsage": "CPU उपयोग", - "memoryUsage": "मेमोरी उपयोग", - "diskUsage": "डिस्क उपयोग", - "networkInterfaces": "नेटवर्क इंटरफ़ेस", - "uptime": "अपटाइम", - "systemInfo": "सिस्टम जानकारी", - "guestsSummary": "अतिथियों", - "storagePools": "भंडारण पूल", - "clusterHealth": "क्लस्टर स्वास्थ्य", - "cpuCores_one": "{{count}} कोर", - "cpuCores_other": "{{count}} कोर", - "used": "प्रयुक्त", - "total": "कुल", - "hostname": "होस्टनाम", - "kernel": "कर्नेल", - "pveVersion": "PVE संस्करण", - "name": "नाम", - "status": "स्थिति", - "type": "प्रकार", - "cpu": "CPU", - "mem": "मेम", - "disk": "डिस्क", - "running": "चालू", - "stopped": "बंद", - "guestCounts": "{{running}}/{{total}} चल रहा है", - "inactive": "निष्क्रिय", - "notClustered": "स्टैंडअलोन नोड (क्लस्टर का हिस्सा नहीं)", - "quorate": "कोरेट", - "notQuorate": "निर्धारित संख्या पूरी नहीं हुई", - "nodeOnline": "ऑनलाइन", - "nodeOffline": "ऑफलाइन", - "noHostSelected": "कोई होस्ट चयनित नहीं है", - "pveshNotFound": "pvesh नहीं मिला - क्या यह Proxmox नोड है?", - "nodeDetectionFailed": "वैध प्रोक्समॉक्स नोड नाम निर्धारित करने में असमर्थ", - "connecting": "कनेक्ट हो रहा है...", - "connectionFailed": "Proxmox नोड से कनेक्ट करने में विफल", - "refresh": "ताज़ा करें", - "searchGuests": "अतिथियों को खोजें...", - "filterAll": "सभी", - "filterVm": "वीएम", - "filterLxc": "एलएक्ससी", - "noGuests": "इस नोड पर कोई वीएम या एलएक्ससी कंटेनर नहीं मिला।", - "noGuestsMatch": "वर्तमान फ़िल्टरों से कोई अतिथि मेल नहीं खाता।" - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Proxmox सांख्यिकी सक्षम करें", - "enableProxmoxStatsDesc": "इस होस्ट के लिए एक प्रोक्समॉक्स स्टैट्स टैब दिखाएं जिसमें एसएसएच के माध्यम से एकत्रित किए गए लाइव नोड, गेस्ट, स्टोरेज और क्लस्टर स्टैट्स शामिल हों।", - "proxmoxStatsPollInterval": "मतदान अंतराल (सेकंड)", - "proxmoxStatsPollIntervalDesc": "कम से कम 15 सेकंड। प्रोक्समॉक्स नोड के आंकड़ों को कितनी बार रीफ्रेश करना है?", - "proxmoxStatsNodeOverride": "नोड नाम ओवरराइड", - "proxmoxStatsNodeOverrideDesc": "Proxmox नोड नाम की स्वतः पहचान को ओवरराइड करें (जो डिफ़ॉल्ट रूप से होस्टनाम के माध्यम से पता लगाया जाता है)।", - "proxmoxStatsNodeOverridePlaceholder": "होस्टनाम से स्वतः पता लगाया गया", - "authTypeStepca": "स्टेप सीए", - "proxmoxIntegration": "Proxmox एकीकरण", - "enableProxmox": "Proxmox सक्षम करें", - "enableProxmoxDesc": "इस होस्ट को Proxmox नोड के रूप में चिह्नित करें। इस होस्ट से सीधे अतिथि खोज और आयात सक्षम करता है।", - "proxmoxDefaultAuthType": "डिफ़ॉल्ट प्रमाणीकरण प्रकार", - "proxmoxDefaultAuthTypeDesc": "आयातित अतिथि होस्ट पर लागू प्रमाणीकरण विधि। वह प्रमाणीकरण प्रकार चुनें जो आपके अतिथियों से कनेक्ट करने के तरीके से मेल खाता हो।", - "authTypePassword": "पासवर्ड", - "authTypeKey": "SSH कुंजी", - "authTypeCredential": "क्रेडेंशियल", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "कोई नहीं", - "proxmoxDefaultCredential": "डिफ़ॉल्ट क्रेडेंशियल", - "proxmoxDefaultCredentialDesc": "जब प्रमाणीकरण प्रकार 'क्रेडेंशियल' पर सेट होता है तब आयातित अतिथि होस्ट के लिए उपयोग किया जाने वाला क्रेडेंशियल।", - "proxmoxWindowsDetection": "Windows / RDP पहचान", - "proxmoxWindowsDetectionDesc": "अल्पविराम से अलग नाम पैटर्न जो SSH के बजाय RDP ट्रिगर करते हैं (अक्षर-आकार असंवेदी)", - "proxmoxDockerDetection": "Docker पहचान", - "proxmoxDockerDetectionDesc": "अल्पविराम से अलग नाम पैटर्न जो मिलान वाले अतिथियों के लिए Docker सक्षम करते हैं", - "proxmoxPreferredRanges": "पसंदीदा IP श्रेणियाँ", - "proxmoxPreferredRangesDesc": "अतिथि के पास कई इंटरफ़ेस होने पर IP चयन के लिए प्राथमिकता क्रम में अल्पविराम से अलग उपसर्ग", - "proxmoxAutoSync": "अतिथि स्वतः सिंक", - "proxmoxAutoSyncDesc": "जब आपका सत्र अनलॉक हो तो समय-समय पर इस Proxmox नोड की खोज करें और आयातित अतिथि होस्ट बनाएँ या अपडेट करें।", - "proxmoxSyncInterval": "सिंक अंतराल (मिनट)", - "proxmoxSyncIntervalDesc": "न्यूनतम 5 मिनट। शेड्यूलर लॉक उपयोगकर्ता सत्रों को छोड़ देता है।", - "proxmoxMarkMissing": "गायब अतिथियों को चिह्नित करें", - "proxmoxMarkMissingDesc": "जब पहले से आयातित कोई अतिथि गायब हो जाए तो उसे हटाने के बजाय एक 'proxmox-missing' टैग जोड़ें।", - "proxmoxLastSync": "अंतिम सिंक", - "proxmoxLastSyncNever": "अभी तक सिंक नहीं हुआ", - "proxmoxLastSyncNoResult": "कोई परिणाम विवरण नहीं", - "proxmoxLastSyncSummary": "{{created}} बनाए गए, {{updated}} अपडेट किए गए, {{markedMissing}} गायब, {{skipped}} छोड़े गए", - "proxmoxLastSyncStatus": { - "error": "विफल", - "pending": "लंबित", - "success": "सफलता" - }, - "proxmoxDiscoverAction": "Proxmox अतिथियों की खोज करें और आयात करें", - "proxmoxImportTitle": "Proxmox से आयात करें", - "proxmoxSelectHost": "एक Proxmox होस्ट चुनें…", - "proxmoxDiscover": "खोजें", - "proxmoxDiscovering": "खोज रहा है…", - "proxmoxDiscoverGuests": "अतिथियों की खोज करें", - "proxmoxSelectAll": "सभी चुनें", - "proxmoxDeselectAll": "सभी अचयनित करें", - "proxmoxNoGuests": "इस Proxmox नोड पर कोई अतिथि नहीं मिला।", - "proxmoxResultImported": "{{count}} आयातित", - "proxmoxResultUpdated": "{{count}} अद्यतित", - "proxmoxResultFailed": "{{count}} विफल", - "proxmoxImportComplete": "Proxmox आयात पूर्ण: {{summary}}", - "proxmoxDiscoveryFailed": "डिस्कवरी विफल", - "proxmoxImportFailed": "आयात विफल", - "docsLink": "दस्तावेज़ देखें", - "hostNotFound": "होस्ट नहीं मिला", - "loadingHost": "होस्ट लोड हो रहा है...", - "none": "कोई नहीं", - "proxmoxGuestsSelected_one": "{{count}} अतिथि, {{selected}} चयनित", - "proxmoxGuestsSelected_other": "{{count}} अतिथि, {{selected}} चयनित", - "proxmoxImportButton_one": "{{count}} होस्ट आयात करें", - "proxmoxImportButton_other": "{{count}} होस्ट आयात करें" - }, - "terminal": { - "connected": "कनेक्टेड" - } -} diff --git a/plugins/proxmox/locales/translated/hu_HU.json b/plugins/proxmox/locales/translated/hu_HU.json deleted file mode 100644 index c778ce3af..000000000 --- a/plugins/proxmox/locales/translated/hu_HU.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox statisztikák" - }, - "proxmoxStats": { - "tabTitle": "Proxmox statisztikák", - "cpuUsage": "CPU-használat", - "memoryUsage": "Memóriahasználat", - "diskUsage": "Lemezhasználat", - "networkInterfaces": "Hálózati interfészek", - "uptime": "Üzemidő", - "systemInfo": "Rendszerinformációk", - "guestsSummary": "Vendégek", - "storagePools": "Tárolómedencék", - "clusterHealth": "Klaszter Egészségügy", - "cpuCores_one": "{{count}} Mag", - "cpuCores_other": "{{count}} Magok", - "used": "Használt", - "total": "Teljes", - "hostname": "Gazdagépnév", - "kernel": "Kernel", - "pveVersion": "PVE verzió", - "name": "Név", - "status": "Állapot", - "type": "Típus", - "cpu": "CPU", - "mem": "Mem", - "disk": "Korong", - "running": "Futás", - "stopped": "Leállítva", - "guestCounts": "{{running}}/{{total}} fut", - "inactive": "Inaktív", - "notClustered": "Önálló csomópont (nem része fürtnek)", - "quorate": "Kvórat", - "notQuorate": "Nem határozatképes", - "nodeOnline": "Online", - "nodeOffline": "Offline", - "noHostSelected": "Nincs kiválasztva gazdagép", - "pveshNotFound": "pvesh nem található - ez egy Proxmox csomópont?", - "nodeDetectionFailed": "Nem sikerült érvényes Proxmox csomópontnevet meghatározni.", - "connecting": "Kapcsolódás...", - "connectionFailed": "Nem sikerült csatlakozni a Proxmox csomóponthoz", - "refresh": "Frissítés", - "searchGuests": "Vendégek keresése...", - "filterAll": "Minden", - "filterVm": "Virtuális gép", - "filterLxc": "LXC", - "noGuests": "Nem találhatók virtuális gépek vagy LXC konténerek ezen a csomóponton.", - "noGuestsMatch": "Egyik vendég sem felel meg a jelenlegi szűrőknek." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Proxmox statisztikák engedélyezése", - "enableProxmoxStatsDesc": "Proxmox Statisztika fül megjelenítése ehhez a gazdagéphez, élő csomópont-, vendég-, tároló- és fürtstatisztikákkal, SSH-n keresztül gyűjtött adatokkal.", - "proxmoxStatsPollInterval": "Lekérdezési intervallum (másodperc)", - "proxmoxStatsPollIntervalDesc": "Minimum 15 másodperc. A Proxmox csomópont statisztikáinak frissítési gyakorisága.", - "proxmoxStatsNodeOverride": "Csomópont nevének felülbírálása", - "proxmoxStatsNodeOverrideDesc": "A Proxmox csomópont nevének automatikus észlelésének felülbírálása (alapértelmezés szerint a gazdagépnév alapján észlelhető).", - "proxmoxStatsNodeOverridePlaceholder": "Automatikusan felismerve a gazdagépnév alapján", - "authTypeStepca": "CA lépés", - "proxmoxIntegration": "Proxmox integráció", - "enableProxmox": "Proxmox engedélyezése", - "enableProxmoxDesc": "Jelölje meg ezt a gépet Proxmox csomópontként. Engedélyezi a vendégfelderítést és az importálást közvetlenül erről a gépről.", - "proxmoxDefaultAuthType": "Alapértelmezett hitelesítési típus", - "proxmoxDefaultAuthTypeDesc": "Hitelesítési módszer alkalmazva az importált vendéggazdagépekre. Válassza ki azt a hitelesítési típust, amelyik megfelel a vendéggazdagépekhez való csatlakozás módjának.", - "authTypePassword": "Jelszó", - "authTypeKey": "SSH-kulcs", - "authTypeCredential": "Hitelesítő adat", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Egyik sem", - "proxmoxDefaultCredential": "Alapértelmezett hitelesítő adat", - "proxmoxDefaultCredentialDesc": "Az importált vendéggazdagépekhez használt hitelesítő adat, ha a hitelesítési típus Hitelesítő adatra van állítva.", - "proxmoxWindowsDetection": "Windows/RDP-észlelés", - "proxmoxWindowsDetectionDesc": "Vesszővel elválasztott névminták, amelyek RDP-t indítanak el SSH helyett (kis- és nagybetűket nem megkülönböztető)", - "proxmoxDockerDetection": "Docker-észlelés", - "proxmoxDockerDetectionDesc": "Vesszővel elválasztott névminták, amelyek lehetővé teszik a Docker számára a vendégek egyezését", - "proxmoxPreferredRanges": "Előnyben részesített IP-tartományok", - "proxmoxPreferredRangesDesc": "Vesszővel elválasztott előtagok prioritási sorrendben IP-cím kiválasztásához, ha egy vendégnek több interfésze van", - "proxmoxAutoSync": "Vendégek automatikus szinkronizálása", - "proxmoxAutoSyncDesc": "Rendszeresen fedezze fel ezt a Proxmox csomópontot, és hozzon létre vagy frissítsen importált vendéggazdagépeket, miközben a munkamenet fel van oldva.", - "proxmoxSyncInterval": "Szinkronizálási időköz (perc)", - "proxmoxSyncIntervalDesc": "Minimum 5 perc. Az ütemező kihagyja a zárolt felhasználói munkameneteket.", - "proxmoxMarkMissing": "Jelöld meg az eltűnt vendégeket", - "proxmoxMarkMissingDesc": "Hozzáad egy proxmox-missing címkét, amikor egy korábban importált vendég eltűnik, ahelyett, hogy törölné azt.", - "proxmoxLastSync": "Utolsó szinkronizálás", - "proxmoxLastSyncNever": "Még nincs szinkronizálva", - "proxmoxLastSyncNoResult": "Nincsenek találati részletek", - "proxmoxLastSyncSummary": "{{created}} létrehozva, {{updated}} frissítve, {{markedMissing}} hiányzik, {{skipped}} kihagyva", - "proxmoxLastSyncStatus": { - "error": "Sikertelen", - "pending": "Függőben levő", - "success": "Siker" - }, - "proxmoxDiscoverAction": "Proxmox vendégek felfedezése és importálása", - "proxmoxImportTitle": "Importálás a Proxmoxból", - "proxmoxSelectHost": "Válasszon egy Proxmox hosztot…", - "proxmoxDiscover": "Felfedezés", - "proxmoxDiscovering": "Felfedezés…", - "proxmoxDiscoverGuests": "Fedezze fel a vendégeket", - "proxmoxSelectAll": "Összes kijelölése", - "proxmoxDeselectAll": "Összes kijelölés törlése", - "proxmoxNoGuests": "Nem találhatók vendégek ezen a Proxmox csomóponton.", - "proxmoxResultImported": "{{count}} importálva", - "proxmoxResultUpdated": "{{count}} frissítve", - "proxmoxResultFailed": "{{count}} sikertelen", - "proxmoxImportComplete": "Proxmox importálás kész: {{summary}}", - "proxmoxDiscoveryFailed": "A felfedezés sikertelen", - "proxmoxImportFailed": "Importálás sikertelen", - "docsLink": "Dokumentumok megtekintése", - "hostNotFound": "Nem található gazdagép", - "loadingHost": "Gazdagép betöltése...", - "none": "Egyik sem", - "proxmoxGuestsSelected_one": "{{count}} vendég, {{selected}} kiválasztva", - "proxmoxGuestsSelected_other": "{{count}} vendég, {{selected}} kiválasztva", - "proxmoxImportButton_one": "Importálja a {{count}} gazdagépet", - "proxmoxImportButton_other": "{{count}} hosztok importálása" - }, - "terminal": { - "connected": "Csatlakoztatva" - } -} diff --git a/plugins/proxmox/locales/translated/id_ID.json b/plugins/proxmox/locales/translated/id_ID.json deleted file mode 100644 index 1a2d6a7d4..000000000 --- a/plugins/proxmox/locales/translated/id_ID.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Statistik Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Statistik Proxmox", - "cpuUsage": "Penggunaan CPU", - "memoryUsage": "Penggunaan Memori", - "diskUsage": "Penggunaan Disk", - "networkInterfaces": "Antarmuka Jaringan", - "uptime": "Waktu aktif", - "systemInfo": "Informasi Sistem", - "guestsSummary": "Tamu", - "storagePools": "Kolam Penyimpanan", - "clusterHealth": "Kesehatan Klaster", - "cpuCores_one": "{{count}} Inti", - "cpuCores_other": "{{count}} Inti", - "used": "Digunakan", - "total": "Total", - "hostname": "Nama host", - "kernel": "Inti", - "pveVersion": "Versi PVE", - "name": "Nama", - "status": "Status", - "type": "Jenis", - "cpu": "CPU", - "mem": "Mem", - "disk": "Disk", - "running": "Berlari", - "stopped": "Berhenti", - "guestCounts": "{{running}}/{{total}} berjalan", - "inactive": "Tidak aktif", - "notClustered": "Node mandiri (bukan bagian dari klaster)", - "quorate": "Kutipan", - "notQuorate": "Tidak memenuhi kuorum", - "nodeOnline": "On line", - "nodeOffline": "Offline", - "noHostSelected": "Tidak ada pembawa acara yang dipilih.", - "pveshNotFound": "pvesh tidak ditemukan - apakah ini node Proxmox?", - "nodeDetectionFailed": "Tidak dapat menentukan nama node Proxmox yang valid.", - "connecting": "Menghubungkan...", - "connectionFailed": "Gagal terhubung ke node Proxmox", - "refresh": "Menyegarkan", - "searchGuests": "Mencari tamu...", - "filterAll": "Semua", - "filterVm": "Mesin virtual", - "filterLxc": "LXC", - "noGuests": "Tidak ditemukan VM atau kontainer LXC pada node ini.", - "noGuestsMatch": "Tidak ada tamu yang sesuai dengan filter saat ini." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Aktifkan Statistik Proxmox", - "enableProxmoxStatsDesc": "Tampilkan tab Statistik Proxmox untuk host ini dengan statistik node, guest, penyimpanan, dan cluster secara langsung yang dikumpulkan melalui SSH.", - "proxmoxStatsPollInterval": "Interval polling (detik)", - "proxmoxStatsPollIntervalDesc": "Minimal 15 detik. Seberapa sering statistik node Proxmox diperbarui.", - "proxmoxStatsNodeOverride": "Penggantian nama node", - "proxmoxStatsNodeOverrideDesc": "Ganti deteksi otomatis nama node Proxmox (yang secara default dideteksi melalui nama host).", - "proxmoxStatsNodeOverridePlaceholder": "Terdeteksi secara otomatis dari nama host", - "authTypeStepca": "Langkah CA", - "proxmoxIntegration": "Integrasi Proxmox", - "enableProxmox": "Aktifkan Proxmox", - "enableProxmoxDesc": "Tandai host ini sebagai node Proxmox. Memungkinkan penemuan dan impor guest langsung dari host ini.", - "proxmoxDefaultAuthType": "Jenis Otentikasi Default", - "proxmoxDefaultAuthTypeDesc": "Metode otentikasi yang diterapkan pada host tamu yang diimpor. Pilih jenis otentikasi yang sesuai dengan cara Anda terhubung dengan tamu Anda.", - "authTypePassword": "Kata sandi", - "authTypeKey": "Kunci SSH", - "authTypeCredential": "Mandat", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Tidak ada", - "proxmoxDefaultCredential": "Kredensial Default", - "proxmoxDefaultCredentialDesc": "Kredensial yang digunakan untuk host tamu yang diimpor ketika tipe otentikasi diatur ke Kredensial.", - "proxmoxWindowsDetection": "Deteksi Windows / RDP", - "proxmoxWindowsDetectionDesc": "Pola nama yang dipisahkan koma yang memicu RDP alih-alih SSH (tidak peka huruf besar/kecil)", - "proxmoxDockerDetection": "Deteksi Docker", - "proxmoxDockerDetectionDesc": "Pola nama yang dipisahkan koma yang memungkinkan Docker untuk mencocokkan mesin virtual.", - "proxmoxPreferredRanges": "Rentang IP yang disukai", - "proxmoxPreferredRangesDesc": "Awalan yang dipisahkan koma dalam urutan prioritas untuk pemilihan IP ketika tamu memiliki beberapa antarmuka.", - "proxmoxAutoSync": "Sinkronisasi otomatis tamu", - "proxmoxAutoSyncDesc": "Secara berkala, temukan node Proxmox ini dan buat atau perbarui host tamu yang diimpor saat sesi Anda tidak terkunci.", - "proxmoxSyncInterval": "Interval sinkronisasi (menit)", - "proxmoxSyncIntervalDesc": "Minimal 5 menit. Penjadwal akan melewati sesi pengguna yang terkunci.", - "proxmoxMarkMissing": "Tandai tamu yang hilang", - "proxmoxMarkMissingDesc": "Tambahkan tag proxmox-missing ketika tamu yang sebelumnya diimpor menghilang, alih-alih menghapusnya.", - "proxmoxLastSync": "Sinkronisasi terakhir", - "proxmoxLastSyncNever": "Belum disinkronkan", - "proxmoxLastSyncNoResult": "Tidak ada detail hasil.", - "proxmoxLastSyncSummary": "{{created}} dibuat, {{updated}} diperbarui, {{markedMissing}} hilang, {{skipped}} dilewati", - "proxmoxLastSyncStatus": { - "error": "Gagal", - "pending": "Tertunda", - "success": "Kesuksesan" - }, - "proxmoxDiscoverAction": "Temukan & impor tamu Proxmox", - "proxmoxImportTitle": "Impor dari Proxmox", - "proxmoxSelectHost": "Pilih host Proxmox…", - "proxmoxDiscover": "Menemukan", - "proxmoxDiscovering": "Menemukan…", - "proxmoxDiscoverGuests": "Temukan tamu", - "proxmoxSelectAll": "Pilih semua", - "proxmoxDeselectAll": "Batalkan pilihan semua", - "proxmoxNoGuests": "Tidak ada tamu yang ditemukan di node Proxmox ini.", - "proxmoxResultImported": "{{count}} diimpor", - "proxmoxResultUpdated": "{{count}} diperbarui", - "proxmoxResultFailed": "{{count}} gagal", - "proxmoxImportComplete": "Impor Proxmox selesai: {{summary}}", - "proxmoxDiscoveryFailed": "Penemuan gagal", - "proxmoxImportFailed": "Impor gagal", - "docsLink": "Lihat dokumen", - "hostNotFound": "Host tidak ditemukan", - "loadingHost": "Memuat host...", - "none": "Tidak ada", - "proxmoxGuestsSelected_one": "{{count}} tamu, {{selected}} terpilih", - "proxmoxGuestsSelected_other": "{{count}} tamu, {{selected}} terpilih", - "proxmoxImportButton_one": "Impor {{count}} host", - "proxmoxImportButton_other": "Impor {{count}} host" - }, - "terminal": { - "connected": "Terhubung" - } -} diff --git a/plugins/proxmox/locales/translated/it_IT.json b/plugins/proxmox/locales/translated/it_IT.json deleted file mode 100644 index 3d65a8797..000000000 --- a/plugins/proxmox/locales/translated/it_IT.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Statistiche di Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Statistiche di Proxmox", - "cpuUsage": "Utilizzo CPU", - "memoryUsage": "Utilizzo memoria", - "diskUsage": "Utilizzo disco", - "networkInterfaces": "Interfacce di rete", - "uptime": "Uptime", - "systemInfo": "Informazioni di sistema", - "guestsSummary": "Ospiti", - "storagePools": "Piscine di stoccaggio", - "clusterHealth": "Cluster Health", - "cpuCores_one": "{{count}} Core", - "cpuCores_other": "{{count}} Core", - "used": "Usato", - "total": "Totale", - "hostname": "Nome host", - "kernel": "Kernel", - "pveVersion": "Versione PVE", - "name": "Nome", - "status": "Stato", - "type": "Tipo", - "cpu": "CPU", - "mem": "Mem", - "disk": "Disco", - "running": "In esecuzione", - "stopped": "Arrestato", - "guestCounts": "{{running}}/{{total}} in esecuzione", - "inactive": "Inattivo", - "notClustered": "Nodo autonomo (non facente parte di un cluster)", - "quorate": "Citazione", - "notQuorate": "Non è stato raggiunto il quorum", - "nodeOnline": "Online", - "nodeOffline": "Offline", - "noHostSelected": "Nessun host selezionato", - "pveshNotFound": "pvesh non trovato: si tratta di un nodo Proxmox?", - "nodeDetectionFailed": "Impossibile determinare un nome nodo Proxmox valido", - "connecting": "Connessione in corso...", - "connectionFailed": "Impossibile connettersi al nodo Proxmox", - "refresh": "Aggiorna", - "searchGuests": "Cerca ospiti...", - "filterAll": "Tutte", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "Nessuna macchina virtuale o container LXC trovato su questo nodo.", - "noGuestsMatch": "Nessun ospite corrisponde ai filtri attualmente attivi." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Abilita le statistiche di Proxmox", - "enableProxmoxStatsDesc": "Mostra la scheda Statistiche di Proxmox per questo host con statistiche in tempo reale su nodi, guest, storage e cluster, raccolte tramite SSH.", - "proxmoxStatsPollInterval": "Intervallo di polling (secondi)", - "proxmoxStatsPollIntervalDesc": "Minimo 15 secondi. Frequenza di aggiornamento delle statistiche dei nodi Proxmox.", - "proxmoxStatsNodeOverride": "Sovrascrittura del nome del nodo", - "proxmoxStatsNodeOverrideDesc": "Ignora il rilevamento automatico del nome del nodo Proxmox (rilevato tramite nome host per impostazione predefinita).", - "proxmoxStatsNodeOverridePlaceholder": "Rilevato automaticamente dal nome host", - "authTypeStepca": "Passo CA", - "proxmoxIntegration": "Integrazione Proxmox", - "enableProxmox": "Abilita Proxmox", - "enableProxmoxDesc": "Contrassegna questo host come nodo Proxmox. Abilita il discovery e l'importazione di guest direttamente da questo host.", - "proxmoxDefaultAuthType": "Tipo di autenticazione predefinito", - "proxmoxDefaultAuthTypeDesc": "Metodo di autenticazione applicato agli host guest importati. Scegli il tipo di autenticazione corrispondente a come ti connetti ai tuoi guest.", - "authTypePassword": "Password", - "authTypeKey": "Chiave SSH", - "authTypeCredential": "Credenziale", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Nessuno", - "proxmoxDefaultCredential": "Credenziale predefinita", - "proxmoxDefaultCredentialDesc": "Credenziale utilizzata per gli host guest importati quando il tipo di autenticazione è impostato su Credenziale.", - "proxmoxWindowsDetection": "Rilevamento Windows / RDP", - "proxmoxWindowsDetectionDesc": "Elenco di pattern di nomi separati da virgole che attivano RDP invece di SSH (case‑insensitive)", - "proxmoxDockerDetection": "Rilevamento Docker", - "proxmoxDockerDetectionDesc": "Elenco di pattern di nomi separati da virgole che abilitano Docker per i guest corrispondenti", - "proxmoxPreferredRanges": "Intervalli IP preferiti", - "proxmoxPreferredRangesDesc": "Prefissi separati da virgole in ordine di priorità per la selezione dell'IP quando un guest ha più interfacce", - "proxmoxAutoSync": "Sincronizzazione automatica dei guest", - "proxmoxAutoSyncDesc": "Scopri periodicamente questo nodo Proxmox e crea o aggiorna gli host guest importati mentre la sessione è sbloccata.", - "proxmoxSyncInterval": "Intervallo di sincronizzazione (minuti)", - "proxmoxSyncIntervalDesc": "Minimo 5 minuti. La pianificazione salta le sessioni utente bloccate.", - "proxmoxMarkMissing": "Segna i guest mancanti", - "proxmoxMarkMissingDesc": "Aggiungi un tag proxmox-missing quando un guest precedentemente importato scompare invece di eliminarlo.", - "proxmoxLastSync": "Ultima sincronizzazione", - "proxmoxLastSyncNever": "Non ancora sincronizzato", - "proxmoxLastSyncNoResult": "Nessun dettaglio del risultato", - "proxmoxLastSyncSummary": "{{created}} creati, {{updated}} aggiornati, {{markedMissing}} mancanti, {{skipped}} saltati", - "proxmoxLastSyncStatus": { - "error": "Fallita", - "pending": "In attesa", - "success": "Riuscita" - }, - "proxmoxDiscoverAction": "Scopri e importa guest Proxmox", - "proxmoxImportTitle": "Importa da Proxmox", - "proxmoxSelectHost": "Seleziona un host Proxmox…", - "proxmoxDiscover": "Scopri", - "proxmoxDiscovering": "Scoperta in corso…", - "proxmoxDiscoverGuests": "Scopri guest", - "proxmoxSelectAll": "Seleziona tutto", - "proxmoxDeselectAll": "Deseleziona tutto", - "proxmoxNoGuests": "Nessun guest trovato su questo nodo Proxmox.", - "proxmoxResultImported": "{{count}} importati", - "proxmoxResultUpdated": "{{count}} aggiornati", - "proxmoxResultFailed": "{{count}} falliti", - "proxmoxImportComplete": "Importazione Proxmox completata: {{summary}}", - "proxmoxDiscoveryFailed": "Rilevamento non riuscito", - "proxmoxImportFailed": "Importazione non riuscita", - "docsLink": "Visualizza documentazione", - "hostNotFound": "Host non trovato", - "loadingHost": "Caricamento host...", - "none": "Nessuna", - "proxmoxGuestsSelected_one": "{{count}} ospite, {{selected}} selezionato", - "proxmoxGuestsSelected_other": "{{count}} ospiti, {{selected}} selezionati", - "proxmoxImportButton_one": "Importa {{count}} host", - "proxmoxImportButton_other": "Importa {{count}} host" - }, - "terminal": { - "connected": "Connesso" - } -} diff --git a/plugins/proxmox/locales/translated/ja_JP.json b/plugins/proxmox/locales/translated/ja_JP.json deleted file mode 100644 index 79738a230..000000000 --- a/plugins/proxmox/locales/translated/ja_JP.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox統計情報" - }, - "proxmoxStats": { - "tabTitle": "Proxmox統計情報", - "cpuUsage": "CPU使用率", - "memoryUsage": "メモリ使用量", - "diskUsage": "ディスク使用率", - "networkInterfaces": "ネットワークインターフェース", - "uptime": "アップタイム", - "systemInfo": "システム情報", - "guestsSummary": "ゲスト", - "storagePools": "ストレージプール", - "clusterHealth": "クラスターヘルス", - "cpuCores_one": "{{count}} コア", - "cpuCores_other": "{{count}} コア", - "used": "使用量", - "total": "合計", - "hostname": "ホスト名", - "kernel": "カーネル", - "pveVersion": "PVEバージョン", - "name": "名前", - "status": "状態", - "type": "タイプ", - "cpu": "CPU", - "mem": "メモリ", - "disk": "ディスク", - "running": "実行中", - "stopped": "停止中", - "guestCounts": "{{running}}/{{total}} 実行中", - "inactive": "非アクティブ", - "notClustered": "スタンドアロンノード(クラスタの一部ではない)", - "quorate": "引用", - "notQuorate": "定足数に満たない", - "nodeOnline": "オンライン", - "nodeOffline": "オフライン", - "noHostSelected": "ホストが選択されていません", - "pveshNotFound": "pveshが見つかりません - これはProxmoxノードですか?", - "nodeDetectionFailed": "有効な Proxmox ノード名を特定できません", - "connecting": "接続中...", - "connectionFailed": "Proxmoxノードへの接続に失敗しました", - "refresh": "再読み込み", - "searchGuests": "ゲストを検索...", - "filterAll": "すべて選択", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "このノード上にはVMまたはLXCコンテナは見つかりませんでした。", - "noGuestsMatch": "現在の検索条件に一致するゲストはいません。" - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Proxmox Statsを有効にする", - "enableProxmoxStatsDesc": "SSH経由で収集された、このホストのリアルタイムのノード、ゲスト、ストレージ、およびクラスタの統計情報を表示するProxmox統計タブを表示します。", - "proxmoxStatsPollInterval": "ポーリング間隔(秒)", - "proxmoxStatsPollIntervalDesc": "最低15秒。Proxmoxノードの統計情報を更新する頻度。", - "proxmoxStatsNodeOverride": "ノード名の上書き", - "proxmoxStatsNodeOverrideDesc": "Proxmoxノード名の自動検出(デフォルトではホスト名で検出)を上書きします。", - "proxmoxStatsNodeOverridePlaceholder": "ホスト名から自動検出", - "authTypeStepca": "ステップCA", - "proxmoxIntegration": "Proxmox統合", - "enableProxmox": "Proxmoxを有効にする", - "enableProxmoxDesc": "このホストをProxmoxノードとしてマークします。ゲストの検出とインポートをこのホストから直接行えるようにします。", - "proxmoxDefaultAuthType": "デフォルト認証タイプ", - "proxmoxDefaultAuthTypeDesc": "インポートされたゲストホストに適用される認証方式です。ゲストへの接続方法に合わせて認証タイプを選択してください。", - "authTypePassword": "パスワード", - "authTypeKey": "SSH鍵", - "authTypeCredential": "クレデンシャル", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "なし", - "proxmoxDefaultCredential": "デフォルト認証情報", - "proxmoxDefaultCredentialDesc": "認証タイプが「Credential」に設定されている場合に、インポートされたゲストホストに使用される認証情報。", - "proxmoxWindowsDetection": "Windows / RDP 検出", - "proxmoxWindowsDetectionDesc": "SSH の代わりに RDP をトリガーする、カンマ区切りの名前パターン(大文字と小文字を区別しない)", - "proxmoxDockerDetection": "Docker 検出", - "proxmoxDockerDetectionDesc": "一致するゲストに対して Docker を有効にする、カンマ区切りの名前パターン", - "proxmoxPreferredRanges": "優先 IP 範囲", - "proxmoxPreferredRangesDesc": "ゲストが複数のインターフェースを持つ場合の IP 選択の優先順位で指定する、カンマ区切りのプレフィックス", - "proxmoxAutoSync": "ゲストの自動同期", - "proxmoxAutoSyncDesc": "セッションがロックされていないときに、この Proxmox ノードを定期的に検出し、インポートされたゲストホストを作成または更新します。", - "proxmoxSyncInterval": "同期間隔(分)", - "proxmoxSyncIntervalDesc": "最小 5 分。スケジューラーはロックされたユーザーセッションをスキップします。", - "proxmoxMarkMissing": "欠落ゲストをマーク", - "proxmoxMarkMissingDesc": "以前にインポートされたゲストが消えたときに、削除する代わりに proxmox-missing タグを追加します。", - "proxmoxLastSync": "最終同期", - "proxmoxLastSyncNever": "未同期", - "proxmoxLastSyncNoResult": "結果の詳細なし", - "proxmoxLastSyncSummary": "{{created}} 作成、{{updated}} 更新、{{markedMissing}} 欠落、{{skipped}} スキップ", - "proxmoxLastSyncStatus": { - "error": "失敗", - "pending": "保留中", - "success": "成功" - }, - "proxmoxDiscoverAction": "Proxmox ゲストの検出とインポート", - "proxmoxImportTitle": "Proxmox からインポート", - "proxmoxSelectHost": "Proxmox ホストを選択…", - "proxmoxDiscover": "検出", - "proxmoxDiscovering": "検出中…", - "proxmoxDiscoverGuests": "ゲストを検出", - "proxmoxSelectAll": "すべて選択", - "proxmoxDeselectAll": "すべて選択解除", - "proxmoxNoGuests": "この Proxmox ノードにゲストが見つかりません。", - "proxmoxResultImported": "{{count}} インポート済み", - "proxmoxResultUpdated": "{{count}} 更新済み", - "proxmoxResultFailed": "{{count}} 失敗", - "proxmoxImportComplete": "Proxmox インポート完了: {{summary}}", - "proxmoxDiscoveryFailed": "検出に失敗しました", - "proxmoxImportFailed": "インポートに失敗しました", - "docsLink": "ドキュメントを表示", - "hostNotFound": "ホストが見つかりません", - "loadingHost": "ホストを読み込み中...", - "none": "なし", - "proxmoxGuestsSelected_one": "{{count}} ゲスト、 {{selected}} 選択済み", - "proxmoxGuestsSelected_other": "{{count}} ゲスト、 {{selected}} 選択済み", - "proxmoxImportButton_one": "{{count}} ホストをインポート", - "proxmoxImportButton_other": "{{count}} 台のホストをインポート" - }, - "terminal": { - "connected": "接続中" - } -} diff --git a/plugins/proxmox/locales/translated/ko_KR.json b/plugins/proxmox/locales/translated/ko_KR.json deleted file mode 100644 index 1ba89061c..000000000 --- a/plugins/proxmox/locales/translated/ko_KR.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "프록스목스 통계" - }, - "proxmoxStats": { - "tabTitle": "프록스목스 통계", - "cpuUsage": "CPU 사용률", - "memoryUsage": "메모리 사용률", - "diskUsage": "디스크 사용률", - "networkInterfaces": "네트워크 인터페이스", - "uptime": "업타임", - "systemInfo": "시스템 정보", - "guestsSummary": "손님", - "storagePools": "저장 풀", - "clusterHealth": "클러스터 헬스", - "cpuCores_one": "{{count}} 코어", - "cpuCores_other": "{{count}} 코어", - "used": "사용", - "total": "총", - "hostname": "호스트명", - "kernel": "커널", - "pveVersion": "PVE 버전", - "name": "이름", - "status": "상태", - "type": "유형", - "cpu": "CPU", - "mem": "메모리", - "disk": "디스크", - "running": "실행 중", - "stopped": "중지됨", - "guestCounts": "{{running}}/{{total}} 실행 중", - "inactive": "비활성", - "notClustered": "독립형 노드(클러스터의 일부가 아님)", - "quorate": "할당량", - "notQuorate": "정족수 미달", - "nodeOnline": "온라인", - "nodeOffline": "오프라인", - "noHostSelected": "선택된 호스트가 없습니다", - "pveshNotFound": "pvesh를 찾을 수 없습니다. Proxmox 노드입니까?", - "nodeDetectionFailed": "유효한 Proxmox 노드 이름을 확인할 수 없습니다.", - "connecting": "연결 중...", - "connectionFailed": "Proxmox 노드에 연결하지 못했습니다.", - "refresh": "새로 고침", - "searchGuests": "투숙객 검색...", - "filterAll": "모두", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "이 노드에서 VM 또는 LXC 컨테이너를 찾을 수 없습니다.", - "noGuestsMatch": "현재 필터 조건에 맞는 투숙객이 없습니다." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Proxmox 통계 활성화", - "enableProxmoxStatsDesc": "SSH를 통해 수집된 실시간 노드, 게스트, 스토리지 및 클러스터 통계를 포함하는 이 호스트의 Proxmox 통계 탭을 표시합니다.", - "proxmoxStatsPollInterval": "폴링 간격(초)", - "proxmoxStatsPollIntervalDesc": "최소 15초. Proxmox 노드 통계를 새로 고치는 빈도.", - "proxmoxStatsNodeOverride": "노드 이름 재정의", - "proxmoxStatsNodeOverrideDesc": "Proxmox 노드 이름 자동 감지 기능을 재정의합니다(기본적으로 호스트 이름을 통해 감지됨).", - "proxmoxStatsNodeOverridePlaceholder": "호스트 이름에서 자동 감지됨", - "authTypeStepca": "스텝 CA", - "proxmoxIntegration": "Proxmox 통합", - "enableProxmox": "Proxmox 활성화", - "enableProxmoxDesc": "이 호스트를 Proxmox 노드로 표시합니다. 이 호스트에서 직접 게스트를 검색하고 가져올 수 있도록 합니다.", - "proxmoxDefaultAuthType": "기본 인증 유형", - "proxmoxDefaultAuthTypeDesc": "가져온 게스트 호스트에 적용되는 인증 방법입니다. 게스트에 연결하는 방식에 맞는 인증 유형을 선택하세요.", - "authTypePassword": "비밀번호", - "authTypeKey": "SSH 키", - "authTypeCredential": "자격 증명", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "없음", - "proxmoxDefaultCredential": "기본 자격 증명", - "proxmoxDefaultCredentialDesc": "인증 유형이 자격 증명으로 설정된 경우 가져온 게스트 호스트에 사용되는 자격 증명입니다.", - "proxmoxWindowsDetection": "Windows / RDP 감지", - "proxmoxWindowsDetectionDesc": "SSH 대신 RDP를 트리거하는 쉼표로 구분된 이름 패턴(대소문자 구분 안 함)", - "proxmoxDockerDetection": "Docker 감지", - "proxmoxDockerDetectionDesc": "일치하는 게스트에 대해 Docker를 활성화하는 쉼표로 구분된 이름 패턴", - "proxmoxPreferredRanges": "선호하는 IP 범위", - "proxmoxPreferredRangesDesc": "게스트에 여러 인터페이스가 있을 때 IP 선택을 위한 우선 순위 순서의 쉼표로 구분된 접두사", - "proxmoxAutoSync": "게스트 자동 동기화", - "proxmoxAutoSyncDesc": "세션이 잠금 해제되어 있을 때 주기적으로 이 Proxmox 노드를 검색하고 가져온 게스트 호스트를 생성하거나 업데이트합니다.", - "proxmoxSyncInterval": "동기화 간격(분)", - "proxmoxSyncIntervalDesc": "최소 5분. 스케줄러는 잠긴 사용자 세션을 건너뜁니다.", - "proxmoxMarkMissing": "누락된 게스트 표시", - "proxmoxMarkMissingDesc": "이전에 가져온 게스트가 사라지면 삭제 대신 proxmox-missing 태그를 추가합니다.", - "proxmoxLastSync": "마지막 동기화", - "proxmoxLastSyncNever": "아직 동기화되지 않음", - "proxmoxLastSyncNoResult": "결과 세부 정보 없음", - "proxmoxLastSyncSummary": "생성 {{created}}개, 업데이트 {{updated}}개, 누락 {{markedMissing}}개, 건너뜀 {{skipped}}개", - "proxmoxLastSyncStatus": { - "error": "실패", - "pending": "보류 중", - "success": "성공" - }, - "proxmoxDiscoverAction": "Proxmox 게스트 검색 및 가져오기", - "proxmoxImportTitle": "Proxmox에서 가져오기", - "proxmoxSelectHost": "Proxmox 호스트 선택…", - "proxmoxDiscover": "검색", - "proxmoxDiscovering": "검색 중…", - "proxmoxDiscoverGuests": "게스트 검색", - "proxmoxSelectAll": "전체 선택", - "proxmoxDeselectAll": "전체 선택 취소", - "proxmoxNoGuests": "이 Proxmox 노드에서 게스트를 찾을 수 없습니다.", - "proxmoxResultImported": "{{count}}개 가져옴", - "proxmoxResultUpdated": "{{count}}개 업데이트됨", - "proxmoxResultFailed": "{{count}}개 실패", - "proxmoxImportComplete": "Proxmox 가져오기 완료: {{summary}}", - "proxmoxDiscoveryFailed": "검색 실패", - "proxmoxImportFailed": "가져오기 실패", - "docsLink": "문서 보기", - "hostNotFound": "호스트를 찾을 수 없습니다", - "loadingHost": "호스트 로드 중...", - "none": "없음", - "proxmoxGuestsSelected_one": "{{count}} 게스트, {{selected}} 선택됨", - "proxmoxGuestsSelected_other": "{{count}} 명의 손님, {{selected}} 명이 선택되었습니다.", - "proxmoxImportButton_one": "호스트 {{count}}개 가져오기", - "proxmoxImportButton_other": "{{count}}개 호스트 가져오기" - }, - "terminal": { - "connected": "연결됨" - } -} diff --git a/plugins/proxmox/locales/translated/nl_NL.json b/plugins/proxmox/locales/translated/nl_NL.json deleted file mode 100644 index 2e1f20e8b..000000000 --- a/plugins/proxmox/locales/translated/nl_NL.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox-statistieken" - }, - "proxmoxStats": { - "tabTitle": "Proxmox-statistieken", - "cpuUsage": "CPU-gebruik", - "memoryUsage": "Geheugengebruik", - "diskUsage": "Schijfgebruik", - "networkInterfaces": "Netwerkinterfaces", - "uptime": "Beschikbaarheid", - "systemInfo": "Systeeminformatie", - "guestsSummary": "Gasten", - "storagePools": "Opslagbassins", - "clusterHealth": "Clustergezondheid", - "cpuCores_one": "{{count}} Kern", - "cpuCores_other": "{{count}} Kernen", - "used": "Gebruikt", - "total": "Totaal", - "hostname": "Hostnaam", - "kernel": "Kernel", - "pveVersion": "PVE-versie", - "name": "Naam", - "status": "Status", - "type": "Type", - "cpu": "CPU", - "mem": "Mem", - "disk": "Schijf", - "running": "Rennen", - "stopped": "Gestopt", - "guestCounts": "{{running}}/{{total}} rennend", - "inactive": "Inactief", - "notClustered": "Zelfstandig knooppunt (geen onderdeel van een cluster)", - "quorate": "Quoraat", - "notQuorate": "Niet quorum", - "nodeOnline": "Online", - "nodeOffline": "Offline", - "noHostSelected": "Geen host geselecteerd", - "pveshNotFound": "pvesh niet gevonden - is dit een Proxmox-node?", - "nodeDetectionFailed": "Kan geen geldige Proxmox-knooppuntnaam bepalen", - "connecting": "Verbinden...", - "connectionFailed": "Verbinding met Proxmox-node mislukt.", - "refresh": "Vernieuwen", - "searchGuests": "Gasten zoeken...", - "filterAll": "Alle", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "Er zijn geen VM's of LXC-containers gevonden op dit knooppunt.", - "noGuestsMatch": "Er zijn geen gasten die aan de huidige filters voldoen." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Proxmox-statistieken inschakelen", - "enableProxmoxStatsDesc": "Toon een tabblad Proxmox-statistieken voor deze host met live statistieken van knooppunten, gastsystemen, opslag en het cluster, verzameld via SSH.", - "proxmoxStatsPollInterval": "Pollinterval (seconden)", - "proxmoxStatsPollIntervalDesc": "Minimaal 15 seconden. Hoe vaak moeten de Proxmox-node-statistieken worden vernieuwd?", - "proxmoxStatsNodeOverride": "Knooppuntnaam overschrijven", - "proxmoxStatsNodeOverrideDesc": "Schakel de automatische detectie van de Proxmox-knooppuntnaam uit (deze wordt standaard gedetecteerd via de hostnaam).", - "proxmoxStatsNodeOverridePlaceholder": "Automatisch gedetecteerd op basis van hostnaam", - "authTypeStepca": "Stap CA", - "proxmoxIntegration": "Proxmox-integratie", - "enableProxmox": "Proxmox inschakelen", - "enableProxmoxDesc": "Markeer deze host als een Proxmox-node. Dit maakt het mogelijk om virtuele machines direct vanaf deze host te detecteren en te importeren.", - "proxmoxDefaultAuthType": "Standaard authenticatietype", - "proxmoxDefaultAuthTypeDesc": "Authenticatiemethode toegepast op geïmporteerde gasthosts. Kies het authenticatietype dat overeenkomt met de manier waarop u verbinding maakt met uw gasten.", - "authTypePassword": "Wachtwoord", - "authTypeKey": "SSH-sleutel", - "authTypeCredential": "Referentie", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Geen", - "proxmoxDefaultCredential": "Standaard inloggegevens", - "proxmoxDefaultCredentialDesc": "De inloggegevens die worden gebruikt voor geïmporteerde gasthosts wanneer het authenticatietype is ingesteld op 'Inloggegevens'.", - "proxmoxWindowsDetection": "Windows-/RDP-detectie", - "proxmoxWindowsDetectionDesc": "Door komma's gescheiden naampatronen die RDP in plaats van SSH activeren (hoofdlettergevoeligheid speelt geen rol).", - "proxmoxDockerDetection": "Docker-detectie", - "proxmoxDockerDetectionDesc": "Door komma's gescheiden naampatronen waarmee Docker gastsystemen kan herkennen.", - "proxmoxPreferredRanges": "Voorkeurs-IP-bereiken", - "proxmoxPreferredRangesDesc": "Door komma's gescheiden voorvoegsels in prioriteitsvolgorde voor IP-selectie wanneer een gast meerdere interfaces heeft.", - "proxmoxAutoSync": "Automatische synchronisatie van gasten", - "proxmoxAutoSyncDesc": "Ontdek periodiek dit Proxmox-knooppunt en maak of update geïmporteerde gasthosts terwijl uw sessie ontgrendeld is.", - "proxmoxSyncInterval": "Synchronisatie-interval (minuten)", - "proxmoxSyncIntervalDesc": "Minimaal 5 minuten. De planner slaat vergrendelde gebruikerssessies over.", - "proxmoxMarkMissing": "Markeer ontbrekende gasten", - "proxmoxMarkMissingDesc": "Voeg een 'proxmox-missing'-tag toe wanneer een eerder geïmporteerde gast verdwijnt, in plaats van deze te verwijderen.", - "proxmoxLastSync": "Laatste synchronisatie", - "proxmoxLastSyncNever": "Nog niet gesynchroniseerd", - "proxmoxLastSyncNoResult": "Geen resultaatdetails", - "proxmoxLastSyncSummary": "{{created}} aangemaakt, {{updated}} bijgewerkt, {{markedMissing}} ontbreekt, {{skipped}} overgeslagen", - "proxmoxLastSyncStatus": { - "error": "Mislukt", - "pending": "In behandeling", - "success": "Succes" - }, - "proxmoxDiscoverAction": "Ontdek en importeer Proxmox-gasten", - "proxmoxImportTitle": "Importeren vanuit Proxmox", - "proxmoxSelectHost": "Selecteer een Proxmox-host…", - "proxmoxDiscover": "Ontdekken", - "proxmoxDiscovering": "Ontdekken…", - "proxmoxDiscoverGuests": "Ontdek gasten", - "proxmoxSelectAll": "Alles selecteren", - "proxmoxDeselectAll": "Alles deselecteren", - "proxmoxNoGuests": "Er zijn geen gasten gevonden op dit Proxmox-knooppunt.", - "proxmoxResultImported": "{{count}} geïmporteerd", - "proxmoxResultUpdated": "{{count}} bijgewerkt", - "proxmoxResultFailed": "{{count}} mislukt", - "proxmoxImportComplete": "Proxmox-import voltooid: {{summary}}", - "proxmoxDiscoveryFailed": "Ontdekking mislukt", - "proxmoxImportFailed": "Importeren mislukt", - "docsLink": "Documenten bekijken", - "hostNotFound": "Host niet gevonden", - "loadingHost": "Host wordt geladen...", - "none": "Geen", - "proxmoxGuestsSelected_one": "{{count}} gast, {{selected}} geselecteerd", - "proxmoxGuestsSelected_other": "{{count}} gasten, {{selected}} geselecteerd", - "proxmoxImportButton_one": "Importeer {{count}} host", - "proxmoxImportButton_other": "Importeer {{count}} hosts" - }, - "terminal": { - "connected": "Aangesloten" - } -} diff --git a/plugins/proxmox/locales/translated/no_NO.json b/plugins/proxmox/locales/translated/no_NO.json deleted file mode 100644 index f15c3a0c0..000000000 --- a/plugins/proxmox/locales/translated/no_NO.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox-statistikk" - }, - "proxmoxStats": { - "tabTitle": "Proxmox-statistikk", - "cpuUsage": "CPU-bruk", - "memoryUsage": "Minnebruk", - "diskUsage": "Diskbruk", - "networkInterfaces": "Nettverksgrensesnitt", - "uptime": "Oppetid", - "systemInfo": "Systeminformasjon", - "guestsSummary": "Gjester", - "storagePools": "Lagringsbassenger", - "clusterHealth": "Klyngehelse", - "cpuCores_one": "{{count}} Kjerne", - "cpuCores_other": "{{count}} Kjerner", - "used": "Brukt", - "total": "Totalt", - "hostname": "Vertsnavn", - "kernel": "Kjerne", - "pveVersion": "PVE-versjon", - "name": "Navn", - "status": "Status", - "type": "Type", - "cpu": "CPU", - "mem": "Minne", - "disk": "Disk", - "running": "Kjører", - "stopped": "Stoppet", - "guestCounts": "{{running}}/{{total}} løping", - "inactive": "Inaktiv", - "notClustered": "Frittstående node (ikke en del av en klynge)", - "quorate": "Kvorat", - "notQuorate": "Ikke beslutningsdyktig", - "nodeOnline": "Tilkoblet", - "nodeOffline": "Frakoblet", - "noHostSelected": "Ingen vert valgt", - "pveshNotFound": "pvesh ikke funnet – er dette en Proxmox-node?", - "nodeDetectionFailed": "Kan ikke bestemme et gyldig Proxmox-nodenavn", - "connecting": "Kobler til...", - "connectionFailed": "Kunne ikke koble til Proxmox-noden", - "refresh": "Oppdater", - "searchGuests": "Søk etter gjester...", - "filterAll": "Alle", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "Ingen virtuelle maskiner eller LXC-containere funnet på denne noden.", - "noGuestsMatch": "Ingen gjester samsvarer med de gjeldende filtrene." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Aktiver Proxmox-statistikk", - "enableProxmoxStatsDesc": "Vis en Proxmox-statistikkfane for denne verten med live node-, gjeste-, lagrings- og klyngestatistikk samlet inn via SSH.", - "proxmoxStatsPollInterval": "Avstemningsintervall (sekunder)", - "proxmoxStatsPollIntervalDesc": "Minimum 15 sekunder. Hvor ofte Proxmox-nodestatistikken skal oppdateres.", - "proxmoxStatsNodeOverride": "Overstyring av nodenavn", - "proxmoxStatsNodeOverrideDesc": "Overstyr automatisk deteksjon av Proxmox-nodenavnet (detekteres via vertsnavn som standard).", - "proxmoxStatsNodeOverridePlaceholder": "Automatisk oppdaget fra vertsnavn", - "authTypeStepca": "Trinn CA", - "proxmoxIntegration": "Proxmox-integrasjon", - "enableProxmox": "Aktiver Proxmox", - "enableProxmoxDesc": "Merk denne verten som en Proxmox-node. Aktiverer gjesteoppdagelse og import direkte fra denne verten.", - "proxmoxDefaultAuthType": "Standard autentiseringstype", - "proxmoxDefaultAuthTypeDesc": "Autentiseringsmetode brukt på importerte gjesteverter. Velg autentiseringstypen som samsvarer med hvordan du kobler til gjestene dine.", - "authTypePassword": "Passord", - "authTypeKey": "SSH-nøkkel", - "authTypeCredential": "Legitimasjon", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Ingen", - "proxmoxDefaultCredential": "Standard legitimasjon", - "proxmoxDefaultCredentialDesc": "Legitimasjon som brukes for importerte gjesteverter når godkjenningstypen er satt til Legitimasjon.", - "proxmoxWindowsDetection": "Windows / RDP-deteksjon", - "proxmoxWindowsDetectionDesc": "Kommaseparerte navnemønstre som utløser RDP i stedet for SSH (ikke-skillende mellom store og små bokstaver)", - "proxmoxDockerDetection": "Docker-deteksjon", - "proxmoxDockerDetectionDesc": "Kommaseparerte navnemønstre som gjør det mulig for Docker å matche gjester", - "proxmoxPreferredRanges": "Foretrukne IP-områder", - "proxmoxPreferredRangesDesc": "Kommaseparerte prefikser i prioritert rekkefølge for IP-valg når en gjest har flere grensesnitt", - "proxmoxAutoSync": "Automatisk synkronisering av gjester", - "proxmoxAutoSyncDesc": "Oppdag denne Proxmox-noden med jevne mellomrom og opprett eller oppdater importerte gjesteverter mens økten din er ulåst.", - "proxmoxSyncInterval": "Synkroniseringsintervall (minutter)", - "proxmoxSyncIntervalDesc": "Minimum 5 minutter. Planleggeren hopper over låste brukerøkter.", - "proxmoxMarkMissing": "Merk manglende gjester", - "proxmoxMarkMissingDesc": "Legg til en proxmox-missing-tag når en tidligere importert gjest forsvinner i stedet for å slette den.", - "proxmoxLastSync": "Siste synkronisering", - "proxmoxLastSyncNever": "Ikke synkronisert ennå", - "proxmoxLastSyncNoResult": "Ingen resultatdetaljer", - "proxmoxLastSyncSummary": "{{created}} opprettet, {{updated}} oppdatert, {{markedMissing}} mangler, {{skipped}} hoppet over", - "proxmoxLastSyncStatus": { - "error": "Mislyktes", - "pending": "I påvente av", - "success": "Suksess" - }, - "proxmoxDiscoverAction": "Oppdag og importer Proxmox-gjester", - "proxmoxImportTitle": "Importer fra Proxmox", - "proxmoxSelectHost": "Velg en Proxmox-vert…", - "proxmoxDiscover": "Oppdage", - "proxmoxDiscovering": "Oppdager…", - "proxmoxDiscoverGuests": "Oppdag gjester", - "proxmoxSelectAll": "Velg alle", - "proxmoxDeselectAll": "Fjern merkingen av alt", - "proxmoxNoGuests": "Ingen gjester funnet på denne Proxmox-noden.", - "proxmoxResultImported": "{{count}} importert", - "proxmoxResultUpdated": "{{count}} oppdatert", - "proxmoxResultFailed": "{{count}} mislyktes", - "proxmoxImportComplete": "Proxmox-import fullført: {{summary}}", - "proxmoxDiscoveryFailed": "Oppdagelsen mislyktes", - "proxmoxImportFailed": "Import mislyktes", - "docsLink": "Vis dokumenter", - "hostNotFound": "Verten ble ikke funnet", - "loadingHost": "Laster inn vert ...", - "none": "Ingen", - "proxmoxGuestsSelected_one": "{{count}} gjest, {{selected}} valgt", - "proxmoxGuestsSelected_other": "{{count}} gjester, {{selected}} valgt", - "proxmoxImportButton_one": "Importer {{count}} vert", - "proxmoxImportButton_other": "Importer {{count}} verter" - }, - "terminal": { - "connected": "Tilkoblet" - } -} diff --git a/plugins/proxmox/locales/translated/pl_PL.json b/plugins/proxmox/locales/translated/pl_PL.json deleted file mode 100644 index 1dd4fa94a..000000000 --- a/plugins/proxmox/locales/translated/pl_PL.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Statystyki Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Statystyki Proxmox", - "cpuUsage": "Wykorzystanie procesora", - "memoryUsage": "Wykorzystanie pamięci", - "diskUsage": "Wykorzystanie dysku", - "networkInterfaces": "Interfejsy sieciowe", - "uptime": "Czas działania", - "systemInfo": "Informacje o systemie", - "guestsSummary": "Goście", - "storagePools": "Pule magazynowe", - "clusterHealth": "Kondycja klastra", - "cpuCores_one": "{{count}} rdzeń", - "cpuCores_other": "{{count}} rdzeni", - "used": "Użyte", - "total": "Łącznie", - "hostname": "Nazwa hosta", - "kernel": "Jądro", - "pveVersion": "Wersja PVE", - "name": "Nazwa", - "status": "Status", - "type": "Typ", - "cpu": "Procesor", - "mem": "Pamięć", - "disk": "Dysk", - "running": "Działa", - "stopped": "Zatrzymany", - "guestCounts": "{{running}}/{{total}} działa", - "inactive": "Nieaktywny", - "notClustered": "Samodzielny węzeł (nie jest częścią klastra)", - "quorate": "Kworum osiągnięte", - "notQuorate": "Brak kworum", - "nodeOnline": "Online", - "nodeOffline": "Offline", - "noHostSelected": "Nie wybrano hosta", - "pveshNotFound": "Nie znaleziono pvesh. Czy to węzeł Proxmox?", - "nodeDetectionFailed": "Nie można ustalić prawidłowej nazwy węzła Proxmox", - "connecting": "Łączenie...", - "connectionFailed": "Nie udało się połączyć z węzłem Proxmox", - "refresh": "Odśwież", - "searchGuests": "Wyszukaj gości...", - "filterAll": "Wszystkie", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "Nie znaleziono maszyn wirtualnych ani kontenerów LXC na tym węźle.", - "noGuestsMatch": "Żaden gość nie pasuje do bieżących filtrów." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Włącz statystyki Proxmox", - "enableProxmoxStatsDesc": "Pokaż kartę statystyk Proxmox dla tego hosta z bieżącymi statystykami węzła, gości, magazynu i klastra zbieranymi przez SSH.", - "proxmoxStatsPollInterval": "Interwał sondowania (sekundy)", - "proxmoxStatsPollIntervalDesc": "Minimum 15 sekund. Jak często odświeżać statystyki węzła Proxmox.", - "proxmoxStatsNodeOverride": "Nadpisanie nazwy węzła", - "proxmoxStatsNodeOverrideDesc": "Zastąp automatyczne wykrywanie nazwy węzła Proxmox (domyślnie wykrywane za pomocą nazwy hosta).", - "proxmoxStatsNodeOverridePlaceholder": "Automatycznie wykryto na podstawie nazwy hosta", - "authTypeStepca": "Krok CA", - "proxmoxIntegration": "Integracja Proxmox", - "enableProxmox": "Włącz Proxmox", - "enableProxmoxDesc": "Oznacz ten host jako węzeł Proxmox. Umożliwia wykrywanie gości i importowanie bezpośrednio z tego hosta.", - "proxmoxDefaultAuthType": "Domyślny typ uwierzytelniania", - "proxmoxDefaultAuthTypeDesc": "Metoda uwierzytelniania stosowana do importowanych hostów gościnnych. Wybierz typ uwierzytelniania odpowiadający sposobowi, w jaki łączysz się z gośćmi.", - "authTypePassword": "Hasło", - "authTypeKey": "Klucz SSH", - "authTypeCredential": "Poświadczenie", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Brak", - "proxmoxDefaultCredential": "Domyślne poświadczenie", - "proxmoxDefaultCredentialDesc": "Poświadczenie używane dla importowanych hostów gości, gdy typ uwierzytelniania to Poświadczenie.", - "proxmoxWindowsDetection": "Wykrywanie systemu Windows/RDP", - "proxmoxWindowsDetectionDesc": "Wzorce nazw rozdzielone przecinkami, które wyzwalają protokół RDP zamiast protokołu SSH (bez uwzględniania wielkości liter)", - "proxmoxDockerDetection": "Wykrywanie Dockera", - "proxmoxDockerDetectionDesc": "Wzorce nazw rozdzielone przecinkami, które włączają Docker dla pasujących gości", - "proxmoxPreferredRanges": "Preferowane zakresy adresów IP", - "proxmoxPreferredRangesDesc": "Prefiksy rozdzielone przecinkami w kolejności priorytetów wyboru adresu IP, gdy gość ma wiele interfejsów", - "proxmoxAutoSync": "Automatyczna synchronizacja gości", - "proxmoxAutoSyncDesc": "Okresowo wykrywaj ten węzeł Proxmox i twórz lub aktualizuj importowane hosty gości, dopóki Twoja sesja jest odblokowana.", - "proxmoxSyncInterval": "Interwał synchronizacji (minuty)", - "proxmoxSyncIntervalDesc": "Minimum 5 minut. Harmonogram pomija zablokowane sesje użytkowników.", - "proxmoxMarkMissing": "Oznacz brakujących gości", - "proxmoxMarkMissingDesc": "Dodaj tag proxmox-missing, gdy wcześniej zaimportowany gość zniknie, zamiast go usuwać.", - "proxmoxLastSync": "Ostatnia synchronizacja", - "proxmoxLastSyncNever": "Jeszcze nie zsynchronizowano", - "proxmoxLastSyncNoResult": "Brak szczegółów wyników", - "proxmoxLastSyncSummary": "utworzono {{created}}, zaktualizowano {{updated}}, brakujących {{markedMissing}}, pominięto {{skipped}}", - "proxmoxLastSyncStatus": { - "error": "Niepowodzenie", - "pending": "Oczekuje", - "success": "Powodzenie" - }, - "proxmoxDiscoverAction": "Wykryj i zaimportuj gości Proxmox", - "proxmoxImportTitle": "Import z Proxmox", - "proxmoxSelectHost": "Wybierz hosta Proxmox…", - "proxmoxDiscover": "Wykryj", - "proxmoxDiscovering": "Wykrywanie…", - "proxmoxDiscoverGuests": "Wykryj gości", - "proxmoxSelectAll": "Zaznacz wszystko", - "proxmoxDeselectAll": "Odznacz wszystko", - "proxmoxNoGuests": "Nie znaleziono gości na tym węźle Proxmox.", - "proxmoxResultImported": "{{count}} zaimportowano", - "proxmoxResultUpdated": "{{count}} zaktualizowano", - "proxmoxResultFailed": "{{count}} nie powiodło się", - "proxmoxImportComplete": "Zakończono importowanie Proxmox: {{summary}}", - "proxmoxDiscoveryFailed": "Wykrywanie nie powiodło się", - "proxmoxImportFailed": "Import nie powiódł się", - "docsLink": "Zobacz dokumentację", - "hostNotFound": "Nie znaleziono hosta", - "loadingHost": "Ładowanie hosta...", - "none": "Brak", - "proxmoxGuestsSelected_one": "{{count}} gość, {{selected}} wybrano", - "proxmoxGuestsSelected_other": "{{count}} gości, {{selected}} wybranych", - "proxmoxImportButton_one": "Importuj {{count}} host", - "proxmoxImportButton_other": "Importuj {{count}} hostów" - }, - "terminal": { - "connected": "Połączono" - } -} diff --git a/plugins/proxmox/locales/translated/pt_BR.json b/plugins/proxmox/locales/translated/pt_BR.json deleted file mode 100644 index 58e0a53c4..000000000 --- a/plugins/proxmox/locales/translated/pt_BR.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Estatísticas do Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Estatísticas do Proxmox", - "cpuUsage": "Utilização da CPU", - "memoryUsage": "Uso de memória", - "diskUsage": "Utilização do disco", - "networkInterfaces": "Interfaces de rede", - "uptime": "Tempo de atividade", - "systemInfo": "Informações do sistema", - "guestsSummary": "Convidados", - "storagePools": "Piscinas de armazenamento", - "clusterHealth": "Cluster Health", - "cpuCores_one": "{{count}} Núcleo", - "cpuCores_other": "{{count}} Núcleos", - "used": "Usado", - "total": "Total", - "hostname": "Nome do host", - "kernel": "Kernel", - "pveVersion": "Versão PvE", - "name": "Nome", - "status": "Status", - "type": "Tipo", - "cpu": "CPU", - "mem": "Membro", - "disk": "Disco", - "running": "Correndo", - "stopped": "Parou", - "guestCounts": "{{running}}/{{total}} em execução", - "inactive": "Inativo", - "notClustered": "Nó independente (não faz parte de um cluster)", - "quorate": "Quorate", - "notQuorate": "Sem quórum", - "nodeOnline": "Online", - "nodeOffline": "Offline", - "noHostSelected": "Nenhum host selecionado", - "pveshNotFound": "pvesh não encontrado - este é um nó Proxmox?", - "nodeDetectionFailed": "Não foi possível determinar um nome de nó Proxmox válido.", - "connecting": "Conectando...", - "connectionFailed": "Falha ao conectar-se ao nó Proxmox", - "refresh": "Atualizar", - "searchGuests": "Pesquisar hóspedes...", - "filterAll": "Todos", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "Nenhuma máquina virtual ou contêiner LXC foi encontrado neste nó.", - "noGuestsMatch": "Nenhum hóspede corresponde aos filtros atuais." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Ativar estatísticas do Proxmox", - "enableProxmoxStatsDesc": "Exibe a aba de Estatísticas do Proxmox para este host com estatísticas em tempo real de nós, máquinas virtuais, armazenamento e cluster, coletadas via SSH.", - "proxmoxStatsPollInterval": "Intervalo de sondagem (segundos)", - "proxmoxStatsPollIntervalDesc": "Mínimo de 15 segundos. Com que frequência as estatísticas do nó Proxmox devem ser atualizadas.", - "proxmoxStatsNodeOverride": "Substituição do nome do nó", - "proxmoxStatsNodeOverrideDesc": "Substituir a detecção automática do nome do nó Proxmox (detectado por padrão através do nome do host).", - "proxmoxStatsNodeOverridePlaceholder": "Detectado automaticamente a partir do nome do host", - "authTypeStepca": "Etapa CA", - "proxmoxIntegration": "Integração Proxmox", - "enableProxmox": "Ativar Proxmox", - "enableProxmoxDesc": "Marque este host como um nó Proxmox. Isso permite a descoberta e importação de máquinas virtuais diretamente deste host.", - "proxmoxDefaultAuthType": "Tipo de autenticação padrão", - "proxmoxDefaultAuthTypeDesc": "Método de autenticação aplicado aos hosts convidados importados. Escolha o tipo de autenticação que corresponde à forma como você se conecta aos seus convidados.", - "authTypePassword": "Senha", - "authTypeKey": "Chave SSH", - "authTypeCredential": "Credencial", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Nenhum", - "proxmoxDefaultCredential": "Credencial padrão", - "proxmoxDefaultCredentialDesc": "Credencial usada para hosts convidados importados quando o tipo de autenticação está definido como Credencial.", - "proxmoxWindowsDetection": "Detecção de Windows/RDP", - "proxmoxWindowsDetectionDesc": "Padrões de nomes separados por vírgulas que acionam o RDP em vez do SSH (sem distinção entre maiúsculas e minúsculas)", - "proxmoxDockerDetection": "Detecção de Docker", - "proxmoxDockerDetectionDesc": "Padrões de nomes separados por vírgulas que permitem ao Docker identificar máquinas virtuais.", - "proxmoxPreferredRanges": "Faixas de IP preferenciais", - "proxmoxPreferredRangesDesc": "Prefixos separados por vírgula em ordem de prioridade para seleção de IP quando um convidado possui múltiplas interfaces.", - "proxmoxAutoSync": "Sincronização automática de convidados", - "proxmoxAutoSyncDesc": "Descubra periodicamente este nó Proxmox e crie ou atualize hosts convidados importados enquanto sua sessão estiver desbloqueada.", - "proxmoxSyncInterval": "Intervalo de sincronização (minutos)", - "proxmoxSyncIntervalDesc": "Tempo mínimo de 5 minutos. O agendador ignora sessões de usuário bloqueadas.", - "proxmoxMarkMissing": "Marque os hóspedes ausentes", - "proxmoxMarkMissingDesc": "Adicione a tag proxmox-missing quando uma máquina virtual previamente importada desaparecer, em vez de excluí-la.", - "proxmoxLastSync": "Última sincronização", - "proxmoxLastSyncNever": "Ainda não sincronizado", - "proxmoxLastSyncNoResult": "Nenhum detalhe do resultado", - "proxmoxLastSyncSummary": "{{created}} criado, {{updated}} atualizado, {{markedMissing}} ausente, {{skipped}} ignorado", - "proxmoxLastSyncStatus": { - "error": "Fracassado", - "pending": "Pendente", - "success": "Sucesso" - }, - "proxmoxDiscoverAction": "Descubra e importe hóspedes do Proxmox", - "proxmoxImportTitle": "Importar do Proxmox", - "proxmoxSelectHost": "Selecione um host Proxmox…", - "proxmoxDiscover": "Descobrir", - "proxmoxDiscovering": "Descobrindo…", - "proxmoxDiscoverGuests": "Descubra os hóspedes", - "proxmoxSelectAll": "Selecionar tudo", - "proxmoxDeselectAll": "Desmarque tudo", - "proxmoxNoGuests": "Nenhum convidado encontrado neste nó Proxmox.", - "proxmoxResultImported": "{{count}} importado", - "proxmoxResultUpdated": "{{count}} atualizado", - "proxmoxResultFailed": "{{count}} falhou", - "proxmoxImportComplete": "Importação do Proxmox concluída: {{summary}}", - "proxmoxDiscoveryFailed": "A descoberta falhou", - "proxmoxImportFailed": "A importação falhou", - "docsLink": "Documentação", - "hostNotFound": "Host não encontrado", - "loadingHost": "Carregando host...", - "none": "Nenhum", - "proxmoxGuestsSelected_one": "{{count}} convidado, {{selected}} selecionado", - "proxmoxGuestsSelected_other": "{{count}} convidados, {{selected}} selecionados", - "proxmoxImportButton_one": "Importar {{count}} host", - "proxmoxImportButton_other": "Importar {{count}} hosts" - }, - "terminal": { - "connected": "Conectado" - } -} diff --git a/plugins/proxmox/locales/translated/pt_PT.json b/plugins/proxmox/locales/translated/pt_PT.json deleted file mode 100644 index d71843ab4..000000000 --- a/plugins/proxmox/locales/translated/pt_PT.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Estatísticas do Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Estatísticas do Proxmox", - "cpuUsage": "Utilização da CPU", - "memoryUsage": "Utilização da memória", - "diskUsage": "Utilização do disco", - "networkInterfaces": "Interfaces de Rede", - "uptime": "Tempo de Atividade", - "systemInfo": "Informação do Sistema", - "guestsSummary": "Convidados", - "storagePools": "Piscinas de armazenamento", - "clusterHealth": "Saúde em grupo", - "cpuCores_one": "{{count}} Núcleo", - "cpuCores_other": "{{count}} Núcleos", - "used": "Utilizado", - "total": "Total", - "hostname": "Nome do host", - "kernel": "Kernel", - "pveVersion": "Versão PvE", - "name": "Nome", - "status": "Estado", - "type": "Tipo", - "cpu": "CPU", - "mem": "Membro", - "disk": "Disco", - "running": "Em execução", - "stopped": "Parado", - "guestCounts": "{{running}}/{{total}} em execução", - "inactive": "Inativo", - "notClustered": "Nó independente (não faz parte de um cluster)", - "quorate": "Quorate", - "notQuorate": "Sem quórum", - "nodeOnline": "Online", - "nodeOffline": "Offline", - "noHostSelected": "Nenhum anfitrião selecionado", - "pveshNotFound": "pvesh não encontrado - este é um nó Proxmox?", - "nodeDetectionFailed": "Não foi possível determinar um nome de nó Proxmox válido.", - "connecting": "A ligar...", - "connectionFailed": "Falha ao ligar ao nó Proxmox", - "refresh": "Atualizar", - "searchGuests": "Pesquisar hóspedes...", - "filterAll": "Todas", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "Nenhuma máquina virtual ou contentor LXC foi encontrado neste nó.", - "noGuestsMatch": "Nenhum hóspede corresponde aos filtros atuais." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Ativar estatísticas do Proxmox", - "enableProxmoxStatsDesc": "Apresenta o separador de Estatísticas do Proxmox para este host com estatísticas em tempo real dos nós, máquinas virtuais, armazenamento e cluster, recolhidas via SSH.", - "proxmoxStatsPollInterval": "Intervalo de sondagem (segundos)", - "proxmoxStatsPollIntervalDesc": "Mínimo de 15 segundos. Com que frequência as estatísticas do nó Proxmox devem ser atualizadas.", - "proxmoxStatsNodeOverride": "Substituição do nome do nó", - "proxmoxStatsNodeOverrideDesc": "Substituir a deteção automática do nome do nó Proxmox (detetado por defeito através do nome do host).", - "proxmoxStatsNodeOverridePlaceholder": "Detetado automaticamente a partir do nome do host", - "authTypeStepca": "Etapa CA", - "proxmoxIntegration": "Integração com Proxmox", - "enableProxmox": "Ativar Proxmox", - "enableProxmoxDesc": "Marcar este host como um nó Proxmox. Permite a descoberta e importação de convidados diretamente a partir deste host.", - "proxmoxDefaultAuthType": "Tipo de Autenticação Padrão", - "proxmoxDefaultAuthTypeDesc": "Método de autenticação aplicado aos hosts convidados importados. Escolha o tipo de autenticação que corresponde à forma como se liga aos seus convidados.", - "authTypePassword": "Palavra-passe", - "authTypeKey": "Chave SSH", - "authTypeCredential": "Credencial", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Nenhum", - "proxmoxDefaultCredential": "Credencial padrão", - "proxmoxDefaultCredentialDesc": "Credencial usada para os convidados importados quando o tipo de autenticação é definido como Credencial.", - "proxmoxWindowsDetection": "Deteção de Windows / RDP", - "proxmoxWindowsDetectionDesc": "Padrões de nomes separados por vírgulas que acionam RDP em vez de SSH (sem distinção entre maiúsculas/minúsculas)", - "proxmoxDockerDetection": "Deteção de Docker", - "proxmoxDockerDetectionDesc": "Padrões de nomes separados por vírgulas que ativam o Docker nos convidados que correspondam.", - "proxmoxPreferredRanges": "Intervalos de IP preferenciais", - "proxmoxPreferredRangesDesc": "Prefixos separados por vírgulas em ordem de prioridade para seleção de IP quando um convidado tem várias interfaces.", - "proxmoxAutoSync": "Sincronização automática", - "proxmoxAutoSyncDesc": "Descobrir periodicamente este nó Proxmox e criar ou atualizar os convidados importados enquanto a sessão estiver desbloqueada.", - "proxmoxSyncInterval": "Intervalo de sincronização (minutos)", - "proxmoxSyncIntervalDesc": "Mínimo de 5 minutos. O agendador ignora sessões de utilizador bloqueadas.", - "proxmoxMarkMissing": "Marcar convidados em falta", - "proxmoxMarkMissingDesc": "Adicionar a etiqueta proxmox-missing quando um convidado previamente importado desaparecer, em vez de o eliminar.", - "proxmoxLastSync": "Última sincronização", - "proxmoxLastSyncNever": "Ainda não sincronizado", - "proxmoxLastSyncNoResult": "Sem detalhes do resultado", - "proxmoxLastSyncSummary": "{{created}} criados, {{updated}} atualizados, {{markedMissing}} em falta, {{skipped}} ignorados", - "proxmoxLastSyncStatus": { - "error": "Falhou", - "pending": "Pendente", - "success": "Sucesso" - }, - "proxmoxDiscoverAction": "Descobrir e importar convidados", - "proxmoxImportTitle": "Importar do Proxmox", - "proxmoxSelectHost": "Selecionar um host Proxmox…", - "proxmoxDiscover": "Descobrir", - "proxmoxDiscovering": "A descobrir…", - "proxmoxDiscoverGuests": "Descobrir convidados", - "proxmoxSelectAll": "Selecionar todos", - "proxmoxDeselectAll": "Desmarcar todos", - "proxmoxNoGuests": "Nenhum convidado encontrado neste nó Proxmox.", - "proxmoxResultImported": "{{count}} importados", - "proxmoxResultUpdated": "{{count}} atualizados", - "proxmoxResultFailed": "{{count}} falhados", - "proxmoxImportComplete": "Importação Proxmox concluída: {{summary}}", - "proxmoxDiscoveryFailed": "Descoberta falhou", - "proxmoxImportFailed": "Importação falhou", - "docsLink": "Ver documentação", - "hostNotFound": "Host não encontrado", - "loadingHost": "A carregar o host...", - "none": "Nenhum", - "proxmoxGuestsSelected_one": "{{count}} convidado, {{selected}} selecionado", - "proxmoxGuestsSelected_other": "{{count}} convidados, {{selected}} selecionados", - "proxmoxImportButton_one": "Importar {{count}} host", - "proxmoxImportButton_other": "Importar {{count}} hosts" - }, - "terminal": { - "connected": "Ligado" - } -} diff --git a/plugins/proxmox/locales/translated/ro_RO.json b/plugins/proxmox/locales/translated/ro_RO.json deleted file mode 100644 index 1bda50c77..000000000 --- a/plugins/proxmox/locales/translated/ro_RO.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Statistici Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Statistici Proxmox", - "cpuUsage": "Utilizarea procesorului", - "memoryUsage": "Utilizarea memoriei", - "diskUsage": "Utilizarea discului", - "networkInterfaces": "Interfețe de rețea", - "uptime": "Timp de funcționare", - "systemInfo": "Informații despre sistem", - "guestsSummary": "Oaspeți", - "storagePools": "Bazine de stocare", - "clusterHealth": "Sănătatea clusterului", - "cpuCores_one": "{{count}} Nucleu", - "cpuCores_other": "{{count}} Nuclee", - "used": "Folosit", - "total": "Total", - "hostname": "Nume gazdă", - "kernel": "Nucleu", - "pveVersion": "Versiunea PVE", - "name": "Nume", - "status": "Stare", - "type": "Tip", - "cpu": "CPU", - "mem": "Mem", - "disk": "Disc", - "running": "Funcţionare", - "stopped": "Oprit", - "guestCounts": "{{running}}/{{total}} rulează", - "inactive": "Inactiv", - "notClustered": "Nod independent (nu face parte dintr-un cluster)", - "quorate": "Cvoat", - "notQuorate": "Nu există cvorum", - "nodeOnline": "Online", - "nodeOffline": "Offline", - "noHostSelected": "Nicio gazdă selectată", - "pveshNotFound": "pvesh nu a fost găsit - este acesta un nod Proxmox?", - "nodeDetectionFailed": "Nu se poate determina un nume de nod Proxmox valid", - "connecting": "Se conectează...", - "connectionFailed": "Conectarea la nodul Proxmox nu a reușit.", - "refresh": "Reîmprospăta", - "searchGuests": "Caută invitați...", - "filterAll": "Toate", - "filterVm": "Mașină virtuală", - "filterLxc": "LXC", - "noGuests": "Nu s-au găsit mașini virtuale sau containere LXC pe acest nod.", - "noGuestsMatch": "Niciun invitat nu corespunde filtrelor actuale." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Activează statisticile Proxmox", - "enableProxmoxStatsDesc": "Afișează o filă Statistici Proxmox pentru această gazdă cu statistici live despre noduri, invitați, stocare și cluster, colectate prin SSH.", - "proxmoxStatsPollInterval": "Interval de sondaj (secunde)", - "proxmoxStatsPollIntervalDesc": "Minim 15 secunde. Cât de des se actualizează statisticile nodului Proxmox.", - "proxmoxStatsNodeOverride": "Suprascriere nume nod", - "proxmoxStatsNodeOverrideDesc": "Suprascrie detectarea automată a numelui nodului Proxmox (detectat implicit prin numele de gazdă).", - "proxmoxStatsNodeOverridePlaceholder": "Detectat automat de la numele gazdei", - "authTypeStepca": "Pasul CA", - "proxmoxIntegration": "Integrare Proxmox", - "enableProxmox": "Activează Proxmox", - "enableProxmoxDesc": "Marchează această gazdă ca nod Proxmox. Activează descoperirea și importul de informații de către oaspeți direct de pe această gazdă.", - "proxmoxDefaultAuthType": "Tip de autentificare implicit", - "proxmoxDefaultAuthTypeDesc": "Metodă de autentificare aplicată gazdelor invitate importate. Alegeți tipul de autentificare care corespunde modului în care vă conectați la invitați.", - "authTypePassword": "Parolă", - "authTypeKey": "Cheie SSH", - "authTypeCredential": "Acreditare", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Nici unul", - "proxmoxDefaultCredential": "Acreditare implicită", - "proxmoxDefaultCredentialDesc": "Acreditare utilizată pentru gazdele invitate importate atunci când tipul de autentificare este setat la Acreditare.", - "proxmoxWindowsDetection": "Detectare Windows / RDP", - "proxmoxWindowsDetectionDesc": "Modele de nume separate prin virgulă care declanșează RDP în loc de SSH (fără sensibilitate la majuscule/minuscule)", - "proxmoxDockerDetection": "Detectarea Docker", - "proxmoxDockerDetectionDesc": "Modele de nume separate prin virgulă care activează Docker pentru invitații potriviți", - "proxmoxPreferredRanges": "Intervalele IP preferate", - "proxmoxPreferredRangesDesc": "Prefixe separate prin virgulă în ordinea priorității pentru selectarea adresei IP atunci când un oaspete are mai multe interfețe", - "proxmoxAutoSync": "Sincronizare automată a invitaților", - "proxmoxAutoSyncDesc": "Descoperiți periodic acest nod Proxmox și creați sau actualizați gazdele invitate importate în timp ce sesiunea este deblocată.", - "proxmoxSyncInterval": "Interval de sincronizare (minute)", - "proxmoxSyncIntervalDesc": "Minim 5 minute. Planificatorul omite sesiunile utilizatorilor blocate.", - "proxmoxMarkMissing": "Marchează oaspeții lipsă", - "proxmoxMarkMissingDesc": "Adăugați o etichetă proxmox-missing atunci când un invitat importat anterior dispare, în loc să îl ștergeți.", - "proxmoxLastSync": "Ultima sincronizare", - "proxmoxLastSyncNever": "Încă nesincronizat", - "proxmoxLastSyncNoResult": "Niciun detaliu al rezultatului", - "proxmoxLastSyncSummary": "{{created}} creat, {{updated}} actualizat, {{markedMissing}} lipsă, {{skipped}} omis", - "proxmoxLastSyncStatus": { - "error": "Eșuat", - "pending": "În așteptare", - "success": "Succes" - }, - "proxmoxDiscoverAction": "Descoperiți și importați invitați Proxmox", - "proxmoxImportTitle": "Import din Proxmox", - "proxmoxSelectHost": "Selectați o gazdă Proxmox…", - "proxmoxDiscover": "Descoperi", - "proxmoxDiscovering": "Descoperind…", - "proxmoxDiscoverGuests": "Descoperă oaspeți", - "proxmoxSelectAll": "Selectați tot", - "proxmoxDeselectAll": "Deselectați tot", - "proxmoxNoGuests": "Nu au fost găsiți invitați pe acest nod Proxmox.", - "proxmoxResultImported": "{{count}} importat", - "proxmoxResultUpdated": "{{count}} actualizat", - "proxmoxResultFailed": "{{count}} a eșuat", - "proxmoxImportComplete": "Import Proxmox finalizat: {{summary}}", - "proxmoxDiscoveryFailed": "Descoperirea a eșuat", - "proxmoxImportFailed": "Importul a eșuat", - "docsLink": "Vizualizați documentele", - "hostNotFound": "Gazdă negăsită", - "loadingHost": "Se încarcă gazda...", - "none": "Nici unul", - "proxmoxGuestsSelected_one": "{{count}} invitat, {{selected}} selectat", - "proxmoxGuestsSelected_other": "{{count}} invitați, {{selected}} selectați", - "proxmoxImportButton_one": "Importați gazda {{count}}", - "proxmoxImportButton_other": "Importați gazde {{count}}" - }, - "terminal": { - "connected": "Conectat" - } -} diff --git a/plugins/proxmox/locales/translated/ru_RU.json b/plugins/proxmox/locales/translated/ru_RU.json deleted file mode 100644 index 9955b8966..000000000 --- a/plugins/proxmox/locales/translated/ru_RU.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Статистика Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Статистика Proxmox", - "cpuUsage": "Использование ЦП", - "memoryUsage": "Использование памяти", - "diskUsage": "Использование диска", - "networkInterfaces": "Сетевые интерфейсы", - "uptime": "Время работы", - "systemInfo": "Информация о системе", - "guestsSummary": "Гости", - "storagePools": "Бассейны для хранения", - "clusterHealth": "Здоровье кластера", - "cpuCores_one": "{{count}} ядро", - "cpuCores_other": "{{count}} ядер", - "used": "Занято", - "total": "Общий", - "hostname": "Имя хоста", - "kernel": "Ядро", - "pveVersion": "Версия для PVE", - "name": "Название", - "status": "Статус", - "type": "Тип", - "cpu": "CPU", - "mem": "Мем", - "disk": "Диск", - "running": "Запущен", - "stopped": "Остановлен", - "guestCounts": "{{running}}/{{total}} бежит", - "inactive": "Неактивен", - "notClustered": "Автономный узел (не входящий в кластер)", - "quorate": "Цитата", - "notQuorate": "Кворум не соблюден", - "nodeOnline": "Онлайн", - "nodeOffline": "Офлайн", - "noHostSelected": "Хост не выбран", - "pveshNotFound": "pvesh не найден — это узел Proxmox?", - "nodeDetectionFailed": "Не удалось определить допустимое имя узла Proxmox.", - "connecting": "Подключение...", - "connectionFailed": "Не удалось подключиться к узлу Proxmox.", - "refresh": "Обновить", - "searchGuests": "Поиск гостей...", - "filterAll": "Все", - "filterVm": "VM", - "filterLxc": "ЛХС", - "noGuests": "На этом узле не обнаружено виртуальных машин или контейнеров LXC.", - "noGuestsMatch": "Ни один из гостей не соответствует текущим фильтрам." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Включить статистику Proxmox", - "enableProxmoxStatsDesc": "Отобразить вкладку Proxmox Stats для этого хоста с актуальными статистическими данными по узлам, гостевым системам, хранилищу и кластеру, собранными по SSH.", - "proxmoxStatsPollInterval": "Интервал опроса (секунды)", - "proxmoxStatsPollIntervalDesc": "Минимум 15 секунд. Как часто обновлять статистику узлов Proxmox.", - "proxmoxStatsNodeOverride": "Переопределение имени узла", - "proxmoxStatsNodeOverrideDesc": "Отключить автоматическое определение имени узла Proxmox (по умолчанию определяется по имени хоста).", - "proxmoxStatsNodeOverridePlaceholder": "Автоматически определено по имени хоста", - "authTypeStepca": "Шаг CA", - "proxmoxIntegration": "Интеграция Proxmox", - "enableProxmox": "Включить Proxmox", - "enableProxmoxDesc": "Пометить этот хост как узел Proxmox. Включает обнаружение и импорт гостевых систем непосредственно с этого хоста.", - "proxmoxDefaultAuthType": "Тип аутентификации по умолчанию", - "proxmoxDefaultAuthTypeDesc": "Метод аутентификации, применяемый к импортированным гостевым хостам. Выберите тип аутентификации, соответствующий способу подключения к гостевым хостам.", - "authTypePassword": "Пароль", - "authTypeKey": "SSH-ключ", - "authTypeCredential": "Учётные данные", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Нет", - "proxmoxDefaultCredential": "Учётные данные по умолчанию", - "proxmoxDefaultCredentialDesc": "Учётные данные, используемые для импортированных гостевых хостов, когда в качестве типа аутентификации выбраны «Учётные данные».", - "proxmoxWindowsDetection": "Обнаружение Windows / RDP", - "proxmoxWindowsDetectionDesc": "Шаблоны имён через запятую, при совпадении с которыми используется RDP вместо SSH (без учёта регистра).", - "proxmoxDockerDetection": "Обнаружение Docker", - "proxmoxDockerDetectionDesc": "Шаблоны имён через запятую, при совпадении с которыми для гостя включается Docker.", - "proxmoxPreferredRanges": "Предпочтительные IP-диапазоны", - "proxmoxPreferredRangesDesc": "Список префиксов через запятую в порядке приоритета для выбора IP-адреса, если у гостя несколько интерфейсов.", - "proxmoxAutoSync": "Автосинхронизация гостей", - "proxmoxAutoSyncDesc": "Периодически обнаруживать этот узел Proxmox и создавать или обновлять импортированные гостевые хосты, пока ваш сеанс разблокирован.", - "proxmoxSyncInterval": "Интервал синхронизации (мин.)", - "proxmoxSyncIntervalDesc": "Минимум 5 минут. Планировщик пропускает заблокированные сеансы.", - "proxmoxMarkMissing": "Помечать отсутствующих гостей", - "proxmoxMarkMissingDesc": "Добавлять тег proxmox-missing, когда ранее импортированный гость исчезает, вместо его удаления.", - "proxmoxLastSync": "Последняя синхронизация", - "proxmoxLastSyncNever": "Не выполнялась", - "proxmoxLastSyncNoResult": "Нет сведений о результате", - "proxmoxLastSyncSummary": "{{created}} создано, {{updated}} обновлено, {{markedMissing}} отсутствует, {{skipped}} пропущено", - "proxmoxLastSyncStatus": { - "error": "Ошибка", - "pending": "Ожидание", - "success": "Успешно" - }, - "proxmoxDiscoverAction": "Обнаружить и импортировать гостей Proxmox", - "proxmoxImportTitle": "Импорт из Proxmox", - "proxmoxSelectHost": "Выберите хост Proxmox…", - "proxmoxDiscover": "Обнаружить", - "proxmoxDiscovering": "Обнаружение…", - "proxmoxDiscoverGuests": "Обнаружить гостей", - "proxmoxSelectAll": "Выбрать всё", - "proxmoxDeselectAll": "Снять выделение", - "proxmoxNoGuests": "На этом узле Proxmox гости не найдены.", - "proxmoxResultImported": "{{count}} импортировано", - "proxmoxResultUpdated": "{{count}} обновлено", - "proxmoxResultFailed": "{{count}} не удалось", - "proxmoxImportComplete": "Импорт Proxmox завершен: {{summary}}", - "proxmoxDiscoveryFailed": "Обнаружение не удалось", - "proxmoxImportFailed": "Импорт не удался", - "docsLink": "Документация", - "hostNotFound": "Хост не найден", - "loadingHost": "Загрузка хоста...", - "none": "Нет", - "proxmoxGuestsSelected_one": "{{count}} гость, {{selected}} выбран", - "proxmoxGuestsSelected_other": "{{count}} гостей, {{selected}} выбраны", - "proxmoxImportButton_one": "Импортировать {{count}} хост", - "proxmoxImportButton_other": "Импортировать {{count}} хостов" - }, - "terminal": { - "connected": "Подключено" - } -} diff --git a/plugins/proxmox/locales/translated/sr_SP.json b/plugins/proxmox/locales/translated/sr_SP.json deleted file mode 100644 index 7f1173bdf..000000000 --- a/plugins/proxmox/locales/translated/sr_SP.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox статистика" - }, - "proxmoxStats": { - "tabTitle": "Proxmox статистика", - "cpuUsage": "Искоришћеност процесора", - "memoryUsage": "Коришћење меморије", - "diskUsage": "Искоришћеност диска", - "networkInterfaces": "Мрежни интерфејси", - "uptime": "Време непрекидног рада", - "systemInfo": "Системске информације", - "guestsSummary": "Гости", - "storagePools": "Складишни базени", - "clusterHealth": "Здравље кластера", - "cpuCores_one": "{{count}} Језгро", - "cpuCores_other": "{{count}} Језгра", - "used": "Коришћено", - "total": "Укупно", - "hostname": "Име хоста", - "kernel": "Језгро", - "pveVersion": "ПВЕ верзија", - "name": "Име", - "status": "Статус", - "type": "Тип", - "cpu": "Процесор", - "mem": "Мем", - "disk": "Диск", - "running": "Трчање", - "stopped": "Заустављено", - "guestCounts": "{{running}}/{{total}} трчи", - "inactive": "Неактивно", - "notClustered": "Самостални чвор (није део кластера)", - "quorate": "Кворат", - "notQuorate": "Није кворум", - "nodeOnline": "Онлајн", - "nodeOffline": "Офлајн", - "noHostSelected": "Није изабран домаћин", - "pveshNotFound": "pvesh није пронађен - да ли је ово Proxmox чвор?", - "nodeDetectionFailed": "Није могуће утврдити важеће име Proxmox чвора", - "connecting": "Повезивање...", - "connectionFailed": "Повезивање са Proxmox чвором није успело", - "refresh": "Освежи", - "searchGuests": "Претражи госте...", - "filterAll": "Сви", - "filterVm": "Виртуелна машина", - "filterLxc": "LXC", - "noGuests": "На овом чвору нису пронађене виртуелне машине или LXC контејнери.", - "noGuestsMatch": "Ниједан гост не одговара тренутним филтерима." - }, - "hosts": { - "tabProxmox": "Проксмокс", - "enableProxmoxStats": "Омогући Proxmox статистику", - "enableProxmoxStatsDesc": "Прикажи картицу Proxmox Stats за овај хост са статистикама уживо чвора, госта, складишта и кластера прикупљеним преко SSH-а.", - "proxmoxStatsPollInterval": "Интервал анкете (секунде)", - "proxmoxStatsPollIntervalDesc": "Минимум 15 секунди. Колико често освежавати статистику Proxmox чвора.", - "proxmoxStatsNodeOverride": "Замена имена чвора", - "proxmoxStatsNodeOverrideDesc": "Поништи аутоматско откривање имена Proxmox чвора (подразумевано се открива преко имена хоста).", - "proxmoxStatsNodeOverridePlaceholder": "Аутоматски детектовано на основу имена хоста", - "authTypeStepca": "Корак CA", - "proxmoxIntegration": "Интеграција са Proxmox-ом", - "enableProxmox": "Омогући Proxmox", - "enableProxmoxDesc": "Означи овај хост као Proxmox чвор. Омогућава откривање госта и увоз директно са овог хоста.", - "proxmoxDefaultAuthType": "Подразумевани тип овлашћења", - "proxmoxDefaultAuthTypeDesc": "Метод аутентификације примењен на увезене гостујуће хостове. Изаберите тип аутентификације који одговара начину на који се повезујете са гостима.", - "authTypePassword": "Лозинка", - "authTypeKey": "SSH кључ", - "authTypeCredential": "Акредитив", - "authTypeOpkssh": "ОПКШ", - "authTypeNone": "Ниједан", - "proxmoxDefaultCredential": "Подразумевани акредитив", - "proxmoxDefaultCredentialDesc": "Акредитив који се користи за увезене гостујуће хостове када је тип овлашћења подешен на Акредитив.", - "proxmoxWindowsDetection": "Детекција Windows-а / RDP-а", - "proxmoxWindowsDetectionDesc": "Обрасци имена раздвојени зарезима који покрећу RDP уместо SSH (не разликују велика и мала слова)", - "proxmoxDockerDetection": "Детекција Докера", - "proxmoxDockerDetectionDesc": "Шаблони имена одвојени зарезима који омогућавају Докеру да упари госте", - "proxmoxPreferredRanges": "Жељени IP опсези", - "proxmoxPreferredRangesDesc": "Префикси одвојени зарезима по редоследу приоритета за избор IP адресе када гост има више интерфејса", - "proxmoxAutoSync": "Аутоматска синхронизација гостију", - "proxmoxAutoSyncDesc": "Периодично откривајте овај Proxmox чвор и креирајте или ажурирајте увезене гостујуће хостове док је ваша сесија откључана.", - "proxmoxSyncInterval": "Интервал синхронизације (минути)", - "proxmoxSyncIntervalDesc": "Минимум 5 минута. Планер прескаче закључане корисничке сесије.", - "proxmoxMarkMissing": "Означи госте који недостају", - "proxmoxMarkMissingDesc": "Додајте ознаку proxmox-missing када претходно увезени гост нестане уместо да га избришете.", - "proxmoxLastSync": "Последња синхронизација", - "proxmoxLastSyncNever": "Још није синхронизовано", - "proxmoxLastSyncNoResult": "Нема детаља о резултатима", - "proxmoxLastSyncSummary": "{{created}} креирано, {{updated}} ажурирано, {{markedMissing}} недостаје, {{skipped}} прескочено", - "proxmoxLastSyncStatus": { - "error": "Неуспешно", - "pending": "На чекању", - "success": "Успех" - }, - "proxmoxDiscoverAction": "Откријте и увезите госте Proxmox-а", - "proxmoxImportTitle": "Увоз из Proxmox-а", - "proxmoxSelectHost": "Изаберите Proxmox хост…", - "proxmoxDiscover": "Откријте", - "proxmoxDiscovering": "Откривање…", - "proxmoxDiscoverGuests": "Откријте госте", - "proxmoxSelectAll": "Изабери све", - "proxmoxDeselectAll": "Поништи избор свих", - "proxmoxNoGuests": "Није пронађен ниједан гост на овом Proxmox чвору.", - "proxmoxResultImported": "{{count}} увезено", - "proxmoxResultUpdated": "{{count}} ажурирано", - "proxmoxResultFailed": "{{count}} није успело", - "proxmoxImportComplete": "Увоз Proxmox-а завршен: {{summary}}", - "proxmoxDiscoveryFailed": "Откривање није успело", - "proxmoxImportFailed": "Увоз није успео", - "docsLink": "Прикажи документе", - "hostNotFound": "Хост није пронађен", - "loadingHost": "Учитавање хоста...", - "none": "Ниједан", - "proxmoxGuestsSelected_one": "{{count}} гост, {{selected}} изабрано", - "proxmoxGuestsSelected_other": "{{count}} гостију, {{selected}} изабрано", - "proxmoxImportButton_one": "Увоз {{count}} хоста", - "proxmoxImportButton_other": "Увези {{count}} хостова" - }, - "terminal": { - "connected": "Повезано" - } -} diff --git a/plugins/proxmox/locales/translated/sv_SE.json b/plugins/proxmox/locales/translated/sv_SE.json deleted file mode 100644 index cafab0bbe..000000000 --- a/plugins/proxmox/locales/translated/sv_SE.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox-statistik" - }, - "proxmoxStats": { - "tabTitle": "Proxmox-statistik", - "cpuUsage": "CPU-användning", - "memoryUsage": "Minnesanvändning", - "diskUsage": "Diskanvändning", - "networkInterfaces": "Nätverksgränssnitt", - "uptime": "Drifttid", - "systemInfo": "Systeminformation", - "guestsSummary": "Gäster", - "storagePools": "Lagringspooler", - "clusterHealth": "Klusterhälsa", - "cpuCores_one": "{{count}} Kärna", - "cpuCores_other": "{{count}} Kärnor", - "used": "Begagnad", - "total": "Total", - "hostname": "Värdnamn", - "kernel": "Kärna", - "pveVersion": "PVE-version", - "name": "Namn", - "status": "Status", - "type": "Typ", - "cpu": "CPU", - "mem": "Mem", - "disk": "Disk", - "running": "Spring", - "stopped": "Stoppad", - "guestCounts": "{{running}}/{{total}} springer", - "inactive": "Inaktiv", - "notClustered": "Fristående nod (inte en del av ett kluster)", - "quorate": "Kvorat", - "notQuorate": "Inte kvoriöst", - "nodeOnline": "Online", - "nodeOffline": "Off-line", - "noHostSelected": "Ingen värd vald", - "pveshNotFound": "pvesh hittades inte - är detta en Proxmox-nod?", - "nodeDetectionFailed": "Det gick inte att fastställa ett giltigt Proxmox-nodnamn", - "connecting": "Ansluter...", - "connectionFailed": "Misslyckades med att ansluta till Proxmox-noden", - "refresh": "Uppdatera", - "searchGuests": "Sök gäster...", - "filterAll": "Alla", - "filterVm": "VM", - "filterLxc": "LXC", - "noGuests": "Inga virtuella maskiner eller LXC-behållare hittades på den här noden.", - "noGuestsMatch": "Inga gäster matchar de aktuella filtren." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Aktivera Proxmox-statistik", - "enableProxmoxStatsDesc": "Visa en Proxmox-statistikflik för den här värden med statistik för livenoder, gäster, lagring och kluster insamlad via SSH.", - "proxmoxStatsPollInterval": "Undersökningsintervall (sekunder)", - "proxmoxStatsPollIntervalDesc": "Minst 15 sekunder. Hur ofta Proxmox-nodstatistik ska uppdateras.", - "proxmoxStatsNodeOverride": "Åsidosättning av nodnamn", - "proxmoxStatsNodeOverrideDesc": "Åsidosätt automatisk detektering av Proxmox-nodnamnet (detekteras via värdnamn som standard).", - "proxmoxStatsNodeOverridePlaceholder": "Automatiskt identifierad från värdnamn", - "authTypeStepca": "Steg CA", - "proxmoxIntegration": "Proxmox-integration", - "enableProxmox": "Aktivera Proxmox", - "enableProxmoxDesc": "Markera denna värd som en Proxmox-nod. Aktiverar gästidentifiering och import direkt från denna värd.", - "proxmoxDefaultAuthType": "Standardautentiseringstyp", - "proxmoxDefaultAuthTypeDesc": "Autentiseringsmetod tillämpad på importerade gästvärdar. Välj den autentiseringstyp som matchar hur du ansluter till dina gäster.", - "authTypePassword": "Lösenord", - "authTypeKey": "SSH-nyckel", - "authTypeCredential": "Referenser", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Ingen", - "proxmoxDefaultCredential": "Standardautentiseringsuppgifter", - "proxmoxDefaultCredentialDesc": "Autentiseringsuppgifter som används för importerade gästvärdar när autentiseringstypen är inställd på Autentiseringsuppgifter.", - "proxmoxWindowsDetection": "Windows/RDP-detektering", - "proxmoxWindowsDetectionDesc": "Kommaavgränsade namnmönster som utlöser RDP istället för SSH (skiftlägeskänsliga)", - "proxmoxDockerDetection": "Docker-detektering", - "proxmoxDockerDetectionDesc": "Kommaavgränsade namnmönster som gör det möjligt för Docker att matcha gäster", - "proxmoxPreferredRanges": "Föredragna IP-intervall", - "proxmoxPreferredRangesDesc": "Kommaavgränsade prefix i prioritetsordning för IP-val när en gäst har flera gränssnitt", - "proxmoxAutoSync": "Synkronisera gäster automatiskt", - "proxmoxAutoSyncDesc": "Upptäck regelbundet denna Proxmox-nod och skapa eller uppdatera importerade gästvärdar medan din session är upplåst.", - "proxmoxSyncInterval": "Synkroniseringsintervall (minuter)", - "proxmoxSyncIntervalDesc": "Minst 5 minuter. Schemaläggaren hoppar över låsta användarsessioner.", - "proxmoxMarkMissing": "Markera saknade gäster", - "proxmoxMarkMissingDesc": "Lägg till en proxmox-missing-tagg när en tidigare importerad gäst försvinner istället för att ta bort den.", - "proxmoxLastSync": "Senaste synkronisering", - "proxmoxLastSyncNever": "Inte synkroniserad än", - "proxmoxLastSyncNoResult": "Inga resultatdetaljer", - "proxmoxLastSyncSummary": "{{created}} skapad, {{updated}} uppdaterad, {{markedMissing}} saknas, {{skipped}} hoppad över", - "proxmoxLastSyncStatus": { - "error": "Misslyckades", - "pending": "I avvaktan på", - "success": "Framgång" - }, - "proxmoxDiscoverAction": "Upptäck och importera Proxmox-gäster", - "proxmoxImportTitle": "Importera från Proxmox", - "proxmoxSelectHost": "Välj en Proxmox-värd…", - "proxmoxDiscover": "Upptäcka", - "proxmoxDiscovering": "Upptäcker…", - "proxmoxDiscoverGuests": "Upptäck gäster", - "proxmoxSelectAll": "Markera alla", - "proxmoxDeselectAll": "Avmarkera alla", - "proxmoxNoGuests": "Inga gäster hittades på denna Proxmox-nod.", - "proxmoxResultImported": "{{count}} importerad", - "proxmoxResultUpdated": "{{count}} uppdaterad", - "proxmoxResultFailed": "{{count}} misslyckades", - "proxmoxImportComplete": "Proxmox-import klar: {{summary}}", - "proxmoxDiscoveryFailed": "Upptäckten misslyckades", - "proxmoxImportFailed": "Importen misslyckades", - "docsLink": "Visa dokument", - "hostNotFound": "Värden hittades inte", - "loadingHost": "Laddar värd...", - "none": "Ingen", - "proxmoxGuestsSelected_one": "{{count}} gäst, {{selected}} vald", - "proxmoxGuestsSelected_other": "{{count}} gäster, {{selected}} vald", - "proxmoxImportButton_one": "Importera {{count}} värd", - "proxmoxImportButton_other": "Importera {{count}} värdar" - }, - "terminal": { - "connected": "Ansluten" - } -} diff --git a/plugins/proxmox/locales/translated/th_TH.json b/plugins/proxmox/locales/translated/th_TH.json deleted file mode 100644 index 686cc0f2a..000000000 --- a/plugins/proxmox/locales/translated/th_TH.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "สถิติ Proxmox" - }, - "proxmoxStats": { - "tabTitle": "สถิติ Proxmox", - "cpuUsage": "การใช้งาน CPU", - "memoryUsage": "การใช้งานหน่วยความจำ", - "diskUsage": "การใช้งานดิสก์", - "networkInterfaces": "อินเทอร์เฟซเครือข่าย", - "uptime": "เวลาใช้งาน", - "systemInfo": "ข้อมูลระบบ", - "guestsSummary": "แขก", - "storagePools": "สระเก็บน้ำ", - "clusterHealth": "สุขภาพคลัสเตอร์", - "cpuCores_one": "{{count}} แกนหลัก", - "cpuCores_other": "{{count}} คอร์", - "used": "ใช้แล้ว", - "total": "ทั้งหมด", - "hostname": "ชื่อโฮสต์", - "kernel": "เคอร์เนล", - "pveVersion": "เวอร์ชัน PVE", - "name": "ชื่อ", - "status": "สถานะ", - "type": "พิมพ์", - "cpu": "ซีพียู", - "mem": "เมม", - "disk": "ดิสก์", - "running": "วิ่ง", - "stopped": "หยุด", - "guestCounts": "{{running}}/{{total}} กำลังทำงาน", - "inactive": "ไม่ใช้งาน", - "notClustered": "โหนดแบบสแตนด์อโลน (ไม่ได้เป็นส่วนหนึ่งของคลัสเตอร์)", - "quorate": "ควอต", - "notQuorate": "องค์ประชุมไม่ครบ", - "nodeOnline": "ออนไลน์", - "nodeOffline": "ออฟไลน์", - "noHostSelected": "ไม่ได้เลือกโฮสต์", - "pveshNotFound": "ไม่พบ pvesh - นี่เป็นโหนด Proxmox หรือไม่?", - "nodeDetectionFailed": "ไม่สามารถระบุชื่อโหนด Proxmox ที่ถูกต้องได้", - "connecting": "กำลังเชื่อมต่อ...", - "connectionFailed": "ไม่สามารถเชื่อมต่อกับโหนด Proxmox ได้", - "refresh": "รีเฟรช", - "searchGuests": "ค้นหาผู้เข้าพัก...", - "filterAll": "ทั้งหมด", - "filterVm": "วีเอ็ม", - "filterLxc": "LXC", - "noGuests": "ไม่พบเครื่องเสมือน (VM) หรือคอนเทนเนอร์ LXC บนโหนดนี้", - "noGuestsMatch": "ไม่มีแขกท่านใดตรงกับตัวกรองที่มีอยู่ในขณะนี้" - }, - "hosts": { - "tabProxmox": "โปรกซ์ม็อกซ์", - "enableProxmoxStats": "เปิดใช้งานสถิติ Proxmox", - "enableProxmoxStatsDesc": "แสดงแท็บสถิติ Proxmox สำหรับโฮสต์นี้ พร้อมสถิติแบบเรียลไทม์ของโหนด เกสต์ พื้นที่จัดเก็บ และคลัสเตอร์ที่รวบรวมผ่าน SSH", - "proxmoxStatsPollInterval": "ช่วงเวลาการสำรวจ (วินาที)", - "proxmoxStatsPollIntervalDesc": "อย่างน้อย 15 วินาที ความถี่ในการอัปเดตสถิติโหนด Proxmox", - "proxmoxStatsNodeOverride": "การแทนที่ชื่อโหนด", - "proxmoxStatsNodeOverrideDesc": "ยกเลิกการตรวจจับชื่อโหนด Proxmox อัตโนมัติ (โดยค่าเริ่มต้นจะตรวจจับผ่านชื่อโฮสต์)", - "proxmoxStatsNodeOverridePlaceholder": "ตรวจจับอัตโนมัติจากชื่อโฮสต์", - "authTypeStepca": "ขั้นตอน CA", - "proxmoxIntegration": "การผสานรวม Proxmox", - "enableProxmox": "เปิดใช้งาน Proxmox", - "enableProxmoxDesc": "กำหนดให้โฮสต์นี้เป็นโหนด Proxmox เพื่อเปิดใช้งานการค้นหาและนำเข้าเครื่องเสมือนโดยตรงจากโฮสต์นี้", - "proxmoxDefaultAuthType": "ประเภทการตรวจสอบสิทธิ์เริ่มต้น", - "proxmoxDefaultAuthTypeDesc": "วิธีการตรวจสอบสิทธิ์ที่ใช้กับโฮสต์แขกที่นำเข้า เลือกประเภทการตรวจสอบสิทธิ์ที่ตรงกับวิธีการที่คุณเชื่อมต่อกับแขกของคุณ", - "authTypePassword": "รหัสผ่าน", - "authTypeKey": "คีย์ SSH", - "authTypeCredential": "ใบรับรอง", - "authTypeOpkssh": "โอพีเคเอสเอช", - "authTypeNone": "ไม่มี", - "proxmoxDefaultCredential": "ข้อมูลประจำตัวเริ่มต้น", - "proxmoxDefaultCredentialDesc": "ข้อมูลประจำตัวที่ใช้สำหรับโฮสต์แขกที่นำเข้าเมื่อตั้งค่าประเภทการตรวจสอบสิทธิ์เป็นข้อมูลประจำตัว (Credential)", - "proxmoxWindowsDetection": "การตรวจจับ Windows / RDP", - "proxmoxWindowsDetectionDesc": "รูปแบบชื่อที่คั่นด้วยเครื่องหมายจุลภาคซึ่งจะเรียกใช้งาน RDP แทน SSH (ไม่คำนึงถึงตัวพิมพ์ใหญ่เล็ก)", - "proxmoxDockerDetection": "การตรวจจับ Docker", - "proxmoxDockerDetectionDesc": "รูปแบบชื่อที่คั่นด้วยเครื่องหมายจุลภาค ซึ่งช่วยให้ Docker สามารถจับคู่กับเครื่องเสมือนได้", - "proxmoxPreferredRanges": "ช่วง IP ที่ต้องการ", - "proxmoxPreferredRangesDesc": "คำนำหน้า IP ที่คั่นด้วยเครื่องหมายจุลภาคเรียงตามลำดับความสำคัญสำหรับการเลือก IP เมื่อผู้ใช้งานมีอินเทอร์เฟซหลายตัว", - "proxmoxAutoSync": "ซิงค์ข้อมูลแขกโดยอัตโนมัติ", - "proxmoxAutoSyncDesc": "ตรวจสอบโหนด Proxmox นี้เป็นระยะ และสร้างหรืออัปเดตโฮสต์แขกที่นำเข้าในขณะที่เซสชันของคุณไม่ได้ล็อกอยู่", - "proxmoxSyncInterval": "ช่วงเวลาการซิงค์ (นาที)", - "proxmoxSyncIntervalDesc": "อย่างน้อย 5 นาที ระบบกำหนดเวลาจะข้ามเซสชันของผู้ใช้ที่ถูกล็อกไว้", - "proxmoxMarkMissing": "ทำเครื่องหมายแขกที่ขาดหายไป", - "proxmoxMarkMissingDesc": "เพิ่มแท็ก proxmox-missing เมื่อเครื่องเสมือนที่นำเข้าก่อนหน้านี้หายไป แทนที่จะลบออก", - "proxmoxLastSync": "การซิงค์ครั้งล่าสุด", - "proxmoxLastSyncNever": "ยังไม่ได้ซิงค์ข้อมูล", - "proxmoxLastSyncNoResult": "ไม่มีรายละเอียดผลลัพธ์", - "proxmoxLastSyncSummary": "{{created}} ถูกสร้างขึ้น, {{updated}} อัปเดตแล้ว, {{markedMissing}} หายไป, {{skipped}} ข้ามไป", - "proxmoxLastSyncStatus": { - "error": "ล้มเหลว", - "pending": "รอดำเนินการ", - "success": "ความสำเร็จ" - }, - "proxmoxDiscoverAction": "ค้นหาและนำเข้าแขกของ Proxmox", - "proxmoxImportTitle": "นำเข้าจาก Proxmox", - "proxmoxSelectHost": "เลือกโฮสต์ Proxmox…", - "proxmoxDiscover": "ค้นพบ", - "proxmoxDiscovering": "การค้นพบ…", - "proxmoxDiscoverGuests": "ค้นพบแขก", - "proxmoxSelectAll": "เลือกทั้งหมด", - "proxmoxDeselectAll": "ยกเลิกการเลือกทั้งหมด", - "proxmoxNoGuests": "ไม่พบผู้ใช้งานในโหนด Proxmox นี้", - "proxmoxResultImported": "{{count}} นำเข้าแล้ว", - "proxmoxResultUpdated": "{{count}} อัปเดตแล้ว", - "proxmoxResultFailed": "{{count}} ล้มเหลว", - "proxmoxImportComplete": "การนำเข้า Proxmox เสร็จสมบูรณ์: {{summary}}", - "proxmoxDiscoveryFailed": "การค้นพบล้มเหลว", - "proxmoxImportFailed": "การนำเข้าล้มเหลว", - "docsLink": "ดูเอกสาร", - "hostNotFound": "ไม่พบโฮสต์", - "loadingHost": "กำลังโหลดโฮสต์...", - "none": "ไม่มี", - "proxmoxGuestsSelected_one": "{{count}} แขก, {{selected}} ผู้ถูกเลือก", - "proxmoxGuestsSelected_other": "แขก {{count}} คน, เลือก {{selected}} คน", - "proxmoxImportButton_one": "นำเข้าโฮสต์ {{count}}", - "proxmoxImportButton_other": "นำเข้าโฮสต์ {{count}}" - }, - "terminal": { - "connected": "เชื่อมต่อแล้ว" - } -} diff --git a/plugins/proxmox/locales/translated/tr_TR.json b/plugins/proxmox/locales/translated/tr_TR.json deleted file mode 100644 index 765f2d44d..000000000 --- a/plugins/proxmox/locales/translated/tr_TR.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox İstatistikleri" - }, - "proxmoxStats": { - "tabTitle": "Proxmox İstatistikleri", - "cpuUsage": "CPU Kullanımı", - "memoryUsage": "Bellek Kullanımı", - "diskUsage": "Disk Kullanımı", - "networkInterfaces": "Ağ Arayüzleri", - "uptime": "Çalışma Süresi", - "systemInfo": "Sistem Bilgisi", - "guestsSummary": "Misafirler", - "storagePools": "Depolama Havuzları", - "clusterHealth": "Küme Sağlığı", - "cpuCores_one": "{{count}} Çekirdek", - "cpuCores_other": "{{count}} Çekirdek", - "used": "Kullanılan", - "total": "Toplam", - "hostname": "Ana bilgisayar adı", - "kernel": "Çekirdek", - "pveVersion": "PVE Sürümü", - "name": "Ad", - "status": "Durum", - "type": "Tür", - "cpu": "CPU", - "mem": "Mem", - "disk": "Disk", - "running": "Çalışıyor", - "stopped": "Durduruldu", - "guestCounts": "{{running}}/{{total}} çalışıyor", - "inactive": "Etkin Değil", - "notClustered": "Bağımsız düğüm (kümenin parçası olmayan)", - "quorate": "Quora", - "notQuorate": "Yeterli çoğunluğa sahip değil", - "nodeOnline": "çevrimiçi", - "nodeOffline": "Çevrimdışı", - "noHostSelected": "Sunucu seçilmedi", - "pveshNotFound": "pvesh bulunamadı - bu bir Proxmox düğümü mü?", - "nodeDetectionFailed": "Geçerli bir Proxmox düğüm adı belirlenemedi.", - "connecting": "Bağlanıyor...", - "connectionFailed": "Proxmox düğümüne bağlanılamadı.", - "refresh": "Yenile", - "searchGuests": "Misafirleri ara...", - "filterAll": "Tümü", - "filterVm": "Sanal makine", - "filterLxc": "LXC", - "noGuests": "Bu düğümde hiçbir sanal makine veya LXC kapsayıcısı bulunamadı.", - "noGuestsMatch": "Mevcut filtrelere uyan konuk bulunamadı." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Proxmox İstatistiklerini Etkinleştir", - "enableProxmoxStatsDesc": "Bu sunucu için, SSH üzerinden toplanan canlı düğüm, sanal makine, depolama ve küme istatistiklerini içeren bir Proxmox İstatistikleri sekmesi gösterin.", - "proxmoxStatsPollInterval": "Anket aralığı (saniye)", - "proxmoxStatsPollIntervalDesc": "En az 15 saniye. Proxmox düğüm istatistiklerinin ne sıklıkla yenilenmesi gerektiği.", - "proxmoxStatsNodeOverride": "Düğüm adı geçersiz kılma", - "proxmoxStatsNodeOverrideDesc": "Proxmox düğüm adının otomatik olarak algılanmasını (varsayılan olarak ana bilgisayar adı üzerinden algılanır) geçersiz kılın.", - "proxmoxStatsNodeOverridePlaceholder": "Ana bilgisayar adından otomatik olarak algılandı", - "authTypeStepca": "Adım CA", - "proxmoxIntegration": "Proxmox Entegrasyonu", - "enableProxmox": "Proxmox'u Etkinleştir", - "enableProxmoxDesc": "Bu ana bilgisayarı bir Proxmox düğümü olarak işaretle. Bu ana bilgisayardan doğrudan konuk keşfi ve içe aktarmayı etkinleştirir.", - "proxmoxDefaultAuthType": "Varsayılan Kimlik Doğrulama Türü", - "proxmoxDefaultAuthTypeDesc": "İçe aktarılan konuk ana bilgisayarlara uygulanan kimlik doğrulama yöntemi. Konuklarınıza nasıl bağlandığınıza uygun kimlik doğrulama türünü seçin.", - "authTypePassword": "Parola", - "authTypeKey": "SSH Anahtarı", - "authTypeCredential": "Kimlik Bilgisi", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Yok", - "proxmoxDefaultCredential": "Varsayılan Kimlik Bilgisi", - "proxmoxDefaultCredentialDesc": "Kimlik doğrulama türü Kimlik Bilgisi olarak ayarlandığında içe aktarılan misafir ana makineleri için kullanılan kimlik bilgisi.", - "proxmoxWindowsDetection": "Windows / RDP algılama", - "proxmoxWindowsDetectionDesc": "SSH yerine RDP'yi tetikleyen virgülle ayrılmış ad desenleri (büyük/küçük harf duyarsız)", - "proxmoxDockerDetection": "Docker algılama", - "proxmoxDockerDetectionDesc": "Eşleşen misafirler için Docker'ı etkinleştiren virgülle ayrılmış ad desenleri", - "proxmoxPreferredRanges": "Tercih edilen IP aralıkları", - "proxmoxPreferredRangesDesc": "Bir misafirde birden fazla arayüz olduğunda IP seçimi için öncelik sırasına göre virgülle ayrılmış önekler", - "proxmoxAutoSync": "Misafirleri otomatik eşitle", - "proxmoxAutoSyncDesc": "Oturumunuz açıkken bu Proxmox düğümünü periyodik olarak keşfet ve içe aktarılan misafir ana makinelerini oluştur veya güncelle.", - "proxmoxSyncInterval": "Eşitleme aralığı (dakika)", - "proxmoxSyncIntervalDesc": "En az 5 dakika. Zamanlayıcı kilitli kullanıcı oturumlarını atlar.", - "proxmoxMarkMissing": "Kayıp misafirleri işaretle", - "proxmoxMarkMissingDesc": "Daha önce içe aktarılan bir misafir kaybolduğunda onu silmek yerine bir proxmox-missing etiketi ekle.", - "proxmoxLastSync": "Son eşitleme", - "proxmoxLastSyncNever": "Henüz eşitlenmedi", - "proxmoxLastSyncNoResult": "Sonuç detayı yok", - "proxmoxLastSyncSummary": "{{created}} oluşturuldu, {{updated}} güncellendi, {{markedMissing}} kayıp işaretlendi, {{skipped}} atlandı", - "proxmoxLastSyncStatus": { - "error": "Başarısız", - "pending": "Beklemede", - "success": "Başarılı" - }, - "proxmoxDiscoverAction": "Proxmox misafirlerini keşfet ve içe aktar", - "proxmoxImportTitle": "Proxmox'tan içe aktar", - "proxmoxSelectHost": "Bir Proxmox ana makinesi seç…", - "proxmoxDiscover": "Keşfet", - "proxmoxDiscovering": "Keşfediliyor…", - "proxmoxDiscoverGuests": "Misafirleri keşfet", - "proxmoxSelectAll": "Tümünü seç", - "proxmoxDeselectAll": "Tüm seçimi kaldır", - "proxmoxNoGuests": "Bu Proxmox düğümünde hiç misafir bulunamadı.", - "proxmoxResultImported": "{{count}} içe aktarıldı", - "proxmoxResultUpdated": "{{count}} güncellendi", - "proxmoxResultFailed": "{{count}} başarısız", - "proxmoxImportComplete": "Proxmox içe aktarma tamamlandı: {{summary}}", - "proxmoxDiscoveryFailed": "Keşif başarısız", - "proxmoxImportFailed": "İçe aktarma başarısız", - "docsLink": "Dokümanları görüntüle", - "hostNotFound": "Host bulunamadı", - "loadingHost": "Host yükleniyor...", - "none": "Hiçbiri", - "proxmoxGuestsSelected_one": "{{count}} misafir, {{selected}} seçildi", - "proxmoxGuestsSelected_other": "{{count}} misafir, {{selected}} seçildi", - "proxmoxImportButton_one": "{{count}} ana makineyi içe aktar", - "proxmoxImportButton_other": "{{count}} sunucuyu içe aktar" - }, - "terminal": { - "connected": "Bağlandı" - } -} diff --git a/plugins/proxmox/locales/translated/uk_UA.json b/plugins/proxmox/locales/translated/uk_UA.json deleted file mode 100644 index fb10a9a8b..000000000 --- a/plugins/proxmox/locales/translated/uk_UA.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Статистика Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Статистика Proxmox", - "cpuUsage": "Використання процесора", - "memoryUsage": "Використання пам'яті", - "diskUsage": "Використання диска", - "networkInterfaces": "Мережеві інтерфейси", - "uptime": "Час безвідмовної роботи", - "systemInfo": "Інформація про систему", - "guestsSummary": "Гості", - "storagePools": "Пули зберігання", - "clusterHealth": "Кластерне здоров'я", - "cpuCores_one": "{{count}} Ядро", - "cpuCores_other": "{{count}} Ядра", - "used": "Використано", - "total": "Всього", - "hostname": "Ім'я хоста", - "kernel": "Ядро", - "pveVersion": "ПВЕ-версія", - "name": "Ім'я", - "status": "Статус", - "type": "Тип", - "cpu": "Процесор", - "mem": "Мем", - "disk": "Диск", - "running": "Біг", - "stopped": "Зупинено", - "guestCounts": "{{running}}/{{total}} бігає", - "inactive": "Неактивний", - "notClustered": "Автономний вузол (не є частиною кластера)", - "quorate": "Кворат", - "notQuorate": "Не має кворуму", - "nodeOnline": "Онлайн", - "nodeOffline": "Офлайн", - "noHostSelected": "Хост не вибрано", - "pveshNotFound": "pvesh не знайдено - це вузол Proxmox?", - "nodeDetectionFailed": "Не вдається визначити дійсну назву вузла Proxmox", - "connecting": "Підключення...", - "connectionFailed": "Не вдалося підключитися до вузла Proxmox", - "refresh": "Оновити", - "searchGuests": "Пошук гостей...", - "filterAll": "Усі", - "filterVm": "ВМ", - "filterLxc": "LXC", - "noGuests": "На цьому вузлі не знайдено віртуальних машин або контейнерів LXC.", - "noGuestsMatch": "Немає гостей, які відповідають поточним фільтрам." - }, - "hosts": { - "tabProxmox": "Проксмокс", - "enableProxmoxStats": "Увімкнути статистику Proxmox", - "enableProxmoxStatsDesc": "Показати вкладку статистики Proxmox для цього хоста зі статистикою вузла, гостя, сховища та кластера, зібраною через SSH.", - "proxmoxStatsPollInterval": "Інтервал опитування (секунди)", - "proxmoxStatsPollIntervalDesc": "Мінімум 15 секунд. Як часто оновлювати статистику вузла Proxmox.", - "proxmoxStatsNodeOverride": "Перевизначення назви вузла", - "proxmoxStatsNodeOverrideDesc": "Перевизначити автоматичне визначення імені вузла Proxmox (за замовчуванням визначається через ім'я хоста).", - "proxmoxStatsNodeOverridePlaceholder": "Автоматично визначено за назвою хоста", - "authTypeStepca": "Крок CA", - "proxmoxIntegration": "Інтеграція з Proxmox", - "enableProxmox": "Увімкнути Proxmox", - "enableProxmoxDesc": "Позначити цей хост як вузол Proxmox. Дозволяє виявлення гостьових систем та імпорт безпосередньо з цього хоста.", - "proxmoxDefaultAuthType": "Тип автентифікації за замовчуванням", - "proxmoxDefaultAuthTypeDesc": "Метод автентифікації, застосований до імпортованих гостьових хостів. Виберіть тип автентифікації, який відповідає способу підключення до ваших гостей.", - "authTypePassword": "Пароль", - "authTypeKey": "SSH-ключ", - "authTypeCredential": "Посвідчення", - "authTypeOpkssh": "ОПКШ", - "authTypeNone": "Жоден", - "proxmoxDefaultCredential": "Облікові дані за замовчуванням", - "proxmoxDefaultCredentialDesc": "Облікові дані, що використовуються для імпортованих гостьових хостів, коли для типу автентифікації встановлено значення \"Облікові дані\".", - "proxmoxWindowsDetection": "Виявлення Windows / RDP", - "proxmoxWindowsDetectionDesc": "Шаблони імен, розділені комами, які запускають RDP замість SSH (без урахування регістру)", - "proxmoxDockerDetection": "Виявлення Docker", - "proxmoxDockerDetectionDesc": "Шаблони імен, розділені комами, що дозволяють Docker знаходити гостьові файли в відповідниках", - "proxmoxPreferredRanges": "Бажані діапазони IP-адрес", - "proxmoxPreferredRangesDesc": "Префікси, розділені комами, у порядку пріоритетності для вибору IP-адреси, коли гость має кілька інтерфейсів", - "proxmoxAutoSync": "Автоматична синхронізація гостей", - "proxmoxAutoSyncDesc": "Періодично виявляйте цей вузол Proxmox та створюйте або оновлюйте імпортовані гостьові хости, поки ваш сеанс розблоковано.", - "proxmoxSyncInterval": "Інтервал синхронізації (хвилини)", - "proxmoxSyncIntervalDesc": "Мінімум 5 хвилин. Планувальник пропускає заблоковані сеанси користувачів.", - "proxmoxMarkMissing": "Позначте відсутніх гостей", - "proxmoxMarkMissingDesc": "Додати тег proxmox-missing, коли раніше імпортований гостьовий сервер зникає, замість його видалення.", - "proxmoxLastSync": "Остання синхронізація", - "proxmoxLastSyncNever": "Ще не синхронізовано", - "proxmoxLastSyncNoResult": "Немає деталей результату", - "proxmoxLastSyncSummary": "{{created}} створено, {{updated}} оновлено, {{markedMissing}} відсутнє, {{skipped}} пропущено", - "proxmoxLastSyncStatus": { - "error": "Не вдалося", - "pending": "Очікує на розгляд", - "success": "Успіх" - }, - "proxmoxDiscoverAction": "Пошук та імпорт гостьових систем Proxmox", - "proxmoxImportTitle": "Імпорт з Proxmox", - "proxmoxSelectHost": "Виберіть хост Proxmox…", - "proxmoxDiscover": "Відкрийте", - "proxmoxDiscovering": "Відкриття…", - "proxmoxDiscoverGuests": "Відкрийте для себе гостей", - "proxmoxSelectAll": "Вибрати все", - "proxmoxDeselectAll": "Зняти вибір усіх", - "proxmoxNoGuests": "На цьому вузлі Proxmox не знайдено гостей.", - "proxmoxResultImported": "{{count}} імпортовано", - "proxmoxResultUpdated": "{{count}} оновлено", - "proxmoxResultFailed": "{{count}} не вдалося", - "proxmoxImportComplete": "Імпорт Proxmox завершено: {{summary}}", - "proxmoxDiscoveryFailed": "Не вдалося знайти", - "proxmoxImportFailed": "Імпорт не вдалося", - "docsLink": "Переглянути документи", - "hostNotFound": "Хост не знайдено", - "loadingHost": "Завантаження хоста...", - "none": "Жоден", - "proxmoxGuestsSelected_one": "{{count}} гість, {{selected}} вибрано", - "proxmoxGuestsSelected_other": "{{count}} гостей, {{selected}} вибрано", - "proxmoxImportButton_one": "Імпортувати хост {{count}}", - "proxmoxImportButton_other": "Імпортувати {{count}} хостів" - }, - "terminal": { - "connected": "Підключено" - } -} diff --git a/plugins/proxmox/locales/translated/vi_VN.json b/plugins/proxmox/locales/translated/vi_VN.json deleted file mode 100644 index 16f5a4c14..000000000 --- a/plugins/proxmox/locales/translated/vi_VN.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Thống kê Proxmox" - }, - "proxmoxStats": { - "tabTitle": "Thống kê Proxmox", - "cpuUsage": "Sử dụng CPU", - "memoryUsage": "Sử dụng bộ nhớ", - "diskUsage": "Mức sử dụng ổ đĩa", - "networkInterfaces": "Giao diện mạng", - "uptime": "Thời gian hoạt động", - "systemInfo": "Thông tin hệ thống", - "guestsSummary": "Khách", - "storagePools": "Các bể chứa", - "clusterHealth": "Cluster Health", - "cpuCores_one": "{{count}} Lõi", - "cpuCores_other": "{{count}} Lõi", - "used": "Đã dùng", - "total": "Tổng cộng", - "hostname": "Tên máy chủ", - "kernel": "Nhân", - "pveVersion": "Phiên bản PVE", - "name": "Tên", - "status": "Trạng thái", - "type": "Loại", - "cpu": "CPU", - "mem": "Mem", - "disk": "Ổ đĩa", - "running": "Đang chạy", - "stopped": "Đã dừng", - "guestCounts": "{{running}}/{{total}} đang chạy", - "inactive": "Không hoạt động", - "notClustered": "Nút độc lập (không thuộc cụm máy chủ)", - "quorate": "Quorate", - "notQuorate": "Không đủ số lượng", - "nodeOnline": "Trực tuyến", - "nodeOffline": "Ngoại tuyến", - "noHostSelected": "Chưa chọn máy chủ", - "pveshNotFound": "Không tìm thấy pvesh - đây có phải là một node Proxmox không?", - "nodeDetectionFailed": "Không thể xác định tên nút Proxmox hợp lệ.", - "connecting": "Đang kết nối...", - "connectionFailed": "Không thể kết nối đến nút Proxmox.", - "refresh": "Làm mới", - "searchGuests": "Tìm kiếm khách...", - "filterAll": "Tất cả", - "filterVm": "Máy ảo", - "filterLxc": "LXC", - "noGuests": "Không tìm thấy máy ảo hoặc vùng chứa LXC nào trên nút này.", - "noGuestsMatch": "Hiện không có khách nào phù hợp với bộ lọc hiện tại." - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "Bật thống kê Proxmox", - "enableProxmoxStatsDesc": "Hiển thị tab Thống kê Proxmox cho máy chủ này với các số liệu thống kê trực tiếp về nút, máy ảo, bộ nhớ và cụm được thu thập qua SSH.", - "proxmoxStatsPollInterval": "Khoảng thời gian thăm dò (giây)", - "proxmoxStatsPollIntervalDesc": "Tối thiểu 15 giây. Tần suất làm mới số liệu thống kê của node Proxmox.", - "proxmoxStatsNodeOverride": "Ghi đè tên nút", - "proxmoxStatsNodeOverrideDesc": "Ghi đè tính năng tự động phát hiện tên nút Proxmox (mặc định được phát hiện thông qua tên máy chủ).", - "proxmoxStatsNodeOverridePlaceholder": "Tự động nhận diện từ tên máy chủ", - "authTypeStepca": "Bước CA", - "proxmoxIntegration": "Tích hợp Proxmox", - "enableProxmox": "Bật Proxmox", - "enableProxmoxDesc": "Đánh dấu máy chủ này là một node Proxmox. Cho phép khám phá và nhập khách trực tiếp từ máy chủ này.", - "proxmoxDefaultAuthType": "Loại xác thực mặc định", - "proxmoxDefaultAuthTypeDesc": "Phương thức xác thực áp dụng cho các máy khách được nhập. Chọn loại xác thực phù hợp với cách bạn kết nối đến máy khách của mình.", - "authTypePassword": "Mật khẩu", - "authTypeKey": "Khóa SSH", - "authTypeCredential": "Thông tin xác thực", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "Không", - "proxmoxDefaultCredential": "Thông tin đăng nhập mặc định", - "proxmoxDefaultCredentialDesc": "Thông tin xác thực dùng cho máy khách được nhập khi phương thức xác thực là Credential.", - "proxmoxWindowsDetection": "Phát hiện Windows / RDP", - "proxmoxWindowsDetectionDesc": "Các mẫu tên cách nhau bằng dấu phẩy để kích hoạt RDP thay vì SSH (không phân biệt hoa thường).", - "proxmoxDockerDetection": "Phát hiện Docker", - "proxmoxDockerDetectionDesc": "Các mẫu tên cách nhau bằng dấu phẩy để bật Docker cho các máy khách phù hợp.", - "proxmoxPreferredRanges": "Dải IP ưu tiên", - "proxmoxPreferredRangesDesc": "Các tiền tố cách nhau bằng dấu phẩy theo thứ tự ưu tiên để chọn IP khi máy khách có nhiều giao diện mạng.", - "proxmoxAutoSync": "Tự động đồng bộ máy khách", - "proxmoxAutoSyncDesc": "Định kỳ khám phá nút Proxmox này và tạo hoặc cập nhật máy khách đã nhập khi phiên của bạn được mở khóa.", - "proxmoxSyncInterval": "Khoảng đồng bộ (phút)", - "proxmoxSyncIntervalDesc": "Tối thiểu 5 phút. Bộ lập lịch bỏ qua các phiên người dùng bị khóa.", - "proxmoxMarkMissing": "Đánh dấu máy khách bị thiếu", - "proxmoxMarkMissingDesc": "Thêm thẻ proxmox-missing khi máy khách đã nhập trước đó biến mất thay vì xóa nó.", - "proxmoxLastSync": "Lần đồng bộ cuối", - "proxmoxLastSyncNever": "Chưa đồng bộ", - "proxmoxLastSyncNoResult": "Không có chi tiết kết quả", - "proxmoxLastSyncSummary": "Đã tạo {{created}}, cập nhật {{updated}}, thiếu {{markedMissing}}, bỏ qua {{skipped}}", - "proxmoxLastSyncStatus": { - "error": "Thất bại", - "pending": "Đang chờ", - "success": "Thành công" - }, - "proxmoxDiscoverAction": "Khám phá và nhập máy khách Proxmox", - "proxmoxImportTitle": "Nhập từ Proxmox", - "proxmoxSelectHost": "Chọn máy chủ Proxmox…", - "proxmoxDiscover": "Khám phá", - "proxmoxDiscovering": "Đang khám phá…", - "proxmoxDiscoverGuests": "Khám phá máy khách", - "proxmoxSelectAll": "Chọn tất cả", - "proxmoxDeselectAll": "Bỏ chọn tất cả", - "proxmoxNoGuests": "Không tìm thấy máy khách nào trên nút Proxmox này.", - "proxmoxResultImported": "Đã nhập {{count}}", - "proxmoxResultUpdated": "Đã cập nhật {{count}}", - "proxmoxResultFailed": "{{count}} thất bại", - "proxmoxImportComplete": "Hoàn tất nhập từ Proxmox: {{summary}}", - "proxmoxDiscoveryFailed": "Khám phá thất bại", - "proxmoxImportFailed": "Nhập thất bại", - "docsLink": "Xem tài liệu", - "hostNotFound": "Không tìm thấy máy chủ", - "loadingHost": "Đang tải máy chủ...", - "none": "Không có", - "proxmoxGuestsSelected_one": "{{count}} khách, {{selected}} được chọn", - "proxmoxGuestsSelected_other": "{{count}} khách, {{selected}} người được chọn", - "proxmoxImportButton_one": "Nhập {{count}} máy chủ", - "proxmoxImportButton_other": "Nhập {{count}} máy chủ" - }, - "terminal": { - "connected": "Đã kết nối" - } -} diff --git a/plugins/proxmox/locales/translated/zh_CN.json b/plugins/proxmox/locales/translated/zh_CN.json deleted file mode 100644 index b9fcac1e7..000000000 --- a/plugins/proxmox/locales/translated/zh_CN.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox 统计数据" - }, - "proxmoxStats": { - "tabTitle": "Proxmox 统计数据", - "cpuUsage": "CPU 使用率", - "memoryUsage": "内存使用量", - "diskUsage": "磁盘使用率", - "networkInterfaces": "网络接口", - "uptime": "运行时间", - "systemInfo": "系统信息", - "guestsSummary": "客户主机", - "storagePools": "存储池", - "clusterHealth": "集群健康", - "cpuCores_one": "{{count}} 核", - "cpuCores_other": "{{count}} 核", - "used": "已用", - "total": "总计", - "hostname": "主机名", - "kernel": "内核", - "pveVersion": "PVE 版本", - "name": "名称", - "status": "状态", - "type": "类型", - "cpu": "CPU", - "mem": "内存", - "disk": "磁盘", - "running": "运行中", - "stopped": "已停止", - "guestCounts": "{{running}}/{{total}} 运行中", - "inactive": "未激活", - "notClustered": "独立节点(非集群的一部分)", - "quorate": "达到法定节点数量", - "notQuorate": "未达到法定节点数量", - "nodeOnline": "在线", - "nodeOffline": "离线", - "noHostSelected": "未选择主机", - "pveshNotFound": "未找到 pvesh - 这是 Proxmox 节点吗?", - "nodeDetectionFailed": "无法确定有效的 Proxmox 节点名称", - "connecting": "连接中...", - "connectionFailed": "连接到 Proxmox 节点失败", - "refresh": "刷新", - "searchGuests": "搜索客户主机...", - "filterAll": "全部", - "filterVm": "虚拟机", - "filterLxc": "LXC", - "noGuests": "此节点上未发现虚拟机或 LXC 容器。", - "noGuestsMatch": "没有符合当前筛选条件的客户主机。" - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "启用 Proxmox 统计信息", - "enableProxmoxStatsDesc": "显示此主机的 Proxmox 统计信息标签页,其中包含通过 SSH 收集的实时节点、客户机、存储和集群统计信息。", - "proxmoxStatsPollInterval": "轮询间隔(秒)", - "proxmoxStatsPollIntervalDesc": "最短间隔 15 秒。Proxmox 节点统计信息刷新频率。", - "proxmoxStatsNodeOverride": "覆盖节点名称", - "proxmoxStatsNodeOverrideDesc": "覆盖自动检测的 Proxmox 节点名称(默认情况下通过主机名检测)。", - "proxmoxStatsNodeOverridePlaceholder": "从主机名自动检测", - "authTypeStepca": "步骤 CA", - "proxmoxIntegration": "Proxmox 集成", - "enableProxmox": "启用 Proxmox", - "enableProxmoxDesc": "将此主机标记为 Proxmox 节点。启用从此主机直接进行客户机发现和导入。", - "proxmoxDefaultAuthType": "默认认证类型", - "proxmoxDefaultAuthTypeDesc": "应用于导入的客户机主机的认证方法。选择与您连接客户机的方式相匹配的认证类型。", - "authTypePassword": "密码", - "authTypeKey": "SSH 密钥", - "authTypeCredential": "凭据", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "无", - "proxmoxDefaultCredential": "默认凭据", - "proxmoxDefaultCredentialDesc": "当认证类型设置为凭据时,用于导入的客户主机的凭据。", - "proxmoxWindowsDetection": "Windows / RDP 检测", - "proxmoxWindowsDetectionDesc": "逗号分隔的名称模式,匹配时触发 RDP 而非 SSH(不区分大小写)", - "proxmoxDockerDetection": "Docker 检测", - "proxmoxDockerDetectionDesc": "逗号分隔的名称模式,为匹配的客户主机启用 Docker", - "proxmoxPreferredRanges": "首选 IP 范围", - "proxmoxPreferredRangesDesc": "逗号分隔的前缀,按优先级顺序用于当客户主机有多个网络接口时的 IP 选择", - "proxmoxAutoSync": "自动同步客户主机", - "proxmoxAutoSyncDesc": "在会话未锁定时定期发现此 Proxmox 节点,并创建或更新导入的客户主机。", - "proxmoxSyncInterval": "同步间隔(分钟)", - "proxmoxSyncIntervalDesc": "最小 5 分钟。调度器会跳过已锁定的用户会话。", - "proxmoxMarkMissing": "标记缺失的客户主机", - "proxmoxMarkMissingDesc": "当之前导入的客户主机消失时,添加 proxmox-missing 标签,而非删除它。", - "proxmoxLastSync": "上次同步", - "proxmoxLastSyncNever": "尚未同步", - "proxmoxLastSyncNoResult": "无结果详情", - "proxmoxLastSyncSummary": "已创建 {{created}},已更新 {{updated}},已标记缺失 {{markedMissing}},已跳过 {{skipped}}", - "proxmoxLastSyncStatus": { - "error": "失败", - "pending": "待处理", - "success": "成功" - }, - "proxmoxDiscoverAction": "发现并导入 Proxmox 客户主机", - "proxmoxImportTitle": "从 Proxmox 导入", - "proxmoxSelectHost": "选择 Proxmox 主机…", - "proxmoxDiscover": "发现", - "proxmoxDiscovering": "正在发现…", - "proxmoxDiscoverGuests": "发现客户主机", - "proxmoxSelectAll": "全选", - "proxmoxDeselectAll": "取消全选", - "proxmoxNoGuests": "此 Proxmox 节点上未找到客户主机。", - "proxmoxResultImported": "已导入 {{count}} 个", - "proxmoxResultUpdated": "已更新 {{count}} 个", - "proxmoxResultFailed": "已失败 {{count}} 个", - "proxmoxImportComplete": "Proxmox 导入完成:{{summary}}", - "proxmoxDiscoveryFailed": "发现失败", - "proxmoxImportFailed": "导入失败", - "docsLink": "查看文档", - "hostNotFound": "未找到主机", - "loadingHost": "正在加载主机...", - "none": "无", - "proxmoxGuestsSelected_one": "{{count}} 访客, {{selected}} 已选", - "proxmoxGuestsSelected_other": "{{count}} 位客人, {{selected}} 位已选客人", - "proxmoxImportButton_one": "导入 {{count}} 个主机", - "proxmoxImportButton_other": "导入 {{count}} 个主机" - }, - "terminal": { - "connected": "已连接" - } -} diff --git a/plugins/proxmox/locales/translated/zh_TW.json b/plugins/proxmox/locales/translated/zh_TW.json deleted file mode 100644 index f01e786a1..000000000 --- a/plugins/proxmox/locales/translated/zh_TW.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "nav": { - "proxmoxStats": "Proxmox 統計數據" - }, - "proxmoxStats": { - "tabTitle": "Proxmox 統計數據", - "cpuUsage": "CPU 使用率", - "memoryUsage": "記憶體使用量", - "diskUsage": "磁碟使用率", - "networkInterfaces": "網路介面", - "uptime": "運行時間", - "systemInfo": "系統資訊", - "guestsSummary": "賓客", - "storagePools": "儲水池", - "clusterHealth": "集群健康", - "cpuCores_one": "{{count}} 核心", - "cpuCores_other": "{{count}} 核心", - "used": "已使用", - "total": "全部的", - "hostname": "主機名稱", - "kernel": "核心", - "pveVersion": "PVE 版本", - "name": "名稱", - "status": "狀態", - "type": "類型", - "cpu": "CPU", - "mem": "記憶", - "disk": "磁碟", - "running": "執行中", - "stopped": "已停止", - "guestCounts": "{{running}}/{{total}} 運作中", - "inactive": "未啟用", - "notClustered": "獨立節點(非叢集的一部分)", - "quorate": "法定人數", - "notQuorate": "未達法定人數", - "nodeOnline": "在線", - "nodeOffline": "離線", - "noHostSelected": "未選擇主機", - "pveshNotFound": "找不到 pvesh - 這是 Proxmox 節點嗎?", - "nodeDetectionFailed": "無法確定有效的 Proxmox 節點名稱", - "connecting": "連線中…", - "connectionFailed": "連線到 Proxmox 節點失敗", - "refresh": "重新整理", - "searchGuests": "搜尋賓客...", - "filterAll": "全選", - "filterVm": "虛擬機", - "filterLxc": "LXC", - "noGuests": "此節點上未發現虛擬機器或 LXC 容器。", - "noGuestsMatch": "沒有符合目前篩選條件的客人。" - }, - "hosts": { - "tabProxmox": "Proxmox", - "enableProxmoxStats": "啟用 Proxmox 統計訊息", - "enableProxmoxStatsDesc": "顯示此主機的 Proxmox 統計資料標籤,其中包含透過 SSH 收集的即時節點、虛擬機器、儲存和叢集統計資訊。", - "proxmoxStatsPollInterval": "輪詢間隔(秒)", - "proxmoxStatsPollIntervalDesc": "最短間隔 15 秒。 Proxmox 節點統計資訊的刷新頻率。", - "proxmoxStatsNodeOverride": "節點名稱覆蓋", - "proxmoxStatsNodeOverrideDesc": "覆蓋 Proxmox 節點名稱的自動偵測(預設透過主機名稱偵測)。", - "proxmoxStatsNodeOverridePlaceholder": "從主機名稱自動偵測", - "authTypeStepca": "步驟 CA", - "proxmoxIntegration": "Proxmox 整合", - "enableProxmox": "啟用 Proxmox", - "enableProxmoxDesc": "將此主機標記為 Proxmox 節點。啟用直接從此主機探索及匯入客體的功能。", - "proxmoxDefaultAuthType": "預設驗證類型", - "proxmoxDefaultAuthTypeDesc": "套用至匯入客體主機的驗證方法。選擇與您連線至客體的方式相符的驗證類型。", - "authTypePassword": "密碼", - "authTypeKey": "SSH 金鑰", - "authTypeCredential": "憑證", - "authTypeOpkssh": "OPKSSH", - "authTypeNone": "無", - "proxmoxDefaultCredential": "預設憑證", - "proxmoxDefaultCredentialDesc": "當驗證類型設為憑證時,用於已匯入之訪客主機的憑證。", - "proxmoxWindowsDetection": "Windows / RDP 偵測", - "proxmoxWindowsDetectionDesc": "逗號分隔的名稱模式,符合時將觸發 RDP 而非 SSH(不分大小寫)", - "proxmoxDockerDetection": "Docker 偵測", - "proxmoxDockerDetectionDesc": "逗號分隔的名稱模式,符合時將為對應的訪客主機啟用 Docker", - "proxmoxPreferredRanges": "偏好的 IP 範圍", - "proxmoxPreferredRangesDesc": "當訪客主機有多個網路介面時,以此逗號分隔的前綴清單依優先順序選取 IP", - "proxmoxAutoSync": "自動同步訪客主機", - "proxmoxAutoSyncDesc": "在您的工作階段解鎖時,定期探索此 Proxmox 節點並建立或更新已匯入的訪客主機。", - "proxmoxSyncInterval": "同步間隔(分鐘)", - "proxmoxSyncIntervalDesc": "最少 5 分鐘。排程器會略過鎖定的使用者工作階段。", - "proxmoxMarkMissing": "標記遺失的訪客主機", - "proxmoxMarkMissingDesc": "當先前匯入的訪客主機消失時,新增 proxmox-missing 標籤而非將其刪除。", - "proxmoxLastSync": "上次同步", - "proxmoxLastSyncNever": "尚未同步", - "proxmoxLastSyncNoResult": "無結果詳細資料", - "proxmoxLastSyncSummary": "建立 {{created}},更新 {{updated}},遺失 {{markedMissing}},略過 {{skipped}}", - "proxmoxLastSyncStatus": { - "error": "失敗", - "pending": "待處理", - "success": "成功" - }, - "proxmoxDiscoverAction": "探索並匯入 Proxmox 訪客主機", - "proxmoxImportTitle": "從 Proxmox 匯入", - "proxmoxSelectHost": "選取 Proxmox 主機…", - "proxmoxDiscover": "探索", - "proxmoxDiscovering": "探索中…", - "proxmoxDiscoverGuests": "探索訪客主機", - "proxmoxSelectAll": "全選", - "proxmoxDeselectAll": "取消全選", - "proxmoxNoGuests": "此 Proxmox 節點上找不到任何訪客主機。", - "proxmoxResultImported": "已匯入 {{count}} 台", - "proxmoxResultUpdated": "已更新 {{count}} 台", - "proxmoxResultFailed": "失敗 {{count}} 台", - "proxmoxImportComplete": "Proxmox 匯入完成:{{summary}}", - "proxmoxDiscoveryFailed": "探索失敗", - "proxmoxImportFailed": "匯入失敗", - "docsLink": "檢視文件", - "hostNotFound": "找不到主機", - "loadingHost": "正在載入主機...", - "none": "無", - "proxmoxGuestsSelected_one": "{{count}} 訪客, {{selected}} 已選", - "proxmoxGuestsSelected_other": "{{count}} 位客人, {{selected}} 位已選客人", - "proxmoxImportButton_one": "匯入 {{count}} 台主機", - "proxmoxImportButton_other": "匯入 {{count}} 台主機" - }, - "terminal": { - "connected": "已連線" - } -} diff --git a/plugins/proxmox/manifest.json b/plugins/proxmox/manifest.json deleted file mode 100644 index 7a1a6af5f..000000000 --- a/plugins/proxmox/manifest.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "id": "proxmox", - "name": "Proxmox", - "version": "1.0.0", - "description": "Find Proxmox VE guests through an SSH host, import them as hosts and keep them in sync.", - "author": { - "name": "Termix" - }, - "license": "MIT", - "repository": "https://github.com/Termix-SSH/Termix", - "category": "Infrastructure", - "icon": "Server", - "engine": { - "termix": ">=2.9.0", - "api": "1" - }, - "capabilities": [ - "hosts:read", - "hosts:write", - "credentials:use", - "credentials:read", - "ssh:connect", - "db:own", - "network:serve", - "ui:surface", - "users:impersonate" - ], - "contributes": { - "tabs": [ - { - "id": "proxmox-stats", - "titleKey": "nav.proxmoxStats", - "icon": "HardDrive", - "openFrom": ["host-context-menu", "palette"] - } - ], - "settings": { - "host": { - "fields": [ - { - "key": "enableProxmox", - "type": "custom", - "component": "proxmoxHostSettings", - "defaultable": false - }, - { - "key": "proxmoxConfig", - "type": "custom", - "component": "proxmoxHostSettings", - "defaultable": false - }, - { - "key": "enableProxmoxStats", - "type": "custom", - "component": "proxmoxHostSettings", - "defaultable": false - }, - { - "key": "proxmoxStatsConfig", - "type": "custom", - "component": "proxmoxHostSettings", - "defaultable": false - } - ] - } - } - }, - "locales": "locales", - "platforms": ["linux", "win32", "darwin"] -} diff --git a/plugins/proxmox/migrations/mysql/0001_adopt_proxmox_stats.sql b/plugins/proxmox/migrations/mysql/0001_adopt_proxmox_stats.sql deleted file mode 100644 index 472a363b3..000000000 --- a/plugins/proxmox/migrations/mysql/0001_adopt_proxmox_stats.sql +++ /dev/null @@ -1,26 +0,0 @@ --- proxmox 0001: adopt_proxmox_stats --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS `proxmox_node_history` ( - `id` int AUTO_INCREMENT PRIMARY KEY, - `host_id` int NOT NULL, - `ts` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - `cpu_percent` double, - `mem_percent` double, - `disk_percent` double, - `net_rx_bytes` int, - `net_tx_bytes` int, - FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE -); -RENAME TABLE `proxmox_node_history` TO `p_proxmox_node_history`; -CREATE TABLE IF NOT EXISTS `proxmox_stats_preferences` ( - `id` int AUTO_INCREMENT PRIMARY KEY, - `user_id` varchar(255) NOT NULL, - `host_id` int NOT NULL, - `layout` text NOT NULL, - `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, - FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE -); -RENAME TABLE `proxmox_stats_preferences` TO `p_proxmox_stats_preferences`; diff --git a/plugins/proxmox/migrations/postgres/0001_adopt_proxmox_stats.sql b/plugins/proxmox/migrations/postgres/0001_adopt_proxmox_stats.sql deleted file mode 100644 index 36b450c9a..000000000 --- a/plugins/proxmox/migrations/postgres/0001_adopt_proxmox_stats.sql +++ /dev/null @@ -1,27 +0,0 @@ --- proxmox 0001: adopt_proxmox_stats --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS "proxmox_node_history" ( - "id" serial PRIMARY KEY, - "host_id" integer NOT NULL, - "ts" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "cpu_percent" double precision, - "mem_percent" double precision, - "disk_percent" double precision, - "net_rx_bytes" integer, - "net_tx_bytes" integer, - FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE -); -ALTER TABLE "proxmox_node_history" RENAME TO "p_proxmox_node_history"; -CREATE TABLE IF NOT EXISTS "proxmox_stats_preferences" ( - "id" serial PRIMARY KEY, - "user_id" varchar(255) NOT NULL, - "host_id" integer NOT NULL, - "layout" text NOT NULL, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, - FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE -); -CREATE UNIQUE INDEX IF NOT EXISTS "idx_proxmox_stats_prefs_user_host" ON "proxmox_stats_preferences" ("user_id", "host_id"); -ALTER TABLE "proxmox_stats_preferences" RENAME TO "p_proxmox_stats_preferences"; diff --git a/plugins/proxmox/migrations/snapshot.json b/plugins/proxmox/migrations/snapshot.json deleted file mode 100644 index 9665ca851..000000000 --- a/plugins/proxmox/migrations/snapshot.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "version": 1, - "tables": { - "node_history": { - "columns": { - "id": { - "type": "id", - "primaryKey": true, - "notNull": true - }, - "hostId": { - "type": "refHost", - "notNull": true - }, - "ts": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - }, - "cpuPercent": { - "type": "real" - }, - "memPercent": { - "type": "real" - }, - "diskPercent": { - "type": "real" - }, - "netRxBytes": { - "type": "integer" - }, - "netTxBytes": { - "type": "integer" - } - }, - "indexes": [], - "adopts": "proxmox_node_history" - }, - "stats_preferences": { - "columns": { - "id": { - "type": "id", - "primaryKey": true, - "notNull": true - }, - "userId": { - "type": "refUser", - "length": 255, - "notNull": true - }, - "hostId": { - "type": "refHost", - "notNull": true - }, - "layout": { - "type": "text", - "notNull": true - }, - "createdAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - }, - "updatedAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - } - }, - "indexes": [ - { - "name": "idx_proxmox_stats_prefs_user_host", - "columns": ["userId", "hostId"], - "unique": true - } - ], - "adopts": "proxmox_stats_preferences" - } - } -} diff --git a/plugins/proxmox/migrations/sqlite/0001_adopt_proxmox_stats.sql b/plugins/proxmox/migrations/sqlite/0001_adopt_proxmox_stats.sql deleted file mode 100644 index 19bc34bd7..000000000 --- a/plugins/proxmox/migrations/sqlite/0001_adopt_proxmox_stats.sql +++ /dev/null @@ -1,27 +0,0 @@ --- proxmox 0001: adopt_proxmox_stats --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS "proxmox_node_history" ( - "id" integer PRIMARY KEY AUTOINCREMENT, - "host_id" integer NOT NULL, - "ts" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "cpu_percent" real, - "mem_percent" real, - "disk_percent" real, - "net_rx_bytes" integer, - "net_tx_bytes" integer, - FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE -); -ALTER TABLE "proxmox_node_history" RENAME TO "p_proxmox_node_history"; -CREATE TABLE IF NOT EXISTS "proxmox_stats_preferences" ( - "id" integer PRIMARY KEY AUTOINCREMENT, - "user_id" text NOT NULL, - "host_id" integer NOT NULL, - "layout" text NOT NULL, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, - FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE -); -CREATE UNIQUE INDEX IF NOT EXISTS "idx_proxmox_stats_prefs_user_host" ON "proxmox_stats_preferences" ("user_id", "host_id"); -ALTER TABLE "proxmox_stats_preferences" RENAME TO "p_proxmox_stats_preferences"; diff --git a/plugins/proxmox/package.json b/plugins/proxmox/package.json deleted file mode 100644 index d3a7d9e01..000000000 --- a/plugins/proxmox/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@termix-plugin/proxmox", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "build": "termix-plugin build", - "test": "termix-plugin test", - "typecheck": "tsc -p .", - "validate": "termix-plugin validate" - }, - "dependencies": { - "lucide-react": "^1.33.0" - }, - "devDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "@types/node": "^26.2.0", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", - "better-sqlite3": "^13.0.3", - "jsdom": "^30.0.1", - "vitest": "^4.1.11" - }, - "peerDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "drizzle-orm": "^0.45.2", - "express": "^5.2.1", - "react": "^19.2.8", - "sonner": "^2.0.8", - "ssh2": "^1.17.0" - } -} diff --git a/plugins/proxmox/src/backend/guest-sync.ts b/plugins/proxmox/src/backend/guest-sync.ts deleted file mode 100644 index 53ab1558f..000000000 --- a/plugins/proxmox/src/backend/guest-sync.ts +++ /dev/null @@ -1,78 +0,0 @@ -export type ProxmoxSource = { - source: "proxmox"; - sourceHostId: number; - node: string; - vmid: number; - type: "qemu" | "lxc"; - lastSeenAt?: string; - lastStatus?: string; - missingSince?: string | null; -}; - -export function parseJsonObject(value: unknown): Record { - if (!value) return {}; - if (typeof value === "object") return value as Record; - if (typeof value !== "string") return {}; - try { - const parsed = JSON.parse(value); - return parsed && typeof parsed === "object" - ? (parsed as Record) - : {}; - } catch { - return {}; - } -} - -/** The guest a host was imported from, read from its proxmoxConfig setting. */ -export function getProxmoxSource(config: unknown): ProxmoxSource | null { - const source = parseJsonObject(config).source; - if (!source || typeof source !== "object") return null; - const src = source as Record; - if ( - src.source !== "proxmox" || - typeof src.sourceHostId !== "number" || - typeof src.node !== "string" || - typeof src.vmid !== "number" || - (src.type !== "qemu" && src.type !== "lxc") - ) { - return null; - } - return src as ProxmoxSource; -} - -export function proxmoxSourceKey(source: { - sourceHostId: number; - node: string; - type: string; - vmid: number; -}): string { - return `${source.sourceHostId}:${source.node}:${source.type}:${source.vmid}`; -} - -export interface ImportedGuest> { - host: H; - config: Record; - source: ProxmoxSource; -} - -/** - * The hosts already imported from one Proxmox node, keyed by guest. The - * source lives in each guest's proxmoxConfig host setting, not on the host - * row, so the caller passes both. - */ -export function indexImportedGuests( - hosts: H[], - configs: Array<{ hostId: number; value: unknown }>, - sourceHostId: number, -): Map> { - const configById = new Map(configs.map((c) => [c.hostId, c.value])); - const out = new Map>(); - for (const host of hosts) { - const config = parseJsonObject(configById.get(Number(host.id))); - const source = getProxmoxSource(config); - if (source?.sourceHostId === sourceHostId) { - out.set(proxmoxSourceKey(source), { host, config, source }); - } - } - return out; -} diff --git a/plugins/proxmox/src/backend/index.ts b/plugins/proxmox/src/backend/index.ts deleted file mode 100644 index fcc655f84..000000000 --- a/plugins/proxmox/src/backend/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Router } from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { setPluginSsh } from "./ssh.js"; -import { setPluginCtx } from "./plugin-ctx.js"; -import { startProxmoxService, stopProxmoxService } from "./routes.js"; -import { - startProxmoxStatsService, - stopProxmoxStatsService, -} from "./stats-service.js"; -import { proxmoxNodeHistory } from "./tables.js"; -import { createProxmoxNodeHistoryRepository } from "./proxmox-node-history-repository.js"; - -export async function activate(ctx: PluginContext) { - setPluginSsh(ctx.ssh); - setPluginCtx(ctx); - ctx.disposables.add(() => setPluginSsh(null)); - ctx.disposables.add(() => setPluginCtx(null)); - - const historyTable = await ctx.db.define(proxmoxNodeHistory); - const historyRepository = createProxmoxNodeHistoryRepository( - ctx.db, - historyTable, - ); - - const router = ctx.http.router(); - startProxmoxService(router); - ctx.disposables.add(stopProxmoxService); - startProxmoxStatsService(ctx, router, historyRepository); - ctx.disposables.add(stopProxmoxStatsService); - ctx.log.info("Proxmox routes mounted at /plugin-api/proxmox"); -} - -export async function deactivate() { - stopProxmoxService(); - stopProxmoxStatsService(); -} diff --git a/plugins/proxmox/src/backend/plugin-ctx.ts b/plugins/proxmox/src/backend/plugin-ctx.ts deleted file mode 100644 index a36386f49..000000000 --- a/plugins/proxmox/src/backend/plugin-ctx.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { PluginContext } from "@termix/plugin-sdk/backend"; - -let current: PluginContext | null = null; - -/** Set in activate, cleared on deactivate. */ -export function setPluginCtx(ctx: PluginContext | null): void { - current = ctx; -} - -export function pluginCtx(): PluginContext { - if (!current) throw new Error("The plugin is not active"); - return current; -} diff --git a/plugins/proxmox/src/backend/proxmox-import-auth.ts b/plugins/proxmox/src/backend/proxmox-import-auth.ts deleted file mode 100644 index 65194c0dd..000000000 --- a/plugins/proxmox/src/backend/proxmox-import-auth.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Pure decision: which auth settings an imported Proxmox guest inherits. -// -// The frontend carries a parallel copy in -// plugins/proxmox/frontend/proxmox-import-auth.ts. The two drifting apart is -// what produced the reported import bug, so both are kept behaviourally -// identical and each is unit-tested against the same matrix. -export function resolveProxmoxImportAuth( - defaultAuthType: string | undefined, - credentialId: number | null | undefined, -): { - authType: string; - credentialId: number | null; - overrideCredentialUsername: number; -} { - // An explicit special auth type (none/opkssh/tailscale/vault/…) wins. - if ( - defaultAuthType && - defaultAuthType !== "credential" && - !["password", "key"].includes(defaultAuthType) - ) { - return { - authType: defaultAuthType, - credentialId: null, - overrideCredentialUsername: 0, - }; - } - - // A credential (configured default OR inherited from the source host) is a - // concrete auth source -> use it, even when defaultAuthType is the - // "password"/"key" default. - if (credentialId) { - return { - authType: "credential", - credentialId, - overrideCredentialUsername: 0, - }; - } - - return { - authType: "none", - credentialId: null, - overrideCredentialUsername: 0, - }; -} diff --git a/plugins/proxmox/src/backend/proxmox-jump-hosts.ts b/plugins/proxmox/src/backend/proxmox-jump-hosts.ts deleted file mode 100644 index c9cd753a1..000000000 --- a/plugins/proxmox/src/backend/proxmox-jump-hosts.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function parseProxmoxJumpHosts(raw: unknown): unknown[] | null { - if (!raw) return null; - if (Array.isArray(raw)) return raw; - if (typeof raw !== "string") return null; - - try { - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : null; - } catch { - return null; - } -} diff --git a/plugins/proxmox/src/backend/proxmox-node-history-repository.ts b/plugins/proxmox/src/backend/proxmox-node-history-repository.ts deleted file mode 100644 index e5b66dab2..000000000 --- a/plugins/proxmox/src/backend/proxmox-node-history-repository.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { and, gte, lte, eq, asc, lt } from "drizzle-orm"; -import type { PluginDatabase } from "@termix/plugin-sdk/backend"; - -export interface ProxmoxNodeHistoryRow { - id: number; - hostId: number; - ts: string; - cpuPercent: number | null; - memPercent: number | null; - diskPercent: number | null; - netRxBytes: number | null; - netTxBytes: number | null; -} - -export interface ProxmoxNodeHistoryCreateInput { - hostId: number; - cpuPercent: number | null; - memPercent: number | null; - diskPercent: number | null; - netRxBytes: number | null; - netTxBytes: number | null; -} - -/* eslint-disable @typescript-eslint/no-explicit-any */ -type Table = any; -type Drizzle = any; -/* eslint-enable @typescript-eslint/no-explicit-any */ - -export type ProxmoxNodeHistoryRepository = ReturnType< - typeof createProxmoxNodeHistoryRepository ->; - -export function createProxmoxNodeHistoryRepository( - db: PluginDatabase, - table: Table, -) { - const client = () => db.client(); - - return { - async create(input: ProxmoxNodeHistoryCreateInput): Promise { - const drizzle = await client(); - await drizzle.insert(table).values(input); - await db.persist({ lazy: true }); - }, - - async listRange( - hostId: number, - fromIso: string, - toIso: string, - ): Promise { - const drizzle = await client(); - return drizzle - .select() - .from(table) - .where( - and( - eq(table.hostId, hostId), - gte(table.ts, fromIso), - lte(table.ts, toIso), - ), - ) - .orderBy(asc(table.ts)); - }, - - async pruneOlderThan(hostId: number, retentionDays: number): Promise { - // ts columns store CURRENT_TIMESTAMP as "YYYY-MM-DD HH:MM:SS" UTC, which - // sorts lexicographically, so the cutoff is a plain string comparison. - const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000) - .toISOString() - .slice(0, 19) - .replace("T", " "); - const drizzle = await client(); - await drizzle - .delete(table) - .where(and(eq(table.hostId, hostId), lt(table.ts, cutoff))); - await db.persist({ lazy: true }); - }, - }; -} diff --git a/plugins/proxmox/src/backend/proxmox-shared.ts b/plugins/proxmox/src/backend/proxmox-shared.ts deleted file mode 100644 index 112c9f007..000000000 --- a/plugins/proxmox/src/backend/proxmox-shared.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Proxmox node names are restricted to [a-zA-Z0-9-] by PVE itself, -// but we validate defensively before using in a shell command. -const SAFE_NODE_RE = /^[a-zA-Z0-9._-]{1,64}$/; - -export function isSafeNodeName(name: string): boolean { - return SAFE_NODE_RE.test(name); -} diff --git a/plugins/proxmox/src/backend/proxmox-stats-polling.ts b/plugins/proxmox/src/backend/proxmox-stats-polling.ts deleted file mode 100644 index 4bd2d4c42..000000000 --- a/plugins/proxmox/src/backend/proxmox-stats-polling.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { - collectProxmoxStats, - type ProxmoxStatsSnapshot, -} from "./proxmox/collect-proxmox-stats.js"; -import type { ProxmoxNodeHistoryRepository } from "./proxmox-node-history-repository.js"; -import { pluginCtx } from "./plugin-ctx.js"; -import type { Client } from "ssh2"; - -/** - * Proxmox stats polling gets its own concurrency limiter so a burst of polls - * on many hosts cannot starve one host's collection. - */ -class ConcurrentLimiter { - private running = 0; - private queue: Array<() => void> = []; - constructor(private readonly max: number) {} - - async run(fn: () => Promise): Promise { - if (this.running >= this.max) { - await new Promise((resolve) => this.queue.push(resolve)); - } - this.running++; - try { - return await fn(); - } finally { - this.running--; - const next = this.queue.shift(); - if (next) next(); - } - } -} - -const proxmoxStatsPollLimiter = new ConcurrentLimiter(5); - -export interface ProxmoxStatsPollableHost { - id: number; - userId: string; - proxmoxStatsConfig?: string | ProxmoxStatsPollConfig | null; -} - -export interface ProxmoxStatsPollConfig { - nodeName?: string | null; - pollInterval?: number; - enabledCards?: string[]; -} - -const DEFAULT_POLL_INTERVAL_SECONDS = 60; - -export function parseProxmoxStatsConfig( - raw: string | ProxmoxStatsPollConfig | null | undefined, -): ProxmoxStatsPollConfig { - if (!raw) { - return { nodeName: null, pollInterval: DEFAULT_POLL_INTERVAL_SECONDS }; - } - if (typeof raw === "object") { - return { - nodeName: raw.nodeName ?? null, - pollInterval: raw.pollInterval ?? DEFAULT_POLL_INTERVAL_SECONDS, - enabledCards: raw.enabledCards, - }; - } - try { - const parsed = JSON.parse(raw) as ProxmoxStatsPollConfig; - return { - nodeName: parsed.nodeName ?? null, - pollInterval: parsed.pollInterval ?? DEFAULT_POLL_INTERVAL_SECONDS, - enabledCards: parsed.enabledCards, - }; - } catch { - return { nodeName: null, pollInterval: DEFAULT_POLL_INTERVAL_SECONDS }; - } -} - -interface HostPollingEntry { - host: THost; - timer?: NodeJS.Timeout; - viewerUserId?: string; -} - -interface ViewerDetail { - sessionId: string; - userId: string; - hostId: number; - lastHeartbeat: number; -} - -interface CachedSnapshot { - data: ProxmoxStatsSnapshot; - timestamp: number; -} - -interface ErrorSnapshot { - error: string; - timestamp: number; -} - -export class ProxmoxPollingManager< - THost extends ProxmoxStatsPollableHost = ProxmoxStatsPollableHost, -> { - private pollingConfigs = new Map>(); - private snapshotStore = new Map(); - private errorStore = new Map(); - private activeViewers = new Map>(); - private viewerDetails = new Map(); - private inFlight = new Set(); - private viewerCleanupInterval: NodeJS.Timeout; - - constructor( - private readonly deps: { - fetchHostById: ( - hostId: number, - userId: string, - ) => Promise; - withSshConnection: ( - host: THost, - fn: (client: Client) => Promise, - ) => Promise; - historyRepository: ProxmoxNodeHistoryRepository; - historyEnabled?: () => boolean; - }, - ) { - this.viewerCleanupInterval = setInterval(() => { - this.cleanupInactiveViewers(); - }, 60000); - } - - private intervalWithJitter(intervalMs: number, hostId: number): number { - const spread = Math.min(intervalMs * 0.2, 15_000); - const jitter = (hostId * 1103515245) % Math.max(1, Math.floor(spread)); - return intervalMs + jitter; - } - - // Host Metrics' retention setting belongs to that plugin, so node history - // keeps a fixed week. - private readonly retentionDays = 7; - - private async pollHostStats( - host: THost, - viewerUserId?: string, - ): Promise { - if (this.inFlight.has(host.id)) return; - this.inFlight.add(host.id); - - try { - await proxmoxStatsPollLimiter.run(async () => { - const userId = viewerUserId || host.userId; - const refreshed = - (await this.deps.fetchHostById(host.id, userId)) ?? host; - const config = parseProxmoxStatsConfig(refreshed.proxmoxStatsConfig); - - try { - const snapshot = await this.deps.withSshConnection( - refreshed, - (client) => collectProxmoxStats(client, config.nodeName), - ); - - this.snapshotStore.set(host.id, { - data: snapshot, - timestamp: Date.now(), - }); - this.errorStore.delete(host.id); - - if (this.deps.historyEnabled?.() ?? true) { - await this.insertHistory(host.id, snapshot); - } - } catch (error) { - const message = - error instanceof Error ? error.message : String(error); - this.errorStore.set(host.id, { - error: message, - timestamp: Date.now(), - }); - pluginCtx().log.warn( - `Proxmox stats poll failed for host ${host.id}: ${message}`, - ); - } - }); - } finally { - this.inFlight.delete(host.id); - } - } - - private async insertHistory( - hostId: number, - snapshot: ProxmoxStatsSnapshot, - ): Promise { - try { - const iface = snapshot.network?.interfaces?.[0]; - const rxRaw = iface?.rxBytes ? parseInt(iface.rxBytes, 10) : null; - const txRaw = iface?.txBytes ? parseInt(iface.txBytes, 10) : null; - - await this.deps.historyRepository.create({ - hostId, - cpuPercent: snapshot.node?.cpu?.percent ?? null, - memPercent: snapshot.node?.memory?.percent ?? null, - diskPercent: snapshot.node?.disk?.percent ?? null, - netRxBytes: rxRaw !== null && !isNaN(rxRaw) ? rxRaw : null, - netTxBytes: txRaw !== null && !isNaN(txRaw) ? txRaw : null, - }); - - await this.deps.historyRepository.pruneOlderThan( - hostId, - this.retentionDays, - ); - } catch (err) { - pluginCtx().log.warn( - `Failed to write proxmox node history for host ${hostId}: ${err instanceof Error ? err.message : String(err)}`, - ); - } - } - - private startPollingForHost(host: THost, viewerUserId?: string): void { - const existing = this.pollingConfigs.get(host.id); - if (existing?.timer) { - clearInterval(existing.timer); - } - - const config = parseProxmoxStatsConfig(host.proxmoxStatsConfig); - const intervalMs = this.intervalWithJitter( - (config.pollInterval ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000, - host.id, - ); - - void this.pollHostStats(host, viewerUserId); - - const timer = setInterval(() => { - const latest = this.pollingConfigs.get(host.id); - if (latest) { - void this.pollHostStats(latest.host, latest.viewerUserId); - } - }, intervalMs); - - this.pollingConfigs.set(host.id, { host, timer, viewerUserId }); - } - - private stopPollingForHost(hostId: number): void { - const config = this.pollingConfigs.get(hostId); - if (config?.timer) { - clearInterval(config.timer); - } - this.pollingConfigs.delete(hostId); - } - - getStats( - hostId: number, - ): { data: ProxmoxStatsSnapshot; timestamp: number } | undefined { - return this.snapshotStore.get(hostId); - } - - getError(hostId: number): ErrorSnapshot | undefined { - return this.errorStore.get(hostId); - } - - async ensurePolling(host: THost, viewerUserId?: string): Promise { - if (!this.pollingConfigs.has(host.id)) { - this.startPollingForHost(host, viewerUserId); - } - if (!this.snapshotStore.has(host.id) && !this.inFlight.has(host.id)) { - await this.pollHostStats(host, viewerUserId); - } - } - - registerViewer = ( - hostId: number, - sessionId: string, - userId: string, - ): void => { - if (!this.activeViewers.has(hostId)) { - this.activeViewers.set(hostId, new Set()); - } - this.activeViewers.get(hostId)!.add(sessionId); - - this.viewerDetails.set(sessionId, { - sessionId, - userId, - hostId, - lastHeartbeat: Date.now(), - }); - - if (this.activeViewers.get(hostId)!.size === 1) { - Promise.resolve() - .then(async () => { - const host = await this.deps.fetchHostById(hostId, userId); - if (host) { - this.startPollingForHost(host, userId); - } - }) - .catch((err) => { - pluginCtx().log.warn( - `Proxmox stats startPollingForHost rejected (non-fatal) for host ${hostId}: ${err instanceof Error ? err.message : String(err)}`, - ); - }); - } - }; - - unregisterViewer = (hostId: number, sessionId: string): void => { - const viewers = this.activeViewers.get(hostId); - if (viewers) { - viewers.delete(sessionId); - if (viewers.size === 0) { - this.activeViewers.delete(hostId); - this.stopPollingForHost(hostId); - } - } - this.viewerDetails.delete(sessionId); - }; - - updateHeartbeat(sessionId: string): boolean { - const viewer = this.viewerDetails.get(sessionId); - if (viewer) { - viewer.lastHeartbeat = Date.now(); - return true; - } - return false; - } - - private cleanupInactiveViewers(): void { - const now = Date.now(); - const maxInactivity = 120000; - - for (const [sessionId, viewer] of this.viewerDetails.entries()) { - if (now - viewer.lastHeartbeat > maxInactivity) { - this.unregisterViewer(viewer.hostId, sessionId); - } - } - } - - destroy(): void { - clearInterval(this.viewerCleanupInterval); - for (const hostId of this.pollingConfigs.keys()) { - this.stopPollingForHost(hostId); - } - } -} diff --git a/plugins/proxmox/src/backend/proxmox/cluster-health-collector.ts b/plugins/proxmox/src/backend/proxmox/cluster-health-collector.ts deleted file mode 100644 index 6ab539362..000000000 --- a/plugins/proxmox/src/backend/proxmox/cluster-health-collector.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { Client } from "ssh2"; -import { execCommand } from "./common-utils.js"; - -export interface ProxmoxClusterNodeEntry { - name: string; - online: boolean; - local: boolean; - ip: string | null; -} - -export type ProxmoxClusterHealthResult = - | { clustered: false } - | { - clustered: true; - quorate: boolean; - clusterName: string | null; - nodes: ProxmoxClusterNodeEntry[]; - }; - -const EMPTY_RESULT: ProxmoxClusterHealthResult = { clustered: false }; - -export async function collectProxmoxClusterHealth( - client: Client, -): Promise { - try { - const { stdout, code } = await execCommand( - client, - "pvesh get /cluster/status --output-format json", - 15000, - ); - if (code !== 0) { - return EMPTY_RESULT; - } - - const data = JSON.parse(stdout); - if (!Array.isArray(data)) { - return EMPTY_RESULT; - } - - const entries = data as Array>; - const clusterEntry = entries.find((e) => e.type === "cluster"); - if (!clusterEntry) { - return EMPTY_RESULT; - } - - const nodes: ProxmoxClusterNodeEntry[] = entries - .filter((e) => e.type === "node") - .map((e) => ({ - name: typeof e.name === "string" ? e.name : "", - online: e.online === 1 || e.online === true, - local: e.local === 1 || e.local === true, - ip: typeof e.ip === "string" && e.ip ? e.ip : null, - })); - - return { - clustered: true, - quorate: clusterEntry.quorate === 1 || clusterEntry.quorate === true, - clusterName: - typeof clusterEntry.name === "string" && clusterEntry.name - ? clusterEntry.name - : null, - nodes, - }; - } catch { - return EMPTY_RESULT; - } -} diff --git a/plugins/proxmox/src/backend/proxmox/collect-proxmox-stats.ts b/plugins/proxmox/src/backend/proxmox/collect-proxmox-stats.ts deleted file mode 100644 index 846cf21cf..000000000 --- a/plugins/proxmox/src/backend/proxmox/collect-proxmox-stats.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { Client } from "ssh2"; -import { execCommand } from "./common-utils.js"; -import { isSafeNodeName } from "../proxmox-shared.js"; -import { - collectProxmoxNodeStatus, - type ProxmoxNodeStatusResult, -} from "./node-status-collector.js"; -import { - collectProxmoxNodeNetwork, - type ProxmoxNodeNetworkResult, -} from "./node-network-collector.js"; -import { - collectProxmoxGuestsSummary, - type ProxmoxGuestsSummaryResult, -} from "./guests-collector.js"; -import { - collectProxmoxStorage, - type ProxmoxStorageResult, -} from "./storage-collector.js"; -import { - collectProxmoxClusterHealth, - type ProxmoxClusterHealthResult, -} from "./cluster-health-collector.js"; - -export interface ProxmoxStatsSnapshot { - node: ProxmoxNodeStatusResult; - network: ProxmoxNodeNetworkResult; - guests: ProxmoxGuestsSummaryResult; - storage: ProxmoxStorageResult; - cluster: ProxmoxClusterHealthResult; - lastChecked: string; -} - -async function resolveNodeName( - client: Client, - configuredNodeName: string | null | undefined, -): Promise { - if (configuredNodeName) { - // An explicitly configured name is validated and used as-is - an unsafe - // value is rejected outright rather than silently falling back to - // auto-detection, which would mask a misconfigured (or malicious) override. - if (!isSafeNodeName(configuredNodeName)) { - throw new Error("Unable to determine a valid Proxmox node name"); - } - return configuredNodeName; - } - - const { stdout } = await execCommand(client, "hostname", 10000); - return stdout.trim(); -} - -export async function collectProxmoxStats( - client: Client, - configuredNodeName: string | null | undefined, -): Promise { - const pveshCheck = await execCommand( - client, - "command -v pvesh >/dev/null 2>&1 && echo ok || echo missing", - 10000, - ); - if (pveshCheck.stdout.trim() !== "ok") { - throw new Error("pvesh not found — is this a Proxmox node?"); - } - - const nodeName = await resolveNodeName(client, configuredNodeName); - if (!isSafeNodeName(nodeName)) { - throw new Error("Unable to determine a valid Proxmox node name"); - } - - const [node, network, guests, storage, cluster] = await Promise.all([ - collectProxmoxNodeStatus(client, nodeName), - collectProxmoxNodeNetwork(client, nodeName), - collectProxmoxGuestsSummary(client, nodeName), - collectProxmoxStorage(client, nodeName), - collectProxmoxClusterHealth(client), - ]); - - return { - node, - network, - guests, - storage, - cluster, - lastChecked: new Date().toISOString(), - }; -} diff --git a/plugins/proxmox/src/backend/proxmox/common-utils.ts b/plugins/proxmox/src/backend/proxmox/common-utils.ts deleted file mode 100644 index 9e8147e30..000000000 --- a/plugins/proxmox/src/backend/proxmox/common-utils.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Client, ClientChannel } from "ssh2"; - -/** Runs a command over an already-open ssh2 client and collects its output. */ -export function execCommand( - client: Client, - command: string, - timeoutMs = 30000, -): Promise<{ - stdout: string; - stderr: string; - code: number | null; -}> { - return new Promise((resolve, reject) => { - let settled = false; - let stream: ClientChannel | null = null; - - const timeout = setTimeout(() => { - if (!settled) { - settled = true; - cleanup(); - reject(new Error(`Command timeout after ${timeoutMs}ms: ${command}`)); - } - }, timeoutMs); - - const cleanup = () => { - clearTimeout(timeout); - if (stream) { - try { - stream.removeAllListeners(); - if (stream.stderr) { - stream.stderr.removeAllListeners(); - } - stream.destroy(); - } catch { - // expected - cleanup errors ignored - } - } - }; - - client.exec(command, { pty: false }, (err, _stream) => { - if (err) { - if (!settled) { - settled = true; - cleanup(); - reject(err); - } - return; - } - - stream = _stream; - let stdout = ""; - let stderr = ""; - let exitCode: number | null = null; - - stream - .on("close", (code: number | undefined) => { - if (!settled) { - settled = true; - exitCode = typeof code === "number" ? code : null; - cleanup(); - resolve({ stdout, stderr, code: exitCode }); - } - }) - .on("data", (data: Buffer) => { - stdout += data.toString("utf8"); - }) - .on("error", (streamErr: Error) => { - if (!settled) { - settled = true; - cleanup(); - reject(streamErr); - } - }); - - if (stream.stderr) { - stream.stderr - .on("data", (data: Buffer) => { - stderr += data.toString("utf8"); - }) - .on("error", (stderrErr: Error) => { - if (!settled) { - settled = true; - cleanup(); - reject(stderrErr); - } - }); - } - }); - }); -} - -export function toFixedNum( - n: number | null | undefined, - digits = 2, -): number | null { - if (typeof n !== "number" || !Number.isFinite(n)) return null; - return Number(n.toFixed(digits)); -} diff --git a/plugins/proxmox/src/backend/proxmox/guests-collector.ts b/plugins/proxmox/src/backend/proxmox/guests-collector.ts deleted file mode 100644 index 703852050..000000000 --- a/plugins/proxmox/src/backend/proxmox/guests-collector.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { Client } from "ssh2"; -import { execCommand, toFixedNum } from "./common-utils.js"; -import { isSafeNodeName } from "../proxmox-shared.js"; - -export interface ProxmoxGuestSummaryEntry { - vmid: number; - name: string; - type: "qemu" | "lxc"; - status: string; - cpuPercent: number | null; - memPercent: number | null; - memUsedGiB: number | null; - memTotalGiB: number | null; - diskPercent: number | null; - diskUsedGiB: number | null; - diskTotalGiB: number | null; - uptimeSeconds: number | null; -} - -export interface ProxmoxGuestsSummaryResult { - guests: ProxmoxGuestSummaryEntry[]; - counts: { running: number; stopped: number; total: number }; -} - -const EMPTY_RESULT: ProxmoxGuestsSummaryResult = { - guests: [], - counts: { running: 0, stopped: 0, total: 0 }, -}; - -function bytesToGiB(bytes: number): number { - return bytes / (1024 * 1024 * 1024); -} - -export async function collectProxmoxGuestsSummary( - client: Client, - nodeName: string, -): Promise { - if (!isSafeNodeName(nodeName)) { - return EMPTY_RESULT; - } - - try { - const { stdout, code } = await execCommand( - client, - "pvesh get /cluster/resources --output-format json", - 25000, - ); - if (code !== 0) { - return EMPTY_RESULT; - } - - const resources = JSON.parse(stdout); - if (!Array.isArray(resources)) { - return EMPTY_RESULT; - } - - const guests: ProxmoxGuestSummaryEntry[] = []; - for (const r of resources as Array>) { - const type = r.type; - if (type !== "qemu" && type !== "lxc") continue; - if (r.node !== nodeName) continue; - if (r.template === true || r.template === 1) continue; - - const cpuFraction = typeof r.cpu === "number" ? r.cpu : null; - const mem = typeof r.mem === "number" ? r.mem : null; - const maxmem = typeof r.maxmem === "number" ? r.maxmem : null; - const memPercent = - mem !== null && maxmem !== null && maxmem > 0 - ? Math.max(0, Math.min(100, (mem / maxmem) * 100)) - : null; - - const disk = typeof r.disk === "number" ? r.disk : null; - const maxdisk = typeof r.maxdisk === "number" ? r.maxdisk : null; - // QEMU guests without a running agent report maxdisk: 0 - a false 0% - // is worse than an honest "unknown", so treat that as no data at all. - const hasDiskData = maxdisk !== null && maxdisk > 0; - const diskPercent = - hasDiskData && disk !== null - ? Math.max(0, Math.min(100, (disk / maxdisk) * 100)) - : null; - - guests.push({ - vmid: typeof r.vmid === "number" ? r.vmid : Number(r.vmid), - name: - typeof r.name === "string" && r.name ? r.name : String(r.vmid ?? ""), - type, - status: typeof r.status === "string" ? r.status : "unknown", - cpuPercent: toFixedNum( - cpuFraction !== null ? cpuFraction * 100 : null, - 0, - ), - memPercent: toFixedNum(memPercent, 0), - memUsedGiB: mem !== null ? toFixedNum(bytesToGiB(mem), 2) : null, - memTotalGiB: maxmem !== null ? toFixedNum(bytesToGiB(maxmem), 2) : null, - diskPercent: toFixedNum(diskPercent, 0), - diskUsedGiB: - hasDiskData && disk !== null ? toFixedNum(bytesToGiB(disk), 2) : null, - diskTotalGiB: hasDiskData ? toFixedNum(bytesToGiB(maxdisk), 2) : null, - uptimeSeconds: typeof r.uptime === "number" ? r.uptime : null, - }); - } - - const running = guests.filter((g) => g.status === "running").length; - const stopped = guests.filter((g) => g.status !== "running").length; - - return { - guests, - counts: { running, stopped, total: guests.length }, - }; - } catch { - return EMPTY_RESULT; - } -} diff --git a/plugins/proxmox/src/backend/proxmox/node-network-collector.ts b/plugins/proxmox/src/backend/proxmox/node-network-collector.ts deleted file mode 100644 index 0c4a03738..000000000 --- a/plugins/proxmox/src/backend/proxmox/node-network-collector.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { Client } from "ssh2"; -import { execCommand } from "./common-utils.js"; -import { isSafeNodeName } from "../proxmox-shared.js"; - -export interface ProxmoxNodeNetworkInterface { - name: string; - ip: string | null; - state: string | null; - rxBytes: string | null; - txBytes: string | null; -} - -export interface ProxmoxNodeNetworkResult { - interfaces: ProxmoxNodeNetworkInterface[]; -} - -const EMPTY_RESULT: ProxmoxNodeNetworkResult = { interfaces: [] }; - -async function collectFromProcNetDev( - client: Client, -): Promise { - const interfaces: ProxmoxNodeNetworkInterface[] = []; - - try { - const [addrOut, stateOut, procNetOut] = await Promise.all([ - execCommand( - client, - "ip -o addr show | awk '{print $2,$4}' | grep -v '^lo'", - ), - execCommand( - client, - "ip -o link show | awk '{gsub(/:/, \"\", $2); print $2,$9}'", - ), - execCommand(client, "cat /proc/net/dev"), - ]); - - const ifMap = new Map< - string, - { ip: string | null; state: string | null } - >(); - for (const line of addrOut.stdout - .split("\n") - .map((l) => l.trim()) - .filter(Boolean)) { - const parts = line.split(/\s+/); - if (parts.length >= 2) { - const name = parts[0]; - const ip = parts[1].split("/")[0]; - if (!ifMap.has(name)) ifMap.set(name, { ip, state: null }); - } - } - for (const line of stateOut.stdout - .split("\n") - .map((l) => l.trim()) - .filter(Boolean)) { - const parts = line.split(/\s+/); - if (parts.length >= 2) { - const existing = ifMap.get(parts[0]); - if (existing) existing.state = parts[1]; - } - } - - const rxTxMap = new Map(); - for (const line of procNetOut.stdout.split("\n").slice(2)) { - const parts = line.trim().split(/\s+/); - if (parts.length >= 10) { - const ifName = parts[0].replace(":", ""); - rxTxMap.set(ifName, { rx: parts[1], tx: parts[9] }); - } - } - - for (const [name, data] of ifMap.entries()) { - const rxTx = rxTxMap.get(name); - interfaces.push({ - name, - ip: data.ip, - state: data.state, - rxBytes: rxTx?.rx ?? null, - txBytes: rxTx?.tx ?? null, - }); - } - } catch { - return EMPTY_RESULT; - } - - return { interfaces }; -} - -export async function collectProxmoxNodeNetwork( - client: Client, - nodeName: string, -): Promise { - if (!isSafeNodeName(nodeName)) { - return EMPTY_RESULT; - } - - try { - const { stdout, code } = await execCommand( - client, - `pvesh get /nodes/${nodeName}/netstat --output-format json`, - 15000, - ); - if (code !== 0) { - return collectFromProcNetDev(client); - } - - const data = JSON.parse(stdout); - if (!Array.isArray(data)) { - return collectFromProcNetDev(client); - } - - const interfaces: ProxmoxNodeNetworkInterface[] = data - .filter( - (entry): entry is Record => - !!entry && typeof entry === "object", - ) - .map((entry) => ({ - name: - typeof entry.dev === "string" ? entry.dev : String(entry.dev ?? ""), - ip: null, - state: null, - rxBytes: - typeof entry.in === "number" - ? String(entry.in) - : typeof entry.received === "number" - ? String(entry.received) - : null, - txBytes: - typeof entry.out === "number" - ? String(entry.out) - : typeof entry.transmitted === "number" - ? String(entry.transmitted) - : null, - })) - .filter((iface) => iface.name && iface.name !== "lo"); - - if (interfaces.length === 0) { - return collectFromProcNetDev(client); - } - - return { interfaces }; - } catch { - return collectFromProcNetDev(client); - } -} diff --git a/plugins/proxmox/src/backend/proxmox/node-status-collector.ts b/plugins/proxmox/src/backend/proxmox/node-status-collector.ts deleted file mode 100644 index e7bcec87c..000000000 --- a/plugins/proxmox/src/backend/proxmox/node-status-collector.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { Client } from "ssh2"; -import { execCommand, toFixedNum } from "./common-utils.js"; -import { isSafeNodeName } from "../proxmox-shared.js"; - -export interface ProxmoxNodeStatusResult { - cpu: { - percent: number | null; - cores: number | null; - load: [number, number, number] | null; - }; - memory: { - percent: number | null; - usedGiB: number | null; - totalGiB: number | null; - }; - disk: { - percent: number | null; - usedGiB: number | null; - totalGiB: number | null; - }; - uptime: { - seconds: number | null; - formatted: string | null; - }; - system: { - hostname: string | null; - kernel: string | null; - pveVersion: string | null; - }; -} - -const EMPTY_RESULT: ProxmoxNodeStatusResult = { - cpu: { percent: null, cores: null, load: null }, - memory: { percent: null, usedGiB: null, totalGiB: null }, - disk: { percent: null, usedGiB: null, totalGiB: null }, - uptime: { seconds: null, formatted: null }, - system: { hostname: null, kernel: null, pveVersion: null }, -}; - -function bytesToGiB(bytes: number): number { - return bytes / (1024 * 1024 * 1024); -} - -function formatUptime(seconds: number): string { - const days = Math.floor(seconds / 86400); - const hours = Math.floor((seconds % 86400) / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - return `${days}d ${hours}h ${minutes}m`; -} - -export async function collectProxmoxNodeStatus( - client: Client, - nodeName: string, -): Promise { - if (!isSafeNodeName(nodeName)) { - return EMPTY_RESULT; - } - - try { - const { stdout, code } = await execCommand( - client, - `pvesh get /nodes/${nodeName}/status --output-format json`, - 25000, - ); - if (code !== 0) { - return EMPTY_RESULT; - } - - const data = JSON.parse(stdout) as Record; - - const cpuFraction = typeof data.cpu === "number" ? data.cpu : null; - const cpuinfo = (data.cpuinfo as Record) || {}; - const cores = typeof cpuinfo.cores === "number" ? cpuinfo.cores : null; - const loadavgRaw = data.loadavg; - let load: [number, number, number] | null = null; - if (Array.isArray(loadavgRaw) && loadavgRaw.length >= 3) { - const parsed = loadavgRaw - .slice(0, 3) - .map((v) => Number(v)) - .map((v) => (Number.isFinite(v) ? v : 0)); - load = parsed as [number, number, number]; - } - - const memory = (data.memory as Record) || {}; - const memUsed = typeof memory.used === "number" ? memory.used : null; - const memTotal = typeof memory.total === "number" ? memory.total : null; - const memPercent = - memUsed !== null && memTotal !== null && memTotal > 0 - ? Math.max(0, Math.min(100, (memUsed / memTotal) * 100)) - : null; - - const rootfs = (data.rootfs as Record) || {}; - const diskUsed = typeof rootfs.used === "number" ? rootfs.used : null; - const diskTotal = typeof rootfs.total === "number" ? rootfs.total : null; - const diskPercent = - diskUsed !== null && diskTotal !== null && diskTotal > 0 - ? Math.max(0, Math.min(100, (diskUsed / diskTotal) * 100)) - : null; - - const uptimeSeconds = typeof data.uptime === "number" ? data.uptime : null; - - return { - cpu: { - percent: toFixedNum(cpuFraction !== null ? cpuFraction * 100 : null, 0), - cores, - load, - }, - memory: { - percent: toFixedNum(memPercent, 0), - usedGiB: memUsed !== null ? toFixedNum(bytesToGiB(memUsed), 2) : null, - totalGiB: - memTotal !== null ? toFixedNum(bytesToGiB(memTotal), 2) : null, - }, - disk: { - percent: toFixedNum(diskPercent, 0), - usedGiB: diskUsed !== null ? toFixedNum(bytesToGiB(diskUsed), 2) : null, - totalGiB: - diskTotal !== null ? toFixedNum(bytesToGiB(diskTotal), 2) : null, - }, - uptime: { - seconds: uptimeSeconds, - formatted: uptimeSeconds !== null ? formatUptime(uptimeSeconds) : null, - }, - system: { - hostname: - typeof data.hostname === "string" && data.hostname - ? data.hostname - : null, - kernel: - typeof data.kversion === "string" && data.kversion - ? data.kversion - : null, - pveVersion: - typeof data.pveversion === "string" && data.pveversion - ? data.pveversion - : null, - }, - }; - } catch { - return EMPTY_RESULT; - } -} diff --git a/plugins/proxmox/src/backend/proxmox/storage-collector.ts b/plugins/proxmox/src/backend/proxmox/storage-collector.ts deleted file mode 100644 index 9b1375257..000000000 --- a/plugins/proxmox/src/backend/proxmox/storage-collector.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { Client } from "ssh2"; -import { execCommand, toFixedNum } from "./common-utils.js"; -import { isSafeNodeName } from "../proxmox-shared.js"; - -export interface ProxmoxStoragePoolEntry { - name: string; - type: string; - active: boolean; - enabled: boolean; - usedGiB: number | null; - totalGiB: number | null; - availGiB: number | null; - percent: number | null; -} - -export interface ProxmoxStorageResult { - pools: ProxmoxStoragePoolEntry[]; -} - -const EMPTY_RESULT: ProxmoxStorageResult = { pools: [] }; - -function bytesToGiB(bytes: number): number { - return bytes / (1024 * 1024 * 1024); -} - -export async function collectProxmoxStorage( - client: Client, - nodeName: string, -): Promise { - if (!isSafeNodeName(nodeName)) { - return EMPTY_RESULT; - } - - try { - const { stdout, code } = await execCommand( - client, - `pvesh get /nodes/${nodeName}/storage --output-format json`, - 25000, - ); - if (code !== 0) { - return EMPTY_RESULT; - } - - const data = JSON.parse(stdout); - if (!Array.isArray(data)) { - return EMPTY_RESULT; - } - - const pools: ProxmoxStoragePoolEntry[] = ( - data as Array> - ).map((entry) => { - const used = typeof entry.used === "number" ? entry.used : null; - const total = typeof entry.total === "number" ? entry.total : null; - const avail = typeof entry.avail === "number" ? entry.avail : null; - const percent = - used !== null && total !== null && total > 0 - ? Math.max(0, Math.min(100, (used / total) * 100)) - : null; - - return { - name: typeof entry.storage === "string" ? entry.storage : "", - type: typeof entry.type === "string" ? entry.type : "unknown", - active: entry.active === 1 || entry.active === true, - enabled: entry.enabled === 1 || entry.enabled === true, - usedGiB: used !== null ? toFixedNum(bytesToGiB(used), 2) : null, - totalGiB: total !== null ? toFixedNum(bytesToGiB(total), 2) : null, - availGiB: avail !== null ? toFixedNum(bytesToGiB(avail), 2) : null, - percent: toFixedNum(percent, 0), - }; - }); - - return { pools }; - } catch { - return EMPTY_RESULT; - } -} diff --git a/plugins/proxmox/src/backend/routes.ts b/plugins/proxmox/src/backend/routes.ts deleted file mode 100644 index af29f715b..000000000 --- a/plugins/proxmox/src/backend/routes.ts +++ /dev/null @@ -1,1037 +0,0 @@ -import express, { type Router } from "express"; -import type { Client as SSHClient } from "ssh2"; -import { execElevated } from "@termix/plugin-sdk/host-commands"; -import type { - PluginHostCreateInput, - PluginHostJumpHost, - PluginHostUpdateInput, - PluginSshHost, -} from "@termix/plugin-sdk/backend"; -import { connectSsh } from "./ssh.js"; -import { pluginCtx } from "./plugin-ctx.js"; -import { resolveProxmoxImportAuth } from "./proxmox-import-auth.js"; -import { parseProxmoxJumpHosts } from "./proxmox-jump-hosts.js"; -import { isSafeNodeName } from "./proxmox-shared.js"; -import { - indexImportedGuests, - parseJsonObject, - type ProxmoxSource, -} from "./guest-sync.js"; - -/** A thrown value's message, or the fallback when it is not an Error. */ -function getErrorMessage(error: unknown, fallback = "Unknown error"): string { - return error instanceof Error ? error.message : fallback; -} - -const router = express.Router(); -const runningSyncs = new Set(); - -const MIN_SYNC_INTERVAL_MINUTES = 5; -const DEFAULT_SYNC_INTERVAL_MINUTES = 15; - -// Helpers - -function execCommand( - client: SSHClient, - command: string, - timeoutMs = 25000, -): Promise { - return new Promise((resolve, reject) => { - let settled = false; - const timer = setTimeout(() => { - if (!settled) { - settled = true; - reject(new Error(`Command timed out after ${timeoutMs}ms`)); - } - }, timeoutMs); - - client.exec(command, (err, stream) => { - if (err) { - clearTimeout(timer); - return reject(err); - } - let stdout = ""; - let stderr = ""; - stream.on("close", (code: number) => { - if (settled) return; - settled = true; - clearTimeout(timer); - if (code !== 0) - reject(new Error(stderr || `Command exited with code ${code}`)); - else resolve(stdout); - }); - stream.on("data", (data: Buffer) => { - stdout += data.toString(); - }); - stream.stderr.on("data", (data: Buffer) => { - stderr += data.toString(); - }); - }); - }); -} - -export async function execPveshCommand( - client: SSHClient, - command: string, - sudoPassword: string | undefined, - timeoutMs = 25000, -): Promise { - if (!sudoPassword) { - return execCommand(client, command, timeoutMs); - } - - const result = await execElevated(client, command, sudoPassword, { - forceSudo: true, - timeoutMs, - }); - if (result.code !== 0) { - throw new Error(result.stderr || `Command exited with code ${result.code}`); - } - return result.stdout; -} - -// Parse all IPs from LXC net config, then return the one matching the preferred prefix. -function parseLxcIp( - config: Record, - preferredPrefixes: string[] = [], -): string | null { - const ips: string[] = []; - for (const [key, value] of Object.entries(config)) { - if (/^net\d+$/.test(key) && typeof value === "string") { - const m = value.match(/ip=(\d{1,3}(?:\.\d{1,3}){3})/); - if (m) ips.push(m[1]); - } - } - if (!ips.length) return null; - for (const prefix of preferredPrefixes) { - const match = ips.find((ip) => ip.startsWith(prefix)); - if (match) return match; - } - return ips[0]; -} - -function matchesAny(name: string, patterns: string[]): boolean { - const lower = name.toLowerCase(); - return patterns.some((p) => lower.includes(p.toLowerCase())); -} - -function parseProxmoxConfig(raw: unknown): { - windowsPatterns: string[]; - dockerPatterns: string[]; - preferredPrefixes: string[]; - defaultCredentialId: number | null; - defaultAuthType: string; - autoSyncEnabled: boolean; - syncIntervalMinutes: number; - markMissingGuests: boolean; -} { - const split = (s: string) => - s - .split(",") - .map((x) => x.trim()) - .filter(Boolean); - if (!raw || typeof raw !== "object") { - return { - windowsPatterns: ["win", "windows"], - dockerPatterns: ["docker"], - preferredPrefixes: [], - defaultCredentialId: null, - defaultAuthType: "password", - autoSyncEnabled: false, - syncIntervalMinutes: DEFAULT_SYNC_INTERVAL_MINUTES, - markMissingGuests: true, - }; - } - const cfg = raw as Record; - const interval = - typeof cfg.syncIntervalMinutes === "number" - ? cfg.syncIntervalMinutes - : Number.parseInt(String(cfg.syncIntervalMinutes ?? ""), 10); - return { - defaultCredentialId: - typeof cfg.defaultCredentialId === "number" - ? cfg.defaultCredentialId - : null, - defaultAuthType: - typeof cfg.defaultAuthType === "string" - ? cfg.defaultAuthType - : "password", - windowsPatterns: split( - typeof cfg.windowsPatterns === "string" - ? cfg.windowsPatterns - : "win,windows", - ), - dockerPatterns: split( - typeof cfg.dockerPatterns === "string" ? cfg.dockerPatterns : "docker", - ), - preferredPrefixes: split( - typeof cfg.preferredPrefixes === "string" ? cfg.preferredPrefixes : "", - ), - autoSyncEnabled: cfg.autoSyncEnabled === true, - syncIntervalMinutes: - Number.isFinite(interval) && interval >= MIN_SYNC_INTERVAL_MINUTES - ? interval - : DEFAULT_SYNC_INTERVAL_MINUTES, - markMissingGuests: cfg.markMissingGuests !== false, - }; -} - -type ProxmoxGuest = { - name: string; - vmid: number; - type: "qemu" | "lxc"; - node: string; - status: string; - ip: string | null; - connectionType: "ssh" | "rdp"; - enableDocker: boolean; -}; - -/** The resolved host ctx.ssh.connect hands back, plus the sudo secret proxmox needs for pvesh. */ -type PluginSshHostWithSudo = PluginSshHost & { sudoPassword?: string }; - -type ProxmoxSyncResult = { - created: number; - updated: number; - markedMissing: number; - skipped: number; - errors: string[]; -}; - -function guestSourceKey(sourceHostId: number, guest: ProxmoxGuest): string { - return `${sourceHostId}:${guest.node}:${guest.type}:${guest.vmid}`; -} - -function guestTags(guest: ProxmoxGuest): string[] { - const idTag = guest.type === "lxc" ? `ct-${guest.vmid}` : `vm-${guest.vmid}`; - return [ - "proxmox", - guest.type, - guest.node, - idTag, - ...(guest.enableDocker ? ["docker"] : []), - ]; -} - -/** - * The switches another plugin keeps for an imported guest. An entry for a - * plugin that is not running is skipped by core. - */ -export function guestPluginSettings( - connectionType: "ssh" | "rdp", - enableDocker: boolean, -): Record> { - return { - ...(connectionType === "rdp" - ? { "remote-desktop": { enableRdp: true, rdpPort: 3389 } } - : {}), - ...(enableDocker ? { docker: { enableDocker: true } } : {}), - }; -} - -function mergeTags( - existing: unknown, - additions: string[], - removals: string[] = [], -): string { - const removeSet = new Set(removals); - const base = - typeof existing === "string" - ? existing - .split(",") - .map((tag) => tag.trim()) - .filter(Boolean) - : Array.isArray(existing) - ? existing - .map((tag) => (typeof tag === "string" ? tag.trim() : "")) - .filter(Boolean) - : []; - return [...new Set([...base, ...additions])] - .filter((tag) => !removeSet.has(tag)) - .join(","); -} - -async function discoverProxmoxGuestsForHost( - userId: string, - parsedHostId: number, - onProgress?: (done: number, total: number) => void, -): Promise<{ - host: PluginSshHostWithSudo; - guests: ProxmoxGuest[]; - credentialId: number | null; - defaultCredentialId: number | null; - jumpHosts: unknown[] | null; - config: ReturnType; -}> { - const ctx = pluginCtx(); - const proxmoxCfgRaw = await ctx.settings.getHost( - parsedHostId, - "proxmoxConfig", - ); - const config = parseProxmoxConfig(proxmoxCfgRaw); - - const { client, host } = await connectSsh(parsedHostId, { - purpose: "proxmox", - timeoutMs: 35000, - overrides: { tryKeyboard: false, readyTimeout: 30000 }, - }); - // connect() always hands back a redacted host; the sudo password comes - // from resolveHost, which needs credentials:read. - const full = await ctx.ssh.resolveHost(parsedHostId); - const hostWithSudo: PluginSshHostWithSudo = { - ...host, - sudoPassword: (full?.sudoPassword as string | undefined) ?? undefined, - }; - const hostCredentialId = (hostWithSudo.credentialId as number | null) ?? null; - - try { - ctx.log.info("Proxmox discovery SSH connection established"); - - const pveshCheck = await execCommand( - client, - "command -v pvesh >/dev/null 2>&1 && echo ok || echo missing", - ); - if (pveshCheck.trim() !== "ok") { - const error = new Error("pvesh not found — is this a Proxmox node?"); - (error as Error & { status?: number }).status = 422; - throw error; - } - - const resourcesJson = await execPveshCommand( - client, - "pvesh get /cluster/resources --output-format json 2>/dev/null", - hostWithSudo.sudoPassword, - ); - - let resources: Array>; - try { - resources = JSON.parse(resourcesJson); - } catch { - const error = new Error( - "Failed to parse pvesh output — unexpected response", - ); - (error as Error & { status?: number }).status = 502; - throw error; - } - - type GuestBase = { - name: string; - vmid: number; - type: "qemu" | "lxc"; - node: string; - status: string; - }; - - const guestBases: GuestBase[] = []; - for (const r of resources) { - const type = r.type as string; - if (type !== "qemu" && type !== "lxc") continue; - if (r.template) continue; - const node = r.node as string; - if (!isSafeNodeName(node)) { - ctx.log.warn( - `Skipping guest with unsafe node name: ${node} (vmid ${r.vmid})`, - ); - continue; - } - guestBases.push({ - name: (r.name as string) || String(r.vmid), - vmid: Number(r.vmid), - type: type as "qemu" | "lxc", - node, - status: (r.status as string) || "unknown", - }); - } - - async function resolveIp(g: GuestBase): Promise { - if (g.type === "lxc") { - let configIp: string | null = null; - try { - const cfgJson = await execPveshCommand( - client, - `pvesh get /nodes/${g.node}/lxc/${g.vmid}/config --output-format json 2>/dev/null`, - hostWithSudo.sudoPassword, - 25000, - ); - configIp = parseLxcIp(JSON.parse(cfgJson), config.preferredPrefixes); - } catch { - configIp = null; - } - if (configIp) return configIp; - // Static config parsing found nothing (e.g. net0 uses ip=dhcp). - // Fall back to the live interface list for running containers. - if (g.status === "running") { - try { - const ifRaw = await execPveshCommand( - client, - `pvesh get /nodes/${g.node}/lxc/${g.vmid}/interfaces --output-format json 2>/dev/null`, - hostWithSudo.sudoPassword, - 12000, - ); - const data = JSON.parse(ifRaw); - const entries: Array> = Array.isArray(data) - ? data - : []; - const allIps: string[] = []; - for (const entry of entries) { - if (entry.name === "lo") continue; - const inet = entry.inet; - if (typeof inet !== "string") continue; - const m = inet.match(/^(\d{1,3}(?:\.\d{1,3}){3})\/\d+$/); - if (m && !m[1].startsWith("127.")) allIps.push(m[1]); - } - if (allIps.length) { - for (const prefix of config.preferredPrefixes) { - const match = allIps.find((ip) => ip.startsWith(prefix)); - if (match) return match; - } - return allIps[0]; - } - } catch { - // Guest not running or interfaces unavailable - } - } - return null; - } - if (g.type === "qemu" && g.status === "running") { - try { - const ifJson = await execPveshCommand( - client, - `pvesh get /nodes/${g.node}/qemu/${g.vmid}/agent/network-get-interfaces --output-format json 2>/dev/null`, - hostWithSudo.sudoPassword, - 12000, - ); - const data = JSON.parse(ifJson); - const ifaces: Array> = Array.isArray( - data?.result, - ) - ? data.result - : Array.isArray(data) - ? data - : []; - const allIps: string[] = []; - for (const iface of ifaces) { - if (iface.name === "lo") continue; - const addrs = - (iface["ip-addresses"] as Array>) ?? []; - for (const a of addrs) { - if ( - a["ip-address-type"] === "ipv4" && - !a["ip-address"].startsWith("127.") - ) { - allIps.push(a["ip-address"]); - } - } - } - if (allIps.length) { - for (const prefix of config.preferredPrefixes) { - const match = allIps.find((ip) => ip.startsWith(prefix)); - if (match) return match; - } - return allIps[0]; - } - } catch { - // Guest agent absent or timed out - } - } - return null; - } - - // Low concurrency on purpose: pvesh is heavy and small Proxmox nodes - // (especially reached over a high-latency jump chain) suffer severe - // contention when many run at once — calls then exceed execCommand's - // timeout and IPs come back empty. 2 keeps each call well under budget. - const CONCURRENCY = 2; - const ips: (string | null)[] = new Array(guestBases.length).fill(null); - let cursor = 0; - let completed = 0; - onProgress?.(0, guestBases.length); - async function ipWorker() { - while (cursor < guestBases.length) { - const i = cursor++; - ips[i] = await resolveIp(guestBases[i]); - completed++; - onProgress?.(completed, guestBases.length); - } - } - await Promise.all( - Array.from({ length: Math.min(CONCURRENCY, guestBases.length) }, () => - ipWorker(), - ), - ); - - const guests: ProxmoxGuest[] = guestBases.map((g, i) => ({ - ...g, - ip: ips[i], - connectionType: matchesAny(g.name, config.windowsPatterns) - ? "rdp" - : "ssh", - enableDocker: matchesAny(g.name, config.dockerPatterns), - })); - - ctx.log.info( - `Proxmox discovery completed for host ${parsedHostId}: ${guests.length} guest(s)`, - ); - - return { - host: hostWithSudo, - guests, - credentialId: hostCredentialId, - defaultCredentialId: config.defaultCredentialId, - jumpHosts: parseProxmoxJumpHosts(hostWithSudo.jumpHosts), - config, - }; - } finally { - try { - client.end(); - } catch { - // ignore cleanup errors - } - } -} - -async function syncProxmoxHost( - userId: string, - sourceHostId: number, -): Promise { - const ctx = pluginCtx(); - const lockKey = `${userId}:${sourceHostId}`; - if (runningSyncs.has(lockKey)) { - return { - created: 0, - updated: 0, - markedMissing: 0, - skipped: 0, - errors: ["Sync already running"], - }; - } - - runningSyncs.add(lockKey); - const result: ProxmoxSyncResult = { - created: 0, - updated: 0, - markedMissing: 0, - skipped: 0, - errors: [], - }; - const startedAt = new Date().toISOString(); - - try { - const discovery = await discoverProxmoxGuestsForHost(userId, sourceHostId); - const sourceHostName = String(discovery.host.name || "Proxmox"); - const defaultCredentialId = - discovery.config.defaultCredentialId ?? discovery.credentialId ?? null; - const importAuth = resolveProxmoxImportAuth( - discovery.config.defaultAuthType, - defaultCredentialId, - ); - const now = new Date().toISOString(); - - const existingBySource = indexImportedGuests( - await ctx.hosts.listOwned(), - await ctx.settings.listHostValues("proxmoxConfig"), - sourceHostId, - ); - - const seen = new Set(); - for (const guest of discovery.guests) { - const key = guestSourceKey(sourceHostId, guest); - seen.add(key); - const imported = existingBySource.get(key); - const existing = imported?.host; - const source: ProxmoxSource = { - source: "proxmox", - sourceHostId, - node: guest.node, - vmid: guest.vmid, - type: guest.type, - lastSeenAt: now, - lastStatus: guest.status, - missingSince: null, - }; - - const proxmoxConfig = { ...(imported?.config ?? {}), source }; - const existingConnectionType = - existing?.connectionType === "ssh" || existing?.connectionType === "rdp" - ? existing.connectionType - : null; - const connectionType = existingConnectionType ?? guest.connectionType; - const usesImportCredential = - connectionType === "ssh" && importAuth.authType === "credential"; - const existingUsesImportCredential = - usesImportCredential && - existing?.authType === "credential" && - existing?.credentialId === importAuth.credentialId; - const port = - typeof existing?.port === "number" - ? existing.port - : connectionType === "rdp" - ? 3389 - : 22; - const username = - usesImportCredential && (!existing || existingUsesImportCredential) - ? "" - : typeof existing?.username === "string" && existing.username - ? existing.username - : connectionType === "rdp" - ? "" - : "root"; - const update: PluginHostUpdateInput = { - name: guest.name, - ip: guest.ip || existing?.ip || "0.0.0.0", - port, - username, - connectionType, - folder: existing?.folder || sourceHostName, - tags: mergeTags(existing?.tags, guestTags(guest), ["proxmox-missing"]), - pluginSettings: { [ctx.pluginId]: { proxmoxConfig } }, - }; - - if (existing) { - if (existingUsesImportCredential) { - update.credentialId = importAuth.credentialId; - update.overrideCredentialUsername = false; - } - await ctx.hosts.update(existing.id, update); - result.updated++; - continue; - } - - await ctx.hosts.create({ - ...update, - name: guest.name, - ip: update.ip!, - port, - username, - enableSsh: connectionType === "ssh", - pin: false, - authType: connectionType === "rdp" ? "password" : importAuth.authType, - credentialId: connectionType === "ssh" ? importAuth.credentialId : null, - overrideCredentialUsername: !!importAuth.overrideCredentialUsername, - jumpHosts: (parseProxmoxJumpHosts(discovery.jumpHosts) ?? - []) as PluginHostJumpHost[], - forceKeyboardInteractive: false, - pluginSettings: { - ...update.pluginSettings, - ...guestPluginSettings(connectionType, guest.enableDocker), - }, - }); - result.created++; - } - - if (discovery.config.markMissingGuests) { - for (const [key, { host, config, source }] of existingBySource) { - if (seen.has(key)) continue; - await ctx.hosts.update(host.id, { - tags: mergeTags(host.tags, ["proxmox-missing"]), - pluginSettings: { - [ctx.pluginId]: { - proxmoxConfig: { - ...config, - source: { ...source, missingSince: source.missingSince || now }, - }, - }, - }, - }); - result.markedMissing++; - } - } - - await writeSyncStatus(sourceHostId, { - lastSyncAt: startedAt, - lastSyncStatus: "success", - lastSyncError: null, - lastSyncResult: result, - }); - - return result; - } catch (error) { - const message = getErrorMessage(error); - result.errors.push(message); - await writeSyncStatus(sourceHostId, { - lastSyncAt: startedAt, - lastSyncStatus: "error", - lastSyncError: message, - lastSyncResult: result, - }); - throw error; - } finally { - runningSyncs.delete(lockKey); - } -} - -async function writeSyncStatus( - hostId: number, - patch: Record, -): Promise { - const ctx = pluginCtx(); - const config = parseJsonObject( - await ctx.settings.getHost(hostId, "proxmoxConfig"), - ); - await ctx.settings.setHost(hostId, "proxmoxConfig", { ...config, ...patch }); -} - -/** - * @openapi - * /proxmox/sync: - * post: - * summary: Sync Proxmox guests for a host - * description: Re-runs discovery for a Proxmox node and creates, updates or marks missing the imported guest hosts. - * tags: [Proxmox] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [hostId] - * properties: - * hostId: - * type: number - * responses: - * 200: - * description: Sync result. - * 400: - * description: Missing or invalid hostId. - * 500: - * description: Sync failed. - */ -router.post("/sync", async (req, res) => { - const { hostId } = req.body as { hostId?: unknown }; - const userId = pluginCtx().currentActor()!; - - const parsedHostId = Number(hostId); - if (!hostId || !Number.isInteger(parsedHostId) || parsedHostId <= 0) { - return res.status(400).json({ error: "Missing or invalid hostId" }); - } - - try { - const result = await syncProxmoxHost(userId, parsedHostId); - return res.json(result); - } catch (err: unknown) { - const message = getErrorMessage(err); - const status = (err as Error & { status?: number }).status || 500; - pluginCtx().log.error( - `Proxmox sync failed for host ${parsedHostId}`, - err as Error, - ); - return res.status(status).json({ error: `Sync failed: ${message}` }); - } -}); - -async function runDueProxmoxAutoSyncs(): Promise { - const ctx = pluginCtx(); - try { - const enabled = await ctx.settings.listHostValues("enableProxmox"); - - const now = Date.now(); - for (const { hostId, userId, value } of enabled) { - if (value !== true) continue; - - const configRaw = - (await ctx.settings.getHost>( - hostId, - "proxmoxConfig", - )) ?? {}; - const config = parseProxmoxConfig(configRaw); - if (!config.autoSyncEnabled) continue; - - const lastSyncAt = - typeof configRaw.lastSyncAt === "string" - ? Date.parse(configRaw.lastSyncAt) - : 0; - const intervalMs = config.syncIntervalMinutes * 60 * 1000; - if (lastSyncAt && now - lastSyncAt < intervalMs) continue; - - ctx - .asUser(userId, () => syncProxmoxHost(userId, hostId)) - .catch((error) => { - ctx.log.error( - `Scheduled Proxmox sync failed for host ${hostId}`, - error as Error, - ); - }); - } - } catch (error) { - ctx.log.error("Failed to scan Proxmox auto sync jobs", error as Error); - } -} - -let proxmoxAutoSyncTimer: NodeJS.Timeout | undefined; -let proxmoxAutoSyncStartupTimer: NodeJS.Timeout | undefined; - -/** - * @openapi - * /proxmox/discover: - * post: - * summary: Discover Proxmox guests on a node - * description: > - * Connects to an existing SSH host (a Proxmox node) using its stored - * credentials, runs pvesh to enumerate all guests (VMs and LXC - * containers) in the cluster, and returns them ready to be imported as - * Termix hosts. No separate Proxmox API token is required. - * tags: [Proxmox] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [hostId] - * properties: - * hostId: - * type: number - * description: ID of the SSH host that is a Proxmox node. - * responses: - * 200: - * description: Discovered guests. - * content: - * application/json: - * schema: - * type: object - * properties: - * guests: - * type: array - * items: - * type: object - * properties: - * name: - * type: string - * vmid: - * type: number - * type: - * type: string - * enum: [qemu, lxc] - * node: - * type: string - * status: - * type: string - * ip: - * type: string - * nullable: true - * connectionType: - * type: string - * enum: [ssh, rdp] - * enableDocker: - * type: boolean - * credentialId: - * type: number - * nullable: true - * defaultCredentialId: - * type: number - * nullable: true - * 400: - * description: Missing or invalid hostId. - * 401: - * description: Authentication required or session expired. - * 403: - * description: Access denied to the host. - * 404: - * description: Host not found. - * 422: - * description: Host is not a Proxmox node or is unreachable. - * 500: - * description: Discovery failed. - */ -router.get("/discover/stream", async (req, res) => { - const userId = pluginCtx().currentActor()!; - const parsedHostId = Number((req.query as { hostId?: unknown }).hostId); - if (!parsedHostId || !Number.isInteger(parsedHostId) || parsedHostId <= 0) { - return res.status(400).json({ error: "Missing or invalid hostId" }); - } - - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-store, no-transform", - Connection: "keep-alive", - "X-Accel-Buffering": "no", - }); - res.flushHeaders?.(); - - let closed = false; - const send = (event: string, data: unknown) => { - if (closed) return; - try { - res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); - } catch { - closed = true; - } - }; - const heartbeat = setInterval(() => { - if (closed) return; - try { - res.write(": keepalive\n\n"); - } catch { - closed = true; - clearInterval(heartbeat); - } - }, 15000); - req.on("close", () => { - closed = true; - clearInterval(heartbeat); - }); - - try { - const discovery = await discoverProxmoxGuestsForHost( - userId, - parsedHostId, - (done, total) => send("progress", { done, total }), - ); - send("result", { - guests: discovery.guests, - credentialId: discovery.credentialId, - defaultCredentialId: discovery.defaultCredentialId, - jumpHosts: discovery.jumpHosts, - }); - } catch (err: unknown) { - const message = getErrorMessage(err); - pluginCtx().log.error( - `Proxmox discovery (stream) failed for host ${parsedHostId}`, - err as Error, - ); - send("fail", { message }); - } finally { - clearInterval(heartbeat); - if (!closed) { - try { - res.end(); - } catch { - // ignore end errors - } - } - } -}); - -/** - * @openapi - * /plugin-api/proxmox/discover: - * post: - * summary: Discover the guests on a Proxmox node - * description: Lists the node's VMs and containers so they can be added as hosts. /discover/stream reports the same with progress. - * tags: [Proxmox] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [hostId] - * properties: - * hostId: { type: integer } - * responses: - * 200: { description: The guests found. } - * 400: { description: Missing or invalid hostId. } - * 403: { description: Access denied to the host. } - * 404: { description: Host not found. } - * 422: { description: Host is not a Proxmox node or is unreachable. } - */ -router.post("/discover", async (req, res) => { - const { hostId } = req.body as { hostId?: unknown }; - const userId = pluginCtx().currentActor()!; - - const parsedHostId = Number(hostId); - if (!hostId || !Number.isInteger(parsedHostId) || parsedHostId <= 0) { - return res.status(400).json({ error: "Missing or invalid hostId" }); - } - - try { - const discovery = await discoverProxmoxGuestsForHost(userId, parsedHostId); - return res.json({ - guests: discovery.guests, - credentialId: discovery.credentialId, - defaultCredentialId: discovery.defaultCredentialId, - jumpHosts: discovery.jumpHosts, - }); - } catch (err: unknown) { - const message = getErrorMessage(err); - pluginCtx().log.error( - `Proxmox discovery failed for host ${parsedHostId}`, - err as Error, - ); - - const status = - (err as Error & { status?: number }).status || - (message.includes("Authentication failed") || - message.includes("connect ECONNREFUSED") || - message.includes("connect ETIMEDOUT") - ? 422 - : 500); - return res.status(status).json({ error: `Discovery failed: ${message}` }); - } -}); - -/** - * @openapi - * /plugin-api/proxmox/import: - * post: - * summary: Import discovered Proxmox guests as hosts - * description: > - * Creates one host per entry, owned by the caller, and stores each - * entry's proxmox host settings (the guest's source) on the new host. - * tags: - * - Proxmox - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - hosts - * properties: - * hosts: - * type: array - * items: - * type: object - * responses: - * 200: - * description: How many hosts were created and which failed. - * 400: - * description: No hosts given. - */ -router.post("/import", async (req, res) => { - const ctx = pluginCtx(); - const { hosts } = req.body as { hosts?: unknown }; - if (!Array.isArray(hosts) || hosts.length === 0) { - return res.status(400).json({ error: "No hosts to import" }); - } - const result = { success: 0, failed: 0, errors: [] as string[] }; - for (const entry of hosts as PluginHostCreateInput[]) { - try { - // Core refuses any field it does not take, and any host setting its - // plugin does not declare. - await ctx.hosts.create(entry); - result.success++; - } catch (err: unknown) { - result.failed++; - result.errors.push(`${String(entry?.name)}: ${getErrorMessage(err)}`); - } - } - return res.json(result); -}); - -/** Called from activate(). Mounts this router at /proxmox via the shared - * dispatcher, and starts the background auto-sync scan (a 60s interval plus - * a one-off 30s-delayed startup run, both unref'd so they never keep the - * process alive on their own). */ -export function startProxmoxService(mountOn: Router): void { - mountOn.use(router); - proxmoxAutoSyncTimer = setInterval(runDueProxmoxAutoSyncs, 60 * 1000); - proxmoxAutoSyncTimer.unref?.(); - proxmoxAutoSyncStartupTimer = setTimeout(runDueProxmoxAutoSyncs, 30 * 1000); - proxmoxAutoSyncStartupTimer.unref?.(); -} - -/** Called from deactivate(). /proxmox/* falls back to 404 until reactivated, - * and the auto-sync timers are cleared so a disabled plugin stops syncing. */ -export function stopProxmoxService(): void { - // Unmounted by the runtime when the plugin deactivates. - if (proxmoxAutoSyncTimer) clearInterval(proxmoxAutoSyncTimer); - if (proxmoxAutoSyncStartupTimer) clearTimeout(proxmoxAutoSyncStartupTimer); - proxmoxAutoSyncTimer = undefined; - proxmoxAutoSyncStartupTimer = undefined; -} diff --git a/plugins/proxmox/src/backend/ssh.ts b/plugins/proxmox/src/backend/ssh.ts deleted file mode 100644 index b7a221543..000000000 --- a/plugins/proxmox/src/backend/ssh.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Client } from "ssh2"; -import type { - PluginSsh, - PluginSshConnection, - PluginSshConnectOptions, - PluginSshHost, -} from "@termix/plugin-sdk/backend"; - -let current: PluginSsh | null = null; - -/** Set in activate, cleared on deactivate. */ -export function setPluginSsh(ssh: PluginSsh | null): void { - current = ssh; -} - -export function pluginSsh(): PluginSsh { - if (!current) throw new Error("The plugin is not active"); - return current; -} - -/** withConnection with ssh2's Client type filled in. */ -export function withSshConnection( - host: number | PluginSshHost, - options: PluginSshConnectOptions & { pool: string }, - fn: (client: Client) => Promise, -): Promise { - return pluginSsh().withConnection(host, options, fn); -} - -/** connect with ssh2's Client type filled in. */ -export function connectSsh( - host: number | PluginSshHost, - options?: PluginSshConnectOptions, -): Promise> { - return pluginSsh().connect(host, options); -} diff --git a/plugins/proxmox/src/backend/stats-service.ts b/plugins/proxmox/src/backend/stats-service.ts deleted file mode 100644 index c7b0b0f11..000000000 --- a/plugins/proxmox/src/backend/stats-service.ts +++ /dev/null @@ -1,366 +0,0 @@ -/** - * Proxmox node stats: polling, viewer sessions and their routes. - * - * Moved here from host-metrics, which only ever hosted this because the - * plugin split predates B5. host-metrics must not import proxmox code, so - * this plugin owns its own polling manager, SSH connections and routes. - */ - -import type { Router } from "express"; -import express from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { pluginCtx } from "./plugin-ctx.js"; -import { - ProxmoxPollingManager, - parseProxmoxStatsConfig, - type ProxmoxStatsPollableHost, -} from "./proxmox-stats-polling.js"; -import type { ProxmoxNodeHistoryRepository } from "./proxmox-node-history-repository.js"; - -const EMPTY_SNAPSHOT = { - node: { - cpu: { percent: null, cores: null, load: null }, - memory: { percent: null, usedGiB: null, totalGiB: null }, - disk: { percent: null, usedGiB: null, totalGiB: null }, - uptime: { seconds: null, formatted: null }, - system: { hostname: null, kernel: null, pveVersion: null }, - }, - network: { interfaces: [] }, - guests: { guests: [], counts: { running: 0, stopped: 0, total: 0 } }, - storage: { pools: [] }, - cluster: { clustered: false }, - lastChecked: new Date(0).toISOString(), -}; - -type PollableHost = ProxmoxStatsPollableHost & { enableProxmoxStats: boolean }; - -let pollingManager: ProxmoxPollingManager | undefined; -let router: Router | undefined; - -async function fetchHostForPolling( - ctx: PluginContext, - hostId: number, - userId: string, -): Promise { - const enabled = await ctx.asUser(userId, () => - ctx.settings.getHost(hostId, "enableProxmoxStats"), - ); - if (!enabled) return undefined; - const host = await ctx.asUser(userId, () => ctx.hosts.get(hostId)); - if (!host) return undefined; - const proxmoxStatsConfig = await ctx.asUser(userId, () => - ctx.settings.getHost(hostId, "proxmoxStatsConfig"), - ); - return { - id: host.id, - userId: host.userId, - proxmoxStatsConfig: proxmoxStatsConfig as string | undefined, - enableProxmoxStats: true, - }; -} - -/** Called from activate(). Builds the polling manager and mounts its routes. */ -export function startProxmoxStatsService( - ctx: PluginContext, - mountOn: Router, - historyRepository: ProxmoxNodeHistoryRepository, -): void { - pollingManager = new ProxmoxPollingManager({ - fetchHostById: (hostId, userId) => fetchHostForPolling(ctx, hostId, userId), - withSshConnection: (host, fn) => - ctx.ssh.withConnection( - host.id, - { - pool: "proxmox-stats", - purpose: "proxmox", - overrides: { readyTimeout: 60000 }, - }, - fn, - ), - historyRepository, - }); - - router = express.Router(); - registerRoutes(router, pollingManager, historyRepository); - mountOn.use("/stats", router); -} - -export function stopProxmoxStatsService(): void { - pollingManager?.destroy(); - pollingManager = undefined; - router = undefined; -} - -function registerRoutes( - app: Router, - manager: ProxmoxPollingManager, - historyRepository: ProxmoxNodeHistoryRepository, -): void { - /** - * @openapi - * /plugin-api/proxmox/stats/{id}: - * get: - * summary: Get cached Proxmox node stats for a host - * description: Returns the most recently polled Proxmox stats snapshot for a host, or an empty skeleton if none has been collected yet. - * tags: [Proxmox] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Proxmox stats snapshot. - * 404: - * description: Stats not available yet. - */ - app.get("/:id", async (req, res) => { - const id = Number(req.params.id); - const cached = manager.getStats(id); - if (!cached) { - const errorState = manager.getError(id); - return res.status(404).json({ - error: errorState?.error || "Stats not available", - ...EMPTY_SNAPSHOT, - lastChecked: new Date().toISOString(), - }); - } - res.json({ - ...cached.data, - lastChecked: new Date(cached.timestamp).toISOString(), - }); - }); - - /** - * @openapi - * /plugin-api/proxmox/stats/start/{id}: - * post: - * summary: Start Proxmox stats collection - * description: Registers a viewer and starts (or reuses) polling for a host's Proxmox node stats. - * tags: [Proxmox] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Polling started, snapshot returned if already available. - * 403: - * description: Proxmox stats is not enabled for this host. - * 404: - * description: Host not found. - */ - app.post("/start/:id", async (req, res) => { - const id = Number(req.params.id); - const userId = pluginCtx().currentActor()!; - - try { - const host = await fetchHostForPolling(pluginCtx(), id, userId); - if (!host) { - return res - .status(403) - .json({ error: "Proxmox stats is not enabled for this host" }); - } - - const viewerSessionId = `proxmox-viewer-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; - manager.registerViewer(id, viewerSessionId, userId); - await manager.ensurePolling(host, userId); - - const cached = manager.getStats(id); - if (cached) { - return res.json({ - success: true, - viewerSessionId, - ...cached.data, - lastChecked: new Date(cached.timestamp).toISOString(), - }); - } - const errorState = manager.getError(id); - if (errorState) { - return res.json({ - success: true, - viewerSessionId, - status: "error", - error: errorState.error, - }); - } - return res.json({ success: true, viewerSessionId, status: "collecting" }); - } catch (error) { - pluginCtx().log.error( - `Failed to start proxmox stats collection for host ${id}`, - error as Error, - ); - res - .status(500) - .json({ error: "Failed to start proxmox stats collection" }); - } - }); - - /** - * @openapi - * /plugin-api/proxmox/stats/stop/{id}: - * post: - * summary: Stop Proxmox stats collection - * description: Unregisters a viewer session for a host's Proxmox node stats polling. - * tags: [Proxmox] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * requestBody: - * required: false - * content: - * application/json: - * schema: - * type: object - * properties: - * viewerSessionId: - * type: string - * responses: - * 200: - * description: Polling stopped successfully. - */ - app.post("/stop/:id", (req, res) => { - const id = Number(req.params.id); - const { viewerSessionId } = req.body as { viewerSessionId?: string }; - if (viewerSessionId && typeof viewerSessionId === "string") { - manager.unregisterViewer(id, viewerSessionId); - } - res.json({ success: true }); - }); - - /** - * @openapi - * /plugin-api/proxmox/stats/heartbeat: - * post: - * summary: Update Proxmox stats viewer heartbeat - * description: Keeps a Proxmox stats viewer session alive. - * tags: [Proxmox] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * viewerSessionId: - * type: string - * responses: - * 200: - * description: Heartbeat updated. - * 400: - * description: Invalid viewerSessionId. - * 404: - * description: Viewer session not found. - */ - app.post("/heartbeat", (req, res) => { - const { viewerSessionId } = req.body as { viewerSessionId?: string }; - if (!viewerSessionId || typeof viewerSessionId !== "string") { - return res.status(400).json({ error: "Invalid viewerSessionId" }); - } - const ok = manager.updateHeartbeat(viewerSessionId); - if (!ok) return res.status(404).json({ error: "Viewer session not found" }); - res.json({ success: true }); - }); - - /** - * @openapi - * /plugin-api/proxmox/stats/history/{hostId}: - * get: - * summary: Get historical Proxmox node stats for a host - * tags: [Proxmox] - * parameters: - * - in: path - * name: hostId - * required: true - * schema: - * type: integer - * - in: query - * name: range - * schema: - * type: string - * enum: [1h, 6h, 24h, 7d, 30d] - * - in: query - * name: from - * schema: - * type: string - * format: date-time - * - in: query - * name: to - * schema: - * type: string - * format: date-time - * responses: - * 200: - * description: Array of node history rows. - * 400: - * description: Invalid range or date. - */ - app.get("/history/:hostId", async (req, res) => { - const hostId = Number(req.params.hostId); - const { range, from, to } = req.query as Record; - - const RANGE_OFFSETS: Record = { - "1h": 1 * 60 * 60 * 1000, - "6h": 6 * 60 * 60 * 1000, - "24h": 24 * 60 * 60 * 1000, - "7d": 7 * 24 * 60 * 60 * 1000, - "30d": 30 * 24 * 60 * 60 * 1000, - }; - - let fromTs: string; - let toTs = new Date().toISOString(); - if (range) { - const offsetMs = RANGE_OFFSETS[range]; - if (!offsetMs) { - return res - .status(400) - .json({ error: "Invalid range. Use 1h, 6h, 24h, 7d, or 30d" }); - } - fromTs = new Date(Date.now() - offsetMs).toISOString(); - } else if (from && to) { - const fromDate = new Date(from); - const toDate = new Date(to); - if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) { - return res.status(400).json({ error: "Invalid from/to date format" }); - } - fromTs = fromDate.toISOString(); - toTs = toDate.toISOString(); - } else { - fromTs = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - } - - try { - const toSqlite = (iso: string) => - iso.replace("T", " ").replace(/\.\d{3}Z$/, ""); - const rows = ( - await historyRepository.listRange( - hostId, - toSqlite(fromTs), - toSqlite(toTs), - ) - ).map((row) => ({ - ts: row.ts, - cpu_percent: row.cpuPercent, - mem_percent: row.memPercent, - disk_percent: row.diskPercent, - net_rx_bytes: row.netRxBytes, - net_tx_bytes: row.netTxBytes, - })); - res.json({ rows, fromTs, toTs }); - } catch (error) { - pluginCtx().log.error( - `Failed to fetch proxmox stats history for host ${hostId}`, - error as Error, - ); - res.status(500).json({ error: "Failed to fetch proxmox stats history" }); - } - }); -} - -export { parseProxmoxStatsConfig }; diff --git a/plugins/proxmox/src/backend/tables.ts b/plugins/proxmox/src/backend/tables.ts deleted file mode 100644 index 0c91970f6..000000000 --- a/plugins/proxmox/src/backend/tables.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - adoptLegacyTable, - defineTable, - id, - integer, - real, - refHost, - refUser, - text, - timestamp, -} from "@termix/plugin-sdk/db"; - -/** - * Per-host historical node stats samples (CPU, memory, disk, network), - * polled on an interval while a viewer has the Proxmox stats tab open. - * - * Adopted from core's proxmox_node_history, so the column and index names are - * the legacy ones: the rename carries the existing rows across. - */ -export const proxmoxNodeHistory = adoptLegacyTable( - "proxmox_node_history", - defineTable("node_history", { - id: id(), - hostId: refHost(), - ts: timestamp().notNull().defaultNow(), - cpuPercent: real(), - memPercent: real(), - diskPercent: real(), - netRxBytes: integer(), - netTxBytes: integer(), - }), -); - -/** - * One saved dashboard layout per user per host for the Proxmox stats tab. - * - * Adopted from core's proxmox_stats_preferences. - */ -export const proxmoxStatsPreferences = adoptLegacyTable( - "proxmox_stats_preferences", - defineTable( - "stats_preferences", - { - id: id(), - userId: refUser(), - hostId: refHost(), - // JSON-encoded layout. No secrets, so plain text. - layout: text().notNull(), - createdAt: timestamp().notNull().defaultNow(), - updatedAt: timestamp().notNull().defaultNow(), - }, - { - indexes: [ - { - name: "idx_proxmox_stats_prefs_user_host", - columns: ["userId", "hostId"], - unique: true, - }, - ], - }, - ), -); - -export const tables = [proxmoxNodeHistory, proxmoxStatsPreferences]; diff --git a/plugins/proxmox/src/frontend/HostProxmoxTab.tsx b/plugins/proxmox/src/frontend/HostProxmoxTab.tsx deleted file mode 100644 index de154f55e..000000000 --- a/plugins/proxmox/src/frontend/HostProxmoxTab.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import { useEffect, useState } from "react"; -import { useSshAuthTypes, useTranslation } from "@termix/plugin-sdk/frontend"; -import { Server } from "lucide-react"; -import { - FakeSwitch, - Input, - SectionCard, - Select2, - SettingRow, -} from "@termix/plugin-sdk/ui"; -import { listCredentials } from "@termix/plugin-sdk/frontend"; -import type { ProxmoxHostConfig } from "./types"; - -interface HostProxmoxForm { - enableProxmox: boolean; - proxmoxConfig: ProxmoxHostConfig | null; -} - -type SetHostField = (key: string, value: unknown) => void; - -export function HostProxmoxTab({ - form, - setField, -}: { - form: HostProxmoxForm; - setField: SetHostField; -}) { - const { t } = useTranslation(); - const sshAuthTypes = useSshAuthTypes(); - const [credentials, setCredentials] = useState< - { id: number; name: string; username: string | null }[] - >([]); - - useEffect(() => { - listCredentials() - .then((list) => - setCredentials( - list.map((c) => ({ - id: c.id, - name: c.name, - username: c.username ?? null, - })), - ), - ) - .catch(() => {}); - }, []); - - const cfg = form.proxmoxConfig ?? { - defaultCredentialId: null, - defaultAuthType: "password", - windowsPatterns: "win, windows", - dockerPatterns: "docker", - preferredPrefixes: "10., 192.168.", - autoSyncEnabled: false, - syncIntervalMinutes: 15, - markMissingGuests: true, - }; - const lastSyncResult = cfg.lastSyncResult; - const lastSyncSummary = lastSyncResult - ? t("hosts.proxmoxLastSyncSummary", { - created: lastSyncResult.created, - updated: lastSyncResult.updated, - markedMissing: lastSyncResult.markedMissing, - skipped: lastSyncResult.skipped, - }) - : t("hosts.proxmoxLastSyncNoResult"); - const lastSyncDescription = cfg.lastSyncAt - ? `${new Date(cfg.lastSyncAt).toLocaleString()}, ${lastSyncSummary}` - : t("hosts.proxmoxLastSyncNever"); - - return ( - } - > -
- - {t("hosts.enableProxmoxDesc")}{" "} - - {t("hosts.docsLink")} - - - } - > - setField("enableProxmox", v)} - /> - - {form.enableProxmox && ( - <> - - - setField("proxmoxConfig", { - ...cfg, - defaultAuthType: e.target.value, - }) - } - className="h-7 w-44 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring" - > - {sshAuthTypes.types.map((option) => ( - - ))} - - - - - setField("proxmoxConfig", { - ...cfg, - defaultCredentialId: e.target.value - ? Number(e.target.value) - : null, - }) - } - className="h-7 w-44 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring" - > - - {credentials.map((c) => ( - - ))} - - - - - setField("proxmoxConfig", { - ...cfg, - windowsPatterns: e.target.value, - }) - } - placeholder="win, windows" - /> - - - - setField("proxmoxConfig", { - ...cfg, - dockerPatterns: e.target.value, - }) - } - placeholder="docker" - /> - - - - setField("proxmoxConfig", { - ...cfg, - preferredPrefixes: e.target.value, - }) - } - placeholder="10., 192.168." - /> - - - - setField("proxmoxConfig", { - ...cfg, - autoSyncEnabled: v, - }) - } - /> - - - - setField("proxmoxConfig", { - ...cfg, - syncIntervalMinutes: Math.max( - 5, - Number.parseInt(e.target.value || "15", 10), - ), - }) - } - /> - - - - setField("proxmoxConfig", { - ...cfg, - markMissingGuests: v, - }) - } - /> - - - - {cfg.lastSyncStatus - ? t(`hosts.proxmoxLastSyncStatus.${cfg.lastSyncStatus}`) - : t("hosts.proxmoxLastSyncStatus.pending")} - - - - )} -
-
- ); -} diff --git a/plugins/proxmox/src/frontend/ProxmoxDiscoverDialog.tsx b/plugins/proxmox/src/frontend/ProxmoxDiscoverDialog.tsx deleted file mode 100644 index 368feb16d..000000000 --- a/plugins/proxmox/src/frontend/ProxmoxDiscoverDialog.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import { useRef, useState } from "react"; -import { - notifyHostsChanged, - useTranslation, -} from "@termix/plugin-sdk/frontend"; -import { Server, RefreshCw, CheckSquare, Square, Download } from "lucide-react"; -import { toast } from "sonner"; -import { Button } from "@termix/plugin-sdk/ui"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@termix/plugin-sdk/ui"; -import { Select2 } from "@termix/plugin-sdk/ui"; -import { - discoverProxmoxGuestsStream, - importProxmoxHosts, - listHosts, -} from "./proxmox-api"; -import type { PluginHostRecord } from "@termix/plugin-sdk/frontend"; -import type { ProxmoxGuest } from "./types"; -import { resolveProxmoxImportAuth } from "./proxmox-import-auth"; - -interface ProxmoxDiscoverDialogProps { - open: boolean; - onClose: () => void; - hosts: PluginHostRecord[]; - onHostsChanged: (hosts: PluginHostRecord[]) => void; - /** Pre-select a specific host and skip the host picker */ - preselectedHostId?: number; - /** Credential to use for imported hosts */ - defaultCredentialId?: number | null; - /** Auth type to use for imported hosts */ - defaultAuthType?: string; - /** Username from the default credential */ - defaultUsername?: string; -} - -export function ProxmoxDiscoverDialog({ - open, - onClose, - hosts, - onHostsChanged, - preselectedHostId, - defaultCredentialId, - defaultAuthType, - defaultUsername, -}: ProxmoxDiscoverDialogProps) { - const { t } = useTranslation(); - const [selectedHostId, setSelectedHostId] = useState( - preselectedHostId ? String(preselectedHostId) : "", - ); - const [discovering, setDiscovering] = useState(false); - const [progress, setProgress] = useState<{ - done: number; - total: number; - } | null>(null); - const streamCloseRef = useRef<(() => void) | null>(null); - const [guests, setGuests] = useState(null); - const [discoveredCredentialId, setDiscoveredCredentialId] = useState< - number | null - >(null); - const [discoveredJumpHosts, setDiscoveredJumpHosts] = useState< - unknown[] | null - >(null); - const [selected, setSelected] = useState>(new Set()); - const [importing, setImporting] = useState(false); - - // When opened from the dropdown (no preselectedHostId), only show Proxmox-enabled hosts - const sshHosts = hosts.filter( - (h) => - !("isFolder" in h) && - ( - h.pluginSettings as - Record | undefined> | undefined - )?.proxmox?.enableProxmox === true, - ); - - // The Proxmox host the discovery runs against — imported guests are grouped - // into a folder named after it. Use the same id resolution as discovery so - // it also works when launched directly from a host action (preselectedHostId). - const effectiveHostId = - preselectedHostId != null ? String(preselectedHostId) : selectedHostId; - const sourceHost = hosts.find( - (h) => !("isFolder" in h) && String(h.id) === effectiveHostId, - ); - const importFolder = sourceHost?.name || "Proxmox"; - - function reset() { - if (!preselectedHostId) setSelectedHostId(""); - setGuests(null); - setDiscoveredCredentialId(null); - setDiscoveredJumpHosts(null); - setSelected(new Set()); - streamCloseRef.current?.(); - streamCloseRef.current = null; - setProgress(null); - setDiscovering(false); - setImporting(false); - } - - function handleDiscover() { - const hostId = - preselectedHostId ?? (selectedHostId ? Number(selectedHostId) : null); - if (!hostId) return; - setDiscovering(true); - setGuests(null); - setDiscoveredCredentialId(null); - setDiscoveredJumpHosts(null); - setSelected(new Set()); - setProgress(null); - streamCloseRef.current?.(); - streamCloseRef.current = discoverProxmoxGuestsStream(hostId, { - onProgress: (done, total) => setProgress({ done, total }), - onResult: (result) => { - setGuests(result.guests); - setDiscoveredCredentialId(result.credentialId ?? null); - setDiscoveredJumpHosts(result.jumpHosts ?? null); - setSelected( - new Set( - result.guests - .filter((g) => g.status === "running") - .map((g) => g.vmid), - ), - ); - setDiscovering(false); - setProgress(null); - streamCloseRef.current = null; - }, - onError: (message) => { - toast.error(message ?? t("hosts.proxmoxDiscoveryFailed")); - setDiscovering(false); - setProgress(null); - streamCloseRef.current = null; - }, - }); - } - - async function handleImport() { - if (!guests || selected.size === 0) return; - setImporting(true); - try { - // Prefer explicitly configured credential, then fall back to the host's own credential - const credId = defaultCredentialId ?? discoveredCredentialId; - const importAuth = resolveProxmoxImportAuth(defaultAuthType, credId); - - const selectedGuests = guests.filter((g) => selected.has(g.vmid)); - - const toImport = selectedGuests.map((g) => ({ - name: g.name, - // No IP discovered (e.g. QEMU without a running guest agent): import - // with a placeholder so the host is created and the user can fill in - // the real IP. Re-sync keeps the manual value (guest.ip || existing.ip). - ip: g.ip || "0.0.0.0", - port: g.connectionType === "rdp" ? 3389 : 22, - username: - importAuth.authType === "credential" - ? "" - : (defaultUsername ?? "root"), - folder: importFolder, - // Inherit the jump-host chain from the scanned Proxmox host so the - // imported guests are reachable the same way; user can override. - jumpHosts: discoveredJumpHosts ?? undefined, - ...importAuth, - enableSsh: g.connectionType !== "rdp", - connectionType: g.connectionType, - tags: [ - "proxmox", - g.type, - g.node, - g.type === "lxc" ? `ct-${g.vmid}` : `vm-${g.vmid}`, - ...(g.enableDocker ? ["docker"] : []), - ], - // Host settings travel with the import like an export's, keyed by - // the plugin that keeps them. - pluginSettings: { - ...(g.connectionType === "rdp" - ? { "remote-desktop": { enableRdp: true, rdpPort: 3389 } } - : {}), - ...(g.enableDocker ? { docker: { enableDocker: true } } : {}), - proxmox: { - proxmoxConfig: { - source: { - source: "proxmox", - sourceHostId: Number(effectiveHostId), - node: g.node, - vmid: g.vmid, - type: g.type, - lastSeenAt: new Date().toISOString(), - lastStatus: g.status, - missingSince: null, - }, - }, - }, - }, - })); - - const result = toImport.length - ? await importProxmoxHosts(toImport) - : { success: 0, failed: 0, errors: [] }; - - if (toImport.length) { - const updated = await listHosts(); - onHostsChanged(updated); - notifyHostsChanged(); - } - - const msg = [ - result.success - ? t("hosts.proxmoxResultImported", { count: result.success }) - : null, - result.failed - ? t("hosts.proxmoxResultFailed", { count: result.failed }) - : null, - ] - .filter(Boolean) - .join(", "); - toast.success(t("hosts.proxmoxImportComplete", { summary: msg })); - - if (result.failed === 0) { - reset(); - onClose(); - } - } catch (err: any) { - toast.error(err?.message ?? t("hosts.proxmoxImportFailed")); - } finally { - setImporting(false); - } - } - - const nodeGroups = guests - ? guests.reduce( - (acc, g) => { - if (!acc[g.node]) acc[g.node] = []; - acc[g.node].push(g); - return acc; - }, - {} as Record, - ) - : {}; - - const canDiscover = preselectedHostId != null || !!selectedHostId; - - return ( - { - if (!v) { - reset(); - onClose(); - } - }} - > - - - - - {t("hosts.proxmoxImportTitle")} - - - {t("hosts.docsLink")} - - - -
- {/* Host selector — hidden when launched from a specific host */} - {!preselectedHostId && ( -
- setSelectedHostId(event.target.value)} - disabled={discovering} - placeholder={t("hosts.proxmoxSelectHost")} - className="flex-1 text-xs h-8" - > - {sshHosts.map((h) => ( - - ))} - - -
- )} - - {/* When launched from a specific host, show Discover directly */} - {preselectedHostId && !guests && ( - - )} - - {/* Guest list */} - {guests !== null && ( -
-
- - {t("hosts.proxmoxGuestsSelected", { - count: guests.length, - selected: selected.size, - })} - -
- - -
-
- - {guests.length === 0 ? ( -

- {t("hosts.proxmoxNoGuests")} -

- ) : ( -
- {Object.entries(nodeGroups).map(([node, nodeGuests]) => ( -
-
- {node} -
- {nodeGuests.map((g) => ( - - ))} -
- ))} -
- )} -
- )} -
- - - - {guests !== null && selected.size > 0 && ( - - )} - -
-
- ); -} diff --git a/plugins/proxmox/src/frontend/index.tsx b/plugins/proxmox/src/frontend/index.tsx deleted file mode 100644 index b8a6ff499..000000000 --- a/plugins/proxmox/src/frontend/index.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { useEffect, useState, type ComponentType } from "react"; -import { Boxes, HardDrive, Server } from "lucide-react"; -import { - useHosts, - useTranslation, - type HostEditorSectionProps, - type StandaloneViewProps, - type TabProps, - type TermixApp, -} from "@termix/plugin-sdk/frontend"; -import { ComponentSlot, DropdownMenuItem } from "@termix/plugin-sdk/ui"; -import type { PluginHostRecord } from "@termix/plugin-sdk/frontend"; -import { setProxmoxApp } from "./proxmox-api"; -import { ProxmoxDiscoverDialog } from "./ProxmoxDiscoverDialog"; -import { HostProxmoxTab } from "./HostProxmoxTab"; -import { ProxmoxStatsTab } from "./stats/ProxmoxStatsTab"; -import ProxmoxStatsApp from "./stats/ProxmoxStatsApp"; -import { HostProxmoxStatsTab } from "./stats/HostProxmoxStatsTab"; - -interface DiscoverRequest { - hostId?: number; - defaultCredentialId?: number | null; - defaultAuthType?: string; -} - -type Opener = (request: DiscoverRequest) => void; - -// The Proxmox fields are this plugin's host settings, saved with the host -// through form.pluginSettings.proxmox. -function ProxmoxHostSection({ form, updateForm }: HostEditorSectionProps) { - const values = (form.pluginSettings?.proxmox ?? {}) as Record< - string, - unknown - >; - const setField = (key: string, value: unknown) => - updateForm((current) => { - const all = (current.pluginSettings ?? {}) as Record< - string, - Record - >; - return { - ...current, - pluginSettings: { - ...all, - proxmox: { ...(all.proxmox ?? {}), [key]: value }, - }, - }; - }); - const sectionForm = { - enableProxmox: values.enableProxmox === true, - proxmoxConfig: values.proxmoxConfig ?? null, - enableProxmoxStats: values.enableProxmoxStats === true, - proxmoxStatsConfig: values.proxmoxStatsConfig ?? { - pollInterval: 60, - nodeName: null, - }, - }; - return ( - <> - [0]["form"]} - setField={setField as Parameters[0]["setField"]} - /> - [0]["form"]} - setField={ - setField as Parameters[0]["setField"] - } - /> - {/* Other plugins add Proxmox-related settings here. */} - - - ); -} - -function ProxmoxStatsTabView({ sshHost, label, isVisible }: TabProps) { - return ( - - ); -} - -function ProxmoxStatsStandalone({ hostId }: StandaloneViewProps) { - return ; -} - -/** This plugin's host settings, as they arrive on the host record. */ -export function proxmoxSettings( - host: Pick, -): Record { - return host.pluginSettings?.proxmox ?? {}; -} - -export function activate(app: TermixApp): void { - setProxmoxApp(app); - app.onDispose(() => setProxmoxApp(null)); - // The dialog lives in the hosts panel; the menu item and host action open - // it through this, created per activation. - const openers = new Set(); - const openDiscover: Opener = (request) => { - for (const open of openers) open(request); - }; - - function DiscoverDialogHost({ - hosts, - onHostsChanged, - }: { - hosts: PluginHostRecord[]; - onHostsChanged: (hosts: PluginHostRecord[]) => void; - }) { - const [request, setRequest] = useState(null); - useEffect(() => { - openers.add(setRequest); - return () => { - openers.delete(setRequest); - }; - }, []); - if (!request) return null; - return ( - setRequest(null)} - hosts={hosts} - onHostsChanged={onHostsChanged} - preselectedHostId={request.hostId} - defaultCredentialId={request.defaultCredentialId ?? null} - defaultAuthType={request.defaultAuthType} - /> - ); - } - - function ImportMenuItem() { - const { t } = useTranslation(); - const { hosts } = useHosts(); - return ( - openDiscover({})} - disabled={!hosts.some((host) => proxmoxSettings(host).enableProxmox)} - > - - {t("hosts.proxmoxImportTitle")} - - ); - } - - app.registerSlotContribution("hosts.panel", { - actionId: "proxmox.discoverDialog", - titleKey: "hosts.proxmoxImportTitle", - kind: "component", - component: DiscoverDialogHost as unknown as ComponentType< - Record - >, - }); - - app.registerSlotContribution("hosts.importMenu", { - actionId: "proxmox.import", - titleKey: "hosts.proxmoxImportTitle", - kind: "component", - component: ImportMenuItem as ComponentType>, - }); - - app.registerHostAction({ - id: "proxmox-discover", - titleKey: "hosts.proxmoxDiscoverAction", - icon: Boxes, - kind: "open", - order: 90, - when: (host) => !!proxmoxSettings(host).enableProxmox, - run: (host) => { - const config = proxmoxSettings(host).proxmoxConfig as - | { defaultCredentialId?: number | null; defaultAuthType?: string } - | undefined; - openDiscover({ - hostId: Number(host.id), - defaultCredentialId: config?.defaultCredentialId ?? null, - defaultAuthType: config?.defaultAuthType ?? undefined, - }); - }, - }); - - app.registerHostEditorSection({ - id: "proxmox", - group: "ssh", - titleKey: "hosts.tabProxmox", - icon: Server, - order: 50, - component: ProxmoxHostSection, - }); - - app.registerTab("proxmox-stats", ProxmoxStatsTabView, { - icon: HardDrive, - titleKey: "nav.proxmoxStats", - requiresHost: true, - noHostMessageKey: "proxmoxStats.noHostSelected", - standalone: ProxmoxStatsStandalone, - preload: () => import("./stats/ProxmoxStatsTab"), - }); - - app.registerHostAction({ - id: "proxmox-stats", - titleKey: "nav.proxmoxStats", - icon: HardDrive, - kind: "open", - order: 60, - tabType: "proxmox-stats", - copyUrlView: "proxmox-stats", - when: (host) => proxmoxSettings(host).enableProxmoxStats === true, - }); -} diff --git a/plugins/proxmox/src/frontend/proxmox-api.ts b/plugins/proxmox/src/frontend/proxmox-api.ts deleted file mode 100644 index 08bcac634..000000000 --- a/plugins/proxmox/src/frontend/proxmox-api.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { TermixApp } from "@termix/plugin-sdk/frontend"; -import type { ProxmoxDiscoverResult } from "./types"; - -let current: TermixApp | null = null; - -/** Set in activate, cleared on deactivate. */ -export function setProxmoxApp(app: TermixApp | null): void { - current = app; -} - -function app(): TermixApp { - if (!current) throw new Error("The Proxmox plugin is not active"); - return current; -} - -export function listHosts(): ReturnType { - return app().listHosts(); -} - -/** - * Runs a guest discovery over the server-sent event stream and reports - * progress. Returns a function that stops listening. - */ -export function discoverProxmoxGuestsStream( - hostId: number, - handlers: { - onProgress?: (done: number, total: number) => void; - onResult: (result: ProxmoxDiscoverResult) => void; - onError: (message: string) => void; - }, -): () => void { - const controller = new AbortController(); - let settled = false; - const finish = () => { - settled = true; - controller.abort(); - }; - - const handleEvent = (event: string, data: string) => { - if (event === "progress") { - try { - const parsed = JSON.parse(data); - handlers.onProgress?.(parsed.done, parsed.total); - } catch { - // ignore malformed progress frames - } - } else if (event === "result") { - finish(); - try { - handlers.onResult(JSON.parse(data)); - } catch { - handlers.onError("Failed to parse discovery result"); - } - } else if (event === "fail") { - finish(); - let message = "Discovery failed"; - try { - message = JSON.parse(data).message || message; - } catch { - // keep default message - } - handlers.onError(message); - } - }; - - void (async () => { - try { - const response = await app().fetch( - `/discover/stream?hostId=${encodeURIComponent(String(hostId))}`, - { signal: controller.signal }, - ); - if (!response.ok || !response.body) throw new Error("stream failed"); - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - for (;;) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - let split = buffer.indexOf("\n\n"); - while (split !== -1) { - const frame = buffer.slice(0, split); - buffer = buffer.slice(split + 2); - let event = "message"; - const data: string[] = []; - for (const line of frame.split("\n")) { - if (line.startsWith("event:")) event = line.slice(6).trim(); - else if (line.startsWith("data:")) data.push(line.slice(5).trim()); - } - if (data.length) handleEvent(event, data.join("\n")); - split = buffer.indexOf("\n\n"); - } - } - if (!settled) { - settled = true; - handlers.onError("Discovery connection lost"); - } - } catch { - if (settled) return; - settled = true; - handlers.onError("Discovery connection lost"); - } - })(); - - return finish; -} - -export interface ProxmoxImportResult { - success: number; - failed: number; - errors: string[]; -} - -export async function importProxmoxHosts( - hosts: Record[], -): Promise { - const response = await app().api.post("/import", { - hosts, - }); - return response.data; -} diff --git a/plugins/proxmox/src/frontend/proxmox-import-auth.ts b/plugins/proxmox/src/frontend/proxmox-import-auth.ts deleted file mode 100644 index e48ca7ef3..000000000 --- a/plugins/proxmox/src/frontend/proxmox-import-auth.ts +++ /dev/null @@ -1,36 +0,0 @@ -const SECRET_BACKED_AUTH_TYPES = new Set(["password", "key"]); - -export type ProxmoxImportAuth = { - authType: string; - credentialId?: number; - overrideCredentialUsername?: boolean; -}; - -export function resolveProxmoxImportAuth( - defaultAuthType: string | undefined, - credentialId: number | null | undefined, -): ProxmoxImportAuth { - // Any auth type that needs no stored secret (none, agent, or one a plugin - // adds) wins, the same rule the backend copy applies. - if ( - defaultAuthType && - defaultAuthType !== "credential" && - !SECRET_BACKED_AUTH_TYPES.has(defaultAuthType) - ) { - return { authType: defaultAuthType }; - } - - // A credential (configured default OR inherited from the source Proxmox host) - // is a concrete auth source -> use it, even when defaultAuthType is the - // "password"/"key" default. Otherwise imported guests end up as authType - // "none" although the host authenticates via a credential. - if (credentialId) { - return { - authType: "credential", - credentialId, - overrideCredentialUsername: false, - }; - } - - return { authType: "none" }; -} diff --git a/plugins/proxmox/src/frontend/stats/GuestTable.tsx b/plugins/proxmox/src/frontend/stats/GuestTable.tsx deleted file mode 100644 index f5b202c07..000000000 --- a/plugins/proxmox/src/frontend/stats/GuestTable.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { useMemo, useState } from "react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { Box, Search, Server as ServerIcon } from "lucide-react"; -import { Input } from "@termix/plugin-sdk/ui"; -import type { ProxmoxGuestSummary } from "../types"; -import { cn } from "@termix/plugin-sdk/ui"; - -type StatusFilter = "all" | "running" | "stopped"; -type TypeFilter = "all" | "qemu" | "lxc"; - -function formatUptime(seconds: number | null): string { - if (seconds === null || seconds <= 0) return "-"; - const days = Math.floor(seconds / 86400); - const hours = Math.floor((seconds % 86400) / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - if (days > 0) return `${days}d ${hours}h`; - if (hours > 0) return `${hours}h ${minutes}m`; - return `${minutes}m`; -} - -function UsageCell({ - percent, - usedGiB, - totalGiB, -}: { - percent: number | null; - usedGiB?: number | null; - totalGiB?: number | null; -}) { - if (percent === null) { - return -; - } - const clamped = Math.min(100, Math.max(0, percent)); - const barColor = - clamped >= 90 - ? "bg-red-500" - : clamped >= 75 - ? "bg-yellow-500" - : "bg-accent-brand"; - return ( -
-
- - {clamped.toFixed(0)}% - - {usedGiB != null && totalGiB != null && ( - - {usedGiB.toFixed(1)}/{totalGiB.toFixed(1)}G - - )} -
-
-
-
-
- ); -} - -export function GuestTable({ guests }: { guests: ProxmoxGuestSummary[] }) { - const { t } = useTranslation(); - const [query, setQuery] = useState(""); - const [statusFilter, setStatusFilter] = useState("all"); - const [typeFilter, setTypeFilter] = useState("all"); - - const filtered = useMemo(() => { - const q = query.trim().toLowerCase(); - return guests - .filter( - (g) => - statusFilter === "all" || - (statusFilter === "running") === (g.status === "running"), - ) - .filter((g) => typeFilter === "all" || g.type === typeFilter) - .filter( - (g) => - !q || g.name.toLowerCase().includes(q) || String(g.vmid).includes(q), - ) - .sort((a, b) => { - if (a.status === b.status) return a.name.localeCompare(b.name); - return a.status === "running" ? -1 : 1; - }); - }, [guests, query, statusFilter, typeFilter]); - - const running = guests.filter((g) => g.status === "running").length; - - return ( -
-
- - {t("proxmoxStats.guestsSummary")} - - - {t("proxmoxStats.guestCounts", { running, total: guests.length })} - -
-
- - setQuery(e.target.value)} - placeholder={t("proxmoxStats.searchGuests")} - className="h-7 w-40 pl-6 text-xs" - /> -
-
- {(["all", "qemu", "lxc"] as TypeFilter[]).map((f) => ( - - ))} -
-
- {(["all", "running", "stopped"] as StatusFilter[]).map((f) => ( - - ))} -
-
-
- -
- {guests.length === 0 ? ( -
- {t("proxmoxStats.noGuests")} -
- ) : filtered.length === 0 ? ( -
- {t("proxmoxStats.noGuestsMatch")} -
- ) : ( - - - - - - - - - - - - - - {filtered.map((guest) => ( - - - - - - - - - - ))} - -
- {t("proxmoxStats.name")} - - {t("proxmoxStats.status")} - ID - {t("proxmoxStats.cpu")} - - {t("proxmoxStats.mem")} - - {t("proxmoxStats.disk")} - - {t("proxmoxStats.uptime")} -
-
- {guest.type === "lxc" ? ( - - ) : ( - - )} - - {guest.name} - - - {guest.type === "lxc" ? "LXC" : "VM"} - -
-
-
- - - {guest.status === "running" - ? t("proxmoxStats.running") - : t("proxmoxStats.stopped")} - -
-
- {guest.vmid} - - - - - - - - {formatUptime(guest.uptimeSeconds)} -
- )} -
-
- ); -} diff --git a/plugins/proxmox/src/frontend/stats/HostProxmoxStatsTab.tsx b/plugins/proxmox/src/frontend/stats/HostProxmoxStatsTab.tsx deleted file mode 100644 index 8d52b04b5..000000000 --- a/plugins/proxmox/src/frontend/stats/HostProxmoxStatsTab.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { Server } from "lucide-react"; -import { - Input, - SectionCard, - SettingRow, - FakeSwitch, -} from "@termix/plugin-sdk/ui"; -import type { ProxmoxStatsConfig } from "../types"; - -interface HostProxmoxStatsForm { - enableProxmoxStats: boolean; - proxmoxStatsConfig?: ProxmoxStatsConfig | null; -} - -type SetHostField = (key: string, value: unknown) => void; - -export function HostProxmoxStatsTab({ - form, - setField, -}: { - form: HostProxmoxStatsForm; - setField: SetHostField; -}) { - const { t } = useTranslation(); - - return ( - } - > -
- - setField("enableProxmoxStats", v)} - /> - - {form.enableProxmoxStats && ( - <> - - - setField("proxmoxStatsConfig", { - ...form.proxmoxStatsConfig, - pollInterval: Math.max( - 15, - Number.parseInt(e.target.value || "60", 10), - ), - }) - } - /> - - - - setField("proxmoxStatsConfig", { - ...form.proxmoxStatsConfig, - nodeName: e.target.value || null, - }) - } - /> - - - )} -
-
- ); -} diff --git a/plugins/proxmox/src/frontend/stats/NodeSummaryStrip.tsx b/plugins/proxmox/src/frontend/stats/NodeSummaryStrip.tsx deleted file mode 100644 index eaa5b73f5..000000000 --- a/plugins/proxmox/src/frontend/stats/NodeSummaryStrip.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { Cpu, MemoryStick, HardDrive, Clock, Server } from "lucide-react"; -import type { ProxmoxNodeStats } from "../types"; -import { cn } from "@termix/plugin-sdk/ui"; - -function Tile({ - icon, - label, - percent, - detail, - sparkline, -}: { - icon: React.ReactNode; - label: string; - percent: number | null; - detail?: string | null; - sparkline?: number[]; -}) { - const clamped = percent === null ? null : Math.min(100, Math.max(0, percent)); - const barColor = - clamped === null - ? "bg-muted-foreground/30" - : clamped >= 90 - ? "bg-red-500" - : clamped >= 75 - ? "bg-yellow-500" - : "bg-accent-brand"; - - return ( -
-
- {icon} - - {label} - -
-
- - {clamped === null ? "N/A" : `${clamped.toFixed(0)}%`} - - {detail && ( - - {detail} - - )} -
-
-
-
- {sparkline && sparkline.length > 1 && } -
- ); -} - -function MiniSparkline({ data }: { data: number[] }) { - const w = 100; - const h = 20; - const max = 100; - const points = data - .map((v, i) => { - const x = (i / (data.length - 1)) * w; - const y = h - (Math.min(100, Math.max(0, v)) / max) * h; - return `${x.toFixed(1)},${y.toFixed(1)}`; - }) - .join(" "); - return ( - - - - ); -} - -export function NodeSummaryStrip({ - node, - histories, -}: { - node: ProxmoxNodeStats | undefined; - histories: { cpu: number[]; memory: number[]; disk: number[] }; -}) { - const { t } = useTranslation(); - - return ( -
- } - label={t("proxmoxStats.cpuUsage")} - percent={node?.cpu.percent ?? null} - detail={ - node?.cpu.cores != null - ? t("proxmoxStats.cpuCores", { count: node.cpu.cores }) - : undefined - } - sparkline={histories.cpu} - /> - } - label={t("proxmoxStats.memoryUsage")} - percent={node?.memory.percent ?? null} - detail={ - node?.memory.usedGiB != null && node?.memory.totalGiB != null - ? `${node.memory.usedGiB.toFixed(1)} / ${node.memory.totalGiB.toFixed(1)} GiB` - : undefined - } - sparkline={histories.memory} - /> - } - label={t("proxmoxStats.diskUsage")} - percent={node?.disk.percent ?? null} - detail={ - node?.disk.usedGiB != null && node?.disk.totalGiB != null - ? `${node.disk.usedGiB.toFixed(1)} / ${node.disk.totalGiB.toFixed(1)} GiB` - : undefined - } - sparkline={histories.disk} - /> -
-
- - - {t("proxmoxStats.uptime")} - -
- - {node?.uptime.formatted ?? "N/A"} - -
- - {node?.system.hostname ?? "-"} - {node?.system.pveVersion && ( - {node.system.pveVersion} - )} -
-
-
- ); -} diff --git a/plugins/proxmox/src/frontend/stats/ProxmoxStatsApp.tsx b/plugins/proxmox/src/frontend/stats/ProxmoxStatsApp.tsx deleted file mode 100644 index 7a55820b9..000000000 --- a/plugins/proxmox/src/frontend/stats/ProxmoxStatsApp.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from "react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { FullScreenAppWrapper, ConnectionScreen } from "@termix/plugin-sdk/ui"; -import { ProxmoxStatsTab } from "./ProxmoxStatsTab"; - -interface ProxmoxStatsAppProps { - hostId?: string; -} - -const ProxmoxStatsApp: React.FC = ({ hostId }) => { - const { t } = useTranslation(); - return ( - - {(hostConfig, phase) => { - if (phase === "loading") { - return ( -
- -
- ); - } - - if (!hostConfig) { - return ( -
- -
- ); - } - - return ( - - ); - }} -
- ); -}; - -export default ProxmoxStatsApp; diff --git a/plugins/proxmox/src/frontend/stats/ProxmoxStatsTab.tsx b/plugins/proxmox/src/frontend/stats/ProxmoxStatsTab.tsx deleted file mode 100644 index 774faed7b..000000000 --- a/plugins/proxmox/src/frontend/stats/ProxmoxStatsTab.tsx +++ /dev/null @@ -1,387 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import React from "react"; -import { - Button, - ConnectionScreen, - ConnectionLogProvider, - useConnectionLog, -} from "@termix/plugin-sdk/ui"; -import { - useTranslation, - usePluginApi, - useHosts, -} from "@termix/plugin-sdk/frontend"; -import { RefreshCw, Server } from "lucide-react"; -import { createProxmoxStatsApi } from "./proxmox-stats-api"; -import type { ProxmoxStatsSnapshot, ProxmoxStatsConfig } from "../types"; -import { NodeSummaryStrip } from "./NodeSummaryStrip"; -import { GuestTable } from "./GuestTable"; -import { NodeNetworkCard } from "./cards/NodeNetworkCard"; -import { StoragePoolsCard } from "./cards/StoragePoolsCard"; -import { ClusterHealthCard } from "./cards/ClusterHealthCard"; -import { useConnectionRetry } from "@termix/plugin-sdk/frontend"; -import { runAdaptivePolling } from "@termix/plugin-sdk/ui"; - -const HISTORY_LEN = 30; - -function statsChangeKey(data: ProxmoxStatsSnapshot): string { - const bucket = (value: number | null | undefined) => - value == null ? null : Math.round(value / 5) * 5; - return JSON.stringify({ - cpu: bucket(data.node.cpu.percent), - memory: bucket(data.node.memory.percent), - disk: bucket(data.node.disk.percent), - guests: data.guests.counts, - cluster: data.cluster, - }); -} -const DEFAULT_POLL_INTERVAL = 60; - -interface HostConfig { - id: number; - name: string; - ip: string; - username: string; - pluginSettings?: { - proxmox?: { - enableProxmoxStats?: boolean; - proxmoxStatsConfig?: string | ProxmoxStatsConfig | null; - }; - }; - authType?: string; - port?: number; - [key: string]: unknown; -} - -interface ProxmoxStatsProps { - hostConfig?: HostConfig; - title?: string; - isVisible?: boolean; - isTopbarOpen?: boolean; - embedded?: boolean; -} - -interface ProxmoxStatsHistories { - cpu: number[]; - memory: number[]; - disk: number[]; -} - -function parseProxmoxStatsConfig( - raw?: string | ProxmoxStatsConfig | null, -): Required> & ProxmoxStatsConfig { - const defaults = { pollInterval: DEFAULT_POLL_INTERVAL, nodeName: null }; - if (!raw) return defaults; - try { - const parsed = typeof raw === "string" ? JSON.parse(raw) : raw; - return { ...defaults, ...parsed }; - } catch { - return defaults; - } -} - -export function ProxmoxStatsTab(props: ProxmoxStatsProps): React.ReactElement { - return ( - - - - ); -} - -function ProxmoxStatsInner({ - hostConfig, - title, - isVisible = true, - isTopbarOpen = true, - embedded = false, -}: ProxmoxStatsProps): React.ReactElement { - const { t } = useTranslation(); - const { addLog, clearLogs } = useConnectionLog(); - const pluginApi = usePluginApi(); - const api = React.useMemo( - () => createProxmoxStatsApi(pluginApi), - [pluginApi], - ); - const { hosts } = useHosts(); - - const [snapshot, setSnapshot] = React.useState( - null, - ); - const [histories, setHistories] = React.useState({ - cpu: [], - memory: [], - disk: [], - }); - const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig); - const [isRefreshing, setIsRefreshing] = React.useState(false); - const [isPageVisible, setIsPageVisible] = React.useState(!document.hidden); - const [viewerSessionId, setViewerSessionId] = React.useState( - null, - ); - - const proxmoxSettings = currentHostConfig?.pluginSettings?.proxmox; - const statsConfig = React.useMemo( - () => parseProxmoxStatsConfig(proxmoxSettings?.proxmoxStatsConfig), - [proxmoxSettings?.proxmoxStatsConfig], - ); - - React.useEffect(() => { - const onVis = () => setIsPageVisible(!document.hidden); - document.addEventListener("visibilitychange", onVis); - return () => document.removeEventListener("visibilitychange", onVis); - }, []); - - const isActuallyVisible = isVisible && isPageVisible; - - React.useEffect(() => { - if (!viewerSessionId || !isActuallyVisible) return; - const interval = setInterval(() => { - api.sendHeartbeat(viewerSessionId).catch(() => {}); - }, 30000); - return () => clearInterval(interval); - }, [viewerSessionId, isActuallyVisible, api]); - - React.useEffect(() => { - if (hostConfig?.id !== currentHostConfig?.id) { - setSnapshot(null); - setHistories({ cpu: [], memory: [], disk: [] }); - } - setCurrentHostConfig(hostConfig); - }, [hostConfig?.id]); - - React.useEffect(() => { - if (!hostConfig?.id) return; - const updated = hosts.find((h) => String(h.id) === String(hostConfig.id)); - if (updated) setCurrentHostConfig(updated as unknown as HostConfig); - }, [hostConfig?.id, hosts]); - - const pushHistory = React.useCallback((data: ProxmoxStatsSnapshot) => { - setHistories((prev) => { - const add = (arr: number[], v: number | null | undefined) => - [...arr, v ?? 0].slice(-HISTORY_LEN); - return { - cpu: add(prev.cpu, data.node?.cpu?.percent), - memory: add(prev.memory, data.node?.memory?.percent), - disk: add(prev.disk, data.node?.disk?.percent), - }; - }); - }, []); - - const notEnabled = proxmoxSettings?.enableProxmoxStats !== true; - const stopPollingRef = React.useRef<(() => void) | null>(null); - - const fetchSnapshot = React.useCallback(async (): Promise => { - if (!currentHostConfig?.id) return; - - addLog({ - type: "info", - stage: "stats_connecting", - message: t("proxmoxStats.connecting"), - }); - - const result = await api.startPolling(currentHostConfig.id); - if (result.viewerSessionId) setViewerSessionId(result.viewerSessionId); - - addLog({ - type: "info", - stage: "stats_polling", - message: t("proxmoxStats.connecting"), - }); - - const data = await api.getStats(currentHostConfig.id); - if (!data) { - throw new Error(t("proxmoxStats.connectionFailed")); - } - - setSnapshot(data); - addLog({ - type: "success", - stage: "connected", - message: t("terminal.connected"), - }); - - const intervalMs = - (statsConfig.pollInterval ?? DEFAULT_POLL_INTERVAL) * 1000; - let signature = statsChangeKey(data); - stopPollingRef.current?.(); - stopPollingRef.current = runAdaptivePolling( - async () => { - const next = await api.getStats(currentHostConfig.id); - if (!next) throw new Error(t("proxmoxStats.connectionFailed")); - const nextSignature = statsChangeKey(next); - const changed = nextSignature !== signature; - signature = nextSignature; - setSnapshot(next); - pushHistory(next); - return changed; - }, - { - minIntervalMs: intervalMs, - maxIntervalMs: Math.min(120_000, intervalMs * 6), - stablePollsPerStep: 3, - }, - { runImmediately: false }, - ); - }, [ - currentHostConfig?.id, - statsConfig.pollInterval, - addLog, - t, - pushHistory, - api, - ]); - - const retry = useConnectionRetry({ - connect: async () => { - try { - await fetchSnapshot(); - retry.markConnected(); - } catch (error: unknown) { - addLog({ - type: "error", - stage: "error", - message: - error instanceof Error - ? error.message - : t("proxmoxStats.connectionFailed"), - }); - retry.markFailed(); - } - }, - enabled: isPageVisible && !notEnabled && !!currentHostConfig?.id, - autoStart: false, - }); - - const retryRef = React.useRef(retry); - retryRef.current = retry; - - // Connects once per host and stays connected while this tab exists, even - // when the user switches to another tab and back. Only the browser tab - // going into the background (isPageVisible) pauses/resumes it -- switching - // between Termix tabs must not tear down and reconnect the session. - React.useEffect(() => { - if (notEnabled || !currentHostConfig?.id) return; - - let cancelled = false; - const debounce = setTimeout(() => { - if (isPageVisible && !cancelled) { - clearLogs(); - retryRef.current.reset(); - retryRef.current.retryNow(); - } else if (!isPageVisible) { - stopPollingRef.current?.(); - stopPollingRef.current = null; - if (currentHostConfig?.id) { - api.stopPolling(currentHostConfig.id, undefined).catch(() => {}); - } - } - }, 500); - - return () => { - cancelled = true; - clearTimeout(debounce); - stopPollingRef.current?.(); - stopPollingRef.current = null; - if (currentHostConfig?.id) { - api.stopPolling(currentHostConfig.id).catch(() => {}); - } - }; - }, [currentHostConfig?.id, notEnabled, isPageVisible, api]); - - const wrapperStyle: React.CSSProperties = embedded - ? { opacity: isVisible ? 1 : 0, height: "100%", width: "100%" } - : { - opacity: isVisible ? 1 : 0, - margin: isTopbarOpen ? "74px 17px 8px 8px" : "16px 17px 8px 8px", - height: isTopbarOpen ? "calc(100vh - 82px)" : "calc(100vh - 24px)", - }; - - const handleRefresh = async () => { - if (!currentHostConfig?.id) return; - if (retry.status !== "connected") { - retry.retryNow(); - return; - } - try { - setIsRefreshing(true); - const data = await api.getStats(currentHostConfig.id); - if (data) { - setSnapshot(data); - pushHistory(data); - } - } finally { - setIsRefreshing(false); - } - }; - - const showContent = !notEnabled && retry.status === "connected" && snapshot; - - return ( -
-
- {retry.status === "connected" && !notEnabled && ( -
-
-
- -
-

{title}

-
- -
- )} - - {showContent && ( -
-
- -
- - - -
- - - {snapshot.cluster.clustered && ( - - )} -
-
- )} - - - -

- {t("proxmoxStats.noHostSelected")} -

-
- ) : undefined - } - /> -
-
- ); -} diff --git a/plugins/proxmox/src/frontend/stats/cards/ClusterHealthCard.tsx b/plugins/proxmox/src/frontend/stats/cards/ClusterHealthCard.tsx deleted file mode 100644 index 08ee455fd..000000000 --- a/plugins/proxmox/src/frontend/stats/cards/ClusterHealthCard.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Network } from "lucide-react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { MetricCard } from "@termix/plugin-sdk/ui"; -import type { ProxmoxStatsSnapshot } from "../../types"; -import { cn } from "@termix/plugin-sdk/ui"; - -export function ClusterHealthCard({ - snapshot, -}: { - snapshot: ProxmoxStatsSnapshot | null; -}) { - const { t } = useTranslation(); - const cluster = snapshot?.cluster; - - return ( - } - scroll - > - {!cluster || !cluster.clustered ? ( - - {t("proxmoxStats.notClustered")} - - ) : ( -
-
- - - {cluster.clusterName ?? t("proxmoxStats.clusterHealth")} - - - {cluster.quorate - ? t("proxmoxStats.quorate") - : t("proxmoxStats.notQuorate")} - -
-
- {cluster.nodes.map((node) => ( -
- - - {node.name} - - - {node.online - ? t("proxmoxStats.nodeOnline") - : t("proxmoxStats.nodeOffline")} - -
- ))} -
-
- )} -
- ); -} diff --git a/plugins/proxmox/src/frontend/stats/cards/NodeNetworkCard.tsx b/plugins/proxmox/src/frontend/stats/cards/NodeNetworkCard.tsx deleted file mode 100644 index cd0d52756..000000000 --- a/plugins/proxmox/src/frontend/stats/cards/NodeNetworkCard.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Network } from "lucide-react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { StatRow, MetricCard } from "@termix/plugin-sdk/ui"; -import type { ProxmoxStatsSnapshot } from "../../types"; - -export function NodeNetworkCard({ - snapshot, -}: { - snapshot: ProxmoxStatsSnapshot | null; -}) { - const { t } = useTranslation(); - const interfaces = snapshot?.network.interfaces ?? []; - - return ( - } - scroll - > - {interfaces.length === 0 ? ( - N/A - ) : ( -
- {interfaces.map((iface) => ( - - ))} -
- )} -
- ); -} diff --git a/plugins/proxmox/src/frontend/stats/cards/StoragePoolsCard.tsx b/plugins/proxmox/src/frontend/stats/cards/StoragePoolsCard.tsx deleted file mode 100644 index 9b9c48059..000000000 --- a/plugins/proxmox/src/frontend/stats/cards/StoragePoolsCard.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Database } from "lucide-react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { MetricCard } from "@termix/plugin-sdk/ui"; -import type { ProxmoxStatsSnapshot } from "../../types"; - -export function StoragePoolsCard({ - snapshot, -}: { - snapshot: ProxmoxStatsSnapshot | null; -}) { - const { t } = useTranslation(); - const pools = snapshot?.storage.pools ?? []; - - return ( - } - scroll - > - {pools.length === 0 ? ( - N/A - ) : ( -
- {pools.map((pool) => ( -
-
- - {pool.name} - - - {pool.type} - - {!pool.active && ( - - {t("proxmoxStats.inactive")} - - )} -
-
-
-
-
- - {pool.usedGiB !== null && pool.totalGiB !== null - ? `${pool.usedGiB.toFixed(1)} / ${pool.totalGiB.toFixed(1)} GiB` - : "N/A"} - -
-
- ))} -
- )} - - ); -} diff --git a/plugins/proxmox/src/frontend/stats/proxmox-stats-api.ts b/plugins/proxmox/src/frontend/stats/proxmox-stats-api.ts deleted file mode 100644 index 5fd5c4356..000000000 --- a/plugins/proxmox/src/frontend/stats/proxmox-stats-api.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; -import type { ProxmoxStatsSnapshot } from "../types"; - -/** - * The Proxmox node-stats routes, through the plugin's own client. Paths are - * relative to /plugin-api/proxmox/, which the client already points at. - */ -export function createProxmoxStatsApi(api: PluginApiClient) { - return { - async getStats(hostId: number): Promise { - try { - const response = await api.get( - `/stats/${hostId}`, - ); - return response.data; - } catch (error) { - if ( - typeof error === "object" && - error && - "response" in error && - (error as { response?: { status?: number } }).response?.status === 404 - ) { - return null; - } - throw error; - } - }, - - async startPolling(hostId: number): Promise<{ - success: boolean; - viewerSessionId?: string; - status?: string; - error?: string; - }> { - const response = await api.post<{ - success: boolean; - viewerSessionId?: string; - status?: string; - error?: string; - }>(`/stats/start/${hostId}`); - return response.data; - }, - - async stopPolling(hostId: number, viewerSessionId?: string): Promise { - await api.post(`/stats/stop/${hostId}`, { viewerSessionId }); - }, - - async sendHeartbeat(viewerSessionId: string): Promise { - await api.post("/stats/heartbeat", { viewerSessionId }); - }, - - async getHistory( - hostId: number, - opts: { range?: string; from?: string; to?: string }, - ): Promise { - const response = await api.get( - `/stats/history/${hostId}`, - { params: opts }, - ); - return response.data; - }, - }; -} - -export type ProxmoxStatsApi = ReturnType; - -export interface ProxmoxStatsHistoryRow { - ts: string; - cpu_percent: number | null; - mem_percent: number | null; - disk_percent: number | null; - net_rx_bytes: number | null; - net_tx_bytes: number | null; -} - -export interface ProxmoxStatsHistoryResponse { - rows: ProxmoxStatsHistoryRow[]; - fromTs: string; - toTs: string; -} diff --git a/plugins/proxmox/src/frontend/types.ts b/plugins/proxmox/src/frontend/types.ts deleted file mode 100644 index 806002571..000000000 --- a/plugins/proxmox/src/frontend/types.ts +++ /dev/null @@ -1,153 +0,0 @@ -// Frontend-side mirror of the backend Proxmox stats collectors' return -// shapes (src/backend/proxmox/*.ts). Kept in sync by hand. - -export interface ProxmoxNodeStats { - cpu: { - percent: number | null; - cores: number | null; - load: [number, number, number] | null; - }; - memory: { - percent: number | null; - usedGiB: number | null; - totalGiB: number | null; - }; - disk: { - percent: number | null; - usedGiB: number | null; - totalGiB: number | null; - }; - uptime: { - seconds: number | null; - formatted: string | null; - }; - system: { - hostname: string | null; - kernel: string | null; - pveVersion: string | null; - }; -} - -export interface ProxmoxNodeNetwork { - interfaces: Array<{ - name: string; - ip: string | null; - state: string | null; - rxBytes: string | null; - txBytes: string | null; - }>; -} - -export interface ProxmoxGuestSummary { - vmid: number; - name: string; - type: "qemu" | "lxc"; - status: string; - cpuPercent: number | null; - memPercent: number | null; - memUsedGiB: number | null; - memTotalGiB: number | null; - diskPercent: number | null; - diskUsedGiB: number | null; - diskTotalGiB: number | null; - uptimeSeconds: number | null; -} - -export interface ProxmoxGuestsSummary { - guests: ProxmoxGuestSummary[]; - counts: { running: number; stopped: number; total: number }; -} - -export interface ProxmoxStoragePool { - name: string; - type: string; - active: boolean; - enabled: boolean; - usedGiB: number | null; - totalGiB: number | null; - availGiB: number | null; - percent: number | null; -} - -export interface ProxmoxStorage { - pools: ProxmoxStoragePool[]; -} - -export type ProxmoxClusterHealth = - | { clustered: false } - | { - clustered: true; - quorate: boolean; - clusterName: string | null; - nodes: Array<{ - name: string; - online: boolean; - local: boolean; - ip: string | null; - }>; - }; - -export interface ProxmoxStatsSnapshot { - node: ProxmoxNodeStats; - network: ProxmoxNodeNetwork; - guests: ProxmoxGuestsSummary; - storage: ProxmoxStorage; - cluster: ProxmoxClusterHealth; - lastChecked: string; -} - -export interface ProxmoxStatsConfig { - nodeName?: string | null; - pollInterval?: number; - enabledCards?: string[]; -} - -/** The proxmoxConfig host setting. */ -export interface ProxmoxHostConfig { - source?: { - source: "proxmox"; - sourceHostId: number; - node: string; - vmid: number; - type: "qemu" | "lxc"; - lastSeenAt?: string; - lastStatus?: string; - missingSince?: string | null; - }; - defaultCredentialId: number | null; - defaultAuthType?: string; - windowsPatterns: string; - dockerPatterns: string; - preferredPrefixes: string; - autoSyncEnabled?: boolean; - syncIntervalMinutes?: number; - markMissingGuests?: boolean; - lastSyncAt?: string; - lastSyncStatus?: "success" | "error"; - lastSyncError?: string | null; - lastSyncResult?: { - created: number; - updated: number; - markedMissing: number; - skipped: number; - errors: string[]; - }; -} - -export interface ProxmoxGuest { - name: string; - vmid: number; - type: "qemu" | "lxc"; - node: string; - status: string; - ip: string | null; - connectionType: "ssh" | "rdp"; - enableDocker: boolean; -} - -export interface ProxmoxDiscoverResult { - guests: ProxmoxGuest[]; - credentialId: number | null; - defaultCredentialId: number | null; - jumpHosts?: unknown[] | null; -} diff --git a/plugins/proxmox/tests/backend/guest-sync.test.ts b/plugins/proxmox/tests/backend/guest-sync.test.ts deleted file mode 100644 index 53af05465..000000000 --- a/plugins/proxmox/tests/backend/guest-sync.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - getProxmoxSource, - indexImportedGuests, - proxmoxSourceKey, -} from "../../src/backend/guest-sync.js"; - -const source = { - source: "proxmox", - sourceHostId: 1, - node: "pve", - vmid: 101, - type: "qemu", -}; - -describe("getProxmoxSource", () => { - it("reads the source from an object or a JSON string", () => { - expect(getProxmoxSource({ source })).toEqual(source); - expect(getProxmoxSource(JSON.stringify({ source }))).toEqual(source); - }); - - it("refuses a malformed source", () => { - expect(getProxmoxSource(null)).toBeNull(); - expect(getProxmoxSource({ source: { ...source, vmid: "101" } })).toBeNull(); - expect(getProxmoxSource({ source: { ...source, type: "vm" } })).toBeNull(); - }); -}); - -describe("indexImportedGuests", () => { - // The host row carries no proxmoxConfig since 2.9.0; the source only - // exists in the plugin's host settings. - const hosts = [ - { id: 10, name: "guest-a" }, - { id: 11, name: "guest-b" }, - { id: 12, name: "manual" }, - ]; - - it("matches hosts to guests through their settings", () => { - const index = indexImportedGuests( - hosts, - [ - { hostId: 10, value: { source, lastSyncAt: "x" } }, - { hostId: 11, value: { source: { ...source, sourceHostId: 2 } } }, - ], - 1, - ); - expect([...index.keys()]).toEqual([proxmoxSourceKey(source as never)]); - const entry = index.get("1:pve:qemu:101")!; - expect(entry.host.id).toBe(10); - expect(entry.config.lastSyncAt).toBe("x"); - }); - - it("ignores settings for hosts the user does not own", () => { - const index = indexImportedGuests( - hosts, - [{ hostId: 99, value: { source } }], - 1, - ); - expect(index.size).toBe(0); - }); -}); diff --git a/plugins/proxmox/tests/backend/helpers.ts b/plugins/proxmox/tests/backend/helpers.ts deleted file mode 100644 index 90bd1b95c..000000000 --- a/plugins/proxmox/tests/backend/helpers.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { fileURLToPath } from "node:url"; - -export const pluginDir = fileURLToPath(new URL("../..", import.meta.url)); diff --git a/plugins/proxmox/tests/backend/migrations.test.ts b/plugins/proxmox/tests/backend/migrations.test.ts deleted file mode 100644 index 4967217c4..000000000 --- a/plugins/proxmox/tests/backend/migrations.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing"; -import { pluginDir } from "./helpers.js"; - -// proxmox_node_history and proxmox_stats_preferences as core's SQLite -// bootstrap created them before 2.9.0. -const LEGACY_DDL = ` - CREATE TABLE proxmox_node_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL REFERENCES ssh_data(id) ON DELETE CASCADE, - ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - cpu_percent REAL, - mem_percent REAL, - disk_percent REAL, - net_rx_bytes INTEGER, - net_tx_bytes INTEGER - ); - CREATE TABLE proxmox_stats_preferences ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - host_id INTEGER NOT NULL REFERENCES ssh_data(id) ON DELETE CASCADE, - layout TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - CREATE UNIQUE INDEX idx_proxmox_stats_prefs_user_host ON proxmox_stats_preferences (user_id, host_id); -`; - -let db: TestDb | null = null; - -afterEach(() => { - db?.close(); - db = null; -}); - -const tableExists = (name: string) => - !!db!.sqlite - .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") - .get(name); - -const indexOn = (table: string) => - ( - db!.sqlite - .prepare( - "SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND name NOT LIKE 'sqlite_%'", - ) - .all(table) as { name: string }[] - ).map((row) => row.name); - -describe("adopting proxmox_node_history and proxmox_stats_preferences", () => { - it("keeps every existing row when the legacy tables are there", async () => { - db = await createTestDb(pluginDir, { - before: (sqlite) => { - sqlite.exec( - "INSERT INTO users (id, username) VALUES ('user-1', 'user-1')", - ); - sqlite.exec("INSERT INTO ssh_data (id) VALUES (1)"); - sqlite.exec(LEGACY_DDL); - sqlite - .prepare( - "INSERT INTO proxmox_node_history (host_id, ts, cpu_percent) VALUES (?, ?, ?)", - ) - .run(1, "2026-01-01 00:00:00", 42); - sqlite - .prepare( - "INSERT INTO proxmox_stats_preferences (user_id, host_id, layout) VALUES (?, ?, ?)", - ) - .run("user-1", 1, '{"cards":[]}'); - }, - }); - - expect(db.applied).toEqual(["0001_adopt_proxmox_stats"]); - expect(tableExists("proxmox_node_history")).toBe(false); - expect(tableExists("proxmox_stats_preferences")).toBe(false); - expect(tableExists("p_proxmox_node_history")).toBe(true); - expect(tableExists("p_proxmox_stats_preferences")).toBe(true); - - expect( - db.sqlite.prepare("SELECT cpu_percent FROM p_proxmox_node_history").all(), - ).toEqual([{ cpu_percent: 42 }]); - expect( - db.sqlite.prepare("SELECT layout FROM p_proxmox_stats_preferences").all(), - ).toEqual([{ layout: '{"cards":[]}' }]); - expect(indexOn("p_proxmox_stats_preferences")).toEqual([ - "idx_proxmox_stats_prefs_user_host", - ]); - }); - - it("creates the tables fresh when there is no legacy one", async () => { - db = await createTestDb(pluginDir); - - expect(tableExists("proxmox_node_history")).toBe(false); - expect(tableExists("p_proxmox_node_history")).toBe(true); - expect(tableExists("p_proxmox_stats_preferences")).toBe(true); - expect( - db.sqlite.prepare("SELECT * FROM p_proxmox_node_history").all(), - ).toEqual([]); - }); -}); diff --git a/plugins/proxmox/tests/backend/proxmox-import-auth.test.ts b/plugins/proxmox/tests/backend/proxmox-import-auth.test.ts deleted file mode 100644 index 70bf71b93..000000000 --- a/plugins/proxmox/tests/backend/proxmox-import-auth.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { resolveProxmoxImportAuth } from "../../src/backend/proxmox-import-auth.js"; - -// The frontend carries its own copy of this decision in -// plugins/proxmox/frontend/proxmox-import-auth.ts. The two drifting apart is -// what produced the reported bug, so both are held to the same matrix. -describe("resolveProxmoxImportAuth", () => { - it("uses the default credential for key auth when one is configured", () => { - expect(resolveProxmoxImportAuth("key", 7)).toEqual({ - authType: "credential", - credentialId: 7, - overrideCredentialUsername: 0, - }); - }); - - it("uses the default credential for password auth when one is configured", () => { - expect(resolveProxmoxImportAuth("password", 7)).toEqual({ - authType: "credential", - credentialId: 7, - overrideCredentialUsername: 0, - }); - }); - - it("falls back to none when a secret-backed default has no credential", () => { - for (const authType of ["password", "key", "credential"]) { - expect(resolveProxmoxImportAuth(authType, null)).toEqual({ - authType: "none", - credentialId: null, - overrideCredentialUsername: 0, - }); - } - }); - - it("uses the credential when no default auth type is configured", () => { - expect(resolveProxmoxImportAuth(undefined, 42)).toEqual({ - authType: "credential", - credentialId: 42, - overrideCredentialUsername: 0, - }); - expect(resolveProxmoxImportAuth(undefined, null)).toEqual({ - authType: "none", - credentialId: null, - overrideCredentialUsername: 0, - }); - }); - - it("keeps secretless auth types, with or without a credential", () => { - for (const authType of ["none", "agent", "opkssh", "tailscale", "vault"]) { - for (const credentialId of [null, 7]) { - expect(resolveProxmoxImportAuth(authType, credentialId)).toEqual({ - authType, - credentialId: null, - overrideCredentialUsername: 0, - }); - } - } - }); -}); diff --git a/plugins/proxmox/tests/backend/proxmox-jump-hosts.test.ts b/plugins/proxmox/tests/backend/proxmox-jump-hosts.test.ts deleted file mode 100644 index e57743b6a..000000000 --- a/plugins/proxmox/tests/backend/proxmox-jump-hosts.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { parseProxmoxJumpHosts } from "../../src/backend/proxmox-jump-hosts.js"; - -describe("Proxmox jump-host persistence", () => { - it("reads a resolved array or a stored JSON string", () => { - expect(parseProxmoxJumpHosts([{ hostId: 7 }])).toEqual([{ hostId: 7 }]); - expect(parseProxmoxJumpHosts('[{"hostId":7}]')).toEqual([{ hostId: 7 }]); - }); - - it("turns missing or malformed values into null", () => { - expect(parseProxmoxJumpHosts(null)).toBeNull(); - expect(parseProxmoxJumpHosts("invalid")).toBeNull(); - }); -}); diff --git a/plugins/proxmox/tests/backend/proxmox-stats-polling.test.ts b/plugins/proxmox/tests/backend/proxmox-stats-polling.test.ts deleted file mode 100644 index fdbff72f7..000000000 --- a/plugins/proxmox/tests/backend/proxmox-stats-polling.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; - -vi.mock("../../src/backend/plugin-ctx.js", () => ({ - pluginCtx: () => ({ - log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, - }), -})); - -const collectProxmoxStats = vi.fn(); -vi.mock("../../src/backend/proxmox/collect-proxmox-stats.js", () => ({ - collectProxmoxStats: (...args: unknown[]) => collectProxmoxStats(...args), -})); - -import { - ProxmoxPollingManager, - parseProxmoxStatsConfig, -} from "../../src/backend/proxmox-stats-polling.js"; -import type { Client } from "ssh2"; - -interface TestHost { - id: number; - userId: string; - proxmoxStatsConfig?: string | null; -} - -function snapshot(overrides: Partial> = {}) { - return { - node: { - cpu: { percent: 10, cores: 4, load: [0.1, 0.2, 0.3] }, - memory: { percent: 20, usedGiB: 2, totalGiB: 8 }, - disk: { percent: 30, usedGiB: 30, totalGiB: 100 }, - uptime: { seconds: 100, formatted: "0d 0h 1m" }, - system: { hostname: "pve1", kernel: "6.8", pveVersion: "8.2" }, - }, - network: { interfaces: [{ name: "eth0", rxBytes: "10", txBytes: "20" }] }, - guests: { guests: [], counts: { running: 0, stopped: 0, total: 0 } }, - storage: { pools: [] }, - cluster: { clustered: false }, - lastChecked: new Date().toISOString(), - ...overrides, - }; -} - -beforeEach(() => { - vi.useFakeTimers(); - collectProxmoxStats.mockReset(); -}); - -afterEach(() => { - vi.useRealTimers(); -}); - -describe("parseProxmoxStatsConfig", () => { - it("returns default poll interval when config is missing", () => { - expect(parseProxmoxStatsConfig(null)).toEqual({ - nodeName: null, - pollInterval: 60, - }); - }); - - it("parses a JSON string config", () => { - expect( - parseProxmoxStatsConfig('{"nodeName":"pve1","pollInterval":30}'), - ).toEqual({ nodeName: "pve1", pollInterval: 30 }); - }); - - it("falls back to defaults on malformed JSON", () => { - expect(parseProxmoxStatsConfig("{not json")).toEqual({ - nodeName: null, - pollInterval: 60, - }); - }); -}); - -describe("ProxmoxPollingManager", () => { - function makeManager(host: TestHost) { - const fetchHostById = vi.fn(async () => host); - const withSshConnection = vi.fn( - async (_host: TestHost, fn: (client: Client) => Promise) => - fn({} as Client), - ); - const historyRepository = { - create: vi.fn(async () => {}), - pruneOlderThan: vi.fn(async () => {}), - listRange: vi.fn(async () => []), - }; - const manager = new ProxmoxPollingManager({ - fetchHostById, - withSshConnection: withSshConnection as never, - historyRepository, - }); - return { manager, fetchHostById, withSshConnection, historyRepository }; - } - - it("starts polling and caches a snapshot when the first viewer registers", async () => { - const host: TestHost = { id: 1, userId: "user-1" }; - collectProxmoxStats.mockResolvedValue(snapshot()); - const { manager, withSshConnection } = makeManager(host); - - manager.registerViewer(1, "viewer-1", "user-1"); - // registerViewer kicks off polling via a fire-and-forget promise chain. - await vi.waitFor(() => { - expect(withSshConnection).toHaveBeenCalled(); - }); - - const cached = manager.getStats(1); - expect(cached?.data.node.cpu.percent).toBe(10); - manager.destroy(); - }); - - it("stops polling once the last viewer unregisters", async () => { - const host: TestHost = { id: 2, userId: "user-1" }; - collectProxmoxStats.mockResolvedValue(snapshot()); - const { manager } = makeManager(host); - - manager.registerViewer(2, "viewer-a", "user-1"); - manager.registerViewer(2, "viewer-b", "user-1"); - await vi.waitFor(() => expect(manager.getStats(2)).toBeDefined()); - - manager.unregisterViewer(2, "viewer-a"); - // one viewer left - stats stay cached - expect(manager.getStats(2)).toBeDefined(); - - manager.unregisterViewer(2, "viewer-b"); - // Cached snapshot is retained (not cleared) but the interval is stopped; - // registering a new viewer must restart polling. - manager.destroy(); - }); - - it("updateHeartbeat returns false for an unknown session", () => { - const { manager } = makeManager({ id: 3, userId: "user-1" }); - expect(manager.updateHeartbeat("nope")).toBe(false); - manager.destroy(); - }); - - it("records an error snapshot when collection fails, without throwing", async () => { - const host: TestHost = { id: 4, userId: "user-1" }; - collectProxmoxStats.mockRejectedValue(new Error("pvesh not found")); - const { manager } = makeManager(host); - - manager.registerViewer(4, "viewer-1", "user-1"); - await vi.waitFor(() => { - expect(manager.getError(4)?.error).toBe("pvesh not found"); - }); - expect(manager.getStats(4)).toBeUndefined(); - manager.destroy(); - }); -}); diff --git a/plugins/proxmox/tests/backend/proxmox-sudo.test.ts b/plugins/proxmox/tests/backend/proxmox-sudo.test.ts deleted file mode 100644 index 1c49c02a9..000000000 --- a/plugins/proxmox/tests/backend/proxmox-sudo.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { Client } from "ssh2"; - -const execElevated = vi.fn(); -vi.mock("@termix/plugin-sdk/host-commands", async (importOriginal) => ({ - ...(await importOriginal()), - execElevated: (...args: unknown[]) => execElevated(...args), -})); - -const { execPveshCommand } = await import("../../src/backend/routes.js"); - -const client = {} as Client; - -describe("Proxmox discovery sudo execution", () => { - beforeEach(() => { - execElevated.mockReset(); - }); - - it("forces pvesh through the stored sudo credentials", async () => { - execElevated.mockResolvedValue({ - stdout: '[{"type":"qemu"}]', - stderr: "", - code: 0, - usedSudo: true, - }); - - await expect( - execPveshCommand(client, "pvesh get /cluster/resources", "secret", 12000), - ).resolves.toBe('[{"type":"qemu"}]'); - expect(execElevated).toHaveBeenCalledWith( - client, - "pvesh get /cluster/resources", - "secret", - { forceSudo: true, timeoutMs: 12000 }, - ); - }); - - it("surfaces a failed elevated pvesh command", async () => { - execElevated.mockResolvedValue({ - stdout: "", - stderr: "pvesh failed", - code: 255, - usedSudo: true, - }); - - await expect( - execPveshCommand(client, "pvesh get /cluster/resources", "secret"), - ).rejects.toThrow("pvesh failed"); - }); -}); diff --git a/plugins/proxmox/tests/backend/proxmox/cluster-health-collector.test.ts b/plugins/proxmox/tests/backend/proxmox/cluster-health-collector.test.ts deleted file mode 100644 index 666b0aee9..000000000 --- a/plugins/proxmox/tests/backend/proxmox/cluster-health-collector.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -const execCommand = vi.fn(); -vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({ - execCommand: (...args: unknown[]) => execCommand(...args), -})); - -import { collectProxmoxClusterHealth } from "../../../src/backend/proxmox/cluster-health-collector.js"; -import type { Client } from "ssh2"; - -const fakeClient = {} as Client; - -function result(stdout: string, code: number | null = 0) { - return { stdout, stderr: "", code }; -} - -beforeEach(() => { - execCommand.mockReset(); -}); - -describe("collectProxmoxClusterHealth", () => { - it("reports clustered:false for a standalone node (no cluster entry)", async () => { - execCommand.mockResolvedValueOnce(result(JSON.stringify([]), 0)); - const res = await collectProxmoxClusterHealth(fakeClient); - expect(res).toEqual({ clustered: false }); - }); - - it("parses a clustered response with quorum and node entries", async () => { - execCommand.mockResolvedValueOnce( - result( - JSON.stringify([ - { type: "cluster", name: "prod-cluster", quorate: 1, nodes: 3 }, - { type: "node", name: "pve1", online: 1, local: 1, ip: "10.0.0.1" }, - { type: "node", name: "pve2", online: 0, local: 0, ip: "10.0.0.2" }, - ]), - ), - ); - - const res = await collectProxmoxClusterHealth(fakeClient); - expect(res.clustered).toBe(true); - if (res.clustered) { - expect(res.quorate).toBe(true); - expect(res.clusterName).toBe("prod-cluster"); - expect(res.nodes).toHaveLength(2); - expect(res.nodes[0]).toEqual({ - name: "pve1", - online: true, - local: true, - ip: "10.0.0.1", - }); - expect(res.nodes[1].online).toBe(false); - } - }); - - it("returns clustered:false when pvesh fails", async () => { - execCommand.mockResolvedValueOnce(result("", 1)); - const res = await collectProxmoxClusterHealth(fakeClient); - expect(res).toEqual({ clustered: false }); - }); - - it("returns clustered:false on malformed JSON", async () => { - execCommand.mockResolvedValueOnce(result("not json", 0)); - const res = await collectProxmoxClusterHealth(fakeClient); - expect(res).toEqual({ clustered: false }); - }); -}); diff --git a/plugins/proxmox/tests/backend/proxmox/collect-proxmox-stats.test.ts b/plugins/proxmox/tests/backend/proxmox/collect-proxmox-stats.test.ts deleted file mode 100644 index 80dd48ff7..000000000 --- a/plugins/proxmox/tests/backend/proxmox/collect-proxmox-stats.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -const execCommand = vi.fn(); -vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({ - execCommand: (...args: unknown[]) => execCommand(...args), - toFixedNum: (n: number | null | undefined, digits = 2) => { - if (typeof n !== "number" || !Number.isFinite(n)) return null; - return Number(n.toFixed(digits)); - }, -})); - -import { collectProxmoxStats } from "../../../src/backend/proxmox/collect-proxmox-stats.js"; -import type { Client } from "ssh2"; - -const fakeClient = {} as Client; - -function result(stdout: string, code: number | null = 0) { - return { stdout, stderr: "", code }; -} - -beforeEach(() => { - execCommand.mockReset(); -}); - -describe("collectProxmoxStats", () => { - it("throws a distinguishable error when pvesh is missing", async () => { - execCommand.mockResolvedValueOnce(result("missing")); - - await expect(collectProxmoxStats(fakeClient, null)).rejects.toThrow( - /pvesh not found/i, - ); - // Only the pvesh-presence check should have run - no node resolution or - // per-collector execs once that check fails. - expect(execCommand).toHaveBeenCalledTimes(1); - }); - - it("auto-detects the node name via hostname when none is configured", async () => { - execCommand.mockResolvedValueOnce(result("ok")); // pvesh check - execCommand.mockResolvedValueOnce(result("pve-auto\n")); // hostname - // Five collectors run concurrently after that; give each a benign failing - // response so the aggregator still resolves with null-filled sub-shapes. - execCommand.mockResolvedValue(result("", 1)); - - const snapshot = await collectProxmoxStats(fakeClient, null); - expect(snapshot.lastChecked).toBeTruthy(); - expect(snapshot.node).toBeDefined(); - expect(snapshot.guests).toBeDefined(); - expect(snapshot.storage).toBeDefined(); - expect(snapshot.cluster).toEqual({ clustered: false }); - }); - - it("uses the configured node name when it is safe, skipping hostname detection", async () => { - execCommand.mockResolvedValueOnce(result("ok")); // pvesh check - execCommand.mockResolvedValue(result("", 1)); // all collector calls fail benignly - - await collectProxmoxStats(fakeClient, "my-node"); - - // hostname auto-detection command should never have been issued. - const calls = execCommand.mock.calls.map((c) => c[1] as string); - expect(calls).not.toContain("hostname"); - }); - - it("rejects an unsafe configured node name", async () => { - execCommand.mockResolvedValueOnce(result("ok")); // pvesh check - - await expect(collectProxmoxStats(fakeClient, "bad;node")).rejects.toThrow( - /valid Proxmox node name/i, - ); - }); -}); diff --git a/plugins/proxmox/tests/backend/proxmox/guests-collector.test.ts b/plugins/proxmox/tests/backend/proxmox/guests-collector.test.ts deleted file mode 100644 index f2cb5bab3..000000000 --- a/plugins/proxmox/tests/backend/proxmox/guests-collector.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -const execCommand = vi.fn(); -vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({ - execCommand: (...args: unknown[]) => execCommand(...args), - toFixedNum: (n: number | null | undefined, digits = 2) => { - if (typeof n !== "number" || !Number.isFinite(n)) return null; - return Number(n.toFixed(digits)); - }, -})); - -import { collectProxmoxGuestsSummary } from "../../../src/backend/proxmox/guests-collector.js"; -import type { Client } from "ssh2"; - -const fakeClient = {} as Client; - -function result(stdout: string, code: number | null = 0) { - return { stdout, stderr: "", code }; -} - -beforeEach(() => { - execCommand.mockReset(); -}); - -describe("collectProxmoxGuestsSummary", () => { - it("filters to the target node, excludes templates, and computes percentages", async () => { - execCommand.mockResolvedValueOnce( - result( - JSON.stringify([ - { - type: "qemu", - node: "pve1", - vmid: 100, - name: "vm-a", - status: "running", - cpu: 0.25, - mem: 2 * 1024 ** 3, - maxmem: 4 * 1024 ** 3, - disk: 10 * 1024 ** 3, - maxdisk: 40 * 1024 ** 3, - uptime: 3600, - }, - { - type: "lxc", - node: "pve1", - vmid: 101, - name: "ct-b", - status: "stopped", - cpu: 0, - mem: 0, - maxmem: 512 * 1024 ** 2, - disk: 0, - maxdisk: 8 * 1024 ** 3, - uptime: 0, - }, - { - // different node - excluded - type: "qemu", - node: "pve2", - vmid: 200, - name: "elsewhere", - status: "running", - }, - { - // template - excluded - type: "qemu", - node: "pve1", - vmid: 999, - name: "template", - status: "stopped", - template: 1, - }, - { - // non-guest resource type - excluded - type: "storage", - node: "pve1", - }, - ]), - ), - ); - - const res = await collectProxmoxGuestsSummary(fakeClient, "pve1"); - - expect(res.guests).toHaveLength(2); - expect(res.counts).toEqual({ running: 1, stopped: 1, total: 2 }); - - const vmA = res.guests.find((g) => g.vmid === 100)!; - expect(vmA.cpuPercent).toBe(25); - expect(vmA.memPercent).toBe(50); - expect(vmA.diskPercent).toBe(25); - }); - - it("reports null disk fields (not a false 0%) when maxdisk is 0", async () => { - execCommand.mockResolvedValueOnce( - result( - JSON.stringify([ - { - type: "qemu", - node: "pve1", - vmid: 100, - name: "vm-no-agent", - status: "running", - cpu: 0.1, - mem: 1024, - maxmem: 2048, - disk: 0, - maxdisk: 0, - uptime: 10, - }, - ]), - ), - ); - - const res = await collectProxmoxGuestsSummary(fakeClient, "pve1"); - expect(res.guests[0].diskPercent).toBeNull(); - expect(res.guests[0].diskUsedGiB).toBeNull(); - expect(res.guests[0].diskTotalGiB).toBeNull(); - }); - - it("returns an empty guest list when there are no matching resources", async () => { - execCommand.mockResolvedValueOnce(result(JSON.stringify([]), 0)); - const res = await collectProxmoxGuestsSummary(fakeClient, "pve1"); - expect(res.guests).toEqual([]); - expect(res.counts).toEqual({ running: 0, stopped: 0, total: 0 }); - }); - - it("returns an empty result when pvesh is missing (non-zero exit)", async () => { - execCommand.mockResolvedValueOnce(result("", 127)); - const res = await collectProxmoxGuestsSummary(fakeClient, "pve1"); - expect(res.guests).toEqual([]); - }); - - it("returns an empty result on malformed JSON", async () => { - execCommand.mockResolvedValueOnce(result("{not json", 0)); - const res = await collectProxmoxGuestsSummary(fakeClient, "pve1"); - expect(res.guests).toEqual([]); - }); - - it("rejects an unsafe node name before running any command", async () => { - const res = await collectProxmoxGuestsSummary(fakeClient, "../etc"); - expect(res.guests).toEqual([]); - expect(execCommand).not.toHaveBeenCalled(); - }); -}); diff --git a/plugins/proxmox/tests/backend/proxmox/node-network-collector.test.ts b/plugins/proxmox/tests/backend/proxmox/node-network-collector.test.ts deleted file mode 100644 index 8399fa4a6..000000000 --- a/plugins/proxmox/tests/backend/proxmox/node-network-collector.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -const execCommand = vi.fn(); -vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({ - execCommand: (...args: unknown[]) => execCommand(...args), -})); - -import { collectProxmoxNodeNetwork } from "../../../src/backend/proxmox/node-network-collector.js"; -import type { Client } from "ssh2"; - -const fakeClient = {} as Client; - -function result(stdout: string, code: number | null = 0) { - return { stdout, stderr: "", code }; -} - -beforeEach(() => { - execCommand.mockReset(); -}); - -describe("collectProxmoxNodeNetwork", () => { - it("rejects an unsafe node name before running any command", async () => { - const res = await collectProxmoxNodeNetwork(fakeClient, "bad;name"); - expect(res.interfaces).toEqual([]); - expect(execCommand).not.toHaveBeenCalled(); - }); - - it("falls back to /proc/net/dev when pvesh netstat fails", async () => { - // First call: pvesh netstat -> failure. - execCommand.mockResolvedValueOnce(result("", 1)); - // Fallback calls: ip addr, ip link, /proc/net/dev. - execCommand.mockResolvedValueOnce(result("eth0 10.0.0.5/24\n")); - execCommand.mockResolvedValueOnce(result("eth0 UP\n")); - execCommand.mockResolvedValueOnce( - result( - "Inter-| Receive\n" + - " face |bytes packets\n" + - "eth0: 123456 10 0 0 0 0 0 0 654321 20 0 0 0 0 0 0\n", - ), - ); - - const res = await collectProxmoxNodeNetwork(fakeClient, "pve1"); - expect(res.interfaces).toHaveLength(1); - expect(res.interfaces[0]).toMatchObject({ - name: "eth0", - ip: "10.0.0.5", - state: "UP", - rxBytes: "123456", - txBytes: "654321", - }); - }); - - it("falls back to /proc/net/dev when pvesh returns unparseable data", async () => { - execCommand.mockResolvedValueOnce(result("not json", 0)); - execCommand.mockResolvedValueOnce(result("")); - execCommand.mockResolvedValueOnce(result("")); - execCommand.mockResolvedValueOnce(result("Inter-| Receive\n face |\n")); - - const res = await collectProxmoxNodeNetwork(fakeClient, "pve1"); - expect(res.interfaces).toEqual([]); - }); -}); diff --git a/plugins/proxmox/tests/backend/proxmox/node-status-collector.test.ts b/plugins/proxmox/tests/backend/proxmox/node-status-collector.test.ts deleted file mode 100644 index cd6a52638..000000000 --- a/plugins/proxmox/tests/backend/proxmox/node-status-collector.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -const execCommand = vi.fn(); -vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({ - execCommand: (...args: unknown[]) => execCommand(...args), - toFixedNum: (n: number | null | undefined, digits = 2) => { - if (typeof n !== "number" || !Number.isFinite(n)) return null; - return Number(n.toFixed(digits)); - }, -})); - -import { collectProxmoxNodeStatus } from "../../../src/backend/proxmox/node-status-collector.js"; -import type { Client } from "ssh2"; - -const fakeClient = {} as Client; - -function result(stdout: string, code: number | null = 0) { - return { stdout, stderr: "", code }; -} - -beforeEach(() => { - execCommand.mockReset(); -}); - -describe("collectProxmoxNodeStatus", () => { - it("parses a healthy pvesh /nodes/{node}/status response", async () => { - execCommand.mockResolvedValueOnce( - result( - JSON.stringify({ - cpu: 0.15, - cpuinfo: { cores: 8 }, - loadavg: ["0.5", "0.6", "0.7"], - memory: { used: 4 * 1024 ** 3, total: 16 * 1024 ** 3 }, - rootfs: { used: 20 * 1024 ** 3, total: 100 * 1024 ** 3 }, - uptime: 90061, - hostname: "pve1", - kversion: "Linux 6.8.0", - pveversion: "pve-manager/8.2.0", - }), - ), - ); - - const res = await collectProxmoxNodeStatus(fakeClient, "pve1"); - - expect(res.cpu.percent).toBe(15); - expect(res.cpu.cores).toBe(8); - expect(res.cpu.load).toEqual([0.5, 0.6, 0.7]); - expect(res.memory.percent).toBe(25); - expect(res.memory.usedGiB).toBeCloseTo(4, 1); - expect(res.memory.totalGiB).toBeCloseTo(16, 1); - expect(res.disk.percent).toBe(20); - expect(res.uptime.seconds).toBe(90061); - expect(res.uptime.formatted).toBe("1d 1h 1m"); - expect(res.system.hostname).toBe("pve1"); - expect(res.system.kernel).toBe("Linux 6.8.0"); - expect(res.system.pveVersion).toBe("pve-manager/8.2.0"); - }); - - it("returns a fully null-filled shape when pvesh exits non-zero", async () => { - execCommand.mockResolvedValueOnce(result("", 1)); - const res = await collectProxmoxNodeStatus(fakeClient, "pve1"); - expect(res.cpu.percent).toBeNull(); - expect(res.memory.percent).toBeNull(); - expect(res.disk.percent).toBeNull(); - expect(res.uptime.seconds).toBeNull(); - expect(res.system.hostname).toBeNull(); - }); - - it("returns null-filled shape on malformed JSON", async () => { - execCommand.mockResolvedValueOnce(result("not json", 0)); - const res = await collectProxmoxNodeStatus(fakeClient, "pve1"); - expect(res.cpu.percent).toBeNull(); - }); - - it("rejects an unsafe node name before running any command", async () => { - const res = await collectProxmoxNodeStatus(fakeClient, "pve1; rm -rf /"); - expect(res.cpu.percent).toBeNull(); - expect(execCommand).not.toHaveBeenCalled(); - }); -}); diff --git a/plugins/proxmox/tests/backend/proxmox/storage-collector.test.ts b/plugins/proxmox/tests/backend/proxmox/storage-collector.test.ts deleted file mode 100644 index d9aa02d9f..000000000 --- a/plugins/proxmox/tests/backend/proxmox/storage-collector.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -const execCommand = vi.fn(); -vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({ - execCommand: (...args: unknown[]) => execCommand(...args), - toFixedNum: (n: number | null | undefined, digits = 2) => { - if (typeof n !== "number" || !Number.isFinite(n)) return null; - return Number(n.toFixed(digits)); - }, -})); - -import { collectProxmoxStorage } from "../../../src/backend/proxmox/storage-collector.js"; -import type { Client } from "ssh2"; - -const fakeClient = {} as Client; - -function result(stdout: string, code: number | null = 0) { - return { stdout, stderr: "", code }; -} - -beforeEach(() => { - execCommand.mockReset(); -}); - -describe("collectProxmoxStorage", () => { - it("parses storage pools with usage percentages", async () => { - execCommand.mockResolvedValueOnce( - result( - JSON.stringify([ - { - storage: "local", - type: "dir", - active: 1, - enabled: 1, - used: 20 * 1024 ** 3, - total: 100 * 1024 ** 3, - avail: 80 * 1024 ** 3, - }, - { - storage: "local-zfs", - type: "zfspool", - active: 0, - enabled: 1, - used: 0, - total: 0, - avail: 0, - }, - ]), - ), - ); - - const res = await collectProxmoxStorage(fakeClient, "pve1"); - expect(res.pools).toHaveLength(2); - expect(res.pools[0].name).toBe("local"); - expect(res.pools[0].active).toBe(true); - expect(res.pools[0].percent).toBe(20); - expect(res.pools[1].active).toBe(false); - expect(res.pools[1].percent).toBeNull(); - }); - - it("returns an empty pool list when pvesh fails", async () => { - execCommand.mockResolvedValueOnce(result("", 1)); - const res = await collectProxmoxStorage(fakeClient, "pve1"); - expect(res.pools).toEqual([]); - }); - - it("returns an empty pool list on malformed JSON", async () => { - execCommand.mockResolvedValueOnce(result("nope", 0)); - const res = await collectProxmoxStorage(fakeClient, "pve1"); - expect(res.pools).toEqual([]); - }); - - it("rejects an unsafe node name before running any command", async () => { - const res = await collectProxmoxStorage(fakeClient, "$(whoami)"); - expect(res.pools).toEqual([]); - expect(execCommand).not.toHaveBeenCalled(); - }); -}); diff --git a/plugins/proxmox/tests/backend/repositories/proxmox-node-history-repository.test.ts b/plugins/proxmox/tests/backend/repositories/proxmox-node-history-repository.test.ts deleted file mode 100644 index e65949912..000000000 --- a/plugins/proxmox/tests/backend/repositories/proxmox-node-history-repository.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing"; -import { proxmoxNodeHistory } from "../../../src/backend/tables.js"; -import { - createProxmoxNodeHistoryRepository, - type ProxmoxNodeHistoryRepository, -} from "../../../src/backend/proxmox-node-history-repository.js"; -import { pluginDir } from "../helpers.js"; - -describe("ProxmoxNodeHistoryRepository", () => { - let db: TestDb | null = null; - - afterEach(() => { - db?.close(); - db = null; - }); - - async function createRepository(): Promise { - db = await createTestDb(pluginDir, { - before: (sqlite) => { - sqlite.exec( - "INSERT INTO users (id, username) VALUES ('user-1', 'user-1'), ('user-2', 'user-2')", - ); - sqlite.exec("INSERT INTO ssh_data (id) VALUES (1), (2)"); - }, - skipMigrations: false, - }); - // Migrations already ran (adopting proxmox_node_history), so seed rows on - // the real table after the fact. - db.sqlite.exec(` - INSERT INTO p_proxmox_node_history ( - host_id, ts, cpu_percent, mem_percent, disk_percent, net_rx_bytes, net_tx_bytes - ) - VALUES - (1, '2026-01-01 00:00:00', 10, 20, 30, 100, 200), - (1, '2026-01-02 00:00:00', 11, 21, 31, 101, 201), - (1, '2999-01-01 00:00:00', 12, 22, 32, 102, 202), - (2, '2026-01-02 00:00:00', 99, 99, 99, 999, 999); - `); - - return createProxmoxNodeHistoryRepository( - db.database, - await db.database.define(proxmoxNodeHistory), - ); - } - - it("creates and lists node history rows by range", async () => { - const repo = await createRepository(); - - await repo.create({ - hostId: 1, - cpuPercent: 12, - memPercent: 22, - diskPercent: 32, - netRxBytes: 102, - netTxBytes: 202, - }); - - const rows = await repo.listRange( - 1, - "2026-01-01 00:00:00", - "2026-01-02 23:59:59", - ); - - expect(rows.map((row) => row.cpuPercent)).toEqual([10, 11]); - expect(db!.persisted).toBe(1); - }); - - it("prunes old history for a host only", async () => { - const repo = await createRepository(); - - await repo.pruneOlderThan(1, 1); - - const rows = await repo.listRange( - 1, - "2000-01-01 00:00:00", - "2999-12-31 23:59:59", - ); - expect(rows.map((row) => row.ts)).toEqual(["2999-01-01 00:00:00"]); - expect( - await repo.listRange(2, "2026-01-01 00:00:00", "2026-01-03 00:00:00"), - ).toHaveLength(1); - }); -}); diff --git a/plugins/proxmox/tests/backend/routes.test.ts b/plugins/proxmox/tests/backend/routes.test.ts deleted file mode 100644 index c959e4f1d..000000000 --- a/plugins/proxmox/tests/backend/routes.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { guestPluginSettings } from "../../src/backend/routes.js"; - -describe("guestPluginSettings", () => { - it("names the plugin that keeps each switch an imported guest needs", () => { - expect(guestPluginSettings("rdp", true)).toEqual({ - "remote-desktop": { enableRdp: true, rdpPort: 3389 }, - docker: { enableDocker: true }, - }); - expect(guestPluginSettings("ssh", false)).toEqual({}); - }); -}); diff --git a/plugins/proxmox/tests/backend/sudo-capability.test.ts b/plugins/proxmox/tests/backend/sudo-capability.test.ts deleted file mode 100644 index ab3f3d7a1..000000000 --- a/plugins/proxmox/tests/backend/sudo-capability.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, expect, it } from "vitest"; -import fs from "node:fs"; -import path from "node:path"; - -// Elevated commands read the host's sudo password, which core only hands to -// a plugin holding credentials:read. -describe("sudo password access", () => { - it("declares credentials:read", () => { - const manifest = JSON.parse( - fs.readFileSync( - path.resolve(import.meta.dirname, "../../manifest.json"), - "utf8", - ), - ); - expect(manifest.capabilities).toContain("credentials:read"); - }); -}); diff --git a/plugins/proxmox/tests/frontend/activate.test.tsx b/plugins/proxmox/tests/frontend/activate.test.tsx deleted file mode 100644 index 9c693739d..000000000 --- a/plugins/proxmox/tests/frontend/activate.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { - renderWithApp, - type RenderedPluginApp, -} from "@termix/plugin-sdk/testing"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import * as plugin from "../../src/frontend/index"; -import manifestJson from "../../manifest.json"; -import locales from "../../locales/en.json"; - -const manifest = manifestJson as unknown as PluginManifest; - -const declared = (views?: Array<{ id: string }>) => - (views ?? []).map((view) => view.id).sort(); - -let rendered: RenderedPluginApp | null = null; - -afterEach(async () => { - await rendered?.deactivate(); - rendered = null; -}); - -describe(`${manifest.id} activate`, () => { - it("activates and registers only views its manifest declares", async () => { - rendered = await renderWithApp(plugin, { manifest, locales }); - const contributes = manifest.contributes as unknown as Record< - string, - Array<{ id: string }> | undefined - >; - const tabs = declared(contributes.tabs); - // A rail panel may reuse one of the plugin's tab ids. - const panels = [...declared(contributes.panels), ...tabs]; - const cards = declared(contributes.dashboardCards); - for (const id of rendered.registered.tabs()) expect(tabs).toContain(id); - for (const id of rendered.registered.panels()) expect(panels).toContain(id); - for (const id of rendered.registered.dashboardCards()) { - expect(cards).toContain(id); - } - }); - - it("reads its switches from its own host settings", async () => { - rendered = await renderWithApp(plugin, { manifest, locales }); - const host = (settings: Record) => ({ - id: "1", - name: "pve", - ip: "10.0.0.1", - port: 22, - pluginSettings: { proxmox: settings }, - }); - const ids = (settings: Record) => - rendered!.registered.hostActionsFor(host(settings)).map((a) => a.id); - expect(ids({ enableProxmox: true })).toContain("proxmox-discover"); - expect(ids({})).not.toContain("proxmox-discover"); - expect(ids({ enableProxmoxStats: true })).toContain("proxmox-stats"); - expect(plugin.proxmoxSettings(host({ enableProxmoxStats: true }))).toEqual({ - enableProxmoxStats: true, - }); - }); - - it("removes everything it registered on deactivate", async () => { - const app = await renderWithApp(plugin, { manifest, locales }); - await app.deactivate(); - expect(app.registered.tabs()).toEqual([]); - expect(app.registered.panels()).toEqual([]); - expect(app.registered.railItems()).toEqual([]); - expect(app.registered.hostActions()).toEqual([]); - expect(app.registered.hostEditorSections()).toEqual([]); - expect(app.registered.dashboardCards()).toEqual([]); - expect(app.registered.settingsComponents()).toEqual([]); - expect(app.registered.actions()).toEqual([]); - }); -}); diff --git a/plugins/proxmox/tests/frontend/proxmox-import-auth.test.ts b/plugins/proxmox/tests/frontend/proxmox-import-auth.test.ts deleted file mode 100644 index 3120cc95b..000000000 --- a/plugins/proxmox/tests/frontend/proxmox-import-auth.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { resolveProxmoxImportAuth } from "../../src/frontend/proxmox-import-auth"; - -describe("resolveProxmoxImportAuth", () => { - it("inherits an available credential even with the default 'password' authType", () => { - // Regression guard: a Proxmox host authenticating via a Termix credential - // used to yield authType 'none' for imported guests because the default - // authType is 'password', not 'credential'. - expect(resolveProxmoxImportAuth("password", 42)).toEqual({ - authType: "credential", - credentialId: 42, - overrideCredentialUsername: false, - }); - }); - - it("uses the credential when authType is explicitly 'credential'", () => { - expect(resolveProxmoxImportAuth("credential", 7)).toEqual({ - authType: "credential", - credentialId: 7, - overrideCredentialUsername: false, - }); - }); - - it("uses the credential when no authType is given", () => { - expect(resolveProxmoxImportAuth(undefined, 5)).toEqual({ - authType: "credential", - credentialId: 5, - overrideCredentialUsername: false, - }); - }); - - it("lets an explicit secretless authType win over an available credential", () => { - expect(resolveProxmoxImportAuth("none", 42)).toEqual({ authType: "none" }); - expect(resolveProxmoxImportAuth("tailscale", 42)).toEqual({ - authType: "tailscale", - }); - expect(resolveProxmoxImportAuth("agent", 42)).toEqual({ - authType: "agent", - }); - }); - - it("falls back to 'none' when no credential is available", () => { - expect(resolveProxmoxImportAuth("password", null)).toEqual({ - authType: "none", - }); - expect(resolveProxmoxImportAuth("key", undefined)).toEqual({ - authType: "none", - }); - expect(resolveProxmoxImportAuth(undefined, undefined)).toEqual({ - authType: "none", - }); - }); - - it("passes through a non secret-backed special type without a credential", () => { - expect(resolveProxmoxImportAuth("opkssh", null)).toEqual({ - authType: "opkssh", - }); - }); -}); diff --git a/plugins/proxmox/tests/frontend/stats/ProxmoxStatsTab.test.tsx b/plugins/proxmox/tests/frontend/stats/ProxmoxStatsTab.test.tsx deleted file mode 100644 index fc4cfdf54..000000000 --- a/plugins/proxmox/tests/frontend/stats/ProxmoxStatsTab.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom/vitest"; -import { afterEach, describe, expect, it, vi } from "vitest"; - -vi.mock("@termix/plugin-sdk/frontend", async (importOriginal) => ({ - ...(await importOriginal>()), - useTranslation: () => ({ t: (key: string) => key, language: "en" }), - usePluginApi: () => ({}), - useHosts: () => ({ hosts: [] }), -})); - -vi.mock("../../../src/frontend/stats/proxmox-stats-api", () => ({ - createProxmoxStatsApi: () => ({ - startPolling: () => new Promise(() => {}), - stopPolling: () => Promise.resolve(), - getSnapshot: () => new Promise(() => {}), - sendHeartbeat: () => Promise.resolve(), - }), -})); - -import { ProxmoxStatsTab } from "../../../src/frontend/stats/ProxmoxStatsTab"; - -const host = (proxmox: Record) => ({ - id: 1, - name: "pve", - ip: "10.0.0.1", - username: "root", - pluginSettings: { proxmox }, -}); - -afterEach(cleanup); - -describe("ProxmoxStatsTab", () => { - it("reads the stats switch from the plugin host settings", () => { - render(); - expect( - screen.queryByText("proxmoxStats.noHostSelected"), - ).not.toBeInTheDocument(); - }); - - it("shows the empty state when stats are off for the host", () => { - render(); - expect(screen.getByText("proxmoxStats.noHostSelected")).toBeInTheDocument(); - }); -}); diff --git a/plugins/proxmox/tsconfig.json b/plugins/proxmox/tsconfig.json deleted file mode 100644 index 7b35314c3..000000000 --- a/plugins/proxmox/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@termix/plugin-sdk/tsconfig.plugin.json", - "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] -} diff --git a/plugins/proxmox/vitest.config.ts b/plugins/proxmox/vitest.config.ts deleted file mode 100644 index 31ba004fc..000000000 --- a/plugins/proxmox/vitest.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; - -export default pluginVitestConfig(import.meta.url); diff --git a/plugins/remote-desktop/CHANGELOG.md b/plugins/remote-desktop/CHANGELOG.md deleted file mode 100644 index 0b364ff91..000000000 --- a/plugins/remote-desktop/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -## 1.0.0 - -First release, bundled with Termix 2.9.0. - -Adds RDP, VNC and Telnet sessions in the browser through guacd. Supports clipboard, file transfer through RDP drive redirection, session recording and session sharing. diff --git a/plugins/remote-desktop/README.md b/plugins/remote-desktop/README.md deleted file mode 100644 index 148596406..000000000 --- a/plugins/remote-desktop/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Remote Desktop - -RDP, VNC and Telnet sessions in the browser, with clipboard, file transfer, recording and sharing. - -## Features - -- Connect to hosts over RDP, VNC or Telnet from any browser. -- Clipboard sync and file transfer through RDP drive redirection. -- Session recording and session sharing, when those plugins are on. -- Jump host support. -- Opens the Windows Remote Desktop client from the desktop app. - -## Setup - -This plugin needs guacd, the Apache Guacamole proxy that speaks RDP, VNC and Telnet. The Docker compose file runs it as its own `guacamole/guacd` container. This plugin never starts guacd itself. - -Set where guacd runs with the guacd URL admin setting. The `GUACD_URL`, or `GUACD_HOST` and `GUACD_PORT`, environment variables override that setting. - -Other environment variables: - -| Variable | What it sets | -| ------------------------------ | --------------------------------------------------------------------------------- | -| `GUACD_TUNNEL_HOST` | The name guacd uses to reach Termix for a jump host tunnel. Defaults to `termix`. | -| `GUACD_RECORDING_PATH` | Where guacd writes recordings, as guacd sees it. | -| `GUACD_RECORDING_BACKEND_PATH` | The same folder, as Termix sees it. | -| `GUACD_DRIVE_PATH` | The root of each user's RDP drive folder on the guacd host. | -| `GUACAMOLE_ENCRYPTION_KEY` | The connection token key. Made from `JWT_SECRET` when not set. | - -## Settings - -### Admin - -- Enable Remote Desktop: turn remote desktop on or off. -- guacd URL: where guacd runs, as `host:port`. Changes apply without a restart. - -### User - -- RDP defaults: color depth, resize method, wallpaper, font smoothing, audio, printing, drive redirection and clipboard. A host's own values win over these. - -### Host - -- RDP, VNC and Telnet: turn each protocol on and set its port. -- RDP security mode and Ignore certificate errors. -- guacd settings: every other guacd option. -- Remote desktop toolbar: show the toolbar in the session. - -## Permissions - -- `remote-desktop.sessions`: Let session sharing and recording reach your remote desktop sessions. Admins and users have it by default. - -## Services - -Provides to other plugins: - -- `sessions.live` as `rdp`, `vnc` and `telnet`: find a live session so it can be shared. - -Uses from other plugins: - -- `recordings.writer` to record sessions. Optional. -- `tunnels.access` for single-hop jump host tunnels. Optional. - -## Development - -```bash -npm run build # build into dist/ -npm run test # run this plugin's tests -npm run typecheck # type-check this plugin -``` diff --git a/plugins/remote-desktop/locales/en.json b/plugins/remote-desktop/locales/en.json deleted file mode 100644 index 5f3c58597..000000000 --- a/plugins/remote-desktop/locales/en.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Connecting to {{type}} session...", - "checkingGuacd": "Checking remote desktop service...", - "requestingToken": "Requesting {{type}} connection token...", - "openingSession": "Opening {{type}} session...", - "negotiating": "Negotiating {{type}} handshake...", - "sessionReady": "{{type}} session ready", - "connectionError": "Connection error", - "connectionFailed": "Connection failed", - "failedToConnect": "Failed to get connection token", - "hostNotFound": "Host not found", - "noHostSelected": "No host selected", - "reconnect": "Reconnect", - "retry": "Retry", - "guacdUnavailable": "Remote desktop service (guacd) is not available. Please ensure guacd is running and accessible and configured properly in admin settings.", - "credentialPromptTitle": "Enter RDP Credentials", - "credentialPromptDescription": "This host is set to prompt for credentials on connect. They are used for this session only and are not saved.", - "connect": "Connect", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Drive Files", - "parent": "Parent directory", - "refresh": "Refresh", - "empty": "This folder is empty", - "listFailed": "Failed to read this folder", - "download": "Download", - "downloadFailed": "Failed to download {{name}}", - "upload": "Upload files", - "uploading": "Uploading {{name}}...", - "uploaded": "Uploaded {{name}}", - "uploadFailed": "Failed to upload {{name}}", - "uploadDisabled": "Uploads are disabled for this connection", - "driveUnavailable": "Enable RDP drive redirection before uploading files", - "driveNotWritable": "guacd cannot write to the drive folder. Check GUACD_DRIVE_PATH (or the host's Drive Path) and its permissions.", - "dropToUpload": "Drop files to upload" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Lock Screen)", - "winKey": "Windows Key", - "superL": "Super+L (Lock Screen)", - "superKey": "Super Key", - "super": "Super", - "cmdL": "Cmd+L (Lock Screen)", - "cmdKey": "Command Key", - "cmd": "Cmd", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}} (latched - click to release)", - "stickyInactive": "{{key}} (click to latch)", - "esc": "Escape", - "tab": "Tab", - "home": "Home", - "end": "End", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "Arrow Up", - "arrowDown": "Arrow Down", - "arrowLeft": "Arrow Left", - "arrowRight": "Arrow Right", - "fnToggle": "Function Keys", - "reconnect": "Reconnect Session", - "collapse": "Collapse toolbar", - "expand": "Expand toolbar", - "dragHandle": "Drag to reposition", - "hide": "Hide for this session. To disable it permanently, open Host Settings → General → Connection Toolbar.", - "zoomIn": "Zoom in", - "zoomOut": "Zoom out", - "resetZoom": "Reset zoom", - "fitToScreen": "Fit to screen", - "switchToTrackpad": "Switch to trackpad mode (drag to move cursor, tap to click)", - "switchToTouch": "Switch to touch mode (tap directly where you want to click)" - }, - "disabled": "Remote Desktop is turned off. An admin can turn it on in Settings." - }, - "hosts": { - "nativeRdpOpened": "Opened in Windows Remote Desktop", - "nativeRdpFailed": "Failed to open Windows Remote Desktop", - "openNativeRdp": "Open in Windows Remote Desktop", - "guac": { - "toolbar": "Toolbar", - "connection": "Connection", - "authentication": "Authentication", - "storedCredential": "Stored Credential", - "authMethod": "Auth Method", - "authTypeDirect": "Direct", - "authTypeCredential": "Credential", - "authTypeNone": "None", - "authTypeNoneDesc": "No credentials are stored. You'll be prompted for a username and password each time you connect; they are not saved.", - "selectCredential": "Select a credential...", - "connectionSettings": "Connection Settings", - "displaySettings": "Display Settings", - "audioSettings": "Audio Settings", - "rdpPerformance": "RDP Performance", - "deviceRedirection": "Device Redirection", - "session": "Session", - "gateway": "Gateway", - "remoteApp": "RemoteApp", - "clipboard": "Clipboard", - "sessionRecording": "Session Recording", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC Settings", - "terminalSettings": "Terminal Settings", - "rdpPort": "RDP Port", - "username": "Username", - "password": "Password", - "passwordSaved": "Password saved, type to change", - "domain": "Domain", - "securityMode": "Security Mode", - "width": "Width", - "height": "Height", - "dpi": "DPI", - "clientName": "Client Name", - "initialProgram": "Initial Program", - "serverLayout": "Server Layout", - "timezone": "Timezone", - "loadBalanceInfo": "Load Balance Info / Cookie", - "loadBalanceInfoDesc": "RD Connection Broker cookie for RDS farm load balancing (e.g. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd Proxy", - "guacdHostname": "guacd Host", - "guacdHostnamePlaceholder": "Global default", - "guacdPort": "guacd Port", - "guacdProxyDesc": "Override the global guacd instance for this connection. Leave blank to use the globally configured guacd.", - "gatewayHostname": "Gateway Hostname", - "gatewayPort": "Gateway Port", - "gatewayUsername": "Gateway Username", - "gatewayPassword": "Gateway Password", - "gatewayDomain": "Gateway Domain", - "remoteAppProgram": "RemoteApp Program", - "workingDirectory": "Working Directory", - "arguments": "Arguments", - "normalizeLineEndings": "Normalize Line Endings", - "macAddress": "MAC Address", - "broadcastAddress": "Broadcast Address", - "udpPort": "UDP Port", - "waitTimeS": "Wait Time (s)", - "driveName": "Drive Name", - "drivePath": "Drive Path", - "ignoreCertificate": "Ignore Certificate", - "ignoreCertificateDesc": "Allow connections to hosts with self-signed certificates", - "forceLossless": "Force Lossless", - "forceLosslessDesc": "Force lossless image encoding (higher quality, more bandwidth)", - "disableAudio": "Disable Audio", - "disableAudioDesc": "Mute all audio from the remote session", - "enableAudioInput": "Enable Audio Input (Microphone)", - "enableAudioInputDesc": "Forward local microphone to the remote session", - "wallpaper": "Wallpaper", - "wallpaperDesc": "Show desktop wallpaper (disabling improves performance)", - "theming": "Theming", - "themingDesc": "Enable visual themes and styles", - "fontSmoothing": "Font Smoothing", - "fontSmoothingDesc": "Enable ClearType font rendering", - "fullWindowDrag": "Full Window Drag", - "fullWindowDragDesc": "Show window contents while dragging", - "desktopComposition": "Desktop Composition", - "desktopCompositionDesc": "Enable Aero glass effects", - "menuAnimations": "Menu Animations", - "menuAnimationsDesc": "Enable menu fade and slide animations", - "disableBitmapCaching": "Disable Bitmap Caching", - "disableBitmapCachingDesc": "Turn off bitmap cache (may help with glitches)", - "disableOffscreenCaching": "Disable Offscreen Caching", - "disableOffscreenCachingDesc": "Turn off offscreen cache", - "disableGlyphCaching": "Disable Glyph Caching", - "disableGlyphCachingDesc": "Turn off glyph cache", - "enableGfx": "Enable GFX", - "enableGfxDesc": "Use RemoteFX graphics pipeline", - "enablePrinting": "Enable Printing", - "enablePrintingDesc": "Redirect local printers to the remote session", - "enableDriveRedirection": "Enable Drive Redirection", - "enableDriveRedirectionDesc": "Map a local folder as a drive in the remote session", - "createDrivePath": "Create Drive Path", - "createDrivePathDesc": "Automatically create the folder if it does not exist", - "disableDownload": "Disable Download", - "disableDownloadDesc": "Prevent downloading files from the remote session", - "disableUpload": "Disable Upload", - "disableUploadDesc": "Prevent uploading files to the remote session", - "enableTouch": "Enable Touch", - "enableTouchDesc": "Enable touch input forwarding", - "consoleSession": "Console Session", - "consoleSessionDesc": "Connect to the console (session 0) instead of a new session", - "sendWolPacket": "Send WOL Packet", - "sendWolPacketDesc": "Send a magic packet to wake this host before connecting", - "disableCopy": "Disable Copy", - "disableCopyDesc": "Prevent copying text from the remote session", - "disablePaste": "Disable Paste", - "disablePasteDesc": "Prevent pasting text into the remote session", - "createPathIfMissing": "Create Path if Missing", - "createPathIfMissingDesc": "Automatically create the recording directory", - "excludeOutput": "Exclude Output", - "excludeOutputDesc": "Do not record screen output (metadata only)", - "excludeMouse": "Exclude Mouse", - "excludeMouseDesc": "Do not record mouse movements", - "includeKeystrokes": "Include Keystrokes", - "includeKeystrokesDesc": "Record raw keystrokes in addition to screen output", - "vncPort": "VNC Port", - "vncPassword": "VNC Password", - "vncUsernameOptional": "Username (optional)", - "vncLeaveBlank": "Leave blank if not required", - "cursorMode": "Cursor Mode", - "swapRedBlue": "Swap Red/Blue", - "swapRedBlueDesc": "Swap the red and blue color channels (fixes some colour issues)", - "readOnly": "Read-only", - "readOnlyDesc": "View the remote screen without sending any input", - "telnetPort": "Telnet Port", - "terminalType": "Terminal Type", - "fontName": "Font Name", - "fontSize": "Font Size", - "colorScheme": "Color Scheme", - "backspaceKey": "Backspace Key", - "colorDepth": "Color Depth", - "resizeMethod": "Resize Method", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Any", - "auto": "Auto", - "nlaExtended": "NLA Extended", - "color8": "8-bit", - "color16": "16-bit", - "color24": "24-bit", - "color32": "32-bit", - "displayUpdate": "Display Update", - "reconnect": "Reconnect", - "preserve": "Preserve", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Local", - "remote": "Remote", - "failsafe": "failsafe", - "blackOnWhite": "Black on White", - "whiteOnBlack": "White on Black", - "grayOnBlack": "Gray on Black", - "greenOnBlack": "Green on Black" - } - }, - "remoteDesktop": "Remote Desktop", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Virtual Network", - "unencryptedShell": "Unencrypted shell", - "remoteDesktopNotes": "Notes", - "userDefaultsNote": "Settings you leave unchanged follow your host defaults.", - "connectionOriginNote": "On the desktop app, RDP, VNC and Telnet use the remote server unless the host is set to This device, which needs a guacd this machine can reach. Set one in the Remote Desktop settings, or per host under guacd Proxy.", - "docsLink": "View docs", - "connectionOriginGuacamoleNote": "RDP, VNC and Telnet use the remote server unless you pick \"This device\", which needs a guacd this machine can reach. Set one in the guacd URL setting, or per host under guacd Proxy." - }, - "onboarding": { - "feature_desktop": "Remote desktop", - "feature_desktop_desc": "Open RDP, VNC and Telnet sessions in a tab, same as SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Color Depth", - "description": "Used for RDP hosts that leave it on their default." - }, - "resizeMethod": { - "label": "Resize Method", - "description": "How the remote screen follows the window size." - }, - "resizeDisplayUpdate": "Display Update", - "resizeReconnect": "Reconnect", - "inherit": "Inherit", - "on": "On", - "off": "Off", - "forceLossless": { - "label": "Force Lossless", - "description": "Force lossless image encoding (higher quality, more bandwidth)" - }, - "enableWallpaper": { - "label": "Wallpaper", - "description": "Show desktop wallpaper (disabling improves performance)" - }, - "enableFontSmoothing": { - "label": "Font Smoothing", - "description": "Enable ClearType font rendering" - }, - "enableDesktopComposition": { - "label": "Desktop Composition", - "description": "Enable Aero glass effects" - }, - "disableAudio": { - "label": "Disable Audio", - "description": "Mute all audio from the remote session" - }, - "enablePrinting": { - "label": "Enable Printing", - "description": "Redirect local printers to the remote session" - }, - "enableDrive": { - "label": "Enable Drive Redirection", - "description": "Map a local folder as a drive in the remote session" - }, - "disableCopy": { - "label": "Disable Copy", - "description": "Prevent copying text from the remote session" - }, - "disablePaste": { - "label": "Disable Paste", - "description": "Prevent pasting text into the remote session" - }, - "group": "Remote Desktop defaults", - "colorDepth16": "16-bit", - "colorDepth24": "24-bit", - "colorDepth32": "32-bit" - }, - "admin": { - "enabled": { - "label": "Enable Remote Desktop", - "description": "Allow RDP, VNC and Telnet connections through guacd." - }, - "guacdUrl": { - "label": "guacd URL", - "description": "Where guacd listens, as host:port. The GUACD_URL, GUACD_HOST and GUACD_PORT environment variables override this.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP port" - }, - "vncPort": { - "label": "VNC port" - }, - "telnetPort": { - "label": "Telnet port" - }, - "rdpSecurity": { - "label": "RDP security mode" - }, - "rdpIgnoreCert": { - "label": "Ignore certificate errors" - }, - "guacamoleConfig": { - "label": "guacd settings" - }, - "enableToolbar": { - "label": "Remote desktop toolbar", - "description": "Show the toolbar in RDP, VNC and Telnet sessions." - }, - "displayGroup": "Display", - "displayDefaultsTab": "Remote Desktop" - } - }, - "palette": { - "connectRdp": "Connect with RDP", - "connectVnc": "Connect with VNC", - "connectTelnet": "Connect with Telnet" - }, - "permissions": { - "sessions": { - "title": "Share remote desktop sessions", - "description": "Let session sharing and collab rooms show this user's RDP, VNC and Telnet sessions to others." - } - }, - "errors": { - "remoteServerRequired": "Remote server required. Connect a remote server in Settings to use this connection type." - }, - "plugin": { - "name": "Remote Desktop" - } -} diff --git a/plugins/remote-desktop/locales/translated/af_ZA.json b/plugins/remote-desktop/locales/translated/af_ZA.json deleted file mode 100644 index ce3b0406a..000000000 --- a/plugins/remote-desktop/locales/translated/af_ZA.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Verbind met {{type}} sessie...", - "checkingGuacd": "Kontroleer tans afstandwerkskermdiens...", - "requestingToken": "Versoek tans {{type}} verbindingstoken...", - "openingSession": "Maak {{type}} sessie oop...", - "negotiating": "Onderhandel {{type}} handdruk...", - "sessionReady": "{{type}} sessie gereed", - "connectionError": "Verbindingsfout", - "connectionFailed": "Verbinding het misluk", - "failedToConnect": "Kon nie verbindingstoken kry nie", - "hostNotFound": "Gasheer nie gevind nie", - "noHostSelected": "Geen gasheer gekies nie", - "reconnect": "Herkoppel", - "retry": "Probeer weer", - "guacdUnavailable": "Diens vir afstandwerkskerm (guacd) is nie beskikbaar nie. Maak asseblief seker dat guacd loop, toeganklik is en behoorlik in administrateurinstellings gekonfigureer is.", - "credentialPromptTitle": "Voer RDP-aanbewyse in", - "credentialPromptDescription": "Hierdie gasheer is ingestel om vir geloofsbriewe te vra tydens verbinding. Hulle word slegs vir hierdie sessie gebruik en word nie gestoor nie.", - "connect": "Verbind", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Drive-lêers", - "parent": "Ouergids", - "refresh": "Verfris", - "empty": "Hierdie vouer is leeg", - "listFailed": "Kon nie hierdie vouer lees nie", - "download": "Laai af", - "downloadFailed": "Kon nie {{name}} aflaai nie", - "upload": "Laai lêers op", - "uploading": "Laai tans {{name}} op ...", - "uploaded": "Opgelaai {{name}}", - "uploadFailed": "Kon nie {{name}} oplaai nie", - "uploadDisabled": "Oplaaie is gedeaktiveer vir hierdie verbinding", - "driveUnavailable": "Aktiveer RDP-skyfherleiding voordat lêers opgelaai word", - "driveNotWritable": "guacd kan nie na die skyf se lêergids skryf nie. Kontroleer GUACD_DRIVE_PATH (of die gasheer se skyfpad) en sy toestemmings.", - "dropToUpload": "Laat lêers los om op te laai" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Sluitskerm)", - "winKey": "Windows-sleutel", - "superL": "Super+L (Sluitskerm)", - "superKey": "Supersleutel", - "super": "Super", - "cmdL": "Cmd+L (Sluitskerm)", - "cmdKey": "Opdragsleutel", - "cmd": "Cmd", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Skof", - "win": "Wen", - "stickyActive": "{{key}} (vasgeklem - klik om los te maak)", - "stickyInactive": "{{key}} (klik om te sluit)", - "esc": "Ontsnapping", - "tab": "Oortjie", - "home": "Tuis", - "end": "Einde", - "pageUp": "Bladsy op", - "pageDown": "Bladsy af", - "arrowUp": "Pyltjie Op", - "arrowDown": "Pyltjie Af", - "arrowLeft": "Pyltjie links", - "arrowRight": "Pyltjie Regs", - "fnToggle": "Funksiesleutels", - "reconnect": "Herkoppel Sessie", - "collapse": "Vou nutsbalk in", - "expand": "Vou die nutsbalk uit", - "dragHandle": "Sleep om te herposisioneer", - "hide": "Versteek vir hierdie sessie. Om dit permanent te deaktiveer, maak Gasheerinstellings → Algemeen → Verbindingsnutsbalk oop.", - "zoomIn": "Zoem in", - "zoomOut": "Zoem uit", - "resetZoom": "Stel zoom terug", - "fitToScreen": "Pas by skerm", - "switchToTrackpad": "Skakel oor na raakvlakmodus (sleep om wyser te beweeg, tik om te klik)", - "switchToTouch": "Skakel oor na raakmodus (tik direk waar jy wil klik)" - }, - "disabled": "Afstandslessenaar is afgeskakel. 'n Administrateur kan dit in Instellings aanskakel." - }, - "hosts": { - "nativeRdpOpened": "Geopen in Windows Afstandslessenaar", - "nativeRdpFailed": "Kon nie Windows Remote Desktop oopmaak nie", - "openNativeRdp": "Maak oop in Windows Afstandslessenaar", - "guac": { - "toolbar": "Nutsbalk", - "connection": "Verbinding", - "authentication": "Verifikasie", - "storedCredential": "Gestoorde geloofsbriewe", - "authMethod": "Magtigingsmetode", - "authTypeDirect": "Direkte", - "authTypeCredential": "Geloofsbrief", - "authTypeNone": "Geen", - "authTypeNoneDesc": "Geen aanmeldingsbesonderhede word gestoor nie. Jy sal elke keer as jy koppel vir 'n gebruikersnaam en wagwoord gevra word; hulle word nie gestoor nie.", - "selectCredential": "Kies 'n geloofsbrief...", - "connectionSettings": "Verbindingsinstellings", - "displaySettings": "Skerminstellings", - "audioSettings": "Oudio-instellings", - "rdpPerformance": "RDP-prestasie", - "deviceRedirection": "Toestelherleiding", - "session": "Sessie", - "gateway": "Poort", - "remoteApp": "Afstandsprogram", - "clipboard": "Klembord", - "sessionRecording": "Sessie-opname", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC-instellings", - "terminalSettings": "Terminaalinstellings", - "rdpPort": "RDP-poort", - "username": "Gebruikersnaam", - "password": "Wagwoord", - "passwordSaved": "Wagwoord gestoor, tik om te verander", - "domain": "Domein", - "securityMode": "Sekuriteitsmodus", - "width": "Breedte", - "height": "Hoogte", - "dpi": "DPI", - "clientName": "Kliëntnaam", - "initialProgram": "Aanvanklike Program", - "serverLayout": "Bedieneruitleg", - "timezone": "Tydsone", - "loadBalanceInfo": "Laaibalansinligting / Koekie", - "loadBalanceInfoDesc": "RD Connection Broker-koekie vir RDS-plaaslasbalansering (bv. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd Proxy", - "guacdHostname": "guacd-gasheer", - "guacdHostnamePlaceholder": "Globale verstekwaarde", - "guacdPort": "guacd-hawe", - "guacdProxyDesc": "Ignoreer die globale guacd-instansie vir hierdie verbinding. Los leeg om die globaal gekonfigureerde guacd te gebruik.", - "gatewayHostname": "Gateway-gasheernaam", - "gatewayPort": "Gateway-poort", - "gatewayUsername": "Gateway-gebruikersnaam", - "gatewayPassword": "Gateway-wagwoord", - "gatewayDomain": "Gateway-domein", - "remoteAppProgram": "RemoteApp-program", - "workingDirectory": "Werkgids", - "arguments": "Argumente", - "normalizeLineEndings": "Normaliseer Lyn Eindigings", - "macAddress": "MAC-adres", - "broadcastAddress": "Uitsaaiadres", - "udpPort": "UDP-poort", - "waitTimeS": "Wagtyd (s)", - "driveName": "Skyfnaam", - "drivePath": "Rypad", - "ignoreCertificate": "Ignoreer Sertifikaat", - "ignoreCertificateDesc": "Laat verbindings na gashere met selfgetekende sertifikate toe", - "forceLossless": "Forseer verliesloos", - "forceLosslessDesc": "Forseer verlieslose beeldkodering (hoër gehalte, meer bandwydte)", - "disableAudio": "Deaktiveer oudio", - "disableAudioDesc": "Demp alle klank van die afstandsessie", - "enableAudioInput": "Aktiveer oudio-invoer (mikrofoon)", - "enableAudioInputDesc": "Stuur plaaslike mikrofoon na die afgeleë sessie aan", - "wallpaper": "Agtergrond", - "wallpaperDesc": "Wys lessenaar agtergrond (deaktivering verbeter werkverrigting)", - "theming": "Temavorming", - "themingDesc": "Aktiveer visuele temas en style", - "fontSmoothing": "Lettertipe-gladmaak", - "fontSmoothingDesc": "Aktiveer ClearType-lettertipe-weergawe", - "fullWindowDrag": "Volle Venster Sleep", - "fullWindowDragDesc": "Wys vensterinhoud terwyl jy sleep", - "desktopComposition": "Komposisie op die lessenaar", - "desktopCompositionDesc": "Aktiveer Aero-glaseffekte", - "menuAnimations": "Menu Animasies", - "menuAnimationsDesc": "Aktiveer menu-vervaag- en skuifanimasies", - "disableBitmapCaching": "Deaktiveer Bitmap-kasgeheue", - "disableBitmapCachingDesc": "Skakel bitmap-kasgeheue af (kan help met foute)", - "disableOffscreenCaching": "Deaktiveer buiteskerm-kasgeheue", - "disableOffscreenCachingDesc": "Skakel buiteskerm-kasgeheue af", - "disableGlyphCaching": "Deaktiveer glifkasgeheue", - "disableGlyphCachingDesc": "Skakel glifkasgeheue af", - "enableGfx": "Aktiveer GFX", - "enableGfxDesc": "Gebruik RemoteFX-grafikapyplyn", - "enablePrinting": "Aktiveer drukwerk", - "enablePrintingDesc": "Herlei plaaslike drukkers na die afgeleë sessie", - "enableDriveRedirection": "Aktiveer dryfherleiding", - "enableDriveRedirectionDesc": "Karaktereer 'n plaaslike vouer as 'n skyf in die afstandsessie", - "createDrivePath": "Skep 'n rylaanpad", - "createDrivePathDesc": "Skep die lêer outomaties as dit nie bestaan nie", - "disableDownload": "Deaktiveer aflaai", - "disableDownloadDesc": "Voorkom die aflaai van lêers vanaf die afgeleë sessie", - "disableUpload": "Deaktiveer oplaai", - "disableUploadDesc": "Voorkom die oplaai van lêers na die afgeleë sessie", - "enableTouch": "Aktiveer Aanraking", - "enableTouchDesc": "Aktiveer aanstuur van raakinvoer", - "consoleSession": "Konsolesessie", - "consoleSessionDesc": "Koppel aan die konsole (sessie 0) in plaas van 'n nuwe sessie", - "sendWolPacket": "Stuur WOL-pakket", - "sendWolPacketDesc": "Stuur 'n magiese pakket om hierdie gasheer wakker te maak voordat jy verbind", - "disableCopy": "Deaktiveer Kopieer", - "disableCopyDesc": "Voorkom die kopiëring van teks vanaf die afgeleë sessie", - "disablePaste": "Deaktiveer plak", - "disablePasteDesc": "Voorkom die plak van teks in die afgeleë sessie", - "createPathIfMissing": "Skep pad indien ontbreek", - "createPathIfMissingDesc": "Skep outomaties die opnamegids", - "excludeOutput": "Sluit Uitvoer Uit", - "excludeOutputDesc": "Moenie skermuitvoer opneem nie (slegs metadata)", - "excludeMouse": "Sluit Muis uit", - "excludeMouseDesc": "Moenie muisbewegings opneem nie", - "includeKeystrokes": "Sluit toetsaanslagen in", - "includeKeystrokesDesc": "Neem rou toetsaanslagen op benewens skermuitvoer", - "vncPort": "VNC-poort", - "vncPassword": "VNC-wagwoord", - "vncUsernameOptional": "Gebruikersnaam (opsioneel)", - "vncLeaveBlank": "Los leeg indien nie nodig nie", - "cursorMode": "Wysermodus", - "swapRedBlue": "Ruil Rooi/Blou", - "swapRedBlueDesc": "Ruil die rooi en blou kleurkanale om (maak sommige kleurprobleme reg)", - "readOnly": "Leesalleen", - "readOnlyDesc": "Bekyk die afstandskerm sonder om enige insette te stuur", - "telnetPort": "Telnet-poort", - "terminalType": "Terminaaltipe", - "fontName": "Lettertipe Naam", - "fontSize": "Lettergrootte", - "colorScheme": "Kleurskema", - "backspaceKey": "Terugspasie-sleutel", - "colorDepth": "Kleurdiepte", - "resizeMethod": "Vergrootingsmetode", - "driveNamePlaceholder": "Termix-rylaan", - "options": { - "any": "Enige", - "auto": "Outomaties", - "nlaExtended": "NLA Uitgebreide", - "color8": "8-bis", - "color16": "16-bis", - "color24": "24-bis", - "color32": "32-bis", - "displayUpdate": "Skermopdatering", - "reconnect": "Herkoppel", - "preserve": "Bewaar", - "unix": "Unix (LF)", - "windows": "Vensters (CRLF)", - "local": "Plaaslik", - "remote": "Afstandsbediening", - "failsafe": "faalveilig", - "blackOnWhite": "Swart op wit", - "whiteOnBlack": "Wit op Swart", - "grayOnBlack": "Grys op Swart", - "greenOnBlack": "Groen op Swart" - } - }, - "remoteDesktop": "Afstandslessenaar", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Virtuele Netwerk", - "unencryptedShell": "Ongeënkripteerde dop", - "remoteDesktopNotes": "Notas", - "userDefaultsNote": "Instellings wat jy onveranderd laat, volg jou gasheer se verstekwaardes.", - "connectionOriginNote": "Op die lessenaar-app gebruik RDP, VNC en Telnet die afstandbediener tensy die gasheer op Hierdie toestel gestel is, wat 'n guacd benodig wat hierdie masjien kan bereik. Stel een in die Afstandslessenaar-instellings, of per gasheer onder guacd Proxy.", - "docsLink": "Bekyk dokumente", - "connectionOriginGuacamoleNote": "RDP, VNC en Telnet gebruik die afstandbediener tensy jy \"Hierdie toestel\" kies, wat 'n guacd benodig wat hierdie masjien kan bereik. Stel een in die guacd URL-instelling, of per gasheer onder guacd Proxy." - }, - "onboarding": { - "feature_desktop": "Afstandlessenaar", - "feature_desktop_desc": "Maak RDP-, VNC- en Telnet-sessies in 'n oortjie oop, dieselfde as SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Kleurdiepte", - "description": "Gebruik vir RDP-gashere wat dit op hul verstekwaarde laat." - }, - "resizeMethod": { - "label": "Vergrootingsmetode", - "description": "Hoe die afstandskerm die venstergrootte volg." - }, - "resizeDisplayUpdate": "Skermopdatering", - "resizeReconnect": "Herkoppel", - "inherit": "Erf", - "on": "Aan", - "off": "Af", - "forceLossless": { - "label": "Forseer verliesloos", - "description": "Forseer verlieslose beeldkodering (hoër gehalte, meer bandwydte)" - }, - "enableWallpaper": { - "label": "Agtergrond", - "description": "Wys lessenaar agtergrond (deaktivering verbeter werkverrigting)" - }, - "enableFontSmoothing": { - "label": "Lettertipe-gladmaak", - "description": "Aktiveer ClearType-lettertipe-weergawe" - }, - "enableDesktopComposition": { - "label": "Komposisie op die lessenaar", - "description": "Aktiveer Aero-glaseffekte" - }, - "disableAudio": { - "label": "Deaktiveer oudio", - "description": "Demp alle klank van die afstandsessie" - }, - "enablePrinting": { - "label": "Aktiveer drukwerk", - "description": "Herlei plaaslike drukkers na die afgeleë sessie" - }, - "enableDrive": { - "label": "Aktiveer dryfherleiding", - "description": "Karaktereer 'n plaaslike vouer as 'n skyf in die afstandsessie" - }, - "disableCopy": { - "label": "Deaktiveer Kopieer", - "description": "Voorkom die kopiëring van teks vanaf die afgeleë sessie" - }, - "disablePaste": { - "label": "Deaktiveer plak", - "description": "Voorkom die plak van teks in die afgeleë sessie" - }, - "group": "Afstandslessenaar-verstekwaardes", - "colorDepth16": "16-bis", - "colorDepth24": "24-bis", - "colorDepth32": "32-bis" - }, - "admin": { - "enabled": { - "label": "Aktiveer Afstandslessenaar", - "description": "Laat RDP-, VNC- en Telnet-verbindings deur guacd toe." - }, - "guacdUrl": { - "label": "guacd-URL", - "description": "Waar guacd luister, as host:port. Die GUACD_URL, GUACD_HOST en GUACD_PORT omgewingveranderlikes oorskryf dit.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP-poort" - }, - "vncPort": { - "label": "VNC-poort" - }, - "telnetPort": { - "label": "Telnet-poort" - }, - "rdpSecurity": { - "label": "RDP-sekuriteitsmodus" - }, - "rdpIgnoreCert": { - "label": "Ignoreer sertifikaatfoute" - }, - "guacamoleConfig": { - "label": "guacd-instellings" - }, - "enableToolbar": { - "label": "Afstandswerkskerm-nutsbalk", - "description": "Wys die nutsbalk in RDP-, VNC- en Telnet-sessies." - }, - "displayGroup": "Vertoon", - "displayDefaultsTab": "Afstandslessenaar" - } - }, - "palette": { - "connectRdp": "Verbind met RDP", - "connectVnc": "Verbind met VNC", - "connectTelnet": "Verbind met Telnet" - }, - "permissions": { - "sessions": { - "title": "Deel afstandlessenaarsessies", - "description": "Laat sessiedeling en samewerkingskamers hierdie gebruiker se RDP-, VNC- en Telnet-sessies aan ander wys." - } - }, - "errors": { - "remoteServerRequired": "Afstandbediener word benodig. Koppel 'n afstandbediener in Instellings om hierdie verbindingstipe te gebruik." - }, - "plugin": { - "name": "Afstandslessenaar" - } -} diff --git a/plugins/remote-desktop/locales/translated/ar_SA.json b/plugins/remote-desktop/locales/translated/ar_SA.json deleted file mode 100644 index 47bd9d971..000000000 --- a/plugins/remote-desktop/locales/translated/ar_SA.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "جارٍ الاتصال بجلسة {{type}}...", - "checkingGuacd": "جارٍ التحقق من خدمة سطح المكتب البعيد...", - "requestingToken": "طلب رمز اتصال {{type}}...", - "openingSession": "جارٍ فتح الجلسة {{type}}...", - "negotiating": "التفاوض على مصافحة {{type}}...", - "sessionReady": "{{type}} الجلسة جاهزة", - "connectionError": "خطأ في الاتصال", - "connectionFailed": "فشل الاتصال", - "failedToConnect": "فشل الحصول على رمز الاتصال", - "hostNotFound": "المضيف غير موجود", - "noHostSelected": "لم يتم تحديد أي جهاز مضيف", - "reconnect": "إعادة الاتصال", - "retry": "إعادة المحاولة", - "guacdUnavailable": "خدمة سطح المكتب البعيد (guacd) غير متاحة. يرجى التأكد من تشغيل guacd وإمكانية الوصول إليه وتكوينه بشكل صحيح في إعدادات المسؤول.", - "credentialPromptTitle": "أدخل بيانات اعتماد RDP", - "credentialPromptDescription": "تم ضبط هذا المضيف ليطلب بيانات الاعتماد عند الاتصال. تُستخدم هذه البيانات لهذه الجلسة فقط ولا يتم حفظها.", - "connect": "اتصال", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "ملفات محرك الأقراص", - "parent": "الدليل الرئيسي", - "refresh": "تحديث", - "empty": "هذا المجلد فارغ", - "listFailed": "فشل في قراءة هذا المجلد", - "download": "تنزيل", - "downloadFailed": "فشل تنزيل {{name}}", - "upload": "تحميل الملفات", - "uploading": "جارٍ رفع {{name}}...", - "uploaded": "تم التحميل {{name}}", - "uploadFailed": "فشل تحميل {{name}}", - "uploadDisabled": "تم تعطيل التحميلات لهذا الاتصال", - "driveUnavailable": "قم بتمكين إعادة توجيه محرك الأقراص RDP قبل تحميل الملفات", - "driveNotWritable": "لا يستطيع برنامج guacd الكتابة إلى مجلد القرص. تحقق من مسار محرك الأقراص GUACD_DRIVE_PATH (أو مسار محرك الأقراص الخاص بالجهاز المضيف) وصلاحياته.", - "dropToUpload": "قم بإسقاط الملفات لتحميلها" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (قفل الشاشة)", - "winKey": "مفتاح Windows", - "superL": "Super+L (شاشة القفل)", - "superKey": "مفتاح سوبر", - "super": "ممتاز", - "cmdL": "Cmd+L (قفل الشاشة)", - "cmdKey": "مفتاح الاختصار", - "cmd": "الأمر", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}} (مثبت - انقر لتحرير)", - "stickyInactive": "{{key}} (انقر للتثبيت)", - "esc": "Escape", - "tab": "Tab", - "home": "Home", - "end": "End", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "Arrow Up", - "arrowDown": "Arrow Down", - "arrowLeft": "Arrow Left", - "arrowRight": "Arrow Right", - "fnToggle": "مفاتيح الوظائف", - "reconnect": "إعادة الاتصال بالجلسة", - "collapse": "طي شريط الأدوات", - "expand": "توسيع شريط الأدوات", - "dragHandle": "اسحب لتغيير الموضع", - "hide": "إخفاء هذه الميزة لهذه الجلسة. لتعطيلها نهائياً، افتح إعدادات المضيف ← عام ← شريط أدوات الاتصال.", - "zoomIn": "تكبير الصورة", - "zoomOut": "تصغير العرض", - "resetZoom": "إعادة ضبط التكبير", - "fitToScreen": "مناسب للشاشة", - "switchToTrackpad": "قم بالتبديل إلى وضع لوحة اللمس (اسحب لتحريك المؤشر، انقر للنقر)", - "switchToTouch": "قم بالتبديل إلى وضع اللمس (انقر مباشرة في المكان الذي تريد النقر فيه)" - }, - "disabled": "تم إيقاف تشغيل سطح المكتب البعيد. يمكن للمسؤول تشغيله في الإعدادات." - }, - "hosts": { - "nativeRdpOpened": "تم فتحه في سطح المكتب البعيد لنظام التشغيل Windows", - "nativeRdpFailed": "فشل فتح سطح المكتب البعيد لنظام التشغيل Windows", - "openNativeRdp": "افتح في سطح المكتب البعيد لنظام التشغيل Windows", - "guac": { - "toolbar": "شريط الأدوات", - "connection": "اتصال", - "authentication": "المصادقة", - "storedCredential": "بيانات الاعتماد المخزنة", - "authMethod": "طريقة المصادقة", - "authTypeDirect": "مباشر", - "authTypeCredential": "بيانات الاعتماد", - "authTypeNone": "لا شيء", - "authTypeNoneDesc": "لا يتم تخزين أي بيانات اعتماد. سيُطلب منك إدخال اسم المستخدم وكلمة المرور في كل مرة تتصل فيها؛ فهي غير محفوظة.", - "selectCredential": "اختر بيانات اعتماد...", - "connectionSettings": "إعدادات الاتصال", - "displaySettings": "إعدادات العرض", - "audioSettings": "إعدادات الصوت", - "rdpPerformance": "أداء RDP", - "deviceRedirection": "إعادة توجيه الأجهزة", - "session": "جلسة", - "gateway": "بوابة", - "remoteApp": "RemoteApp", - "clipboard": "الحافظة", - "sessionRecording": "تسجيل الجلسة", - "wakeOnLan": "الإيقاظ عبر الشبكة", - "vncSettings": "إعدادات VNC", - "terminalSettings": "إعدادات الطرفية", - "rdpPort": "منفذ RDP", - "username": "اسم المستخدم", - "password": "كلمة المرور", - "passwordSaved": "تم حفظ كلمة المرور، اكتب لتغييرها", - "domain": "مجال", - "securityMode": "وضع الأمان", - "width": "العرض", - "height": "الارتفاع", - "dpi": "DPI", - "clientName": "اسم العميل", - "initialProgram": "البرنامج الأولي", - "serverLayout": "تخطيط الخادم", - "timezone": "المنطقة الزمنية", - "loadBalanceInfo": "معلومات موازنة التحميل / ملف تعريف ارتباط", - "loadBalanceInfoDesc": "ملف تعريف ارتباط وسيط اتصال RD لموازنة تحميل مزرعة RDS (مثل: tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "وكيل guacd", - "guacdHostname": "مضيف guacd", - "guacdHostnamePlaceholder": "الافتراضي العام", - "guacdPort": "منفذ guacd", - "guacdProxyDesc": "تجاوز مثيل guacd العام لهذا الاتصال. اتركه فارغًا لاستخدام إعدادات guacd العمومية.", - "gatewayHostname": "اسم مضيف البوابة", - "gatewayPort": "منفذ البوابة", - "gatewayUsername": "اسم مستخدم البوابة", - "gatewayPassword": "كلمة مرور البوابة", - "gatewayDomain": "مجال البوابة", - "remoteAppProgram": "برنامج RemoteApp", - "workingDirectory": "دليل العمل", - "arguments": "الوسائط", - "normalizeLineEndings": "تطبيع نهايات الأسطر", - "macAddress": "عنوان MAC", - "broadcastAddress": "عنوان البث", - "udpPort": "منفذ UDP", - "waitTimeS": "وقت الانتظار (ث)", - "driveName": "اسم القرص", - "drivePath": "مسار القرص", - "ignoreCertificate": "تجاهل الشهادة", - "ignoreCertificateDesc": "السماح بالاتصال بالمضيفين بشهادات موقعة ذاتيًا", - "forceLossless": "فرض بدون فقدان", - "forceLosslessDesc": "فرض ترميز الصور بدون فقدان (جودة أعلى، استهلاك أكبر للنطاق الترددي)", - "disableAudio": "تعطيل الصوت", - "disableAudioDesc": "كتم كل الصوت من الجلسة البعيدة", - "enableAudioInput": "تمكين إدخال الصوت (الميكروفون)", - "enableAudioInputDesc": "إعادة توجيه الميكروفون المحلي إلى الجلسة البعيدة", - "wallpaper": "خلفية سطح المكتب", - "wallpaperDesc": "إظهار خلفية سطح المكتب (التعطيل يحسن الأداء)", - "theming": "السمات", - "themingDesc": "تمكين السمات والأنماط المرئية", - "fontSmoothing": "تنعيم الخطوط", - "fontSmoothingDesc": "تمكين عرض خطوط ClearType", - "fullWindowDrag": "سحب النافذة بالكامل", - "fullWindowDragDesc": "إظهار محتويات النافذة أثناء السحب", - "desktopComposition": "تركيب سطح المكتب", - "desktopCompositionDesc": "تمكين تأثيرات Aero الزجاجية", - "menuAnimations": "حركات القوائم", - "menuAnimationsDesc": "تمكين حركات التلاشي والانزلاق للقوائم", - "disableBitmapCaching": "تعطيل التخزين المؤقت للصور النقطية", - "disableBitmapCachingDesc": "إيقاف التخزين المؤقت للصور النقطية (قد يساعد في حل المشكلات)", - "disableOffscreenCaching": "تعطيل التخزين المؤقت خارج الشاشة", - "disableOffscreenCachingDesc": "إيقاف التخزين المؤقت خارج الشاشة", - "disableGlyphCaching": "تعطيل التخزين المؤقت للأشكال", - "disableGlyphCachingDesc": "إيقاف التخزين المؤقت للأشكال", - "enableGfx": "تمكين GFX", - "enableGfxDesc": "استخدام خط أنابيب رسوميات RemoteFX", - "enablePrinting": "تمكين الطباعة", - "enablePrintingDesc": "إعادة توجيه الطابعات المحلية إلى الجلسة البعيدة", - "enableDriveRedirection": "تمكين إعادة توجيه القرص", - "enableDriveRedirectionDesc": "تعيين مجلد محلي كقرص في الجلسة البعيدة", - "createDrivePath": "إنشاء مسار محرك الأقراص", - "createDrivePathDesc": "إنشاء المجلد تلقائياً إذا لم يكن موجوداً", - "disableDownload": "تعطيل التحميل", - "disableDownloadDesc": "منع تنزيل الملفات من الجلسة البعيدة", - "disableUpload": "تعطيل الرفع", - "disableUploadDesc": "منع رفع الملفات إلى الجلسة البعيدة", - "enableTouch": "تمكين اللمس", - "enableTouchDesc": "تمكين تمرير إدخالات اللمس", - "consoleSession": "جلسة وحدة التحكم", - "consoleSessionDesc": "الاتصال بوحدة التحكم (الجلسة 0) بدلاً من جلسة جديدة", - "sendWolPacket": "إرسال حزمة WOL", - "sendWolPacketDesc": "إرسال حزمة سحرية لإيقاظ هذا المضيف قبل الاتصال", - "disableCopy": "تعطيل النسخ", - "disableCopyDesc": "منع نسخ النص من الجلسة البعيدة", - "disablePaste": "تعطيل اللصق", - "disablePasteDesc": "منع لصق النص في الجلسة البعيدة", - "createPathIfMissing": "إنشاء المسار إذا كان مفقوداً", - "createPathIfMissingDesc": "إنشاء دليل التسجيل تلقائياً", - "excludeOutput": "استبعاد المخرجات", - "excludeOutputDesc": "عدم تسجيل مخرجات الشاشة (البيانات الوصفية فقط)", - "excludeMouse": "استبعاد الفأرة", - "excludeMouseDesc": "عدم تسجيل حركات الفأرة", - "includeKeystrokes": "تضمين ضغطات المفاتيح", - "includeKeystrokesDesc": "تسجيل ضغطات المفاتيح الخام بالإضافة إلى مخرجات الشاشة", - "vncPort": "منفذ VNC", - "vncPassword": "كلمة مرور VNC", - "vncUsernameOptional": "اسم المستخدم (اختياري)", - "vncLeaveBlank": "اتركه فارغاً إذا لم يكن مطلوباً", - "cursorMode": "وضع المؤشر", - "swapRedBlue": "تبديل الأحمر/الأزرق", - "swapRedBlueDesc": "تبديل قنوات اللون الأحمر والأزرق (يصلح بعض مشكلات الألوان)", - "readOnly": "للقراءة فقط", - "readOnlyDesc": "عرض الشاشة البعيدة دون إرسال أي إدخال", - "telnetPort": "منفذ Telnet", - "terminalType": "نوع الطرفية", - "fontName": "اسم الخط", - "fontSize": "حجم الخط", - "colorScheme": "نظام الألوان", - "backspaceKey": "مفتاح Backspace", - "colorDepth": "عمق اللون", - "resizeMethod": "طريقة تغيير الحجم", - "driveNamePlaceholder": "محرك Termix", - "options": { - "any": "أي", - "auto": "تلقائي", - "nlaExtended": "NLA Extended", - "color8": "8 بت", - "color16": "16 بت", - "color24": "24 بت", - "color32": "32 بت", - "displayUpdate": "تحديث الشاشة", - "reconnect": "إعادة الاتصال", - "preserve": "يحفظ", - "unix": "يونكس (LF)", - "windows": "ويندوز (CRLF)", - "local": "محلي", - "remote": "البعيد", - "failsafe": "نظام أمان", - "blackOnWhite": "أسود على أبيض", - "whiteOnBlack": "أبيض على أسود", - "grayOnBlack": "رمادي على أسود", - "greenOnBlack": "أخضر على أسود" - } - }, - "remoteDesktop": "سطح المكتب البعيد", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "الشبكة الافتراضية", - "unencryptedShell": "shell غير مشفر", - "remoteDesktopNotes": "ملاحظات", - "userDefaultsNote": "تتبع الإعدادات التي تتركها دون تغيير الإعدادات الافتراضية للمضيف.", - "connectionOriginNote": "في تطبيق سطح المكتب، تستخدم بروتوكولات RDP وVNC وTelnet الخادم البعيد ما لم يتم ضبط المضيف على \"هذا الجهاز\"، الأمر الذي يتطلب عنوان IP يمكن لهذا الجهاز الوصول إليه. يمكنك تعيين عنوان IP في إعدادات سطح المكتب البعيد، أو لكل مضيف ضمن \"وكيل guacd\".", - "docsLink": "عرض المستندات", - "connectionOriginGuacamoleNote": "تستخدم بروتوكولات RDP وVNC وTelnet الخادم البعيد ما لم تختر \"هذا الجهاز\"، الأمر الذي يتطلب عنوان IP افتراضيًا (guacd) يمكن لهذا الجهاز الوصول إليه. يمكنك تعيين عنوان IP افتراضي في إعدادات عنوان URL الخاص بـ guacd، أو لكل مضيف ضمن إعدادات وكيل guacd." - }, - "onboarding": { - "feature_desktop": "سطح المكتب البعيد", - "feature_desktop_desc": "افتح جلسات RDP وVNC وTelnet في علامة تبويب، تمامًا مثل SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "عمق اللون", - "description": "يُستخدم مع مضيفي بروتوكول سطح المكتب البعيد (RDP) الذين يتركونه على إعداداتهم الافتراضية." - }, - "resizeMethod": { - "label": "طريقة تغيير الحجم", - "description": "كيف تتكيف الشاشة البعيدة مع حجم النافذة." - }, - "resizeDisplayUpdate": "تحديث الشاشة", - "resizeReconnect": "إعادة الاتصال", - "inherit": "يرث", - "on": "مفعلة", - "off": "غير مفعلة", - "forceLossless": { - "label": "فرض بدون فقدان", - "description": "فرض ترميز الصور بدون فقدان (جودة أعلى، استهلاك أكبر للنطاق الترددي)" - }, - "enableWallpaper": { - "label": "خلفية سطح المكتب", - "description": "إظهار خلفية سطح المكتب (التعطيل يحسن الأداء)" - }, - "enableFontSmoothing": { - "label": "تنعيم الخطوط", - "description": "تمكين عرض خطوط ClearType" - }, - "enableDesktopComposition": { - "label": "تركيب سطح المكتب", - "description": "تمكين تأثيرات Aero الزجاجية" - }, - "disableAudio": { - "label": "تعطيل الصوت", - "description": "كتم كل الصوت من الجلسة البعيدة" - }, - "enablePrinting": { - "label": "تمكين الطباعة", - "description": "إعادة توجيه الطابعات المحلية إلى الجلسة البعيدة" - }, - "enableDrive": { - "label": "تمكين إعادة توجيه القرص", - "description": "تعيين مجلد محلي كقرص في الجلسة البعيدة" - }, - "disableCopy": { - "label": "تعطيل النسخ", - "description": "منع نسخ النص من الجلسة البعيدة" - }, - "disablePaste": { - "label": "تعطيل اللصق", - "description": "منع لصق النص في الجلسة البعيدة" - }, - "group": "الإعدادات الافتراضية لسطح المكتب البعيد", - "colorDepth16": "16 بت", - "colorDepth24": "24 بت", - "colorDepth32": "32 بت" - }, - "admin": { - "enabled": { - "label": "تمكين سطح المكتب البعيد", - "description": "السماح باتصالات RDP وVNC وTelnet عبر guacd." - }, - "guacdUrl": { - "label": "رابط guacd", - "description": "حيث يستمع برنامج guacd، كعنوان المضيف:المنفذ. وتتجاوز متغيرات البيئة GUACD_URL و GUACD_HOST و GUACD_PORT هذا الإعداد.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "منفذ RDP" - }, - "vncPort": { - "label": "منفذ VNC" - }, - "telnetPort": { - "label": "منفذ Telnet" - }, - "rdpSecurity": { - "label": "وضع أمان RDP" - }, - "rdpIgnoreCert": { - "label": "تجاهل أخطاء الشهادة" - }, - "guacamoleConfig": { - "label": "إعدادات guacd" - }, - "enableToolbar": { - "label": "شريط أدوات سطح المكتب البعيد", - "description": "إظهار شريط الأدوات في جلسات RDP وVNC وTelnet." - }, - "displayGroup": "عرض", - "displayDefaultsTab": "سطح المكتب البعيد" - } - }, - "palette": { - "connectRdp": "الاتصال بـ RDP", - "connectVnc": "الاتصال بـ VNC", - "connectTelnet": "اتصل بشبكة Telnet" - }, - "permissions": { - "sessions": { - "title": "مشاركة جلسات سطح المكتب البعيد", - "description": "اسمح لمشاركة الجلسات وغرف التعاون بعرض جلسات RDP وVNC وTelnet الخاصة بهذا المستخدم للآخرين." - } - }, - "errors": { - "remoteServerRequired": "يلزم وجود خادم بعيد. قم بتوصيل خادم بعيد في الإعدادات لاستخدام نوع الاتصال هذا." - }, - "plugin": { - "name": "سطح المكتب البعيد" - } -} diff --git a/plugins/remote-desktop/locales/translated/bg_BG.json b/plugins/remote-desktop/locales/translated/bg_BG.json deleted file mode 100644 index c1f0f70f5..000000000 --- a/plugins/remote-desktop/locales/translated/bg_BG.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Свързване към сесия {{type}}...", - "checkingGuacd": "Проверка на услугата за отдалечен работен плот...", - "requestingToken": "Заявка за {{type}} токен за връзка...", - "openingSession": "Отваряне на {{type}} сесия...", - "negotiating": "Преговаряне {{type}} ръкостискане...", - "sessionReady": "{{type}} сесията е готова", - "connectionError": "Грешка във връзката", - "connectionFailed": "Връзката не бе успешна", - "failedToConnect": "Неуспешно получаване на токен за връзка", - "hostNotFound": "Хостът не е намерен", - "noHostSelected": "Няма избран хост", - "reconnect": "Свържете се отново", - "retry": "Опитай отново", - "guacdUnavailable": "Услугата за отдалечен работен плот (guacd) не е налична. Моля, уверете се, че guacd работи, е достъпна и е конфигурирана правилно в администраторските настройки.", - "credentialPromptTitle": "Въведете RDP идентификационни данни", - "credentialPromptDescription": "Този хост е настроен да изисква идентификационни данни при свързване. Те се използват само за тази сесия и не се запазват.", - "connect": "Свържете се", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Файлове на Диск", - "parent": "Родителска директория", - "refresh": "Обновяване", - "empty": "Тази папка е празна", - "listFailed": "Неуспешно четене на тази папка", - "download": "Изтегляне", - "downloadFailed": "Неуспешно изтегляне {{name}}", - "upload": "Качване на файлове", - "uploading": "Качване {{name}}...", - "uploaded": "Качено {{name}}", - "uploadFailed": "Качването не бе успешно {{name}}", - "uploadDisabled": "Качванията са деактивирани за тази връзка", - "driveUnavailable": "Активирайте пренасочването на RDP устройството, преди да качвате файлове", - "driveNotWritable": "guacd не може да записва в папката на устройството. Проверете GUACD_DRIVE_PATH (или пътя на устройството на хоста) и неговите разрешения.", - "dropToUpload": "Пуснете файловете, за да ги качите" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Заключен екран)", - "winKey": "Клавиш за Windows", - "superL": "Супер+L (Заключен екран)", - "superKey": "Супер ключ", - "super": "Супер", - "cmdL": "Cmd+L (Заключен екран)", - "cmdKey": "Команден ключ", - "cmd": "Команда", - "ctrl": "Ctrl", - "alt": "Алтернативно", - "shift": "Изместване", - "win": "Спечелете", - "stickyActive": "{{key}} (заключено - щракнете за освобождаване)", - "stickyInactive": "{{key}} (щракнете, за да заключите)", - "esc": "Бягство", - "tab": "Раздел", - "home": "Дом", - "end": "Край", - "pageUp": "Страница нагоре", - "pageDown": "Страница надолу", - "arrowUp": "Стрелка нагоре", - "arrowDown": "Стрелка надолу", - "arrowLeft": "Стрелка наляво", - "arrowRight": "Стрелка надясно", - "fnToggle": "Функционални клавиши", - "reconnect": "Сесия за повторно свързване", - "collapse": "Свиване на лентата с инструменти", - "expand": "Разгъване на лентата с инструменти", - "dragHandle": "Плъзнете, за да препозиционирате", - "hide": "Скриване за тази сесия. За да го деактивирате за постоянно, отворете Настройки на хоста → Общи → Лента с инструменти за връзка.", - "zoomIn": "Увеличаване", - "zoomOut": "Намаляване на мащаба", - "resetZoom": "Нулиране на мащаба", - "fitToScreen": "Побиране на екрана", - "switchToTrackpad": "Превключване в режим на тракпад (плъзнете, за да преместите курсора, докоснете, за да щракнете)", - "switchToTouch": "Превключване към режим на докосване (докоснете директно там, където искате да щракнете)" - }, - "disabled": "Отдалеченият работен плот е изключен. Администратор може да го включи в „Настройки“." - }, - "hosts": { - "nativeRdpOpened": "Отворено в отдалечен работен плот на Windows", - "nativeRdpFailed": "Неуспешно отваряне на отдалечения работен плот на Windows", - "openNativeRdp": "Отваряне в отдалечен работен плот на Windows", - "guac": { - "toolbar": "Лента с инструменти", - "connection": "Връзка", - "authentication": "Удостоверяване", - "storedCredential": "Съхранени идентификационни данни", - "authMethod": "Метод за удостоверяване", - "authTypeDirect": "Директно", - "authTypeCredential": "Пълномощия", - "authTypeNone": "Няма", - "authTypeNoneDesc": "Не се съхраняват идентификационни данни. Ще бъдете подканени да въведете потребителско име и парола всеки път, когато се свържете; те не се запазват.", - "selectCredential": "Изберете идентификационен номер...", - "connectionSettings": "Настройки за връзка", - "displaySettings": "Настройки на дисплея", - "audioSettings": "Аудио настройки", - "rdpPerformance": "Производителност на RDP", - "deviceRedirection": "Пренасочване на устройството", - "session": "Сесия", - "gateway": "Портал", - "remoteApp": "Отдалечено приложение", - "clipboard": "Клипборд", - "sessionRecording": "Запис на сесия", - "wakeOnLan": "Събуждане по локална мрежа", - "vncSettings": "VNC настройки", - "terminalSettings": "Настройки на терминала", - "rdpPort": "RDP порт", - "username": "Потребителско име", - "password": "Парола", - "passwordSaved": "Паролата е запазена, въведете, за да я промените", - "domain": "Домейн", - "securityMode": "Режим на сигурност", - "width": "Ширина", - "height": "Височина", - "dpi": "DPI", - "clientName": "Име на клиента", - "initialProgram": "Първоначална програма", - "serverLayout": "Разположение на сървъра", - "timezone": "Часова зона", - "loadBalanceInfo": "Информация за балансиране на натоварването / бисквитка", - "loadBalanceInfoDesc": "„бисквитка“ на RD Connection Broker за балансиране на натоварването на RDS фермата (напр. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd прокси", - "guacdHostname": "guacd хост", - "guacdHostnamePlaceholder": "Глобално по подразбиране", - "guacdPort": "порт guacd", - "guacdProxyDesc": "Заменете глобалния екземпляр на guacd за тази връзка. Оставете празно, за да използвате глобално конфигурирания guacd.", - "gatewayHostname": "Име на хост на шлюз", - "gatewayPort": "Порт на шлюза", - "gatewayUsername": "Потребителско име на шлюз", - "gatewayPassword": "Парола за шлюз", - "gatewayDomain": "Домейн на шлюз", - "remoteAppProgram": "Програма за отдалечено приложение", - "workingDirectory": "Работна директория", - "arguments": "Аргументи", - "normalizeLineEndings": "Нормализиране на края на редовете", - "macAddress": "MAC адрес", - "broadcastAddress": "Адрес за излъчване", - "udpPort": "UDP порт", - "waitTimeS": "Време за изчакване (и)", - "driveName": "Име на устройството", - "drivePath": "Път на устройството", - "ignoreCertificate": "Игнориране на сертификата", - "ignoreCertificateDesc": "Разрешаване на връзки към хостове със самоподписани сертификати", - "forceLossless": "Принудително беззагубно", - "forceLosslessDesc": "Принудително кодиране на изображения без загуби (по-високо качество, по-голяма честотна лента)", - "disableAudio": "Деактивиране на звука", - "disableAudioDesc": "Изключване на звука от отдалечената сесия", - "enableAudioInput": "Активиране на аудио вход (микрофон)", - "enableAudioInputDesc": "Пренасочване на локалния микрофон към отдалечената сесия", - "wallpaper": "Тапет", - "wallpaperDesc": "Показване на тапет на работния плот (деактивирането му подобрява производителността)", - "theming": "Темиране", - "themingDesc": "Активиране на визуални теми и стилове", - "fontSmoothing": "Изглаждане на шрифта", - "fontSmoothingDesc": "Активиране на рендирането на шрифтове ClearType", - "fullWindowDrag": "Плъзгане на целия прозорец", - "fullWindowDragDesc": "Показване на съдържанието на прозореца при плъзгане", - "desktopComposition": "Композиция на работния плот", - "desktopCompositionDesc": "Активиране на ефектите на Aero Glass", - "menuAnimations": "Анимации на менюто", - "menuAnimationsDesc": "Активиране на анимации за избледняване на менюта и слайдове", - "disableBitmapCaching": "Деактивиране на кеширането на растерни изображения", - "disableBitmapCachingDesc": "Изключете кеша на растерните изображения (може да помогне с проблеми)", - "disableOffscreenCaching": "Деактивиране на кеширането извън екрана", - "disableOffscreenCachingDesc": "Изключване на кеша извън екрана", - "disableGlyphCaching": "Деактивиране на кеширането на глифи", - "disableGlyphCachingDesc": "Изключване на кеша на глифове", - "enableGfx": "Активиране на графични ефекти", - "enableGfxDesc": "Използвайте графичен конвейер RemoteFX", - "enablePrinting": "Активиране на печат", - "enablePrintingDesc": "Пренасочване на локални принтери към отдалечена сесия", - "enableDriveRedirection": "Активиране на пренасочване на устройството", - "enableDriveRedirectionDesc": "Карта на локална папка като устройство в отдалечена сесия", - "createDrivePath": "Създаване на път за устройство", - "createDrivePathDesc": "Автоматично създаване на папката, ако тя не съществува", - "disableDownload": "Деактивиране на изтеглянето", - "disableDownloadDesc": "Предотвратяване на изтеглянето на файлове от отдалечената сесия", - "disableUpload": "Деактивиране на качването", - "disableUploadDesc": "Предотвратяване на качването на файлове към отдалечената сесия", - "enableTouch": "Активиране на докосване", - "enableTouchDesc": "Активиране на пренасочване на сензорен вход", - "consoleSession": "Конзолна сесия", - "consoleSessionDesc": "Свържете се с конзолата (сесия 0) вместо с нова сесия", - "sendWolPacket": "Изпращане на WOL пакет", - "sendWolPacketDesc": "Изпратете магически пакет, за да събудите този хост преди свързване", - "disableCopy": "Деактивиране на копирането", - "disableCopyDesc": "Предотвратяване на копирането на текст от отдалечената сесия", - "disablePaste": "Деактивиране на поставянето", - "disablePasteDesc": "Предотвратяване на поставянето на текст в отдалечената сесия", - "createPathIfMissing": "Създаване на път, ако липсва", - "createPathIfMissingDesc": "Автоматично създаване на директорията за записи", - "excludeOutput": "Изключване на изхода", - "excludeOutputDesc": "Не записвайте изхода на екрана (само метаданни)", - "excludeMouse": "Изключване на мишката", - "excludeMouseDesc": "Не записвайте движенията на мишката", - "includeKeystrokes": "Включи клавишни комбинации", - "includeKeystrokesDesc": "Записване на сурови натискания на клавиши в допълнение към изхода на екрана", - "vncPort": "VNC порт", - "vncPassword": "VNC парола", - "vncUsernameOptional": "Потребителско име (по избор)", - "vncLeaveBlank": "Оставете празно, ако не е необходимо", - "cursorMode": "Режим на курсора", - "swapRedBlue": "Разменете червено/синьо", - "swapRedBlueDesc": "Разменете червените и сините цветови канали (коригира някои проблеми с цветовете)", - "readOnly": "Само за четене", - "readOnlyDesc": "Преглед на отдалечения екран без изпращане на каквито и да било входни данни", - "telnetPort": "Telnet порт", - "terminalType": "Тип терминал", - "fontName": "Име на шрифта", - "fontSize": "Размер на шрифта", - "colorScheme": "Цветова схема", - "backspaceKey": "Клавиш Backspace", - "colorDepth": "Дълбочина на цвета", - "resizeMethod": "Метод за преоразмеряване", - "driveNamePlaceholder": "Термикс Драйв", - "options": { - "any": "Всякакви", - "auto": "Автоматично", - "nlaExtended": "Разширена НЛА", - "color8": "8-битов", - "color16": "16-битов", - "color24": "24-битов", - "color32": "32-битов", - "displayUpdate": "Актуализация на дисплея", - "reconnect": "Свържете се отново", - "preserve": "Запази", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Местно", - "remote": "Дистанционно", - "failsafe": "отказоустойчив", - "blackOnWhite": "Черно на бяло", - "whiteOnBlack": "Бяло върху черно", - "grayOnBlack": "Сиво на черно", - "greenOnBlack": "Зелено на черно" - } - }, - "remoteDesktop": "Отдалечен работен плот", - "tabRdp": "ПРСР", - "tabVnc": "VNC", - "tabTelnet": "Телнет", - "virtualNetwork": "Виртуална мрежа", - "unencryptedShell": "Некриптирана обвивка", - "remoteDesktopNotes": "Бележки", - "userDefaultsNote": "Настройките, които оставяте непроменени, следват настройките по подразбиране на вашия хост.", - "connectionOriginNote": "В настолното приложение RDP, VNC и Telnet използват отдалечения сървър, освен ако хостът не е зададен на „Това устройство“, което изисква guacd, до който тази машина може да достигне. Задайте такъв в настройките на отдалечения работен плот или за всеки хост под guacd Proxy.", - "docsLink": "Преглед на документи", - "connectionOriginGuacamoleNote": "RDP, VNC и Telnet използват отдалечения сървър, освен ако не изберете „Това устройство“, което изисква guacd, до който тази машина може да достигне. Задайте такъв в настройката на guacd URL или за всеки хост под guacd Proxy." - }, - "onboarding": { - "feature_desktop": "Отдалечен работен плот", - "feature_desktop_desc": "Отваряне на RDP, VNC и Telnet сесии в раздел, както при SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Дълбочина на цвета", - "description": "Използва се за RDP хостове, които го оставят по подразбиране." - }, - "resizeMethod": { - "label": "Метод за преоразмеряване", - "description": "Как отдалеченият екран следва размера на прозореца." - }, - "resizeDisplayUpdate": "Актуализация на дисплея", - "resizeReconnect": "Свържете се отново", - "inherit": "Наследяване", - "on": "Включено", - "off": "Изключено", - "forceLossless": { - "label": "Принудително беззагубно", - "description": "Принудително кодиране на изображения без загуби (по-високо качество, по-голяма честотна лента)" - }, - "enableWallpaper": { - "label": "Тапет", - "description": "Показване на тапет на работния плот (деактивирането му подобрява производителността)" - }, - "enableFontSmoothing": { - "label": "Изглаждане на шрифта", - "description": "Активиране на рендирането на шрифтове ClearType" - }, - "enableDesktopComposition": { - "label": "Композиция на работния плот", - "description": "Активиране на ефектите на Aero Glass" - }, - "disableAudio": { - "label": "Деактивиране на звука", - "description": "Изключване на звука от отдалечената сесия" - }, - "enablePrinting": { - "label": "Активиране на печат", - "description": "Пренасочване на локални принтери към отдалечена сесия" - }, - "enableDrive": { - "label": "Активиране на пренасочване на устройството", - "description": "Карта на локална папка като устройство в отдалечена сесия" - }, - "disableCopy": { - "label": "Деактивиране на копирането", - "description": "Предотвратяване на копирането на текст от отдалечената сесия" - }, - "disablePaste": { - "label": "Деактивиране на поставянето", - "description": "Предотвратяване на поставянето на текст в отдалечената сесия" - }, - "group": "Настройки по подразбиране за отдалечен работен плот", - "colorDepth16": "16-битов", - "colorDepth24": "24-битов", - "colorDepth32": "32-битов" - }, - "admin": { - "enabled": { - "label": "Активиране на отдалечен работен плот", - "description": "Разрешаване на RDP, VNC и Telnet връзки чрез guacd." - }, - "guacdUrl": { - "label": "URL адрес на GUACD", - "description": "Където guacd слуша, като host:port. Променливите на средата GUACD_URL, GUACD_HOST и GUACD_PORT отменят това.", - "placeholder": "гуакд:4822" - } - }, - "host": { - "enableRdp": { - "label": "ПРСР" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Телнет" - }, - "rdpPort": { - "label": "RDP порт" - }, - "vncPort": { - "label": "VNC порт" - }, - "telnetPort": { - "label": "Telnet порт" - }, - "rdpSecurity": { - "label": "Режим на защита на RDP" - }, - "rdpIgnoreCert": { - "label": "Игнориране на грешки в сертификатите" - }, - "guacamoleConfig": { - "label": "настройки на GUACD" - }, - "enableToolbar": { - "label": "Лента с инструменти за отдалечен работен плот", - "description": "Показване на лентата с инструменти в RDP, VNC и Telnet сесии." - }, - "displayGroup": "Дисплей", - "displayDefaultsTab": "Отдалечен работен плот" - } - }, - "palette": { - "connectRdp": "Свържете се с RDP", - "connectVnc": "Свържете се с VNC", - "connectTelnet": "Свържете се с Telnet" - }, - "permissions": { - "sessions": { - "title": "Споделяне на сесии за отдалечен работен плот", - "description": "Позволете на споделянето на сесии и стаите за сътрудничество да показват RDP, VNC и Telnet сесиите на този потребител на други." - } - }, - "errors": { - "remoteServerRequired": "Изисква се отдалечен сървър. Свържете се с отдалечен сървър в „Настройки“, за да използвате този тип връзка." - }, - "plugin": { - "name": "Отдалечен работен плот" - } -} diff --git a/plugins/remote-desktop/locales/translated/bn_BD.json b/plugins/remote-desktop/locales/translated/bn_BD.json deleted file mode 100644 index 339c2c63d..000000000 --- a/plugins/remote-desktop/locales/translated/bn_BD.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "{{type}} সেশনে সংযোগ স্থাপন করা হচ্ছে...", - "checkingGuacd": "রিমোট ডেস্কটপ পরিষেবা পরীক্ষা করা হচ্ছে...", - "requestingToken": "{{type}} সংযোগ টোকেনের জন্য অনুরোধ করা হচ্ছে...", - "openingSession": "{{type}} সেশন শুরু হচ্ছে...", - "negotiating": "{{type}} করমর্দন নিয়ে আলোচনা চলছে...", - "sessionReady": "{{type}} সেশন প্রস্তুত", - "connectionError": "সংযোগ ত্রুটি", - "connectionFailed": "সংযোগ ব্যর্থ হয়েছে", - "failedToConnect": "সংযোগ টোকেন পেতে ব্যর্থ হয়েছে", - "hostNotFound": "হোস্ট খুঁজে পাওয়া যায়নি", - "noHostSelected": "কোন হোস্ট নির্বাচন করা হয়নি", - "reconnect": "পুনরায় সংযোগ করুন", - "retry": "পুনরায় চেষ্টা করুন", - "guacdUnavailable": "রিমোট ডেস্কটপ সার্ভিস (guacd) উপলব্ধ নেই। অনুগ্রহ করে নিশ্চিত করুন যে guacd চালু ও অ্যাক্সেসযোগ্য আছে এবং অ্যাডমিন সেটিংসে সঠিকভাবে কনফিগার করা আছে।", - "credentialPromptTitle": "RDP পরিচয়পত্র প্রবেশ করান", - "credentialPromptDescription": "এই হোস্টটি সংযোগের সময় ক্রেডেনশিয়াল চাওয়ার জন্য সেট করা আছে। এগুলি শুধুমাত্র এই সেশনের জন্য ব্যবহৃত হয় এবং সংরক্ষিত হয় না।", - "connect": "সংযোগ করুন", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "ড্রাইভ ফাইল", - "parent": "অভিভাবক ডিরেক্টরি", - "refresh": "রিফ্রেশ", - "empty": "এই ফোল্ডারটি খালি", - "listFailed": "এই ফোল্ডারটি পড়তে ব্যর্থ হয়েছে", - "download": "ডাউনলোড করুন", - "downloadFailed": "{{name}} ডাউনলোড করতে ব্যর্থ হয়েছে", - "upload": "ফাইল আপলোড করুন", - "uploading": "{{name}} আপলোড হচ্ছে ...", - "uploaded": "আপলোড করা হয়েছে {{name}}", - "uploadFailed": "{{name}} আপলোড করতে ব্যর্থ হয়েছে", - "uploadDisabled": "এই সংযোগের জন্য আপলোড নিষ্ক্রিয় করা হয়েছে।", - "driveUnavailable": "ফাইল আপলোড করার আগে RDP ড্রাইভ রিডাইরেকশন সক্রিয় করুন।", - "driveNotWritable": "guacd ড্রাইভ ফোল্ডারে লিখতে পারছে না। GUACD_DRIVE_PATH (অথবা হোস্টের ড্রাইভ পাথ) এবং এর অনুমতিগুলো পরীক্ষা করুন।", - "dropToUpload": "আপলোড করতে ফাইলগুলো ড্রপ করুন" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "উইন+এল (লক স্ক্রিন)", - "winKey": "উইন্ডোজ কী", - "superL": "সুপার+এল (লক স্ক্রিন)", - "superKey": "সুপার কী", - "super": "সুপার", - "cmdL": "Cmd+L (লক স্ক্রিন)", - "cmdKey": "কমান্ড কী", - "cmd": "কমান্ডার", - "ctrl": "Ctrl", - "alt": "বিকল্প", - "shift": "শিফট", - "win": "জয়", - "stickyActive": "{{key}} (আটকানো - খুলতে ক্লিক করুন)", - "stickyInactive": "{{key}} (আটকাতে ক্লিক করুন)", - "esc": "পালানো", - "tab": "ট্যাব", - "home": "হোম", - "end": "শেষ", - "pageUp": "পৃষ্ঠা উপরে", - "pageDown": "পৃষ্ঠা নিচে", - "arrowUp": "তীর ঊর্ধ্বমুখী", - "arrowDown": "অ্যারো ডাউন", - "arrowLeft": "বাম তীর", - "arrowRight": "ডান তীরচিহ্ন", - "fnToggle": "ফাংশন কী", - "reconnect": "পুনরায় সংযোগ সেশন", - "collapse": "টুলবার সংকুচিত করুন", - "expand": "টুলবার প্রসারিত করুন", - "dragHandle": "অবস্থান পরিবর্তন করতে টেনে আনুন", - "hide": "এই সেশনের জন্য লুকান। এটিকে স্থায়ীভাবে নিষ্ক্রিয় করতে, হোস্ট সেটিংস → সাধারণ → সংযোগ টুলবার খুলুন।", - "zoomIn": "জুম ইন", - "zoomOut": "জুম আউট", - "resetZoom": "জুম রিসেট করুন", - "fitToScreen": "স্ক্রিনের সাথে মানানসই", - "switchToTrackpad": "ট্র্যাকপ্যাড মোডে যান (কার্সার সরাতে ড্র্যাগ করুন, ক্লিক করতে ট্যাপ করুন)", - "switchToTouch": "টাচ মোডে যান (যেখানে ক্লিক করতে চান সেখানে সরাসরি ট্যাপ করুন)" - }, - "disabled": "রিমোট ডেস্কটপ বন্ধ করা আছে। একজন অ্যাডমিন সেটিংস থেকে এটি চালু করতে পারেন।" - }, - "hosts": { - "nativeRdpOpened": "উইন্ডোজ রিমোট ডেস্কটপে খোলা হয়েছে", - "nativeRdpFailed": "উইন্ডোজ রিমোট ডেস্কটপ খুলতে ব্যর্থ হয়েছে", - "openNativeRdp": "উইন্ডোজ রিমোট ডেস্কটপে খুলুন", - "guac": { - "toolbar": "টুলবার", - "connection": "সংযোগ", - "authentication": "প্রমাণীকরণ", - "storedCredential": "সংরক্ষিত পরিচয়পত্র", - "authMethod": "প্রমাণীকরণ পদ্ধতি", - "authTypeDirect": "সরাসরি", - "authTypeCredential": "শংসাপত্র", - "authTypeNone": "কোনোটিই না", - "authTypeNoneDesc": "কোনো পরিচয়পত্র সংরক্ষিত থাকে না। প্রতিবার সংযোগ করার সময় আপনাকে একটি ইউজারনেম এবং পাসওয়ার্ড দিতে বলা হবে; সেগুলি সংরক্ষণ করা হয় না।", - "selectCredential": "একটি শংসাপত্র নির্বাচন করুন...", - "connectionSettings": "সংযোগ সেটিংস", - "displaySettings": "ডিসপ্লে সেটিংস", - "audioSettings": "অডিও সেটিংস", - "rdpPerformance": "আরডিপি পারফরম্যান্স", - "deviceRedirection": "ডিভাইস পুনঃনির্দেশনা", - "session": "অধিবেশন", - "gateway": "গেটওয়ে", - "remoteApp": "রিমোটঅ্যাপ", - "clipboard": "ক্লিপবোর্ড", - "sessionRecording": "সেশন রেকর্ডিং", - "wakeOnLan": "ওয়েক-অন-ল্যান", - "vncSettings": "VNC সেটিংস", - "terminalSettings": "টার্মিনাল সেটিংস", - "rdpPort": "আরডিপি পোর্ট", - "username": "ব্যবহারকারীর নাম", - "password": "পাসওয়ার্ড", - "passwordSaved": "পাসওয়ার্ড সংরক্ষিত আছে, পরিবর্তন করতে টাইপ করুন", - "domain": "ডোমেইন", - "securityMode": "নিরাপত্তা মোড", - "width": "প্রস্থ", - "height": "উচ্চতা", - "dpi": "ডিপিআই", - "clientName": "ক্লায়েন্টের নাম", - "initialProgram": "প্রাথমিক প্রোগ্রাম", - "serverLayout": "সার্ভার লেআউট", - "timezone": "সময় অঞ্চল", - "loadBalanceInfo": "লোড ব্যালেন্স তথ্য / কুকি", - "loadBalanceInfoDesc": "RDS ফার্ম লোড ব্যালান্সিংয়ের জন্য RD কানেকশন ব্রোকার কুকি (যেমন tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "গুয়াকড প্রক্সি", - "guacdHostname": "গুয়াকড হোস্ট", - "guacdHostnamePlaceholder": "গ্লোবাল ডিফল্ট", - "guacdPort": "গুয়াকড পোর্ট", - "guacdProxyDesc": "এই সংযোগের জন্য গ্লোবাল guacd ইনস্ট্যান্সটি ওভাররাইড করুন। বিশ্বব্যাপী কনফিগার করা guacd ব্যবহার করতে এটি খালি রাখুন।", - "gatewayHostname": "গেটওয়ে হোস্টনাম", - "gatewayPort": "গেটওয়ে পোর্ট", - "gatewayUsername": "গেটওয়ে ব্যবহারকারীর নাম", - "gatewayPassword": "গেটওয়ে পাসওয়ার্ড", - "gatewayDomain": "গেটওয়ে ডোমেইন", - "remoteAppProgram": "রিমোটঅ্যাপ প্রোগ্রাম", - "workingDirectory": "কর্ম ডিরেক্টরি", - "arguments": "যুক্তি", - "normalizeLineEndings": "লাইনের শেষাংশ স্বাভাবিক করুন", - "macAddress": "MAC ঠিকানা", - "broadcastAddress": "সম্প্রচার ঠিকানা", - "udpPort": "ইউডিপি পোর্ট", - "waitTimeS": "অপেক্ষার সময় (সেকেন্ড)", - "driveName": "ড্রাইভের নাম", - "drivePath": "ড্রাইভ পাথ", - "ignoreCertificate": "সার্টিফিকেট উপেক্ষা করুন", - "ignoreCertificateDesc": "সেলফ-সাইনড সার্টিফিকেটযুক্ত হোস্টগুলিতে সংযোগের অনুমতি দিন", - "forceLossless": "বল ক্ষয়হীন", - "forceLosslessDesc": "লসলেস ইমেজ এনকোডিং বাধ্যতামূলক করুন (উচ্চতর গুণমান, বেশি ব্যান্ডউইথ)", - "disableAudio": "অডিও নিষ্ক্রিয় করুন", - "disableAudioDesc": "রিমোট সেশন থেকে সমস্ত অডিও মিউট করুন", - "enableAudioInput": "অডিও ইনপুট (মাইক্রোফোন) সক্রিয় করুন", - "enableAudioInputDesc": "স্থানীয় মাইক্রোফোনটি রিমোট সেশনে ফরোয়ার্ড করুন", - "wallpaper": "ওয়ালপেপার", - "wallpaperDesc": "ডেস্কটপ ওয়ালপেপার দেখান (এটি বন্ধ করলে পারফরম্যান্স উন্নত হয়)", - "theming": "থিমিং", - "themingDesc": "ভিজ্যুয়াল থিম এবং স্টাইল সক্রিয় করুন", - "fontSmoothing": "ফন্ট মসৃণকরণ", - "fontSmoothingDesc": "ক্লিয়ারটাইপ ফন্ট রেন্ডারিং সক্ষম করুন", - "fullWindowDrag": "সম্পূর্ণ উইন্ডো ড্র্যাগ", - "fullWindowDragDesc": "ড্র্যাগ করার সময় উইন্ডোর বিষয়বস্তু দেখান", - "desktopComposition": "ডেস্কটপ কম্পোজিশন", - "desktopCompositionDesc": "এরো গ্লাস এফেক্টস সক্রিয় করুন", - "menuAnimations": "মেনু অ্যানিমেশন", - "menuAnimationsDesc": "মেনু ফেড এবং স্লাইড অ্যানিমেশন সক্রিয় করুন", - "disableBitmapCaching": "বিটম্যাপ ক্যাশিং নিষ্ক্রিয় করুন", - "disableBitmapCachingDesc": "বিটম্যাপ ক্যাশে বন্ধ করুন (এটি গ্লিচ কমাতে সাহায্য করতে পারে)।", - "disableOffscreenCaching": "অফস্ক্রিন ক্যাশিং নিষ্ক্রিয় করুন", - "disableOffscreenCachingDesc": "অফস্ক্রিন ক্যাশে বন্ধ করুন", - "disableGlyphCaching": "গ্লিফ ক্যাশিং নিষ্ক্রিয় করুন", - "disableGlyphCachingDesc": "গ্লিফ ক্যাশে বন্ধ করুন", - "enableGfx": "GFX সক্ষম করুন", - "enableGfxDesc": "RemoteFX গ্রাফিক্স পাইপলাইন ব্যবহার করুন", - "enablePrinting": "প্রিন্টিং সক্ষম করুন", - "enablePrintingDesc": "স্থানীয় প্রিন্টারগুলিকে রিমোট সেশনে পুনঃনির্দেশিত করুন", - "enableDriveRedirection": "ড্রাইভ পুনঃনির্দেশনা সক্ষম করুন", - "enableDriveRedirectionDesc": "রিমোট সেশনে একটি স্থানীয় ফোল্ডারকে ড্রাইভ হিসেবে ম্যাপ করুন।", - "createDrivePath": "ড্রাইভ পাথ তৈরি করুন", - "createDrivePathDesc": "ফোল্ডারটি বিদ্যমান না থাকলে স্বয়ংক্রিয়ভাবে তৈরি করুন।", - "disableDownload": "ডাউনলোড নিষ্ক্রিয় করুন", - "disableDownloadDesc": "রিমোট সেশন থেকে ফাইল ডাউনলোড প্রতিরোধ করুন", - "disableUpload": "আপলোড নিষ্ক্রিয় করুন", - "disableUploadDesc": "রিমোট সেশনে ফাইল আপলোড করা প্রতিরোধ করুন", - "enableTouch": "টাচ সক্ষম করুন", - "enableTouchDesc": "টাচ ইনপুট ফরওয়ার্ডিং সক্ষম করুন", - "consoleSession": "কনসোল সেশন", - "consoleSessionDesc": "নতুন সেশন শুরু করার পরিবর্তে কনসোলে (সেশন ০) সংযোগ করুন।", - "sendWolPacket": "WOL প্যাকেট পাঠান", - "sendWolPacketDesc": "সংযোগ করার আগে এই হোস্টকে জাগিয়ে তুলতে একটি ম্যাজিক প্যাকেট পাঠান।", - "disableCopy": "কপি নিষ্ক্রিয় করুন", - "disableCopyDesc": "রিমোট সেশন থেকে টেক্সট কপি করা প্রতিরোধ করুন", - "disablePaste": "পেস্ট নিষ্ক্রিয় করুন", - "disablePasteDesc": "রিমোট সেশনে টেক্সট পেস্ট করা প্রতিরোধ করুন", - "createPathIfMissing": "অনুপস্থিত থাকলে পাথ তৈরি করুন", - "createPathIfMissingDesc": "স্বয়ংক্রিয়ভাবে রেকর্ডিং ডিরেক্টরি তৈরি করুন", - "excludeOutput": "আউটপুট বাদ দিন", - "excludeOutputDesc": "স্ক্রিন আউটপুট রেকর্ড করবেন না (শুধুমাত্র মেটাডেটা)", - "excludeMouse": "মাউস বাদ দিন", - "excludeMouseDesc": "মাউসের নড়াচড়া রেকর্ড করবেন না।", - "includeKeystrokes": "কীস্ট্রোক অন্তর্ভুক্ত করুন", - "includeKeystrokesDesc": "স্ক্রিন আউটপুটের পাশাপাশি সরাসরি কীস্ট্রোক রেকর্ড করুন।", - "vncPort": "ভিএনসি পোর্ট", - "vncPassword": "VNC পাসওয়ার্ড", - "vncUsernameOptional": "ব্যবহারকারীর নাম (ঐচ্ছিক)", - "vncLeaveBlank": "প্রয়োজন না হলে খালি রাখুন", - "cursorMode": "কার্সার মোড", - "swapRedBlue": "লাল/নীল অদলবদল করুন", - "swapRedBlueDesc": "লাল এবং নীল রঙের চ্যানেলগুলি অদলবদল করুন (কিছু রঙের সমস্যা সমাধান করে)", - "readOnly": "শুধুমাত্র পঠনযোগ্য", - "readOnlyDesc": "কোনো ইনপুট না পাঠিয়ে রিমোট স্ক্রিন দেখুন।", - "telnetPort": "টেলনেট পোর্ট", - "terminalType": "টার্মিনাল টাইপ", - "fontName": "ফন্টের নাম", - "fontSize": "ফন্ট সাইজ", - "colorScheme": "রঙের পরিকল্পনা", - "backspaceKey": "ব্যাকস্পেস কী", - "colorDepth": "রঙের গভীরতা", - "resizeMethod": "আকার পরিবর্তন পদ্ধতি", - "driveNamePlaceholder": "টারমিক্স ড্রাইভ", - "options": { - "any": "যেকোনো", - "auto": "অটো", - "nlaExtended": "এনএলএ বর্ধিত", - "color8": "৮-বিট", - "color16": "১৬-বিট", - "color24": "২৪-বিট", - "color32": "৩২-বিট", - "displayUpdate": "ডিসপ্লে আপডেট", - "reconnect": "পুনরায় সংযোগ করুন", - "preserve": "সংরক্ষণ করুন", - "unix": "ইউনিক্স (এলএফ)", - "windows": "উইন্ডোজ (CRLF)", - "local": "স্থানীয়", - "remote": "দূরবর্তী", - "failsafe": "ব্যর্থতারোধী", - "blackOnWhite": "সাদার উপর কালো", - "whiteOnBlack": "কালোর উপর সাদা", - "grayOnBlack": "কালোর উপর ধূসর", - "greenOnBlack": "কালোর উপর সবুজ" - } - }, - "remoteDesktop": "রিমোট ডেস্কটপ", - "tabRdp": "আরডিপি", - "tabVnc": "ভিএনসি", - "tabTelnet": "টেলনেট", - "virtualNetwork": "ভার্চুয়াল নেটওয়ার্ক", - "unencryptedShell": "এনক্রিপ্টবিহীন শেল", - "remoteDesktopNotes": "নোট", - "userDefaultsNote": "আপনি যে সেটিংস অপরিবর্তিত রাখবেন, তা আপনার হোস্টের ডিফল্ট অনুযায়ী নির্ধারিত হবে।", - "connectionOriginNote": "ডেস্কটপ অ্যাপে, RDP, VNC এবং Telnet রিমোট সার্ভার ব্যবহার করে, যদি না হোস্টটি 'এই ডিভাইস' (This device) হিসেবে সেট করা থাকে। এর জন্য এমন একটি guacd প্রয়োজন, যা এই মেশিনের নাগালের মধ্যে থাকে। রিমোট ডেস্কটপ সেটিংসে অথবা প্রতিটি হোস্টের জন্য guacd প্রক্সির অধীনে এটি সেট করুন।", - "docsLink": "নথি দেখুন", - "connectionOriginGuacamoleNote": "RDP, VNC এবং Telnet রিমোট সার্ভার ব্যবহার করে, যদি না আপনি \"এই ডিভাইস\" বিকল্পটি বেছে নেন, যার জন্য এমন একটি guacd প্রয়োজন যা এই মেশিনের নাগালের মধ্যে থাকে। guacd URL সেটিংসে, অথবা প্রতিটি হোস্টের জন্য guacd Proxy-এর অধীনে এটি সেট করুন।" - }, - "onboarding": { - "feature_desktop": "রিমোট ডেস্কটপ", - "feature_desktop_desc": "SSH-এর মতোই একটি ট্যাবে RDP, VNC এবং Telnet সেশন খুলুন।" - }, - "settings": { - "user": { - "colorDepth": { - "label": "রঙের গভীরতা", - "description": "যেসব RDP হোস্ট তাদের ডিফল্ট সেটিংসে রাখে, তাদের জন্য ব্যবহৃত হয়।" - }, - "resizeMethod": { - "label": "আকার পরিবর্তন পদ্ধতি", - "description": "রিমোট স্ক্রিনটি কীভাবে উইন্ডোর আকার অনুসরণ করে" - }, - "resizeDisplayUpdate": "ডিসপ্লে আপডেট", - "resizeReconnect": "পুনরায় সংযোগ করুন", - "inherit": "উত্তরাধিকার সূত্রে", - "on": "চালু", - "off": "বন্ধ", - "forceLossless": { - "label": "বল ক্ষয়হীন", - "description": "লসলেস ইমেজ এনকোডিং বাধ্যতামূলক করুন (উচ্চতর গুণমান, বেশি ব্যান্ডউইথ)" - }, - "enableWallpaper": { - "label": "ওয়ালপেপার", - "description": "ডেস্কটপ ওয়ালপেপার দেখান (এটি বন্ধ করলে পারফরম্যান্স উন্নত হয়)" - }, - "enableFontSmoothing": { - "label": "ফন্ট মসৃণকরণ", - "description": "ক্লিয়ারটাইপ ফন্ট রেন্ডারিং সক্ষম করুন" - }, - "enableDesktopComposition": { - "label": "ডেস্কটপ কম্পোজিশন", - "description": "এরো গ্লাস এফেক্টস সক্রিয় করুন" - }, - "disableAudio": { - "label": "অডিও নিষ্ক্রিয় করুন", - "description": "রিমোট সেশন থেকে সমস্ত অডিও মিউট করুন" - }, - "enablePrinting": { - "label": "প্রিন্টিং সক্ষম করুন", - "description": "স্থানীয় প্রিন্টারগুলিকে রিমোট সেশনে পুনঃনির্দেশিত করুন" - }, - "enableDrive": { - "label": "ড্রাইভ পুনঃনির্দেশনা সক্ষম করুন", - "description": "রিমোট সেশনে একটি স্থানীয় ফোল্ডারকে ড্রাইভ হিসেবে ম্যাপ করুন।" - }, - "disableCopy": { - "label": "কপি নিষ্ক্রিয় করুন", - "description": "রিমোট সেশন থেকে টেক্সট কপি করা প্রতিরোধ করুন" - }, - "disablePaste": { - "label": "পেস্ট নিষ্ক্রিয় করুন", - "description": "রিমোট সেশনে টেক্সট পেস্ট করা প্রতিরোধ করুন" - }, - "group": "রিমোট ডেস্কটপ ডিফল্ট", - "colorDepth16": "১৬-বিট", - "colorDepth24": "২৪-বিট", - "colorDepth32": "৩২-বিট" - }, - "admin": { - "enabled": { - "label": "রিমোট ডেস্কটপ সক্ষম করুন", - "description": "guacd-এর মাধ্যমে RDP, VNC এবং Telnet সংযোগের অনুমতি দিন।" - }, - "guacdUrl": { - "label": "guacd URL", - "description": "guacd কোথায় শোনে, তা হোস্ট:পোর্ট হিসেবে নির্ধারিত হয়। GUACD_URL, GUACD_HOST এবং GUACD_PORT এনভায়রনমেন্ট ভেরিয়েবলগুলো এটিকে ওভাররাইড করে।", - "placeholder": "গুয়াকডি:৪৮২২" - } - }, - "host": { - "enableRdp": { - "label": "আরডিপি" - }, - "enableVnc": { - "label": "ভিএনসি" - }, - "enableTelnet": { - "label": "টেলনেট" - }, - "rdpPort": { - "label": "আরডিপি পোর্ট" - }, - "vncPort": { - "label": "VNC পোর্ট" - }, - "telnetPort": { - "label": "টেলনেট পোর্ট" - }, - "rdpSecurity": { - "label": "আরডিপি নিরাপত্তা মোড" - }, - "rdpIgnoreCert": { - "label": "সার্টিফিকেটের ত্রুটি উপেক্ষা করুন" - }, - "guacamoleConfig": { - "label": "guacd সেটিংস" - }, - "enableToolbar": { - "label": "রিমোট ডেস্কটপ টুলবার", - "description": "RDP, VNC এবং Telnet সেশনে টুলবারটি দেখান।" - }, - "displayGroup": "প্রদর্শন", - "displayDefaultsTab": "রিমোট ডেস্কটপ" - } - }, - "palette": { - "connectRdp": "RDP-এর সাথে সংযোগ করুন", - "connectVnc": "VNC দিয়ে সংযোগ করুন", - "connectTelnet": "টেলনেটের সাথে সংযোগ করুন" - }, - "permissions": { - "sessions": { - "title": "রিমোট ডেস্কটপ সেশন শেয়ার করুন", - "description": "সেশন শেয়ারিং এবং কোলাব রুমগুলোকে এই ব্যবহারকারীর RDP, VNC এবং Telnet সেশনগুলো অন্যদের কাছে দেখানোর অনুমতি দিন।" - } - }, - "errors": { - "remoteServerRequired": "রিমোট সার্ভার প্রয়োজন। এই সংযোগের ধরণটি ব্যবহার করতে সেটিংস-এ একটি রিমোট সার্ভার সংযুক্ত করুন।" - }, - "plugin": { - "name": "রিমোট ডেস্কটপ" - } -} diff --git a/plugins/remote-desktop/locales/translated/ca_ES.json b/plugins/remote-desktop/locales/translated/ca_ES.json deleted file mode 100644 index 4812f9c2d..000000000 --- a/plugins/remote-desktop/locales/translated/ca_ES.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Connectant a la sessió {{type}}...", - "checkingGuacd": "S'està comprovant el servei d'escriptori remot...", - "requestingToken": "Sol·licitant el testimoni de connexió {{type}}...", - "openingSession": "Obrint sessió {{type}}...", - "negotiating": "Negociant una {{type}} encaixada de mans...", - "sessionReady": "{{type}} sessió a punt", - "connectionError": "Error de connexió", - "connectionFailed": "La connexió ha fallat", - "failedToConnect": "No s'ha pogut obtenir el testimoni de connexió", - "hostNotFound": "No s'ha trobat l'amfitrió", - "noHostSelected": "No s'ha seleccionat cap amfitrió", - "reconnect": "Reconnecta", - "retry": "Torna-ho a intentar", - "guacdUnavailable": "El servei d'escriptori remot (guacd) no està disponible. Assegureu-vos que guacd estigui en execució, sigui accessible i estigui configurat correctament a la configuració d'administrador.", - "credentialPromptTitle": "Introduïu les credencials RDP", - "credentialPromptDescription": "Aquest amfitrió està configurat per demanar credencials en connectar-se. Només s'utilitzen per a aquesta sessió i no es desen.", - "connect": "Connecta", - "ctrlAltDel": "Ctrl+Alt+Supr", - "files": { - "title": "Fitxers de Drive", - "parent": "Directori principal", - "refresh": "Actualitza", - "empty": "Aquesta carpeta està buida", - "listFailed": "No s'ha pogut llegir aquesta carpeta", - "download": "Descarrega", - "downloadFailed": "No s'ha pogut descarregar {{name}}", - "upload": "Puja fitxers", - "uploading": "S'està carregant {{name}}...", - "uploaded": "Pujat {{name}}", - "uploadFailed": "No s'ha pogut carregar {{name}}", - "uploadDisabled": "Les càrregues estan desactivades per a aquesta connexió", - "driveUnavailable": "Activa la redirecció de la unitat RDP abans de carregar fitxers", - "driveNotWritable": "guacd no pot escriure a la carpeta de la unitat. Comproveu GUACD_DRIVE_PATH (o la ruta de la unitat de l'amfitrió) i els seus permisos.", - "dropToUpload": "Deixa anar els fitxers per pujar-los" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Supr", - "winL": "Win+L (Pantalla de bloqueig)", - "winKey": "Clau de Windows", - "superL": "Super+L (Pantalla de bloqueig)", - "superKey": "Superclau", - "super": "Súper", - "cmdL": "Cmd+L (Pantalla de bloqueig)", - "cmdKey": "Tecla de comandament", - "cmd": "Cmd", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Majúscules", - "win": "Guanyar", - "stickyActive": "{{key}} (bloquejat - feu clic per alliberar)", - "stickyInactive": "{{key}} (feu clic per bloquejar)", - "esc": "Escapada", - "tab": "Pestanya", - "home": "Inici", - "end": "Final", - "pageUp": "Pàgina amunt", - "pageDown": "Pàgina avall", - "arrowUp": "Fletxa amunt", - "arrowDown": "Fletxa avall", - "arrowLeft": "Fletxa esquerra", - "arrowRight": "Fletxa dreta", - "fnToggle": "Tecles de funció", - "reconnect": "Sessió de reconnexió", - "collapse": "Replega la barra d'eines", - "expand": "Expandeix la barra d'eines", - "dragHandle": "Arrossega per reposicionar", - "hide": "Amaga-ho per a aquesta sessió. Per desactivar-ho permanentment, obriu Configuració de l'amfitrió → General → Barra d'eines de connexió.", - "zoomIn": "Amplia la imatge", - "zoomOut": "Allunya", - "resetZoom": "Restableix el zoom", - "fitToScreen": "Ajusta a la pantalla", - "switchToTrackpad": "Canvia al mode trackpad (arrossega per moure el cursor, toca per fer clic)", - "switchToTouch": "Canvia al mode tàctil (toca directament on vols fer clic)" - }, - "disabled": "L'escriptori remot està desactivat. Un administrador el pot activar a Configuració." - }, - "hosts": { - "nativeRdpOpened": "Obert a l'escriptori remot de Windows", - "nativeRdpFailed": "No s'ha pogut obrir l'escriptori remot de Windows", - "openNativeRdp": "Obre a l'escriptori remot de Windows", - "guac": { - "toolbar": "Barra d'eines", - "connection": "Connexió", - "authentication": "Autenticació", - "storedCredential": "Credencial emmagatzemada", - "authMethod": "Mètode d'autenticació", - "authTypeDirect": "Directe", - "authTypeCredential": "Credencial", - "authTypeNone": "Cap", - "authTypeNoneDesc": "No s'emmagatzemen les credencials. Se us demanarà un nom d'usuari i una contrasenya cada vegada que us connecteu; no es desen.", - "selectCredential": "Selecciona una credencial...", - "connectionSettings": "Configuració de connexió", - "displaySettings": "Configuració de la pantalla", - "audioSettings": "Configuració d'àudio", - "rdpPerformance": "Rendiment de l'RDP", - "deviceRedirection": "Redirecció de dispositius", - "session": "Sessió", - "gateway": "Porta d'entrada", - "remoteApp": "Aplicació remota", - "clipboard": "Porta-retalls", - "sessionRecording": "Gravació de sessions", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Configuració de VNC", - "terminalSettings": "Configuració del terminal", - "rdpPort": "Port RDP", - "username": "Nom d'usuari", - "password": "Contrasenya", - "passwordSaved": "Contrasenya desada, escriu per canviar-la", - "domain": "Domini", - "securityMode": "Mode de seguretat", - "width": "Amplada", - "height": "Alçada", - "dpi": "DPI", - "clientName": "Nom del client", - "initialProgram": "Programa inicial", - "serverLayout": "Disseny del servidor", - "timezone": "Zona horària", - "loadBalanceInfo": "Informació de balanç de càrrega / Galeta", - "loadBalanceInfoDesc": "Galeta del broker de connexió RD per a l'equilibri de càrrega de granges RDS (per exemple, tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Proxy guacd", - "guacdHostname": "Host guacd", - "guacdHostnamePlaceholder": "Predeterminat global", - "guacdPort": "Port de Guacd", - "guacdProxyDesc": "Substitueix la instància global de guacd per a aquesta connexió. Deixa-ho en blanc per utilitzar el guacd configurat globalment.", - "gatewayHostname": "Nom de l'amfitrió de la passarel·la", - "gatewayPort": "Port de passarel·la", - "gatewayUsername": "Nom d'usuari de la passarel·la", - "gatewayPassword": "Contrasenya de la passarel·la", - "gatewayDomain": "Domini de passarel·la", - "remoteAppProgram": "Programa RemoteApp", - "workingDirectory": "Directori de treball", - "arguments": "Arguments", - "normalizeLineEndings": "Normalitzar els finals de línia", - "macAddress": "Adreça MAC", - "broadcastAddress": "Adreça de difusió", - "udpPort": "Port UDP", - "waitTimeS": "Temps d'espera (s)", - "driveName": "Nom de la unitat", - "drivePath": "Camí de conducció", - "ignoreCertificate": "Ignora el certificat", - "ignoreCertificateDesc": "Permet connexions a amfitrions amb certificats autosignats", - "forceLossless": "Força sense pèrdues", - "forceLosslessDesc": "Força la codificació d'imatges sense pèrdues (més qualitat, més amplada de banda)", - "disableAudio": "Desactiva l'àudio", - "disableAudioDesc": "Silencia tot l'àudio de la sessió remota", - "enableAudioInput": "Activa l'entrada d'àudio (micròfon)", - "enableAudioInputDesc": "Reenvia el micròfon local a la sessió remota", - "wallpaper": "Fons de pantalla", - "wallpaperDesc": "Mostra el fons de pantalla de l'escriptori (desactivar-lo millora el rendiment)", - "theming": "Tematització", - "themingDesc": "Habilita els temes i estils visuals", - "fontSmoothing": "Suavitzat de fonts", - "fontSmoothingDesc": "Activa la renderització de fonts ClearType", - "fullWindowDrag": "Arrossegar per tota la finestra", - "fullWindowDragDesc": "Mostra el contingut de la finestra mentre arrossegues", - "desktopComposition": "Composició d'escriptori", - "desktopCompositionDesc": "Activa els efectes Aero Glass", - "menuAnimations": "Animacions de menú", - "menuAnimationsDesc": "Activa les animacions de diapositives i esvaïment del menú", - "disableBitmapCaching": "Desactiva la memòria cau de mapes de bits", - "disableBitmapCachingDesc": "Desactiva la memòria cau de mapes de bits (pot ajudar amb errors)", - "disableOffscreenCaching": "Desactiva la memòria cau fora de pantalla", - "disableOffscreenCachingDesc": "Desactiva la memòria cau fora de pantalla", - "disableGlyphCaching": "Desactiva la memòria cau de glifs", - "disableGlyphCachingDesc": "Desactiva la memòria cau de glifos", - "enableGfx": "Activa els efectes especials", - "enableGfxDesc": "Utilitza el canal de gràfics RemoteFX", - "enablePrinting": "Habilita la impressió", - "enablePrintingDesc": "Redirigir les impressores locals a la sessió remota", - "enableDriveRedirection": "Activa la redirecció de la unitat", - "enableDriveRedirectionDesc": "Assignar una carpeta local com a unitat a la sessió remota", - "createDrivePath": "Crea una ruta de conducció", - "createDrivePathDesc": "Crea automàticament la carpeta si no existeix", - "disableDownload": "Desactiva la descàrrega", - "disableDownloadDesc": "Impedir la descàrrega de fitxers des de la sessió remota", - "disableUpload": "Desactiva la càrrega", - "disableUploadDesc": "Impedir la càrrega de fitxers a la sessió remota", - "enableTouch": "Activa el tacte", - "enableTouchDesc": "Activa el reenviament d'entrada tàctil", - "consoleSession": "Sessió de consola", - "consoleSessionDesc": "Connecta't a la consola (sessió 0) en comptes d'una nova sessió", - "sendWolPacket": "Enviar paquet WOL", - "sendWolPacketDesc": "Envia un paquet màgic per despertar aquest host abans de connectar-se", - "disableCopy": "Desactiva la còpia", - "disableCopyDesc": "Impedir la còpia de text des de la sessió remota", - "disablePaste": "Desactiva l'enganxament", - "disablePasteDesc": "Impedir enganxar text a la sessió remota", - "createPathIfMissing": "Crea un camí si falta", - "createPathIfMissingDesc": "Crea automàticament el directori d'enregistrament", - "excludeOutput": "Exclou la sortida", - "excludeOutputDesc": "No enregistrar la sortida de pantalla (només metadades)", - "excludeMouse": "Exclou el ratolí", - "excludeMouseDesc": "No enregistreu els moviments del ratolí", - "includeKeystrokes": "Inclou les pulsacions de tecles", - "includeKeystrokesDesc": "Enregistra les pulsacions de tecles en brut a més de la sortida de la pantalla", - "vncPort": "Port VNC", - "vncPassword": "Contrasenya de VNC", - "vncUsernameOptional": "Nom d'usuari (opcional)", - "vncLeaveBlank": "Deixeu-ho en blanc si no és necessari", - "cursorMode": "Mode del cursor", - "swapRedBlue": "Intercanvia vermell/blau", - "swapRedBlueDesc": "Intercanvia els canals de color vermell i blau (corregeix alguns problemes de color)", - "readOnly": "Només lectura", - "readOnlyDesc": "Visualitza la pantalla remota sense enviar cap entrada", - "telnetPort": "Port Telnet", - "terminalType": "Tipus de terminal", - "fontName": "Nom de la font", - "fontSize": "Mida de la lletra", - "colorScheme": "Esquema de colors", - "backspaceKey": "Tecla Retrocés", - "colorDepth": "Profunditat de color", - "resizeMethod": "Mètode de redimensionament", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Qualsevol", - "auto": "Automàtic", - "nlaExtended": "NLA ampliat", - "color8": "8 bits", - "color16": "16 bits", - "color24": "24 bits", - "color32": "32 bits", - "displayUpdate": "Actualització de la pantalla", - "reconnect": "Reconnecta", - "preserve": "Preservar", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Local", - "remote": "Remot", - "failsafe": "a prova de fallades", - "blackOnWhite": "Negre sobre blanc", - "whiteOnBlack": "Blanc sobre negre", - "grayOnBlack": "Gris sobre negre", - "greenOnBlack": "Verd sobre negre" - } - }, - "remoteDesktop": "Escriptori remot", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Xarxa virtual", - "unencryptedShell": "Shell sense xifrar", - "remoteDesktopNotes": "Notes", - "userDefaultsNote": "La configuració que deixeu sense canvis segueix els valors predeterminats de l'amfitrió.", - "connectionOriginNote": "A l'aplicació d'escriptori, RDP, VNC i Telnet utilitzen el servidor remot tret que l'amfitrió estigui configurat com a Aquest dispositiu, que necessita un guacd al qual pugui arribar aquesta màquina. Configureu-ne un a la configuració de l'escriptori remot o per amfitrió a guacd Proxy.", - "docsLink": "Veure documents", - "connectionOriginGuacamoleNote": "RDP, VNC i Telnet utilitzen el servidor remot tret que seleccioneu \"Aquest dispositiu\", que necessita un guacd al qual aquesta màquina pugui arribar. Definiu-ne un a la configuració de l'URL guacd o per host a guacd Proxy." - }, - "onboarding": { - "feature_desktop": "Escriptori remot", - "feature_desktop_desc": "Obriu sessions RDP, VNC i Telnet en una pestanya, igual que SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Profunditat de color", - "description": "S'utilitza per a hosts RDP que el deixen per defecte." - }, - "resizeMethod": { - "label": "Mètode de redimensionament", - "description": "Com la pantalla remota segueix la mida de la finestra." - }, - "resizeDisplayUpdate": "Actualització de la pantalla", - "resizeReconnect": "Reconnecta", - "inherit": "Heretar", - "on": "Activat", - "off": "Desactivat", - "forceLossless": { - "label": "Força sense pèrdues", - "description": "Força la codificació d'imatges sense pèrdues (més qualitat, més amplada de banda)" - }, - "enableWallpaper": { - "label": "Fons de pantalla", - "description": "Mostra el fons de pantalla de l'escriptori (desactivar-lo millora el rendiment)" - }, - "enableFontSmoothing": { - "label": "Suavitzat de fonts", - "description": "Activa la renderització de fonts ClearType" - }, - "enableDesktopComposition": { - "label": "Composició d'escriptori", - "description": "Activa els efectes Aero Glass" - }, - "disableAudio": { - "label": "Desactiva l'àudio", - "description": "Silencia tot l'àudio de la sessió remota" - }, - "enablePrinting": { - "label": "Habilita la impressió", - "description": "Redirigir les impressores locals a la sessió remota" - }, - "enableDrive": { - "label": "Activa la redirecció de la unitat", - "description": "Assignar una carpeta local com a unitat a la sessió remota" - }, - "disableCopy": { - "label": "Desactiva la còpia", - "description": "Impedir la còpia de text des de la sessió remota" - }, - "disablePaste": { - "label": "Desactiva l'enganxament", - "description": "Impedir enganxar text a la sessió remota" - }, - "group": "Valors predeterminats d'escriptori remot", - "colorDepth16": "16 bits", - "colorDepth24": "24 bits", - "colorDepth32": "32 bits" - }, - "admin": { - "enabled": { - "label": "Habilita l'escriptori remot", - "description": "Permet connexions RDP, VNC i Telnet a través de guacd." - }, - "guacdUrl": { - "label": "URL de guacd", - "description": "On guacd escolta, com a host:port. Les variables d'entorn GUACD_URL, GUACD_HOST i GUACD_PORT ho anul·len.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "Port RDP" - }, - "vncPort": { - "label": "Port VNC" - }, - "telnetPort": { - "label": "Port Telnet" - }, - "rdpSecurity": { - "label": "Mode de seguretat RDP" - }, - "rdpIgnoreCert": { - "label": "Ignora els errors de certificat" - }, - "guacamoleConfig": { - "label": "configuració de guacd" - }, - "enableToolbar": { - "label": "Barra d'eines d'escriptori remot", - "description": "Mostra la barra d'eines a les sessions RDP, VNC i Telnet." - }, - "displayGroup": "Pantalla", - "displayDefaultsTab": "Escriptori remot" - } - }, - "palette": { - "connectRdp": "Connecta't amb RDP", - "connectVnc": "Connecta amb VNC", - "connectTelnet": "Connecta amb Telnet" - }, - "permissions": { - "sessions": { - "title": "Compartir sessions d'escriptori remot", - "description": "Permet que les sales de compartició de sessions i col·laboració mostrin les sessions RDP, VNC i Telnet d'aquest usuari a altres persones." - } - }, - "errors": { - "remoteServerRequired": "Cal un servidor remot. Connecteu un servidor remot a Configuració per utilitzar aquest tipus de connexió." - }, - "plugin": { - "name": "Escriptori remot" - } -} diff --git a/plugins/remote-desktop/locales/translated/cs_CZ.json b/plugins/remote-desktop/locales/translated/cs_CZ.json deleted file mode 100644 index 96e0f714b..000000000 --- a/plugins/remote-desktop/locales/translated/cs_CZ.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Připojování k relaci {{type}}...", - "checkingGuacd": "Kontrola služby vzdálené plochy...", - "requestingToken": "Žádost o token připojení {{type}}...", - "openingSession": "Otevírání {{type}} relace...", - "negotiating": "Vyjednávání {{type}} podání ruky...", - "sessionReady": "{{type}} relace připravena", - "connectionError": "Chyba připojení", - "connectionFailed": "Připojení se nezdařilo", - "failedToConnect": "Nepodařilo se získat token připojení", - "hostNotFound": "Hostitel nenalezen", - "noHostSelected": "Nebyl vybrán žádný hostitel", - "reconnect": "Znovu se připojte", - "retry": "Zkusit znovu", - "guacdUnavailable": "Služba vzdálené plochy (guacd) není k dispozici. Ujistěte se prosím, že je guacd spuštěná, přístupná a správně nakonfigurovaná v nastavení administrátora.", - "credentialPromptTitle": "Zadejte přihlašovací údaje RDP", - "credentialPromptDescription": "Tento hostitel je nastaven tak, aby při připojení vyžadoval přihlašovací údaje. Tyto přihlašovací údaje se používají pouze pro tuto relaci a neukládají se.", - "connect": "Připojit", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Soubory na Disku", - "parent": "Nadřazený adresář", - "refresh": "Obnovit", - "empty": "Tato složka je prázdná", - "listFailed": "Tuto složku se nepodařilo přečíst.", - "download": "Stáhnout", - "downloadFailed": "Stažení se nezdařilo {{name}}", - "upload": "Nahrát soubory", - "uploading": "Nahrávání {{name}}...", - "uploaded": "Nahráno {{name}}", - "uploadFailed": "Nahrávání se nezdařilo {{name}}", - "uploadDisabled": "Nahrávání je pro toto připojení zakázáno.", - "driveUnavailable": "Před nahráváním souborů povolte přesměrování disku RDP", - "driveNotWritable": "Příkaz guacd nemůže zapisovat do složky na disku. Zkontrolujte cestu k disku GUACD_DRIVE_PATH (nebo cestu k disku hostitele) a její oprávnění.", - "dropToUpload": "Přetáhněte soubory k nahrání" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Zamykání obrazovky)", - "winKey": "Klávesa Windows", - "superL": "Super+L (Zamykací obrazovka)", - "superKey": "Super klíč", - "super": "Super", - "cmdL": "Cmd+L (Zamykací obrazovka)", - "cmdKey": "Příkazový klíč", - "cmd": "Příkaz", - "ctrl": "Ctrl", - "alt": "Alternativní", - "shift": "Posun", - "win": "Vyhrát", - "stickyActive": "{{key}} (zajištěno - kliknutím uvolníte)", - "stickyInactive": "{{key}} (kliknutím zajistíte)", - "esc": "Uniknout", - "tab": "Záložka", - "home": "Domov", - "end": "Konec", - "pageUp": "Stránka nahoru", - "pageDown": "Stránka dolů", - "arrowUp": "Šipka nahoru", - "arrowDown": "Šipka dolů", - "arrowLeft": "Šipka vlevo", - "arrowRight": "Šipka vpravo", - "fnToggle": "Funkční klávesy", - "reconnect": "Znovu připojit relaci", - "collapse": "Sbalit panel nástrojů", - "expand": "Rozbalit panel nástrojů", - "dragHandle": "Přetažením změníte pozici", - "hide": "Skrýt pro tuto relaci. Chcete-li jej trvale zakázat, otevřete Nastavení hostitele → Obecné → Panel nástrojů pro připojení.", - "zoomIn": "Přiblížit", - "zoomOut": "Oddálit", - "resetZoom": "Obnovit zoom", - "fitToScreen": "Přizpůsobit obrazovce", - "switchToTrackpad": "Přepnout do režimu trackpadu (tažením přesunete kurzor, klepnutím kliknete)", - "switchToTouch": "Přepnout do dotykového režimu (klepněte přímo na místo, kam chcete kliknout)" - }, - "disabled": "Vzdálená plocha je vypnutá. Správce ji může zapnout v Nastavení." - }, - "hosts": { - "nativeRdpOpened": "Otevřeno ve Vzdálené ploše systému Windows", - "nativeRdpFailed": "Nepodařilo se otevřít Vzdálenou plochu systému Windows", - "openNativeRdp": "Otevřít ve Vzdálené ploše systému Windows", - "guac": { - "toolbar": "Panel nástrojů", - "connection": "Spojení", - "authentication": "Ověřování", - "storedCredential": "Uložené přihlašovací údaje", - "authMethod": "Metoda ověřování", - "authTypeDirect": "Řídit", - "authTypeCredential": "Přihlašovací údaj", - "authTypeNone": "Žádný", - "authTypeNoneDesc": "Nejsou uloženy žádné přihlašovací údaje. Při každém připojení budete vyzváni k zadání uživatelského jména a hesla; tyto údaje se neukládají.", - "selectCredential": "Vyberte pověření...", - "connectionSettings": "Nastavení připojení", - "displaySettings": "Nastavení displeje", - "audioSettings": "Nastavení zvuku", - "rdpPerformance": "Výkon RDP", - "deviceRedirection": "Přesměrování zařízení", - "session": "Zasedání", - "gateway": "Brána", - "remoteApp": "Vzdálená aplikace", - "clipboard": "Schránka", - "sessionRecording": "Nahrávání relace", - "wakeOnLan": "Probuzení přes LAN", - "vncSettings": "Nastavení VNC", - "terminalSettings": "Nastavení terminálu", - "rdpPort": "RDP port", - "username": "Uživatelské jméno", - "password": "Heslo", - "passwordSaved": "Heslo uloženo, zadejte heslo pro změnu", - "domain": "Doména", - "securityMode": "Bezpečnostní režim", - "width": "Šířka", - "height": "Výška", - "dpi": "DPI", - "clientName": "Jméno klienta", - "initialProgram": "Počáteční program", - "serverLayout": "Rozložení serveru", - "timezone": "Časové pásmo", - "loadBalanceInfo": "Informace o vyrovnávání zátěže / soubor cookie", - "loadBalanceInfoDesc": "Soubor cookie RD Connection Broker pro vyvažování zátěže farmy RDS (např. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Proxy GUACD", - "guacdHostname": "Hostitel guacd", - "guacdHostnamePlaceholder": "Globální výchozí nastavení", - "guacdPort": "Port GUACD", - "guacdProxyDesc": "Pro toto připojení přepište globální instanci guacd. Pokud chcete použít globálně nakonfigurovaný guacd, ponechte pole prázdné.", - "gatewayHostname": "Název hostitele brány", - "gatewayPort": "Port brány", - "gatewayUsername": "Uživatelské jméno brány", - "gatewayPassword": "Heslo brány", - "gatewayDomain": "Doména brány", - "remoteAppProgram": "Program RemoteApp", - "workingDirectory": "Pracovní adresář", - "arguments": "Argumenty", - "normalizeLineEndings": "Normalizovat konce řádků", - "macAddress": "MAC adresa", - "broadcastAddress": "Vysílací adresa", - "udpPort": "UDP port", - "waitTimeS": "Čekací doba (s)", - "driveName": "Název disku", - "drivePath": "Cesta pohonu", - "ignoreCertificate": "Ignorovat certifikát", - "ignoreCertificateDesc": "Povolit připojení k hostitelům s certifikáty podepsanými držitelem", - "forceLossless": "Vynutit bezztrátové", - "forceLosslessDesc": "Vynucení bezztrátového kódování obrazu (vyšší kvalita, větší šířka pásma)", - "disableAudio": "Zakázat zvuk", - "disableAudioDesc": "Ztlumit veškerý zvuk ze vzdálené relace", - "enableAudioInput": "Povolit zvukový vstup (mikrofon)", - "enableAudioInputDesc": "Přesměrování lokálního mikrofonu do vzdálené relace", - "wallpaper": "Tapeta", - "wallpaperDesc": "Zobrazit tapetu plochy (zakázání zlepšuje výkon)", - "theming": "Témata", - "themingDesc": "Povolit vizuální motivy a styly", - "fontSmoothing": "Vyhlazování písma", - "fontSmoothingDesc": "Povolit vykreslování písma ClearType", - "fullWindowDrag": "Přetažení celého okna", - "fullWindowDragDesc": "Zobrazit obsah okna při přetahování", - "desktopComposition": "Složení plochy", - "desktopCompositionDesc": "Povolit efekty Aero Glass", - "menuAnimations": "Animace v menu", - "menuAnimationsDesc": "Povolit animace prolínání a snímání nabídek", - "disableBitmapCaching": "Zakázat ukládání bitmap do mezipaměti", - "disableBitmapCachingDesc": "Vypnout mezipaměť bitmap (může pomoci s chybami)", - "disableOffscreenCaching": "Zakázat ukládání do mezipaměti mimo obrazovku", - "disableOffscreenCachingDesc": "Vypnout mezipaměť mimo obrazovku", - "disableGlyphCaching": "Zakázat ukládání glyfů do mezipaměti", - "disableGlyphCachingDesc": "Vypnout mezipaměť glyfů", - "enableGfx": "Povolit grafické efekty", - "enableGfxDesc": "Použití grafického kanálu RemoteFX", - "enablePrinting": "Povolit tisk", - "enablePrintingDesc": "Přesměrování lokálních tiskáren na vzdálenou relaci", - "enableDriveRedirection": "Povolit přesměrování disku", - "enableDriveRedirectionDesc": "Namapování lokální složky jako jednotky ve vzdálené relaci", - "createDrivePath": "Vytvořit cestu k disku", - "createDrivePathDesc": "Automaticky vytvořit složku, pokud neexistuje", - "disableDownload": "Zakázat stahování", - "disableDownloadDesc": "Zabránit stahování souborů ze vzdálené relace", - "disableUpload": "Zakázat nahrávání", - "disableUploadDesc": "Zabránit nahrávání souborů do vzdálené relace", - "enableTouch": "Povolit dotyk", - "enableTouchDesc": "Povolit přesměrování dotykového vstupu", - "consoleSession": "Relace konzole", - "consoleSessionDesc": "Připojení ke konzoli (relace 0) namísto nové relace", - "sendWolPacket": "Odeslat paket WOL", - "sendWolPacketDesc": "Před připojením odešlete magický paket, který probudí tento hostitel.", - "disableCopy": "Zakázat kopírování", - "disableCopyDesc": "Zabránit kopírování textu ze vzdálené relace", - "disablePaste": "Zakázat vkládání", - "disablePasteDesc": "Zabránit vkládání textu do vzdálené relace", - "createPathIfMissing": "Vytvořit cestu, pokud chybí", - "createPathIfMissingDesc": "Automaticky vytvořit adresář pro nahrávání", - "excludeOutput": "Vyloučit výstup", - "excludeOutputDesc": "Nezaznamenávat výstup obrazovky (pouze metadata)", - "excludeMouse": "Vyloučit myš", - "excludeMouseDesc": "Nezaznamenávat pohyby myši", - "includeKeystrokes": "Zahrnout klávesové zkratky", - "includeKeystrokesDesc": "Zaznamenávat nezpracované stisky kláves kromě výstupu na obrazovku", - "vncPort": "VNC port", - "vncPassword": "Heslo VNC", - "vncUsernameOptional": "Uživatelské jméno (volitelné)", - "vncLeaveBlank": "Pokud není vyžadováno, nechte prázdné.", - "cursorMode": "Režim kurzoru", - "swapRedBlue": "Prohodit červenou/modrou", - "swapRedBlueDesc": "Prohoďte červený a modrý barevný kanál (opravuje některé problémy s barvami)", - "readOnly": "Pouze pro čtení", - "readOnlyDesc": "Zobrazení vzdálené obrazovky bez odesílání jakýchkoli vstupních dat", - "telnetPort": "Telnetový port", - "terminalType": "Typ terminálu", - "fontName": "Název písma", - "fontSize": "Velikost písma", - "colorScheme": "Barevné schéma", - "backspaceKey": "Klávesa Backspace", - "colorDepth": "Barevná hloubka", - "resizeMethod": "Metoda změny velikosti", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Žádný", - "auto": "Auto", - "nlaExtended": "NLA Extended", - "color8": "8bitový", - "color16": "16bitový", - "color24": "24bitový", - "color32": "32bitový", - "displayUpdate": "Aktualizace displeje", - "reconnect": "Znovu se připojte", - "preserve": "Zachovat", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Místní", - "remote": "Vzdálený", - "failsafe": "zabezpečený proti selhání", - "blackOnWhite": "Černá na bílém", - "whiteOnBlack": "Bílá na černé", - "grayOnBlack": "Šedá na černé", - "greenOnBlack": "Zelená na černém" - } - }, - "remoteDesktop": "Vzdálená plocha", - "tabRdp": "PRV", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Virtuální síť", - "unencryptedShell": "Nešifrovaný shell", - "remoteDesktopNotes": "Poznámky", - "userDefaultsNote": "Nastavení, která ponecháte beze změny, se řídí výchozími hodnotami vašeho hostitele.", - "connectionOriginNote": "V desktopové aplikaci používají RDP, VNC a Telnet vzdálený server, pokud není hostitel nastaven na Toto zařízení, které vyžaduje guacd, ke kterému má tento počítač přístup. Nastavte ho v nastavení Vzdálené plochy nebo pro každého hostitele v části guacd Proxy.", - "docsLink": "Zobrazit dokumenty", - "connectionOriginGuacamoleNote": "RDP, VNC a Telnet používají vzdálený server, pokud nevyberete možnost „Toto zařízení“, která vyžaduje GUACD, ke kterému má tento počítač přístup. Nastavte ho v nastavení URL GUACD nebo pro každého hostitele v části Proxy GUACD." - }, - "onboarding": { - "feature_desktop": "Vzdálená plocha", - "feature_desktop_desc": "Otevírejte relace RDP, VNC a Telnet na kartě, stejně jako SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Barevná hloubka", - "description": "Používá se pro hostitele RDP, kteří jej ponechávají na výchozím nastavení." - }, - "resizeMethod": { - "label": "Metoda změny velikosti", - "description": "Jak vzdálená obrazovka kopíruje velikost okna." - }, - "resizeDisplayUpdate": "Aktualizace displeje", - "resizeReconnect": "Znovu se připojte", - "inherit": "Zdědit", - "on": "Na", - "off": "Vypnuto", - "forceLossless": { - "label": "Vynutit bezztrátové", - "description": "Vynucení bezztrátového kódování obrazu (vyšší kvalita, větší šířka pásma)" - }, - "enableWallpaper": { - "label": "Tapeta", - "description": "Zobrazit tapetu plochy (zakázání zlepšuje výkon)" - }, - "enableFontSmoothing": { - "label": "Vyhlazování písma", - "description": "Povolit vykreslování písma ClearType" - }, - "enableDesktopComposition": { - "label": "Složení plochy", - "description": "Povolit efekty Aero Glass" - }, - "disableAudio": { - "label": "Zakázat zvuk", - "description": "Ztlumit veškerý zvuk ze vzdálené relace" - }, - "enablePrinting": { - "label": "Povolit tisk", - "description": "Přesměrování lokálních tiskáren na vzdálenou relaci" - }, - "enableDrive": { - "label": "Povolit přesměrování disku", - "description": "Namapování lokální složky jako jednotky ve vzdálené relaci" - }, - "disableCopy": { - "label": "Zakázat kopírování", - "description": "Zabránit kopírování textu ze vzdálené relace" - }, - "disablePaste": { - "label": "Zakázat vkládání", - "description": "Zabránit vkládání textu do vzdálené relace" - }, - "group": "Výchozí nastavení vzdálené plochy", - "colorDepth16": "16bitový", - "colorDepth24": "24bitový", - "colorDepth32": "32bitový" - }, - "admin": { - "enabled": { - "label": "Povolit vzdálenou plochu", - "description": "Povolit připojení RDP, VNC a Telnet přes guacd." - }, - "guacdUrl": { - "label": "URL GUACD", - "description": "Kde guacd naslouchá, jako host:port. Proměnné prostředí GUACD_URL, GUACD_HOST a GUACD_PORT toto přepíší.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "PRV" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP port" - }, - "vncPort": { - "label": "VNC port" - }, - "telnetPort": { - "label": "Telnetový port" - }, - "rdpSecurity": { - "label": "Režim zabezpečení RDP" - }, - "rdpIgnoreCert": { - "label": "Ignorovat chyby certifikátu" - }, - "guacamoleConfig": { - "label": "nastavení GUACD" - }, - "enableToolbar": { - "label": "Panel nástrojů Vzdálená plocha", - "description": "Zobrazit panel nástrojů v relacích RDP, VNC a Telnet." - }, - "displayGroup": "Zobrazit", - "displayDefaultsTab": "Vzdálená plocha" - } - }, - "palette": { - "connectRdp": "Připojení k RDP", - "connectVnc": "Spojte se s VNC", - "connectTelnet": "Připojení přes Telnet" - }, - "permissions": { - "sessions": { - "title": "Sdílení relací vzdálené plochy", - "description": "Umožnit sdílení relací a místnostem pro spolupráci zobrazovat relace RDP, VNC a Telnet tohoto uživatele ostatním." - } - }, - "errors": { - "remoteServerRequired": "Je vyžadován vzdálený server. Pro použití tohoto typu připojení se připojte ke vzdálenému serveru v Nastavení." - }, - "plugin": { - "name": "Vzdálená plocha" - } -} diff --git a/plugins/remote-desktop/locales/translated/da_DK.json b/plugins/remote-desktop/locales/translated/da_DK.json deleted file mode 100644 index b5340bd30..000000000 --- a/plugins/remote-desktop/locales/translated/da_DK.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Forbinder til {{type}} session...", - "checkingGuacd": "Tjekker fjernskrivebordstjeneste...", - "requestingToken": "Anmoder om {{type}} forbindelsestoken...", - "openingSession": "Åbner {{type}} session...", - "negotiating": "Forhandler {{type}} håndtryk...", - "sessionReady": "{{type}} session klar", - "connectionError": "Forbindelsesfejl", - "connectionFailed": "Forbindelsen mislykkedes", - "failedToConnect": "Kunne ikke hente forbindelsestoken", - "hostNotFound": "Værten blev ikke fundet", - "noHostSelected": "Ingen vært valgt", - "reconnect": "Genopret forbindelse", - "retry": "Prøv igen", - "guacdUnavailable": "Fjernskrivebordstjenesten (guacd) er ikke tilgængelig. Sørg for, at guacd kører, er tilgængelig og konfigureret korrekt i administratorindstillingerne.", - "credentialPromptTitle": "Indtast RDP-legitimationsoplysninger", - "credentialPromptDescription": "Denne vært er indstillet til at bede om legitimationsoplysninger ved forbindelse. De bruges kun til denne session og gemmes ikke.", - "connect": "Forbinde", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Drev-filer", - "parent": "Forældreregister", - "refresh": "Opfriske", - "empty": "Denne mappe er tom", - "listFailed": "Kunne ikke læse denne mappe", - "download": "Download", - "downloadFailed": "Kunne ikke downloade {{name}}", - "upload": "Upload filer", - "uploading": "Uploader {{name}}...", - "uploaded": "Uploadet {{name}}", - "uploadFailed": "Kunne ikke uploade {{name}}", - "uploadDisabled": "Uploads er deaktiveret for denne forbindelse", - "driveUnavailable": "Aktivér omdirigering af RDP-drev før upload af filer", - "driveNotWritable": "guacd kan ikke skrive til drevmappen. Kontroller GUACD_DRIVE_PATH (eller værtens drevsti) og dens tilladelser.", - "dropToUpload": "Slip filer for at uploade" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Låseskærm)", - "winKey": "Windows-nøgle", - "superL": "Super+L (Låseskærm)", - "superKey": "Supernøgle", - "super": "Super", - "cmdL": "Cmd+L (Låseskærm)", - "cmdKey": "Kommandotast", - "cmd": "Kommando", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Flytte", - "win": "Vinde", - "stickyActive": "{{key}} (låst - klik for at slippe)", - "stickyInactive": "{{key}} (klik for at låse)", - "esc": "Flugt", - "tab": "Faneblad", - "home": "Hjem", - "end": "Ende", - "pageUp": "Side op", - "pageDown": "Side ned", - "arrowUp": "Pil op", - "arrowDown": "Pil ned", - "arrowLeft": "Pil til venstre", - "arrowRight": "Pil til højre", - "fnToggle": "Funktionstaster", - "reconnect": "Genopret forbindelse til sessionen", - "collapse": "Skjul værktøjslinjen", - "expand": "Udvid værktøjslinjen", - "dragHandle": "Træk for at flytte", - "hide": "Skjul for denne session. For at deaktivere det permanent skal du åbne Værtsindstillinger → Generelt → Forbindelsesværktøjslinje.", - "zoomIn": "Zoom ind", - "zoomOut": "Zoom ud", - "resetZoom": "Nulstil zoom", - "fitToScreen": "Tilpas til skærm", - "switchToTrackpad": "Skift til trackpad-tilstand (træk for at flytte markøren, tryk for at klikke)", - "switchToTouch": "Skift til berøringstilstand (tryk direkte der, hvor du vil klikke)" - }, - "disabled": "Fjernskrivebord er slået fra. En administrator kan slå det til i Indstillinger." - }, - "hosts": { - "nativeRdpOpened": "Åbnet i Windows Fjernskrivebord", - "nativeRdpFailed": "Kunne ikke åbne Windows Fjernskrivebord", - "openNativeRdp": "Åbn i Windows Fjernskrivebord", - "guac": { - "toolbar": "Værktøjslinje", - "connection": "Forbindelse", - "authentication": "Godkendelse", - "storedCredential": "Gemte legitimationsoplysninger", - "authMethod": "Godkendelsesmetode", - "authTypeDirect": "Direkte", - "authTypeCredential": "Legitimationsoplysninger", - "authTypeNone": "Ingen", - "authTypeNoneDesc": "Ingen loginoplysninger gemmes. Du vil blive bedt om et brugernavn og en adgangskode, hver gang du opretter forbindelse; de gemmes ikke.", - "selectCredential": "Vælg en legitimationsoplysninger...", - "connectionSettings": "Forbindelsesindstillinger", - "displaySettings": "Skærmindstillinger", - "audioSettings": "Lydindstillinger", - "rdpPerformance": "RDP-ydeevne", - "deviceRedirection": "Omdirigering af enhed", - "session": "Session", - "gateway": "Gateway", - "remoteApp": "Fjernbetjening", - "clipboard": "Udklipsholder", - "sessionRecording": "Sessionsoptagelse", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC-indstillinger", - "terminalSettings": "Terminalindstillinger", - "rdpPort": "RDP-port", - "username": "Brugernavn", - "password": "Adgangskode", - "passwordSaved": "Adgangskode gemt, skriv for at ændre", - "domain": "Domæne", - "securityMode": "Sikkerhedstilstand", - "width": "Bredde", - "height": "Højde", - "dpi": "DPI", - "clientName": "Klientnavn", - "initialProgram": "Indledende program", - "serverLayout": "Serverlayout", - "timezone": "Tidszone", - "loadBalanceInfo": "Oplysninger om belastningsbalance / cookie", - "loadBalanceInfoDesc": "RD Connection Broker-cookie til belastningsbalancering af RDS-farm (f.eks. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd Proxy", - "guacdHostname": "guacd-vært", - "guacdHostnamePlaceholder": "Global standard", - "guacdPort": "guacd Port", - "guacdProxyDesc": "Tilsidesæt den globale guacd-instans for denne forbindelse. Lad være tom for at bruge den globalt konfigurerede guacd.", - "gatewayHostname": "Gateway-værtsnavn", - "gatewayPort": "Gateway-porten", - "gatewayUsername": "Gateway-brugernavn", - "gatewayPassword": "Gateway-adgangskode", - "gatewayDomain": "Gateway-domæne", - "remoteAppProgram": "RemoteApp-programmet", - "workingDirectory": "Arbejdskatalog", - "arguments": "Argumenter", - "normalizeLineEndings": "Normaliser linjeafslutninger", - "macAddress": "MAC-adresse", - "broadcastAddress": "Udsendelsesadresse", - "udpPort": "UDP-port", - "waitTimeS": "Ventetid (s)", - "driveName": "Drevnavn", - "drivePath": "Kørselssti", - "ignoreCertificate": "Ignorer certifikat", - "ignoreCertificateDesc": "Tillad forbindelser til værter med selvsignerede certifikater", - "forceLossless": "Tving tabsfri", - "forceLosslessDesc": "Tving tabsfri billedkodning (højere kvalitet, mere båndbredde)", - "disableAudio": "Deaktiver lyd", - "disableAudioDesc": "Slå al lyd fra fra fjernsessionen", - "enableAudioInput": "Aktivér lydindgang (mikrofon)", - "enableAudioInputDesc": "Videresend den lokale mikrofon til den eksterne session", - "wallpaper": "Tapet", - "wallpaperDesc": "Vis skrivebordsbaggrund (deaktivering forbedrer ydeevnen)", - "theming": "Temaer", - "themingDesc": "Aktivér visuelle temaer og stilarter", - "fontSmoothing": "Skrifttypeudjævning", - "fontSmoothingDesc": "Aktivér gengivelse af ClearType-skrifttyper", - "fullWindowDrag": "Træk i hele vinduet", - "fullWindowDragDesc": "Vis vinduesindhold under træk", - "desktopComposition": "Skrivebordskomposition", - "desktopCompositionDesc": "Aktivér Aero Glass-effekter", - "menuAnimations": "Menuanimationer", - "menuAnimationsDesc": "Aktivér menuudtoning og slideanimationer", - "disableBitmapCaching": "Deaktiver bitmap-caching", - "disableBitmapCachingDesc": "Slå bitmap-cache fra (kan muligvis hjælpe med fejl)", - "disableOffscreenCaching": "Deaktiver cachelagring uden for skærmen", - "disableOffscreenCachingDesc": "Slå cache uden for skærmen fra", - "disableGlyphCaching": "Deaktiver glyf-cachelagring", - "disableGlyphCachingDesc": "Slå glyfcache fra", - "enableGfx": "Aktivér GFX", - "enableGfxDesc": "Brug RemoteFX-grafikpipeline", - "enablePrinting": "Aktivér udskrivning", - "enablePrintingDesc": "Omdiriger lokale printere til fjernsessionen", - "enableDriveRedirection": "Aktivér drevomdirigering", - "enableDriveRedirectionDesc": "Tilknyt en lokal mappe som et drev i den eksterne session", - "createDrivePath": "Opret drevsti", - "createDrivePathDesc": "Opret mappen automatisk, hvis den ikke findes", - "disableDownload": "Deaktiver download", - "disableDownloadDesc": "Forhindr download af filer fra fjernsessionen", - "disableUpload": "Deaktiver upload", - "disableUploadDesc": "Forhindr upload af filer til fjernsessionen", - "enableTouch": "Aktivér berøring", - "enableTouchDesc": "Aktivér videresendelse af berøringsinput", - "consoleSession": "Konsolsession", - "consoleSessionDesc": "Opret forbindelse til konsollen (session 0) i stedet for en ny session", - "sendWolPacket": "Send WOL-pakke", - "sendWolPacketDesc": "Send en magisk pakke for at vække denne vært før forbindelse", - "disableCopy": "Deaktiver kopiering", - "disableCopyDesc": "Forhindr kopiering af tekst fra fjernsessionen", - "disablePaste": "Deaktiver indsætning", - "disablePasteDesc": "Forhindr indsættelse af tekst i fjernsessionen", - "createPathIfMissing": "Opret sti, hvis den mangler", - "createPathIfMissingDesc": "Opret automatisk optagelsesmappen", - "excludeOutput": "Udeluk output", - "excludeOutputDesc": "Optag ikke skærmoutput (kun metadata)", - "excludeMouse": "Udeluk mus", - "excludeMouseDesc": "Optag ikke musebevægelser", - "includeKeystrokes": "Inkluder tastetryk", - "includeKeystrokesDesc": "Optag rå tastetryk ud over skærmoutput", - "vncPort": "VNC-port", - "vncPassword": "VNC-adgangskode", - "vncUsernameOptional": "Brugernavn (valgfrit)", - "vncLeaveBlank": "Lad stå tomt, hvis det ikke er nødvendigt", - "cursorMode": "Markørtilstand", - "swapRedBlue": "Skift rød/blå", - "swapRedBlueDesc": "Byt de røde og blå farvekanaler (løser nogle farveproblemer)", - "readOnly": "Skrivebeskyttet", - "readOnlyDesc": "Se fjernskærmen uden at sende input", - "telnetPort": "Telnet-port", - "terminalType": "Terminaltype", - "fontName": "Skrifttypenavn", - "fontSize": "Skriftstørrelse", - "colorScheme": "Farveskema", - "backspaceKey": "Tilbagetast", - "colorDepth": "Farvedybde", - "resizeMethod": "Ændre størrelse-metode", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Enhver", - "auto": "Bil", - "nlaExtended": "NLA udvidet", - "color8": "8-bit", - "color16": "16-bit", - "color24": "24-bit", - "color32": "32-bit", - "displayUpdate": "Opdatering af display", - "reconnect": "Genopret forbindelse", - "preserve": "Bevare", - "unix": "Unix (LF)", - "windows": "Vinduer (CRLF)", - "local": "Lokal", - "remote": "Fjern", - "failsafe": "fejlsikker", - "blackOnWhite": "Sort på hvid", - "whiteOnBlack": "Hvid på sort", - "grayOnBlack": "Grå på sort", - "greenOnBlack": "Grøn på sort" - } - }, - "remoteDesktop": "Fjernskrivebord", - "tabRdp": "Landdistriktsudviklingsplan", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Virtuelt netværk", - "unencryptedShell": "Ukrypteret skal", - "remoteDesktopNotes": "Noter", - "userDefaultsNote": "Indstillinger, du lader være uændrede, følger din værts standardindstillinger.", - "connectionOriginNote": "I desktop-appen bruger RDP, VNC og Telnet den eksterne server, medmindre værten er indstillet til Denne enhed, som kræver en guacd, som denne maskine kan nå. Angiv en i indstillingerne for Fjernskrivebord, eller pr. vært under guacd Proxy.", - "docsLink": "Se dokumenter", - "connectionOriginGuacamoleNote": "RDP, VNC og Telnet bruger den eksterne server, medmindre du vælger \"Denne enhed\", som kræver en guacd, som denne maskine kan nå. Angiv en i guacd URL-indstillingen eller pr. vært under guacd Proxy." - }, - "onboarding": { - "feature_desktop": "Fjernskrivebord", - "feature_desktop_desc": "Åbn RDP-, VNC- og Telnet-sessioner i en fane, ligesom med SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Farvedybde", - "description": "Bruges til RDP-værter, der lader den være på standardværdien." - }, - "resizeMethod": { - "label": "Ændre størrelse-metode", - "description": "Hvordan fjernskærmen følger vinduets størrelse." - }, - "resizeDisplayUpdate": "Opdatering af display", - "resizeReconnect": "Genopret forbindelse", - "inherit": "Arve", - "on": "På", - "off": "Slukket", - "forceLossless": { - "label": "Tving tabsfri", - "description": "Tving tabsfri billedkodning (højere kvalitet, mere båndbredde)" - }, - "enableWallpaper": { - "label": "Tapet", - "description": "Vis skrivebordsbaggrund (deaktivering forbedrer ydeevnen)" - }, - "enableFontSmoothing": { - "label": "Skrifttypeudjævning", - "description": "Aktivér gengivelse af ClearType-skrifttyper" - }, - "enableDesktopComposition": { - "label": "Skrivebordskomposition", - "description": "Aktivér Aero Glass-effekter" - }, - "disableAudio": { - "label": "Deaktiver lyd", - "description": "Slå al lyd fra fra fjernsessionen" - }, - "enablePrinting": { - "label": "Aktivér udskrivning", - "description": "Omdiriger lokale printere til fjernsessionen" - }, - "enableDrive": { - "label": "Aktivér drevomdirigering", - "description": "Tilknyt en lokal mappe som et drev i den eksterne session" - }, - "disableCopy": { - "label": "Deaktiver kopiering", - "description": "Forhindr kopiering af tekst fra fjernsessionen" - }, - "disablePaste": { - "label": "Deaktiver indsætning", - "description": "Forhindr indsættelse af tekst i fjernsessionen" - }, - "group": "Standardindstillinger for fjernskrivebord", - "colorDepth16": "16-bit", - "colorDepth24": "24-bit", - "colorDepth32": "32-bit" - }, - "admin": { - "enabled": { - "label": "Aktivér fjernskrivebord", - "description": "Tillad RDP-, VNC- og Telnet-forbindelser via guacd." - }, - "guacdUrl": { - "label": "guacd-URL", - "description": "Hvor guacd lytter som host:port. Miljøvariablerne GUACD_URL, GUACD_HOST og GUACD_PORT tilsidesætter dette.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "Landdistriktsudviklingsplan" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP-port" - }, - "vncPort": { - "label": "VNC-port" - }, - "telnetPort": { - "label": "Telnet-port" - }, - "rdpSecurity": { - "label": "RDP-sikkerhedstilstand" - }, - "rdpIgnoreCert": { - "label": "Ignorer certifikatfejl" - }, - "guacamoleConfig": { - "label": "guacd-indstillinger" - }, - "enableToolbar": { - "label": "Værktøjslinjen til fjernskrivebord", - "description": "Vis værktøjslinjen i RDP-, VNC- og Telnet-sessioner." - }, - "displayGroup": "Vise", - "displayDefaultsTab": "Fjernskrivebord" - } - }, - "palette": { - "connectRdp": "Opret forbindelse med RDP", - "connectVnc": "Opret forbindelse med VNC", - "connectTelnet": "Opret forbindelse med Telnet" - }, - "permissions": { - "sessions": { - "title": "Del fjernskrivebordssessioner", - "description": "Lad sessionsdeling og samarbejdsrum vise denne brugers RDP-, VNC- og Telnet-sessioner til andre." - } - }, - "errors": { - "remoteServerRequired": "Fjernserver kræves. Opret forbindelse til en fjernserver i Indstillinger for at bruge denne forbindelsestype." - }, - "plugin": { - "name": "Fjernskrivebord" - } -} diff --git a/plugins/remote-desktop/locales/translated/de_DE.json b/plugins/remote-desktop/locales/translated/de_DE.json deleted file mode 100644 index a2bdf44c9..000000000 --- a/plugins/remote-desktop/locales/translated/de_DE.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Verbinde mit {{type}}-Sitzung...", - "checkingGuacd": "Überprüfung des Remotedesktopdienstes...", - "requestingToken": "Verbindungstoken {{type}} wird angefordert...", - "openingSession": "Öffne {{type}} Sitzung...", - "negotiating": "Verhandlung {{type}} Handschlag...", - "sessionReady": "{{type}} Sitzung bereit", - "connectionError": "Verbindungsfehler", - "connectionFailed": "Verbindung fehlgeschlagen", - "failedToConnect": "Fehler beim Abrufen des Verbindungstokens", - "hostNotFound": "Host nicht gefunden", - "noHostSelected": "Kein Host ausgewählt", - "reconnect": "Erneut verbinden", - "retry": "Erneut versuchen", - "guacdUnavailable": "Der Remote-Desktop-Dienst (guacd) ist nicht verfügbar. Bitte stellen Sie sicher, dass guacd ausgeführt wird, erreichbar ist und in den Admin-Einstellungen richtig konfiguriert ist.", - "credentialPromptTitle": "Geben Sie die RDP-Anmeldeinformationen ein.", - "credentialPromptDescription": "Dieser Host ist so konfiguriert, dass er beim Verbindungsaufbau zur Eingabe von Anmeldeinformationen auffordert. Diese werden nur für diese Sitzung verwendet und nicht gespeichert.", - "connect": "Verbinden", - "ctrlAltDel": "Strg+Alt+Entf", - "files": { - "title": "Laufwerksdateien", - "parent": "Übergeordnetes Verzeichnis", - "refresh": "Aktualisieren", - "empty": "Dieser Ordner ist leer", - "listFailed": "Dieser Ordner konnte nicht gelesen werden", - "download": "Herunterladen", - "downloadFailed": "Download fehlgeschlagen {{name}}", - "upload": "Dateien hochladen", - "uploading": "Lade {{name}} hoch...", - "uploaded": "Hochgeladen {{name}}", - "uploadFailed": "Hochladen fehlgeschlagen {{name}}", - "uploadDisabled": "Uploads sind für diese Verbindung deaktiviert.", - "driveUnavailable": "Aktivieren Sie die RDP-Laufwerksumleitung, bevor Sie Dateien hochladen.", - "driveNotWritable": "guacd kann nicht in den Laufwerksordner schreiben. Überprüfen Sie GUACD_DRIVE_PATH (oder den Laufwerkspfad des Hosts) und dessen Berechtigungen.", - "dropToUpload": "Dateien zum Hochladen per Drag & Drop ablegen" - }, - "toolbar": { - "ctrlAltDel": "Strg+Alt+Entf", - "winL": "Win+L (Bildschirm sperren)", - "winKey": "Windows-Taste", - "superL": "Super+L (Sperrbildschirm)", - "superKey": "Super Key", - "super": "Super", - "cmdL": "Cmd+L (Sperrbildschirm)", - "cmdKey": "Befehlstaste", - "cmd": "Cmd", - "ctrl": "Strg", - "alt": "Alt", - "shift": "Umschalt", - "win": "Win", - "stickyActive": "{{key}} (eingerastet – zum Lösen klicken)", - "stickyInactive": "{{key}} (zum Einrasten klicken)", - "esc": "Escape", - "tab": "Tab", - "home": "Startseite", - "end": "Ende", - "pageUp": "Bild auf", - "pageDown": "Bild ab", - "arrowUp": "Pfeil nach oben", - "arrowDown": "Pfeil nach unten", - "arrowLeft": "Pfeil nach links", - "arrowRight": "Pfeil nach rechts", - "fnToggle": "Funktionstasten", - "reconnect": "Sitzung erneut verbinden", - "collapse": "Symbolleiste einklappen", - "expand": "Symbolleiste ausklappen", - "dragHandle": "Zum Neupositionieren ziehen", - "hide": "Für diese Sitzung ausblenden. Um es dauerhaft zu deaktivieren, öffnen Sie Host-Einstellungen → Allgemein → Verbindungssymbolleiste.", - "zoomIn": "Vergrößern", - "zoomOut": "Herauszoomen", - "resetZoom": "Zoom zurücksetzen", - "fitToScreen": "An Bildschirm anpassen", - "switchToTrackpad": "In den Trackpad-Modus wechseln (Zum Bewegen des Cursors ziehen, zum Klicken tippen)", - "switchToTouch": "Wechseln Sie in den Touch-Modus (tippen Sie direkt auf die Stelle, an der Sie klicken möchten)." - }, - "disabled": "Die Remotedesktopverbindung ist deaktiviert. Ein Administrator kann sie in den Einstellungen aktivieren." - }, - "hosts": { - "nativeRdpOpened": "In Windows-Remotedesktop geöffnet", - "nativeRdpFailed": "Windows-Remotedesktop konnte nicht geöffnet werden", - "openNativeRdp": "In Windows-Remotedesktop öffnen", - "guac": { - "toolbar": "Symbolleiste", - "connection": "Verbindung", - "authentication": "Authentifizierung", - "storedCredential": "Gespeicherte Anmeldeinformation", - "authMethod": "Authentifizierungsmethode", - "authTypeDirect": "Direkt", - "authTypeCredential": "Anmeldeinformation", - "authTypeNone": "Keine", - "authTypeNoneDesc": "Es werden keine Zugangsdaten gespeichert. Sie werden bei jeder Verbindung zur Eingabe eines Benutzernamens und eines Passworts aufgefordert; diese werden nicht gespeichert.", - "selectCredential": "Anmeldeinformation auswählen...", - "connectionSettings": "Verbindungseinstellungen", - "displaySettings": "Anzeigeeinstellungen", - "audioSettings": "Audioeinstellungen", - "rdpPerformance": "RDP-Leistung", - "deviceRedirection": "Geräteumleitung", - "session": "Sitzung", - "gateway": "Gateway", - "remoteApp": "RemoteApp", - "clipboard": "Zwischenablage", - "sessionRecording": "Sitzungsaufzeichnung", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC-Einstellungen", - "terminalSettings": "Terminaleinstellungen", - "rdpPort": "RDP-Port", - "username": "Benutzername", - "password": "Passwort", - "passwordSaved": "Passwort gespeichert, zum Ändern eingeben", - "domain": "Domäne", - "securityMode": "Sicherheitsmodus", - "width": "Breite", - "height": "Höhe", - "dpi": "DPI", - "clientName": "Client-Name", - "initialProgram": "Startprogramm", - "serverLayout": "Tastaturlayout", - "timezone": "Zeitzone", - "loadBalanceInfo": "Lastverteilungsinfo / Cookie", - "loadBalanceInfoDesc": "RD-Verbindungsbroker-Cookie für den Lastausgleich einer RDS-Farm (z.B. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd-Proxy", - "guacdHostname": "guacd-Host", - "guacdHostnamePlaceholder": "Globaler Standard", - "guacdPort": "guacd-Port", - "guacdProxyDesc": "Überschreibt die globale guacd-Instanz für diese Verbindung. Leer lassen, um die global konfigurierte guacd zu verwenden.", - "gatewayHostname": "Gateway-Hostname", - "gatewayPort": "Gateway-Port", - "gatewayUsername": "Gateway-Benutzername", - "gatewayPassword": "Gateway-Passwort", - "gatewayDomain": "Gateway-Domäne", - "remoteAppProgram": "RemoteApp-Programm", - "workingDirectory": "Arbeitsverzeichnis", - "arguments": "Argumente", - "normalizeLineEndings": "Zeilenenden normalisieren", - "macAddress": "MAC-Adresse", - "broadcastAddress": "Broadcast-Adresse", - "udpPort": "UDP-Port", - "waitTimeS": "Wartezeit (s)", - "driveName": "Laufwerksname", - "drivePath": "Laufwerkspfad", - "ignoreCertificate": "Zertifikat ignorieren", - "ignoreCertificateDesc": "Verbindungen zu Hosts mit selbstsignierten Zertifikaten erlauben", - "forceLossless": "Verlustfreie Übertragung erzwingen", - "forceLosslessDesc": "Verlustfreie Bildcodierung erzwingen (höhere Qualität, mehr Bandbreite)", - "disableAudio": "Audio deaktivieren", - "disableAudioDesc": "Gesamten Ton der Remote-Sitzung stummschalten", - "enableAudioInput": "Audioeingabe aktivieren (Mikrofon)", - "enableAudioInputDesc": "Lokales Mikrofon an die Remote-Sitzung weiterleiten", - "wallpaper": "Hintergrundbild", - "wallpaperDesc": "Desktop-Hintergrundbild anzeigen (Deaktivierung verbessert die Leistung)", - "theming": "Designs", - "themingDesc": "Visuelle Designs und Stile aktivieren", - "fontSmoothing": "Schriftglättung", - "fontSmoothingDesc": "ClearType-Schriftdarstellung aktivieren", - "fullWindowDrag": "Vollständiges Fensterziehen", - "fullWindowDragDesc": "Fensterinhalt beim Ziehen anzeigen", - "desktopComposition": "Desktopkomposition", - "desktopCompositionDesc": "Aero-Glaseffekte aktivieren", - "menuAnimations": "Menüanimationen", - "menuAnimationsDesc": "Ein- und Ausblendeanimationen für Menüs aktivieren", - "disableBitmapCaching": "Bitmap-Caching deaktivieren", - "disableBitmapCachingDesc": "Bitmap-Cache deaktivieren (kann bei Grafikfehlern helfen)", - "disableOffscreenCaching": "Offscreen-Caching deaktivieren", - "disableOffscreenCachingDesc": "Offscreen-Cache deaktivieren", - "disableGlyphCaching": "Glyphen-Caching deaktivieren", - "disableGlyphCachingDesc": "Glyphen-Cache deaktivieren", - "enableGfx": "GFX aktivieren", - "enableGfxDesc": "RemoteFX-Grafikpipeline verwenden", - "enablePrinting": "Drucken aktivieren", - "enablePrintingDesc": "Lokale Drucker an die Remote-Sitzung umleiten", - "enableDriveRedirection": "Laufwerksumleitung aktivieren", - "enableDriveRedirectionDesc": "Lokalen Ordner als Laufwerk in der Remote-Sitzung einbinden", - "createDrivePath": "Laufwerkspfad erstellen", - "createDrivePathDesc": "Ordner automatisch erstellen, falls nicht vorhanden", - "disableDownload": "Download deaktivieren", - "disableDownloadDesc": "Herunterladen von Dateien aus der entfernten Sitzung verhindern", - "disableUpload": "Upload deaktivieren", - "disableUploadDesc": "Hochladen von Dateien in die entfernte Sitzung verhindern", - "enableTouch": "Touch aktivieren", - "enableTouchDesc": "Touch-Eingabeweiterleitung aktivieren", - "consoleSession": "Konsolensitzung", - "consoleSessionDesc": "Mit der Konsole (Sitzung 0) statt mit einer neuen Sitzung verbinden", - "sendWolPacket": "WOL-Paket senden", - "sendWolPacketDesc": "Magic Packet senden, um diesen Host vor dem Verbinden aufzuwecken", - "disableCopy": "Kopieren deaktivieren", - "disableCopyDesc": "Kopieren von Text aus der entfernten Sitzung verhindern", - "disablePaste": "Einfügen deaktivieren", - "disablePasteDesc": "Einfügen von Text in die entfernte Sitzung verhindern", - "createPathIfMissing": "Pfad erstellen, falls nicht vorhanden", - "createPathIfMissingDesc": "Aufzeichnungsverzeichnis automatisch erstellen", - "excludeOutput": "Ausgabe ausschließen", - "excludeOutputDesc": "Bildschirmausgabe nicht aufzeichnen (nur Metadaten)", - "excludeMouse": "Maus ausschließen", - "excludeMouseDesc": "Mausbewegungen nicht aufzeichnen", - "includeKeystrokes": "Tastatureingaben einschließen", - "includeKeystrokesDesc": "Rohe Tastatureingaben zusätzlich zur Bildschirmausgabe aufzeichnen", - "vncPort": "VNC-Port", - "vncPassword": "VNC-Passwort", - "vncUsernameOptional": "Benutzername (optional)", - "vncLeaveBlank": "Leer lassen, falls nicht erforderlich", - "cursorMode": "Cursor-Modus", - "swapRedBlue": "Rot/Blau tauschen", - "swapRedBlueDesc": "Die roten und blauen Farbkanäle tauschen (behebt einige Farbprobleme)", - "readOnly": "Schreibgeschützt", - "readOnlyDesc": "Bildschirm anzeigen, ohne Eingaben zu senden", - "telnetPort": "Telnet-Port", - "terminalType": "Terminaltyp", - "fontName": "Schriftname", - "fontSize": "Schriftgröße", - "colorScheme": "Farbschema", - "backspaceKey": "Backspace-Taste", - "colorDepth": "Farbtiefe", - "resizeMethod": "Skalierungsmethode", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Beliebig", - "auto": "Automatisch", - "nlaExtended": "NLA Extended", - "color8": "8-Bit", - "color16": "16-Bit", - "color24": "24-Bit", - "color32": "32-Bit", - "displayUpdate": "Anzeigeaktualisierung", - "reconnect": "Erneut verbinden", - "preserve": "Bewahren", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Lokal", - "remote": "Remote", - "failsafe": "Ausfallsicherung", - "blackOnWhite": "Schwarz auf Weiß", - "whiteOnBlack": "Weiß auf Schwarz", - "grayOnBlack": "Grau auf Schwarz", - "greenOnBlack": "Grün auf Schwarz" - } - }, - "remoteDesktop": "Remotedesktop", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Virtuelles Netzwerk", - "unencryptedShell": "Unverschlüsselte Shell", - "remoteDesktopNotes": "Notizen", - "userDefaultsNote": "Einstellungen, die Sie unverändert lassen, verwenden die Standardeinstellungen Ihres Hostsystems.", - "connectionOriginNote": "In der Desktop-App verwenden RDP, VNC und Telnet den Remote-Server, es sei denn, der Host ist auf „Dieses Gerät“ eingestellt. In diesem Fall wird ein guacd-Server benötigt, den dieser Rechner erreichen kann. Legen Sie einen solchen Server in den Remote-Desktop-Einstellungen oder pro Host unter „guacd-Proxy“ fest.", - "docsLink": "Dokumentation anzeigen", - "connectionOriginGuacamoleNote": "RDP, VNC und Telnet verwenden den Remote-Server, es sei denn, Sie wählen „Dieses Gerät“. Dafür ist ein guacd-Server erforderlich, den dieser Rechner erreichen kann. Legen Sie einen solchen Server in den guacd-URL-Einstellungen oder pro Host unter „guacd-Proxy“ fest." - }, - "onboarding": { - "feature_desktop": "Remote-Desktop", - "feature_desktop_desc": "Öffnen Sie RDP-, VNC- und Telnet-Sitzungen in einem Tab, genau wie bei SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Farbtiefe", - "description": "Wird für RDP-Hosts verwendet, die die Standardeinstellung beibehalten." - }, - "resizeMethod": { - "label": "Skalierungsmethode", - "description": "Wie sich der Bildschirm der Fernbedienung an die Fenstergröße anpasst." - }, - "resizeDisplayUpdate": "Anzeigeaktualisierung", - "resizeReconnect": "Erneut verbinden", - "inherit": "Erben", - "on": "Ein", - "off": "Aus", - "forceLossless": { - "label": "Verlustfreie Übertragung erzwingen", - "description": "Verlustfreie Bildcodierung erzwingen (höhere Qualität, mehr Bandbreite)" - }, - "enableWallpaper": { - "label": "Hintergrundbild", - "description": "Desktop-Hintergrundbild anzeigen (Deaktivierung verbessert die Leistung)" - }, - "enableFontSmoothing": { - "label": "Schriftglättung", - "description": "ClearType-Schriftdarstellung aktivieren" - }, - "enableDesktopComposition": { - "label": "Desktopkomposition", - "description": "Aero-Glaseffekte aktivieren" - }, - "disableAudio": { - "label": "Audio deaktivieren", - "description": "Gesamten Ton der Remote-Sitzung stummschalten" - }, - "enablePrinting": { - "label": "Drucken aktivieren", - "description": "Lokale Drucker an die Remote-Sitzung umleiten" - }, - "enableDrive": { - "label": "Laufwerksumleitung aktivieren", - "description": "Lokalen Ordner als Laufwerk in der Remote-Sitzung einbinden" - }, - "disableCopy": { - "label": "Kopieren deaktivieren", - "description": "Kopieren von Text aus der entfernten Sitzung verhindern" - }, - "disablePaste": { - "label": "Einfügen deaktivieren", - "description": "Einfügen von Text in die entfernte Sitzung verhindern" - }, - "group": "Standardeinstellungen für Remotedesktop", - "colorDepth16": "16-Bit", - "colorDepth24": "24-Bit", - "colorDepth32": "32-Bit" - }, - "admin": { - "enabled": { - "label": "Remotedesktop aktivieren", - "description": "RDP-, VNC- und Telnet-Verbindungen über guacd zulassen." - }, - "guacdUrl": { - "label": "guacd-URL", - "description": "Der Hostname „guacd“ gibt an, wo er lauscht. Die Umgebungsvariablen GUACD_URL, GUACD_HOST und GUACD_PORT überschreiben diese Angabe.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP-Port" - }, - "vncPort": { - "label": "VNC-Port" - }, - "telnetPort": { - "label": "Telnet-Port" - }, - "rdpSecurity": { - "label": "RDP-Sicherheitsmodus" - }, - "rdpIgnoreCert": { - "label": "Zertifikatsfehler ignorieren" - }, - "guacamoleConfig": { - "label": "guacd-Einstellungen" - }, - "enableToolbar": { - "label": "Symbolleiste für Remote-Desktop", - "description": "Die Symbolleiste wird in RDP-, VNC- und Telnet-Sitzungen angezeigt." - }, - "displayGroup": "Anzeige", - "displayDefaultsTab": "Remotedesktop" - } - }, - "palette": { - "connectRdp": "Mit RDP verbinden", - "connectVnc": "Mit VNC verbinden", - "connectTelnet": "Mit Telnet verbinden" - }, - "permissions": { - "sessions": { - "title": "Remote-Desktop-Sitzungen freigeben", - "description": "Erlauben Sie, dass die RDP-, VNC- und Telnet-Sitzungen dieses Benutzers anderen Benutzern angezeigt werden, wenn diese die Sitzungsfreigabe und Kollaborationsräume nutzen." - } - }, - "errors": { - "remoteServerRequired": "Remote-Server erforderlich. Um diesen Verbindungstyp zu verwenden, verbinden Sie einen Remote-Server in den Einstellungen." - }, - "plugin": { - "name": "Remotedesktop" - } -} diff --git a/plugins/remote-desktop/locales/translated/el_GR.json b/plugins/remote-desktop/locales/translated/el_GR.json deleted file mode 100644 index df37507a9..000000000 --- a/plugins/remote-desktop/locales/translated/el_GR.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Σύνδεση σε συνεδρία {{type}}...", - "checkingGuacd": "Έλεγχος υπηρεσίας απομακρυσμένης επιφάνειας εργασίας...", - "requestingToken": "Αίτημα διακριτικού σύνδεσης {{type}}...", - "openingSession": "Άνοιγμα συνεδρίας {{type}}...", - "negotiating": "Διαπραγμάτευση χειραψίας {{type}}...", - "sessionReady": "{{type}} συνεδρία έτοιμη", - "connectionError": "Σφάλμα σύνδεσης", - "connectionFailed": "Η σύνδεση απέτυχε", - "failedToConnect": "Αποτυχία λήψης διακριτικού σύνδεσης", - "hostNotFound": "Δεν βρέθηκε κεντρικός υπολογιστής", - "noHostSelected": "Δεν έχει επιλεγεί κεντρικός υπολογιστής", - "reconnect": "Επανασύνδεση", - "retry": "Δοκιμάζω πάλι", - "guacdUnavailable": "Η υπηρεσία απομακρυσμένης επιφάνειας εργασίας (guacd) δεν είναι διαθέσιμη. Βεβαιωθείτε ότι το guacd εκτελείται και είναι προσβάσιμο, καθώς και ότι έχει ρυθμιστεί σωστά στις ρυθμίσεις διαχειριστή.", - "credentialPromptTitle": "Εισαγάγετε τα διαπιστευτήρια RDP", - "credentialPromptDescription": "Αυτός ο κεντρικός υπολογιστής έχει ρυθμιστεί να ζητά διαπιστευτήρια κατά τη σύνδεση. Χρησιμοποιούνται μόνο για αυτήν την περίοδο σύνδεσης και δεν αποθηκεύονται.", - "connect": "Συνδέω", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Αρχεία μονάδας δίσκου", - "parent": "Γονικός κατάλογος", - "refresh": "Φρεσκάρω", - "empty": "Αυτός ο φάκελος είναι κενός", - "listFailed": "Αποτυχία ανάγνωσης αυτού του φακέλου", - "download": "Λήψη", - "downloadFailed": "Αποτυχία λήψης {{name}}", - "upload": "Μεταφόρτωση αρχείων", - "uploading": "Μεταφόρτωση {{name}}...", - "uploaded": "Μεταφορτώθηκε {{name}}", - "uploadFailed": "Αποτυχία μεταφόρτωσης {{name}}", - "uploadDisabled": "Οι μεταφορτώσεις είναι απενεργοποιημένες για αυτήν τη σύνδεση", - "driveUnavailable": "Ενεργοποίηση ανακατεύθυνσης μονάδας δίσκου RDP πριν από την αποστολή αρχείων", - "driveNotWritable": "Το guacd δεν μπορεί να γράψει στον φάκελο της μονάδας δίσκου. Ελέγξτε το GUACD_DRIVE_PATH (ή τη διαδρομή μονάδας δίσκου του κεντρικού υπολογιστή) και τα δικαιώματά του.", - "dropToUpload": "Αποθέστε αρχεία για μεταφόρτωση" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Κλείδωμα οθόνης)", - "winKey": "Κλειδί των Windows", - "superL": "Super+L (Κλείδωμα οθόνης)", - "superKey": "Σούπερ Κλειδί", - "super": "Σούπερ", - "cmdL": "Cmd+L (Κλείδωμα οθόνης)", - "cmdKey": "Πλήκτρο εντολών", - "cmd": "Εκτελεστής", - "ctrl": "Ctrl", - "alt": "Εναλλακτική", - "shift": "Αλλαγή", - "win": "Νίκη", - "stickyActive": "{{key}} (κλειδωμένο - κάντε κλικ για απελευθέρωση)", - "stickyInactive": "{{key}} (κάντε κλικ για να κλειδώσετε)", - "esc": "Διαφυγή", - "tab": "Αυτί", - "home": "Σπίτι", - "end": "Τέλος", - "pageUp": "Σελίδα προς τα πάνω", - "pageDown": "Σελίδα προς τα κάτω", - "arrowUp": "Βέλος προς τα πάνω", - "arrowDown": "Βέλος προς τα κάτω", - "arrowLeft": "Βέλος αριστερά", - "arrowRight": "Βέλος δεξιά", - "fnToggle": "Πλήκτρα λειτουργιών", - "reconnect": "Επανασύνδεση περιόδου σύνδεσης", - "collapse": "Σύμπτυξη γραμμής εργαλείων", - "expand": "Ανάπτυξη γραμμής εργαλείων", - "dragHandle": "Σύρετε για αλλαγή θέσης", - "hide": "Απόκρυψη για αυτήν την περίοδο λειτουργίας. Για να την απενεργοποιήσετε μόνιμα, ανοίξτε τις Ρυθμίσεις κεντρικού υπολογιστή → Γενικά → Γραμμή εργαλείων σύνδεσης.", - "zoomIn": "Μεγέθυνση", - "zoomOut": "Σμίκρυνση", - "resetZoom": "Επαναφορά ζουμ", - "fitToScreen": "Προσαρμογή στην οθόνη", - "switchToTrackpad": "Μετάβαση σε λειτουργία trackpad (σύρσιμο για μετακίνηση του κέρσορα, άγγιγμα για κλικ)", - "switchToTouch": "Μετάβαση σε λειτουργία αφής (πατήστε απευθείας εκεί που θέλετε να κάνετε κλικ)" - }, - "disabled": "Η Απομακρυσμένη επιφάνεια εργασίας είναι απενεργοποιημένη. Ένας διαχειριστής μπορεί να την ενεργοποιήσει στις Ρυθμίσεις." - }, - "hosts": { - "nativeRdpOpened": "Άνοιξε στην Απομακρυσμένη επιφάνεια εργασίας των Windows", - "nativeRdpFailed": "Αποτυχία ανοίγματος της Απομακρυσμένης επιφάνειας εργασίας των Windows", - "openNativeRdp": "Άνοιγμα στην Απομακρυσμένη επιφάνεια εργασίας των Windows", - "guac": { - "toolbar": "Γραμμή εργαλείων", - "connection": "Σύνδεση", - "authentication": "Πιστοποίηση", - "storedCredential": "Αποθηκευμένα διαπιστευτήρια", - "authMethod": "Μέθοδος Εξουσιοδότησης", - "authTypeDirect": "Απευθείας", - "authTypeCredential": "Πιστοποιητικό", - "authTypeNone": "Κανένας", - "authTypeNoneDesc": "Δεν αποθηκεύονται διαπιστευτήρια. Θα σας ζητείται όνομα χρήστη και κωδικός πρόσβασης κάθε φορά που συνδέεστε. Δεν αποθηκεύονται.", - "selectCredential": "Επιλέξτε ένα πιστοποιητικό...", - "connectionSettings": "Ρυθμίσεις σύνδεσης", - "displaySettings": "Ρυθμίσεις οθόνης", - "audioSettings": "Ρυθμίσεις ήχου", - "rdpPerformance": "Απόδοση RDP", - "deviceRedirection": "Ανακατεύθυνση συσκευής", - "session": "Συνεδρίαση", - "gateway": "Είσοδος πυλών", - "remoteApp": "Απομακρυσμένη εφαρμογή", - "clipboard": "Πρόχειρο", - "sessionRecording": "Ηχογράφηση συνεδρίας", - "wakeOnLan": "Αφύπνιση μέσω LAN", - "vncSettings": "Ρυθμίσεις VNC", - "terminalSettings": "Ρυθμίσεις τερματικού", - "rdpPort": "Θύρα RDP", - "username": "Όνομα χρήστη", - "password": "Σύνθημα", - "passwordSaved": "Ο κωδικός πρόσβασης αποθηκεύτηκε, πληκτρολογήστε για να τον αλλάξετε", - "domain": "Πεδίο ορισμού", - "securityMode": "Λειτουργία ασφαλείας", - "width": "Πλάτος", - "height": "Υψος", - "dpi": "DPI", - "clientName": "Όνομα πελάτη", - "initialProgram": "Αρχικό Πρόγραμμα", - "serverLayout": "Διάταξη διακομιστή", - "timezone": "Ζώνη ώρας", - "loadBalanceInfo": "Πληροφορίες ισορροπίας φορτίου / Cookie", - "loadBalanceInfoDesc": "Cookie RD Connection Broker για εξισορρόπηση φόρτου συστοιχίας RDS (π.χ. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd Proxy", - "guacdHostname": "οικοδεσπότης guacd", - "guacdHostnamePlaceholder": "Παγκόσμια προεπιλογή", - "guacdPort": "Λιμάνι Guacd", - "guacdProxyDesc": "Παρακάμψτε την καθολική παρουσία guacd για αυτήν τη σύνδεση. Αφήστε το κενό για να χρησιμοποιήσετε την καθολικά διαμορφωμένη παρουσία guacd.", - "gatewayHostname": "Όνομα κεντρικού υπολογιστή πύλης", - "gatewayPort": "Λιμάνι Πύλης", - "gatewayUsername": "Όνομα χρήστη πύλης", - "gatewayPassword": "Κωδικός πρόσβασης πύλης", - "gatewayDomain": "Τομέας πύλης", - "remoteAppProgram": "Πρόγραμμα RemoteApp", - "workingDirectory": "Κατάλογος εργασίας", - "arguments": "Επιχειρήματα", - "normalizeLineEndings": "Κανονικοποίηση τελών γραμμών", - "macAddress": "Διεύθυνση MAC", - "broadcastAddress": "Διεύθυνση μετάδοσης", - "udpPort": "Θύρα UDP", - "waitTimeS": "Χρόνος αναμονής (s)", - "driveName": "Όνομα μονάδας δίσκου", - "drivePath": "Μονοπάτι οδήγησης", - "ignoreCertificate": "Αγνόηση πιστοποιητικού", - "ignoreCertificateDesc": "Επιτρέψτε συνδέσεις σε κεντρικούς υπολογιστές με αυτο-υπογεγραμμένα πιστοποιητικά", - "forceLossless": "Δύναμη χωρίς απώλειες", - "forceLosslessDesc": "Επιβολή κωδικοποίησης εικόνας χωρίς απώλειες (υψηλότερη ποιότητα, μεγαλύτερο εύρος ζώνης)", - "disableAudio": "Απενεργοποίηση ήχου", - "disableAudioDesc": "Σίγαση όλου του ήχου από την απομακρυσμένη συνεδρία", - "enableAudioInput": "Ενεργοποίηση εισόδου ήχου (Μικρόφωνο)", - "enableAudioInputDesc": "Προώθηση τοπικού μικροφώνου στην απομακρυσμένη συνεδρία", - "wallpaper": "Ταπετσαρία", - "wallpaperDesc": "Εμφάνιση ταπετσαρίας επιφάνειας εργασίας (η απενεργοποίηση βελτιώνει την απόδοση)", - "theming": "Θέμα", - "themingDesc": "Ενεργοποίηση οπτικών θεμάτων και στυλ", - "fontSmoothing": "Εξομάλυνση γραμματοσειράς", - "fontSmoothingDesc": "Ενεργοποίηση απόδοσης γραμματοσειράς ClearType", - "fullWindowDrag": "Πλήρες σύρσιμο παραθύρου", - "fullWindowDragDesc": "Εμφάνιση περιεχομένων παραθύρου κατά τη μεταφορά", - "desktopComposition": "Σύνθεση επιφάνειας εργασίας", - "desktopCompositionDesc": "Ενεργοποίηση εφέ Aero glass", - "menuAnimations": "Κινήσεις μενού", - "menuAnimationsDesc": "Ενεργοποίηση κινούμενων εικόνων fade και slide μενού", - "disableBitmapCaching": "Απενεργοποίηση προσωρινής αποθήκευσης bitmap", - "disableBitmapCachingDesc": "Απενεργοποίηση της προσωρινής μνήμης bitmap (μπορεί να βοηθήσει με προβλήματα)", - "disableOffscreenCaching": "Απενεργοποίηση προσωρινής αποθήκευσης εκτός οθόνης", - "disableOffscreenCachingDesc": "Απενεργοποίηση προσωρινής μνήμης εκτός οθόνης", - "disableGlyphCaching": "Απενεργοποίηση προσωρινής αποθήκευσης γλύφων", - "disableGlyphCachingDesc": "Απενεργοποίηση προσωρινής μνήμης glyph", - "enableGfx": "Ενεργοποίηση GFX", - "enableGfxDesc": "Χρήση αγωγού γραφικών RemoteFX", - "enablePrinting": "Ενεργοποίηση εκτύπωσης", - "enablePrintingDesc": "Ανακατεύθυνση τοπικών εκτυπωτών στην απομακρυσμένη συνεδρία", - "enableDriveRedirection": "Ενεργοποίηση ανακατεύθυνσης μονάδας δίσκου", - "enableDriveRedirectionDesc": "Αντιστοίχιση τοπικού φακέλου ως μονάδας δίσκου στην απομακρυσμένη συνεδρία", - "createDrivePath": "Δημιουργία διαδρομής μονάδας δίσκου", - "createDrivePathDesc": "Αυτόματη δημιουργία του φακέλου εάν δεν υπάρχει", - "disableDownload": "Απενεργοποίηση λήψης", - "disableDownloadDesc": "Αποτροπή λήψης αρχείων από την απομακρυσμένη συνεδρία", - "disableUpload": "Απενεργοποίηση μεταφόρτωσης", - "disableUploadDesc": "Αποτροπή μεταφόρτωσης αρχείων στην απομακρυσμένη συνεδρία", - "enableTouch": "Ενεργοποίηση αφής", - "enableTouchDesc": "Ενεργοποίηση προώθησης εισόδου αφής", - "consoleSession": "Συνεδρία κονσόλας", - "consoleSessionDesc": "Σύνδεση στην κονσόλα (περίοδος λειτουργίας 0) αντί για νέα περίοδο λειτουργίας", - "sendWolPacket": "Αποστολή πακέτου WOL", - "sendWolPacketDesc": "Στείλτε ένα μαγικό πακέτο για να αφυπνίσετε αυτόν τον κεντρικό υπολογιστή πριν από τη σύνδεση", - "disableCopy": "Απενεργοποίηση αντιγραφής", - "disableCopyDesc": "Αποτροπή αντιγραφής κειμένου από την απομακρυσμένη συνεδρία", - "disablePaste": "Απενεργοποίηση επικόλλησης", - "disablePasteDesc": "Αποτροπή επικόλλησης κειμένου στην απομακρυσμένη συνεδρία", - "createPathIfMissing": "Δημιουργία διαδρομής εάν λείπει", - "createPathIfMissingDesc": "Αυτόματη δημιουργία του καταλόγου ηχογράφησης", - "excludeOutput": "Εξαίρεση εξόδου", - "excludeOutputDesc": "Μην καταγράφετε την έξοδο οθόνης (μόνο μεταδεδομένα)", - "excludeMouse": "Εξαίρεση ποντικιού", - "excludeMouseDesc": "Μην καταγράφετε τις κινήσεις του ποντικιού", - "includeKeystrokes": "Συμπερίληψη πληκτρολογήσεων", - "includeKeystrokesDesc": "Εγγραφή ακατέργαστων πληκτρολογήσεων εκτός από την έξοδο οθόνης", - "vncPort": "Θύρα VNC", - "vncPassword": "Κωδικός πρόσβασης VNC", - "vncUsernameOptional": "Όνομα χρήστη (προαιρετικό)", - "vncLeaveBlank": "Αφήστε κενό εάν δεν απαιτείται", - "cursorMode": "Λειτουργία δρομέα", - "swapRedBlue": "Εναλλαγή Κόκκινου/Μπλε", - "swapRedBlueDesc": "Εναλλαγή των καναλιών κόκκινου και μπλε χρώματος (διορθώνει ορισμένα προβλήματα χρώματος)", - "readOnly": "Μόνο για ανάγνωση", - "readOnlyDesc": "Δείτε την οθόνη του τηλεχειριστηρίου χωρίς να στείλετε καμία πληροφορία", - "telnetPort": "Θύρα Telnet", - "terminalType": "Τύπος τερματικού", - "fontName": "Όνομα γραμματοσειράς", - "fontSize": "Μέγεθος γραμματοσειράς", - "colorScheme": "Χρωματικό σχέδιο", - "backspaceKey": "Πλήκτρο Backspace", - "colorDepth": "Βάθος χρώματος", - "resizeMethod": "Μέθοδος αλλαγής μεγέθους", - "driveNamePlaceholder": "Οδός Τέρμιξ", - "options": { - "any": "Κάθε", - "auto": "Αυτο", - "nlaExtended": "Επέκταση NLA", - "color8": "8-bit", - "color16": "16-bit", - "color24": "24-bit", - "color32": "32-bit", - "displayUpdate": "Ενημέρωση οθόνης", - "reconnect": "Επανασύνδεση", - "preserve": "Διατηρώ", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Τοπικός", - "remote": "Μακρινός", - "failsafe": "ασφαλής", - "blackOnWhite": "Μαύρο σε άσπρο", - "whiteOnBlack": "Λευκό σε μαύρο", - "grayOnBlack": "Γκρι σε μαύρο", - "greenOnBlack": "Πράσινο σε μαύρο" - } - }, - "remoteDesktop": "Απομακρυσμένη επιφάνεια εργασίας", - "tabRdp": "ΠΑΑ", - "tabVnc": "VNC", - "tabTelnet": "Τέλνετ", - "virtualNetwork": "Εικονικό Δίκτυο", - "unencryptedShell": "Μη κρυπτογραφημένο κέλυφος", - "remoteDesktopNotes": "Σημειώσεις", - "userDefaultsNote": "Οι ρυθμίσεις που αφήνετε αμετάβλητες ακολουθούν τις προεπιλογές του κεντρικού υπολογιστή σας.", - "connectionOriginNote": "Στην εφαρμογή για υπολογιστές, τα RDP, VNC και Telnet χρησιμοποιούν τον απομακρυσμένο διακομιστή, εκτός εάν ο κεντρικός υπολογιστής έχει οριστεί σε Αυτή η συσκευή, η οποία απαιτεί ένα guacd που μπορεί να φτάσει σε αυτό το μηχάνημα. Ορίστε ένα στις ρυθμίσεις Απομακρυσμένης επιφάνειας εργασίας ή ανά κεντρικό υπολογιστή στην ενότητα Guacd Proxy.", - "docsLink": "Προβολή εγγράφων", - "connectionOriginGuacamoleNote": "Τα RDP, VNC και Telnet χρησιμοποιούν τον απομακρυσμένο διακομιστή, εκτός εάν επιλέξετε \"Αυτή η συσκευή\", κάτι που απαιτεί ένα guacd στο οποίο μπορεί να φτάσει αυτό το μηχάνημα. Ορίστε ένα στη ρύθμιση URL guacd ή ανά κεντρικό υπολογιστή στην ενότητα Guacd Proxy." - }, - "onboarding": { - "feature_desktop": "Απομακρυσμένη επιφάνεια εργασίας", - "feature_desktop_desc": "Ανοίξτε τις συνεδρίες RDP, VNC και Telnet σε μια καρτέλα, όπως και το SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Βάθος χρώματος", - "description": "Χρησιμοποιείται για κεντρικούς υπολογιστές RDP που το αφήνουν στην προεπιλεγμένη τιμή τους." - }, - "resizeMethod": { - "label": "Μέθοδος αλλαγής μεγέθους", - "description": "Πώς η απομακρυσμένη οθόνη ακολουθεί το μέγεθος του παραθύρου." - }, - "resizeDisplayUpdate": "Ενημέρωση οθόνης", - "resizeReconnect": "Επανασύνδεση", - "inherit": "Κληρονομώ", - "on": "Επί", - "off": "Μακριά από", - "forceLossless": { - "label": "Δύναμη χωρίς απώλειες", - "description": "Επιβολή κωδικοποίησης εικόνας χωρίς απώλειες (υψηλότερη ποιότητα, μεγαλύτερο εύρος ζώνης)" - }, - "enableWallpaper": { - "label": "Ταπετσαρία", - "description": "Εμφάνιση ταπετσαρίας επιφάνειας εργασίας (η απενεργοποίηση βελτιώνει την απόδοση)" - }, - "enableFontSmoothing": { - "label": "Εξομάλυνση γραμματοσειράς", - "description": "Ενεργοποίηση απόδοσης γραμματοσειράς ClearType" - }, - "enableDesktopComposition": { - "label": "Σύνθεση επιφάνειας εργασίας", - "description": "Ενεργοποίηση εφέ Aero glass" - }, - "disableAudio": { - "label": "Απενεργοποίηση ήχου", - "description": "Σίγαση όλου του ήχου από την απομακρυσμένη συνεδρία" - }, - "enablePrinting": { - "label": "Ενεργοποίηση εκτύπωσης", - "description": "Ανακατεύθυνση τοπικών εκτυπωτών στην απομακρυσμένη συνεδρία" - }, - "enableDrive": { - "label": "Ενεργοποίηση ανακατεύθυνσης μονάδας δίσκου", - "description": "Αντιστοίχιση τοπικού φακέλου ως μονάδας δίσκου στην απομακρυσμένη συνεδρία" - }, - "disableCopy": { - "label": "Απενεργοποίηση αντιγραφής", - "description": "Αποτροπή αντιγραφής κειμένου από την απομακρυσμένη συνεδρία" - }, - "disablePaste": { - "label": "Απενεργοποίηση επικόλλησης", - "description": "Αποτροπή επικόλλησης κειμένου στην απομακρυσμένη συνεδρία" - }, - "group": "Προεπιλογές απομακρυσμένης επιφάνειας εργασίας", - "colorDepth16": "16-bit", - "colorDepth24": "24-bit", - "colorDepth32": "32-bit" - }, - "admin": { - "enabled": { - "label": "Ενεργοποίηση απομακρυσμένης επιφάνειας εργασίας", - "description": "Επιτρέψτε συνδέσεις RDP, VNC και Telnet μέσω του guacd." - }, - "guacdUrl": { - "label": "URL guacd", - "description": "Όπου το guacd εκτελεί ακρόαση, ως host:port. Οι μεταβλητές περιβάλλοντος GUACD_URL, GUACD_HOST και GUACD_PORT το παρακάμπτουν.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "ΠΑΑ" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Τέλνετ" - }, - "rdpPort": { - "label": "Θύρα RDP" - }, - "vncPort": { - "label": "Θύρα VNC" - }, - "telnetPort": { - "label": "Θύρα Telnet" - }, - "rdpSecurity": { - "label": "Λειτουργία ασφαλείας RDP" - }, - "rdpIgnoreCert": { - "label": "Αγνόηση σφαλμάτων πιστοποιητικών" - }, - "guacamoleConfig": { - "label": "ρυθμίσεις guacd" - }, - "enableToolbar": { - "label": "Γραμμή εργαλείων απομακρυσμένης επιφάνειας εργασίας", - "description": "Εμφάνιση της γραμμής εργαλείων σε συνεδρίες RDP, VNC και Telnet." - }, - "displayGroup": "Επίδειξη", - "displayDefaultsTab": "Απομακρυσμένη επιφάνεια εργασίας" - } - }, - "palette": { - "connectRdp": "Σύνδεση με το RDP", - "connectVnc": "Σύνδεση με VNC", - "connectTelnet": "Σύνδεση με Telnet" - }, - "permissions": { - "sessions": { - "title": "Κοινή χρήση περιόδων λειτουργίας απομακρυσμένης επιφάνειας εργασίας", - "description": "Να επιτρέπεται η κοινή χρήση συνεδριών και οι χώροι συνεργασίας να εμφανίζουν τις συνεδρίες RDP, VNC και Telnet αυτού του χρήστη σε άλλους." - } - }, - "errors": { - "remoteServerRequired": "Απαιτείται απομακρυσμένος διακομιστής. Συνδέστε έναν απομακρυσμένο διακομιστή στις Ρυθμίσεις για να χρησιμοποιήσετε αυτόν τον τύπο σύνδεσης." - }, - "plugin": { - "name": "Απομακρυσμένη επιφάνεια εργασίας" - } -} diff --git a/plugins/remote-desktop/locales/translated/es_ES.json b/plugins/remote-desktop/locales/translated/es_ES.json deleted file mode 100644 index ba54e4848..000000000 --- a/plugins/remote-desktop/locales/translated/es_ES.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Conectando a la sesión {{type}}...", - "checkingGuacd": "Comprobando el servicio de escritorio remoto...", - "requestingToken": "Solicitando el token de conexión {{type}}...", - "openingSession": "Abriendo {{type}} sesión...", - "negotiating": "Negociando el apretón de manos {{type}}...", - "sessionReady": "{{type}} sesión lista", - "connectionError": "Error de conexión", - "connectionFailed": "Conexión fallida", - "failedToConnect": "Error al obtener el token de conexión", - "hostNotFound": "Host no encontrado", - "noHostSelected": "Ningún host seleccionado", - "reconnect": "Reconectar", - "retry": "Reintentar", - "guacdUnavailable": "El servicio de escritorio remoto (guacd) no está disponible. Por favor, asegúrate de que guacd se está ejecutando, es accesible y está correctamente configurado en los ajustes de administración.", - "credentialPromptTitle": "Introduzca las credenciales de RDP", - "credentialPromptDescription": "Este host está configurado para solicitar credenciales al conectarse. Estas se utilizan únicamente para esta sesión y no se guardan.", - "connect": "Conectar", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Archivos de Drive", - "parent": "Directorio principal", - "refresh": "Actualizar", - "empty": "Esta carpeta está vacía", - "listFailed": "No se pudo leer esta carpeta.", - "download": "Descargar", - "downloadFailed": "Error al descargar {{name}}", - "upload": "Subir archivos", - "uploading": "Subiendo {{name}}...", - "uploaded": "Subido {{name}}", - "uploadFailed": "Error al cargar {{name}}", - "uploadDisabled": "Las cargas están deshabilitadas para esta conexión.", - "driveUnavailable": "Habilite la redirección de unidades RDP antes de cargar archivos.", - "driveNotWritable": "guacd no puede escribir en la carpeta de la unidad. Compruebe GUACD_DRIVE_PATH (o la ruta de la unidad del host) y sus permisos.", - "dropToUpload": "Arrastra y suelta los archivos para subirlos" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Bloquear pantalla)", - "winKey": "Tecla Windows", - "superL": "Super+L (Pantalla de bloqueo)", - "superKey": "Súper llave", - "super": "Súper", - "cmdL": "Cmd+L (Pantalla de bloqueo)", - "cmdKey": "Tecla de comando", - "cmd": "Comando", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Mayús", - "win": "Win", - "stickyActive": "{{key}} (activado - clic para soltar)", - "stickyInactive": "{{key}} (clic para activar)", - "esc": "Escape", - "tab": "Tab", - "home": "Inicio", - "end": "Fin", - "pageUp": "Re Pág", - "pageDown": "Av Pág", - "arrowUp": "Flecha arriba", - "arrowDown": "Flecha abajo", - "arrowLeft": "Flecha izquierda", - "arrowRight": "Flecha derecha", - "fnToggle": "Teclas de función", - "reconnect": "Reconectar sesión", - "collapse": "Contraer barra de herramientas", - "expand": "Expandir barra de herramientas", - "dragHandle": "Arrastrar para reposicionar", - "hide": "Ocultar para esta sesión. Para desactivarlo permanentemente, abra Configuración del host → General → Barra de herramientas de conexión.", - "zoomIn": "Dar un golpe de zoom", - "zoomOut": "Aleja la vista", - "resetZoom": "Restablecer zoom", - "fitToScreen": "Ajustar a la pantalla", - "switchToTrackpad": "Cambia al modo panel táctil (arrastra para mover el cursor, toca para hacer clic).", - "switchToTouch": "Cambia al modo táctil (pulsa directamente donde quieras hacer clic)." - }, - "disabled": "El Escritorio remoto está desactivado. Un administrador puede activarlo en la configuración." - }, - "hosts": { - "nativeRdpOpened": "Abierto en Escritorio remoto de Windows", - "nativeRdpFailed": "No se pudo abrir el Escritorio remoto de Windows.", - "openNativeRdp": "Abrir en Escritorio remoto de Windows", - "guac": { - "toolbar": "Barra de herramientas", - "connection": "Conexión", - "authentication": "Autenticación", - "storedCredential": "Credencial almacenada", - "authMethod": "Método de autenticación", - "authTypeDirect": "Directa", - "authTypeCredential": "Credencial", - "authTypeNone": "Ninguna", - "authTypeNoneDesc": "No se almacenan credenciales. Se le solicitará un nombre de usuario y una contraseña cada vez que se conecte; estos no se guardan.", - "selectCredential": "Seleccionar una credencial...", - "connectionSettings": "Configuración de conexión", - "displaySettings": "Configuración de pantalla", - "audioSettings": "Configuración de audio", - "rdpPerformance": "Rendimiento RDP", - "deviceRedirection": "Redirección de dispositivos", - "session": "Sesión", - "gateway": "Gateway", - "remoteApp": "RemoteApp", - "clipboard": "Portapapeles", - "sessionRecording": "Grabación de sesión", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Configuración de VNC", - "terminalSettings": "Configuración de terminal", - "rdpPort": "Puerto RDP", - "username": "Nombre de usuario", - "password": "Contraseña", - "passwordSaved": "Contraseña guardada, escriba para cambiarla", - "domain": "Dominio", - "securityMode": "Modo de seguridad", - "width": "Ancho", - "height": "Alto", - "dpi": "DPI", - "clientName": "Nombre del cliente", - "initialProgram": "Programa inicial", - "serverLayout": "Disposición del teclado", - "timezone": "Zona horaria", - "loadBalanceInfo": "Información de balanceo de carga / Cookie", - "loadBalanceInfoDesc": "Cookie del agente de conexiones a Escritorio Remoto para el balanceo de carga de la granja RDS (p. ej. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Proxy de guacd", - "guacdHostname": "Host de guacd", - "guacdHostnamePlaceholder": "Predeterminado global", - "guacdPort": "Puerto de guacd", - "guacdProxyDesc": "Sobrescribir la instancia global de guacd para esta conexión. Dejar en blanco para usar la configuración global de guacd.", - "gatewayHostname": "Nombre de host de la puerta de enlace", - "gatewayPort": "Puerto de la puerta de enlace", - "gatewayUsername": "Nombre de usuario de la puerta de enlace", - "gatewayPassword": "Contraseña de la puerta de enlace", - "gatewayDomain": "Dominio de la puerta de enlace", - "remoteAppProgram": "Programa RemoteApp", - "workingDirectory": "Directorio de trabajo", - "arguments": "Argumentos", - "normalizeLineEndings": "Normalizar finales de línea", - "macAddress": "Dirección MAC", - "broadcastAddress": "Dirección de difusión", - "udpPort": "Puerto UDP", - "waitTimeS": "Tiempo de espera (s)", - "driveName": "Nombre de la unidad", - "drivePath": "Ruta de la unidad", - "ignoreCertificate": "Ignorar certificado", - "ignoreCertificateDesc": "Permitir conexiones a hosts con certificados autofirmados", - "forceLossless": "Forzar sin pérdida", - "forceLosslessDesc": "Forzar codificación de imagen sin pérdida (más calidad, más ancho de banda)", - "disableAudio": "Desactivar audio", - "disableAudioDesc": "Silenciar todo el audio de la sesión remota", - "enableAudioInput": "Activar entrada de audio (micrófono)", - "enableAudioInputDesc": "Reenviar el micrófono local a la sesión remota", - "wallpaper": "Fondo de pantalla", - "wallpaperDesc": "Mostrar fondo de escritorio (desactivarlo mejora el rendimiento)", - "theming": "Temas", - "themingDesc": "Activar temas y estilos visuales", - "fontSmoothing": "Suavizado de fuentes", - "fontSmoothingDesc": "Activar renderizado de fuentes ClearType", - "fullWindowDrag": "Arrastre de ventana completa", - "fullWindowDragDesc": "Mostrar el contenido de la ventana al arrastrar", - "desktopComposition": "Composición de escritorio", - "desktopCompositionDesc": "Activar efectos Aero Glass", - "menuAnimations": "Animaciones de menú", - "menuAnimationsDesc": "Activar animaciones de fundido y deslizamiento de menú", - "disableBitmapCaching": "Desactivar caché de mapa de bits", - "disableBitmapCachingDesc": "Desactivar la caché de mapa de bits (puede ayudar con fallos)", - "disableOffscreenCaching": "Desactivar caché fuera de pantalla", - "disableOffscreenCachingDesc": "Desactivar la caché fuera de pantalla", - "disableGlyphCaching": "Desactivar caché de glifos", - "disableGlyphCachingDesc": "Desactivar la caché de glifos", - "enableGfx": "Activar GFX", - "enableGfxDesc": "Usar la canalización de gráficos RemoteFX", - "enablePrinting": "Activar impresión", - "enablePrintingDesc": "Redirigir las impresoras locales a la sesión remota", - "enableDriveRedirection": "Activar redirección de unidades", - "enableDriveRedirectionDesc": "Asignar una carpeta local como una unidad en la sesión remota", - "createDrivePath": "Crear ruta de unidad", - "createDrivePathDesc": "Crear automáticamente la carpeta si no existe", - "disableDownload": "Deshabilitar descarga", - "disableDownloadDesc": "Impedir la descarga de archivos de la sesión remota", - "disableUpload": "Deshabilitar subida", - "disableUploadDesc": "Impedir la subida de archivos a la sesión remota", - "enableTouch": "Habilitar táctil", - "enableTouchDesc": "Habilitar el reenvío de entrada táctil", - "consoleSession": "Sesión de consola", - "consoleSessionDesc": "Conectar a la consola (sesión 0) en lugar de una nueva sesión", - "sendWolPacket": "Enviar paquete WOL", - "sendWolPacketDesc": "Enviar un paquete mágico para encender este equipo antes de conectar", - "disableCopy": "Deshabilitar copia", - "disableCopyDesc": "Impedir la copia de texto de la sesión remota", - "disablePaste": "Deshabilitar pegado", - "disablePasteDesc": "Impedir el pegado de texto en la sesión remota", - "createPathIfMissing": "Crear ruta si no existe", - "createPathIfMissingDesc": "Crear automáticamente el directorio de grabación", - "excludeOutput": "Excluir salida", - "excludeOutputDesc": "No grabar la salida de pantalla (solo metadatos)", - "excludeMouse": "Excluir ratón", - "excludeMouseDesc": "No grabar los movimientos del ratón", - "includeKeystrokes": "Incluir pulsaciones de teclas", - "includeKeystrokesDesc": "Grabar las pulsaciones de teclas en bruto además de la salida de pantalla", - "vncPort": "Puerto VNC", - "vncPassword": "Contraseña VNC", - "vncUsernameOptional": "Nombre de usuario (opcional)", - "vncLeaveBlank": "Dejar en blanco si no es necesario", - "cursorMode": "Modo de cursor", - "swapRedBlue": "Intercambiar rojo/azul", - "swapRedBlueDesc": "Intercambiar los canales de color rojo y azul (corrige algunos problemas de color)", - "readOnly": "Solo lectura", - "readOnlyDesc": "Ver la pantalla remota sin enviar ninguna entrada", - "telnetPort": "Puerto Telnet", - "terminalType": "Tipo de terminal", - "fontName": "Nombre de fuente", - "fontSize": "Tamaño de fuente", - "colorScheme": "Esquema de color", - "backspaceKey": "Tecla de retroceso", - "colorDepth": "Profundidad de color", - "resizeMethod": "Método de redimensionamiento", - "driveNamePlaceholder": "Unidad Termix", - "options": { - "any": "Cualquier", - "auto": "Automático", - "nlaExtended": "NLA Extendido", - "color8": "8 bits", - "color16": "16 bits", - "color24": "24 bits", - "color32": "32 bits", - "displayUpdate": "Actualización de pantalla", - "reconnect": "Reconectar", - "preserve": "Preservar", - "unix": "Unix (LF)", - "windows": "Ventanas (CRLF)", - "local": "Local", - "remote": "Remoto", - "failsafe": "a prueba de fallos", - "blackOnWhite": "Negro sobre blanco", - "whiteOnBlack": "Blanco sobre negro", - "grayOnBlack": "Gris sobre negro", - "greenOnBlack": "Verde sobre negro" - } - }, - "remoteDesktop": "Escritorio remoto", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Red virtual", - "unencryptedShell": "Shell sin cifrar", - "remoteDesktopNotes": "Notas", - "userDefaultsNote": "Los ajustes que no modifiques seguirán la configuración predeterminada de tu servidor.", - "connectionOriginNote": "En la aplicación de escritorio, RDP, VNC y Telnet usan el servidor remoto a menos que el host esté configurado como Este dispositivo, que necesita un proxy guacd al que esta máquina pueda acceder. Configure uno en la configuración de Escritorio remoto o por host en Proxy guacd.", - "docsLink": "Ver documentación", - "connectionOriginGuacamoleNote": "RDP, VNC y Telnet utilizan el servidor remoto a menos que seleccione \"Este dispositivo\", en cuyo caso se necesita un servidor guacd al que esta máquina pueda acceder. Configure uno en la configuración de URL de guacd o por host en la configuración de proxy de guacd." - }, - "onboarding": { - "feature_desktop": "Escritorio remoto", - "feature_desktop_desc": "Abre sesiones RDP, VNC y Telnet en una pestaña, igual que con SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Profundidad de color", - "description": "Se utiliza para hosts RDP que lo dejan en su configuración predeterminada." - }, - "resizeMethod": { - "label": "Método de redimensionamiento", - "description": "Cómo la pantalla remota se ajusta al tamaño de la ventana." - }, - "resizeDisplayUpdate": "Actualización de pantalla", - "resizeReconnect": "Reconectar", - "inherit": "Heredar", - "on": "Activado", - "off": "Desactivado", - "forceLossless": { - "label": "Forzar sin pérdida", - "description": "Forzar codificación de imagen sin pérdida (más calidad, más ancho de banda)" - }, - "enableWallpaper": { - "label": "Fondo de pantalla", - "description": "Mostrar fondo de escritorio (desactivarlo mejora el rendimiento)" - }, - "enableFontSmoothing": { - "label": "Suavizado de fuentes", - "description": "Activar renderizado de fuentes ClearType" - }, - "enableDesktopComposition": { - "label": "Composición de escritorio", - "description": "Activar efectos Aero Glass" - }, - "disableAudio": { - "label": "Desactivar audio", - "description": "Silenciar todo el audio de la sesión remota" - }, - "enablePrinting": { - "label": "Activar impresión", - "description": "Redirigir las impresoras locales a la sesión remota" - }, - "enableDrive": { - "label": "Activar redirección de unidades", - "description": "Asignar una carpeta local como una unidad en la sesión remota" - }, - "disableCopy": { - "label": "Deshabilitar copia", - "description": "Impedir la copia de texto de la sesión remota" - }, - "disablePaste": { - "label": "Deshabilitar pegado", - "description": "Impedir el pegado de texto en la sesión remota" - }, - "group": "Configuración predeterminada de Escritorio remoto", - "colorDepth16": "16 bits", - "colorDepth24": "24 bits", - "colorDepth32": "32 bits" - }, - "admin": { - "enabled": { - "label": "Habilitar Escritorio remoto", - "description": "Permitir conexiones RDP, VNC y Telnet a través de guacd." - }, - "guacdUrl": { - "label": "URL de guacd", - "description": "Donde guacd escucha, como host:puerto. Las variables de entorno GUACD_URL, GUACD_HOST y GUACD_PORT anulan esto.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "Puerto RDP" - }, - "vncPort": { - "label": "puerto VNC" - }, - "telnetPort": { - "label": "Puerto Telnet" - }, - "rdpSecurity": { - "label": "Modo de seguridad RDP" - }, - "rdpIgnoreCert": { - "label": "Ignorar errores de certificado" - }, - "guacamoleConfig": { - "label": "Configuración de guacd" - }, - "enableToolbar": { - "label": "Barra de herramientas de escritorio remoto", - "description": "Mostrar la barra de herramientas en las sesiones RDP, VNC y Telnet." - }, - "displayGroup": "Mostrar", - "displayDefaultsTab": "Escritorio remoto" - } - }, - "palette": { - "connectRdp": "Conéctese mediante RDP", - "connectVnc": "Conéctate con VNC", - "connectTelnet": "Conéctese con Telnet" - }, - "permissions": { - "sessions": { - "title": "Compartir sesiones de escritorio remoto", - "description": "Permitir que las salas de colaboración y de uso compartido de sesiones muestren las sesiones RDP, VNC y Telnet de este usuario a otros." - } - }, - "errors": { - "remoteServerRequired": "Se requiere un servidor remoto. Conecte un servidor remoto en Configuración para usar este tipo de conexión." - }, - "plugin": { - "name": "Escritorio remoto" - } -} diff --git a/plugins/remote-desktop/locales/translated/fi_FI.json b/plugins/remote-desktop/locales/translated/fi_FI.json deleted file mode 100644 index a75328463..000000000 --- a/plugins/remote-desktop/locales/translated/fi_FI.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Yhdistetään {{type}} -istuntoon...", - "checkingGuacd": "Tarkistetaan etätyöpöytäpalvelua...", - "requestingToken": "Pyydetään {{type}} yhteystunnusta...", - "openingSession": "Avataan {{type}} -istuntoa...", - "negotiating": "Neuvottelen {{type}} kädenpuristuksesta...", - "sessionReady": "{{type}} istunto valmis", - "connectionError": "Yhteysvirhe", - "connectionFailed": "Yhteys epäonnistui", - "failedToConnect": "Yhteystunnuksen hakeminen epäonnistui", - "hostNotFound": "Isäntää ei löydy", - "noHostSelected": "Ei valittua isäntää", - "reconnect": "Yhdistä uudelleen", - "retry": "Yritä uudelleen", - "guacdUnavailable": "Etätyöpöytäpalvelu (guacd) ei ole käytettävissä. Varmista, että guacd on käynnissä, käytettävissä ja määritetty oikein järjestelmänvalvojan asetuksissa.", - "credentialPromptTitle": "Syötä RDP-tunnistetiedot", - "credentialPromptDescription": "Tämä isäntä on asetettu kysymään tunnistetietoja yhteyden muodostamisen yhteydessä. Niitä käytetään vain tässä istunnossa, eikä niitä tallenneta.", - "connect": "Yhdistä", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Drive-tiedostot", - "parent": "Ylähakemisto", - "refresh": "Päivitä", - "empty": "Tämä kansio on tyhjä", - "listFailed": "Tämän kansion lukeminen epäonnistui", - "download": "Lataa", - "downloadFailed": "Lataus epäonnistui {{name}}", - "upload": "Lataa tiedostoja", - "uploading": "Ladataan {{name}}...", - "uploaded": "Ladattu {{name}}", - "uploadFailed": "Lataus epäonnistui {{name}}", - "uploadDisabled": "Lähetykset on poistettu käytöstä tällä yhteydellä", - "driveUnavailable": "Ota RDP-aseman uudelleenohjaus käyttöön ennen tiedostojen lataamista", - "driveNotWritable": "guacd ei voi kirjoittaa asemakansioon. Tarkista GUACD_DRIVE_PATH (tai isännän asemapolku) ja sen käyttöoikeudet.", - "dropToUpload": "Pudota tiedostot ladattavaksi" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Lukitusnäyttö)", - "winKey": "Windows-näppäin", - "superL": "Super+L (Lukitusnäyttö)", - "superKey": "Superavain", - "super": "Super", - "cmdL": "Cmd+L (Lukitusnäyttö)", - "cmdKey": "Komento-näppäin", - "cmd": "Komento", - "ctrl": "Ctrl", - "alt": "Vaihtoehto", - "shift": "Siirtää", - "win": "Voittaa", - "stickyActive": "{{key}} (lukittu - napsauta vapauttaaksesi)", - "stickyInactive": "{{key}} (napsauta lukitaksesi)", - "esc": "Paeta", - "tab": "Välilehti", - "home": "Kotiin", - "end": "Loppu", - "pageUp": "Sivu ylös", - "pageDown": "Sivu alas", - "arrowUp": "Nuoli ylös", - "arrowDown": "Nuoli alas", - "arrowLeft": "Nuoli vasemmalle", - "arrowRight": "Nuoli oikealle", - "fnToggle": "Toimintonäppäimet", - "reconnect": "Yhdistä istunto uudelleen", - "collapse": "Pienennä työkalupalkki", - "expand": "Laajenna työkalupalkkia", - "dragHandle": "Vedä siirtääksesi", - "hide": "Piilota tältä istunnolta. Voit poistaa sen käytöstä pysyvästi avaamalla Isäntäasetukset → Yleiset → Yhteystyökalurivi.", - "zoomIn": "Lähennä", - "zoomOut": "Loitonna", - "resetZoom": "Palauta zoomaus", - "fitToScreen": "Sovita näyttöön", - "switchToTrackpad": "Vaihda ohjauslevytilaan (vedä siirtääksesi kohdistinta, napauta napsauttamalla)", - "switchToTouch": "Vaihda kosketustilaan (napauta suoraan haluamaasi kohtaan)" - }, - "disabled": "Etätyöpöytä on poistettu käytöstä. Ylläpitäjä voi ottaa sen käyttöön asetuksissa." - }, - "hosts": { - "nativeRdpOpened": "Avattu Windowsin etätyöpöydällä", - "nativeRdpFailed": "Windowsin etätyöpöydän avaaminen epäonnistui", - "openNativeRdp": "Avaa Windowsin etätyöpöydällä", - "guac": { - "toolbar": "Työkalurivi", - "connection": "Yhteys", - "authentication": "Todennus", - "storedCredential": "Tallennetut tunnistetiedot", - "authMethod": "Todennustapa", - "authTypeDirect": "Suoraan", - "authTypeCredential": "Valtakirja", - "authTypeNone": "Ei mitään", - "authTypeNoneDesc": "Tunnuksia ei tallenneta. Sinulta kysytään käyttäjätunnusta ja salasanaa aina, kun muodostat yhteyden; niitä ei tallenneta.", - "selectCredential": "Valitse tunnistetiedot...", - "connectionSettings": "Yhteysasetukset", - "displaySettings": "Näyttöasetukset", - "audioSettings": "Ääniasetukset", - "rdpPerformance": "RDP-suorituskyky", - "deviceRedirection": "Laitteen uudelleenohjaus", - "session": "Istunto", - "gateway": "Yhdyskäytävä", - "remoteApp": "Etäsovellus", - "clipboard": "Leikepöytä", - "sessionRecording": "Istunnon tallennus", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC-asetukset", - "terminalSettings": "Pääteasetukset", - "rdpPort": "RDP-portti", - "username": "Käyttäjätunnus", - "password": "Salasana", - "passwordSaved": "Salasana tallennettu, kirjoita muuttaaksesi", - "domain": "Verkkotunnus", - "securityMode": "Suojaustila", - "width": "Leveys", - "height": "Korkeus", - "dpi": "DPI", - "clientName": "Asiakkaan nimi", - "initialProgram": "Alkuperäinen ohjelma", - "serverLayout": "Palvelimen asettelu", - "timezone": "Aikavyöhyke", - "loadBalanceInfo": "Kuormituksen tasapainotustiedot / eväste", - "loadBalanceInfoDesc": "RD Connection Broker -eväste RDS-farmin kuormituksen tasapainottamista varten (esim. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd-välityspalvelin", - "guacdHostname": "guacd-isäntä", - "guacdHostnamePlaceholder": "Globaali oletusarvo", - "guacdPort": "guacdin satama", - "guacdProxyDesc": "Ohita tämän yhteyden globaali guacd-instanssi. Jätä tyhjäksi käyttääksesi globaalisti määritettyä guacd-instanssia.", - "gatewayHostname": "Yhdyskäytävän isäntänimi", - "gatewayPort": "Yhdyskäytäväportti", - "gatewayUsername": "Yhdyskäytävän käyttäjätunnus", - "gatewayPassword": "Yhdyskäytävän salasana", - "gatewayDomain": "Yhdyskäytävän verkkotunnus", - "remoteAppProgram": "RemoteApp-ohjelma", - "workingDirectory": "Työhakemisto", - "arguments": "Argumentit", - "normalizeLineEndings": "Normalisoi rivinpäätteet", - "macAddress": "MAC-osoite", - "broadcastAddress": "Lähetysosoite", - "udpPort": "UDP-portti", - "waitTimeS": "Odotusaika (s)", - "driveName": "Aseman nimi", - "drivePath": "Ajopolku", - "ignoreCertificate": "Ohita varmenne", - "ignoreCertificateDesc": "Salli yhteydet itse allekirjoitettuja varmenteita käyttäviin isäntiin", - "forceLossless": "Pakota häviötön", - "forceLosslessDesc": "Pakota häviötön kuvan koodaus (korkeampi laatu, enemmän kaistanleveyttä)", - "disableAudio": "Poista ääni käytöstä", - "disableAudioDesc": "Mykistä kaikki etäistunnon äänet", - "enableAudioInput": "Ota äänitulo käyttöön (mikrofoni)", - "enableAudioInputDesc": "Lähetä paikallinen mikrofoni etäistuntoon", - "wallpaper": "Taustakuva", - "wallpaperDesc": "Näytä työpöydän taustakuva (poistaminen käytöstä parantaa suorituskykyä)", - "theming": "Teemat", - "themingDesc": "Ota käyttöön visuaaliset teemat ja tyylit", - "fontSmoothing": "Fontin pehmennys", - "fontSmoothingDesc": "Ota ClearType-fonttien renderöinti käyttöön", - "fullWindowDrag": "Koko ikkunan vetäminen", - "fullWindowDragDesc": "Näytä ikkunan sisältö vetämisen aikana", - "desktopComposition": "Työpöydän sommittelu", - "desktopCompositionDesc": "Ota Aero Glass -tehosteet käyttöön", - "menuAnimations": "Valikkoanimaatiot", - "menuAnimationsDesc": "Ota käyttöön valikoiden häivytys ja dia-animaatiot", - "disableBitmapCaching": "Poista bittikarttavälimuisti käytöstä", - "disableBitmapCachingDesc": "Poista bittikarttavälimuisti käytöstä (saattaa auttaa häiriöiden kanssa)", - "disableOffscreenCaching": "Poista käytöstä näytön ulkopuolinen välimuisti", - "disableOffscreenCachingDesc": "Poista käytöstä näytön ulkopuolinen välimuisti", - "disableGlyphCaching": "Poista käytöstä glyfien välimuisti", - "disableGlyphCachingDesc": "Poista merkkijonojen välimuisti käytöstä", - "enableGfx": "Ota käyttöön näytönsäästäjä", - "enableGfxDesc": "Käytä RemoteFX-grafiikkaputkea", - "enablePrinting": "Ota tulostus käyttöön", - "enablePrintingDesc": "Ohjaa paikalliset tulostimet etäistuntoon", - "enableDriveRedirection": "Ota aseman uudelleenohjaus käyttöön", - "enableDriveRedirectionDesc": "Yhdistä paikallinen kansio asemaksi etäistunnossa", - "createDrivePath": "Luo ajopolku", - "createDrivePathDesc": "Luo kansio automaattisesti, jos sitä ei ole olemassa", - "disableDownload": "Poista lataus käytöstä", - "disableDownloadDesc": "Estä tiedostojen lataaminen etäistunnosta", - "disableUpload": "Poista lähetys käytöstä", - "disableUploadDesc": "Estä tiedostojen lataaminen etäistuntoon", - "enableTouch": "Ota kosketus käyttöön", - "enableTouchDesc": "Ota kosketussyötteen edelleenlähetys käyttöön", - "consoleSession": "Konsoli-istunto", - "consoleSessionDesc": "Yhdistä konsoliin (istunto 0) uuden istunnon sijaan", - "sendWolPacket": "Lähetä WOL-paketti", - "sendWolPacketDesc": "Lähetä taikapaketti herättääksesi tämän isännän ennen yhteyden muodostamista", - "disableCopy": "Poista kopiointi käytöstä", - "disableCopyDesc": "Estä tekstin kopioiminen etäistunnosta", - "disablePaste": "Poista liittäminen käytöstä", - "disablePasteDesc": "Estä tekstin liittäminen etäistuntoon", - "createPathIfMissing": "Luo polku, jos se puuttuu", - "createPathIfMissingDesc": "Luo tallennushakemisto automaattisesti", - "excludeOutput": "Sulje pois tuloste", - "excludeOutputDesc": "Älä tallenna näytön tulostetta (vain metatiedot)", - "excludeMouse": "Sulje hiiri pois", - "excludeMouseDesc": "Älä tallenna hiiren liikkeitä", - "includeKeystrokes": "Sisällytä näppäinpainallukset", - "includeKeystrokesDesc": "Tallenna raakat näppäinpainallukset näytön tulosteen lisäksi", - "vncPort": "VNC-portti", - "vncPassword": "VNC-salasana", - "vncUsernameOptional": "Käyttäjätunnus (valinnainen)", - "vncLeaveBlank": "Jätä tyhjäksi, jos ei pakollinen", - "cursorMode": "Kohdistimen tila", - "swapRedBlue": "Vaihda punainen/sininen", - "swapRedBlueDesc": "Vaihda punaisen ja sinisen värikanavan paikkaa (korjaa joitakin väriongelmia)", - "readOnly": "Vain lukuoikeus", - "readOnlyDesc": "Katso etänäyttöä lähettämättä mitään syötettä", - "telnetPort": "Telnet-portti", - "terminalType": "Päätetyyppi", - "fontName": "Fontin nimi", - "fontSize": "Fonttikoko", - "colorScheme": "Värimaailma", - "backspaceKey": "Askelpalautin-näppäin", - "colorDepth": "Värisyvyys", - "resizeMethod": "Koon muuttamismenetelmä", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Mikä tahansa", - "auto": "Auto", - "nlaExtended": "NLA Laajennettu", - "color8": "8-bittinen", - "color16": "16-bittinen", - "color24": "24-bittinen", - "color32": "32-bittinen", - "displayUpdate": "Näyttöpäivitys", - "reconnect": "Yhdistä uudelleen", - "preserve": "Säilyttää", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Paikallinen", - "remote": "Kauko", - "failsafe": "vikaturvallinen", - "blackOnWhite": "Mustaa valkoisella", - "whiteOnBlack": "Valkoinen mustalla", - "grayOnBlack": "Harmaa mustalla pohjalla", - "greenOnBlack": "Vihreä mustalla" - } - }, - "remoteDesktop": "Etätyöpöytä", - "tabRdp": "Maaseudun kehittämisohjelma", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Virtuaaliverkko", - "unencryptedShell": "Salaamaton kuori", - "remoteDesktopNotes": "Muistiinpanoja", - "userDefaultsNote": "Muuttamatta jätetyt asetukset noudattavat isäntäsi oletusasetuksia.", - "connectionOriginNote": "Työpöytäsovelluksessa RDP, VNC ja Telnet käyttävät etäpalvelinta, ellei isäntäasetukseksi ole asetettu Tämä laite, jolloin tarvitaan GUACD, johon tämä kone voi tavoittaa. Aseta sellainen etätyöpöytäasetuksissa tai isäntäkohtaisesti GUACD-välityspalvelimen alla.", - "docsLink": "Näytä dokumentit", - "connectionOriginGuacamoleNote": "RDP, VNC ja Telnet käyttävät etäpalvelinta, ellet valitse \"Tämä laite\", jolloin tarvitset GUACD:n, johon tämä kone voi tavoittaa. Aseta sellainen GUACD:n URL-asetuksissa tai isäntäkohtaisesti GUACD-välityspalvelimen alla." - }, - "onboarding": { - "feature_desktop": "Etätyöpöytä", - "feature_desktop_desc": "Avaa RDP-, VNC- ja Telnet-istunnot välilehdellä, samalla tavalla kuin SSH:ssa." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Värisyvyys", - "description": "Käytetään RDP-isännille, jotka jättävät sen oletusarvoiseksi." - }, - "resizeMethod": { - "label": "Koon muuttamismenetelmä", - "description": "Miten etänäyttö seuraa ikkunan kokoa." - }, - "resizeDisplayUpdate": "Näyttöpäivitys", - "resizeReconnect": "Yhdistä uudelleen", - "inherit": "Periä", - "on": "Päällä", - "off": "Pois", - "forceLossless": { - "label": "Pakota häviötön", - "description": "Pakota häviötön kuvan koodaus (korkeampi laatu, enemmän kaistanleveyttä)" - }, - "enableWallpaper": { - "label": "Taustakuva", - "description": "Näytä työpöydän taustakuva (poistaminen käytöstä parantaa suorituskykyä)" - }, - "enableFontSmoothing": { - "label": "Fontin pehmennys", - "description": "Ota ClearType-fonttien renderöinti käyttöön" - }, - "enableDesktopComposition": { - "label": "Työpöydän sommittelu", - "description": "Ota Aero Glass -tehosteet käyttöön" - }, - "disableAudio": { - "label": "Poista ääni käytöstä", - "description": "Mykistä kaikki etäistunnon äänet" - }, - "enablePrinting": { - "label": "Ota tulostus käyttöön", - "description": "Ohjaa paikalliset tulostimet etäistuntoon" - }, - "enableDrive": { - "label": "Ota aseman uudelleenohjaus käyttöön", - "description": "Yhdistä paikallinen kansio asemaksi etäistunnossa" - }, - "disableCopy": { - "label": "Poista kopiointi käytöstä", - "description": "Estä tekstin kopioiminen etäistunnosta" - }, - "disablePaste": { - "label": "Poista liittäminen käytöstä", - "description": "Estä tekstin liittäminen etäistuntoon" - }, - "group": "Etätyöpöydän oletusasetukset", - "colorDepth16": "16-bittinen", - "colorDepth24": "24-bittinen", - "colorDepth32": "32-bittinen" - }, - "admin": { - "enabled": { - "label": "Ota etätyöpöytä käyttöön", - "description": "Salli RDP-, VNC- ja Telnet-yhteydet guacdin kautta." - }, - "guacdUrl": { - "label": "guacd-URL-osoite", - "description": "Missä guacd kuuntelee, muodossa host:port. GUACD_URL-, GUACD_HOST- ja GUACD_PORT-ympäristömuuttujat korvaavat tämän.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "Maaseudun kehittämisohjelma" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP-portti" - }, - "vncPort": { - "label": "VNC-portti" - }, - "telnetPort": { - "label": "Telnet-portti" - }, - "rdpSecurity": { - "label": "RDP-suojaustila" - }, - "rdpIgnoreCert": { - "label": "Ohita varmennevirheet" - }, - "guacamoleConfig": { - "label": "guacd-asetukset" - }, - "enableToolbar": { - "label": "Etätyöpöydän työkalupalkki", - "description": "Näytä työkalupalkki RDP-, VNC- ja Telnet-istunnoissa." - }, - "displayGroup": "Näyttö", - "displayDefaultsTab": "Etätyöpöytä" - } - }, - "palette": { - "connectRdp": "Yhdistä RDP:hen", - "connectVnc": "Yhdistä VNC:hen", - "connectTelnet": "Yhdistä Telnetillä" - }, - "permissions": { - "sessions": { - "title": "Jaa etätyöpöytäistuntoja", - "description": "Salli istuntojen jakamisen ja yhteistyöhuoneiden näyttää tämän käyttäjän RDP-, VNC- ja Telnet-istunnot muille." - } - }, - "errors": { - "remoteServerRequired": "Etäpalvelin vaaditaan. Yhdistä etäpalvelin asetuksissa käyttääksesi tätä yhteystyyppiä." - }, - "plugin": { - "name": "Etätyöpöytä" - } -} diff --git a/plugins/remote-desktop/locales/translated/fr_FR.json b/plugins/remote-desktop/locales/translated/fr_FR.json deleted file mode 100644 index b3cc4be50..000000000 --- a/plugins/remote-desktop/locales/translated/fr_FR.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Connexion à la session {{type}}...", - "checkingGuacd": "Vérification du service de bureau à distance...", - "requestingToken": "Demande de jeton de connexion {{type}}...", - "openingSession": "Ouverture de la session {{type}}...", - "negotiating": "Negotiating {{type}} handshake...", - "sessionReady": "{{type}} session prête", - "connectionError": "Erreur de connexion", - "connectionFailed": "Échec de la connexion", - "failedToConnect": "Échec de l'obtention du jeton de connexion", - "hostNotFound": "Hôte introuvable", - "noHostSelected": "Aucun hôte sélectionné", - "reconnect": "Reconnecter", - "retry": "Réessayer", - "guacdUnavailable": "Le service de bureau à distance (guacd) n'est pas disponible. Veuillez vous assurer que guacd est en cours d'exécution, accessible et correctement configuré dans les paramètres administrateur.", - "credentialPromptTitle": "Saisir les informations d'identification RDP", - "credentialPromptDescription": "Cet hôte est configuré pour demander des identifiants lors de la connexion. Ces identifiants sont utilisés uniquement pour cette session et ne sont pas enregistrés.", - "connect": "Se connecter", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Fichiers Drive", - "parent": "Répertoire parent", - "refresh": "Actualiser", - "empty": "Ce dossier est vide", - "listFailed": "Impossible de lire ce dossier", - "download": "Télécharger", - "downloadFailed": "Échec du téléchargement {{name}}", - "upload": "Téléverser des fichiers", - "uploading": "Téléversement de {{name}}...", - "uploaded": "Téléchargé {{name}}", - "uploadFailed": "Échec du chargement {{name}}", - "uploadDisabled": "Les chargements sont désactivés pour cette connexion.", - "driveUnavailable": "Activez la redirection du lecteur RDP avant de télécharger les fichiers", - "driveNotWritable": "guacd ne peut pas écrire dans le dossier du lecteur. Vérifiez la variable d'environnement GUACD_DRIVE_PATH (ou le chemin d'accès au lecteur de l'hôte) et ses autorisations.", - "dropToUpload": "Déposez les fichiers à télécharger" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Verrouiller l'écran)", - "winKey": "Touche Windows", - "superL": "Super+L (Écran de verrouillage)", - "superKey": "Super Clé", - "super": "Super", - "cmdL": "Cmd+L (Écran de verrouillage)", - "cmdKey": "Touche de commande", - "cmd": "Commande", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Maj", - "win": "Win", - "stickyActive": "{{key}} (verrouillé - cliquer pour libérer)", - "stickyInactive": "{{key}} (cliquer pour verrouiller)", - "esc": "Échap", - "tab": "Tab", - "home": "Début", - "end": "Fin", - "pageUp": "Page préc.", - "pageDown": "Page suiv.", - "arrowUp": "Flèche haut", - "arrowDown": "Flèche bas", - "arrowLeft": "Flèche gauche", - "arrowRight": "Flèche droite", - "fnToggle": "Touches de fonction", - "reconnect": "Reconnecter la session", - "collapse": "Réduire la barre d'outils", - "expand": "Développer la barre d'outils", - "dragHandle": "Glisser pour repositionner", - "hide": "Masquer cette option pour cette session. Pour la désactiver définitivement, ouvrez Paramètres de l'hôte → Général → Barre d'outils de connexion.", - "zoomIn": "Zoomer", - "zoomOut": "Dézoomer", - "resetZoom": "Réinitialiser le zoom", - "fitToScreen": "Ajuster à l'écran", - "switchToTrackpad": "Passez en mode pavé tactile (glissez pour déplacer le curseur, appuyez pour cliquer)", - "switchToTouch": "Passez en mode tactile (appuyez directement à l'endroit où vous souhaitez cliquer)." - }, - "disabled": "Le Bureau à distance est désactivé. Un administrateur peut l'activer dans les paramètres." - }, - "hosts": { - "nativeRdpOpened": "Ouvert dans Bureau à distance Windows", - "nativeRdpFailed": "Impossible d'ouvrir le Bureau à distance Windows", - "openNativeRdp": "Ouvrir dans le Bureau à distance Windows", - "guac": { - "toolbar": "barre d'outils", - "connection": "Connexion", - "authentication": "Authentification", - "storedCredential": "Identifiant enregistré", - "authMethod": "Méthode d'authentification", - "authTypeDirect": "Direct", - "authTypeCredential": "Identifiant", - "authTypeNone": "Aucune", - "authTypeNoneDesc": "Aucune information d'identification n'est stockée. Un nom d'utilisateur et un mot de passe vous seront demandés à chaque connexion ; ils ne sont pas enregistrés.", - "selectCredential": "Sélectionner un identifiant...", - "connectionSettings": "Paramètres de connexion", - "displaySettings": "Paramètres d'affichage", - "audioSettings": "Paramètres audio", - "rdpPerformance": "Performances RDP", - "deviceRedirection": "Redirection de périphériques", - "session": "Session", - "gateway": "Passerelle", - "remoteApp": "RemoteApp", - "clipboard": "Presse-papiers", - "sessionRecording": "Enregistrement de session", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Paramètres VNC", - "terminalSettings": "Paramètres du terminal", - "rdpPort": "Port RDP", - "username": "Nom d'utilisateur", - "password": "Mot de passe", - "passwordSaved": "Mot de passe enregistré, saisissez-le pour le modifier", - "domain": "Domaine", - "securityMode": "Mode de sécurité", - "width": "Largeur", - "height": "Hauteur", - "dpi": "DPI", - "clientName": "Nom du client", - "initialProgram": "Programme initial", - "serverLayout": "Disposition du clavier", - "timezone": "Fuseau horaire", - "loadBalanceInfo": "Infos d'équilibrage de charge / Cookie", - "loadBalanceInfoDesc": "Cookie du RD Connection Broker pour l'équilibrage de charge d'une ferme RDS (ex. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Proxy guacd", - "guacdHostname": "Hôte guacd", - "guacdHostnamePlaceholder": "Valeur globale par défaut", - "guacdPort": "Port guacd", - "guacdProxyDesc": "Remplacer l'instance guacd globale pour cette connexion. Laisser vide pour utiliser le guacd configuré globalement.", - "gatewayHostname": "Nom d'hôte de la passerelle", - "gatewayPort": "Port de la passerelle", - "gatewayUsername": "Nom d'utilisateur de la passerelle", - "gatewayPassword": "Mot de passe de la passerelle", - "gatewayDomain": "Domaine de la passerelle", - "remoteAppProgram": "Programme RemoteApp", - "workingDirectory": "Répertoire de travail", - "arguments": "Arguments", - "normalizeLineEndings": "Normaliser les fins de ligne", - "macAddress": "Adresse MAC", - "broadcastAddress": "Adresse de diffusion", - "udpPort": "Port UDP", - "waitTimeS": "Délai d'attente (s)", - "driveName": "Nom du lecteur", - "drivePath": "Chemin du lecteur", - "ignoreCertificate": "Ignorer le certificat", - "ignoreCertificateDesc": "Autoriser les connexions aux hôtes avec des certificats auto-signés", - "forceLossless": "Forcer sans perte", - "forceLosslessDesc": "Forcer l'encodage d'image sans perte (meilleure qualité, plus de bande passante)", - "disableAudio": "Désactiver l'audio", - "disableAudioDesc": "Couper tout le son de la session distante", - "enableAudioInput": "Activer l'entrée audio (microphone)", - "enableAudioInputDesc": "Transférer le microphone local vers la session distante", - "wallpaper": "Fond d'écran", - "wallpaperDesc": "Afficher le fond d'écran du bureau (désactiver cette option améliore les performances)", - "theming": "Thèmes", - "themingDesc": "Activer les thèmes et styles visuels", - "fontSmoothing": "Lissage des polices", - "fontSmoothingDesc": "Activer le rendu des polices ClearType", - "fullWindowDrag": "Afficher le contenu de la fenêtre lors du déplacement", - "fullWindowDragDesc": "Afficher le contenu de la fenêtre lors du déplacement", - "desktopComposition": "Composition du bureau", - "desktopCompositionDesc": "Activer les effets Aero Glass", - "menuAnimations": "Animations des menus", - "menuAnimationsDesc": "Activer les animations de fondu et de glissement des menus", - "disableBitmapCaching": "Désactiver le cache bitmap", - "disableBitmapCachingDesc": "Désactiver le cache bitmap (peut aider en cas de problèmes graphiques)", - "disableOffscreenCaching": "Désactiver le cache hors écran", - "disableOffscreenCachingDesc": "Désactiver le cache hors écran", - "disableGlyphCaching": "Désactiver le cache des glyphes", - "disableGlyphCachingDesc": "Désactiver le cache des glyphes", - "enableGfx": "Activer GFX", - "enableGfxDesc": "Utiliser le pipeline graphique RemoteFX", - "enablePrinting": "Activer l'impression", - "enablePrintingDesc": "Rediriger les imprimantes locales vers la session distante", - "enableDriveRedirection": "Activer la redirection de lecteur", - "enableDriveRedirectionDesc": "Monter un dossier local en tant que lecteur dans la session distante", - "createDrivePath": "Créer le chemin du lecteur", - "createDrivePathDesc": "Créer automatiquement le dossier s'il n'existe pas", - "disableDownload": "Désactiver le téléchargement", - "disableDownloadDesc": "Empêcher le téléchargement des fichiers depuis la session distante", - "disableUpload": "Désactiver le téléversement", - "disableUploadDesc": "Empêcher le téléversement de fichiers vers la session distante", - "enableTouch": "Activer le tactile", - "enableTouchDesc": "Activer la transmission des entrées tactiles", - "consoleSession": "Session console", - "consoleSessionDesc": "Se connecter à la console (session 0) au lieu d'une nouvelle session", - "sendWolPacket": "Envoyer un paquet WOL", - "sendWolPacketDesc": "Envoyer un paquet magique pour réveiller cet hôte avant la connexion", - "disableCopy": "Désactiver la copie", - "disableCopyDesc": "Empêcher la copie de texte depuis la session distante", - "disablePaste": "Désactiver le collage", - "disablePasteDesc": "Empêcher le collage de texte dans la session distante", - "createPathIfMissing": "Créer le chemin si absent", - "createPathIfMissingDesc": "Créer automatiquement le répertoire d'enregistrement", - "excludeOutput": "Exclure la sortie", - "excludeOutputDesc": "Ne pas enregistrer l'affichage écran (métadonnées uniquement)", - "excludeMouse": "Exclure la souris", - "excludeMouseDesc": "Ne pas enregistrer les mouvements de la souris", - "includeKeystrokes": "Inclure les frappes", - "includeKeystrokesDesc": "Enregistrer les frappes brutes en plus de l'affichage écran", - "vncPort": "Port VNC", - "vncPassword": "Mot de passe VNC", - "vncUsernameOptional": "Nom d'utilisateur (facultatif)", - "vncLeaveBlank": "Laisser vide si non requis", - "cursorMode": "Mode curseur", - "swapRedBlue": "Échanger rouge/bleu", - "swapRedBlueDesc": "Échanger les canaux de couleur rouge et bleu (corrige certains problèmes de couleur)", - "readOnly": "Lecture seule", - "readOnlyDesc": "Afficher l'écran distant sans envoyer d'entrée", - "telnetPort": "Port Telnet", - "terminalType": "Type de terminal", - "fontName": "Nom de la police", - "fontSize": "Taille de la police", - "colorScheme": "Schéma de couleurs", - "backspaceKey": "Touche Backspace", - "colorDepth": "Profondeur de couleur", - "resizeMethod": "Méthode de redimensionnement", - "driveNamePlaceholder": "Lecteur Termix", - "options": { - "any": "N'importe lequel", - "auto": "Auto", - "nlaExtended": "NLA étendu", - "color8": "8 bits", - "color16": "16 bits", - "color24": "24 bits", - "color32": "32 bits", - "displayUpdate": "Mise à jour de l'affichage", - "reconnect": "Reconnecter", - "preserve": "Préserver", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Locale", - "remote": "Distant", - "failsafe": "sécurité intégrée", - "blackOnWhite": "Noir sur blanc", - "whiteOnBlack": "Blanc sur noir", - "grayOnBlack": "Gris sur noir", - "greenOnBlack": "Vert sur noir" - } - }, - "remoteDesktop": "Bureau à distance", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Réseau virtuel", - "unencryptedShell": "Shell non chiffré", - "remoteDesktopNotes": "Notes", - "userDefaultsNote": "Les paramètres que vous laissez inchangés suivent les valeurs par défaut de votre hôte.", - "connectionOriginNote": "Sur l'application de bureau, RDP, VNC et Telnet utilisent le serveur distant, sauf si l'hôte est configuré sur « Cet appareil », auquel cas un proxy guacd accessible par cette machine est nécessaire. Configurez-en un dans les paramètres du Bureau à distance ou pour chaque hôte dans les paramètres du proxy guacd.", - "docsLink": "Voir la documentation", - "connectionOriginGuacamoleNote": "RDP, VNC et Telnet utilisent le serveur distant, sauf si vous sélectionnez « Cet appareil », auquel cas un serveur guacd accessible par cette machine est nécessaire. Configurez-en un dans les paramètres d'URL guacd ou par hôte dans les paramètres de proxy guacd." - }, - "onboarding": { - "feature_desktop": "Bureau à distance", - "feature_desktop_desc": "Ouvrez les sessions RDP, VNC et Telnet dans un onglet, de la même manière que SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Profondeur de couleur", - "description": "Utilisé pour les hôtes RDP qui conservent leurs paramètres par défaut." - }, - "resizeMethod": { - "label": "Méthode de redimensionnement", - "description": "Comment l'écran distant s'adapte à la taille de la fenêtre." - }, - "resizeDisplayUpdate": "Mise à jour de l'affichage", - "resizeReconnect": "Reconnecter", - "inherit": "Hériter", - "on": "Activée", - "off": "Désactivée", - "forceLossless": { - "label": "Forcer sans perte", - "description": "Forcer l'encodage d'image sans perte (meilleure qualité, plus de bande passante)" - }, - "enableWallpaper": { - "label": "Fond d'écran", - "description": "Afficher le fond d'écran du bureau (désactiver cette option améliore les performances)" - }, - "enableFontSmoothing": { - "label": "Lissage des polices", - "description": "Activer le rendu des polices ClearType" - }, - "enableDesktopComposition": { - "label": "Composition du bureau", - "description": "Activer les effets Aero Glass" - }, - "disableAudio": { - "label": "Désactiver l'audio", - "description": "Couper tout le son de la session distante" - }, - "enablePrinting": { - "label": "Activer l'impression", - "description": "Rediriger les imprimantes locales vers la session distante" - }, - "enableDrive": { - "label": "Activer la redirection de lecteur", - "description": "Monter un dossier local en tant que lecteur dans la session distante" - }, - "disableCopy": { - "label": "Désactiver la copie", - "description": "Empêcher la copie de texte depuis la session distante" - }, - "disablePaste": { - "label": "Désactiver le collage", - "description": "Empêcher le collage de texte dans la session distante" - }, - "group": "Paramètres par défaut du Bureau à distance", - "colorDepth16": "16 bits", - "colorDepth24": "24 bits", - "colorDepth32": "32 bits" - }, - "admin": { - "enabled": { - "label": "Activer le bureau à distance", - "description": "Autoriser les connexions RDP, VNC et Telnet via guacd." - }, - "guacdUrl": { - "label": "URL guacd", - "description": "L'adresse d'écoute de guacd est indiquée par l'hôte et le port. Les variables d'environnement GUACD_URL, GUACD_HOST et GUACD_PORT prévalent sur ce comportement.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "Port RDP" - }, - "vncPort": { - "label": "port VNC" - }, - "telnetPort": { - "label": "Port Telnet" - }, - "rdpSecurity": { - "label": "Mode de sécurité RDP" - }, - "rdpIgnoreCert": { - "label": "Ignorer les erreurs de certificat" - }, - "guacamoleConfig": { - "label": "paramètres guacd" - }, - "enableToolbar": { - "label": "barre d'outils de bureau à distance", - "description": "Afficher la barre d'outils dans les sessions RDP, VNC et Telnet." - }, - "displayGroup": "Afficher", - "displayDefaultsTab": "Bureau à distance" - } - }, - "palette": { - "connectRdp": "Se connecter via RDP", - "connectVnc": "Se connecter avec VNC", - "connectTelnet": "Se connecter avec Telnet" - }, - "permissions": { - "sessions": { - "title": "Sessions de bureau à distance partagées", - "description": "Autoriser le partage de session et les salles de collaboration à afficher les sessions RDP, VNC et Telnet de cet utilisateur aux autres." - } - }, - "errors": { - "remoteServerRequired": "Serveur distant requis. Connectez un serveur distant dans les paramètres pour utiliser ce type de connexion." - }, - "plugin": { - "name": "Bureau à distance" - } -} diff --git a/plugins/remote-desktop/locales/translated/he_IL.json b/plugins/remote-desktop/locales/translated/he_IL.json deleted file mode 100644 index 5ee0ef160..000000000 --- a/plugins/remote-desktop/locales/translated/he_IL.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "מתחבר לסשן {{type}}...", - "checkingGuacd": "בודק שירות שולחן עבודה מרוחק...", - "requestingToken": "מבקש אסימון חיבור {{type}}...", - "openingSession": "פותח סשן {{type}}...", - "negotiating": "מנהל משא ומתן על לחיצת יד {{type}}...", - "sessionReady": "{{type}} מוכנה לסשן", - "connectionError": "שגיאת חיבור", - "connectionFailed": "החיבור נכשל", - "failedToConnect": "נכשל בקבלת אסימון החיבור", - "hostNotFound": "המארח לא נמצא", - "noHostSelected": "לא נבחר מארח", - "reconnect": "התחבר מחדש", - "retry": "נסה שוב", - "guacdUnavailable": "שירות שולחן עבודה מרוחק (guacd) אינו זמין. אנא ודא ש-guacd פועל, נגיש ומוגדר כראוי בהגדרות המנהל.", - "credentialPromptTitle": "הזן אישורי RDP", - "credentialPromptDescription": "מארח זה מוגדר לבקש אישורים בעת התחברות. הם משמשים עבור הפעלה זו בלבד ואינם נשמרים.", - "connect": "לְחַבֵּר", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "קבצי דרייב", - "parent": "ספריית הורים", - "refresh": "לְרַעֲנֵן", - "empty": "תיקייה זו ריקה", - "listFailed": "נכשלה קריאת התיקייה הזו", - "download": "הורדה", - "downloadFailed": "נכשל בהורדת {{name}}", - "upload": "העלאת קבצים", - "uploading": "מעלה {{name}}...", - "uploaded": "הועלה {{name}}", - "uploadFailed": "נכשלה העלאת {{name}}", - "uploadDisabled": "העלאות מושבתות עבור חיבור זה", - "driveUnavailable": "הפעל ניתוב מחדש של כונן RDP לפני העלאת קבצים", - "driveNotWritable": "ל-guacd אין אפשרות לכתוב לתיקיית הכונן. בדוק את GUACD_DRIVE_PATH (או את נתיב הכונן של המארח) ואת ההרשאות שלו.", - "dropToUpload": "שחררו קבצים להעלאה" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (מסך נעילה)", - "winKey": "מפתח חלונות", - "superL": "סופר+L (מסך נעילה)", - "superKey": "סופר קי", - "super": "סוּפֶּר", - "cmdL": "Cmd+L (מסך נעילה)", - "cmdKey": "מקש פקודה", - "cmd": "פקודה", - "ctrl": "קונטרול", - "alt": "אלט", - "shift": "מִשׁמֶרֶת", - "win": "לְנַצֵחַ", - "stickyActive": "{{key}} (נעול - לחץ לשחרור)", - "stickyInactive": "{{key}} (לחץ כדי לנעול)", - "esc": "לִברוֹחַ", - "tab": "טאב", - "home": "בַּיִת", - "end": "סוֹף", - "pageUp": "עמוד למעלה", - "pageDown": "עמוד למטה", - "arrowUp": "חץ למעלה", - "arrowDown": "חץ למטה", - "arrowLeft": "חץ שמאלה", - "arrowRight": "חץ ימינה", - "fnToggle": "מקשי פונקציה", - "reconnect": "חיבור מחדש של הפעלת המושב", - "collapse": "כיווץ סרגל הכלים", - "expand": "הרחב את סרגל הכלים", - "dragHandle": "גרור כדי למקם מחדש", - "hide": "הסתר עבור הפעלה זו. כדי להשבית זאת לצמיתות, פתח את הגדרות המארח ← כללי ← סרגל הכלים לחיבור.", - "zoomIn": "לְהִתְמַקֵד", - "zoomOut": "התרחקות", - "resetZoom": "איפוס זום", - "fitToScreen": "התאמה למסך", - "switchToTrackpad": "מעבר למצב משטח עקיבה (גרור כדי להזיז את הסמן, הקש כדי ללחוץ)", - "switchToTouch": "מעבר למצב מגע (הקש ישירות במקום שברצונך ללחוץ)" - }, - "disabled": "שולחן עבודה מרוחק כבוי. מנהל יכול להפעיל אותו בהגדרות." - }, - "hosts": { - "nativeRdpOpened": "נפתח בשולחן עבודה מרוחק של Windows", - "nativeRdpFailed": "נכשלה פתיחת שולחן עבודה מרוחק של Windows", - "openNativeRdp": "פתח בשולחן עבודה מרוחק של Windows", - "guac": { - "toolbar": "סרגל כלים", - "connection": "קֶשֶׁר", - "authentication": "אימות", - "storedCredential": "אישורים מאוחסנים", - "authMethod": "שיטת אימות", - "authTypeDirect": "יָשִׁיר", - "authTypeCredential": "תְעוּדָה", - "authTypeNone": "אַף לֹא אֶחָד", - "authTypeNoneDesc": "לא נשמרים פרטי גישה. תתבקש להזין שם משתמש וסיסמה בכל פעם שתתחבר; הם לא נשמרים.", - "selectCredential": "בחר אישור...", - "connectionSettings": "הגדרות חיבור", - "displaySettings": "הגדרות תצוגה", - "audioSettings": "הגדרות שמע", - "rdpPerformance": "ביצועי RDP", - "deviceRedirection": "ניתוב מחדש של מכשירים", - "session": "מוֹשָׁב", - "gateway": "כְּנִיסָה", - "remoteApp": "אפליקציה מרחוק", - "clipboard": "לוח כתיבה", - "sessionRecording": "הקלטת סשן", - "wakeOnLan": "התעוררות ברשת LAN", - "vncSettings": "הגדרות VNC", - "terminalSettings": "הגדרות מסוף", - "rdpPort": "יציאת RDP", - "username": "שם משתמש", - "password": "סִיסמָה", - "passwordSaved": "הסיסמה נשמרה, הקלד כדי לשנות", - "domain": "תְחוּם", - "securityMode": "מצב אבטחה", - "width": "רוֹחַב", - "height": "גוֹבַה", - "dpi": "DPI", - "clientName": "שם הלקוח", - "initialProgram": "תוכנית ראשונית", - "serverLayout": "פריסת שרת", - "timezone": "אזור זמן", - "loadBalanceInfo": "מידע על מאזן עומס / קובץ Cookie", - "loadBalanceInfoDesc": "קובץ cookie של RD Connection Broker עבור איזון עומסים של חוות RDS (לדוגמה tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "פרוקסי של guacd", - "guacdHostname": "מארח guacd", - "guacdHostnamePlaceholder": "ברירת מחדל גלובלית", - "guacdPort": "נמל גואקד", - "guacdProxyDesc": "עקוף את מופע ה-guacd הגלובלי עבור חיבור זה. השאר ריק כדי להשתמש ב-guacd שתצורתו הגלובלית.", - "gatewayHostname": "שם מארח השער", - "gatewayPort": "שער היציאה", - "gatewayUsername": "שם משתמש של השער", - "gatewayPassword": "סיסמת השער", - "gatewayDomain": "דומיין שער", - "remoteAppProgram": "תוכנית RemoteApp", - "workingDirectory": "ספריית עבודה", - "arguments": "טיעונים", - "normalizeLineEndings": "נרמול סיומות שורות", - "macAddress": "כתובת MAC", - "broadcastAddress": "כתובת שידור", - "udpPort": "יציאת UDP", - "waitTimeS": "זמן המתנה (שניות)", - "driveName": "שם הכונן", - "drivePath": "נתיב הנסיעה", - "ignoreCertificate": "התעלם מהתעודה", - "ignoreCertificateDesc": "אפשר חיבורים למארחים עם אישורים חתומים עצמית", - "forceLossless": "כפיית אובדן נתונים", - "forceLosslessDesc": "כפיית קידוד תמונה ללא אובדן נתונים (איכות גבוהה יותר, רוחב פס גדול יותר)", - "disableAudio": "השבתת שמע", - "disableAudioDesc": "השתקת כל האודיו מההפעלה המרוחקת", - "enableAudioInput": "הפעלת קלט שמע (מיקרופון)", - "enableAudioInputDesc": "העברת המיקרופון המקומי לסשן המרוחק", - "wallpaper": "טָפֵּט", - "wallpaperDesc": "הצגת טפט שולחן עבודה (השבתה משפרת את הביצועים)", - "theming": "ת'מינג", - "themingDesc": "הפעלת ערכות נושא וסגנונות חזותיים", - "fontSmoothing": "החלקת גופנים", - "fontSmoothingDesc": "הפעלת עיבוד גופני ClearType", - "fullWindowDrag": "גרירת חלון מלאה", - "fullWindowDragDesc": "הצגת תוכן החלון בעת גרירה", - "desktopComposition": "קומפוזיציה לשולחן העבודה", - "desktopCompositionDesc": "הפעלת אפקטים של Aero Glass", - "menuAnimations": "אנימציות תפריטים", - "menuAnimationsDesc": "הפעלת אנימציות של דהייה והחלקה בתפריט", - "disableBitmapCaching": "השבתת אחסון במטמון של מפת סיביות", - "disableBitmapCachingDesc": "כבה את מטמון הסיביות (עשוי לעזור עם תקלות)", - "disableOffscreenCaching": "השבתת אחסון במטמון מחוץ למסך", - "disableOffscreenCachingDesc": "כבה את המטמון מחוץ למסך", - "disableGlyphCaching": "השבתת אחסון גליפים במטמון", - "disableGlyphCachingDesc": "כבה את מטמון הגליפים", - "enableGfx": "הפעלת GFX", - "enableGfxDesc": "השתמש בצינור הגרפיקה של RemoteFX", - "enablePrinting": "הפעל הדפסה", - "enablePrintingDesc": "ניתוב מחדש של מדפסות מקומיות להפעלה מרוחקת", - "enableDriveRedirection": "הפעל ניתוב מחדש של כונן", - "enableDriveRedirectionDesc": "מיפוי תיקייה מקומית ככונן בהפעלה מרוחקת", - "createDrivePath": "צור נתיב כונן", - "createDrivePathDesc": "צור את התיקייה באופן אוטומטי אם היא אינה קיימת", - "disableDownload": "השבתת הורדה", - "disableDownloadDesc": "מניעת הורדת קבצים מההפעלה המרוחקת", - "disableUpload": "השבתת העלאה", - "disableUploadDesc": "מניעת העלאת קבצים לסשן מרחוק", - "enableTouch": "הפעל מגע", - "enableTouchDesc": "הפעל העברת קלט מגע", - "consoleSession": "סשן קונסולה", - "consoleSessionDesc": "התחברות לקונסולה (סשן 0) במקום סשן חדש", - "sendWolPacket": "שלח חבילת WOL", - "sendWolPacketDesc": "שלח חבילת קסם כדי להעיר את המארח הזה לפני התחברות", - "disableCopy": "השבתת העתקה", - "disableCopyDesc": "מניעת העתקת טקסט מההפעלה המרוחקת", - "disablePaste": "השבתת הדבקה", - "disablePasteDesc": "מניעת הדבקת טקסט לתוך ההפעלה המרוחקת", - "createPathIfMissing": "צור נתיב אם חסר", - "createPathIfMissingDesc": "צור אוטומטית את ספריית ההקלטה", - "excludeOutput": "אי הכללת פלט", - "excludeOutputDesc": "אל תקליטו פלט מסך (מטא-נתונים בלבד)", - "excludeMouse": "אי הכללת עכבר", - "excludeMouseDesc": "אל תתעדו תנועות עכבר", - "includeKeystrokes": "כלול הקשות מקשים", - "includeKeystrokesDesc": "הקלטת הקשות גולמיות בנוסף לפלט המסך", - "vncPort": "יציאת VNC", - "vncPassword": "סיסמת VNC", - "vncUsernameOptional": "שם משתמש (אופציונלי)", - "vncLeaveBlank": "השאר ריק אם לא נדרש", - "cursorMode": "מצב סמן", - "swapRedBlue": "החלפת אדום/כחול", - "swapRedBlueDesc": "החלפת ערוצי הצבע האדום והכחול (תיקון של כמה בעיות צבע)", - "readOnly": "לקריאה בלבד", - "readOnlyDesc": "צפה במסך השלט הרחוק מבלי לשלוח קלט כלשהו", - "telnetPort": "יציאת טלנט", - "terminalType": "סוג הטרמינל", - "fontName": "שם הגופן", - "fontSize": "גודל גופן", - "colorScheme": "ערכת צבעים", - "backspaceKey": "מקש Backspace", - "colorDepth": "עומק צבע", - "resizeMethod": "שיטת שינוי גודל", - "driveNamePlaceholder": "כונן טרמיקס", - "options": { - "any": "כֹּל", - "auto": "אוטומטי", - "nlaExtended": "NLA מורחב", - "color8": "8 סיביות", - "color16": "16 סיביות", - "color24": "24 סיביות", - "color32": "32 סיביות", - "displayUpdate": "עדכון תצוגה", - "reconnect": "התחבר מחדש", - "preserve": "לְשַׁמֵר", - "unix": "יוניקס (LF)", - "windows": "חלונות (CRLF)", - "local": "מְקוֹמִי", - "remote": "מְרוּחָק", - "failsafe": "אַל כֶּשֶׁל", - "blackOnWhite": "שחור על לבן", - "whiteOnBlack": "לבן על שחור", - "grayOnBlack": "אפור על שחור", - "greenOnBlack": "ירוק על שחור" - } - }, - "remoteDesktop": "שולחן עבודה מרוחק", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "טלנט", - "virtualNetwork": "רשת וירטואלית", - "unencryptedShell": "מעטפת לא מוצפנת", - "remoteDesktopNotes": "הערות", - "userDefaultsNote": "הגדרות שאתה משאיר ללא שינוי עוקבות אחר הגדרות ברירת המחדל של המארח.", - "connectionOriginNote": "באפליקציית שולחן העבודה, RDP, VNC ו-Telnet משתמשים בשרת המרוחק אלא אם כן המארח מוגדר ל-This device, אשר זקוק ל-guacd שאליו מכונה זו יכולה להגיע. הגדר אחד בהגדרות שולחן העבודה המרוחק, או לכל מארח תחת guacd Proxy.", - "docsLink": "הצג מסמכים", - "connectionOriginGuacamoleNote": "RDP, VNC ו-Telnet משתמשים בשרת המרוחק אלא אם כן תבחרו באפשרות \"התקן זה\", אשר דורש גישה ל-guacd. הגדר קוד כזה בהגדרת כתובת ה-URL של guacd, או לכל מחשב מארח תחת Proxy של guacd." - }, - "onboarding": { - "feature_desktop": "שולחן עבודה מרוחק", - "feature_desktop_desc": "פתחו הפעלות RDP, VNC ו-Telnet בכרטיסייה, בדומה ל-SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "עומק צבע", - "description": "משמש עבור מארחי RDP שמשאירים אותו כברירת מחדל." - }, - "resizeMethod": { - "label": "שיטת שינוי גודל", - "description": "כיצד מסך השלט עוקב אחר גודל החלון." - }, - "resizeDisplayUpdate": "עדכון תצוגה", - "resizeReconnect": "התחבר מחדש", - "inherit": "לָרֶשֶׁת", - "on": "עַל", - "off": "כבוי", - "forceLossless": { - "label": "כפה ללא אובדן", - "description": "כפיית קידוד תמונה ללא אובדן נתונים (איכות גבוהה יותר, רוחב פס גדול יותר)" - }, - "enableWallpaper": { - "label": "טָפֵּט", - "description": "הצגת טפט שולחן עבודה (השבתה משפרת את הביצועים)" - }, - "enableFontSmoothing": { - "label": "החלקת גופנים", - "description": "הפעלת עיבוד גופני ClearType" - }, - "enableDesktopComposition": { - "label": "קומפוזיציה לשולחן העבודה", - "description": "הפעלת אפקטים של Aero Glass" - }, - "disableAudio": { - "label": "השבתת שמע", - "description": "השתקת כל האודיו מההפעלה המרוחקת" - }, - "enablePrinting": { - "label": "הפעל הדפסה", - "description": "ניתוב מחדש של מדפסות מקומיות להפעלה מרוחקת" - }, - "enableDrive": { - "label": "הפעל ניתוב מחדש של כונן", - "description": "מיפוי תיקייה מקומית ככונן בהפעלה מרוחקת" - }, - "disableCopy": { - "label": "השבתת העתקה", - "description": "מניעת העתקת טקסט מההפעלה המרוחקת" - }, - "disablePaste": { - "label": "השבתת הדבקה", - "description": "מניעת הדבקת טקסט לתוך ההפעלה המרוחקת" - }, - "group": "ברירות מחדל של שולחן עבודה מרוחק", - "colorDepth16": "16 סיביות", - "colorDepth24": "24 סיביות", - "colorDepth32": "32 סיביות" - }, - "admin": { - "enabled": { - "label": "הפעלת שולחן עבודה מרוחק", - "description": "אפשר חיבורי RDP, VNC ו-Telnet דרך guacd." - }, - "guacdUrl": { - "label": "כתובת אתר של guacd", - "description": "כאשר guacd מאזין, כ-host:port. משתני הסביבה GUACD_URL, GUACD_HOST ו-GUACD_PORT עוקפים זאת.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "טלנט" - }, - "rdpPort": { - "label": "יציאת RDP" - }, - "vncPort": { - "label": "יציאת VNC" - }, - "telnetPort": { - "label": "יציאת טלנט" - }, - "rdpSecurity": { - "label": "מצב אבטחה של RDP" - }, - "rdpIgnoreCert": { - "label": "התעלם משגיאות אישור" - }, - "guacamoleConfig": { - "label": "הגדרות guacd" - }, - "enableToolbar": { - "label": "סרגל הכלים של שולחן עבודה מרוחק", - "description": "הצג את סרגל הכלים בהפעלות RDP, VNC ו-Telnet." - }, - "displayGroup": "לְהַצִיג", - "displayDefaultsTab": "שולחן עבודה מרוחק" - } - }, - "palette": { - "connectRdp": "התחבר עם RDP", - "connectVnc": "התחבר עם VNC", - "connectTelnet": "התחבר עם טלנט" - }, - "permissions": { - "sessions": { - "title": "שיתוף הפעלות שולחן עבודה מרוחק", - "description": "אפשר לשיתוף סשנים וחדרי שיתוף פעולה להציג את סשנים של RDP, VNC ו-Telnet של משתמש זה לאחרים." - } - }, - "errors": { - "remoteServerRequired": "נדרש שרת מרוחק. יש להתחבר לשרת מרוחק בהגדרות כדי להשתמש בסוג חיבור זה." - }, - "plugin": { - "name": "שולחן עבודה מרוחק" - } -} diff --git a/plugins/remote-desktop/locales/translated/hi_IN.json b/plugins/remote-desktop/locales/translated/hi_IN.json deleted file mode 100644 index ecd938915..000000000 --- a/plugins/remote-desktop/locales/translated/hi_IN.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "{{type}} सत्र से कनेक्ट हो रहा है...", - "checkingGuacd": "रिमोट डेस्कटॉप सेवा की जाँच की जा रही है...", - "requestingToken": "{{type}} कनेक्शन टोकन का अनुरोध किया जा रहा है...", - "openingSession": "Opening {{type}} session...", - "negotiating": "बातचीत चल रही है {{type}} हाथ मिलाना...", - "sessionReady": "{{type}} सत्र तैयार है", - "connectionError": "कनेक्शन त्रुटि", - "connectionFailed": "कनेक्शन विफल", - "failedToConnect": "कनेक्शन टोकन प्राप्त करने में विफल", - "hostNotFound": "होस्ट नहीं मिला", - "noHostSelected": "कोई होस्ट चयनित नहीं है", - "reconnect": "पुनः कनेक्ट करें", - "retry": "पुनः प्रयास करें", - "guacdUnavailable": "रिमोट डेस्कटॉप सेवा (guacd) उपलब्ध नहीं है। कृपया सुनिश्चित करें कि guacd चल रहा है और पहुंच योग्य है और एडमिन सेटिंग्स में ठीक से कॉन्फ़िगर किया गया है।", - "credentialPromptTitle": "आरडीपी क्रेडेंशियल दर्ज करें", - "credentialPromptDescription": "इस होस्ट पर कनेक्ट होने पर क्रेडेंशियल मांगे जाएंगे। ये क्रेडेंशियल केवल इसी सेशन के लिए उपयोग किए जाएंगे और सेव नहीं किए जाएंगे।", - "connect": "कनेक्ट", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "ड्राइव फ़ाइलें", - "parent": "मूल निर्देशिका", - "refresh": "ताज़ा करें", - "empty": "यह फ़ोल्डर खाली है", - "listFailed": "इस फ़ोल्डर को पढ़ने में विफल रहा", - "download": "डाउनलोड", - "downloadFailed": "डाउनलोड करने में विफल {{name}}", - "upload": "फाइलें अपलोड करें", - "uploading": "{{name}} अपलोड हो रहा है...", - "uploaded": "अपलोड किया गया {{name}}", - "uploadFailed": "अपलोड करने में विफल {{name}}", - "uploadDisabled": "इस कनेक्शन के लिए अपलोड अक्षम हैं।", - "driveUnavailable": "फ़ाइलें अपलोड करने से पहले RDP ड्राइव रीडायरेक्शन को सक्षम करें", - "driveNotWritable": "GUACD ड्राइव फ़ोल्डर में लिख नहीं सकता। कृपया GUACD_DRIVE_PATH (या होस्ट का ड्राइव पाथ) और उसकी अनुमतियाँ जाँचें।", - "dropToUpload": "अपलोड करने के लिए फ़ाइलें ड्रॉप करें" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (लॉक स्क्रीन)", - "winKey": "विंडोज की", - "superL": "सुपर+एल (लॉक स्क्रीन)", - "superKey": "सुपर कुंजी", - "super": "बहुत अच्छा", - "cmdL": "Cmd+L (लॉक स्क्रीन)", - "cmdKey": "कमांड कुंजी", - "cmd": "अध्यक्ष एवं प्रबंध निदेशक", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}} (लैच्ड - रिलीज़ करने के लिए क्लिक करें)", - "stickyInactive": "{{key}} (लैच करने के लिए क्लिक करें)", - "esc": "Escape", - "tab": "Tab", - "home": "Home", - "end": "एंड", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "ऊपर तीर", - "arrowDown": "नीचे तीर", - "arrowLeft": "बाएँ तीर", - "arrowRight": "दाएँ तीर", - "fnToggle": "फ़ंक्शन कुंजियाँ", - "reconnect": "सत्र पुनः कनेक्ट करें", - "collapse": "टूलबार समेटें", - "expand": "टूलबार विस्तारित करें", - "dragHandle": "स्थान बदलने के लिए खींचें", - "hide": "इस सत्र के लिए इसे छुपाएं। इसे स्थायी रूप से अक्षम करने के लिए, होस्ट सेटिंग्स → सामान्य → कनेक्शन टूलबार खोलें।", - "zoomIn": "ज़ूम इन", - "zoomOut": "ज़ूम आउट", - "resetZoom": "ज़ूम रीसेट करें", - "fitToScreen": "स्क्रीनिंग के लिए उपयुक्त", - "switchToTrackpad": "ट्रैकपैड मोड पर स्विच करें (कर्सर को स्थानांतरित करने के लिए ड्रैग करें, क्लिक करने के लिए टैप करें)", - "switchToTouch": "टच मोड पर स्विच करें (जहां क्लिक करना चाहते हैं वहां सीधे टैप करें)" - }, - "disabled": "रिमोट डेस्कटॉप बंद है। व्यवस्थापक सेटिंग्स में जाकर इसे चालू कर सकते हैं।" - }, - "hosts": { - "nativeRdpOpened": "विंडोज़ रिमोट डेस्कटॉप में खोला गया", - "nativeRdpFailed": "विंडोज़ रिमोट डेस्कटॉप खोलने में विफल", - "openNativeRdp": "विंडोज़ रिमोट डेस्कटॉप में खोलें", - "guac": { - "toolbar": "उपकरण पट्टी", - "connection": "कनेक्शन", - "authentication": "प्रमाणीकरण", - "storedCredential": "संग्रहीत क्रेडेंशियल", - "authMethod": "प्रमाणीकरण विधि", - "authTypeDirect": "सीधा", - "authTypeCredential": "क्रेडेंशियल", - "authTypeNone": "कोई नहीं", - "authTypeNoneDesc": "कोई भी क्रेडेंशियल स्टोर नहीं किए जाते हैं। हर बार कनेक्ट करते समय आपसे उपयोगकर्ता नाम और पासवर्ड मांगा जाएगा; वे सहेजे नहीं जाते हैं।", - "selectCredential": "एक क्रेडेंशियल चुनें...", - "connectionSettings": "कनेक्शन सेटिंग्स", - "displaySettings": "प्रदर्शन सेटिंग्स", - "audioSettings": "ऑडियो सेटिंग्स", - "rdpPerformance": "RDP प्रदर्शन", - "deviceRedirection": "डिवाइस पुनर्निर्देशन", - "session": "सत्र", - "gateway": "गेटवे", - "remoteApp": "RemoteApp", - "clipboard": "क्लिपबोर्ड", - "sessionRecording": "सत्र रिकॉर्डिंग", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC सेटिंग्स", - "terminalSettings": "टर्मिनल सेटिंग्स", - "rdpPort": "RDP पोर्ट", - "username": "उपयोगकर्ता नाम", - "password": "पासवर्ड", - "passwordSaved": "पासवर्ड सहेज लिया गया है, बदलने के लिए टाइप करें", - "domain": "डोमेन", - "securityMode": "सुरक्षा मोड", - "width": "चौड़ाई", - "height": "ऊंचाई", - "dpi": "DPI", - "clientName": "क्लाइंट नाम", - "initialProgram": "आरंभिक प्रोग्राम", - "serverLayout": "सर्वर लेआउट", - "timezone": "समय क्षेत्र", - "loadBalanceInfo": "लोड बैलेंस जानकारी / कुकी", - "loadBalanceInfoDesc": "RDS फ़ार्म लोड संतुलन के लिए RD कनेक्शन ब्रोकर कुकी (उदा. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd प्रॉक्सी", - "guacdHostname": "guacd होस्ट", - "guacdHostnamePlaceholder": "वैश्विक डिफ़ॉल्ट", - "guacdPort": "guacd पोर्ट", - "guacdProxyDesc": "इस कनेक्शन के लिए वैश्विक guacd इंस्टेंस को अधिलेखित करें। वैश्विक रूप से कॉन्फ़िगर किए गए guacd का उपयोग करने के लिए खाली छोड़ें।", - "gatewayHostname": "गेटवे होस्टनाम", - "gatewayPort": "गेटवे पोर्ट", - "gatewayUsername": "गेटवे उपयोगकर्ता नाम", - "gatewayPassword": "गेटवे पासवर्ड", - "gatewayDomain": "गेटवे डोमेन", - "remoteAppProgram": "RemoteApp प्रोग्राम", - "workingDirectory": "कार्यशील निर्देशिका", - "arguments": "तर्क", - "normalizeLineEndings": "लाइन अंत सामान्यीकृत करें", - "macAddress": "MAC पता", - "broadcastAddress": "प्रसारण पता", - "udpPort": "UDP पोर्ट", - "waitTimeS": "प्रतीक्षा समय (s)", - "driveName": "ड्राइव का नाम", - "drivePath": "ड्राइव पथ", - "ignoreCertificate": "प्रमाणपत्र अनदेखा करें", - "ignoreCertificateDesc": "स्व-हस्ताक्षरित प्रमाणपत्र वाले होस्ट से कनेक्शन की अनुमति दें", - "forceLossless": "दोषरहित बाध्य करें", - "forceLosslessDesc": "दोषरहित छवि एन्कोडिंग बाध्य करें (उच्च गुणवत्ता, अधिक बैंडविड्थ)", - "disableAudio": "ऑडियो अक्षम करें", - "disableAudioDesc": "दूरस्थ सत्र से सभी ऑडियो म्यूट करें", - "enableAudioInput": "ऑडियो इनपुट सक्षम करें (माइक्रोफ़ोन)", - "enableAudioInputDesc": "स्थानीय माइक्रोफ़ोन को दूरस्थ सत्र में अग्रेषित करें", - "wallpaper": "वॉलपेपर", - "wallpaperDesc": "डेस्कटॉप वॉलपेपर दिखाएं (अक्षम करने से प्रदर्शन बेहतर होता है)", - "theming": "थीमिंग", - "themingDesc": "दृश्य थीम और शैलियाँ सक्षम करें", - "fontSmoothing": "फ़ॉन्ट स्मूथिंग", - "fontSmoothingDesc": "ClearType फ़ॉन्ट रेंडरिंग सक्षम करें", - "fullWindowDrag": "पूर्ण विंडो ड्रैग", - "fullWindowDragDesc": "ड्रैग करते समय विंडो सामग्री दिखाएं", - "desktopComposition": "डेस्कटॉप कंपोज़िशन", - "desktopCompositionDesc": "Aero ग्लास प्रभाव सक्षम करें", - "menuAnimations": "मेन्यू एनिमेशन", - "menuAnimationsDesc": "मेन्यू फेड और स्लाइड एनिमेशन सक्षम करें", - "disableBitmapCaching": "बिटमैप कैशिंग अक्षम करें", - "disableBitmapCachingDesc": "बिटमैप कैश बंद करें (गड़बड़ियों में मदद कर सकता है)", - "disableOffscreenCaching": "ऑफस्क्रीन कैशिंग अक्षम करें", - "disableOffscreenCachingDesc": "ऑफस्क्रीन कैश बंद करें", - "disableGlyphCaching": "ग्लिफ़ कैशिंग अक्षम करें", - "disableGlyphCachingDesc": "ग्लिफ़ कैश बंद करें", - "enableGfx": "GFX सक्षम करें", - "enableGfxDesc": "RemoteFX ग्राफ़िक्स पाइपलाइन का उपयोग करें", - "enablePrinting": "प्रिंटिंग सक्षम करें", - "enablePrintingDesc": "स्थानीय प्रिंटर को दूरस्थ सत्र में पुनर्निर्देशित करें", - "enableDriveRedirection": "ड्राइव पुनर्निर्देशन सक्षम करें", - "enableDriveRedirectionDesc": "स्थानीय फ़ोल्डर को दूरस्थ सत्र में ड्राइव के रूप में मैप करें", - "createDrivePath": "ड्राइव पथ बनाएँ", - "createDrivePathDesc": "यदि फ़ोल्डर मौजूद न हो तो स्वचालित रूप से बनाएँ", - "disableDownload": "डाउनलोड अक्षम करें", - "disableDownloadDesc": "रिमोट सत्र से फ़ाइलें डाउनलोड करना रोकें", - "disableUpload": "अपलोड अक्षम करें", - "disableUploadDesc": "रिमोट सत्र पर फ़ाइलें अपलोड करना रोकें", - "enableTouch": "टच सक्षम करें", - "enableTouchDesc": "टच इनपुट अग्रेषण सक्षम करें", - "consoleSession": "कंसोल सत्र", - "consoleSessionDesc": "नए सत्र के बजाय कंसोल (सत्र 0) से कनेक्ट करें", - "sendWolPacket": "WOL पैकेट भेजें", - "sendWolPacketDesc": "कनेक्ट करने से पहले इस होस्ट को जगाने के लिए मैजिक पैकेट भेजें", - "disableCopy": "कॉपी अक्षम करें", - "disableCopyDesc": "रिमोट सत्र से टेक्स्ट कॉपी करना रोकें", - "disablePaste": "पेस्ट अक्षम करें", - "disablePasteDesc": "रिमोट सत्र में टेक्स्ट पेस्ट करना रोकें", - "createPathIfMissing": "यदि पथ मौजूद न हो तो बनाएँ", - "createPathIfMissingDesc": "रिकॉर्डिंग निर्देशिका स्वचालित रूप से बनाएँ", - "excludeOutput": "आउटपुट बहिष्कृत करें", - "excludeOutputDesc": "स्क्रीन आउटपुट रिकॉर्ड न करें (केवल मेटाडेटा)", - "excludeMouse": "माउस बहिष्कृत करें", - "excludeMouseDesc": "माउस गतिविधियाँ रिकॉर्ड न करें", - "includeKeystrokes": "कीस्ट्रोक्स शामिल करें", - "includeKeystrokesDesc": "स्क्रीन आउटपुट के अतिरिक्त कच्चे कीस्ट्रोक्स रिकॉर्ड करें", - "vncPort": "VNC पोर्ट", - "vncPassword": "VNC पासवर्ड", - "vncUsernameOptional": "उपयोगकर्ता नाम (वैकल्पिक)", - "vncLeaveBlank": "यदि आवश्यक न हो तो खाली छोड़ें", - "cursorMode": "कर्सर मोड", - "swapRedBlue": "लाल/नीला अदल-बदल करें", - "swapRedBlueDesc": "लाल और नीले रंग चैनलों को अदल-बदल करें (कुछ रंग समस्याओं को ठीक करता है)", - "readOnly": "केवल-पठनीय", - "readOnlyDesc": "बिना कोई इनपुट भेजे रिमोट स्क्रीन देखें", - "telnetPort": "टेलनेट पोर्ट", - "terminalType": "टर्मिनल प्रकार", - "fontName": "फ़ॉन्ट नाम", - "fontSize": "फ़ॉन्ट आकार", - "colorScheme": "रंग योजना", - "backspaceKey": "बैकस्पेस कुंजी", - "colorDepth": "रंग गहराई", - "resizeMethod": "रीसाइज़ विधि", - "driveNamePlaceholder": "टर्मिक्स ड्राइव", - "options": { - "any": "कोई", - "auto": "स्वचालित", - "nlaExtended": "एनएलए विस्तारित", - "color8": "8 बिट", - "color16": "16-बिट", - "color24": "24-बिट", - "color32": "32-बिट", - "displayUpdate": "डिस्प्ले अपडेट", - "reconnect": "पुनः कनेक्ट करें", - "preserve": "संरक्षित करना", - "unix": "यूनिक्स (एलएफ)", - "windows": "विंडोज़ (सीआरएलएफ)", - "local": "स्थानीय", - "remote": "रिमोट", - "failsafe": "सुरक्षा कम होना", - "blackOnWhite": "सफेद पर काला", - "whiteOnBlack": "काले पर सफेद", - "grayOnBlack": "काले पर धूसर", - "greenOnBlack": "काले पर हरा" - } - }, - "remoteDesktop": "दूरस्थ डेस्कटॉप", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "वर्चुअल नेटवर्क", - "unencryptedShell": "अनएन्क्रिप्टेड शेल", - "remoteDesktopNotes": "नोट्स", - "userDefaultsNote": "जिन सेटिंग्स को आप अपरिवर्तित छोड़ते हैं, वे आपके होस्ट की डिफ़ॉल्ट सेटिंग्स का पालन करती हैं।", - "connectionOriginNote": "डेस्कटॉप ऐप पर, RDP, VNC और Telnet रिमोट सर्वर का उपयोग करते हैं, जब तक कि होस्ट को \"यह डिवाइस\" पर सेट न किया गया हो। इसके लिए एक GUACD प्रॉक्सी की आवश्यकता होती है जिस तक यह मशीन पहुंच सके। इसे रिमोट डेस्कटॉप सेटिंग्स में या GUACD प्रॉक्सी के अंतर्गत प्रति होस्ट सेट करें।", - "docsLink": "दस्तावेज़ देखें", - "connectionOriginGuacamoleNote": "RDP, VNC और Telnet रिमोट सर्वर का उपयोग करते हैं, जब तक कि आप \"यह डिवाइस\" का चयन न करें, जिसके लिए एक guacd की आवश्यकता होती है जिस तक यह मशीन पहुंच सके। इसे guacd URL सेटिंग में या guacd प्रॉक्सी के अंतर्गत प्रति होस्ट सेट करें।" - }, - "onboarding": { - "feature_desktop": "दूरवर्ती डेस्कटॉप", - "feature_desktop_desc": "RDP, VNC और Telnet सेशन को SSH की तरह ही एक टैब में खोलें।" - }, - "settings": { - "user": { - "colorDepth": { - "label": "रंग गहराई", - "description": "इसका उपयोग उन आरडीपी होस्ट के लिए किया जाता है जो इसे डिफ़ॉल्ट सेटिंग पर छोड़ देते हैं।" - }, - "resizeMethod": { - "label": "रीसाइज़ विधि", - "description": "रिमोट स्क्रीन विंडो के आकार का अनुसरण कैसे करती है।" - }, - "resizeDisplayUpdate": "डिस्प्ले अपडेट", - "resizeReconnect": "पुनः कनेक्ट करें", - "inherit": "इनहेरिट", - "on": "चालू", - "off": "बंद", - "forceLossless": { - "label": "दोषरहित बाध्य करें", - "description": "दोषरहित छवि एन्कोडिंग बाध्य करें (उच्च गुणवत्ता, अधिक बैंडविड्थ)" - }, - "enableWallpaper": { - "label": "वॉलपेपर", - "description": "डेस्कटॉप वॉलपेपर दिखाएं (अक्षम करने से प्रदर्शन बेहतर होता है)" - }, - "enableFontSmoothing": { - "label": "फ़ॉन्ट स्मूथिंग", - "description": "ClearType फ़ॉन्ट रेंडरिंग सक्षम करें" - }, - "enableDesktopComposition": { - "label": "डेस्कटॉप कंपोज़िशन", - "description": "Aero ग्लास प्रभाव सक्षम करें" - }, - "disableAudio": { - "label": "ऑडियो अक्षम करें", - "description": "दूरस्थ सत्र से सभी ऑडियो म्यूट करें" - }, - "enablePrinting": { - "label": "प्रिंटिंग सक्षम करें", - "description": "स्थानीय प्रिंटर को दूरस्थ सत्र में पुनर्निर्देशित करें" - }, - "enableDrive": { - "label": "ड्राइव पुनर्निर्देशन सक्षम करें", - "description": "स्थानीय फ़ोल्डर को दूरस्थ सत्र में ड्राइव के रूप में मैप करें" - }, - "disableCopy": { - "label": "कॉपी अक्षम करें", - "description": "रिमोट सत्र से टेक्स्ट कॉपी करना रोकें" - }, - "disablePaste": { - "label": "पेस्ट अक्षम करें", - "description": "रिमोट सत्र में टेक्स्ट पेस्ट करना रोकें" - }, - "group": "रिमोट डेस्कटॉप डिफ़ॉल्ट", - "colorDepth16": "16-बिट", - "colorDepth24": "24-बिट", - "colorDepth32": "32-बिट" - }, - "admin": { - "enabled": { - "label": "रिमोट डेस्कटॉप सक्षम करें", - "description": "guacd के माध्यम से RDP, VNC और Telnet कनेक्शन की अनुमति दें।" - }, - "guacdUrl": { - "label": "guacd URL", - "description": "जहां guacd होस्ट:पोर्ट के रूप में सुनता है। GUACD_URL, GUACD_HOST और GUACD_PORT पर्यावरण चर इसे ओवरराइड करते हैं।", - "placeholder": "गुआकडी:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "आरडीपी पोर्ट" - }, - "vncPort": { - "label": "वीएनसी पोर्ट" - }, - "telnetPort": { - "label": "टेलनेट पोर्ट" - }, - "rdpSecurity": { - "label": "आरडीपी सुरक्षा मोड" - }, - "rdpIgnoreCert": { - "label": "प्रमाणपत्र त्रुटियों को अनदेखा करें" - }, - "guacamoleConfig": { - "label": "गुआकडी सेटिंग्स" - }, - "enableToolbar": { - "label": "रिमोट डेस्कटॉप टूलबार", - "description": "RDP, VNC और Telnet सत्रों में टूलबार दिखाएँ।" - }, - "displayGroup": "प्रदर्शन", - "displayDefaultsTab": "दूरस्थ डेस्कटॉप" - } - }, - "palette": { - "connectRdp": "आरडीपी से जुड़ें", - "connectVnc": "VNC से कनेक्ट करें", - "connectTelnet": "टेलनेट के माध्यम से कनेक्ट करें" - }, - "permissions": { - "sessions": { - "title": "रिमोट डेस्कटॉप सेशन साझा करें", - "description": "सेशन शेयरिंग और कोलैब रूम को इस उपयोगकर्ता के RDP, VNC और Telnet सेशन दूसरों को दिखाने की अनुमति दें।" - } - }, - "errors": { - "remoteServerRequired": "रिमोट सर्वर आवश्यक है। इस कनेक्शन प्रकार का उपयोग करने के लिए सेटिंग्स में जाकर रिमोट सर्वर कनेक्ट करें।" - }, - "plugin": { - "name": "दूरस्थ डेस्कटॉप" - } -} diff --git a/plugins/remote-desktop/locales/translated/hu_HU.json b/plugins/remote-desktop/locales/translated/hu_HU.json deleted file mode 100644 index a3b1dcccd..000000000 --- a/plugins/remote-desktop/locales/translated/hu_HU.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Kapcsolódás ehhez: {{type}} munkamenet...", - "checkingGuacd": "Távoli asztal szolgáltatás ellenőrzése...", - "requestingToken": "{{type}} kapcsolati token kérése...", - "openingSession": "{{type}} munkamenet megnyitása...", - "negotiating": "Kézfogásról tárgyalunk... {{type}}", - "sessionReady": "{{type}} munkamenet kész", - "connectionError": "Csatlakozási hiba", - "connectionFailed": "Kapcsolódás sikertelen", - "failedToConnect": "Nem sikerült lekérni a kapcsolati tokent", - "hostNotFound": "Nem található gazdagép", - "noHostSelected": "Nincs kiválasztva gazdagép", - "reconnect": "Újracsatlakozás", - "retry": "Újrapróbálkozás", - "guacdUnavailable": "A távoli asztali szolgáltatás (guacd) nem érhető el. Kérjük, győződjön meg róla, hogy a guacd fut, elérhető és megfelelően van konfigurálva az adminisztrátori beállításokban.", - "credentialPromptTitle": "RDP hitelesítő adatok megadása", - "credentialPromptDescription": "Ez a gazdagép úgy van beállítva, hogy csatlakozáskor hitelesítő adatokat kérjen. Ezeket csak ehhez a munkamenethez használjuk, és nem kerülnek mentésre.", - "connect": "Csatlakozás", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Drive-fájlok", - "parent": "Szülőkönyvtár", - "refresh": "Frissítés", - "empty": "Ez a mappa üres", - "listFailed": "Nem sikerült beolvasni ezt a mappát", - "download": "Letöltés", - "downloadFailed": "Nem sikerült letölteni {{name}}", - "upload": "Fájlok feltöltése", - "uploading": "Feltöltés {{name}}...", - "uploaded": "Feltöltve {{name}}", - "uploadFailed": "Nem sikerült feltölteni {{name}}", - "uploadDisabled": "A feltöltések le vannak tiltva ennél a kapcsolatnál.", - "driveUnavailable": "RDP meghajtó átirányításának engedélyezése fájlok feltöltése előtt", - "driveNotWritable": "A guacd nem tud írni a meghajtó mappájába. Ellenőrizd a GUACD_DRIVE_PATH-ot (vagy a gazdagép meghajtó elérési útját) és a hozzá tartozó jogosultságokat.", - "dropToUpload": "Fájlok húzása a feltöltéshez" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Képernyőzár)", - "winKey": "Windows billentyű", - "superL": "Super+L (Képernyőzár)", - "superKey": "Szuperkulcs", - "super": "Szuper", - "cmdL": "Cmd+L (Képernyő lezárása)", - "cmdKey": "Parancsbillentyű", - "cmd": "Parancs", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Váltás", - "win": "Győzelem", - "stickyActive": "{{key}} (reteszelve - kattintson a kioldáshoz)", - "stickyInactive": "{{key}} (kattintson a rögzítéshez)", - "esc": "Menekülés", - "tab": "Fül", - "home": "Otthon", - "end": "Vége", - "pageUp": "Oldal fel", - "pageDown": "Oldal le", - "arrowUp": "Felfelé nyíl", - "arrowDown": "Lefelé mutató nyíl", - "arrowLeft": "Balra nyíl", - "arrowRight": "Jobbra nyíl", - "fnToggle": "Funkcióbillentyűk", - "reconnect": "Munkamenet újracsatlakoztatása", - "collapse": "Eszköztár összecsukása", - "expand": "Eszköztár kibontása", - "dragHandle": "Húzással áthelyezhető", - "hide": "Elrejtés ehhez a munkamenethez. A végleges letiltáshoz nyissa meg a Gazdagép beállításai → Általános → Kapcsolat eszköztár menüpontot.", - "zoomIn": "Nagyítás", - "zoomOut": "Kicsinyítés", - "resetZoom": "Nagyítás visszaállítása", - "fitToScreen": "Képernyőhöz igazítás", - "switchToTrackpad": "Váltás trackpad módra (húzással mozgathatja a kurzort, koppintással kattinthat)", - "switchToTouch": "Váltás érintőképernyős módra (koppintson közvetlenül oda, ahová kattintani szeretne)" - }, - "disabled": "A Távoli asztal ki van kapcsolva. Egy rendszergazda bekapcsolhatja a Beállításokban." - }, - "hosts": { - "nativeRdpOpened": "Megnyitva a Windows Távoli asztal alkalmazásban", - "nativeRdpFailed": "Nem sikerült megnyitni a Windows távoli asztalt", - "openNativeRdp": "Megnyitás a Windows távoli asztalon", - "guac": { - "toolbar": "Eszköztár", - "connection": "Kapcsolat", - "authentication": "Hitelesítés", - "storedCredential": "Tárolt hitelesítő adatok", - "authMethod": "Aut. módszer", - "authTypeDirect": "Közvetlen", - "authTypeCredential": "Hitelesítő adat", - "authTypeNone": "Egyik sem", - "authTypeNoneDesc": "Nincsenek tárolt hitelesítő adatok. Minden csatlakozáskor meg kell adnia a felhasználónevet és a jelszót; ezek nem kerülnek mentésre.", - "selectCredential": "Válasszon hitelesítő adatot...", - "connectionSettings": "Kapcsolati beállítások", - "displaySettings": "Kijelzőbeállítások", - "audioSettings": "Hangbeállítások", - "rdpPerformance": "RDP-teljesítmény", - "deviceRedirection": "Eszközátirányítás", - "session": "Ülés", - "gateway": "Átjáró", - "remoteApp": "RemoteApp", - "clipboard": "Vágólap", - "sessionRecording": "Munkamenet-felvétel", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC-beállítások", - "terminalSettings": "Terminálbeállítások", - "rdpPort": "RDP-port", - "username": "Felhasználónév", - "password": "Jelszó", - "passwordSaved": "Jelszó mentve, gépelje be a módosításhoz", - "domain": "Domain", - "securityMode": "Biztonsági mód", - "width": "Szélesség", - "height": "Magasság", - "dpi": "DPI", - "clientName": "Ügyfél neve", - "initialProgram": "Kezdeti program", - "serverLayout": "Szerver elrendezése", - "timezone": "Időzóna", - "loadBalanceInfo": "Terheléselosztási információk / Süti", - "loadBalanceInfoDesc": "RD Connection Broker süti az RDS farm terheléselosztásához (pl. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd proxy", - "guacdHostname": "guacd Host", - "guacdHostnamePlaceholder": "Globális alapértelmezett", - "guacdPort": "guacd kikötő", - "guacdProxyDesc": "Írja felül a globális guacd példányt ehhez a kapcsolathoz. Hagyja üresen a globálisan konfigurált guacd használatához.", - "gatewayHostname": "Átjáró állomásneve", - "gatewayPort": "Átjáró kikötő", - "gatewayUsername": "Átjáró felhasználóneve", - "gatewayPassword": "Átjáró jelszava", - "gatewayDomain": "Átjáró domain", - "remoteAppProgram": "RemoteApp program", - "workingDirectory": "Munkakönyvtár", - "arguments": "érvek", - "normalizeLineEndings": "Sorvégek normalizálása", - "macAddress": "MAC-cím", - "broadcastAddress": "Sugárzási cím", - "udpPort": "UDP port", - "waitTimeS": "Várakozási idő (s)", - "driveName": "Meghajtó neve", - "drivePath": "Útvonal", - "ignoreCertificate": "Tanúsítvány figyelmen kívül hagyása", - "ignoreCertificateDesc": "Kapcsolódás engedélyezése önaláírt tanúsítványokkal rendelkező gazdagépekhez", - "forceLossless": "Veszteségmentes erő", - "forceLosslessDesc": "Veszteségmentes képkódolás kényszerítése (jobb minőség, nagyobb sávszélesség)", - "disableAudio": "Hang letiltása", - "disableAudioDesc": "Némítsa el az összes hangot a távoli munkamenetből", - "enableAudioInput": "Hangbemenet engedélyezése (mikrofon)", - "enableAudioInputDesc": "Helyi mikrofon továbbítása a távoli munkamenetbe", - "wallpaper": "Tapéta", - "wallpaperDesc": "Asztali háttérkép megjelenítése (a letiltás javítja a teljesítményt)", - "theming": "Témázás", - "themingDesc": "Vizuális témák és stílusok engedélyezése", - "fontSmoothing": "Betűsimítás", - "fontSmoothingDesc": "ClearType betűtípus-megjelenítés engedélyezése", - "fullWindowDrag": "Teljes ablak húzása", - "fullWindowDragDesc": "Ablak tartalmának megjelenítése húzás közben", - "desktopComposition": "Asztali kompozíció", - "desktopCompositionDesc": "Aero Glass effektek engedélyezése", - "menuAnimations": "Menüanimációk", - "menuAnimationsDesc": "Menü elhalványulásának és diaanimációk engedélyezése", - "disableBitmapCaching": "Bitképes gyorsítótár letiltása", - "disableBitmapCachingDesc": "Kapcsold ki a bitképes gyorsítótárat (segíthet a hibákon)", - "disableOffscreenCaching": "Képernyőn kívüli gyorsítótár letiltása", - "disableOffscreenCachingDesc": "Kapcsolja ki a képernyőn kívüli gyorsítótárat", - "disableGlyphCaching": "Karakterjel-gyorsítótár letiltása", - "disableGlyphCachingDesc": "Kapcsolja ki a karakterjel-gyorsítótárat", - "enableGfx": "GFX engedélyezése", - "enableGfxDesc": "RemoteFX grafikus folyamatsor használata", - "enablePrinting": "Nyomtatás engedélyezése", - "enablePrintingDesc": "Helyi nyomtatók átirányítása a távoli munkamenetre", - "enableDriveRedirection": "Meghajtó átirányításának engedélyezése", - "enableDriveRedirectionDesc": "Helyi mappa meghajtóként való leképezése a távoli munkamenetben", - "createDrivePath": "Útvonal létrehozása", - "createDrivePathDesc": "Automatikusan létrehozza a mappát, ha az nem létezik", - "disableDownload": "Letöltés letiltása", - "disableDownloadDesc": "Fájlok letöltésének megakadályozása a távoli munkamenetből", - "disableUpload": "Feltöltés letiltása", - "disableUploadDesc": "Fájlok távoli munkamenetbe való feltöltésének megakadályozása", - "enableTouch": "Érintés engedélyezése", - "enableTouchDesc": "Érintéses bevitel továbbításának engedélyezése", - "consoleSession": "Konzol munkamenet", - "consoleSessionDesc": "Csatlakozás a konzolhoz (0. munkamenet) egy új munkamenet helyett", - "sendWolPacket": "WOL csomag küldése", - "sendWolPacketDesc": "Küldjön egy varázslatos csomagot a gazdagép felébresztéséhez a csatlakozás előtt", - "disableCopy": "Másolás letiltása", - "disableCopyDesc": "Szöveg másolásának megakadályozása a távoli munkamenetből", - "disablePaste": "Beillesztés letiltása", - "disablePasteDesc": "Szöveg beillesztésének megakadályozása a távoli munkamenetbe", - "createPathIfMissing": "Útvonal létrehozása, ha hiányzik", - "createPathIfMissingDesc": "Felvételi könyvtár automatikus létrehozása", - "excludeOutput": "Kimenet kizárása", - "excludeOutputDesc": "Ne rögzítse a képernyőkimenetet (csak metaadatok)", - "excludeMouse": "Egér kizárása", - "excludeMouseDesc": "Ne rögzítse az egér mozgását", - "includeKeystrokes": "Billentyűleütések belefoglalása", - "includeKeystrokesDesc": "Nyers billentyűleütések rögzítése a képernyőn megjelenő kimenet mellett", - "vncPort": "VNC port", - "vncPassword": "VNC jelszó", - "vncUsernameOptional": "Felhasználónév (opcionális)", - "vncLeaveBlank": "Hagyja üresen, ha nem szükséges", - "cursorMode": "Kurzor mód", - "swapRedBlue": "Piros/Kék csere", - "swapRedBlueDesc": "Cserélje fel a piros és kék színcsatornákat (javít néhány színproblémát)", - "readOnly": "Csak olvasható", - "readOnlyDesc": "Távoli képernyő megtekintése bemenet küldése nélkül", - "telnetPort": "Telnet port", - "terminalType": "Terminál típusa", - "fontName": "Betűtípus neve", - "fontSize": "Betűméret", - "colorScheme": "Színséma", - "backspaceKey": "Visszatörlés billentyű", - "colorDepth": "Színmélység", - "resizeMethod": "Átméretezési módszer", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Bármilyen", - "auto": "Autó", - "nlaExtended": "NLA kiterjesztve", - "color8": "8 bites", - "color16": "16 bites", - "color24": "24 bites", - "color32": "32 bites", - "displayUpdate": "Kijelzőfrissítés", - "reconnect": "Újracsatlakozás", - "preserve": "Megőrzés", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Helyi", - "remote": "Távoli", - "failsafe": "meghibásodásbiztos", - "blackOnWhite": "Fekete-fehér", - "whiteOnBlack": "Fehér a feketén", - "grayOnBlack": "Szürke feketén", - "greenOnBlack": "Zöld a feketén" - } - }, - "remoteDesktop": "Távoli asztal", - "tabRdp": "Vidékfejlesztési Program", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Virtuális hálózat", - "unencryptedShell": "Titkosítatlan shell", - "remoteDesktopNotes": "Megjegyzések", - "userDefaultsNote": "A változatlanul hagyott beállítások a gazdagép alapértelmezett beállításait követik.", - "connectionOriginNote": "Az asztali alkalmazásban az RDP, a VNC és a Telnet a távoli szervert használja, kivéve, ha a gazdagép értéke „Ez az eszköz”, amelyhez egy olyan GUACD-re van szükség, amelyet ez a gép el tud érni. Állítson be egyet a Távoli asztal beállításaiban, vagy gazdagépenként a GUACD Proxy alatt.", - "docsLink": "Dokumentumok megtekintése", - "connectionOriginGuacamoleNote": "Az RDP, a VNC és a Telnet a távoli szervert használja, kivéve, ha az „Ez az eszköz” lehetőséget választod, amelyhez egy olyan GUACD-re van szükség, amelyet ez a gép el tud érni. Állíts be egyet a GUACD URL-beállításaiban, vagy hosztonként a GUACD Proxy alatt." - }, - "onboarding": { - "feature_desktop": "Távoli asztal", - "feature_desktop_desc": "Nyisson meg RDP, VNC és Telnet munkameneteket egy lapon, ugyanúgy, mint az SSH-n." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Színmélység", - "description": "Azoknál az RDP-gazdagépeknél használatos, amelyek az alapértelmezett értéken hagyják." - }, - "resizeMethod": { - "label": "Átméretezési módszer", - "description": "Hogyan követi a távoli képernyő az ablak méretét." - }, - "resizeDisplayUpdate": "Kijelzőfrissítés", - "resizeReconnect": "Újracsatlakozás", - "inherit": "Örököl", - "on": "On", - "off": "Le", - "forceLossless": { - "label": "Veszteségmentes erő", - "description": "Veszteségmentes képkódolás kényszerítése (jobb minőség, nagyobb sávszélesség)" - }, - "enableWallpaper": { - "label": "Tapéta", - "description": "Asztali háttérkép megjelenítése (a letiltás javítja a teljesítményt)" - }, - "enableFontSmoothing": { - "label": "Betűsimítás", - "description": "ClearType betűtípus-megjelenítés engedélyezése" - }, - "enableDesktopComposition": { - "label": "Asztali kompozíció", - "description": "Aero Glass effektek engedélyezése" - }, - "disableAudio": { - "label": "Hang letiltása", - "description": "Némítsa el az összes hangot a távoli munkamenetből" - }, - "enablePrinting": { - "label": "Nyomtatás engedélyezése", - "description": "Helyi nyomtatók átirányítása a távoli munkamenetre" - }, - "enableDrive": { - "label": "Meghajtó átirányításának engedélyezése", - "description": "Helyi mappa meghajtóként való leképezése a távoli munkamenetben" - }, - "disableCopy": { - "label": "Másolás letiltása", - "description": "Szöveg másolásának megakadályozása a távoli munkamenetből" - }, - "disablePaste": { - "label": "Beillesztés letiltása", - "description": "Szöveg beillesztésének megakadályozása a távoli munkamenetbe" - }, - "group": "Távoli asztal alapértelmezett beállításai", - "colorDepth16": "16 bites", - "colorDepth24": "24 bites", - "colorDepth32": "32 bites" - }, - "admin": { - "enabled": { - "label": "Távoli asztal engedélyezése", - "description": "RDP, VNC és Telnet kapcsolatok engedélyezése GUACD-n keresztül." - }, - "guacdUrl": { - "label": "guacd URL", - "description": "Ahol a guacd figyel, host:portként. A GUACD_URL, GUACD_HOST és GUACD_PORT környezeti változók felülírják ezt.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "Vidékfejlesztési Program" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP-port" - }, - "vncPort": { - "label": "VNC-port" - }, - "telnetPort": { - "label": "Telnet port" - }, - "rdpSecurity": { - "label": "RDP biztonsági mód" - }, - "rdpIgnoreCert": { - "label": "Tanúsítványhibák figyelmen kívül hagyása" - }, - "guacamoleConfig": { - "label": "guacd beállítások" - }, - "enableToolbar": { - "label": "Távoli asztal eszköztár", - "description": "Eszköztár megjelenítése RDP, VNC és Telnet munkamenetekben." - }, - "displayGroup": "Kijelző", - "displayDefaultsTab": "Távoli asztal" - } - }, - "palette": { - "connectRdp": "Kapcsolódjon az RDP-hez", - "connectVnc": "Csatlakozás VNC-vel", - "connectTelnet": "Csatlakozás Telnet-hez" - }, - "permissions": { - "sessions": { - "title": "Távoli asztali munkamenetek megosztása", - "description": "A munkamenet-megosztási és együttműködési szobák megjeleníthetik a felhasználó RDP-, VNC- és Telnet-munkameneteit mások számára." - } - }, - "errors": { - "remoteServerRequired": "Távoli szerver szükséges. Csatlakoztasson egy távoli szervert a Beállításokban a kapcsolattípus használatához." - }, - "plugin": { - "name": "Távoli asztal" - } -} diff --git a/plugins/remote-desktop/locales/translated/id_ID.json b/plugins/remote-desktop/locales/translated/id_ID.json deleted file mode 100644 index 04354841a..000000000 --- a/plugins/remote-desktop/locales/translated/id_ID.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Menghubungkan ke sesi {{type}}...", - "checkingGuacd": "Memeriksa layanan desktop jarak jauh...", - "requestingToken": "Meminta token koneksi {{type}}...", - "openingSession": "Membuka sesi {{type}}...", - "negotiating": "Bernegosiasi {{type}} jabat tangan...", - "sessionReady": "{{type}} sesi siap", - "connectionError": "Kesalahan koneksi", - "connectionFailed": "Koneksi gagal", - "failedToConnect": "Gagal mendapatkan token koneksi.", - "hostNotFound": "Host tidak ditemukan", - "noHostSelected": "Tidak ada pembawa acara yang dipilih.", - "reconnect": "Terhubung kembali", - "retry": "Mencoba kembali", - "guacdUnavailable": "Layanan desktop jarak jauh (guacd) tidak tersedia. Pastikan guacd berjalan, dapat diakses, dan dikonfigurasi dengan benar di pengaturan admin.", - "credentialPromptTitle": "Masukkan Kredensial RDP", - "credentialPromptDescription": "Host ini diatur untuk meminta kredensial saat terhubung. Kredensial tersebut hanya digunakan untuk sesi ini dan tidak disimpan.", - "connect": "Menghubungkan", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "File Drive", - "parent": "Direktori induk", - "refresh": "Menyegarkan", - "empty": "Folder ini kosong", - "listFailed": "Gagal membaca folder ini", - "download": "Unduh", - "downloadFailed": "Gagal mengunduh {{name}}", - "upload": "Unggah file", - "uploading": "Mengunggah {{name}}...", - "uploaded": "Diunggah {{name}}", - "uploadFailed": "Gagal mengunggah {{name}}", - "uploadDisabled": "Unggahan dinonaktifkan untuk koneksi ini.", - "driveUnavailable": "Aktifkan pengalihan drive RDP sebelum mengunggah file.", - "driveNotWritable": "guacd tidak dapat menulis ke folder drive. Periksa GUACD_DRIVE_PATH (atau Drive Path host) dan izinnya.", - "dropToUpload": "Seret file untuk diunggah" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Kunci Layar)", - "winKey": "Kunci Windows", - "superL": "Super+L (Layar Kunci)", - "superKey": "Kunci Super", - "super": "Super", - "cmdL": "Cmd+L (Kunci Layar)", - "cmdKey": "Tombol Perintah", - "cmd": "Perintah", - "ctrl": "Ctrl", - "alt": "Alternatif", - "shift": "Menggeser", - "win": "Menang", - "stickyActive": "{{key}} (terkunci - klik untuk membuka)", - "stickyInactive": "{{key}} (klik untuk mengunci)", - "esc": "Melarikan diri", - "tab": "Tab", - "home": "Rumah", - "end": "Akhir", - "pageUp": "Halaman Atas", - "pageDown": "Halaman ke Bawah", - "arrowUp": "Panah Atas", - "arrowDown": "Panah ke Bawah", - "arrowLeft": "Panah Kiri", - "arrowRight": "Panah Kanan", - "fnToggle": "Tombol Fungsi", - "reconnect": "Sesi Terhubung Kembali", - "collapse": "Ciutkan bilah alat", - "expand": "Perluas bilah alat", - "dragHandle": "Seret untuk mengubah posisi", - "hide": "Sembunyikan untuk sesi ini. Untuk menonaktifkannya secara permanen, buka Pengaturan Host → Umum → Bilah Alat Koneksi.", - "zoomIn": "Perbesar", - "zoomOut": "Perbesar tampilan", - "resetZoom": "Atur ulang zoom", - "fitToScreen": "Sesuaikan dengan layar", - "switchToTrackpad": "Beralih ke mode trackpad (seret untuk memindahkan kursor, ketuk untuk mengklik)", - "switchToTouch": "Beralih ke mode sentuh (ketuk langsung di tempat yang ingin Anda klik)" - }, - "disabled": "Akses Desktop Jarak Jauh dimatikan. Administrator dapat mengaktifkannya di Pengaturan." - }, - "hosts": { - "nativeRdpOpened": "Dibuka di Windows Remote Desktop", - "nativeRdpFailed": "Gagal membuka Windows Remote Desktop", - "openNativeRdp": "Buka di Windows Remote Desktop", - "guac": { - "toolbar": "Bilah alat", - "connection": "Koneksi", - "authentication": "Autentikasi", - "storedCredential": "Kredensial Tersimpan", - "authMethod": "Metode Otentikasi", - "authTypeDirect": "Langsung", - "authTypeCredential": "Mandat", - "authTypeNone": "Tidak ada", - "authTypeNoneDesc": "Tidak ada kredensial yang disimpan. Anda akan diminta memasukkan nama pengguna dan kata sandi setiap kali Anda terhubung; informasi tersebut tidak disimpan.", - "selectCredential": "Pilih kredensial...", - "connectionSettings": "Pengaturan Koneksi", - "displaySettings": "Pengaturan Tampilan", - "audioSettings": "Pengaturan Audio", - "rdpPerformance": "Kinerja RDP", - "deviceRedirection": "Pengalihan Perangkat", - "session": "Sidang", - "gateway": "Gerbang", - "remoteApp": "RemoteApp", - "clipboard": "Papan klip", - "sessionRecording": "Rekaman Sesi", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Pengaturan VNC", - "terminalSettings": "Pengaturan Terminal", - "rdpPort": "Port RDP", - "username": "Nama pengguna", - "password": "Kata sandi", - "passwordSaved": "Kata sandi tersimpan, ketik untuk mengubahnya", - "domain": "Domain", - "securityMode": "Mode Keamanan", - "width": "Lebar", - "height": "Tinggi", - "dpi": "DPI", - "clientName": "Nama Klien", - "initialProgram": "Program Awal", - "serverLayout": "Tata Letak Server", - "timezone": "Zona waktu", - "loadBalanceInfo": "Informasi Saldo Beban / Cookie", - "loadBalanceInfoDesc": "Cookie RD Connection Broker untuk penyeimbangan beban farm RDS (misalnya tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Proksi guacd", - "guacdHostname": "guacd Host", - "guacdHostnamePlaceholder": "Pengaturan default global", - "guacdPort": "guacd Port", - "guacdProxyDesc": "Ganti instance guacd global untuk koneksi ini. Biarkan kosong untuk menggunakan guacd yang dikonfigurasi secara global.", - "gatewayHostname": "Nama Host Gateway", - "gatewayPort": "Port Gerbang", - "gatewayUsername": "Nama Pengguna Gateway", - "gatewayPassword": "Kata Sandi Gerbang", - "gatewayDomain": "Domain Gerbang", - "remoteAppProgram": "Program RemoteApp", - "workingDirectory": "Direktori Kerja", - "arguments": "Argumen", - "normalizeLineEndings": "Normalisasi Akhiran Baris", - "macAddress": "Alamat MAC", - "broadcastAddress": "Pidato Siaran", - "udpPort": "Port UDP", - "waitTimeS": "Waktu Tunggu (detik)", - "driveName": "Nama Drive", - "drivePath": "Jalur Berkendara", - "ignoreCertificate": "Abaikan Sertifikat", - "ignoreCertificateDesc": "Izinkan koneksi ke host dengan sertifikat yang ditandatangani sendiri.", - "forceLossless": "Paksa Tanpa Kehilangan", - "forceLosslessDesc": "Paksa pengkodean gambar tanpa kehilangan data (kualitas lebih tinggi, bandwidth lebih besar)", - "disableAudio": "Nonaktifkan Audio", - "disableAudioDesc": "Nonaktifkan semua audio dari sesi jarak jauh.", - "enableAudioInput": "Aktifkan Input Audio (Mikrofon)", - "enableAudioInputDesc": "Teruskan mikrofon lokal ke sesi jarak jauh.", - "wallpaper": "Wallpaper", - "wallpaperDesc": "Tampilkan wallpaper desktop (menonaktifkannya akan meningkatkan kinerja)", - "theming": "Tema", - "themingDesc": "Aktifkan tema dan gaya visual.", - "fontSmoothing": "Penghalusan Font", - "fontSmoothingDesc": "Aktifkan rendering font ClearType", - "fullWindowDrag": "Geser Jendela Penuh", - "fullWindowDragDesc": "Tampilkan isi jendela saat menyeret", - "desktopComposition": "Komposisi Desktop", - "desktopCompositionDesc": "Aktifkan efek kaca Aero", - "menuAnimations": "Animasi Menu", - "menuAnimationsDesc": "Aktifkan animasi pudar dan geser menu", - "disableBitmapCaching": "Nonaktifkan Cache Bitmap", - "disableBitmapCachingDesc": "Matikan cache bitmap (dapat membantu mengatasi gangguan)", - "disableOffscreenCaching": "Nonaktifkan Cache di Luar Layar", - "disableOffscreenCachingDesc": "Matikan cache di luar layar.", - "disableGlyphCaching": "Nonaktifkan Cache Glif", - "disableGlyphCachingDesc": "Matikan cache glif", - "enableGfx": "Aktifkan GFX", - "enableGfxDesc": "Gunakan pipeline grafis RemoteFX.", - "enablePrinting": "Aktifkan Pencetakan", - "enablePrintingDesc": "Alihkan printer lokal ke sesi jarak jauh.", - "enableDriveRedirection": "Aktifkan Pengalihan Drive", - "enableDriveRedirectionDesc": "Petakan folder lokal sebagai drive di sesi jarak jauh.", - "createDrivePath": "Buat Jalur Mengemudi", - "createDrivePathDesc": "Buat folder secara otomatis jika belum ada.", - "disableDownload": "Nonaktifkan Unduhan", - "disableDownloadDesc": "Mencegah pengunduhan file dari sesi jarak jauh", - "disableUpload": "Nonaktifkan Unggahan", - "disableUploadDesc": "Mencegah pengunggahan file ke sesi jarak jauh", - "enableTouch": "Aktifkan Sentuhan", - "enableTouchDesc": "Aktifkan penerusan input sentuh", - "consoleSession": "Sesi Konsol", - "consoleSessionDesc": "Hubungkan ke konsol (sesi 0) alih-alih sesi baru.", - "sendWolPacket": "Kirim Paket WOL", - "sendWolPacketDesc": "Kirim paket ajaib untuk membangunkan host ini sebelum terhubung.", - "disableCopy": "Nonaktifkan Salin", - "disableCopyDesc": "Mencegah penyalinan teks dari sesi jarak jauh", - "disablePaste": "Nonaktifkan Tempel", - "disablePasteDesc": "Mencegah penyisipan teks ke dalam sesi jarak jauh", - "createPathIfMissing": "Buat Jalur jika Belum Ada", - "createPathIfMissingDesc": "Membuat direktori rekaman secara otomatis.", - "excludeOutput": "Kecualikan Output", - "excludeOutputDesc": "Jangan merekam output layar (hanya metadata)", - "excludeMouse": "Kecualikan Tikus", - "excludeMouseDesc": "Jangan merekam pergerakan mouse.", - "includeKeystrokes": "Sertakan Penekanan Tombol", - "includeKeystrokesDesc": "Rekam setiap ketukan keyboard mentah selain output layar.", - "vncPort": "Port VNC", - "vncPassword": "Kata Sandi VNC", - "vncUsernameOptional": "Nama pengguna (opsional)", - "vncLeaveBlank": "Kosongkan jika tidak diperlukan", - "cursorMode": "Mode Kursor", - "swapRedBlue": "Tukar Merah/Biru", - "swapRedBlueDesc": "Tukar saluran warna merah dan biru (memperbaiki beberapa masalah warna)", - "readOnly": "Hanya baca", - "readOnlyDesc": "Lihat layar jarak jauh tanpa mengirimkan input apa pun.", - "telnetPort": "Port Telnet", - "terminalType": "Jenis Terminal", - "fontName": "Nama Font", - "fontSize": "Ukuran Huruf", - "colorScheme": "Skema Warna", - "backspaceKey": "Tombol Backspace", - "colorDepth": "Kedalaman Warna", - "resizeMethod": "Metode Pengubahan Ukuran", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Setiap", - "auto": "Mobil", - "nlaExtended": "NLA Diperpanjang", - "color8": "8-bit", - "color16": "16-bit", - "color24": "24-bit", - "color32": "32-bit", - "displayUpdate": "Pembaruan Tampilan", - "reconnect": "Terhubung kembali", - "preserve": "Melestarikan", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Lokal", - "remote": "Terpencil", - "failsafe": "pengaman", - "blackOnWhite": "Hitam di atas Putih", - "whiteOnBlack": "Putih di atas Hitam", - "grayOnBlack": "Abu-abu di atas Hitam", - "greenOnBlack": "Hijau di atas Hitam" - } - }, - "remoteDesktop": "Desktop Jarak Jauh", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Jaringan Virtual", - "unencryptedShell": "Shell yang tidak terenkripsi", - "remoteDesktopNotes": "Catatan", - "userDefaultsNote": "Pengaturan yang Anda biarkan tidak berubah akan mengikuti pengaturan default host Anda.", - "connectionOriginNote": "Pada aplikasi desktop, RDP, VNC, dan Telnet menggunakan server jarak jauh kecuali jika host diatur ke Perangkat ini, yang memerlukan guacd yang dapat dijangkau oleh mesin ini. Atur satu di pengaturan Remote Desktop, atau per host di bawah guacd Proxy.", - "docsLink": "Lihat dokumen", - "connectionOriginGuacamoleNote": "RDP, VNC, dan Telnet menggunakan server jarak jauh kecuali Anda memilih \"Perangkat ini\", yang membutuhkan guacd yang dapat dijangkau oleh mesin ini. Tetapkan satu di pengaturan URL guacd, atau per host di bawah Proksi guacd." - }, - "onboarding": { - "feature_desktop": "Desktop jarak jauh", - "feature_desktop_desc": "Buka sesi RDP, VNC, dan Telnet dalam satu tab, sama seperti SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Kedalaman Warna", - "description": "Digunakan untuk host RDP yang membiarkan pengaturan tersebut pada nilai default." - }, - "resizeMethod": { - "label": "Metode Pengubahan Ukuran", - "description": "Bagaimana tampilan jarak jauh mengikuti ukuran jendela." - }, - "resizeDisplayUpdate": "Pembaruan Tampilan", - "resizeReconnect": "Terhubung kembali", - "inherit": "Mewarisi", - "on": "Pada", - "off": "Mati", - "forceLossless": { - "label": "Paksa Tanpa Kehilangan", - "description": "Paksa pengkodean gambar tanpa kehilangan data (kualitas lebih tinggi, bandwidth lebih besar)" - }, - "enableWallpaper": { - "label": "Wallpaper", - "description": "Tampilkan wallpaper desktop (menonaktifkannya akan meningkatkan kinerja)" - }, - "enableFontSmoothing": { - "label": "Penghalusan Font", - "description": "Aktifkan rendering font ClearType" - }, - "enableDesktopComposition": { - "label": "Komposisi Desktop", - "description": "Aktifkan efek kaca Aero" - }, - "disableAudio": { - "label": "Nonaktifkan Audio", - "description": "Nonaktifkan semua audio dari sesi jarak jauh." - }, - "enablePrinting": { - "label": "Aktifkan Pencetakan", - "description": "Alihkan printer lokal ke sesi jarak jauh." - }, - "enableDrive": { - "label": "Aktifkan Pengalihan Drive", - "description": "Petakan folder lokal sebagai drive di sesi jarak jauh." - }, - "disableCopy": { - "label": "Nonaktifkan Salin", - "description": "Mencegah penyalinan teks dari sesi jarak jauh" - }, - "disablePaste": { - "label": "Nonaktifkan Tempel", - "description": "Mencegah penyisipan teks ke dalam sesi jarak jauh" - }, - "group": "Pengaturan default Remote Desktop", - "colorDepth16": "16-bit", - "colorDepth24": "24-bit", - "colorDepth32": "32-bit" - }, - "admin": { - "enabled": { - "label": "Aktifkan Remote Desktop", - "description": "Izinkan koneksi RDP, VNC, dan Telnet melalui guacd." - }, - "guacdUrl": { - "label": "URL guacd", - "description": "Guacd mendengarkan di mana, sebagai host:port. Variabel lingkungan GUACD_URL, GUACD_HOST, dan GUACD_PORT akan menimpa pengaturan ini.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "Port RDP" - }, - "vncPort": { - "label": "Port VNC" - }, - "telnetPort": { - "label": "Port Telnet" - }, - "rdpSecurity": { - "label": "Mode keamanan RDP" - }, - "rdpIgnoreCert": { - "label": "Abaikan kesalahan sertifikat" - }, - "guacamoleConfig": { - "label": "pengaturan guacd" - }, - "enableToolbar": { - "label": "Toolbar desktop jarak jauh", - "description": "Tampilkan bilah alat di sesi RDP, VNC, dan Telnet." - }, - "displayGroup": "Menampilkan", - "displayDefaultsTab": "Desktop Jarak Jauh" - } - }, - "palette": { - "connectRdp": "Terhubung dengan RDP", - "connectVnc": "Terhubung dengan VNC", - "connectTelnet": "Terhubung dengan Telnet" - }, - "permissions": { - "sessions": { - "title": "Berbagi sesi desktop jarak jauh", - "description": "Izinkan fitur berbagi sesi dan ruang kolaborasi untuk menampilkan sesi RDP, VNC, dan Telnet pengguna ini kepada pengguna lain." - } - }, - "errors": { - "remoteServerRequired": "Diperlukan server jarak jauh. Hubungkan server jarak jauh di Pengaturan untuk menggunakan jenis koneksi ini." - }, - "plugin": { - "name": "Desktop Jarak Jauh" - } -} diff --git a/plugins/remote-desktop/locales/translated/it_IT.json b/plugins/remote-desktop/locales/translated/it_IT.json deleted file mode 100644 index 4075903fe..000000000 --- a/plugins/remote-desktop/locales/translated/it_IT.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Connessione alla sessione {{type}} in corso...", - "checkingGuacd": "Verifica del servizio desktop remoto...", - "requestingToken": "Richiesta del token di connessione {{type}}...", - "openingSession": "Apertura della sessione {{type}}...", - "negotiating": "Negoziazione della stretta di mano {{type}}...", - "sessionReady": "{{type}} sessione pronta", - "connectionError": "Errore di connessione", - "connectionFailed": "Connessione non riuscita", - "failedToConnect": "Impossibile ottenere il token di connessione", - "hostNotFound": "Host non trovato", - "noHostSelected": "Nessun host selezionato", - "reconnect": "Riconnetti", - "retry": "Riprova", - "guacdUnavailable": "Il servizio desktop remoto (guacd) non è disponibile. Assicurati che guacd sia in esecuzione, accessibile e configurato correttamente nelle impostazioni di amministrazione.", - "credentialPromptTitle": "Inserisci le credenziali RDP", - "credentialPromptDescription": "Questo host è configurato per richiedere le credenziali al momento della connessione. Tali credenziali vengono utilizzate solo per questa sessione e non vengono salvate.", - "connect": "Connetti", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "File di Drive", - "parent": "Cartella principale", - "refresh": "Aggiorna", - "empty": "Questa cartella è vuota", - "listFailed": "Impossibile leggere questa cartella", - "download": "Scarica", - "downloadFailed": "Impossibile scaricare {{name}}", - "upload": "Carica i file", - "uploading": "Caricamento di {{name}}...", - "uploaded": "Caricato {{name}}", - "uploadFailed": "Impossibile caricare {{name}}", - "uploadDisabled": "Il caricamento è disabilitato per questa connessione.", - "driveUnavailable": "Abilita il reindirizzamento delle unità RDP prima di caricare i file", - "driveNotWritable": "guacd non può scrivere nella cartella dell'unità. Verificare GUACD_DRIVE_PATH (o il percorso dell'unità host) e le relative autorizzazioni.", - "dropToUpload": "Trascina i file da caricare" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Blocca schermo)", - "winKey": "Tasto Windows", - "superL": "Super+L (Schermata di blocco)", - "superKey": "Super Chiave", - "super": "Super", - "cmdL": "Cmd+L (Blocca schermo)", - "cmdKey": "Tasto di comando", - "cmd": "Comando", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Maiusc", - "win": "Win", - "stickyActive": "{{key}} (bloccato - clicca per rilasciare)", - "stickyInactive": "{{key}} (clicca per bloccare)", - "esc": "Esc", - "tab": "Tab", - "home": "Home", - "end": "Fine", - "pageUp": "Pag Su", - "pageDown": "Pag Giù", - "arrowUp": "Freccia Su", - "arrowDown": "Freccia Giù", - "arrowLeft": "Freccia Sinistra", - "arrowRight": "Freccia Destra", - "fnToggle": "Tasti funzione", - "reconnect": "Riconnetti sessione", - "collapse": "Comprimi barra degli strumenti", - "expand": "Espandi barra degli strumenti", - "dragHandle": "Trascina per riposizionare", - "hide": "Nascondi per questa sessione. Per disabilitarlo in modo permanente, apri Impostazioni host → Generale → Barra degli strumenti di connessione.", - "zoomIn": "Ingrandisci", - "zoomOut": "Riduci lo zoom", - "resetZoom": "Ripristina lo zoom", - "fitToScreen": "Adatto allo schermo", - "switchToTrackpad": "Passa alla modalità trackpad (trascina per spostare il cursore, tocca per fare clic)", - "switchToTouch": "Passa alla modalità touch (tocca direttamente il punto in cui desideri fare clic)" - }, - "disabled": "Desktop remoto è disattivato. Un amministratore può attivarlo nelle Impostazioni." - }, - "hosts": { - "nativeRdpOpened": "Aperto in Desktop remoto di Windows", - "nativeRdpFailed": "Impossibile aprire Desktop remoto di Windows", - "openNativeRdp": "Apri in Desktop remoto di Windows", - "guac": { - "toolbar": "barra degli strumenti", - "connection": "Connessione", - "authentication": "Autenticazione", - "storedCredential": "Credenziale memorizzata", - "authMethod": "Metodo di autenticazione", - "authTypeDirect": "Diretta", - "authTypeCredential": "Credenziale", - "authTypeNone": "Nessuno", - "authTypeNoneDesc": "Non vengono memorizzate credenziali. Ti verrà richiesto di inserire nome utente e password ogni volta che effettuerai la connessione; questi dati non verranno salvati.", - "selectCredential": "Seleziona una credenziale...", - "connectionSettings": "Impostazioni di connessione", - "displaySettings": "Impostazioni schermo", - "audioSettings": "Impostazioni audio", - "rdpPerformance": "Prestazioni RDP", - "deviceRedirection": "Reindirizzamento dispositivi", - "session": "Sessione", - "gateway": "Gateway", - "remoteApp": "RemoteApp", - "clipboard": "Appunti", - "sessionRecording": "Registrazione sessione", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Impostazioni VNC", - "terminalSettings": "Impostazioni terminale", - "rdpPort": "Porta RDP", - "username": "Nome utente", - "password": "Password", - "passwordSaved": "Password salvata, digita per modificarla", - "domain": "Dominio", - "securityMode": "Modalità di sicurezza", - "width": "Larghezza", - "height": "Altezza", - "dpi": "DPI", - "clientName": "Nome client", - "initialProgram": "Programma iniziale", - "serverLayout": "Layout del server", - "timezone": "Fuso orario", - "loadBalanceInfo": "Info bilanciamento carico / Cookie", - "loadBalanceInfoDesc": "Cookie del RD Connection Broker per il bilanciamento del carico della farm RDS (es. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Proxy guacd", - "guacdHostname": "Host guacd", - "guacdHostnamePlaceholder": "Default globale", - "guacdPort": "Porta guacd", - "guacdProxyDesc": "Sostituisci l'istanza guacd globale per questa connessione. Lascia vuoto per utilizzare il guacd configurato globalmente.", - "gatewayHostname": "Hostname del gateway", - "gatewayPort": "Porta del gateway", - "gatewayUsername": "Nome utente gateway", - "gatewayPassword": "Password gateway", - "gatewayDomain": "Dominio gateway", - "remoteAppProgram": "Programma RemoteApp", - "workingDirectory": "Directory di lavoro", - "arguments": "Argomenti", - "normalizeLineEndings": "Normalizza terminazioni di riga", - "macAddress": "Indirizzo MAC", - "broadcastAddress": "Indirizzo di broadcast", - "udpPort": "Porta UDP", - "waitTimeS": "Tempo di attesa (s)", - "driveName": "Nome unità", - "drivePath": "Percorso unità", - "ignoreCertificate": "Ignora certificato", - "ignoreCertificateDesc": "Consente connessioni a host con certificati autofirmati", - "forceLossless": "Forza lossless", - "forceLosslessDesc": "Forza la codifica lossless delle immagini (qualità più alta, maggiore utilizzo di banda)", - "disableAudio": "Disabilita audio", - "disableAudioDesc": "Disattiva tutto l'audio dalla sessione remota", - "enableAudioInput": "Abilita ingresso audio (microfono)", - "enableAudioInputDesc": "Inoltra il microfono locale alla sessione remota", - "wallpaper": "Sfondo", - "wallpaperDesc": "Mostra lo sfondo del desktop (disabilitarlo migliora le prestazioni)", - "theming": "Temi", - "themingDesc": "Abilita temi e stili visivi", - "fontSmoothing": "Smussamento caratteri", - "fontSmoothingDesc": "Abilita il rendering dei caratteri ClearType", - "fullWindowDrag": "Trascinamento finestra intera", - "fullWindowDragDesc": "Mostra il contenuto della finestra durante il trascinamento", - "desktopComposition": "Composizione desktop", - "desktopCompositionDesc": "Abilita gli effetti Aero Glass", - "menuAnimations": "Animazioni menu", - "menuAnimationsDesc": "Abilita animazioni di dissolvenza e scorrimento dei menu", - "disableBitmapCaching": "Disabilita cache bitmap", - "disableBitmapCachingDesc": "Disattiva la cache bitmap (può essere utile per risolvere anomalie)", - "disableOffscreenCaching": "Disabilita cache fuori schermo", - "disableOffscreenCachingDesc": "Disattiva la cache fuori schermo", - "disableGlyphCaching": "Disabilita cache glifi", - "disableGlyphCachingDesc": "Disattiva la cache dei glifi", - "enableGfx": "Abilita GFX", - "enableGfxDesc": "Usa la pipeline grafica RemoteFX", - "enablePrinting": "Abilita stampa", - "enablePrintingDesc": "Reindirizza le stampanti locali alla sessione remota", - "enableDriveRedirection": "Abilita reindirizzamento unità", - "enableDriveRedirectionDesc": "Mappa una cartella locale come unità nella sessione remota", - "createDrivePath": "Crea percorso drive", - "createDrivePathDesc": "Crea automaticamente la cartella se non esiste", - "disableDownload": "Disabilita download", - "disableDownloadDesc": "Impedisce il download di file dalla sessione remota", - "disableUpload": "Disabilita upload", - "disableUploadDesc": "Impedisce il caricamento di file sulla sessione remota", - "enableTouch": "Abilita input tattile", - "enableTouchDesc": "Abilita l'inoltro dell'input tattile", - "consoleSession": "Sessione console", - "consoleSessionDesc": "Connetti alla console (sessione 0) invece di una nuova sessione", - "sendWolPacket": "Invia pacchetto WOL", - "sendWolPacketDesc": "Invia un pacchetto magico per riattivare questo host prima della connessione", - "disableCopy": "Disabilita copia", - "disableCopyDesc": "Impedisce la copia di testo dalla sessione remota", - "disablePaste": "Disabilita incolla", - "disablePasteDesc": "Impedisce di incollare testo nella sessione remota", - "createPathIfMissing": "Crea percorso se mancante", - "createPathIfMissingDesc": "Crea automaticamente la directory di registrazione", - "excludeOutput": "Escludi output", - "excludeOutputDesc": "Non registrare l'output dello schermo (solo metadati)", - "excludeMouse": "Escludi mouse", - "excludeMouseDesc": "Non registrare i movimenti del mouse", - "includeKeystrokes": "Includi tasti premuti", - "includeKeystrokesDesc": "Registra i tasti premuti non elaborati in aggiunta all'output dello schermo", - "vncPort": "Porta VNC", - "vncPassword": "Password VNC", - "vncUsernameOptional": "Nome utente (opzionale)", - "vncLeaveBlank": "Lascia vuoto se non richiesto", - "cursorMode": "Modalità cursore", - "swapRedBlue": "Scambia Rosso/Blu", - "swapRedBlueDesc": "Scambia i canali di colore rosso e blu (corregge alcuni problemi di colore)", - "readOnly": "Sola lettura", - "readOnlyDesc": "Visualizza lo schermo remoto senza inviare alcun input", - "telnetPort": "Porta Telnet", - "terminalType": "Tipo terminale", - "fontName": "Nome font", - "fontSize": "Dimensione font", - "colorScheme": "Schema colori", - "backspaceKey": "Tasto Backspace", - "colorDepth": "Profondità colore", - "resizeMethod": "Metodo di ridimensionamento", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Qualunque", - "auto": "Auto", - "nlaExtended": "NLA esteso", - "color8": "8 bit", - "color16": "16 bit", - "color24": "24 bit", - "color32": "32 bit", - "displayUpdate": "Aggiornamento della visualizzazione", - "reconnect": "Riconnetti", - "preserve": "Conserva", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Locale", - "remote": "Remoto", - "failsafe": "Sistema di sicurezza", - "blackOnWhite": "Nero su bianco", - "whiteOnBlack": "Bianco su nero", - "grayOnBlack": "Grigio su nero", - "greenOnBlack": "Verde su nero" - } - }, - "remoteDesktop": "Desktop remoto", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Rete virtuale", - "unencryptedShell": "Shell non crittografata", - "remoteDesktopNotes": "Note", - "userDefaultsNote": "Le impostazioni che lasci invariate seguiranno le impostazioni predefinite del tuo provider di hosting.", - "connectionOriginNote": "Nell'applicazione desktop, RDP, VNC e Telnet utilizzano il server remoto a meno che l'host non sia impostato su \"Questo dispositivo\", che richiede un GUACD raggiungibile da questa macchina. Impostalo nelle impostazioni di Desktop remoto oppure per ogni host in Proxy GUACD.", - "docsLink": "Visualizza documentazione", - "connectionOriginGuacamoleNote": "RDP, VNC e Telnet utilizzano il server remoto a meno che non si selezioni \"Questo dispositivo\", che richiede un GUACD raggiungibile da questa macchina. È possibile impostarlo nelle impostazioni dell'URL di GUACD oppure per ogni host in \"Proxy di GUACD\"." - }, - "onboarding": { - "feature_desktop": "Desktop remoto", - "feature_desktop_desc": "Apri le sessioni RDP, VNC e Telnet in una scheda, proprio come per SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Profondità colore", - "description": "Utilizzato per gli host RDP che mantengono l'impostazione predefinita." - }, - "resizeMethod": { - "label": "Metodo di ridimensionamento", - "description": "Come lo schermo remoto si adatta alle dimensioni della finestra." - }, - "resizeDisplayUpdate": "Aggiornamento della visualizzazione", - "resizeReconnect": "Riconnetti", - "inherit": "Ereditare", - "on": "Attivo", - "off": "Disattivo", - "forceLossless": { - "label": "Forza lossless", - "description": "Forza la codifica lossless delle immagini (qualità più alta, maggiore utilizzo di banda)" - }, - "enableWallpaper": { - "label": "Sfondo", - "description": "Mostra lo sfondo del desktop (disabilitarlo migliora le prestazioni)" - }, - "enableFontSmoothing": { - "label": "Smussamento caratteri", - "description": "Abilita il rendering dei caratteri ClearType" - }, - "enableDesktopComposition": { - "label": "Composizione desktop", - "description": "Abilita gli effetti Aero Glass" - }, - "disableAudio": { - "label": "Disabilita audio", - "description": "Disattiva tutto l'audio dalla sessione remota" - }, - "enablePrinting": { - "label": "Abilita stampa", - "description": "Reindirizza le stampanti locali alla sessione remota" - }, - "enableDrive": { - "label": "Abilita reindirizzamento unità", - "description": "Mappa una cartella locale come unità nella sessione remota" - }, - "disableCopy": { - "label": "Disabilita copia", - "description": "Impedisce la copia di testo dalla sessione remota" - }, - "disablePaste": { - "label": "Disabilita incolla", - "description": "Impedisce di incollare testo nella sessione remota" - }, - "group": "Impostazioni predefinite di Desktop remoto", - "colorDepth16": "16 bit", - "colorDepth24": "24 bit", - "colorDepth32": "32 bit" - }, - "admin": { - "enabled": { - "label": "Abilita Desktop remoto", - "description": "Consenti connessioni RDP, VNC e Telnet tramite guacd." - }, - "guacdUrl": { - "label": "URL guacd", - "description": "Dove guacd è in ascolto, come host:porta. Le variabili d'ambiente GUACD_URL, GUACD_HOST e GUACD_PORT sovrascrivono questa impostazione.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "porta RDP" - }, - "vncPort": { - "label": "porta VNC" - }, - "telnetPort": { - "label": "Porta Telnet" - }, - "rdpSecurity": { - "label": "Modalità di sicurezza RDP" - }, - "rdpIgnoreCert": { - "label": "Ignora gli errori del certificato" - }, - "guacamoleConfig": { - "label": "impostazioni guacd" - }, - "enableToolbar": { - "label": "Barra degli strumenti del desktop remoto", - "description": "Mostra la barra degli strumenti nelle sessioni RDP, VNC e Telnet." - }, - "displayGroup": "Display", - "displayDefaultsTab": "Desktop remoto" - } - }, - "palette": { - "connectRdp": "Connettiti tramite RDP", - "connectVnc": "Connettiti tramite VNC", - "connectTelnet": "Connettiti con Telnet" - }, - "permissions": { - "sessions": { - "title": "Condividere sessioni di desktop remoto", - "description": "Consenti la condivisione di sessioni e le stanze di collaborazione per mostrare le sessioni RDP, VNC e Telnet di questo utente agli altri." - } - }, - "errors": { - "remoteServerRequired": "È necessario un server remoto. Per utilizzare questo tipo di connessione, collega un server remoto nelle Impostazioni." - }, - "plugin": { - "name": "Desktop remoto" - } -} diff --git a/plugins/remote-desktop/locales/translated/ja_JP.json b/plugins/remote-desktop/locales/translated/ja_JP.json deleted file mode 100644 index 937e1babb..000000000 --- a/plugins/remote-desktop/locales/translated/ja_JP.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "{{type}} セッションに接続中...", - "checkingGuacd": "リモートデスクトップサービスを確認しています...", - "requestingToken": "{{type}} 接続トークンを要求しています...", - "openingSession": "{{type}} セッションを開始しています...", - "negotiating": "交渉中 {{type}} 握手...", - "sessionReady": "{{type}} セッション準備完了", - "connectionError": "接続エラー", - "connectionFailed": "接続に失敗しました", - "failedToConnect": "接続トークンの取得に失敗しました", - "hostNotFound": "ホストが見つかりません", - "noHostSelected": "ホストが選択されていません", - "reconnect": "再接続", - "retry": "再試行", - "guacdUnavailable": "リモートデスクトップサービス (guacd) が利用できません。guacd が実行中でアクセス可能であり、管理者設定で正しく構成されていることを確認してください。", - "credentialPromptTitle": "RDP認証情報を入力してください", - "credentialPromptDescription": "このホストは、接続時に認証情報の入力を求めるように設定されています。認証情報は今回のセッションのみで使用され、保存されません。", - "connect": "接続", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "ドライブファイル", - "parent": "親ディレクトリ", - "refresh": "再読み込み", - "empty": "このフォルダーは空です", - "listFailed": "このフォルダの読み取りに失敗しました", - "download": "ダウンロード", - "downloadFailed": "ダウンロードに失敗しました {{name}}", - "upload": "ファイルをアップロードする", - "uploading": "{{name}} をアップロード中...", - "uploaded": "アップロード済み {{name}}", - "uploadFailed": "アップロードに失敗しました {{name}}", - "uploadDisabled": "この接続ではアップロードが無効になっています。", - "driveUnavailable": "ファイルをアップロードする前に、RDPドライブリダイレクトを有効にしてください。", - "driveNotWritable": "guacd はドライブフォルダに書き込むことができません。GUACD_DRIVE_PATH(またはホストのドライブパス)とそのアクセス許可を確認してください。", - "dropToUpload": "アップロードするファイルをドロップしてください" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (画面ロック)", - "winKey": "Windows キー", - "superL": "Super+L(ロック画面)", - "superKey": "スーパーキー", - "super": "素晴らしい", - "cmdL": "Cmd+L(画面ロック)", - "cmdKey": "コマンドキー", - "cmd": "コマンド", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}}(ラッチ中 - クリックで解除)", - "stickyInactive": "{{key}}(クリックでラッチ)", - "esc": "Escape", - "tab": "Tab", - "home": "Home", - "end": "エンド", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "↑", - "arrowDown": "↓", - "arrowLeft": "←", - "arrowRight": "→", - "fnToggle": "ファンクションキー", - "reconnect": "セッション再接続", - "collapse": "ツールバーを折りたたむ", - "expand": "ツールバーを展開", - "dragHandle": "ドラッグで位置変更", - "hide": "このセッション中は非表示にします。完全に無効にするには、ホスト設定 → 一般 → 接続ツールバーを開いてください。", - "zoomIn": "ズームイン", - "zoomOut": "ズームアウト", - "resetZoom": "ズームをリセット", - "fitToScreen": "画面に合わせる", - "switchToTrackpad": "トラックパッドモードに切り替える(ドラッグでカーソルを移動、タップでクリック)", - "switchToTouch": "タッチモードに切り替える(クリックしたい場所を直接タップする)" - }, - "disabled": "リモートデスクトップは無効になっています。管理者は設定画面で有効にできます。" - }, - "hosts": { - "nativeRdpOpened": "Windowsリモートデスクトップで開きました", - "nativeRdpFailed": "Windowsリモートデスクトップを開くことができませんでした", - "openNativeRdp": "Windowsリモートデスクトップで開く", - "guac": { - "toolbar": "ツールバー", - "connection": "接続", - "authentication": "認証", - "storedCredential": "保存された認証情報", - "authMethod": "認証方法", - "authTypeDirect": "直接", - "authTypeCredential": "認証情報", - "authTypeNone": "すべて解除", - "authTypeNoneDesc": "認証情報は保存されません。接続するたびにユーザー名とパスワードの入力を求められますが、それらは保存されません。", - "selectCredential": "認証情報を選択...", - "connectionSettings": "接続設定", - "displaySettings": "表示設定", - "audioSettings": "オーディオ設定", - "rdpPerformance": "RDP パフォーマンス", - "deviceRedirection": "デバイスリダイレクト", - "session": "セッション", - "gateway": "ゲートウェイ", - "remoteApp": "RemoteApp", - "clipboard": "クリップボード", - "sessionRecording": "セッション記録", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC設定", - "terminalSettings": "ターミナル設定", - "rdpPort": "RDPポート", - "username": "ユーザー名", - "password": "パスワード", - "passwordSaved": "パスワードが保存されました。変更するには入力してください。", - "domain": "ドメイン", - "securityMode": "セキュリティモード", - "width": "幅", - "height": "高さ", - "dpi": "DPI", - "clientName": "クライアント名", - "initialProgram": "初期プログラム", - "serverLayout": "サーバーレイアウト", - "timezone": "タイムゾーン", - "loadBalanceInfo": "負荷分散情報 / Cookie", - "loadBalanceInfoDesc": "RDSファームの負荷分散用RD接続ブローカーCookie (例: tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacdプロキシ", - "guacdHostname": "guacdホスト", - "guacdHostnamePlaceholder": "全体のデフォルト", - "guacdPort": "guacdポート", - "guacdProxyDesc": "この接続に対してグローバル guacd インスタンスを上書きします。空白にするとグローバルに設定された guacd を使用します。", - "gatewayHostname": "ゲートウェイホスト名", - "gatewayPort": "ゲートウェイポート", - "gatewayUsername": "ゲートウェイユーザー名", - "gatewayPassword": "ゲートウェイパスワード", - "gatewayDomain": "ゲートウェイドメイン", - "remoteAppProgram": "RemoteAppプログラム", - "workingDirectory": "作業ディレクトリ", - "arguments": "引数", - "normalizeLineEndings": "改行コードの正規化", - "macAddress": "MACアドレス", - "broadcastAddress": "ブロードキャストアドレス", - "udpPort": "UDPポート", - "waitTimeS": "待機時間(秒)", - "driveName": "ドライブ名", - "drivePath": "ドライブパス", - "ignoreCertificate": "証明書を無視", - "ignoreCertificateDesc": "自己署名証明書を持つホストへの接続を許可する", - "forceLossless": "ロスレスを強制", - "forceLosslessDesc": "ロスレス画像エンコーディングを強制する(高品質だが帯域幅を多く消費)", - "disableAudio": "オーディオを無効化", - "disableAudioDesc": "リモートセッションの音声をすべてミュートする", - "enableAudioInput": "音声入力(マイク)を有効化", - "enableAudioInputDesc": "ローカルマイクをリモートセッションに転送する", - "wallpaper": "壁紙", - "wallpaperDesc": "デスクトップ壁紙を表示する(無効にするとパフォーマンスが向上)", - "theming": "テーマ", - "themingDesc": "ビジュアルテーマとスタイルを有効にする", - "fontSmoothing": "フォントスムージング", - "fontSmoothingDesc": "ClearTypeフォントレンダリングを有効にする", - "fullWindowDrag": "フルウィンドウドラッグ", - "fullWindowDragDesc": "ウィンドウドラッグ中に内容を表示する", - "desktopComposition": "デスクトップコンポジション", - "desktopCompositionDesc": "Aero Glass効果を有効にする", - "menuAnimations": "メニューアニメーション", - "menuAnimationsDesc": "メニューのフェードおよびスライドアニメーションを有効にする", - "disableBitmapCaching": "ビットマップキャッシュを無効化", - "disableBitmapCachingDesc": "ビットマップキャッシュをオフにする(不具合の解決に役立つ場合がある)", - "disableOffscreenCaching": "オフスクリーンキャッシュを無効化", - "disableOffscreenCachingDesc": "オフスクリーンキャッシュをオフにする", - "disableGlyphCaching": "グリフキャッシュを無効化", - "disableGlyphCachingDesc": "グリフキャッシュをオフにする", - "enableGfx": "GFXを有効化", - "enableGfxDesc": "RemoteFXグラフィックスパイプラインを使用する", - "enablePrinting": "印刷を有効化", - "enablePrintingDesc": "ローカルプリンターをリモートセッションにリダイレクトする", - "enableDriveRedirection": "ドライブリダイレクションを有効化", - "enableDriveRedirectionDesc": "リモートセッションでローカルフォルダーをドライブとしてマッピングする", - "createDrivePath": "ドライブパスを作成", - "createDrivePathDesc": "フォルダが存在しない場合に自動的に作成します", - "disableDownload": "ダウンロードを無効にする", - "disableDownloadDesc": "リモートセッションからファイルをダウンロードできないようにします", - "disableUpload": "アップロードを無効にする", - "disableUploadDesc": "リモートセッションにファイルをアップロードできないようにします", - "enableTouch": "タッチを有効にする", - "enableTouchDesc": "タッチ入力転送を有効にします", - "consoleSession": "コンソールセッション", - "consoleSessionDesc": "新しいセッションではなくコンソール(セッション0)に接続します", - "sendWolPacket": "WOLパケットを送信", - "sendWolPacketDesc": "接続前にこのホストを起動するためのマジックパケットを送信します", - "disableCopy": "コピーを無効にする", - "disableCopyDesc": "リモートセッションからのテキストコピーを禁止します", - "disablePaste": "貼り付けを無効にする", - "disablePasteDesc": "リモートセッションへのテキスト貼り付けを禁止します", - "createPathIfMissing": "パスが存在しない場合に作成", - "createPathIfMissingDesc": "レコーディングディレクトリが存在しない場合に自動的に作成します", - "excludeOutput": "出力を除外", - "excludeOutputDesc": "画面出力を記録しません(メタデータのみ)", - "excludeMouse": "マウスを除外", - "excludeMouseDesc": "マウスの動きを記録しません", - "includeKeystrokes": "キーストロークを含める", - "includeKeystrokesDesc": "画面出力に加えて生のキーストロークを記録します", - "vncPort": "VNCポート", - "vncPassword": "VNCパスワード", - "vncUsernameOptional": "ユーザー名(オプション)", - "vncLeaveBlank": "必要ない場合は空白のままにしてください", - "cursorMode": "カーソルモード", - "swapRedBlue": "赤/青を入れ替え", - "swapRedBlueDesc": "赤と青の色チャネルを入れ替えます(色の問題を修正)", - "readOnly": "読み取り専用", - "readOnlyDesc": "入力を送信せずにリモート画面を表示します", - "telnetPort": "Telnetポート", - "terminalType": "端末タイプ", - "fontName": "フォント名", - "fontSize": "フォントサイズ", - "colorScheme": "カラースキーム", - "backspaceKey": "バックスペースキー", - "colorDepth": "色深度", - "resizeMethod": "リサイズ方法", - "driveNamePlaceholder": "ターミックスドライブ", - "options": { - "any": "どれでも", - "auto": "自動", - "nlaExtended": "NLA延長", - "color8": "8ビット", - "color16": "16ビット", - "color24": "24ビット", - "color32": "32ビット", - "displayUpdate": "ディスプレイの更新", - "reconnect": "再接続", - "preserve": "保存する", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "ローカル", - "remote": "リモート", - "failsafe": "フェイルセーフ", - "blackOnWhite": "白地に黒", - "whiteOnBlack": "白地に黒", - "grayOnBlack": "黒地にグレー", - "greenOnBlack": "黒地に緑" - } - }, - "remoteDesktop": "リモートデスクトップ", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "仮想ネットワーク", - "unencryptedShell": "暗号化されていないシェル", - "remoteDesktopNotes": "メモ", - "userDefaultsNote": "変更しない設定は、ホストのデフォルト設定に従います。", - "connectionOriginNote": "デスクトップアプリでは、ホストが「このデバイス」に設定されていない限り、RDP、VNC、Telnetはリモートサーバーを使用します。「このデバイス」に設定するには、このマシンがアクセスできるguacdが必要です。リモートデスクトップの設定、またはguacdプロキシでホストごとに設定してください。", - "docsLink": "ドキュメントを表示", - "connectionOriginGuacamoleNote": "RDP、VNC、Telnetは、リモートサーバーを使用します。「このデバイス」を選択した場合は、このマシンからアクセスできるguacdが必要です。guacd URL設定で設定するか、guacdプロキシでホストごとに設定してください。" - }, - "onboarding": { - "feature_desktop": "リモートデスクトップ", - "feature_desktop_desc": "RDP、VNC、Telnetのセッションは、SSHと同様にタブで開きます。" - }, - "settings": { - "user": { - "colorDepth": { - "label": "色深度", - "description": "デフォルト設定のままになっているRDPホストで使用されます。" - }, - "resizeMethod": { - "label": "リサイズ方法", - "description": "リモート画面がウィンドウサイズに追従する仕組み。" - }, - "resizeDisplayUpdate": "ディスプレイの更新", - "resizeReconnect": "再接続", - "inherit": "継承する", - "on": "オン", - "off": "オフ", - "forceLossless": { - "label": "ロスレスを強制", - "description": "ロスレス画像エンコーディングを強制する(高品質だが帯域幅を多く消費)" - }, - "enableWallpaper": { - "label": "壁紙", - "description": "デスクトップ壁紙を表示する(無効にするとパフォーマンスが向上)" - }, - "enableFontSmoothing": { - "label": "フォントスムージング", - "description": "ClearTypeフォントレンダリングを有効にする" - }, - "enableDesktopComposition": { - "label": "デスクトップコンポジション", - "description": "Aero Glass効果を有効にする" - }, - "disableAudio": { - "label": "オーディオを無効化", - "description": "リモートセッションの音声をすべてミュートする" - }, - "enablePrinting": { - "label": "印刷を有効化", - "description": "ローカルプリンターをリモートセッションにリダイレクトする" - }, - "enableDrive": { - "label": "ドライブリダイレクションを有効化", - "description": "リモートセッションでローカルフォルダーをドライブとしてマッピングする" - }, - "disableCopy": { - "label": "コピーを無効にする", - "description": "リモートセッションからのテキストコピーを禁止します" - }, - "disablePaste": { - "label": "貼り付けを無効にする", - "description": "リモートセッションへのテキスト貼り付けを禁止します" - }, - "group": "リモートデスクトップのデフォルト設定", - "colorDepth16": "16ビット", - "colorDepth24": "24ビット", - "colorDepth32": "32ビット" - }, - "admin": { - "enabled": { - "label": "リモートデスクトップを有効にする", - "description": "guacd を介して RDP、VNC、および Telnet 接続を許可する。" - }, - "guacdUrl": { - "label": "guacd URL", - "description": "guacd がリッスンする場所をホスト:ポートとして指定します。GUACD_URL、GUACD_HOST、GUACD_PORT 環境変数によって、この設定は上書きされます。", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDPポート" - }, - "vncPort": { - "label": "VNCポート" - }, - "telnetPort": { - "label": "Telnetポート" - }, - "rdpSecurity": { - "label": "RDPセキュリティモード" - }, - "rdpIgnoreCert": { - "label": "証明書エラーを無視する" - }, - "guacamoleConfig": { - "label": "guacd 設定" - }, - "enableToolbar": { - "label": "リモートデスクトップツールバー", - "description": "RDP、VNC、Telnetセッションでツールバーを表示する。" - }, - "displayGroup": "画面", - "displayDefaultsTab": "リモートデスクトップ" - } - }, - "palette": { - "connectRdp": "RDPで接続する", - "connectVnc": "VNCで接続する", - "connectTelnet": "Telnetで接続する" - }, - "permissions": { - "sessions": { - "title": "リモートデスクトップセッションを共有する", - "description": "セッション共有とコラボレーションルームで、このユーザーのRDP、VNC、およびTelnetセッションを他のユーザーに表示できるようにする。" - } - }, - "errors": { - "remoteServerRequired": "リモートサーバーが必要です。この接続タイプを使用するには、設定でリモートサーバーを接続してください。" - }, - "plugin": { - "name": "リモートデスクトップ" - } -} diff --git a/plugins/remote-desktop/locales/translated/ko_KR.json b/plugins/remote-desktop/locales/translated/ko_KR.json deleted file mode 100644 index c2a6ecd72..000000000 --- a/plugins/remote-desktop/locales/translated/ko_KR.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "{{type}} 세션에 연결 중...", - "checkingGuacd": "원격 데스크톱 서비스를 확인하는 중...", - "requestingToken": "{{type}} 연결 토큰을 요청하는 중...", - "openingSession": "{{type}} 세션을 여는 중...", - "negotiating": "협상 중 {{type}} 악수...", - "sessionReady": "{{type}} 세션 준비 완료", - "connectionError": "연결 오류", - "connectionFailed": "연결 실패", - "failedToConnect": "연결 토큰을 가져오지 못했습니다.", - "hostNotFound": "호스트를 찾을 수 없음", - "noHostSelected": "선택된 호스트가 없습니다", - "reconnect": "재연결", - "retry": "재시도", - "guacdUnavailable": "원격 데스크톱 서비스(guacd)를 사용할 수 없습니다. guacd가 실행 중이고 접근 가능하며 관리자 설정에서 올바르게 구성되었는지 확인하세요.", - "credentialPromptTitle": "RDP 자격 증명을 입력하십시오.", - "credentialPromptDescription": "이 호스트는 연결 시 자격 증명을 입력하라는 메시지를 표시하도록 설정되어 있습니다. 입력된 자격 증명은 해당 세션 동안에만 사용되며 저장되지 않습니다.", - "connect": "연결", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "드라이브 파일", - "parent": "상위 디렉토리", - "refresh": "새로 고침", - "empty": "이 폴더는 비어 있습니다", - "listFailed": "이 폴더를 읽는 데 실패했습니다.", - "download": "다운로드", - "downloadFailed": "{{name}} 다운로드에 실패했습니다.", - "upload": "파일 업로드", - "uploading": "{{name}} 업로드 중...", - "uploaded": "업로드됨 {{name}}", - "uploadFailed": "{{name}} 업로드에 실패했습니다.", - "uploadDisabled": "이 연결에서는 업로드가 비활성화되어 있습니다.", - "driveUnavailable": "파일을 업로드하기 전에 RDP 드라이브 리디렉션을 활성화하십시오.", - "driveNotWritable": "guacd가 드라이브 폴더에 쓰기 권한이 없습니다. GUACD_DRIVE_PATH(또는 호스트의 드라이브 경로)와 해당 권한을 확인하십시오.", - "dropToUpload": "업로드할 파일을 드롭하세요" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (화면 잠금)", - "winKey": "Windows 키", - "superL": "Super+L (잠금 화면)", - "superKey": "슈퍼 키", - "super": "감독자", - "cmdL": "Cmd+L (화면 잠금)", - "cmdKey": "명령 키", - "cmd": "명령", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}} (고정됨 - 클릭하여 해제)", - "stickyInactive": "{{key}} (클릭하여 고정)", - "esc": "Esc", - "tab": "Tab", - "home": "홈", - "end": "End", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "위쪽 화살표", - "arrowDown": "아래쪽 화살표", - "arrowLeft": "왼쪽 화살표", - "arrowRight": "오른쪽 화살표", - "fnToggle": "기능 키", - "reconnect": "세션 재연결", - "collapse": "도구 모음 접기", - "expand": "도구 모음 펼치기", - "dragHandle": "드래그하여 위치 이동", - "hide": "이번 세션 동안에는 숨깁니다. 영구적으로 비활성화하려면 호스트 설정 → 일반 → 연결 도구 모음을 여세요.", - "zoomIn": "확대", - "zoomOut": "축소하기", - "resetZoom": "확대/축소 초기화", - "fitToScreen": "화면에 맞추기", - "switchToTrackpad": "트랙패드 모드로 전환하세요 (드래그하여 커서 이동, 탭하여 클릭).", - "switchToTouch": "터치 모드로 전환하세요 (클릭하려는 위치를 직접 탭하세요)." - }, - "disabled": "원격 데스크톱 기능이 꺼져 있습니다. 관리자는 설정에서 이 기능을 켤 수 있습니다." - }, - "hosts": { - "nativeRdpOpened": "Windows 원격 데스크톱에서 열림", - "nativeRdpFailed": "Windows 원격 데스크톱을 열지 못했습니다.", - "openNativeRdp": "Windows 원격 데스크톱에서 열기", - "guac": { - "toolbar": "툴바", - "connection": "연결", - "authentication": "인증", - "storedCredential": "저장된 자격 증명", - "authMethod": "인증 방법", - "authTypeDirect": "직접", - "authTypeCredential": "자격 증명", - "authTypeNone": "전체 해제", - "authTypeNoneDesc": "사용자 이름은 저장되지 않습니다. 접속할 때마다 사용자 이름과 비밀번호를 입력하라는 메시지가 표시되며, 해당 정보는 저장되지 않습니다.", - "selectCredential": "자격 증명 선택...", - "connectionSettings": "연결 설정", - "displaySettings": "화면 설정", - "audioSettings": "오디오 설정", - "rdpPerformance": "RDP 성능", - "deviceRedirection": "장치 리디렉션", - "session": "세션", - "gateway": "게이트웨이", - "remoteApp": "RemoteApp", - "clipboard": "클립보드", - "sessionRecording": "세션 녹화", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC 설정", - "terminalSettings": "터미널 설정", - "rdpPort": "RDP 포트", - "username": "사용자 이름", - "password": "비밀번호", - "passwordSaved": "비밀번호가 저장되었습니다. 변경하려면 입력하세요.", - "domain": "도메인", - "securityMode": "보안 모드", - "width": "너비", - "height": "높이", - "dpi": "DPI", - "clientName": "클라이언트 이름", - "initialProgram": "초기 프로그램", - "serverLayout": "서버 레이아웃", - "timezone": "시간대", - "loadBalanceInfo": "로드 밸런스 정보 / 쿠키", - "loadBalanceInfoDesc": "RDS 팜 로드 밸런싱을 위한 RD 연결 브로커 쿠키 (예: tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd 프록시", - "guacdHostname": "guacd 호스트", - "guacdHostnamePlaceholder": "전역 기본값", - "guacdPort": "guacd 포트", - "guacdProxyDesc": "이 연결에 대해 전역 guacd 인스턴스를 재정의합니다. 공란으로 두면 전역으로 구성된 guacd를 사용합니다.", - "gatewayHostname": "게이트웨이 호스트 이름", - "gatewayPort": "게이트웨이 포트", - "gatewayUsername": "게이트웨이 사용자 이름", - "gatewayPassword": "게이트웨이 비밀번호", - "gatewayDomain": "게이트웨이 도메인", - "remoteAppProgram": "RemoteApp 프로그램", - "workingDirectory": "작업 디렉터리", - "arguments": "인수", - "normalizeLineEndings": "줄 끝 정규화", - "macAddress": "MAC 주소", - "broadcastAddress": "브로드캐스트 주소", - "udpPort": "UDP 포트", - "waitTimeS": "대기 시간(초)", - "driveName": "드라이브 이름", - "drivePath": "드라이브 경로", - "ignoreCertificate": "인증서 무시", - "ignoreCertificateDesc": "자체 서명 인증서가 있는 호스트에 연결 허용", - "forceLossless": "무손실 강제", - "forceLosslessDesc": "무손실 이미지 인코딩 강제 (더 높은 품질, 더 많은 대역폭)", - "disableAudio": "오디오 비활성화", - "disableAudioDesc": "원격 세션의 모든 오디오 음소거", - "enableAudioInput": "오디오 입력 활성화 (마이크)", - "enableAudioInputDesc": "로컬 마이크를 원격 세션으로 전달", - "wallpaper": "배경 화면", - "wallpaperDesc": "바탕 화면 배경 표시 (비활성화 시 성능 향상)", - "theming": "테마", - "themingDesc": "비주얼 테마 및 스타일 활성화", - "fontSmoothing": "글꼴 다듬기", - "fontSmoothingDesc": "ClearType 글꼴 렌더링 사용", - "fullWindowDrag": "전체 창 끌기", - "fullWindowDragDesc": "드래그하는 동안 창 내용 표시", - "desktopComposition": "데스크톱 구성", - "desktopCompositionDesc": "Aero 글래스 효과 활성화", - "menuAnimations": "메뉴 애니메이션", - "menuAnimationsDesc": "메뉴 페이드 및 슬라이드 애니메이션 활성화", - "disableBitmapCaching": "비트맵 캐싱 비활성화", - "disableBitmapCachingDesc": "비트맵 캐시 끄기 (오류 해결에 도움이 될 수 있음)", - "disableOffscreenCaching": "오프스크린 캐싱 비활성화", - "disableOffscreenCachingDesc": "오프스크린 캐시 끄기", - "disableGlyphCaching": "글리프 캐싱 비활성화", - "disableGlyphCachingDesc": "글리프 캐시 끄기", - "enableGfx": "GFX 활성화", - "enableGfxDesc": "RemoteFX 그래픽 파이프라인 사용", - "enablePrinting": "인쇄 활성화", - "enablePrintingDesc": "로컬 프린터를 원격 세션으로 리디렉션", - "enableDriveRedirection": "드라이브 리디렉션 활성화", - "enableDriveRedirectionDesc": "로컬 폴더를 원격 세션에서 드라이브로 매핑", - "createDrivePath": "드라이브 경로 생성", - "createDrivePathDesc": "폴더가 없으면 자동 생성", - "disableDownload": "다운로드 비활성화", - "disableDownloadDesc": "원격 세션에서 파일 다운로드 방지", - "disableUpload": "업로드 비활성화", - "disableUploadDesc": "원격 세션으로 파일 업로드 방지", - "enableTouch": "터치 활성화", - "enableTouchDesc": "터치 입력 전달 활성화", - "consoleSession": "콘솔 세션", - "consoleSessionDesc": "새 세션 대신 콘솔(세션 0)에 연결", - "sendWolPacket": "WOL 패킷 전송", - "sendWolPacketDesc": "연결 전에 호스트를 깨우기 위해 매직 패킷 전송", - "disableCopy": "복사 비활성화", - "disableCopyDesc": "원격 세션에서 텍스트 복사 방지", - "disablePaste": "붙여넣기 비활성화", - "disablePasteDesc": "원격 세션으로 텍스트 붙여넣기 방지", - "createPathIfMissing": "경로가 없으면 생성", - "createPathIfMissingDesc": "녹화 디렉터리 자동 생성", - "excludeOutput": "출력 제외", - "excludeOutputDesc": "화면 출력 기록 안 함(메타데이터만)", - "excludeMouse": "마우스 제외", - "excludeMouseDesc": "마우스 움직임 기록 안 함", - "includeKeystrokes": "키 입력 포함", - "includeKeystrokesDesc": "화면 출력 외에 원시 키 입력 기록", - "vncPort": "VNC 포트", - "vncPassword": "VNC 비밀번호", - "vncUsernameOptional": "사용자 이름(선택 사항)", - "vncLeaveBlank": "필요하지 않으면 비워 두세요", - "cursorMode": "커서 모드", - "swapRedBlue": "적색/청색 교환", - "swapRedBlueDesc": "적색과 청색 채널을 교환(일부 색상 문제 해결)", - "readOnly": "읽기 전용", - "readOnlyDesc": "입력을 보내지 않고 원격 화면 보기", - "telnetPort": "Telnet 포트", - "terminalType": "터미널 유형", - "fontName": "글꼴 이름", - "fontSize": "글꼴 크기", - "colorScheme": "색 구성표", - "backspaceKey": "백스페이스 키", - "colorDepth": "색 농도", - "resizeMethod": "크기 조정 방법", - "driveNamePlaceholder": "터믹스 드라이브", - "options": { - "any": "어느", - "auto": "자동", - "nlaExtended": "NLA 확장", - "color8": "8비트", - "color16": "16비트", - "color24": "24비트", - "color32": "32비트", - "displayUpdate": "디스플레이 업데이트", - "reconnect": "재연결", - "preserve": "보존하다", - "unix": "유닉스(LF)", - "windows": "윈도우(CRLF)", - "local": "로컬", - "remote": "원격", - "failsafe": "안전장치", - "blackOnWhite": "흰색 바탕에 검은색", - "whiteOnBlack": "검정색 바탕에 흰색", - "grayOnBlack": "검정색 바탕에 회색", - "greenOnBlack": "검정색 바탕에 녹색" - } - }, - "remoteDesktop": "원격 데스크톱", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "텔넷", - "virtualNetwork": "가상 네트워크", - "unencryptedShell": "암호화되지 않은 셸", - "remoteDesktopNotes": "메모", - "userDefaultsNote": "설정을 변경하지 않고 그대로 두면 호스트의 기본 설정이 적용됩니다.", - "connectionOriginNote": "데스크톱 앱에서 RDP, VNC 및 Telnet은 호스트가 \"이 장치\"로 설정되어 있지 않은 한 원격 서버를 사용합니다. \"이 장치\"로 설정된 경우 해당 장치가 연결할 수 있는 guacd 프록시가 필요합니다. 원격 데스크톱 설정 또는 호스트별 guacd 프록시 설정에서 guacd 프록시를 설정하십시오.", - "docsLink": "문서 보기", - "connectionOriginGuacamoleNote": "RDP, VNC, Telnet은 \"이 장치\"를 선택하지 않는 한 원격 서버를 사용합니다. \"이 장치\"를 선택하려면 해당 장치가 연결할 수 있는 guacd가 필요합니다. guacd URL 설정에서 또는 guacd 프록시 설정에서 호스트별로 guacd를 설정하십시오." - }, - "onboarding": { - "feature_desktop": "원격 데스크톱", - "feature_desktop_desc": "SSH와 마찬가지로 RDP, VNC 및 Telnet 세션을 탭에서 열 수 있습니다." - }, - "settings": { - "user": { - "colorDepth": { - "label": "색 농도", - "description": "기본 설정으로 유지되는 RDP 호스트에 사용됩니다." - }, - "resizeMethod": { - "label": "크기 조정 방법", - "description": "원격 화면이 창 크기에 따라 어떻게 변하는지." - }, - "resizeDisplayUpdate": "디스플레이 업데이트", - "resizeReconnect": "재연결", - "inherit": "상속하다", - "on": "켜짐", - "off": "꺼짐", - "forceLossless": { - "label": "무손실 강제", - "description": "무손실 이미지 인코딩 강제 (더 높은 품질, 더 많은 대역폭)" - }, - "enableWallpaper": { - "label": "배경 화면", - "description": "바탕 화면 배경 표시 (비활성화 시 성능 향상)" - }, - "enableFontSmoothing": { - "label": "글꼴 다듬기", - "description": "ClearType 글꼴 렌더링 사용" - }, - "enableDesktopComposition": { - "label": "데스크톱 구성", - "description": "Aero 글래스 효과 활성화" - }, - "disableAudio": { - "label": "오디오 비활성화", - "description": "원격 세션의 모든 오디오 음소거" - }, - "enablePrinting": { - "label": "인쇄 활성화", - "description": "로컬 프린터를 원격 세션으로 리디렉션" - }, - "enableDrive": { - "label": "드라이브 리디렉션 활성화", - "description": "로컬 폴더를 원격 세션에서 드라이브로 매핑" - }, - "disableCopy": { - "label": "복사 비활성화", - "description": "원격 세션에서 텍스트 복사 방지" - }, - "disablePaste": { - "label": "붙여넣기 비활성화", - "description": "원격 세션으로 텍스트 붙여넣기 방지" - }, - "group": "원격 데스크톱 기본 설정", - "colorDepth16": "16비트", - "colorDepth24": "24비트", - "colorDepth32": "32비트" - }, - "admin": { - "enabled": { - "label": "원격 데스크톱 활성화", - "description": "guacd를 통해 RDP, VNC 및 Telnet 연결을 허용합니다." - }, - "guacdUrl": { - "label": "guacd URL", - "description": "guacd가 수신 대기하는 주소는 호스트:포트 형식입니다. GUACD_URL, GUACD_HOST 및 GUACD_PORT 환경 변수가 이 값을 재정의합니다.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP 포트" - }, - "vncPort": { - "label": "VNC 포트" - }, - "telnetPort": { - "label": "텔넷 포트" - }, - "rdpSecurity": { - "label": "RDP 보안 모드" - }, - "rdpIgnoreCert": { - "label": "인증서 오류를 무시하세요" - }, - "guacamoleConfig": { - "label": "guacd 설정" - }, - "enableToolbar": { - "label": "원격 데스크톱 툴바", - "description": "RDP, VNC 및 Telnet 세션에서 툴바를 표시합니다." - }, - "displayGroup": "표시하다", - "displayDefaultsTab": "원격 데스크톱" - } - }, - "palette": { - "connectRdp": "RDP에 연결", - "connectVnc": "VNC로 연결", - "connectTelnet": "텔넷으로 연결" - }, - "permissions": { - "sessions": { - "title": "원격 데스크톱 세션을 공유하세요", - "description": "세션 공유 및 공동 작업 공간에서 이 사용자의 RDP, VNC 및 Telnet 세션을 다른 사용자에게 표시할 수 있도록 허용합니다." - } - }, - "errors": { - "remoteServerRequired": "원격 서버가 필요합니다. 이 연결 유형을 사용하려면 설정에서 원격 서버를 연결하세요." - }, - "plugin": { - "name": "원격 데스크톱" - } -} diff --git a/plugins/remote-desktop/locales/translated/nl_NL.json b/plugins/remote-desktop/locales/translated/nl_NL.json deleted file mode 100644 index 91e0dd158..000000000 --- a/plugins/remote-desktop/locales/translated/nl_NL.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Verbinding maken met de {{type}} sessie...", - "checkingGuacd": "De externe bureaubladservice controleren...", - "requestingToken": "Verzoek om {{type}} verbindingstoken...", - "openingSession": "Sessie {{type}} openen...", - "negotiating": "Onderhandelen over een {{type}} handdruk...", - "sessionReady": "{{type}} sessie gereed", - "connectionError": "Verbindingsfout", - "connectionFailed": "Verbinding mislukt", - "failedToConnect": "Verbindingstoken kon niet worden verkregen.", - "hostNotFound": "Host niet gevonden", - "noHostSelected": "Geen host geselecteerd", - "reconnect": "Opnieuw verbinding maken", - "retry": "Opnieuw proberen", - "guacdUnavailable": "De service voor extern bureaublad (guacd) is niet beschikbaar. Zorg ervoor dat guacd actief en toegankelijk is en correct is geconfigureerd in de beheerdersinstellingen.", - "credentialPromptTitle": "Voer de RDP-inloggegevens in.", - "credentialPromptDescription": "Deze host is zo ingesteld dat er bij het verbinden om inloggegevens wordt gevraagd. Deze worden alleen voor deze sessie gebruikt en niet opgeslagen.", - "connect": "Verbinden", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Drive-bestanden", - "parent": "Hoofdmap", - "refresh": "Vernieuwen", - "empty": "Deze map is leeg.", - "listFailed": "Het lezen van deze map is mislukt.", - "download": "Download", - "downloadFailed": "Downloaden mislukt {{name}}", - "upload": "Bestanden uploaden", - "uploading": "Bezig met uploaden {{name}}...", - "uploaded": "Geüpload {{name}}", - "uploadFailed": "Uploaden mislukt {{name}}", - "uploadDisabled": "Uploaden is uitgeschakeld voor deze verbinding.", - "driveUnavailable": "Schakel RDP-schijfomleiding in voordat u bestanden uploadt.", - "driveNotWritable": "guacd kan niet naar de schijfmap schrijven. Controleer GUACD_DRIVE_PATH (of het schijfpad van de host) en de bijbehorende machtigingen.", - "dropToUpload": "Sleep de bestanden hierheen om ze te uploaden." - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Vergrendelscherm)", - "winKey": "Windows-toets", - "superL": "Super+L (Vergrendelscherm)", - "superKey": "Supersleutel", - "super": "Super", - "cmdL": "Cmd+L (Scherm vergrendelen)", - "cmdKey": "Commando-toets", - "cmd": "Commando", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Verschuiving", - "win": "Winnen", - "stickyActive": "{{key}} (vergrendeld - klik om te ontgrendelen)", - "stickyInactive": "{{key}} (klik om te vergrendelen)", - "esc": "Ontsnappen", - "tab": "Tab", - "home": "Thuis", - "end": "Einde", - "pageUp": "Pagina omhoog", - "pageDown": "Pagina omlaag", - "arrowUp": "Pijl omhoog", - "arrowDown": "Pijl omlaag", - "arrowLeft": "Pijl naar links", - "arrowRight": "Pijl naar rechts", - "fnToggle": "Functietoetsen", - "reconnect": "Herverbindingssessie", - "collapse": "Werkbalk inklappen", - "expand": "Werkbalk uitklappen", - "dragHandle": "Sleep om te verplaatsen", - "hide": "Voor deze sessie verbergen. Om het permanent uit te schakelen, opent u Hostinstellingen → Algemeen → Verbindingswerkbalk.", - "zoomIn": "Zoom in", - "zoomOut": "Uitzoomen", - "resetZoom": "Zoom opnieuw instellen", - "fitToScreen": "Aan het scherm aanpassen", - "switchToTrackpad": "Schakel over naar de trackpadmodus (sleep om de cursor te verplaatsen, tik om te klikken).", - "switchToTouch": "Schakel over naar de aanraakmodus (tik direct op de plek waar u wilt klikken)." - }, - "disabled": "Extern bureaublad is uitgeschakeld. Een beheerder kan dit inschakelen in de instellingen." - }, - "hosts": { - "nativeRdpOpened": "Geopend in Windows Remote Desktop", - "nativeRdpFailed": "Het openen van Windows Remote Desktop is mislukt.", - "openNativeRdp": "Openen in Windows Extern bureaublad", - "guac": { - "toolbar": "Werkbalk", - "connection": "Verbinding", - "authentication": "Authenticatie", - "storedCredential": "Opgeslagen inloggegevens", - "authMethod": "Authenticatiemethode", - "authTypeDirect": "Direct", - "authTypeCredential": "Referentie", - "authTypeNone": "Geen", - "authTypeNoneDesc": "Er worden geen inloggegevens opgeslagen. U wordt bij elke verbinding gevraagd om een gebruikersnaam en wachtwoord; deze worden niet bewaard.", - "selectCredential": "Selecteer een certificaat...", - "connectionSettings": "Verbindingsinstellingen", - "displaySettings": "Weergave-instellingen", - "audioSettings": "Audio-instellingen", - "rdpPerformance": "RDP-prestaties", - "deviceRedirection": "Apparaatomleiding", - "session": "Sessie", - "gateway": "Gateway", - "remoteApp": "RemoteApp", - "clipboard": "Klembord", - "sessionRecording": "Sessieopname", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC-instellingen", - "terminalSettings": "Terminalinstellingen", - "rdpPort": "RDP-poort", - "username": "Gebruikersnaam", - "password": "Wachtwoord", - "passwordSaved": "Wachtwoord opgeslagen, typ om te wijzigen", - "domain": "Domein", - "securityMode": "Beveiligingsmodus", - "width": "Breedte", - "height": "Hoogte", - "dpi": "DPI", - "clientName": "Klantnaam", - "initialProgram": "Initiële programma", - "serverLayout": "Serverindeling", - "timezone": "Tijdzone", - "loadBalanceInfo": "Informatie over load balancing / Cookie", - "loadBalanceInfoDesc": "RD Connection Broker-cookie voor load balancing van RDS-farms (bijv. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd-proxy", - "guacdHostname": "guacd Host", - "guacdHostnamePlaceholder": "Globale standaard", - "guacdPort": "bewakingshaven", - "guacdProxyDesc": "Overschrijf de globale guacd-instantie voor deze verbinding. Laat dit veld leeg om de globaal geconfigureerde guacd te gebruiken.", - "gatewayHostname": "Gateway-hostnaam", - "gatewayPort": "Gateway-poort", - "gatewayUsername": "Gateway-gebruikersnaam", - "gatewayPassword": "Gateway-wachtwoord", - "gatewayDomain": "Gateway-domein", - "remoteAppProgram": "RemoteApp-programma", - "workingDirectory": "Werkmap", - "arguments": "Argumenten", - "normalizeLineEndings": "Normaliseer regeleinden", - "macAddress": "MAC-adres", - "broadcastAddress": "Uitzendadres", - "udpPort": "UDP-poort", - "waitTimeS": "Wachttijd (s)", - "driveName": "Schijfnaam", - "drivePath": "Rijpad", - "ignoreCertificate": "Certificaat negeren", - "ignoreCertificateDesc": "Sta verbindingen toe met hosts die zelfondertekende certificaten hebben.", - "forceLossless": "Krachtverliesloos", - "forceLosslessDesc": "Forceer verliesvrije beeldcodering (hogere kwaliteit, meer bandbreedte)", - "disableAudio": "Geluid uitschakelen", - "disableAudioDesc": "Demp alle audio van de externe sessie.", - "enableAudioInput": "Audio-ingang (microfoon) inschakelen", - "enableAudioInputDesc": "Stuur de lokale microfoon door naar de externe sessie.", - "wallpaper": "Behang", - "wallpaperDesc": "Toon bureaubladachtergrond (uitschakelen verbetert de prestaties)", - "theming": "Thema", - "themingDesc": "Schakel visuele thema's en stijlen in.", - "fontSmoothing": "Lettertype-vloeiendheid", - "fontSmoothingDesc": "Schakel ClearType-lettertypeweergave in", - "fullWindowDrag": "Volledig venster slepen", - "fullWindowDragDesc": "Toon de inhoud van het venster tijdens het slepen.", - "desktopComposition": "Bureaubladcompositie", - "desktopCompositionDesc": "Schakel Aero-glaseffecten in", - "menuAnimations": "Menu-animaties", - "menuAnimationsDesc": "Schakel menu-vervagings- en schuifanimaties in.", - "disableBitmapCaching": "Bitmapcaching uitschakelen", - "disableBitmapCachingDesc": "Schakel de bitmapcache uit (dit kan helpen bij storingen).", - "disableOffscreenCaching": "Offscreen-caching uitschakelen", - "disableOffscreenCachingDesc": "Schakel de offscreen cache uit.", - "disableGlyphCaching": "Glyph-caching uitschakelen", - "disableGlyphCachingDesc": "Schakel de glyph-cache uit.", - "enableGfx": "Schakel GFX in", - "enableGfxDesc": "Gebruik de RemoteFX-grafische pipeline", - "enablePrinting": "Afdrukken inschakelen", - "enablePrintingDesc": "Lokale printers doorverwijzen naar de externe sessie", - "enableDriveRedirection": "Schakel schijfomleiding in", - "enableDriveRedirectionDesc": "Een lokale map toewijzen als schijfstation in de externe sessie.", - "createDrivePath": "Maak een rijpad aan", - "createDrivePathDesc": "Maak de map automatisch aan als deze nog niet bestaat.", - "disableDownload": "Download uitschakelen", - "disableDownloadDesc": "Voorkom het downloaden van bestanden vanuit de externe sessie.", - "disableUpload": "Uploaden uitschakelen", - "disableUploadDesc": "Voorkom het uploaden van bestanden naar de externe sessie.", - "enableTouch": "Aanraakbediening inschakelen", - "enableTouchDesc": "Schakel het doorsturen van aanraakinvoer in.", - "consoleSession": "Consolesessie", - "consoleSessionDesc": "Maak verbinding met de console (sessie 0) in plaats van een nieuwe sessie te starten.", - "sendWolPacket": "Verzend WOL-pakket", - "sendWolPacketDesc": "Verzend een magic packet om deze host te activeren voordat u verbinding maakt.", - "disableCopy": "Kopiëren uitschakelen", - "disableCopyDesc": "Voorkom het kopiëren van tekst vanuit de externe sessie.", - "disablePaste": "Plakken uitschakelen", - "disablePasteDesc": "Voorkom dat tekst in de externe sessie wordt geplakt.", - "createPathIfMissing": "Maak een pad aan als dit ontbreekt.", - "createPathIfMissingDesc": "Maak automatisch de opnamemap aan.", - "excludeOutput": "Uitvoer uitsluiten", - "excludeOutputDesc": "Schermuitvoer niet opnemen (alleen metadata).", - "excludeMouse": "Muis uitsluiten", - "excludeMouseDesc": "Neem geen muisbewegingen op.", - "includeKeystrokes": "Inclusief toetsaanslagen", - "includeKeystrokesDesc": "Registreer naast de schermuitvoer ook onbewerkte toetsaanslagen.", - "vncPort": "VNC-poort", - "vncPassword": "VNC-wachtwoord", - "vncUsernameOptional": "Gebruikersnaam (optioneel)", - "vncLeaveBlank": "Laat dit veld leeg als het niet verplicht is.", - "cursorMode": "Cursormodus", - "swapRedBlue": "Wissel rood/blauw om.", - "swapRedBlueDesc": "Wissel de rode en blauwe kleurkanalen om (dit lost enkele kleurproblemen op).", - "readOnly": "Alleen-lezen", - "readOnlyDesc": "Bekijk het externe scherm zonder invoer te verzenden.", - "telnetPort": "Telnet-poort", - "terminalType": "Terminaltype", - "fontName": "Lettertypenaam", - "fontSize": "Lettergrootte", - "colorScheme": "Kleurenschema", - "backspaceKey": "Backspace-toets", - "colorDepth": "Kleurdiepte", - "resizeMethod": "Formaatwijzigingsmethode", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Elk", - "auto": "Auto", - "nlaExtended": "NLA Extended", - "color8": "8-bit", - "color16": "16-bit", - "color24": "24-bit", - "color32": "32-bit", - "displayUpdate": "Weergave bijwerken", - "reconnect": "Opnieuw verbinding maken", - "preserve": "Beschermen", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Lokaal", - "remote": "Op afstand", - "failsafe": "faalveilige", - "blackOnWhite": "Zwart op wit", - "whiteOnBlack": "Wit op zwart", - "grayOnBlack": "Grijs op zwart", - "greenOnBlack": "Groen op zwart" - } - }, - "remoteDesktop": "Extern bureaublad", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Virtueel netwerk", - "unencryptedShell": "Niet-versleutelde shell", - "remoteDesktopNotes": "Notities", - "userDefaultsNote": "Instellingen die u ongewijzigd laat, volgen de standaardinstellingen van uw host.", - "connectionOriginNote": "In de desktop-app gebruiken RDP, VNC en Telnet de externe server, tenzij de host is ingesteld op 'Dit apparaat'. In dat geval is een guacd-proxy nodig die deze machine kan bereiken. Stel er een in via de instellingen voor Extern bureaublad of per host onder guacd-proxy.", - "docsLink": "Documenten bekijken", - "connectionOriginGuacamoleNote": "RDP, VNC en Telnet gebruiken de externe server, tenzij u 'Dit apparaat' selecteert. In dat geval is een guacd-server nodig die deze machine kan bereiken. Stel er een in bij de guacd-URL-instellingen of per host onder guacd-proxy." - }, - "onboarding": { - "feature_desktop": "Extern bureaublad", - "feature_desktop_desc": "Open RDP-, VNC- en Telnet-sessies in een tabblad, net als bij SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Kleurdiepte", - "description": "Gebruikt voor RDP-hosts die dit standaard ingeschakeld laten." - }, - "resizeMethod": { - "label": "Formaatwijzigingsmethode", - "description": "Hoe het scherm op afstand de venstergrootte aanpast." - }, - "resizeDisplayUpdate": "Weergave bijwerken", - "resizeReconnect": "Opnieuw verbinding maken", - "inherit": "Erven", - "on": "Op", - "off": "Uit", - "forceLossless": { - "label": "Krachtverliesloos", - "description": "Forceer verliesvrije beeldcodering (hogere kwaliteit, meer bandbreedte)" - }, - "enableWallpaper": { - "label": "Behang", - "description": "Toon bureaubladachtergrond (uitschakelen verbetert de prestaties)" - }, - "enableFontSmoothing": { - "label": "Lettertype-vloeiendheid", - "description": "Schakel ClearType-lettertypeweergave in" - }, - "enableDesktopComposition": { - "label": "Bureaubladcompositie", - "description": "Schakel Aero-glaseffecten in" - }, - "disableAudio": { - "label": "Geluid uitschakelen", - "description": "Demp alle audio van de externe sessie." - }, - "enablePrinting": { - "label": "Afdrukken inschakelen", - "description": "Lokale printers doorverwijzen naar de externe sessie" - }, - "enableDrive": { - "label": "Schakel schijfomleiding in", - "description": "Een lokale map toewijzen als schijfstation in de externe sessie." - }, - "disableCopy": { - "label": "Kopiëren uitschakelen", - "description": "Voorkom het kopiëren van tekst vanuit de externe sessie." - }, - "disablePaste": { - "label": "Plakken uitschakelen", - "description": "Voorkom dat tekst in de externe sessie wordt geplakt." - }, - "group": "Standaardinstellingen voor Remote Desktop", - "colorDepth16": "16-bit", - "colorDepth24": "24-bit", - "colorDepth32": "32-bit" - }, - "admin": { - "enabled": { - "label": "Schakel Extern bureaublad in.", - "description": "Sta RDP-, VNC- en Telnet-verbindingen toe via guacd." - }, - "guacdUrl": { - "label": "guacd URL", - "description": "Waar guacd luistert, als host:poort. De omgevingsvariabelen GUACD_URL, GUACD_HOST en GUACD_PORT overschrijven dit.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP-poort" - }, - "vncPort": { - "label": "VNC-poort" - }, - "telnetPort": { - "label": "Telnet-poort" - }, - "rdpSecurity": { - "label": "RDP-beveiligingsmodus" - }, - "rdpIgnoreCert": { - "label": "Certificaatfouten negeren" - }, - "guacamoleConfig": { - "label": "GUACD-instellingen" - }, - "enableToolbar": { - "label": "Werkbalk voor extern bureaublad", - "description": "Toon de werkbalk in RDP-, VNC- en Telnet-sessies." - }, - "displayGroup": "Weergave", - "displayDefaultsTab": "Extern bureaublad" - } - }, - "palette": { - "connectRdp": "Verbinden via RDP", - "connectVnc": "Verbinden met VNC", - "connectTelnet": "Verbinden via Telnet" - }, - "permissions": { - "sessions": { - "title": "Externe bureaubladsessies delen", - "description": "Zorg ervoor dat sessiedeling en samenwerkingsruimtes de RDP-, VNC- en Telnet-sessies van deze gebruiker aan anderen kunnen tonen." - } - }, - "errors": { - "remoteServerRequired": "Externe server vereist. Verbind een externe server in de instellingen om dit verbindingstype te gebruiken." - }, - "plugin": { - "name": "Extern bureaublad" - } -} diff --git a/plugins/remote-desktop/locales/translated/no_NO.json b/plugins/remote-desktop/locales/translated/no_NO.json deleted file mode 100644 index ff3256314..000000000 --- a/plugins/remote-desktop/locales/translated/no_NO.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Kobler til {{type}} økt...", - "checkingGuacd": "Sjekker tjenesten for eksternt skrivebord...", - "requestingToken": "Ber om {{type}} tilkoblingstoken...", - "openingSession": "Åpner {{type}} økt...", - "negotiating": "Forhandler {{type}} håndtrykk...", - "sessionReady": "{{type}} økt klar", - "connectionError": "Tilkoblingsfeil", - "connectionFailed": "Tilkobling mislyktes", - "failedToConnect": "Kunne ikke hente tilkoblingstoken", - "hostNotFound": "Verten ble ikke funnet", - "noHostSelected": "Ingen vert valgt", - "reconnect": "Koble til på nytt", - "retry": "Prøv igjen", - "guacdUnavailable": "Ekstern skrivebordstjeneste (guacd) er ikke tilgjengelig. Sørg for at guacd kjører, er tilgjengelig og riktig konfigurert i administratorinnstillingene.", - "credentialPromptTitle": "Skriv inn RDP-legitimasjon", - "credentialPromptDescription": "Denne verten er satt til å be om påloggingsinformasjon ved tilkobling. Disse brukes kun for denne økten og lagres ikke.", - "connect": "Koble til", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Disk-filer", - "parent": "Foreldrekatalog", - "refresh": "Oppdater", - "empty": "Denne mappen er tom", - "listFailed": "Kunne ikke lese denne mappen", - "download": "Last ned", - "downloadFailed": "Kunne ikke laste ned {{name}}", - "upload": "Last opp filer", - "uploading": "Laster opp {{name}}...", - "uploaded": "Lastet opp {{name}}", - "uploadFailed": "Kunne ikke laste opp {{name}}", - "uploadDisabled": "Opplastinger er deaktivert for denne tilkoblingen", - "driveUnavailable": "Aktiver omdirigering av RDP-stasjoner før du laster opp filer", - "driveNotWritable": "guacd kan ikke skrive til stasjonsmappen. Sjekk GUACD_DRIVE_PATH (eller vertens stasjonsbane) og dens tillatelser.", - "dropToUpload": "Slipp filer for å laste opp" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Låseskjerm)", - "winKey": "Windows-nøkkel", - "superL": "Super+L (Låseskjerm)", - "superKey": "Supernøkkel", - "super": "Super", - "cmdL": "Cmd+L (Låseskjerm)", - "cmdKey": "Kommandotast", - "cmd": "Kommando", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Skifte", - "win": "Vinne", - "stickyActive": "{{key}} (låst – klikk for å slippe)", - "stickyInactive": "{{key}} (klikk for å låse)", - "esc": "Flykte", - "tab": "Fane", - "home": "Hjem", - "end": "Slutt", - "pageUp": "Side opp", - "pageDown": "Side ned", - "arrowUp": "Pil opp", - "arrowDown": "Pil ned", - "arrowLeft": "Pil venstre", - "arrowRight": "Pil høyre", - "fnToggle": "Funksjonstaster", - "reconnect": "Koble til økten på nytt", - "collapse": "Skjul verktøylinjen", - "expand": "Utvid verktøylinjen", - "dragHandle": "Dra for å flytte", - "hide": "Skjul for denne økten. For å deaktivere den permanent, åpne Vertsinnstillinger → Generelt → Tilkoblingsverktøylinje.", - "zoomIn": "Zoom inn", - "zoomOut": "Zoom ut", - "resetZoom": "Tilbakestill zoom", - "fitToScreen": "Tilpass til skjerm", - "switchToTrackpad": "Bytt til styreflatemodus (dra for å flytte markøren, trykk for å klikke)", - "switchToTouch": "Bytt til berøringsmodus (trykk direkte der du vil klikke)" - }, - "disabled": "Eksternt skrivebord er slått av. En administrator kan slå det på i Innstillinger." - }, - "hosts": { - "nativeRdpOpened": "Åpnet i Windows Eksternt skrivebord", - "nativeRdpFailed": "Kunne ikke åpne Windows Eksternt skrivebord", - "openNativeRdp": "Åpne i Windows Eksternt skrivebord", - "guac": { - "toolbar": "Verktøylinjen", - "connection": "Tilkobling", - "authentication": "Autentisering", - "storedCredential": "Lagret legitimasjon", - "authMethod": "Autorisasjonsmetode", - "authTypeDirect": "Direkte", - "authTypeCredential": "Legitimasjon", - "authTypeNone": "Ingen", - "authTypeNoneDesc": "Ingen påloggingsinformasjon lagres. Du blir bedt om å oppgi brukernavn og passord hver gang du kobler til; de lagres ikke.", - "selectCredential": "Velg en legitimasjon...", - "connectionSettings": "Tilkoblingsinnstillinger", - "displaySettings": "Skjerminnstillinger", - "audioSettings": "Lydinnstillinger", - "rdpPerformance": "RDP-ytelse", - "deviceRedirection": "Enhetsomdirigering", - "session": "Økt", - "gateway": "Inngangsport", - "remoteApp": "Fjernapp", - "clipboard": "Utklippstavlen", - "sessionRecording": "Øktopptak", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC-innstillinger", - "terminalSettings": "Terminalinnstillinger", - "rdpPort": "RDP-port", - "username": "Brukernavn", - "password": "Passord", - "passwordSaved": "Passord lagret, skriv inn for å endre", - "domain": "Domene", - "securityMode": "Sikkerhetsmodus", - "width": "Bredde", - "height": "Høyde", - "dpi": "DPI", - "clientName": "Klientnavn", - "initialProgram": "Innledende program", - "serverLayout": "Serveroppsett", - "timezone": "Tidssone", - "loadBalanceInfo": "Informasjon om lastbalanse / informasjonskapsler", - "loadBalanceInfoDesc": "RD Connection Broker-informasjonskapsel for lastbalansering på RDS-farm (f.eks. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd-proxy", - "guacdHostname": "guacd-vert", - "guacdHostnamePlaceholder": "Global standard", - "guacdPort": "guacd-porten", - "guacdProxyDesc": "Overstyr den globale guacd-forekomsten for denne tilkoblingen. La stå tomt for å bruke den globalt konfigurerte guacd-en.", - "gatewayHostname": "Gateway-vertsnavn", - "gatewayPort": "Gateway-porten", - "gatewayUsername": "Gateway-brukernavn", - "gatewayPassword": "Gateway-passord", - "gatewayDomain": "Gateway-domene", - "remoteAppProgram": "RemoteApp-programmet", - "workingDirectory": "Arbeidskatalog", - "arguments": "Argumenter", - "normalizeLineEndings": "Normaliser linjeavslutninger", - "macAddress": "MAC-adresse", - "broadcastAddress": "Kringkastingsadresse", - "udpPort": "UDP-port", - "waitTimeS": "Ventetid (s)", - "driveName": "Stasjonsnavn", - "drivePath": "Kjøresti", - "ignoreCertificate": "Ignorer sertifikat", - "ignoreCertificateDesc": "Tillat tilkoblinger til verter med selvsignerte sertifikater", - "forceLossless": "Tving tapsfri", - "forceLosslessDesc": "Tving frem tapsfri bildekoding (høyere kvalitet, mer båndbredde)", - "disableAudio": "Deaktiver lyd", - "disableAudioDesc": "Demp all lyd fra den eksterne økten", - "enableAudioInput": "Aktiver lydinngang (mikrofon)", - "enableAudioInputDesc": "Videresend den lokale mikrofonen til den eksterne økten", - "wallpaper": "Bakgrunn", - "wallpaperDesc": "Vis skrivebordsbakgrunn (deaktivering forbedrer ytelsen)", - "theming": "Temaer", - "themingDesc": "Aktiver visuelle temaer og stiler", - "fontSmoothing": "Skriftutjevning", - "fontSmoothingDesc": "Aktiver gjengivelse av ClearType-skrifttyper", - "fullWindowDrag": "Dra i hele vinduet", - "fullWindowDragDesc": "Vis vindusinnhold mens du drar", - "desktopComposition": "Skrivebordskomposisjon", - "desktopCompositionDesc": "Aktiver Aero Glass-effekter", - "menuAnimations": "Menyanimasjoner", - "menuAnimationsDesc": "Aktiver menyuttoning og lysbildeanimasjoner", - "disableBitmapCaching": "Deaktiver punktmap-hurtigbufring", - "disableBitmapCachingDesc": "Slå av bitmap-hurtigbufferen (kan hjelpe med feil)", - "disableOffscreenCaching": "Deaktiver mellomlagring utenfor skjermen", - "disableOffscreenCachingDesc": "Slå av hurtigbuffer utenfor skjermen", - "disableGlyphCaching": "Deaktiver glyff-caching", - "disableGlyphCachingDesc": "Slå av glyph-cache", - "enableGfx": "Aktiver GFX", - "enableGfxDesc": "Bruk RemoteFX-grafikkpipeline", - "enablePrinting": "Aktiver utskrift", - "enablePrintingDesc": "Omdiriger lokale skrivere til den eksterne økten", - "enableDriveRedirection": "Aktiver omdirigering av stasjoner", - "enableDriveRedirectionDesc": "Tilordne en lokal mappe som en stasjon i den eksterne økten", - "createDrivePath": "Opprett stasjonssti", - "createDrivePathDesc": "Opprett mappen automatisk hvis den ikke finnes", - "disableDownload": "Deaktiver nedlasting", - "disableDownloadDesc": "Forhindre nedlasting av filer fra den eksterne økten", - "disableUpload": "Deaktiver opplasting", - "disableUploadDesc": "Forhindre opplasting av filer til den eksterne økten", - "enableTouch": "Aktiver berøring", - "enableTouchDesc": "Aktiver videresending av berøringsinndata", - "consoleSession": "Konsolløkt", - "consoleSessionDesc": "Koble til konsollen (økt 0) i stedet for en ny økt", - "sendWolPacket": "Send WOL-pakke", - "sendWolPacketDesc": "Send en magisk pakke for å vekke denne verten før du kobler til", - "disableCopy": "Deaktiver kopiering", - "disableCopyDesc": "Forhindre kopiering av tekst fra den eksterne økten", - "disablePaste": "Deaktiver liming", - "disablePasteDesc": "Forhindre innliming av tekst i den eksterne økten", - "createPathIfMissing": "Opprett sti hvis den mangler", - "createPathIfMissingDesc": "Opprett opptaksmappen automatisk", - "excludeOutput": "Ekskluder utdata", - "excludeOutputDesc": "Ikke ta opp skjermutdata (kun metadata)", - "excludeMouse": "Utelukk mus", - "excludeMouseDesc": "Ikke opptak musebevegelser", - "includeKeystrokes": "Inkluder tastetrykk", - "includeKeystrokesDesc": "Ta opp rå tastetrykk i tillegg til skjermutdata", - "vncPort": "VNC-port", - "vncPassword": "VNC-passord", - "vncUsernameOptional": "Brukernavn (valgfritt)", - "vncLeaveBlank": "La stå tomt hvis det ikke er nødvendig", - "cursorMode": "Markørmodus", - "swapRedBlue": "Bytt rød/blå", - "swapRedBlueDesc": "Bytt om de røde og blå fargekanalene (rettar opp nokre fargeproblem)", - "readOnly": "Skrivebeskyttet", - "readOnlyDesc": "Vis fjernskjermen uten å sende noen input", - "telnetPort": "Telnet-port", - "terminalType": "Terminaltype", - "fontName": "Skriftnavn", - "fontSize": "Skriftstørrelse", - "colorScheme": "Fargeskjema", - "backspaceKey": "Tilbaketast", - "colorDepth": "Fargedybde", - "resizeMethod": "Endre størrelse-metoden", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Noen", - "auto": "Bil", - "nlaExtended": "NLA utvidet", - "color8": "8-bit", - "color16": "16-bit", - "color24": "24-bit", - "color32": "32-bit", - "displayUpdate": "Oppdatering av skjerm", - "reconnect": "Koble til på nytt", - "preserve": "Bevare", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Lokal", - "remote": "Fjern", - "failsafe": "feilsikker", - "blackOnWhite": "Svart på hvitt", - "whiteOnBlack": "Hvitt på svart", - "grayOnBlack": "Grått på svart", - "greenOnBlack": "Grønt på svart" - } - }, - "remoteDesktop": "Eksternt skrivebord", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Virtuelt nettverk", - "unencryptedShell": "Ukryptert skall", - "remoteDesktopNotes": "Notater", - "userDefaultsNote": "Innstillinger du lar være uendret følger vertens standardinnstillinger.", - "connectionOriginNote": "I skrivebordsappen bruker RDP, VNC og Telnet den eksterne serveren med mindre verten er satt til Denne enheten, som trenger en GUACD som denne maskinen kan nå. Angi én i innstillingene for eksternt skrivebord, eller per vert under GUACD Proxy.", - "docsLink": "Vis dokumenter", - "connectionOriginGuacamoleNote": "RDP, VNC og Telnet bruker den eksterne serveren med mindre du velger «Denne enheten», som trenger en GUACD som denne maskinen kan nå. Angi én i GUACD URL-innstillingen, eller per vert under GUACD Proxy." - }, - "onboarding": { - "feature_desktop": "Eksternt skrivebord", - "feature_desktop_desc": "Åpne RDP-, VNC- og Telnet-økter i en fane, på samme måte som SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Fargedybde", - "description": "Brukes for RDP-verter som lar den være på standardverdi." - }, - "resizeMethod": { - "label": "Endre størrelse-metoden", - "description": "Hvordan den eksterne skjermen følger vindusstørrelsen." - }, - "resizeDisplayUpdate": "Oppdatering av skjerm", - "resizeReconnect": "Koble til på nytt", - "inherit": "Arve", - "on": "På", - "off": "Av", - "forceLossless": { - "label": "Tving tapsfri", - "description": "Tving frem tapsfri bildekoding (høyere kvalitet, mer båndbredde)" - }, - "enableWallpaper": { - "label": "Bakgrunn", - "description": "Vis skrivebordsbakgrunn (deaktivering forbedrer ytelsen)" - }, - "enableFontSmoothing": { - "label": "Skriftutjevning", - "description": "Aktiver gjengivelse av ClearType-skrifttyper" - }, - "enableDesktopComposition": { - "label": "Skrivebordskomposisjon", - "description": "Aktiver Aero-glasseffekter" - }, - "disableAudio": { - "label": "Deaktiver lyd", - "description": "Demp all lyd fra den eksterne økten" - }, - "enablePrinting": { - "label": "Aktiver utskrift", - "description": "Omdiriger lokale skrivere til den eksterne økten" - }, - "enableDrive": { - "label": "Aktiver omdirigering av stasjoner", - "description": "Tilordne en lokal mappe som en stasjon i den eksterne økten" - }, - "disableCopy": { - "label": "Deaktiver kopiering", - "description": "Forhindre kopiering av tekst fra den eksterne økten" - }, - "disablePaste": { - "label": "Deaktiver liming", - "description": "Forhindre innliming av tekst i den eksterne økten" - }, - "group": "Standardinnstillinger for eksternt skrivebord", - "colorDepth16": "16-bit", - "colorDepth24": "24-bit", - "colorDepth32": "32-bit" - }, - "admin": { - "enabled": { - "label": "Aktiver eksternt skrivebord", - "description": "Tillat RDP-, VNC- og Telnet-tilkoblinger gjennom guacd." - }, - "guacdUrl": { - "label": "guacd-URL", - "description": "Der guacd lytter som host:port. Miljøvariablene GUACD_URL, GUACD_HOST og GUACD_PORT overstyrer dette.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP-port" - }, - "vncPort": { - "label": "VNC-port" - }, - "telnetPort": { - "label": "Telnet-port" - }, - "rdpSecurity": { - "label": "RDP-sikkerhetsmodus" - }, - "rdpIgnoreCert": { - "label": "Ignorer sertifikatfeil" - }, - "guacamoleConfig": { - "label": "guacd-innstillinger" - }, - "enableToolbar": { - "label": "Verktøylinjen for eksternt skrivebord", - "description": "Vis verktøylinjen i RDP-, VNC- og Telnet-økter." - }, - "displayGroup": "Utstilling", - "displayDefaultsTab": "Eksternt skrivebord" - } - }, - "palette": { - "connectRdp": "Koble til RDP", - "connectVnc": "Koble til VNC", - "connectTelnet": "Koble til med Telnet" - }, - "permissions": { - "sessions": { - "title": "Del eksterne skrivebordsøkter", - "description": "La øktdeling og samarbeidsrom vise denne brukerens RDP-, VNC- og Telnet-økter til andre." - } - }, - "errors": { - "remoteServerRequired": "Ekstern server kreves. Koble til en ekstern server i Innstillinger for å bruke denne tilkoblingstypen." - }, - "plugin": { - "name": "Eksternt skrivebord" - } -} diff --git a/plugins/remote-desktop/locales/translated/pl_PL.json b/plugins/remote-desktop/locales/translated/pl_PL.json deleted file mode 100644 index f9070c67c..000000000 --- a/plugins/remote-desktop/locales/translated/pl_PL.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Łączenie z sesją {{type}}...", - "checkingGuacd": "Sprawdzanie usługi pulpitu zdalnego...", - "requestingToken": "Żądanie tokena połączenia {{type}}...", - "openingSession": "Otwieranie sesji {{type}}...", - "negotiating": "Negocjowanie {{type}} uścisku dłoni...", - "sessionReady": "{{type}} sesja gotowa", - "connectionError": "Błąd połączenia", - "connectionFailed": "Połączenie nieudane", - "failedToConnect": "Nie udało się uzyskać tokena połączenia", - "hostNotFound": "Nie znaleziono hosta", - "noHostSelected": "Nie wybrano hosta", - "reconnect": "Połącz ponownie", - "retry": "Ponów", - "guacdUnavailable": "Usługa pulpitu zdalnego (guacd) jest niedostępna. Upewnij się, że usługa guacd jest uruchomiona, dostępna i poprawnie skonfigurowana w ustawieniach administratora.", - "credentialPromptTitle": "Wprowadź poświadczenia RDP", - "credentialPromptDescription": "Ten host jest ustawiony tak, aby pytać o poświadczenia przy łączeniu. Są używane tylko w tej sesji i nie są zapisywane.", - "connect": "Połączenie", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Pliki dysku", - "parent": "Katalog nadrzędny", - "refresh": "Odśwież", - "empty": "Ten folder jest pusty", - "listFailed": "Nie udało się odczytać tego folderu", - "download": "Pobierz", - "downloadFailed": "Nie udało się pobrać {{name}}", - "upload": "Prześlij pliki", - "uploading": "Przesyłanie {{name}}...", - "uploaded": "Przesłano {{name}}", - "uploadFailed": "Nie udało się przesłać {{name}}", - "uploadDisabled": "Przesyłanie plików jest wyłączone dla tego połączenia", - "driveUnavailable": "Włącz przekierowanie dysku RDP przed przesłaniem plików", - "driveNotWritable": "guacd nie może zapisać do folderu dysku. Sprawdź zmienną GUACD_DRIVE_PATH (lub ścieżkę do dysku hosta) i jej uprawnienia.", - "dropToUpload": "Upuść pliki, aby je przesłać" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (zablokuj ekran)", - "winKey": "Klawisz Windows", - "superL": "Super+L (ekran blokady)", - "superKey": "Super klucz", - "super": "Super", - "cmdL": "Cmd+L (ekran blokady)", - "cmdKey": "Klawisz polecenia", - "cmd": "Polecenie", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}} (zatrzaśnięty - kliknij, aby zwolnić)", - "stickyInactive": "{{key}} (kliknij, aby zatrzasnąć)", - "esc": "Esc", - "tab": "Tab", - "home": "Home", - "end": "End", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "Strzałka w górę", - "arrowDown": "Strzałka w dół", - "arrowLeft": "Strzałka w lewo", - "arrowRight": "Strzałka w prawo", - "fnToggle": "Klawisze funkcyjne", - "reconnect": "Połącz sesję ponownie", - "collapse": "Zwiń pasek narzędzi", - "expand": "Rozwiń pasek narzędzi", - "dragHandle": "Przeciągnij, aby zmienić położenie", - "hide": "Ukryj na czas tej sesji. Aby trwale wyłączyć tę opcję, otwórz Ustawienia hosta → Ogólne → Pasek narzędzi połączenia.", - "zoomIn": "Powiększ", - "zoomOut": "Oddal", - "resetZoom": "Zresetuj powiększenie", - "fitToScreen": "Dopasuj do ekranu", - "switchToTrackpad": "Przełącz na tryb gładzika (przeciągnij, aby przesunąć kursor, dotknij, aby kliknąć)", - "switchToTouch": "Przełącz na tryb dotykowy (dotknij bezpośrednio miejsca, w którym chcesz kliknąć)" - }, - "disabled": "Pulpit zdalny jest wyłączony. Administrator może go włączyć w Ustawieniach." - }, - "hosts": { - "nativeRdpOpened": "Otwarto w Pulpicie zdalnym systemu Windows", - "nativeRdpFailed": "Nie udało się otworzyć pulpitu zdalnego systemu Windows", - "openNativeRdp": "Otwórz w Pulpicie zdalnym systemu Windows", - "guac": { - "toolbar": "Pasek narzędzi", - "connection": "Połączenie", - "authentication": "Uwierzytelnianie", - "storedCredential": "Zapisane poświadczenie", - "authMethod": "Metoda uwierzytelniania", - "authTypeDirect": "Bezpośredni", - "authTypeCredential": "Poświadczenie", - "authTypeNone": "Brak", - "authTypeNoneDesc": "Żadne poświadczenia nie są przechowywane. Przy każdym połączeniu pojawi się prośba o nazwę użytkownika i hasło; nie są zapisywane.", - "selectCredential": "Wybierz poświadczenie...", - "connectionSettings": "Ustawienia połączenia", - "displaySettings": "Ustawienia wyświetlania", - "audioSettings": "Ustawienia audio", - "rdpPerformance": "Wydajność RDP", - "deviceRedirection": "Przekierowanie urządzenia", - "session": "Sesja", - "gateway": "Brama", - "remoteApp": "RemoteApp", - "clipboard": "Schowek", - "sessionRecording": "Nagrywanie sesji", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Ustawienia VNC", - "terminalSettings": "Ustawienia terminala", - "rdpPort": "Port RDP", - "username": "Nazwa użytkownika", - "password": "Hasło", - "passwordSaved": "Hasło zapisane, wpisz, aby zmienić", - "domain": "Domena", - "securityMode": "Tryb bezpieczeństwa", - "width": "Szerokość", - "height": "Wysokość", - "dpi": "DPI", - "clientName": "Nazwa klienta", - "initialProgram": "Program początkowy", - "serverLayout": "Układ serwera", - "timezone": "Strefa czasowa", - "loadBalanceInfo": "Informacje o równoważeniu obciążenia / cookie", - "loadBalanceInfoDesc": "Plik cookie brokera połączeń usług pulpitu zdalnego do równoważenia obciążenia farmy usług pulpitu zdalnego (np. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Serwer proxy guacd", - "guacdHostname": "Host guacd", - "guacdHostnamePlaceholder": "Globalne ustawienie domyślne", - "guacdPort": "Port guacd", - "guacdProxyDesc": "Zastąp globalną instancję guacd dla tego połączenia. Pozostaw puste, aby użyć globalnie skonfigurowanego guacd.", - "gatewayHostname": "Nazwa hosta bramy", - "gatewayPort": "Port bramy", - "gatewayUsername": "Nazwa użytkownika bramy", - "gatewayPassword": "Hasło bramy", - "gatewayDomain": "Domena bramy", - "remoteAppProgram": "Program RemoteApp", - "workingDirectory": "Katalog roboczy", - "arguments": "Argumenty", - "normalizeLineEndings": "Normalizuj zakończenia linii", - "macAddress": "Adres MAC", - "broadcastAddress": "Adres rozgłoszeniowy", - "udpPort": "Port UDP", - "waitTimeS": "Czas oczekiwania (s)", - "driveName": "Nazwa dysku", - "drivePath": "Ścieżka dysku", - "ignoreCertificate": "Ignoruj certyfikat", - "ignoreCertificateDesc": "Zezwalaj na połączenia z hostami z certyfikatami samopodpisanymi", - "forceLossless": "Wymuś bezstratność", - "forceLosslessDesc": "Wymuś bezstratne kodowanie obrazu (wyższa jakość, większa przepustowość)", - "disableAudio": "Wyłącz dźwięk", - "disableAudioDesc": "Wycisz cały dźwięk z sesji zdalnej", - "enableAudioInput": "Włącz wejście audio (mikrofon)", - "enableAudioInputDesc": "Przekaż lokalny mikrofon do sesji zdalnej", - "wallpaper": "Tapeta", - "wallpaperDesc": "Pokaż tapetę pulpitu (wyłączenie poprawia wydajność)", - "theming": "Motywy", - "themingDesc": "Włącz motywy i style wizualne", - "fontSmoothing": "Wygładzanie czcionek", - "fontSmoothingDesc": "Włącz renderowanie czcionek ClearType", - "fullWindowDrag": "Przeciąganie pełnego okna", - "fullWindowDragDesc": "Pokaż zawartość okna podczas przeciągania", - "desktopComposition": "Kompozycja pulpitu", - "desktopCompositionDesc": "Włącz efekty Aero Glass", - "menuAnimations": "Animacje menu", - "menuAnimationsDesc": "Włącz animacje zanikania i wysuwania menu", - "disableBitmapCaching": "Wyłącz buforowanie bitmap", - "disableBitmapCachingDesc": "Wyłącz pamięć podręczną bitmap (może pomóc przy artefaktach)", - "disableOffscreenCaching": "Wyłącz buforowanie poza ekranem", - "disableOffscreenCachingDesc": "Wyłącz pamięć podręczną poza ekranem", - "disableGlyphCaching": "Wyłącz buforowanie glifów", - "disableGlyphCachingDesc": "Wyłącz pamięć podręczną glifów", - "enableGfx": "Włącz GFX", - "enableGfxDesc": "Użyj potoku graficznego RemoteFX", - "enablePrinting": "Włącz drukowanie", - "enablePrintingDesc": "Przekieruj drukarki lokalne do sesji zdalnej", - "enableDriveRedirection": "Włącz przekierowanie dysku", - "enableDriveRedirectionDesc": "Mapuj folder lokalny jako dysk w sesji zdalnej", - "createDrivePath": "Utwórz ścieżkę dysku", - "createDrivePathDesc": "Automatycznie utwórz folder, jeśli nie istnieje", - "disableDownload": "Wyłącz pobieranie", - "disableDownloadDesc": "Zapobiegaj pobieraniu plików z sesji zdalnej", - "disableUpload": "Wyłącz przesyłanie", - "disableUploadDesc": "Zapobiegaj przesyłaniu plików do sesji zdalnej", - "enableTouch": "Włącz dotyk", - "enableTouchDesc": "Włącz przekazywanie wejścia dotykowego", - "consoleSession": "Sesja konsoli", - "consoleSessionDesc": "Połącz się z konsolą (sesja 0) zamiast z nową sesją", - "sendWolPacket": "Wyślij pakiet WOL", - "sendWolPacketDesc": "Wyślij magic packet, aby obudzić tego hosta przed połączeniem", - "disableCopy": "Wyłącz kopiowanie", - "disableCopyDesc": "Zapobiegaj kopiowaniu tekstu z sesji zdalnej", - "disablePaste": "Wyłącz wklejanie", - "disablePasteDesc": "Zapobiegaj wklejaniu tekstu do sesji zdalnej", - "createPathIfMissing": "Utwórz ścieżkę, jeśli nie istnieje", - "createPathIfMissingDesc": "Automatycznie utwórz katalog nagrań", - "excludeOutput": "Wyklucz obraz", - "excludeOutputDesc": "Nie nagrywaj obrazu ekranu (tylko metadane)", - "excludeMouse": "Wyklucz mysz", - "excludeMouseDesc": "Nie nagrywaj ruchów myszy", - "includeKeystrokes": "Uwzględnij naciśnięcia klawiszy", - "includeKeystrokesDesc": "Rejestruj surowe naciśnięcia klawiszy oprócz obrazu ekranu", - "vncPort": "Port VNC", - "vncPassword": "Hasło VNC", - "vncUsernameOptional": "Nazwa użytkownika (opcjonalnie)", - "vncLeaveBlank": "Pozostaw puste, jeśli nie jest wymagane", - "cursorMode": "Tryb kursora", - "swapRedBlue": "Zamień czerwony/niebieski", - "swapRedBlueDesc": "Zamień kanały kolorów czerwonego i niebieskiego (rozwiązuje niektóre problemy z kolorami)", - "readOnly": "Tylko do odczytu", - "readOnlyDesc": "Oglądaj zdalny ekran bez wysyłania jakiegokolwiek wejścia", - "telnetPort": "Port Telnet", - "terminalType": "Typ terminala", - "fontName": "Nazwa czcionki", - "fontSize": "Rozmiar czcionki", - "colorScheme": "Schemat kolorów", - "backspaceKey": "Klawisz Backspace", - "colorDepth": "Głębia koloru", - "resizeMethod": "Metoda zmiany rozmiaru", - "driveNamePlaceholder": "Napęd Termix", - "options": { - "any": "Każdy", - "auto": "Auto", - "nlaExtended": "NLA rozszerzony", - "color8": "8-bit", - "color16": "16-bit", - "color24": "24-bit", - "color32": "32-bit", - "displayUpdate": "Aktualizacja ekranu", - "reconnect": "Połącz ponownie", - "preserve": "Utrzymywać", - "unix": "Unix (LF)", - "windows": "Okna (CRLF)", - "local": "Lokalne", - "remote": "Zdalny", - "failsafe": "bezpieczny w razie awarii", - "blackOnWhite": "Czarno na białym", - "whiteOnBlack": "Białe na czarnym", - "grayOnBlack": "Szary na czarnym", - "greenOnBlack": "Zielony na czarnym" - } - }, - "remoteDesktop": "Pulpit zdalny", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Sieć wirtualna", - "unencryptedShell": "Niezaszyfrowana powłoka", - "remoteDesktopNotes": "Notatki", - "userDefaultsNote": "Ustawienia, które pozostawisz bez zmian, będą zgodne z ustawieniami domyślnymi Twojego hosta.", - "connectionOriginNote": "W aplikacji desktopowej RDP, VNC i Telnet korzystają ze zdalnego serwera, chyba że host jest ustawiony na „To urządzenie”, co wymaga identyfikatora GUACD, z którym ten komputer może się połączyć. Ustaw go w ustawieniach pulpitu zdalnego lub dla każdego hosta w sekcji Serwer proxy GUACD.", - "docsLink": "Zobacz dokumentację", - "connectionOriginGuacamoleNote": "RDP, VNC i Telnet korzystają ze zdalnego serwera, chyba że wybierzesz opcję „To urządzenie”, co wymaga identyfikatora GUACD, z którym ten komputer może się połączyć. Ustaw go w ustawieniu adresu URL GUACD lub dla każdego hosta w sekcji Serwer proxy GUACD." - }, - "onboarding": { - "feature_desktop": "Zdalny pulpit", - "feature_desktop_desc": "Otwieraj sesje RDP, VNC i Telnet w karcie, tak samo jak SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Głębia koloru", - "description": "Używane w przypadku hostów RDP, które pozostawiają tę wartość domyślną." - }, - "resizeMethod": { - "label": "Metoda zmiany rozmiaru", - "description": "W jaki sposób zdalny ekran dostosowuje się do rozmiaru okna." - }, - "resizeDisplayUpdate": "Aktualizacja ekranu", - "resizeReconnect": "Połącz ponownie", - "inherit": "Dziedzicz", - "on": "Wł.", - "off": "Wył.", - "forceLossless": { - "label": "Wymuś bezstratność", - "description": "Wymuś bezstratne kodowanie obrazu (wyższa jakość, większa przepustowość)" - }, - "enableWallpaper": { - "label": "Tapeta", - "description": "Pokaż tapetę pulpitu (wyłączenie poprawia wydajność)" - }, - "enableFontSmoothing": { - "label": "Wygładzanie czcionek", - "description": "Włącz renderowanie czcionek ClearType" - }, - "enableDesktopComposition": { - "label": "Kompozycja pulpitu", - "description": "Włącz efekty Aero Glass" - }, - "disableAudio": { - "label": "Wyłącz dźwięk", - "description": "Wycisz cały dźwięk z sesji zdalnej" - }, - "enablePrinting": { - "label": "Włącz drukowanie", - "description": "Przekieruj drukarki lokalne do sesji zdalnej" - }, - "enableDrive": { - "label": "Włącz przekierowanie dysku", - "description": "Mapuj folder lokalny jako dysk w sesji zdalnej" - }, - "disableCopy": { - "label": "Wyłącz kopiowanie", - "description": "Zapobiegaj kopiowaniu tekstu z sesji zdalnej" - }, - "disablePaste": { - "label": "Wyłącz wklejanie", - "description": "Zapobiegaj wklejaniu tekstu do sesji zdalnej" - }, - "group": "Domyślne ustawienia pulpitu zdalnego", - "colorDepth16": "16-bit", - "colorDepth24": "24-bit", - "colorDepth32": "32-bit" - }, - "admin": { - "enabled": { - "label": "Włącz pulpit zdalny", - "description": "Zezwalaj na połączenia RDP, VNC i Telnet przez guacd." - }, - "guacdUrl": { - "label": "Adres URL guacd", - "description": "Miejsce, w którym nasłuchuje GUACD, jako host:port. Zmienne środowiskowe GUACD_URL, GUACD_HOST i GUACD_PORT zastępują to ustawienie.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "Port RDP" - }, - "vncPort": { - "label": "Port VNC" - }, - "telnetPort": { - "label": "Port Telnet" - }, - "rdpSecurity": { - "label": "Tryb bezpieczeństwa RDP" - }, - "rdpIgnoreCert": { - "label": "Ignoruj błędy certyfikatu" - }, - "guacamoleConfig": { - "label": "ustawienia GUACD" - }, - "enableToolbar": { - "label": "Pasek narzędzi pulpitu zdalnego", - "description": "Wyświetla pasek narzędzi w sesjach RDP, VNC i Telnet." - }, - "displayGroup": "Wyświetlanie", - "displayDefaultsTab": "Pulpit zdalny" - } - }, - "palette": { - "connectRdp": "Połącz się z RDP", - "connectVnc": "Połącz się z VNC", - "connectTelnet": "Połącz się przez Telnet" - }, - "permissions": { - "sessions": { - "title": "Udostępniaj sesje pulpitu zdalnego", - "description": "Zezwól na udostępnianie sesji i współpracę w pokojach, aby sesje RDP, VNC i Telnet tego użytkownika były widoczne dla innych użytkowników." - } - }, - "errors": { - "remoteServerRequired": "Wymagany jest serwer zdalny. Połącz się z serwerem zdalnym w Ustawieniach, aby korzystać z tego typu połączenia." - }, - "plugin": { - "name": "Pulpit zdalny" - } -} diff --git a/plugins/remote-desktop/locales/translated/pt_BR.json b/plugins/remote-desktop/locales/translated/pt_BR.json deleted file mode 100644 index 4fe26c8d3..000000000 --- a/plugins/remote-desktop/locales/translated/pt_BR.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Conectando à sessão {{type}}...", - "checkingGuacd": "Verificando o serviço de área de trabalho remota...", - "requestingToken": "Solicitando token de conexão {{type}}...", - "openingSession": "Abrindo {{type}} sessão...", - "negotiating": "Negotiating {{type}} handshake...", - "sessionReady": "{{type}} sessão pronta", - "connectionError": "Erro de conexão", - "connectionFailed": "Falha na conexão", - "failedToConnect": "Falha ao obter o token de conexão", - "hostNotFound": "Host não encontrado", - "noHostSelected": "Nenhum host selecionado", - "reconnect": "Reconectar", - "retry": "Tentar novamente", - "guacdUnavailable": "O serviço de área de trabalho remota (guacd) não está disponível. Certifique-se de que o guacd esteja em execução, acessível e configurado corretamente nas configurações de administrador.", - "credentialPromptTitle": "Insira as credenciais RDP", - "credentialPromptDescription": "Este host está configurado para solicitar credenciais ao conectar. Elas são usadas apenas para esta sessão e não são salvas.", - "connect": "Conectar", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Arquivos do Drive", - "parent": "Diretório pai", - "refresh": "Atualizar", - "empty": "Esta pasta está vazia", - "listFailed": "Não foi possível ler esta pasta.", - "download": "Download", - "downloadFailed": "Falha ao baixar {{name}}", - "upload": "Carregar arquivos", - "uploading": "Carregando {{name}}...", - "uploaded": "Enviado {{name}}", - "uploadFailed": "Falha ao carregar {{name}}", - "uploadDisabled": "Os uploads estão desativados para esta conexão.", - "driveUnavailable": "Ative o redirecionamento de unidade RDP antes de enviar arquivos.", - "driveNotWritable": "O guacd não consegue gravar na pasta da unidade. Verifique a variável de ambiente GUACD_DRIVE_PATH (ou o caminho da unidade do host) e suas permissões.", - "dropToUpload": "Arraste e solte os arquivos para fazer o upload." - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Bloquear tela)", - "winKey": "Tecla do Windows", - "superL": "Super+L (Tela de bloqueio)", - "superKey": "Super Chave", - "super": "Super", - "cmdL": "Cmd+L (Bloquear tela)", - "cmdKey": "Tecla de comando", - "cmd": "Comando", - "ctrl": "Ctrl", - "alt": "Alternativa", - "shift": "Mudança", - "win": "Ganhar", - "stickyActive": "{{key}} (travado - clique para liberar)", - "stickyInactive": "{{key}} (clique para travar)", - "esc": "Escapar", - "tab": "Aba", - "home": "Inicio", - "end": "Fim", - "pageUp": "Página acima", - "pageDown": "Página abaixo", - "arrowUp": "Seta para cima", - "arrowDown": "Seta para baixo", - "arrowLeft": "Seta para a esquerda", - "arrowRight": "Seta para a direita", - "fnToggle": "Teclas de função", - "reconnect": "Sessão de reconexão", - "collapse": "Recolher barra de ferramentas", - "expand": "Expandir barra de ferramentas", - "dragHandle": "Arraste para reposicionar", - "hide": "Ocultar para esta sessão. Para desativá-lo permanentemente, abra Configurações do Host → Geral → Barra de Ferramentas de Conexão.", - "zoomIn": "Ampliar", - "zoomOut": "Diminuir o zoom", - "resetZoom": "Redefinir zoom", - "fitToScreen": "Ajustar à tela", - "switchToTrackpad": "Alternar para o modo trackpad (arraste para mover o cursor, toque para clicar)", - "switchToTouch": "Mude para o modo de toque (toque diretamente onde deseja clicar)." - }, - "disabled": "A Área de Trabalho Remota está desativada. Um administrador pode ativá-la nas Configurações." - }, - "hosts": { - "nativeRdpOpened": "Aberto na Área de Trabalho Remota do Windows", - "nativeRdpFailed": "Falha ao abrir a Área de Trabalho Remota do Windows", - "openNativeRdp": "Abrir no Windows Remote Desktop", - "guac": { - "toolbar": "Barra de ferramentas", - "connection": "Conexão", - "authentication": "Autenticação", - "storedCredential": "Credencial Armazenado", - "authMethod": "Método de Autenticação", - "authTypeDirect": "Direto", - "authTypeCredential": "Credencial", - "authTypeNone": "Nenhum", - "authTypeNoneDesc": "Nenhuma credencial é armazenada. Você será solicitado a inserir um nome de usuário e uma senha sempre que se conectar; eles não são salvos.", - "selectCredential": "Selecione uma credencial...", - "connectionSettings": "Configurações de conexão", - "displaySettings": "Configurações de exibição", - "audioSettings": "Configurações de áudio", - "rdpPerformance": "Desempenho do RDP", - "deviceRedirection": "Redirecionamento de Dispositivos", - "session": "Sessão", - "gateway": "Portal", - "remoteApp": "Aplicativo remoto", - "clipboard": "Área de transferência", - "sessionRecording": "Gravação da sessão", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Configurações VNC", - "terminalSettings": "Configurações do terminal", - "rdpPort": "Porta RDP", - "username": "Nome de usuário", - "password": "Senha", - "passwordSaved": "Senha salva, digite para alterar", - "domain": "Domínio", - "securityMode": "Modo de segurança", - "width": "Largura", - "height": "Altura", - "dpi": "DPI", - "clientName": "Nome do cliente", - "initialProgram": "Programa Inicial", - "serverLayout": "Layout do servidor", - "timezone": "Fuso horário", - "loadBalanceInfo": "Informações de balanceamento de carga / Cookies", - "loadBalanceInfoDesc": "Cookie do agente de conexão RD para balanceamento de carga do farm RDS (ex: tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Proxy guacd", - "guacdHostname": "guacd Host", - "guacdHostnamePlaceholder": "Padrão global", - "guacdPort": "Porto guacd", - "guacdProxyDesc": "Substitua a instância global do guacd para esta conexão. Deixe em branco para usar o guacd configurado globalmente.", - "gatewayHostname": "Nome do host do gateway", - "gatewayPort": "Porta de entrada", - "gatewayUsername": "Nome de usuário do gateway", - "gatewayPassword": "Senha do Gateway", - "gatewayDomain": "Domínio de entrada", - "remoteAppProgram": "Programa RemoteApp", - "workingDirectory": "Diretório de Trabalho", - "arguments": "Argumentos", - "normalizeLineEndings": "Normalizar as quebras de linha", - "macAddress": "Endereço MAC", - "broadcastAddress": "Discurso de transmissão", - "udpPort": "Porta UDP", - "waitTimeS": "Tempo de espera (s)", - "driveName": "Nome da unidade", - "drivePath": "Percurso de condução", - "ignoreCertificate": "Ignorar certificado", - "ignoreCertificateDesc": "Permitir conexões a hosts com certificados autoassinados", - "forceLossless": "Força sem perdas", - "forceLosslessDesc": "Forçar codificação de imagem sem perdas (maior qualidade, mais largura de banda)", - "disableAudio": "Desativar áudio", - "disableAudioDesc": "Silenciar todo o áudio da sessão remota.", - "enableAudioInput": "Ativar entrada de áudio (microfone)", - "enableAudioInputDesc": "Encaminhar o microfone local para a sessão remota.", - "wallpaper": "Papel de parede", - "wallpaperDesc": "Exibir papel de parede da área de trabalho (desativar melhora o desempenho)", - "theming": "Tematização", - "themingDesc": "Ativar temas e estilos visuais", - "fontSmoothing": "Suavização de fontes", - "fontSmoothingDesc": "Ativar renderização de fontes ClearType", - "fullWindowDrag": "Arrastar janela inteira", - "fullWindowDragDesc": "Exibir o conteúdo da janela enquanto arrasta", - "desktopComposition": "Composição de mesa", - "desktopCompositionDesc": "Ative os efeitos de vidro Aero", - "menuAnimations": "Animações de menu", - "menuAnimationsDesc": "Ativar animações de transição e deslizamento do menu", - "disableBitmapCaching": "Desativar o cache de bitmaps", - "disableBitmapCachingDesc": "Desative o cache de bitmaps (pode ajudar a resolver problemas técnicos).", - "disableOffscreenCaching": "Desativar o cache fora da tela", - "disableOffscreenCachingDesc": "Desative o cache fora da tela", - "disableGlyphCaching": "Desativar o cache de glifos", - "disableGlyphCachingDesc": "Desative o cache de glifos", - "enableGfx": "Ativar GFX", - "enableGfxDesc": "Utilize o pipeline gráfico RemoteFX", - "enablePrinting": "Habilitar impressão", - "enablePrintingDesc": "Redirecionar impressoras locais para a sessão remota.", - "enableDriveRedirection": "Ativar o redirecionamento de unidades", - "enableDriveRedirectionDesc": "Mapear uma pasta local como uma unidade na sessão remota.", - "createDrivePath": "Criar caminho de unidade", - "createDrivePathDesc": "Criar automaticamente a pasta caso ela não exista.", - "disableDownload": "Desativar download", - "disableDownloadDesc": "Impedir o download de arquivos da sessão remota.", - "disableUpload": "Desativar o upload", - "disableUploadDesc": "Impedir o envio de arquivos para a sessão remota.", - "enableTouch": "Ativar toque", - "enableTouchDesc": "Ativar o encaminhamento de entrada por toque", - "consoleSession": "Sessão do console", - "consoleSessionDesc": "Conecte-se ao console (sessão 0) em vez de uma nova sessão.", - "sendWolPacket": "Enviar pacote WOL", - "sendWolPacketDesc": "Envie um pacote mágico para ativar este host antes de se conectar.", - "disableCopy": "Desativar cópia", - "disableCopyDesc": "Impedir a cópia de texto da sessão remota", - "disablePaste": "Desativar colagem", - "disablePasteDesc": "Impedir a colagem de texto na sessão remota.", - "createPathIfMissing": "Criar caminho se estiver faltando", - "createPathIfMissingDesc": "Criar automaticamente o diretório de gravação", - "excludeOutput": "Excluir saída", - "excludeOutputDesc": "Não gravar a saída da tela (apenas metadados).", - "excludeMouse": "Excluir o rato", - "excludeMouseDesc": "Não grave movimentos do mouse", - "includeKeystrokes": "Incluir teclas pressionadas", - "includeKeystrokesDesc": "Registre as teclas digitadas em sua forma original, além da saída na tela.", - "vncPort": "Porta VNC", - "vncPassword": "Senha VNC", - "vncUsernameOptional": "Nome de usuário (opcional)", - "vncLeaveBlank": "Deixe em branco se não for necessário.", - "cursorMode": "Modo Cursor", - "swapRedBlue": "Trocar Vermelho/Azul", - "swapRedBlueDesc": "Trocar os canais de cor vermelho e azul resolve alguns problemas de cor.", - "readOnly": "Somente leitura", - "readOnlyDesc": "Visualize a tela remota sem enviar nenhum comando.", - "telnetPort": "Porta Telnet", - "terminalType": "Tipo de terminal", - "fontName": "Nome da fonte", - "fontSize": "Tamanho da Fonte", - "colorScheme": "Esquema de cores", - "backspaceKey": "Tecla Backspace", - "colorDepth": "Profundidade de cor", - "resizeMethod": "Método de redimensionamento", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Qualquer", - "auto": "Auto", - "nlaExtended": "NLA Estendido", - "color8": "8 bits", - "color16": "16 bits", - "color24": "24 bits", - "color32": "32 bits", - "displayUpdate": "Atualização de exibição", - "reconnect": "Reconectar", - "preserve": "Preservar", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Local", - "remote": "Remoto", - "failsafe": "segurança contra falhas", - "blackOnWhite": "Preto sobre branco", - "whiteOnBlack": "Branco sobre preto", - "grayOnBlack": "Cinza sobre preto", - "greenOnBlack": "Verde sobre preto" - } - }, - "remoteDesktop": "Área de trabalho remota", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Rede Virtual", - "unencryptedShell": "Shell não criptografado", - "remoteDesktopNotes": "Notas", - "userDefaultsNote": "As configurações que você deixar inalteradas seguirão as configurações padrão do seu host.", - "connectionOriginNote": "No aplicativo para desktop, RDP, VNC e Telnet usam o servidor remoto, a menos que o host esteja configurado como \"Este dispositivo\", o que requer um proxy guacd que esta máquina possa acessar. Defina um nas configurações da Área de Trabalho Remota ou por host em \"Proxy guacd\".", - "docsLink": "Documentação", - "connectionOriginGuacamoleNote": "RDP, VNC e Telnet usam o servidor remoto, a menos que você selecione \"Este dispositivo\", que requer um guacd que esta máquina possa acessar. Defina um nas configurações de URL do guacd ou por host em Proxy do guacd." - }, - "onboarding": { - "feature_desktop": "Área de trabalho remota", - "feature_desktop_desc": "Abra sessões RDP, VNC e Telnet em uma aba, da mesma forma que o SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Profundidade de cor", - "description": "Usado para hosts RDP que o deixam na configuração padrão." - }, - "resizeMethod": { - "label": "Método de redimensionamento", - "description": "Como a tela remota acompanha o tamanho da janela." - }, - "resizeDisplayUpdate": "Atualização de exibição", - "resizeReconnect": "Reconectar", - "inherit": "Herdar", - "on": "Sobre", - "off": "Desligado", - "forceLossless": { - "label": "Força sem perdas", - "description": "Forçar codificação de imagem sem perdas (maior qualidade, mais largura de banda)" - }, - "enableWallpaper": { - "label": "Papel de parede", - "description": "Exibir papel de parede da área de trabalho (desativar melhora o desempenho)" - }, - "enableFontSmoothing": { - "label": "Suavização de fontes", - "description": "Ativar renderização de fontes ClearType" - }, - "enableDesktopComposition": { - "label": "Composição de mesa", - "description": "Ative os efeitos de vidro Aero" - }, - "disableAudio": { - "label": "Desativar áudio", - "description": "Silenciar todo o áudio da sessão remota." - }, - "enablePrinting": { - "label": "Habilitar impressão", - "description": "Redirecionar impressoras locais para a sessão remota." - }, - "enableDrive": { - "label": "Ativar o redirecionamento de unidades", - "description": "Mapear uma pasta local como uma unidade na sessão remota." - }, - "disableCopy": { - "label": "Desativar cópia", - "description": "Impedir a cópia de texto da sessão remota" - }, - "disablePaste": { - "label": "Desativar colagem", - "description": "Impedir a colagem de texto na sessão remota." - }, - "group": "Configurações padrão da Área de Trabalho Remota", - "colorDepth16": "16 bits", - "colorDepth24": "24 bits", - "colorDepth32": "32 bits" - }, - "admin": { - "enabled": { - "label": "Habilitar Área de Trabalho Remota", - "description": "Permitir conexões RDP, VNC e Telnet através do guacd." - }, - "guacdUrl": { - "label": "URL do guacd", - "description": "O local onde o guacd está escutando é definido como host:porta. As variáveis de ambiente GUACD_URL, GUACD_HOST e GUACD_PORT têm precedência sobre isso.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "Porta RDP" - }, - "vncPort": { - "label": "Porta VNC" - }, - "telnetPort": { - "label": "porta Telnet" - }, - "rdpSecurity": { - "label": "Modo de segurança RDP" - }, - "rdpIgnoreCert": { - "label": "Ignorar erros de certificado" - }, - "guacamoleConfig": { - "label": "configurações do guacd" - }, - "enableToolbar": { - "label": "Barra de ferramentas da área de trabalho remota", - "description": "Exibir a barra de ferramentas em sessões RDP, VNC e Telnet." - }, - "displayGroup": "Mostrar", - "displayDefaultsTab": "Área de trabalho remota" - } - }, - "palette": { - "connectRdp": "Conectar com RDP", - "connectVnc": "Conecte-se com VNC", - "connectTelnet": "Conecte-se com Telnet" - }, - "permissions": { - "sessions": { - "title": "Compartilhar sessões de área de trabalho remota", - "description": "Permitir que o compartilhamento de sessões e as salas de colaboração mostrem as sessões RDP, VNC e Telnet deste usuário para outros usuários." - } - }, - "errors": { - "remoteServerRequired": "É necessário um servidor remoto. Conecte um servidor remoto nas Configurações para usar este tipo de conexão." - }, - "plugin": { - "name": "Área de trabalho remota" - } -} diff --git a/plugins/remote-desktop/locales/translated/pt_PT.json b/plugins/remote-desktop/locales/translated/pt_PT.json deleted file mode 100644 index 4573dcc7b..000000000 --- a/plugins/remote-desktop/locales/translated/pt_PT.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "A ligar à sessão {{type}}...", - "checkingGuacd": "Verificação do serviço de ambiente de trabalho remoto...", - "requestingToken": "A solicitar token de ligação {{type}}...", - "openingSession": "Abertura {{type}} sessão...", - "negotiating": "Negotiating {{type}} handshake...", - "sessionReady": "{{type}} sessão pronta", - "connectionError": "Erro de ligação", - "connectionFailed": "Falha na ligação", - "failedToConnect": "Falha ao obter o token de ligação", - "hostNotFound": "Servidor não encontrado", - "noHostSelected": "Nenhum servidor selecionado", - "reconnect": "Reconectar", - "retry": "Repetir", - "guacdUnavailable": "O serviço de ambiente de trabalho remoto (guacd) não está disponível. Certifique-se de que o guacd está em execução, acessível e devidamente configurado nas definições de administração.", - "credentialPromptTitle": "Introduza as credenciais RDP", - "credentialPromptDescription": "Este host está configurado para solicitar credenciais ao ligar. São utilizadas apenas para esta sessão e não são guardadas.", - "connect": "Ligar", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Arquivos do Drive", - "parent": "Diretório pai", - "refresh": "Atualizar", - "empty": "Esta pasta está vazia", - "listFailed": "Não foi possível ler esta pasta.", - "download": "Descarregar", - "downloadFailed": "Falha ao descarregar {{name}}", - "upload": "Carregar ficheiros", - "uploading": "A enviar {{name}}...", - "uploaded": "Enviado {{name}}", - "uploadFailed": "Falha ao carregar {{name}}", - "uploadDisabled": "Os uploads estão desativados para esta ligação.", - "driveUnavailable": "Ative o redireccionamento da unidade RDP antes de enviar ficheiros.", - "driveNotWritable": "O guacd não consegue escrever na pasta da unidade. Verifique a variável de ambiente GUACD_DRIVE_PATH (ou o caminho da unidade do host) e as suas permissões.", - "dropToUpload": "Arraste e largue os ficheiros para fazer o upload." - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Bloquear ecrã)", - "winKey": "Tecla Windows", - "superL": "Super+L (Ecrã de bloqueio)", - "superKey": "Super Chave", - "super": "Super", - "cmdL": "Cmd+L (Bloquear ecrã)", - "cmdKey": "Tecla de comando", - "cmd": "Comando", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}} (bloqueado - clique para libertar)", - "stickyInactive": "{{key}} (clique para bloquear)", - "esc": "Escape", - "tab": "Tab", - "home": "Home", - "end": "Fim", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "Seta para Cima", - "arrowDown": "Seta para Baixo", - "arrowLeft": "Seta para a Esquerda", - "arrowRight": "Seta para a Direita", - "fnToggle": "Teclas de Função", - "reconnect": "Reconectar Sessão", - "collapse": "Recolher barra", - "expand": "Expandir barra", - "dragHandle": "Arraste para reposicionar", - "hide": "Ocultar para esta sessão. Para o desativar permanentemente, abra as Definições do Host → Geral → Barra de Ferramentas de Ligação.", - "zoomIn": "Ampliar", - "zoomOut": "Diminuir o zoom", - "resetZoom": "Redefinir zoom", - "fitToScreen": "Ajustar ao ecrã", - "switchToTrackpad": "Mudar para o modo trackpad (arraste para mover o cursor, toque para clicar)", - "switchToTouch": "Mude para o modo de toque (toque diretamente onde pretende clicar)." - }, - "disabled": "A Área de Trabalho Remota está desativada. Um administrador pode ativá-la nas Definições." - }, - "hosts": { - "nativeRdpOpened": "Aberto na Área de Trabalho Remota do Windows", - "nativeRdpFailed": "Falha ao abrir a Área de Trabalho Remota do Windows", - "openNativeRdp": "Abrir no Windows Remote Desktop", - "guac": { - "toolbar": "Barra de ferramentas", - "connection": "Ligação", - "authentication": "Autenticação", - "storedCredential": "Credencial Armazenada", - "authMethod": "Método de Autenticação", - "authTypeDirect": "Direta", - "authTypeCredential": "Credencial", - "authTypeNone": "Nenhuma", - "authTypeNoneDesc": "Nenhuma credencial é armazenada. Ser-lhe-á pedido que insira um nome de utilizador e uma senha sempre que se conectar; não são salvos.", - "selectCredential": "Selecionar uma credencial...", - "connectionSettings": "Definições de Ligação", - "displaySettings": "Definições de Ecrã", - "audioSettings": "Definições de Áudio", - "rdpPerformance": "Desempenho do RDP", - "deviceRedirection": "Redirecionamento de Dispositivos", - "session": "Sessão", - "gateway": "Gateway", - "remoteApp": "RemoteApp", - "clipboard": "Área de Transferência", - "sessionRecording": "Gravação de Sessão", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Definições VNC", - "terminalSettings": "Definições do Terminal", - "rdpPort": "Porta RDP", - "username": "Nome de utilizador", - "password": "Palavra-passe", - "passwordSaved": "Palavra-passe guardada, digite para alterar", - "domain": "Domínio", - "securityMode": "Modo de segurança", - "width": "Largura", - "height": "Altura", - "dpi": "DPI", - "clientName": "Nome do cliente", - "initialProgram": "Programa inicial", - "serverLayout": "Layout do servidor", - "timezone": "Fuso horário", - "loadBalanceInfo": "Informação de balanceamento de carga / Cookie", - "loadBalanceInfoDesc": "Cookie do RD Connection Broker para balanceamento de carga em farms RDS (ex: tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Proxy guacd", - "guacdHostname": "Host guacd", - "guacdHostnamePlaceholder": "Padrão global", - "guacdPort": "Porta guacd", - "guacdProxyDesc": "Substituir a instância guacd global para esta ligação. Deixe em branco para usar o guacd configurado globalmente.", - "gatewayHostname": "Nome de host do gateway", - "gatewayPort": "Porta do gateway", - "gatewayUsername": "Nome de utilizador do gateway", - "gatewayPassword": "Palavra-passe do gateway", - "gatewayDomain": "Domínio do gateway", - "remoteAppProgram": "Programa RemoteApp", - "workingDirectory": "Diretório de trabalho", - "arguments": "Argumentos", - "normalizeLineEndings": "Normalizar fins de linha", - "macAddress": "Endereço MAC", - "broadcastAddress": "Endereço de broadcast", - "udpPort": "Porta UDP", - "waitTimeS": "Tempo de Espera (s)", - "driveName": "Nome da Unidade", - "drivePath": "Caminho da Unidade", - "ignoreCertificate": "Ignorar Certificado", - "ignoreCertificateDesc": "Permitir ligações a hosts com certificados auto-assinados", - "forceLossless": "Forçar sem perdas", - "forceLosslessDesc": "Forçar codificação de imagem sem perdas (maior qualidade, mais largura de banda)", - "disableAudio": "Desativar Áudio", - "disableAudioDesc": "Silenciar todo o áudio da sessão remota", - "enableAudioInput": "Ativar Entrada de Áudio (Microfone)", - "enableAudioInputDesc": "Encaminhar microfone local para a sessão remota", - "wallpaper": "Fundo de ecrã", - "wallpaperDesc": "Mostrar fundo de ecrã (desativar melhora o desempenho)", - "theming": "Temas", - "themingDesc": "Ativar temas e estilos visuais", - "fontSmoothing": "Suavização de Fontes", - "fontSmoothingDesc": "Ativar renderização de fontes ClearType", - "fullWindowDrag": "Arrasto de Janela Completo", - "fullWindowDragDesc": "Mostrar conteúdo da janela ao arrastar", - "desktopComposition": "Composição do ambiente de trabalho", - "desktopCompositionDesc": "Ativar efeitos de transparência Aero", - "menuAnimations": "Animações de Menu", - "menuAnimationsDesc": "Ativar animações de desvanecimento e deslizamento de menus", - "disableBitmapCaching": "Desativar cache de bitmaps", - "disableBitmapCachingDesc": "Desativar a cache de bitmaps (pode ajudar com falhas)", - "disableOffscreenCaching": "Desativar cache fora do ecrã", - "disableOffscreenCachingDesc": "Desativar a cache fora do ecrã", - "disableGlyphCaching": "Desativar cache de glifos", - "disableGlyphCachingDesc": "Desativar a cache de glifos", - "enableGfx": "Ativar GFX", - "enableGfxDesc": "Usar pipeline gráfico RemoteFX", - "enablePrinting": "Ativar Impressão", - "enablePrintingDesc": "Redirecionar impressoras locais para a sessão remota", - "enableDriveRedirection": "Ativar Redirecionamento de Unidades", - "enableDriveRedirectionDesc": "Mapear uma pasta local como unidade na sessão remota", - "createDrivePath": "Criar Caminho de Unidade", - "createDrivePathDesc": "Criar automaticamente a pasta se não existir", - "disableDownload": "Desativar Download", - "disableDownloadDesc": "Impedir o download de ficheiros da sessão remota", - "disableUpload": "Desativar Upload", - "disableUploadDesc": "Impedir o envio de ficheiros para a sessão remota", - "enableTouch": "Ativar Toque", - "enableTouchDesc": "Ativar o reencaminhamento de entrada tátil", - "consoleSession": "Sessão de Consola", - "consoleSessionDesc": "Ligar à consola (sessão 0) em vez de uma nova sessão", - "sendWolPacket": "Enviar Pacote WOL", - "sendWolPacketDesc": "Enviar um pacote mágico para despertar este host antes de ligar", - "disableCopy": "Desativar Cópia", - "disableCopyDesc": "Impedir a cópia de texto da sessão remota", - "disablePaste": "Desativar Colagem", - "disablePasteDesc": "Impedir a colagem de texto na sessão remota", - "createPathIfMissing": "Criar caminho se não existir", - "createPathIfMissingDesc": "Criar automaticamente o diretório de gravação", - "excludeOutput": "Excluir Saída", - "excludeOutputDesc": "Não gravar a saída do ecrã (apenas metadados)", - "excludeMouse": "Excluir Rato", - "excludeMouseDesc": "Não gravar os movimentos do rato", - "includeKeystrokes": "Incluir Teclas", - "includeKeystrokesDesc": "Gravar as teclas premidas em bruto, além da saída do ecrã", - "vncPort": "Porta VNC", - "vncPassword": "Palavra-passe VNC", - "vncUsernameOptional": "Nome de utilizador (opcional)", - "vncLeaveBlank": "Deixar em branco se não for necessário", - "cursorMode": "Modo do Cursor", - "swapRedBlue": "Trocar Vermelho/Azul", - "swapRedBlueDesc": "Trocar os canais de cor vermelha e azul (corrige alguns problemas de cor)", - "readOnly": "Apenas leitura", - "readOnlyDesc": "Visualizar o ecrã remoto sem enviar qualquer entrada", - "telnetPort": "Porta Telnet", - "terminalType": "Tipo de Terminal", - "fontName": "Nome da Fonte", - "fontSize": "Tamanho da Fonte", - "colorScheme": "Esquema de Cores", - "backspaceKey": "Tecla Backspace", - "colorDepth": "Profundidade de cor", - "resizeMethod": "Método de redimensionamento", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Qualquer", - "auto": "Automático", - "nlaExtended": "NLA Estendido", - "color8": "8 bits", - "color16": "16 bits", - "color24": "24 bits", - "color32": "32 bits", - "displayUpdate": "Atualização de visualização", - "reconnect": "Reconectar", - "preserve": "Preservar", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Local", - "remote": "Remoto", - "failsafe": "segurança contra falhas", - "blackOnWhite": "Preto sobre branco", - "whiteOnBlack": "Branco sobre preto", - "grayOnBlack": "Cinza sobre preto", - "greenOnBlack": "Verde sobre preto" - } - }, - "remoteDesktop": "Ambiente de trabalho remoto", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Rede Virtual", - "unencryptedShell": "Shell não encriptado", - "remoteDesktopNotes": "Notas", - "userDefaultsNote": "As definições que deixar inalteradas seguirão as definições padrão do seu host.", - "connectionOriginNote": "Na aplicação para desktop, o RDP, o VNC e o Telnet utilizam o servidor remoto, a menos que o host esteja configurado como \"Este dispositivo\", o que requer um proxy guacd a que esta máquina possa aceder. Defina um nas definições da Área de Trabalho Remota ou por host em \"Proxy guacd\".", - "docsLink": "Ver documentação", - "connectionOriginGuacamoleNote": "O RDP, o VNC e o Telnet utilizam o servidor remoto, a menos que seleccione \"Este dispositivo\", que requer um guacd a que esta máquina possa aceder. Defina um nas definições de URL do guacd ou por host em Proxy do guacd." - }, - "onboarding": { - "feature_desktop": "Área de trabalho remota", - "feature_desktop_desc": "Abra as sessões RDP, VNC e Telnet num separador, da mesma forma que o SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Profundidade de cor", - "description": "Utilizado para hosts RDP que o deixam na configuração padrão." - }, - "resizeMethod": { - "label": "Método de redimensionamento", - "description": "Como o ecrã remoto acompanha o tamanho da janela." - }, - "resizeDisplayUpdate": "Atualização de visualização", - "resizeReconnect": "Reconectar", - "inherit": "Herdar", - "on": "Ativado", - "off": "Desativado", - "forceLossless": { - "label": "Forçar sem perdas", - "description": "Forçar codificação de imagem sem perdas (maior qualidade, mais largura de banda)" - }, - "enableWallpaper": { - "label": "Fundo de ecrã", - "description": "Mostrar fundo de ecrã (desativar melhora o desempenho)" - }, - "enableFontSmoothing": { - "label": "Suavização de Fontes", - "description": "Ativar renderização de fontes ClearType" - }, - "enableDesktopComposition": { - "label": "Composição do ambiente de trabalho", - "description": "Ativar efeitos de transparência Aero" - }, - "disableAudio": { - "label": "Desativar Áudio", - "description": "Silenciar todo o áudio da sessão remota" - }, - "enablePrinting": { - "label": "Ativar Impressão", - "description": "Redirecionar impressoras locais para a sessão remota" - }, - "enableDrive": { - "label": "Ativar Redirecionamento de Unidades", - "description": "Mapear uma pasta local como unidade na sessão remota" - }, - "disableCopy": { - "label": "Desativar Cópia", - "description": "Impedir a cópia de texto da sessão remota" - }, - "disablePaste": { - "label": "Desativar Colagem", - "description": "Impedir a colagem de texto na sessão remota" - }, - "group": "Configurações padrão da Área de Trabalho Remota", - "colorDepth16": "16 bits", - "colorDepth24": "24 bits", - "colorDepth32": "32 bits" - }, - "admin": { - "enabled": { - "label": "Ativar Área de Trabalho Remota", - "description": "Permitir ligações RDP, VNC e Telnet através do guacd." - }, - "guacdUrl": { - "label": "URL do guacd", - "description": "O local onde o guacd está a escutar é definido como host:porta. As variáveis de ambiente GUACD_URL, GUACD_HOST e GUACD_PORT têm precedência sobre isto.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "Porta RDP" - }, - "vncPort": { - "label": "Porta VNC" - }, - "telnetPort": { - "label": "porta Telnet" - }, - "rdpSecurity": { - "label": "Modo de segurança RDP" - }, - "rdpIgnoreCert": { - "label": "Ignorar erros de certificado" - }, - "guacamoleConfig": { - "label": "configurações do guacd" - }, - "enableToolbar": { - "label": "Barra de ferramentas da área de trabalho remota", - "description": "Visualizar a barra de ferramentas nas sessões RDP, VNC e Telnet." - }, - "displayGroup": "Ecrã", - "displayDefaultsTab": "Ambiente de trabalho remoto" - } - }, - "palette": { - "connectRdp": "Ligar com RDP", - "connectVnc": "Ligue-se com VNC", - "connectTelnet": "Ligue-se com Telnet" - }, - "permissions": { - "sessions": { - "title": "Partilhar sessões de ambiente de trabalho remoto", - "description": "Permitir que a partilha de sessões e as salas de colaboração mostrem as sessões RDP, VNC e Telnet deste utilizador a outros utilizadores." - } - }, - "errors": { - "remoteServerRequired": "É necessário um servidor remoto. Ligue um servidor remoto nas Definições para utilizar este tipo de ligação." - }, - "plugin": { - "name": "Ambiente de trabalho remoto" - } -} diff --git a/plugins/remote-desktop/locales/translated/ro_RO.json b/plugins/remote-desktop/locales/translated/ro_RO.json deleted file mode 100644 index 76b4a63cf..000000000 --- a/plugins/remote-desktop/locales/translated/ro_RO.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Se conectează la sesiunea {{type}}...", - "checkingGuacd": "Se verifică serviciul desktop la distanță...", - "requestingToken": "Se solicită tokenul de conexiune {{type}}...", - "openingSession": "Se deschide sesiunea {{type}}...", - "negotiating": "Se negociază o strângere de mână {{type}}...", - "sessionReady": "{{type}} sesiune gata", - "connectionError": "Eroare de conexiune", - "connectionFailed": "Conexiunea a eșuat", - "failedToConnect": "Nu s-a putut obține token-ul de conexiune", - "hostNotFound": "Gazdă negăsită", - "noHostSelected": "Nicio gazdă selectată", - "reconnect": "Reconectați-vă", - "retry": "Reîncercați", - "guacdUnavailable": "Serviciul desktop la distanță (guacd) nu este disponibil. Asigurați-vă că guacd rulează, este accesibil și configurat corect în setările de administrare.", - "credentialPromptTitle": "Introduceți acreditările RDP", - "credentialPromptDescription": "Această gazdă este configurată să solicite acreditări la conectare. Acestea sunt utilizate doar pentru această sesiune și nu sunt salvate.", - "connect": "Conectează-te", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Fișiere Drive", - "parent": "Directorul părinte", - "refresh": "Reîmprospăta", - "empty": "Acest folder este gol", - "listFailed": "Nu s-a putut citi acest folder", - "download": "Descărcare", - "downloadFailed": "Descărcarea {{name}} a eșuat", - "upload": "Încărcați fișiere", - "uploading": "Se încarcă {{name}}...", - "uploaded": "Încărcat {{name}}", - "uploadFailed": "Nu s-a putut încărca {{name}}", - "uploadDisabled": "Încărcările sunt dezactivate pentru această conexiune", - "driveUnavailable": "Activați redirecționarea unității RDP înainte de încărcarea fișierelor", - "driveNotWritable": "guacd nu poate scrie în folderul unității. Verificați GUACD_DRIVE_PATH (sau calea unității gazdă) și permisiunile acesteia.", - "dropToUpload": "Plasați fișierele pentru încărcare" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Ecran de blocare)", - "winKey": "Cheie Windows", - "superL": "Super+L (Ecran de blocare)", - "superKey": "Super Cheie", - "super": "Super", - "cmdL": "Cmd+L (Ecran de blocare)", - "cmdKey": "Tastă de comandă", - "cmd": "Comandă", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Schimbare", - "win": "Câştiga", - "stickyActive": "{{key}} (blocat - clic pentru eliberare)", - "stickyInactive": "{{key}} (clic pentru blocare)", - "esc": "Evadare", - "tab": "Tab", - "home": "Acasă", - "end": "Sfârşit", - "pageUp": "Pagină sus", - "pageDown": "Pagină în jos", - "arrowUp": "Săgeată în sus", - "arrowDown": "Săgeată în jos", - "arrowLeft": "Săgeată stânga", - "arrowRight": "Săgeată dreapta", - "fnToggle": "Taste funcționale", - "reconnect": "Sesiune de reconectare", - "collapse": "Restrângeți bara de instrumente", - "expand": "Extindeți bara de instrumente", - "dragHandle": "Trageți pentru a repoziționa", - "hide": "Ascundeți pentru această sesiune. Pentru a o dezactiva definitiv, deschideți Setări gazdă → General → Bara de instrumente conexiune.", - "zoomIn": "Măriți", - "zoomOut": "Micșorare", - "resetZoom": "Resetare zoom", - "fitToScreen": "Potrivire la ecran", - "switchToTrackpad": "Comutare la modul trackpad (tragere pentru mutarea cursorului, atingere pentru clic)", - "switchToTouch": "Comutare la modul tactil (atingere direct unde doriți să faceți clic)" - }, - "disabled": "Desktopul la distanță este dezactivat. Un administrator îl poate activa în Setări." - }, - "hosts": { - "nativeRdpOpened": "Deschis în Desktop la distanță Windows", - "nativeRdpFailed": "Nu s-a putut deschide Desktop la distanță Windows", - "openNativeRdp": "Deschideți în Desktop la distanță Windows", - "guac": { - "toolbar": "Bara de instrumente", - "connection": "Conexiune", - "authentication": "Autentificare", - "storedCredential": "Acreditare stocată", - "authMethod": "Metodă de autentificare", - "authTypeDirect": "Direct", - "authTypeCredential": "Acreditare", - "authTypeNone": "Nici unul", - "authTypeNoneDesc": "Nu sunt stocate acreditări. Vi se va solicita un nume de utilizator și o parolă de fiecare dată când vă conectați; acestea nu sunt salvate.", - "selectCredential": "Selectați o acreditare...", - "connectionSettings": "Setări de conectare", - "displaySettings": "Setări de afișare", - "audioSettings": "Setări audio", - "rdpPerformance": "Performanța RDP", - "deviceRedirection": "Redirecționare dispozitiv", - "session": "Sesiune", - "gateway": "Poartă de acces", - "remoteApp": "RemoteApp", - "clipboard": "Clipboard", - "sessionRecording": "Înregistrarea sesiunii", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Setări VNC", - "terminalSettings": "Setări terminal", - "rdpPort": "Port RDP", - "username": "Nume de utilizator", - "password": "Parolă", - "passwordSaved": "Parolă salvată, tastați pentru a o modifica", - "domain": "Domeniu", - "securityMode": "Mod de securitate", - "width": "Lăţime", - "height": "Înălţime", - "dpi": "DPI", - "clientName": "Numele clientului", - "initialProgram": "Program inițial", - "serverLayout": "Aspectul serverului", - "timezone": "Fus orar", - "loadBalanceInfo": "Informații despre echilibrul încărcării / Cookie", - "loadBalanceInfoDesc": "Cookie-ul RD Connection Broker pentru echilibrarea încărcării fermei RDS (de exemplu, tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Proxy guacd", - "guacdHostname": "Gazdă guacd", - "guacdHostnamePlaceholder": "Implicit global", - "guacdPort": "Portul Guacd", - "guacdProxyDesc": "Suprascrie instanța globală guacd pentru această conexiune. Lasă câmpul necompletat pentru a utiliza guacd-ul configurat global.", - "gatewayHostname": "Nume gazdă gateway", - "gatewayPort": "Portul Gateway", - "gatewayUsername": "Nume de utilizator Gateway", - "gatewayPassword": "Parolă gateway", - "gatewayDomain": "Domeniu Gateway", - "remoteAppProgram": "Programul RemoteApp", - "workingDirectory": "Director de lucru", - "arguments": "Argumente", - "normalizeLineEndings": "Normalizarea sfârșitului de linie", - "macAddress": "Adresă MAC", - "broadcastAddress": "Adresă de difuzare", - "udpPort": "Port UDP", - "waitTimeS": "Timp de așteptare (s)", - "driveName": "Nume unitate", - "drivePath": "Cale de acces", - "ignoreCertificate": "Ignoră certificatul", - "ignoreCertificateDesc": "Permiteți conexiuni la gazde cu certificate autosemnate", - "forceLossless": "Forța fără pierderi", - "forceLosslessDesc": "Forțează codificarea imaginilor fără pierderi (calitate mai mare, lățime de bandă mai mare)", - "disableAudio": "Dezactivați sunetul", - "disableAudioDesc": "Dezactivați tot sunetul din sesiunea la distanță", - "enableAudioInput": "Activare intrare audio (microfon)", - "enableAudioInputDesc": "Redirecționează microfonul local către sesiunea la distanță", - "wallpaper": "Tapet", - "wallpaperDesc": "Afișează imaginea de fundal a desktopului (dezactivarea îmbunătățește performanța)", - "theming": "Tematizare", - "themingDesc": "Activează teme și stiluri vizuale", - "fontSmoothing": "Netezirea fonturilor", - "fontSmoothingDesc": "Activează randarea fonturilor ClearType", - "fullWindowDrag": "Tragere pe toată ferestra", - "fullWindowDragDesc": "Afișează conținutul ferestrei la tragere", - "desktopComposition": "Compoziție desktop", - "desktopCompositionDesc": "Activează efectele Aero Glass", - "menuAnimations": "Animații de meniu", - "menuAnimationsDesc": "Activează estomparea meniului și animațiile diapozitivelor", - "disableBitmapCaching": "Dezactivați memorarea în cache a bitmap-urilor", - "disableBitmapCachingDesc": "Dezactivați memoria cache bitmap (poate ajuta la rezolvarea erorilor)", - "disableOffscreenCaching": "Dezactivați memoria cache în afara ecranului", - "disableOffscreenCachingDesc": "Dezactivați memoria cache în afara ecranului", - "disableGlyphCaching": "Dezactivați memoria cache a glifelor", - "disableGlyphCachingDesc": "Dezactivați memoria cache a glifurilor", - "enableGfx": "Activează efectele grafice", - "enableGfxDesc": "Folosește canalul grafic RemoteFX", - "enablePrinting": "Activează imprimarea", - "enablePrintingDesc": "Redirecționează imprimantele locale către sesiunea la distanță", - "enableDriveRedirection": "Activați redirecționarea unității", - "enableDriveRedirectionDesc": "Maparea unui folder local ca unitate în sesiunea la distanță", - "createDrivePath": "Creați o cale de acces", - "createDrivePathDesc": "Creează automat folderul dacă acesta nu există", - "disableDownload": "Dezactivați descărcarea", - "disableDownloadDesc": "Împiedicați descărcarea fișierelor din sesiunea la distanță", - "disableUpload": "Dezactivați încărcarea", - "disableUploadDesc": "Împiedicați încărcarea fișierelor în sesiunea la distanță", - "enableTouch": "Activează atingerea", - "enableTouchDesc": "Activează redirecționarea prin introducerea tactilă", - "consoleSession": "Sesiune consolă", - "consoleSessionDesc": "Conectați-vă la consolă (sesiunea 0) în loc de o sesiune nouă", - "sendWolPacket": "Trimiteți pachetul WOL", - "sendWolPacketDesc": "Trimiteți un pachet magic pentru a activa această gazdă înainte de conectare", - "disableCopy": "Dezactivați copierea", - "disableCopyDesc": "Împiedicați copierea textului din sesiunea la distanță", - "disablePaste": "Dezactivați lipirea", - "disablePasteDesc": "Împiedicați lipirea textului în sesiunea la distanță", - "createPathIfMissing": "Creați o cale dacă lipsește", - "createPathIfMissingDesc": "Creați automat directorul de înregistrare", - "excludeOutput": "Excludeți ieșirea", - "excludeOutputDesc": "Nu înregistrați ieșirea ecranului (doar metadate)", - "excludeMouse": "Excludeți mouse-ul", - "excludeMouseDesc": "Nu înregistrați mișcările mouse-ului", - "includeKeystrokes": "Includeți apăsările de taste", - "includeKeystrokesDesc": "Înregistrați apăsările de taste brute pe lângă ieșirea de pe ecran", - "vncPort": "Port VNC", - "vncPassword": "Parolă VNC", - "vncUsernameOptional": "Nume de utilizator (opțional)", - "vncLeaveBlank": "Lăsați necompletat dacă nu este necesar", - "cursorMode": "Mod cursor", - "swapRedBlue": "Schimbă roșu/albastru", - "swapRedBlueDesc": "Schimbarea canalelor de culoare roșu și albastru (rezolvă unele probleme de culoare)", - "readOnly": "Doar citire", - "readOnlyDesc": "Vizualizați ecranul de la distanță fără a trimite nicio intrare", - "telnetPort": "Port Telnet", - "terminalType": "Tipul terminalului", - "fontName": "Nume font", - "fontSize": "Dimensiunea fontului", - "colorScheme": "Schemă de culori", - "backspaceKey": "Tasta Backspace", - "colorDepth": "Adâncimea culorii", - "resizeMethod": "Metodă de redimensionare", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Orice", - "auto": "Auto", - "nlaExtended": "NLA extins", - "color8": "8 biți", - "color16": "16 biți", - "color24": "24 de biți", - "color32": "32 de biți", - "displayUpdate": "Actualizare afișare", - "reconnect": "Reconectați-vă", - "preserve": "Păstrați", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Local", - "remote": "De la distanță", - "failsafe": "siguranță", - "blackOnWhite": "Negru pe alb", - "whiteOnBlack": "Alb pe negru", - "grayOnBlack": "Gri pe negru", - "greenOnBlack": "Verde pe negru" - } - }, - "remoteDesktop": "Desktop la distanță", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Rețea virtuală", - "unencryptedShell": "Shell necriptat", - "remoteDesktopNotes": "Note", - "userDefaultsNote": "Setările pe care le lăsați neschimbate respectă valorile implicite ale gazdei.", - "connectionOriginNote": "În aplicația desktop, RDP, VNC și Telnet folosesc serverul la distanță, cu excepția cazului în care gazda este setată la Acest dispozitiv, care necesită un guacd pe care acest computer îl poate accesa. Setați unul în setările Desktop la distanță sau pentru fiecare gazdă în secțiunea Proxy guacd.", - "docsLink": "Vizualizați documentele", - "connectionOriginGuacamoleNote": "RDP, VNC și Telnet folosesc serverul la distanță, cu excepția cazului în care selectați „Acest dispozitiv”, care necesită un guacd pe care această mașină îl poate accesa. Setați unul în setarea URL guacd sau pentru fiecare gazdă în guacd Proxy." - }, - "onboarding": { - "feature_desktop": "Desktop la distanță", - "feature_desktop_desc": "Deschideți sesiuni RDP, VNC și Telnet într-o filă, la fel ca în cazul SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Adâncimea culorii", - "description": "Folosit pentru gazdele RDP care îl lasă pe setările implicite." - }, - "resizeMethod": { - "label": "Metodă de redimensionare", - "description": "Cum urmărește ecranul de la distanță dimensiunea ferestrei." - }, - "resizeDisplayUpdate": "Actualizare afișare", - "resizeReconnect": "Reconectați-vă", - "inherit": "Moşteni", - "on": "Pe", - "off": "Dezactivat", - "forceLossless": { - "label": "Forța fără pierderi", - "description": "Forțează codificarea imaginilor fără pierderi (calitate mai mare, lățime de bandă mai mare)" - }, - "enableWallpaper": { - "label": "Tapet", - "description": "Afișează imaginea de fundal a desktopului (dezactivarea îmbunătățește performanța)" - }, - "enableFontSmoothing": { - "label": "Netezirea fonturilor", - "description": "Activează randarea fonturilor ClearType" - }, - "enableDesktopComposition": { - "label": "Compoziție desktop", - "description": "Activează efectele Aero Glass" - }, - "disableAudio": { - "label": "Dezactivați sunetul", - "description": "Dezactivați tot sunetul din sesiunea la distanță" - }, - "enablePrinting": { - "label": "Activează imprimarea", - "description": "Redirecționează imprimantele locale către sesiunea la distanță" - }, - "enableDrive": { - "label": "Activați redirecționarea unității", - "description": "Maparea unui folder local ca unitate în sesiunea la distanță" - }, - "disableCopy": { - "label": "Dezactivați copierea", - "description": "Împiedicați copierea textului din sesiunea la distanță" - }, - "disablePaste": { - "label": "Dezactivați lipirea", - "description": "Împiedicați lipirea textului în sesiunea la distanță" - }, - "group": "Setări implicite Desktop la distanță", - "colorDepth16": "16 biți", - "colorDepth24": "24 de biți", - "colorDepth32": "32 de biți" - }, - "admin": { - "enabled": { - "label": "Activează Desktop la distanță", - "description": "Permite conexiuni RDP, VNC și Telnet prin guacd." - }, - "guacdUrl": { - "label": "URL-ul guacd", - "description": "Unde guacd ascultă, ca host:port. Variabilele de mediu GUACD_URL, GUACD_HOST și GUACD_PORT suprascriu acest lucru.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "Port RDP" - }, - "vncPort": { - "label": "Port VNC" - }, - "telnetPort": { - "label": "Port Telnet" - }, - "rdpSecurity": { - "label": "Mod de securitate RDP" - }, - "rdpIgnoreCert": { - "label": "Ignorați erorile de certificat" - }, - "guacamoleConfig": { - "label": "setări guacd" - }, - "enableToolbar": { - "label": "Bara de instrumente desktop la distanță", - "description": "Afișează bara de instrumente în sesiunile RDP, VNC și Telnet." - }, - "displayGroup": "Afişa", - "displayDefaultsTab": "Desktop la distanță" - } - }, - "palette": { - "connectRdp": "Conectați-vă cu RDP", - "connectVnc": "Conectați-vă cu VNC", - "connectTelnet": "Conectați-vă cu Telnet" - }, - "permissions": { - "sessions": { - "title": "Partajați sesiuni desktop la distanță", - "description": "Permiteți ca partajarea sesiunilor și camerele de colaborare să arate sesiunile RDP, VNC și Telnet ale acestui utilizator către ceilalți." - } - }, - "errors": { - "remoteServerRequired": "Este necesar un server la distanță. Conectați un server la distanță în Setări pentru a utiliza acest tip de conexiune." - }, - "plugin": { - "name": "Desktop la distanță" - } -} diff --git a/plugins/remote-desktop/locales/translated/ru_RU.json b/plugins/remote-desktop/locales/translated/ru_RU.json deleted file mode 100644 index 338f9206b..000000000 --- a/plugins/remote-desktop/locales/translated/ru_RU.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Подключение к сессии {{type}}...", - "checkingGuacd": "Проверка службы удаленного рабочего стола...", - "requestingToken": "Запрос токена подключения {{type}}...", - "openingSession": "Открытие сессии {{type}}...", - "negotiating": "Переговоры {{type}} рукопожатие...", - "sessionReady": "{{type}} сессия готова", - "connectionError": "Ошибка подключения", - "connectionFailed": "Ошибка подключения", - "failedToConnect": "Не удалось получить токен подключения", - "hostNotFound": "Хост не найден", - "noHostSelected": "Хост не выбран", - "reconnect": "Переподключиться", - "retry": "Повторить", - "guacdUnavailable": "Служба удалённого рабочего стола (guacd) недоступна. Убедитесь, что guacd запущен, доступен и правильно настроен в настройках администратора.", - "credentialPromptTitle": "Введите учетные данные RDP.", - "credentialPromptDescription": "На этом хосте настроен запрос учетных данных при подключении. Они используются только в течение этой сессии и не сохраняются.", - "connect": "Соединить", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Файлы диска", - "parent": "Родительский каталог", - "refresh": "Обновить", - "empty": "Эта папка пуста", - "listFailed": "Не удалось прочитать эту папку.", - "download": "Скачать", - "downloadFailed": "Не удалось загрузить {{name}}", - "upload": "Загрузка файлов", - "uploading": "Загрузка {{name}}...", - "uploaded": "Загружено {{name}}", - "uploadFailed": "Не удалось загрузить {{name}}", - "uploadDisabled": "Загрузка файлов для этого соединения отключена.", - "driveUnavailable": "Включите перенаправление дисков RDP перед загрузкой файлов.", - "driveNotWritable": "guacd не может записывать данные в папку на диске. Проверьте переменную GUACD_DRIVE_PATH (или путь к диску на хосте) и ее права доступа.", - "dropToUpload": "Перетащите файлы для загрузки" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Блокировка экрана)", - "winKey": "Клавиша Windows", - "superL": "Super+L (экран блокировки)", - "superKey": "Суперклавиша", - "super": "Супер", - "cmdL": "Cmd+L (Блокировка экрана)", - "cmdKey": "Клавиша Command", - "cmd": "Команда", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}} (зафиксирована – кликните для отключения)", - "stickyInactive": "{{key}} (кликните для фиксации)", - "esc": "Escape", - "tab": "Tab", - "home": "Главная", - "end": "End", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "Вверх", - "arrowDown": "Вниз", - "arrowLeft": "Влево", - "arrowRight": "Вправо", - "fnToggle": "Функциональные клавиши", - "reconnect": "Переподключить сеанс", - "collapse": "Свернуть панель", - "expand": "Развернуть панель", - "dragHandle": "Перетащите для перемещения", - "hide": "Скрыть на время текущей сессии. Чтобы отключить эту функцию навсегда, откройте «Настройки хоста» → «Общие» → «Панель подключения».", - "zoomIn": "Увеличить масштаб", - "zoomOut": "Отдалиться", - "resetZoom": "Сбросить масштаб", - "fitToScreen": "Подогнать под экран", - "switchToTrackpad": "Переключитесь в режим трекпада (перетащите для перемещения курсора, коснитесь для щелчка).", - "switchToTouch": "Переключитесь в сенсорный режим (коснитесь непосредственно того места, куда хотите нажать)." - }, - "disabled": "Функция удаленного рабочего стола отключена. Администратор может включить ее в настройках." - }, - "hosts": { - "nativeRdpOpened": "Открыто в Windows Remote Desktop", - "nativeRdpFailed": "Не удалось открыть удаленный рабочий стол Windows.", - "openNativeRdp": "Открыть в удаленном рабочем столе Windows", - "guac": { - "toolbar": "Панель инструментов", - "connection": "Подключение", - "authentication": "Аутентификация", - "storedCredential": "Сохраненные учетные данные", - "authMethod": "Метод аутентификации", - "authTypeDirect": "Прямой", - "authTypeCredential": "Учетные данные", - "authTypeNone": "Ни одного", - "authTypeNoneDesc": "Учетные данные не сохраняются. При каждом подключении вам будет предлагаться ввести имя пользователя и пароль; они не сохраняются.", - "selectCredential": "Выберите учетные данные...", - "connectionSettings": "Параметры подключения", - "displaySettings": "Параметры отображения", - "audioSettings": "Параметры звука", - "rdpPerformance": "Производительность RDP", - "deviceRedirection": "Перенаправление устройств", - "session": "Сессия", - "gateway": "Шлюз", - "remoteApp": "RemoteApp", - "clipboard": "Буфер обмена", - "sessionRecording": "Запись сессии", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Параметры VNC", - "terminalSettings": "Параметры терминала", - "rdpPort": "Порт RDP", - "username": "Имя пользователя", - "password": "Пароль", - "passwordSaved": "Пароль сохранен, введите для изменения.", - "domain": "Домен", - "securityMode": "Режим безопасности", - "width": "Ширина", - "height": "Высота", - "dpi": "DPI", - "clientName": "Имя клиента", - "initialProgram": "Начальная программа", - "serverLayout": "Раскладка сервера", - "timezone": "Часовой пояс", - "loadBalanceInfo": "Информация о балансировке нагрузки / Cookie", - "loadBalanceInfoDesc": "Cookie брокера подключений к удаленному рабочему столу для балансировки нагрузки фермы RDS (например, tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Прокси guacd", - "guacdHostname": "Хост guacd", - "guacdHostnamePlaceholder": "Глобальное значение по умолчанию", - "guacdPort": "Порт guacd", - "guacdProxyDesc": "Переопределить глобальный экземпляр guacd для этого соединения. Оставьте пустым для использования глобально настроенного guacd.", - "gatewayHostname": "Имя хоста шлюза", - "gatewayPort": "Порт шлюза", - "gatewayUsername": "Имя пользователя шлюза", - "gatewayPassword": "Пароль шлюза", - "gatewayDomain": "Домен шлюза", - "remoteAppProgram": "Программа RemoteApp", - "workingDirectory": "Рабочий каталог", - "arguments": "Аргументы", - "normalizeLineEndings": "Нормализовать окончания строк", - "macAddress": "MAC-адрес", - "broadcastAddress": "Широковещательный адрес", - "udpPort": "Порт UDP", - "waitTimeS": "Время ожидания (с)", - "driveName": "Имя диска", - "drivePath": "Путь к диску", - "ignoreCertificate": "Игнорировать сертификат", - "ignoreCertificateDesc": "Разрешить подключения к хостам с самоподписанными сертификатами", - "forceLossless": "Принудительно без потерь", - "forceLosslessDesc": "Принудительное кодирование изображений без потерь (более высокое качество, больший расход трафика)", - "disableAudio": "Отключить звук", - "disableAudioDesc": "Отключить весь звук в удаленном сеансе", - "enableAudioInput": "Включить аудиовход (микрофон)", - "enableAudioInputDesc": "Передавать локальный микрофон в удаленный сеанс", - "wallpaper": "Обои", - "wallpaperDesc": "Показывать обои рабочего стола (отключение повышает производительность)", - "theming": "Темы", - "themingDesc": "Включить визуальные темы и стили", - "fontSmoothing": "Сглаживание шрифтов", - "fontSmoothingDesc": "Включить рендеринг шрифтов ClearType", - "fullWindowDrag": "Перетаскивание окон с содержимым", - "fullWindowDragDesc": "Показывать содержимое окна при перетаскивании", - "desktopComposition": "Композиция рабочего стола", - "desktopCompositionDesc": "Включить эффекты Aero Glass", - "menuAnimations": "Анимации меню", - "menuAnimationsDesc": "Включить анимацию исчезновения и сдвига меню", - "disableBitmapCaching": "Отключить кеширование растровых изображений", - "disableBitmapCachingDesc": "Отключить кеш растровых изображений (может помочь при сбоях)", - "disableOffscreenCaching": "Отключить внеэкранное кеширование", - "disableOffscreenCachingDesc": "Отключить внеэкранный кеш", - "disableGlyphCaching": "Отключить кеширование глифов", - "disableGlyphCachingDesc": "Отключить кеш глифов", - "enableGfx": "Включить GFX", - "enableGfxDesc": "Использовать графический конвейер RemoteFX", - "enablePrinting": "Включить печать", - "enablePrintingDesc": "Перенаправлять локальные принтеры в удаленный сеанс", - "enableDriveRedirection": "Включить перенаправление дисков", - "enableDriveRedirectionDesc": "Подключать локальную папку как диск в удаленном сеансе", - "createDrivePath": "Создать путь к диску", - "createDrivePathDesc": "Автоматически создать папку, если она не существует", - "disableDownload": "Отключить загрузку", - "disableDownloadDesc": "Запретить загрузку файлов из удаленного сеанса", - "disableUpload": "Отключить выгрузку", - "disableUploadDesc": "Запретить выгрузку файлов в удаленный сеанс", - "enableTouch": "Включить сенсорный ввод", - "enableTouchDesc": "Включить передачу сенсорного ввода", - "consoleSession": "Консольный сеанс", - "consoleSessionDesc": "Подключиться к консоли (сеанс 0) вместо создания нового сеанса", - "sendWolPacket": "Отправить WOL-пакет", - "sendWolPacketDesc": "Отправить magic-пакет для пробуждения хоста перед подключением", - "disableCopy": "Отключить копирование", - "disableCopyDesc": "Запретить копирование текста из удаленного сеанса", - "disablePaste": "Отключить вставку", - "disablePasteDesc": "Запретить вставку текста в удаленный сеанс", - "createPathIfMissing": "Создать путь, если отсутствует", - "createPathIfMissingDesc": "Автоматически создать каталог для записей", - "excludeOutput": "Исключить вывод", - "excludeOutputDesc": "Не записывать вывод экрана (только метаданные)", - "excludeMouse": "Исключить мышь", - "excludeMouseDesc": "Не записывать движения мыши", - "includeKeystrokes": "Записывать нажатия", - "includeKeystrokesDesc": "Записывать необработанные нажатия клавиш в дополнение к выводу экрана", - "vncPort": "Порт VNC", - "vncPassword": "Пароль VNC", - "vncUsernameOptional": "Имя пользователя (необязательно)", - "vncLeaveBlank": "Оставьте пустым, если не требуется", - "cursorMode": "Режим курсора", - "swapRedBlue": "Поменять красный/синий", - "swapRedBlueDesc": "Обменять красный и синий цветовые каналы (исправляет некоторые проблемы с цветом)", - "readOnly": "Только чтение", - "readOnlyDesc": "Просмотр удаленного экрана без отправки ввода", - "telnetPort": "Порт Telnet", - "terminalType": "Тип терминала", - "fontName": "Название шрифта", - "fontSize": "Размер шрифта", - "colorScheme": "Цветовая схема", - "backspaceKey": "Клавиша Backspace", - "colorDepth": "Глубина цвета", - "resizeMethod": "Метод изменения размера", - "driveNamePlaceholder": "Термикс Драйв", - "options": { - "any": "Любой", - "auto": "Авто", - "nlaExtended": "NLA Extended", - "color8": "8-битный", - "color16": "16-бит", - "color24": "24-бит", - "color32": "32-битный", - "displayUpdate": "Обновление дисплея", - "reconnect": "Переподключиться", - "preserve": "Сохранять", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Локальная", - "remote": "Удалённый", - "failsafe": "отказоустойчивость", - "blackOnWhite": "Черное на белом", - "whiteOnBlack": "Белый на чёрном", - "grayOnBlack": "Серый на чёрном", - "greenOnBlack": "Зеленый на черном" - } - }, - "remoteDesktop": "Удаленный рабочий стол", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Виртуальная сеть", - "unencryptedShell": "Незашифрованная оболочка", - "remoteDesktopNotes": "Заметки", - "userDefaultsNote": "Настройки, которые вы оставляете без изменений, соответствуют настройкам по умолчанию вашего хоста.", - "connectionOriginNote": "В настольном приложении RDP, VNC и Telnet используют удаленный сервер, если только для хоста не установлен параметр «Это устройство», для которого требуется guacd, к которому может подключиться эта машина. Задайте его в настройках удаленного рабочего стола или для каждого хоста в разделе guacd Proxy.", - "docsLink": "Документация", - "connectionOriginGuacamoleNote": "RDP, VNC и Telnet используют удаленный сервер, если вы не выберете «Это устройство», для которого требуется guacd, к которому может подключиться эта машина. Задайте его в настройках URL-адреса guacd или для каждого хоста в разделе «Прокси guacd»." - }, - "onboarding": { - "feature_desktop": "Удаленный рабочий стол", - "feature_desktop_desc": "Открывайте сеансы RDP, VNC и Telnet во вкладке, как и SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Глубина цвета", - "description": "Используется для RDP-хостов, которые оставляют этот параметр по умолчанию." - }, - "resizeMethod": { - "label": "Метод изменения размера", - "description": "Как удаленный экран подстраивается под размер окна." - }, - "resizeDisplayUpdate": "Обновление дисплея", - "resizeReconnect": "Переподключиться", - "inherit": "Наследовать", - "on": "Включено", - "off": "Отключено", - "forceLossless": { - "label": "Принудительно без потерь", - "description": "Принудительное кодирование изображений без потерь (более высокое качество, больший расход трафика)" - }, - "enableWallpaper": { - "label": "Обои", - "description": "Показывать обои рабочего стола (отключение повышает производительность)" - }, - "enableFontSmoothing": { - "label": "Сглаживание шрифтов", - "description": "Включить рендеринг шрифтов ClearType" - }, - "enableDesktopComposition": { - "label": "Композиция рабочего стола", - "description": "Включить эффекты Aero Glass" - }, - "disableAudio": { - "label": "Отключить звук", - "description": "Отключить весь звук в удаленном сеансе" - }, - "enablePrinting": { - "label": "Включить печать", - "description": "Перенаправлять локальные принтеры в удаленный сеанс" - }, - "enableDrive": { - "label": "Включить перенаправление дисков", - "description": "Подключать локальную папку как диск в удаленном сеансе" - }, - "disableCopy": { - "label": "Отключить копирование", - "description": "Запретить копирование текста из удаленного сеанса" - }, - "disablePaste": { - "label": "Отключить вставку", - "description": "Запретить вставку текста в удаленный сеанс" - }, - "group": "Настройки удаленного рабочего стола по умолчанию", - "colorDepth16": "16-бит", - "colorDepth24": "24-бит", - "colorDepth32": "32-битный" - }, - "admin": { - "enabled": { - "label": "Включить удалённый рабочий стол", - "description": "Разрешите подключения RDP, VNC и Telnet через guacd." - }, - "guacdUrl": { - "label": "URL guacd", - "description": "guacd прослушивает порты и хосты. Переменные окружения GUACD_URL, GUACD_HOST и GUACD_PORT переопределяют это.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "порт RDP" - }, - "vncPort": { - "label": "порт VNC" - }, - "telnetPort": { - "label": "порт Telnet" - }, - "rdpSecurity": { - "label": "режим безопасности RDP" - }, - "rdpIgnoreCert": { - "label": "Игнорировать ошибки сертификатов" - }, - "guacamoleConfig": { - "label": "настройки guacd" - }, - "enableToolbar": { - "label": "Панель инструментов удаленного рабочего стола", - "description": "Отображение панели инструментов в сеансах RDP, VNC и Telnet." - }, - "displayGroup": "Отображать", - "displayDefaultsTab": "Удаленный рабочий стол" - } - }, - "palette": { - "connectRdp": "Подключитесь к RDP", - "connectVnc": "Подключитесь к VNC", - "connectTelnet": "Подключитесь через Telnet" - }, - "permissions": { - "sessions": { - "title": "Совместное использование сеансов удалённого рабочего стола", - "description": "Разрешите совместное использование сессий и комнаты для совместной работы отображать сессии этого пользователя по протоколам RDP, VNC и Telnet другим пользователям." - } - }, - "errors": { - "remoteServerRequired": "Требуется удаленный сервер. Для использования этого типа подключения подключите удаленный сервер в настройках." - }, - "plugin": { - "name": "Удаленный рабочий стол" - } -} diff --git a/plugins/remote-desktop/locales/translated/sr_SP.json b/plugins/remote-desktop/locales/translated/sr_SP.json deleted file mode 100644 index 1d8659525..000000000 --- a/plugins/remote-desktop/locales/translated/sr_SP.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Повезивање са {{type}} сесијом...", - "checkingGuacd": "Провера услуге удаљене радне површине...", - "requestingToken": "Захтева се {{type}} токен за повезивање...", - "openingSession": "Отварање {{type}} сесије...", - "negotiating": "Преговарање {{type}} руковање...", - "sessionReady": "{{type}} сесија спремна", - "connectionError": "Грешка у повезивању", - "connectionFailed": "Повезивање није успело", - "failedToConnect": "Није успело добијање токена за повезивање", - "hostNotFound": "Хост није пронађен", - "noHostSelected": "Није изабран домаћин", - "reconnect": "Поново се повежите", - "retry": "Покушај поново", - "guacdUnavailable": "Услуга удаљеног рачунара (guacd) није доступна. Молимо вас да се уверите да је guacd покренут, доступан и правилно конфигурисан у администраторским подешавањима.", - "credentialPromptTitle": "Унесите RDP акредитиве", - "credentialPromptDescription": "Овај хост је подешен да тражи акредитиве при повезивању. Они се користе само за ову сесију и не чувају се.", - "connect": "Повежи се", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Датотеке на Диску", - "parent": "Надређени директоријум", - "refresh": "Освежи", - "empty": "Ова фасцикла је празна", - "listFailed": "Није успело читање ове фасцикле", - "download": "Преузми", - "downloadFailed": "Преузимање није успело {{name}}", - "upload": "Отпреми датотеке", - "uploading": "Отпремање {{name}}...", - "uploaded": "Отпремљено {{name}}", - "uploadFailed": "Није успело отпремање {{name}}", - "uploadDisabled": "Отпремања су онемогућена за ову везу", - "driveUnavailable": "Омогућите преусмеравање RDP диска пре отпремања датотека", - "driveNotWritable": "guacd не може да пише у фасциклу диска. Проверите GUACD_DRIVE_PATH (или путању диска хоста) и његове дозволе.", - "dropToUpload": "Отпустите датотеке да бисте их отпремили" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Закључавање екрана)", - "winKey": "Windows тастер", - "superL": "Супер+Л (Закључани екран)", - "superKey": "Супер кључ", - "super": "Супер", - "cmdL": "Cmd+L (Закључани екран)", - "cmdKey": "Командни тастер", - "cmd": "Команда", - "ctrl": "Ctrl", - "alt": "Алтернативно", - "shift": "Померање", - "win": "Победа", - "stickyActive": "{{key}} (закључано - кликните да бисте отпустили)", - "stickyInactive": "{{key}} (кликните да бисте закључали)", - "esc": "Бег", - "tab": "Таб", - "home": "Дом", - "end": "Крај", - "pageUp": "Страница нагоре", - "pageDown": "Страница надоле", - "arrowUp": "Стрелица нагоре", - "arrowDown": "Стрелица надоле", - "arrowLeft": "Стрелица лево", - "arrowRight": "Стрелица десно", - "fnToggle": "Функцијски тастери", - "reconnect": "Поновно повезивање сесије", - "collapse": "Савиј траку са алаткама", - "expand": "Прошири траку са алаткама", - "dragHandle": "Превуците да бисте променили положај", - "hide": "Сакриј за ову сесију. Да бисте га трајно онемогућили, отворите Подешавања хоста → Опште → Трака са алаткама за повезивање.", - "zoomIn": "Увећај", - "zoomOut": "Умањи", - "resetZoom": "Ресетуј зумирање", - "fitToScreen": "Прилагоди екрану", - "switchToTrackpad": "Пребаците се у режим додирне табле (превуците да бисте померили курсор, додирните да бисте кликнули)", - "switchToTouch": "Пребаците се на режим додира (додирните директно место где желите да кликнете)" - }, - "disabled": "Удаљена радна површина је искључена. Администратор је може укључити у подешавањима." - }, - "hosts": { - "nativeRdpOpened": "Отворено у Windows удаљеној радној површини", - "nativeRdpFailed": "Није успело отварање Windows удаљене радне површине", - "openNativeRdp": "Отвори у Windows удаљеној радној површини", - "guac": { - "toolbar": "Трака са алаткама", - "connection": "Веза", - "authentication": "Аутентификација", - "storedCredential": "Сачувани акредитив", - "authMethod": "Метода овлашћења", - "authTypeDirect": "Директно", - "authTypeCredential": "Акредитив", - "authTypeNone": "Ниједан", - "authTypeNoneDesc": "Акредитиви се не чувају. Сваки пут када се повежете, биће вам затражено корисничко име и лозинка; они се не чувају.", - "selectCredential": "Изаберите акредитив...", - "connectionSettings": "Подешавања везе", - "displaySettings": "Подешавања екрана", - "audioSettings": "Подешавања звука", - "rdpPerformance": "Перформансе РДП-а", - "deviceRedirection": "Преусмеравање уређаја", - "session": "Сесија", - "gateway": "Капија", - "remoteApp": "Удаљена апликација", - "clipboard": "Међуспремник", - "sessionRecording": "Снимање сесије", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC подешавања", - "terminalSettings": "Подешавања терминала", - "rdpPort": "РДП порт", - "username": "Корисничко име", - "password": "Лозинка", - "passwordSaved": "Лозинка сачувана, унесите текст да бисте је променили", - "domain": "Домен", - "securityMode": "Безбедносни режим", - "width": "Ширина", - "height": "Висина", - "dpi": "DPI", - "clientName": "Име клијента", - "initialProgram": "Почетни програм", - "serverLayout": "Распоред сервера", - "timezone": "Временска зона", - "loadBalanceInfo": "Информације о балансу оптерећења / колачић", - "loadBalanceInfoDesc": "Колачић RD Connection Broker-а за балансирање оптерећења RDS фарме (нпр. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd прокси", - "guacdHostname": "guacd хост", - "guacdHostnamePlaceholder": "Глобално подразумевано", - "guacdPort": "ГУАЦД порт", - "guacdProxyDesc": "Замените глобалну инстанцу guacd-а за ову везу. Оставите празно да бисте користили глобално конфигурисани guacd.", - "gatewayHostname": "Име хоста мрежног пролаза", - "gatewayPort": "Порт за пролаз", - "gatewayUsername": "Корисничко име на гејтвеју", - "gatewayPassword": "Лозинка за пролаз", - "gatewayDomain": "Домен капије", - "remoteAppProgram": "Програм RemoteApp", - "workingDirectory": "Радни директоријум", - "arguments": "Аргументи", - "normalizeLineEndings": "Нормализуј завршетке линија", - "macAddress": "MAC адреса", - "broadcastAddress": "Адреса за емитовање", - "udpPort": "UDP порт", - "waitTimeS": "Време чекања (s)", - "driveName": "Назив диска", - "drivePath": "Путања диска", - "ignoreCertificate": "Игнориши сертификат", - "ignoreCertificateDesc": "Дозволи везе са хостовима са самопотписаним сертификатима", - "forceLossless": "Присилно без губитака", - "forceLosslessDesc": "Присилно кодирање слике без губитака (виши квалитет, већи пропусни опсег)", - "disableAudio": "Онемогући звук", - "disableAudioDesc": "Искључите сав звук из удаљене сесије", - "enableAudioInput": "Омогући аудио улаз (микрофон)", - "enableAudioInputDesc": "Прослеђивање локалног микрофона на удаљену сесију", - "wallpaper": "Позадина", - "wallpaperDesc": "Прикажи позадину радне површине (онемогућавање побољшава перформансе)", - "theming": "Темирање", - "themingDesc": "Омогући визуелне теме и стилове", - "fontSmoothing": "Изглађивање фонта", - "fontSmoothingDesc": "Омогући приказивање ClearType фонта", - "fullWindowDrag": "Превлачење преко целог прозора", - "fullWindowDragDesc": "Приказ садржаја прозора током превлачења", - "desktopComposition": "Композиција на радној површини", - "desktopCompositionDesc": "Омогући ефекте Aero Glass-а", - "menuAnimations": "Анимације менија", - "menuAnimationsDesc": "Омогући анимације постепеног постепеног померања менија и слајдова", - "disableBitmapCaching": "Онемогући кеширање битмапа", - "disableBitmapCachingDesc": "Искључите кеш битмапа (може помоћи код грешака)", - "disableOffscreenCaching": "Онемогући кеширање ван екрана", - "disableOffscreenCachingDesc": "Искључите кеш меморију ван екрана", - "disableGlyphCaching": "Онемогући кеширање глифова", - "disableGlyphCachingDesc": "Искључите кеш глифова", - "enableGfx": "Омогући графичке ефекте", - "enableGfxDesc": "Користите графички цевовод RemoteFX", - "enablePrinting": "Омогући штампање", - "enablePrintingDesc": "Преусмерите локалне штампаче на удаљену сесију", - "enableDriveRedirection": "Омогући преусмеравање диска", - "enableDriveRedirectionDesc": "Мапирајте локалну фасциклу као диск у удаљеној сесији", - "createDrivePath": "Креирај путању диска", - "createDrivePathDesc": "Аутоматски креирај фасциклу ако не постоји", - "disableDownload": "Онемогући преузимање", - "disableDownloadDesc": "Спречите преузимање датотека са удаљене сесије", - "disableUpload": "Онемогући отпремање", - "disableUploadDesc": "Спречите отпремање датотека на удаљену сесију", - "enableTouch": "Омогући додир", - "enableTouchDesc": "Омогући преусмеравање уноса додиром", - "consoleSession": "Конзолна сесија", - "consoleSessionDesc": "Повежите се са конзолом (сесија 0) уместо нове сесије", - "sendWolPacket": "Пошаљи WOL пакет", - "sendWolPacketDesc": "Пошаљите магични пакет да пробудите овај хост пре повезивања", - "disableCopy": "Онемогући копирање", - "disableCopyDesc": "Спречите копирање текста из удаљене сесије", - "disablePaste": "Онемогући лепљење", - "disablePasteDesc": "Спречите лепљење текста у удаљену сесију", - "createPathIfMissing": "Креирај путању ако недостаје", - "createPathIfMissingDesc": "Аутоматски креирајте директоријум за снимање", - "excludeOutput": "Искључи излаз", - "excludeOutputDesc": "Не снимај излаз екрана (само метаподаци)", - "excludeMouse": "Искључи миш", - "excludeMouseDesc": "Не снимај покрете миша", - "includeKeystrokes": "Укључи притиске на тастатури", - "includeKeystrokesDesc": "Снимање сирових притиска тастера поред излаза на екран", - "vncPort": "VNC порт", - "vncPassword": "VNC лозинка", - "vncUsernameOptional": "Корисничко име (опционо)", - "vncLeaveBlank": "Оставите празно ако није потребно", - "cursorMode": "Режим курсора", - "swapRedBlue": "Замена црвене/плаве", - "swapRedBlueDesc": "Замена црвених и плавих канала боја (решава неке проблеме са бојама)", - "readOnly": "Само за читање", - "readOnlyDesc": "Погледајте удаљени екран без слања било каквог уноса", - "telnetPort": "Телнет порт", - "terminalType": "Тип терминала", - "fontName": "Назив фонта", - "fontSize": "Величина фонта", - "colorScheme": "Шема боја", - "backspaceKey": "Тастер Backspace", - "colorDepth": "Дубина боје", - "resizeMethod": "Метод промене величине", - "driveNamePlaceholder": "Термикс Драјв", - "options": { - "any": "Било који", - "auto": "Аутоматски", - "nlaExtended": "НЛА продужена", - "color8": "8-битни", - "color16": "16-битни", - "color24": "24-битни", - "color32": "32-битни", - "displayUpdate": "Ажурирање екрана", - "reconnect": "Поново се повежите", - "preserve": "Сачувај", - "unix": "Јуникс (ЛФ)", - "windows": "Виндоус (CRLF)", - "local": "Локално", - "remote": "Даљински", - "failsafe": "безбедан", - "blackOnWhite": "Црно на белом", - "whiteOnBlack": "Бело на црном", - "grayOnBlack": "Сиво на црном", - "greenOnBlack": "Зелено на црном" - } - }, - "remoteDesktop": "Удаљена радна површина", - "tabRdp": "РДП", - "tabVnc": "VNC", - "tabTelnet": "Телнет", - "virtualNetwork": "Виртуелна мрежа", - "unencryptedShell": "Нешифрована љуска", - "remoteDesktopNotes": "Белешке", - "userDefaultsNote": "Подешавања која оставите непромењена прате подразумеване вредности вашег хоста.", - "connectionOriginNote": "У десктоп апликацији, RDP, VNC и Telnet користе удаљени сервер осим ако хост није подешен на „Овај уређај“, чему је потребан guacd који ова машина може да досегне. Подесите га у подешавањима удаљене радне површине или по хосту под guacd прокси.", - "docsLink": "Прикажи документе", - "connectionOriginGuacamoleNote": "RDP, VNC и Telnet користе удаљени сервер осим ако не изаберете „Овај уређај“, за шта је потребан guacd који ова машина може да досегне. Подесите га у подешавању guacd URL-а или по хосту под guacd прокси." - }, - "onboarding": { - "feature_desktop": "Удаљена радна површина", - "feature_desktop_desc": "Отворите RDP, VNC и Telnet сесије у картици, исто као и SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Дубина боје", - "description": "Користи се за RDP хостове који га остављају на подразумеваним подешавањима." - }, - "resizeMethod": { - "label": "Метод промене величине", - "description": "Како удаљени екран прати величину прозора." - }, - "resizeDisplayUpdate": "Ажурирање екрана", - "resizeReconnect": "Поново се повежите", - "inherit": "Наследи", - "on": "Укључено", - "off": "Искључено", - "forceLossless": { - "label": "Присилно без губитака", - "description": "Присилно кодирање слике без губитака (виши квалитет, већи пропусни опсег)" - }, - "enableWallpaper": { - "label": "Позадина", - "description": "Прикажи позадину радне површине (онемогућавање побољшава перформансе)" - }, - "enableFontSmoothing": { - "label": "Изглађивање фонта", - "description": "Омогући приказивање ClearType фонта" - }, - "enableDesktopComposition": { - "label": "Композиција на радној површини", - "description": "Омогући ефекте Aero Glass-а" - }, - "disableAudio": { - "label": "Онемогући звук", - "description": "Искључите сав звук из удаљене сесије" - }, - "enablePrinting": { - "label": "Омогући штампање", - "description": "Преусмерите локалне штампаче на удаљену сесију" - }, - "enableDrive": { - "label": "Омогући преусмеравање диска", - "description": "Мапирајте локалну фасциклу као диск у удаљеној сесији" - }, - "disableCopy": { - "label": "Онемогући копирање", - "description": "Спречите копирање текста из удаљене сесије" - }, - "disablePaste": { - "label": "Онемогући лепљење", - "description": "Спречите лепљење текста у удаљену сесију" - }, - "group": "Подразумевана подешавања удаљене радне површине", - "colorDepth16": "16-битни", - "colorDepth24": "24-битни", - "colorDepth32": "32-битни" - }, - "admin": { - "enabled": { - "label": "Омогући удаљену радну површину", - "description": "Дозволите RDP, VNC и Telnet везе путем guacd-а." - }, - "guacdUrl": { - "label": "URL адреса guacd-а", - "description": "Где guacd слуша, као host:port. Променљиве окружења GUACD_URL, GUACD_HOST и GUACD_PORT ово поништавају.", - "placeholder": "гуацд:4822" - } - }, - "host": { - "enableRdp": { - "label": "РДП" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Телнет" - }, - "rdpPort": { - "label": "РДП порт" - }, - "vncPort": { - "label": "VNC порт" - }, - "telnetPort": { - "label": "Телнет порт" - }, - "rdpSecurity": { - "label": "RDP безбедносни режим" - }, - "rdpIgnoreCert": { - "label": "Игноришите грешке сертификата" - }, - "guacamoleConfig": { - "label": "подешавања GUACD-а" - }, - "enableToolbar": { - "label": "Трака са алаткама за удаљену радну површину", - "description": "Прикажи траку са алаткама у RDP, VNC и Telnet сесијама." - }, - "displayGroup": "Приказ", - "displayDefaultsTab": "Удаљена радна површина" - } - }, - "palette": { - "connectRdp": "Повежите се са RDP-ом", - "connectVnc": "Повежите се са VNC-ом", - "connectTelnet": "Повежите се са Телнетом" - }, - "permissions": { - "sessions": { - "title": "Делите сесије удаљене радне површине", - "description": "Дозволите дељењу сесија и собама за сарадњу да приказују RDP, VNC и Telnet сесије овог корисника другима." - } - }, - "errors": { - "remoteServerRequired": "Потребан је удаљени сервер. Повежите се са удаљеним сервером у Подешавањима да бисте користили овај тип везе." - }, - "plugin": { - "name": "Удаљена радна површина" - } -} diff --git a/plugins/remote-desktop/locales/translated/sv_SE.json b/plugins/remote-desktop/locales/translated/sv_SE.json deleted file mode 100644 index 448b074ef..000000000 --- a/plugins/remote-desktop/locales/translated/sv_SE.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Ansluter till {{type}} session...", - "checkingGuacd": "Kontrollerar fjärrskrivbordstjänsten...", - "requestingToken": "Begär {{type}} anslutningstoken...", - "openingSession": "Öppnar {{type}} session...", - "negotiating": "Förhandlar {{type}} handslag...", - "sessionReady": "{{type}} session klar", - "connectionError": "Anslutningsfel", - "connectionFailed": "Anslutningen misslyckades", - "failedToConnect": "Misslyckades med att hämta anslutningstoken", - "hostNotFound": "Värden hittades inte", - "noHostSelected": "Ingen värd vald", - "reconnect": "Återanslut", - "retry": "Försöka igen", - "guacdUnavailable": "Fjärrskrivbordstjänsten (guacd) är inte tillgänglig. Se till att guacd körs, är tillgängligt och korrekt konfigurerat i administratörsinställningarna.", - "credentialPromptTitle": "Ange RDP-inloggningsuppgifter", - "credentialPromptDescription": "Den här värden är inställd på att be om inloggningsuppgifter vid anslutning. De används endast för den här sessionen och sparas inte.", - "connect": "Ansluta", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Drive-filer", - "parent": "Föräldrakatalog", - "refresh": "Uppdatera", - "empty": "Den här mappen är tom", - "listFailed": "Kunde inte läsa den här mappen", - "download": "Ladda ner", - "downloadFailed": "Misslyckades med att ladda ner {{name}}", - "upload": "Ladda upp filer", - "uploading": "Laddar upp {{name}}...", - "uploaded": "Uppladdad {{name}}", - "uploadFailed": "Misslyckades med att ladda upp {{name}}", - "uploadDisabled": "Uppladdningar är inaktiverade för den här anslutningen", - "driveUnavailable": "Aktivera omdirigering av RDP-enheter innan du laddar upp filer", - "driveNotWritable": "guacd kan inte skriva till enhetsmappen. Kontrollera GUACD_DRIVE_PATH (eller värdens sökväg för enhet) och dess behörigheter.", - "dropToUpload": "Släpp filer för att ladda upp" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Låsskärm)", - "winKey": "Windows-tangenten", - "superL": "Super+L (Låsskärm)", - "superKey": "Supernyckel", - "super": "Super", - "cmdL": "Cmd+L (Låsskärm)", - "cmdKey": "Kommandotangent", - "cmd": "Kommando", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Flytta", - "win": "Vinna", - "stickyActive": "{{key}} (låst – klicka för att släppa)", - "stickyInactive": "{{key}} (klicka för att låsa)", - "esc": "Fly", - "tab": "Flik", - "home": "Hem", - "end": "Avsluta", - "pageUp": "Sida upp", - "pageDown": "Sida ner", - "arrowUp": "Pil upp", - "arrowDown": "Pil nedåt", - "arrowLeft": "Pil vänster", - "arrowRight": "Pil höger", - "fnToggle": "Funktionstangenter", - "reconnect": "Återanslut sessionen", - "collapse": "Komprimera verktygsfältet", - "expand": "Expandera verktygsfältet", - "dragHandle": "Dra för att flytta", - "hide": "Dölj för den här sessionen. För att inaktivera det permanent, öppna Värdinställningar → Allmänt → Anslutningsverktygsfält.", - "zoomIn": "Zooma in", - "zoomOut": "Zooma ut", - "resetZoom": "Återställ zoom", - "fitToScreen": "Anpassa till skärmen", - "switchToTrackpad": "Växla till styrplatteläge (dra för att flytta markören, tryck för att klicka)", - "switchToTouch": "Växla till pekläge (tryck direkt där du vill klicka)" - }, - "disabled": "Fjärrskrivbord är avstängt. En administratör kan aktivera det i Inställningar." - }, - "hosts": { - "nativeRdpOpened": "Öppnad i Windows Fjärrskrivbord", - "nativeRdpFailed": "Misslyckades med att öppna Windows Fjärrskrivbord", - "openNativeRdp": "Öppna i Windows Fjärrskrivbord", - "guac": { - "toolbar": "Verktygsfält", - "connection": "Förbindelse", - "authentication": "Autentisering", - "storedCredential": "Lagrade autentiseringsuppgifter", - "authMethod": "Autentiseringsmetod", - "authTypeDirect": "Direkt", - "authTypeCredential": "Referenser", - "authTypeNone": "Ingen", - "authTypeNoneDesc": "Inga inloggningsuppgifter lagras. Du kommer att bli ombedd att ange ett användarnamn och lösenord varje gång du ansluter; de sparas inte.", - "selectCredential": "Välj en behörighetsinloggning...", - "connectionSettings": "Anslutningsinställningar", - "displaySettings": "Skärminställningar", - "audioSettings": "Ljudinställningar", - "rdpPerformance": "RDP-prestanda", - "deviceRedirection": "Omdirigering av enhet", - "session": "Session", - "gateway": "Inkörsport", - "remoteApp": "Fjärrapp", - "clipboard": "Urklipp", - "sessionRecording": "Sessionsinspelning", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC-inställningar", - "terminalSettings": "Terminalinställningar", - "rdpPort": "RDP-port", - "username": "Användarnamn", - "password": "Lösenord", - "passwordSaved": "Lösenordet sparats, skriv för att ändra", - "domain": "Domän", - "securityMode": "Säkerhetsläge", - "width": "Bredd", - "height": "Höjd", - "dpi": "DPI", - "clientName": "Klientnamn", - "initialProgram": "Inledande program", - "serverLayout": "Serverlayout", - "timezone": "Tidszon", - "loadBalanceInfo": "Information om belastningsbalans / Cookie", - "loadBalanceInfoDesc": "RD Connection Broker-cookie för lastbalansering av RDS-farm (t.ex. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd-proxy", - "guacdHostname": "guacd-värd", - "guacdHostnamePlaceholder": "Global standard", - "guacdPort": "guacd-porten", - "guacdProxyDesc": "Åsidosätt den globala guacd-instansen för den här anslutningen. Lämna tomt om du vill använda den globalt konfigurerade guacd-instansen.", - "gatewayHostname": "Gateway-värdnamn", - "gatewayPort": "Gateway-porten", - "gatewayUsername": "Gateway-användarnamn", - "gatewayPassword": "Gateway-lösenord", - "gatewayDomain": "Gateway-domän", - "remoteAppProgram": "RemoteApp-programmet", - "workingDirectory": "Arbetskatalog", - "arguments": "Argument", - "normalizeLineEndings": "Normalisera radavslutningar", - "macAddress": "MAC-adress", - "broadcastAddress": "Sändningsadress", - "udpPort": "UDP-port", - "waitTimeS": "Väntetid (s)", - "driveName": "Enhetsnamn", - "drivePath": "Körväg", - "ignoreCertificate": "Ignorera certifikat", - "ignoreCertificateDesc": "Tillåt anslutningar till värdar med självsignerade certifikat", - "forceLossless": "Tvinga förlustfri", - "forceLosslessDesc": "Tvinga fram förlustfri bildkodning (högre kvalitet, mer bandbredd)", - "disableAudio": "Inaktivera ljud", - "disableAudioDesc": "Stäng av allt ljud från fjärrsessionen", - "enableAudioInput": "Aktivera ljudingång (mikrofon)", - "enableAudioInputDesc": "Vidarebefordra den lokala mikrofonen till fjärrsessionen", - "wallpaper": "Tapet", - "wallpaperDesc": "Visa skrivbordsbakgrund (inaktivering förbättrar prestandan)", - "theming": "Teman", - "themingDesc": "Aktivera visuella teman och stilar", - "fontSmoothing": "Utjämning av teckensnitt", - "fontSmoothingDesc": "Aktivera rendering av ClearType-teckensnitt", - "fullWindowDrag": "Drag i hela fönstret", - "fullWindowDragDesc": "Visa fönsterinnehållet när du drar", - "desktopComposition": "Skrivbordskomposition", - "desktopCompositionDesc": "Aktivera Aero Glass-effekter", - "menuAnimations": "Menyanimationer", - "menuAnimationsDesc": "Aktivera menytoning och glidande animationer", - "disableBitmapCaching": "Inaktivera bitmappscachning", - "disableBitmapCachingDesc": "Stäng av bitmappscachen (kan hjälpa till med problem)", - "disableOffscreenCaching": "Inaktivera cachelagring utanför skärmen", - "disableOffscreenCachingDesc": "Stäng av cache utanför skärmen", - "disableGlyphCaching": "Inaktivera glyf-cachning", - "disableGlyphCachingDesc": "Stäng av glyph-cache", - "enableGfx": "Aktivera GFX", - "enableGfxDesc": "Använd RemoteFX-grafikpipeline", - "enablePrinting": "Aktivera utskrift", - "enablePrintingDesc": "Omdirigera lokala skrivare till fjärrsessionen", - "enableDriveRedirection": "Aktivera omdirigering av kördata", - "enableDriveRedirectionDesc": "Mappa en lokal mapp som en enhet i fjärrsessionen", - "createDrivePath": "Skapa drivväg", - "createDrivePathDesc": "Skapa mappen automatiskt om den inte finns", - "disableDownload": "Inaktivera nedladdning", - "disableDownloadDesc": "Förhindra nedladdning av filer från fjärrsessionen", - "disableUpload": "Inaktivera uppladdning", - "disableUploadDesc": "Förhindra uppladdning av filer till fjärrsessionen", - "enableTouch": "Aktivera pekskärm", - "enableTouchDesc": "Aktivera vidarebefordran av pekinmatning", - "consoleSession": "Konsolsession", - "consoleSessionDesc": "Anslut till konsolen (session 0) istället för en ny session", - "sendWolPacket": "Skicka WOL-paket", - "sendWolPacketDesc": "Skicka ett magiskt paket för att väcka den här värden innan anslutning", - "disableCopy": "Inaktivera kopiering", - "disableCopyDesc": "Förhindra kopiering av text från fjärrsessionen", - "disablePaste": "Inaktivera inklistring", - "disablePasteDesc": "Förhindra att text klistras in i fjärrsessionen", - "createPathIfMissing": "Skapa sökväg om den saknas", - "createPathIfMissingDesc": "Skapa inspelningskatalogen automatiskt", - "excludeOutput": "Exkludera utdata", - "excludeOutputDesc": "Spela inte in skärmutdata (endast metadata)", - "excludeMouse": "Uteslut mus", - "excludeMouseDesc": "Spela inte in musrörelser", - "includeKeystrokes": "Inkludera tangenttryckningar", - "includeKeystrokesDesc": "Spela in råa tangenttryckningar utöver skärmutdata", - "vncPort": "VNC-port", - "vncPassword": "VNC-lösenord", - "vncUsernameOptional": "Användarnamn (valfritt)", - "vncLeaveBlank": "Lämna tomt om det inte behövs", - "cursorMode": "Markörläge", - "swapRedBlue": "Byt röd/blå", - "swapRedBlueDesc": "Byt ut de röda och blå färgkanalerna (åtgärdar vissa färgproblem)", - "readOnly": "Skrivskyddad", - "readOnlyDesc": "Visa fjärrskärmen utan att skicka någon inmatning", - "telnetPort": "Telnet-port", - "terminalType": "Terminaltyp", - "fontName": "Typsnittsnamn", - "fontSize": "Fontstorlek", - "colorScheme": "Färgschema", - "backspaceKey": "Backstegstangenten", - "colorDepth": "Färgdjup", - "resizeMethod": "Ändra storleksmetod", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Några", - "auto": "Bil", - "nlaExtended": "NLA-utökning", - "color8": "8-bitars", - "color16": "16-bitars", - "color24": "24-bitars", - "color32": "32-bitars", - "displayUpdate": "Uppdatering av displayen", - "reconnect": "Återanslut", - "preserve": "Bevara", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Lokal", - "remote": "Avlägsen", - "failsafe": "felsäker", - "blackOnWhite": "Svart på vitt", - "whiteOnBlack": "Vitt på svart", - "grayOnBlack": "Grått på svart", - "greenOnBlack": "Grönt på svart" - } - }, - "remoteDesktop": "Fjärrskrivbord", - "tabRdp": "Landsbygdsutveckling", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Virtuellt nätverk", - "unencryptedShell": "Okrypterat skal", - "remoteDesktopNotes": "Anteckningar", - "userDefaultsNote": "Inställningar som du lämnar oförändrade följer dina standardinställningar.", - "connectionOriginNote": "I skrivbordsappen använder RDP, VNC och Telnet fjärrservern om inte värden är inställd på Den här enheten, vilket kräver en guacd som den här maskinen kan nå. Ställ in en i inställningarna för fjärrskrivbord, eller per värd under guacd Proxy.", - "docsLink": "Visa dokument", - "connectionOriginGuacamoleNote": "RDP, VNC och Telnet använder fjärrservern om du inte väljer \"Den här enheten\", vilket kräver en guacd som den här maskinen kan nå. Ange en i guacd URL-inställningen, eller per värd under guacd Proxy." - }, - "onboarding": { - "feature_desktop": "Fjärrskrivbord", - "feature_desktop_desc": "Öppna RDP-, VNC- och Telnet-sessioner i en flik, samma som med SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Färgdjup", - "description": "Används för RDP-värdar som lämnar den på standardvärdet." - }, - "resizeMethod": { - "label": "Ändra storleksmetod", - "description": "Hur fjärrskärmen följer fönsterstorleken." - }, - "resizeDisplayUpdate": "Uppdatering av displayen", - "resizeReconnect": "Återanslut", - "inherit": "Ärva", - "on": "På", - "off": "Av", - "forceLossless": { - "label": "Tvinga förlustfri", - "description": "Tvinga fram förlustfri bildkodning (högre kvalitet, mer bandbredd)" - }, - "enableWallpaper": { - "label": "Tapet", - "description": "Visa skrivbordsbakgrund (inaktivering förbättrar prestandan)" - }, - "enableFontSmoothing": { - "label": "Utjämning av teckensnitt", - "description": "Aktivera rendering av ClearType-teckensnitt" - }, - "enableDesktopComposition": { - "label": "Skrivbordskomposition", - "description": "Aktivera Aero Glass-effekter" - }, - "disableAudio": { - "label": "Inaktivera ljud", - "description": "Stäng av allt ljud från fjärrsessionen" - }, - "enablePrinting": { - "label": "Aktivera utskrift", - "description": "Omdirigera lokala skrivare till fjärrsessionen" - }, - "enableDrive": { - "label": "Aktivera omdirigering av kördata", - "description": "Mappa en lokal mapp som en enhet i fjärrsessionen" - }, - "disableCopy": { - "label": "Inaktivera kopiering", - "description": "Förhindra kopiering av text från fjärrsessionen" - }, - "disablePaste": { - "label": "Inaktivera inklistring", - "description": "Förhindra att text klistras in i fjärrsessionen" - }, - "group": "Standardinställningar för fjärrskrivbord", - "colorDepth16": "16-bitars", - "colorDepth24": "24-bitars", - "colorDepth32": "32-bitars" - }, - "admin": { - "enabled": { - "label": "Aktivera fjärrskrivbord", - "description": "Tillåt RDP-, VNC- och Telnet-anslutningar via guacd." - }, - "guacdUrl": { - "label": "guacd-URL", - "description": "Där guacd lyssnar som host:port. Miljövariablerna GUACD_URL, GUACD_HOST och GUACD_PORT åsidosätter detta.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "Landsbygdsutveckling" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP-port" - }, - "vncPort": { - "label": "VNC-port" - }, - "telnetPort": { - "label": "Telnet-port" - }, - "rdpSecurity": { - "label": "RDP-säkerhetsläge" - }, - "rdpIgnoreCert": { - "label": "Ignorera certifikatfel" - }, - "guacamoleConfig": { - "label": "guacd-inställningar" - }, - "enableToolbar": { - "label": "Verktygsfält för fjärrskrivbord", - "description": "Visa verktygsfältet i RDP-, VNC- och Telnet-sessioner." - }, - "displayGroup": "Visa", - "displayDefaultsTab": "Fjärrskrivbord" - } - }, - "palette": { - "connectRdp": "Anslut med RDP", - "connectVnc": "Anslut med VNC", - "connectTelnet": "Anslut med Telnet" - }, - "permissions": { - "sessions": { - "title": "Dela fjärrskrivbordssessioner", - "description": "Låt sessionsdelning och samarbetsrum visa den här användarens RDP-, VNC- och Telnet-sessioner för andra." - } - }, - "errors": { - "remoteServerRequired": "Fjärrserver krävs. Anslut en fjärrserver i Inställningar för att använda den här anslutningstypen." - }, - "plugin": { - "name": "Fjärrskrivbord" - } -} diff --git a/plugins/remote-desktop/locales/translated/th_TH.json b/plugins/remote-desktop/locales/translated/th_TH.json deleted file mode 100644 index 059400967..000000000 --- a/plugins/remote-desktop/locales/translated/th_TH.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "กำลังเชื่อมต่อกับเซสชัน {{type}}...", - "checkingGuacd": "กำลังตรวจสอบบริการเดสก์ท็อประยะไกล...", - "requestingToken": "กำลังขอโทเค็นการเชื่อมต่อ {{type}}...", - "openingSession": "กำลังเปิดเซสชั่น {{type}}...", - "negotiating": "กำลังเจรจาต่อรองด้วยการจับมือ {{type}}...", - "sessionReady": "{{type}} เซสชั่นพร้อมใช้งาน", - "connectionError": "ข้อผิดพลาดในการเชื่อมต่อ", - "connectionFailed": "การเชื่อมต่อล้มเหลว", - "failedToConnect": "ไม่สามารถรับโทเค็นการเชื่อมต่อได้", - "hostNotFound": "ไม่พบโฮสต์", - "noHostSelected": "ไม่ได้เลือกโฮสต์", - "reconnect": "เชื่อมต่อใหม่", - "retry": "ลองใหม่อีกครั้ง", - "guacdUnavailable": "บริการเดสก์ท็อประยะไกล (guacd) ไม่พร้อมใช้งาน โปรดตรวจสอบให้แน่ใจว่า guacd กำลังทำงาน สามารถเข้าถึงได้ และกำหนดค่าอย่างถูกต้องในการตั้งค่าผู้ดูแลระบบ", - "credentialPromptTitle": "ป้อนข้อมูลรับรอง RDP", - "credentialPromptDescription": "โฮสต์นี้ตั้งค่าให้ขอข้อมูลรับรองเมื่อเชื่อมต่อ ข้อมูลเหล่านี้จะใช้สำหรับเซสชันนี้เท่านั้นและจะไม่ถูกบันทึกไว้", - "connect": "เชื่อมต่อ", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "ไฟล์ไดรฟ์", - "parent": "ไดเร็กทอรีผู้ปกครอง", - "refresh": "รีเฟรช", - "empty": "โฟลเดอร์นี้ว่างเปล่า", - "listFailed": "ไม่สามารถอ่านโฟลเดอร์นี้ได้", - "download": "ดาวน์โหลด", - "downloadFailed": "ไม่สามารถดาวน์โหลด {{name}} ได้", - "upload": "อัปโหลดไฟล์", - "uploading": "กำลังอัปโหลด {{name}}...", - "uploaded": "อัปโหลดแล้ว {{name}}", - "uploadFailed": "การอัปโหลดล้มเหลว {{name}}", - "uploadDisabled": "การอัปโหลดถูกปิดใช้งานสำหรับการเชื่อมต่อนี้", - "driveUnavailable": "เปิดใช้งานการส่งต่อไดรฟ์ RDP ก่อนอัปโหลดไฟล์", - "driveNotWritable": "guacd ไม่สามารถเขียนลงในโฟลเดอร์ไดรฟ์ได้ โปรดตรวจสอบ GUACD_DRIVE_PATH (หรือเส้นทางไดรฟ์ของโฮสต์) และสิทธิ์การเข้าถึง", - "dropToUpload": "ลากไฟล์เพื่ออัปโหลด" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (หน้าจอล็อก)", - "winKey": "ปุ่ม Windows", - "superL": "Super+L (หน้าจอล็อก)", - "superKey": "ซูเปอร์คีย์", - "super": "สุดยอด", - "cmdL": "กด Cmd+L (หน้าจอล็อก)", - "cmdKey": "ปุ่มคำสั่ง", - "cmd": "ผู้บัญชาการ", - "ctrl": "Ctrl", - "alt": "อัลท์", - "shift": "กะ", - "win": "ชนะ", - "stickyActive": "{{key}} (ล็อคอยู่ - คลิกเพื่อปลดล็อค)", - "stickyInactive": "{{key}} (คลิกเพื่อล็อค)", - "esc": "หนี", - "tab": "แท็บ", - "home": "บ้าน", - "end": "จบ", - "pageUp": "เลื่อนขึ้น", - "pageDown": "เลื่อนลง", - "arrowUp": "ลูกศรขึ้น", - "arrowDown": "ลูกศรลง", - "arrowLeft": "ลูกศรซ้าย", - "arrowRight": "ลูกศรขวา", - "fnToggle": "ปุ่มฟังก์ชัน", - "reconnect": "เซสชั่นเชื่อมต่อใหม่", - "collapse": "ยุบแถบเครื่องมือ", - "expand": "ขยายแถบเครื่องมือ", - "dragHandle": "ลากเพื่อจัดตำแหน่งใหม่", - "hide": "ซ่อนไว้สำหรับการใช้งานในครั้งนี้ หากต้องการปิดใช้งานอย่างถาวร ให้เปิด การตั้งค่าโฮสต์ → ทั่วไป → แถบเครื่องมือการเชื่อมต่อ", - "zoomIn": "ซูมเข้า", - "zoomOut": "ซูมออก", - "resetZoom": "รีเซ็ตการซูม", - "fitToScreen": "พอดีกับหน้าจอ", - "switchToTrackpad": "เปลี่ยนไปใช้โหมดแทร็กแพด (ลากเพื่อเลื่อนเคอร์เซอร์ แตะเพื่อคลิก)", - "switchToTouch": "เปลี่ยนเป็นโหมดสัมผัส (แตะตรงจุดที่คุณต้องการคลิก)" - }, - "disabled": "ฟังก์ชันการเข้าถึงระยะไกล (Remote Desktop) ถูกปิดใช้งานอยู่ ผู้ดูแลระบบสามารถเปิดใช้งานได้ในการตั้งค่า" - }, - "hosts": { - "nativeRdpOpened": "เปิดใช้งานใน Windows Remote Desktop", - "nativeRdpFailed": "ไม่สามารถเปิด Windows Remote Desktop ได้", - "openNativeRdp": "เปิดใน Windows Remote Desktop", - "guac": { - "toolbar": "แถบเครื่องมือ", - "connection": "การเชื่อมต่อ", - "authentication": "การตรวจสอบสิทธิ์", - "storedCredential": "ข้อมูลประจำตัวที่บันทึกไว้", - "authMethod": "วิธีการตรวจสอบสิทธิ์", - "authTypeDirect": "โดยตรง", - "authTypeCredential": "ใบรับรอง", - "authTypeNone": "ไม่มี", - "authTypeNoneDesc": "ไม่มีการจัดเก็บข้อมูลประจำตัวใดๆ คุณจะต้องป้อนชื่อผู้ใช้และรหัสผ่านทุกครั้งที่เชื่อมต่อ ซึ่งจะไม่ถูกบันทึกไว้", - "selectCredential": "เลือกข้อมูลประจำตัว...", - "connectionSettings": "การตั้งค่าการเชื่อมต่อ", - "displaySettings": "การตั้งค่าการแสดงผล", - "audioSettings": "การตั้งค่าเสียง", - "rdpPerformance": "ประสิทธิภาพ RDP", - "deviceRedirection": "การเปลี่ยนเส้นทางอุปกรณ์", - "session": "การประชุม", - "gateway": "เกตเวย์", - "remoteApp": "รีโมทแอป", - "clipboard": "คลิปบอร์ด", - "sessionRecording": "การบันทึกการประชุม", - "wakeOnLan": "ปลุกเครื่องผ่าน LAN", - "vncSettings": "การตั้งค่า VNC", - "terminalSettings": "การตั้งค่าเทอร์มินัล", - "rdpPort": "พอร์ต RDP", - "username": "ชื่อผู้ใช้", - "password": "รหัสผ่าน", - "passwordSaved": "บันทึกรหัสผ่านแล้ว กรุณาพิมพ์เพื่อเปลี่ยนรหัสผ่าน", - "domain": "โดเมน", - "securityMode": "โหมดความปลอดภัย", - "width": "ความกว้าง", - "height": "ความสูง", - "dpi": "ดีพีไอ", - "clientName": "ชื่อลูกค้า", - "initialProgram": "โปรแกรมเริ่มต้น", - "serverLayout": "เค้าโครงเซิร์ฟเวอร์", - "timezone": "เขตเวลา", - "loadBalanceInfo": "ข้อมูลการปรับสมดุลโหลด / คุกกี้", - "loadBalanceInfoDesc": "คุกกี้ RD Connection Broker สำหรับการปรับสมดุลโหลดของฟาร์ม RDS (เช่น tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd Proxy", - "guacdHostname": "guacd Host", - "guacdHostnamePlaceholder": "ค่าเริ่มต้นทั่วโลก", - "guacdPort": "guacd Port", - "guacdProxyDesc": "แทนที่อินสแตนซ์ guacd ทั่วโลกสำหรับการเชื่อมต่อนี้ หากต้องการใช้ guacd ที่กำหนดค่าไว้ทั่วโลก ให้เว้นว่างไว้", - "gatewayHostname": "ชื่อโฮสต์เกตเวย์", - "gatewayPort": "พอร์ตเกตเวย์", - "gatewayUsername": "ชื่อผู้ใช้เกตเวย์", - "gatewayPassword": "รหัสผ่านเกตเวย์", - "gatewayDomain": "โดเมนเกตเวย์", - "remoteAppProgram": "โปรแกรม RemoteApp", - "workingDirectory": "ไดเร็กทอรีการทำงาน", - "arguments": "ข้อโต้แย้ง", - "normalizeLineEndings": "ปรับความยาวบรรทัดให้เป็นมาตรฐาน", - "macAddress": "ที่อยู่ MAC", - "broadcastAddress": "ที่อยู่กระจายเสียง", - "udpPort": "พอร์ต UDP", - "waitTimeS": "ระยะเวลารอคอย (วินาที)", - "driveName": "ชื่อไดรฟ์", - "drivePath": "เส้นทางขับรถ", - "ignoreCertificate": "ละเว้นใบรับรอง", - "ignoreCertificateDesc": "อนุญาตการเชื่อมต่อกับโฮสต์ที่มีใบรับรองที่ลงนามด้วยตนเอง", - "forceLossless": "บังคับการสูญเสีย", - "forceLosslessDesc": "บังคับใช้การเข้ารหัสภาพแบบไม่สูญเสียคุณภาพ (คุณภาพสูงกว่า ใช้แบนด์วิดท์มากกว่า)", - "disableAudio": "ปิดใช้งานเสียง", - "disableAudioDesc": "ปิดเสียงทั้งหมดจากการเชื่อมต่อระยะไกล", - "enableAudioInput": "เปิดใช้งานการรับเสียง (ไมโครโฟน)", - "enableAudioInputDesc": "ส่งต่อไมโครโฟนในเครื่องไปยังเซสชันระยะไกล", - "wallpaper": "วอลเปเปอร์", - "wallpaperDesc": "แสดงภาพพื้นหลังเดสก์ท็อป (การปิดใช้งานจะช่วยเพิ่มประสิทธิภาพ)", - "theming": "ธีม", - "themingDesc": "เปิดใช้งานธีมและสไตล์ภาพ", - "fontSmoothing": "การปรับความเรียบของแบบอักษร", - "fontSmoothingDesc": "เปิดใช้งานการแสดงผลฟอนต์ ClearType", - "fullWindowDrag": "ลากแบบเต็มหน้าต่าง", - "fullWindowDragDesc": "แสดงเนื้อหาในหน้าต่างขณะลาก", - "desktopComposition": "การจัดองค์ประกอบเดสก์ท็อป", - "desktopCompositionDesc": "เปิดใช้งานเอฟเฟกต์กระจก Aero", - "menuAnimations": "แอนิเมชันเมนู", - "menuAnimationsDesc": "เปิดใช้งานแอนิเมชั่นเฟดและสไลด์ของเมนู", - "disableBitmapCaching": "ปิดใช้งานการแคชภาพบิตแมป", - "disableBitmapCachingDesc": "ปิดใช้งานแคชภาพบิตแมป (อาจช่วยลดปัญหาภาพกระตุกได้)", - "disableOffscreenCaching": "ปิดใช้งานการแคชแบบออฟสกรีน", - "disableOffscreenCachingDesc": "ปิดใช้งานแคชแบบออฟไลน์", - "disableGlyphCaching": "ปิดใช้งานการแคชสัญลักษณ์", - "disableGlyphCachingDesc": "ปิดใช้งานแคชสัญลักษณ์", - "enableGfx": "เปิดใช้งาน GFX", - "enableGfxDesc": "ใช้ไปป์ไลน์กราฟิก RemoteFX", - "enablePrinting": "เปิดใช้งานการพิมพ์", - "enablePrintingDesc": "โอนย้ายเครื่องพิมพ์ในเครื่องไปยังเซสชันระยะไกล", - "enableDriveRedirection": "เปิดใช้งานการเปลี่ยนเส้นทางไดรฟ์", - "enableDriveRedirectionDesc": "แมปโฟลเดอร์ในเครื่องเป็นไดรฟ์ในเซสชันระยะไกล", - "createDrivePath": "สร้างเส้นทางไดรฟ์", - "createDrivePathDesc": "สร้างโฟลเดอร์โดยอัตโนมัติหากยังไม่มีอยู่", - "disableDownload": "ปิดใช้งานการดาวน์โหลด", - "disableDownloadDesc": "ป้องกันการดาวน์โหลดไฟล์จากการเชื่อมต่อระยะไกล", - "disableUpload": "ปิดใช้งานการอัปโหลด", - "disableUploadDesc": "ป้องกันการอัปโหลดไฟล์ไปยังเซสชันระยะไกล", - "enableTouch": "เปิดใช้งานระบบสัมผัส", - "enableTouchDesc": "เปิดใช้งานการส่งต่อข้อมูลการสัมผัส", - "consoleSession": "เซสชันคอนโซล", - "consoleSessionDesc": "เชื่อมต่อกับคอนโซล (เซสชัน 0) แทนที่จะสร้างเซสชันใหม่", - "sendWolPacket": "ส่งแพ็กเก็ต WOL", - "sendWolPacketDesc": "ส่งแพ็กเก็ตเวทมนตร์เพื่อปลุกโฮสต์นี้ก่อนเชื่อมต่อ", - "disableCopy": "ปิดใช้งานการคัดลอก", - "disableCopyDesc": "ป้องกันการคัดลอกข้อความจากเซสชันระยะไกล", - "disablePaste": "ปิดใช้งานการวาง", - "disablePasteDesc": "ป้องกันการวางข้อความลงในเซสชันระยะไกล", - "createPathIfMissing": "สร้างเส้นทางหากยังไม่มีอยู่", - "createPathIfMissingDesc": "สร้างไดเร็กทอรีสำหรับการบันทึกโดยอัตโนมัติ", - "excludeOutput": "ไม่รวมผลลัพธ์", - "excludeOutputDesc": "ห้ามบันทึกข้อมูลที่แสดงบนหน้าจอ (บันทึกเฉพาะข้อมูลเมตาเท่านั้น)", - "excludeMouse": "ไม่รวมเมาส์", - "excludeMouseDesc": "ห้ามบันทึกการเคลื่อนไหวของเมาส์", - "includeKeystrokes": "รวมการกดแป้นพิมพ์", - "includeKeystrokesDesc": "บันทึกการกดแป้นพิมพ์ดิบนอกเหนือจากข้อมูลที่แสดงผลบนหน้าจอ", - "vncPort": "พอร์ต VNC", - "vncPassword": "รหัสผ่าน VNC", - "vncUsernameOptional": "ชื่อผู้ใช้ (ไม่บังคับ)", - "vncLeaveBlank": "เว้นว่างไว้หากไม่ต้องการใช้", - "cursorMode": "โหมดเคอร์เซอร์", - "swapRedBlue": "สลับสีแดง/น้ำเงิน", - "swapRedBlueDesc": "สลับช่องสีแดงและสีน้ำเงิน (แก้ไขปัญหาเรื่องสีบางอย่าง)", - "readOnly": "อ่านอย่างเดียว", - "readOnlyDesc": "ดูหน้าจอระยะไกลโดยไม่ต้องส่งข้อมูลใดๆ", - "telnetPort": "พอร์ตเทลเน็ต", - "terminalType": "ประเภทเทอร์มินัล", - "fontName": "ชื่อแบบอักษร", - "fontSize": "ขนาดตัวอักษร", - "colorScheme": "โทนสี", - "backspaceKey": "ปุ่มลบ", - "colorDepth": "ความลึกของสี", - "resizeMethod": "วิธีการปรับขนาด", - "driveNamePlaceholder": "เทอร์มิกซ์ไดรฟ์", - "options": { - "any": "ใดๆ", - "auto": "อัตโนมัติ", - "nlaExtended": "NLA ขยาย", - "color8": "8 บิต", - "color16": "16 บิต", - "color24": "24 บิต", - "color32": "32 บิต", - "displayUpdate": "อัปเดตการแสดงผล", - "reconnect": "เชื่อมต่อใหม่", - "preserve": "อนุรักษ์", - "unix": "ยูนิกซ์ (LF)", - "windows": "วินโดวส์ (CRLF)", - "local": "ท้องถิ่น", - "remote": "ระยะไกล", - "failsafe": "ระบบป้องกันความล้มเหลว", - "blackOnWhite": "สีดำบนพื้นขาว", - "whiteOnBlack": "สีขาวบนพื้นดำ", - "grayOnBlack": "สีเทาบนพื้นดำ", - "greenOnBlack": "สีเขียวบนพื้นดำ" - } - }, - "remoteDesktop": "รีโมทเดสก์ท็อป", - "tabRdp": "อาร์ดีพี", - "tabVnc": "วีเอ็นซี", - "tabTelnet": "เทลเน็ต", - "virtualNetwork": "เครือข่ายเสมือน", - "unencryptedShell": "เชลล์ที่ไม่ได้เข้ารหัส", - "remoteDesktopNotes": "หมายเหตุ", - "userDefaultsNote": "การตั้งค่าที่คุณไม่ได้เปลี่ยนแปลงจะใช้ค่าเริ่มต้นของโฮสต์ของคุณ", - "connectionOriginNote": "ในแอปพลิเคชันบนเดสก์ท็อป RDP, VNC และ Telnet จะใช้เซิร์ฟเวอร์ระยะไกล เว้นแต่ว่าโฮสต์จะถูกตั้งค่าเป็น \"อุปกรณ์นี้\" ซึ่งจำเป็นต้องมี guacd ที่เครื่องนี้สามารถเข้าถึงได้ ตั้งค่าได้ในการตั้งค่าเดสก์ท็อประยะไกล หรือต่อโฮสต์ภายใต้ guacd Proxy", - "docsLink": "ดูเอกสาร", - "connectionOriginGuacamoleNote": "RDP, VNC และ Telnet จะใช้เซิร์ฟเวอร์ระยะไกล เว้นแต่คุณจะเลือก \"อุปกรณ์นี้\" ซึ่งจำเป็นต้องมี guacd ที่เครื่องนี้สามารถเข้าถึงได้ ตั้งค่าได้ในการตั้งค่า URL ของ guacd หรือต่อโฮสต์ภายใต้พร็อกซีของ guacd" - }, - "onboarding": { - "feature_desktop": "การเข้าถึงเดสก์ท็อประยะไกล", - "feature_desktop_desc": "เปิดเซสชัน RDP, VNC และ Telnet ในแท็บเดียวกัน เหมือนกับการเปิด SSH" - }, - "settings": { - "user": { - "colorDepth": { - "label": "ความลึกของสี", - "description": "ใช้สำหรับโฮสต์ RDP ที่ตั้งค่าไว้เป็นค่าเริ่มต้น" - }, - "resizeMethod": { - "label": "วิธีการปรับขนาด", - "description": "หน้าจอระยะไกลจะปรับขนาดตามขนาดหน้าต่างอย่างไร" - }, - "resizeDisplayUpdate": "อัปเดตการแสดงผล", - "resizeReconnect": "เชื่อมต่อใหม่", - "inherit": "สืบทอด", - "on": "บน", - "off": "ปิด", - "forceLossless": { - "label": "บังคับการสูญเสีย", - "description": "บังคับใช้การเข้ารหัสภาพแบบไม่สูญเสียคุณภาพ (คุณภาพสูงกว่า ใช้แบนด์วิดท์มากกว่า)" - }, - "enableWallpaper": { - "label": "วอลเปเปอร์", - "description": "แสดงภาพพื้นหลังเดสก์ท็อป (การปิดใช้งานจะช่วยเพิ่มประสิทธิภาพ)" - }, - "enableFontSmoothing": { - "label": "การปรับความเรียบของแบบอักษร", - "description": "เปิดใช้งานการแสดงผลฟอนต์ ClearType" - }, - "enableDesktopComposition": { - "label": "การจัดองค์ประกอบเดสก์ท็อป", - "description": "เปิดใช้งานเอฟเฟกต์กระจก Aero" - }, - "disableAudio": { - "label": "ปิดใช้งานเสียง", - "description": "ปิดเสียงทั้งหมดจากการเชื่อมต่อระยะไกล" - }, - "enablePrinting": { - "label": "เปิดใช้งานการพิมพ์", - "description": "โอนย้ายเครื่องพิมพ์ในเครื่องไปยังเซสชันระยะไกล" - }, - "enableDrive": { - "label": "เปิดใช้งานการเปลี่ยนเส้นทางไดรฟ์", - "description": "แมปโฟลเดอร์ในเครื่องเป็นไดรฟ์ในเซสชันระยะไกล" - }, - "disableCopy": { - "label": "ปิดใช้งานการคัดลอก", - "description": "ป้องกันการคัดลอกข้อความจากเซสชันระยะไกล" - }, - "disablePaste": { - "label": "ปิดใช้งานการวาง", - "description": "ป้องกันการวางข้อความลงในเซสชันระยะไกล" - }, - "group": "ค่าเริ่มต้นของการเข้าถึงเดสก์ท็อประยะไกล", - "colorDepth16": "16 บิต", - "colorDepth24": "24 บิต", - "colorDepth32": "32 บิต" - }, - "admin": { - "enabled": { - "label": "เปิดใช้งานเดสก์ท็อประยะไกล", - "description": "อนุญาตการเชื่อมต่อ RDP, VNC และ Telnet ผ่าน guacd" - }, - "guacdUrl": { - "label": "URL guacd", - "description": "ตำแหน่งที่ guacd รับฟังข้อมูล จะแสดงเป็น host:port ตัวแปรสภาพแวดล้อม GUACD_URL, GUACD_HOST และ GUACD_PORT จะแทนที่ค่านี้", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "อาร์ดีพี" - }, - "enableVnc": { - "label": "วีเอ็นซี" - }, - "enableTelnet": { - "label": "เทลเน็ต" - }, - "rdpPort": { - "label": "พอร์ต RDP" - }, - "vncPort": { - "label": "พอร์ต VNC" - }, - "telnetPort": { - "label": "พอร์ตเทลเน็ต" - }, - "rdpSecurity": { - "label": "โหมดความปลอดภัย RDP" - }, - "rdpIgnoreCert": { - "label": "ละเว้นข้อผิดพลาดเกี่ยวกับใบรับรอง" - }, - "guacamoleConfig": { - "label": "การตั้งค่า guacd" - }, - "enableToolbar": { - "label": "แถบเครื่องมือเดสก์ท็อประยะไกล", - "description": "แสดงแถบเครื่องมือในเซสชัน RDP, VNC และ Telnet" - }, - "displayGroup": "แสดง", - "displayDefaultsTab": "รีโมทเดสก์ท็อป" - } - }, - "palette": { - "connectRdp": "เชื่อมต่อด้วย RDP", - "connectVnc": "เชื่อมต่อกับ VNC", - "connectTelnet": "เชื่อมต่อด้วย Telnet" - }, - "permissions": { - "sessions": { - "title": "แชร์เซสชันเดสก์ท็อประยะไกล", - "description": "อนุญาตให้การแชร์เซสชันและห้องทำงานร่วมกันแสดงเซสชัน RDP, VNC และ Telnet ของผู้ใช้รายนี้แก่ผู้อื่น" - } - }, - "errors": { - "remoteServerRequired": "จำเป็นต้องใช้เซิร์ฟเวอร์ระยะไกล เชื่อมต่อเซิร์ฟเวอร์ระยะไกลในการตั้งค่าเพื่อใช้ประเภทการเชื่อมต่อนี้" - }, - "plugin": { - "name": "รีโมทเดสก์ท็อป" - } -} diff --git a/plugins/remote-desktop/locales/translated/tr_TR.json b/plugins/remote-desktop/locales/translated/tr_TR.json deleted file mode 100644 index b83a43724..000000000 --- a/plugins/remote-desktop/locales/translated/tr_TR.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "{{type}} oturumuna bağlanılıyor...", - "checkingGuacd": "Uzaktan masaüstü hizmeti kontrol ediliyor...", - "requestingToken": "{{type}} bağlantı belirteci isteniyor...", - "openingSession": "{{type}} oturumu açılıyor...", - "negotiating": "{{type}} el sıkışması üzerinde pazarlık yapılıyor...", - "sessionReady": "{{type}} oturum hazır", - "connectionError": "Bağlantı hatası", - "connectionFailed": "Bağlantı başarısız", - "failedToConnect": "Bağlantı belirteci alınamadı", - "hostNotFound": "Sunucu bulunamadı", - "noHostSelected": "Sunucu seçilmedi", - "reconnect": "Yeniden Bağlan", - "retry": "Yeniden dene", - "guacdUnavailable": "Uzak masaüstü hizmeti (guacd) kullanılamıyor. Lütfen guacd'nin çalıştığından, erişilebilir olduğundan ve yönetici ayarlarında düzgün yapılandırıldığından emin olun.", - "credentialPromptTitle": "RDP Kimlik Bilgilerini Girin", - "credentialPromptDescription": "Bu sunucu, bağlantı kurulurken kimlik bilgilerini isteyecek şekilde ayarlanmıştır. Bu bilgiler yalnızca bu oturum için kullanılır ve kaydedilmez.", - "connect": "Bağlan", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Sürücü Dosyaları", - "parent": "Üst dizin", - "refresh": "Yenile", - "empty": "Bu klasör boş", - "listFailed": "Bu klasör okunamadı.", - "download": "İndir", - "downloadFailed": "{{name}} indirme işlemi başarısız oldu.", - "upload": "Dosyaları yükle", - "uploading": "{{name}} yükleniyor...", - "uploaded": "Yüklenen {{name}}", - "uploadFailed": "{{name}} yüklenemedi.", - "uploadDisabled": "Bu bağlantı için yüklemeler devre dışı bırakıldı.", - "driveUnavailable": "Dosyaları yüklemeden önce RDP sürücü yönlendirmesini etkinleştirin.", - "driveNotWritable": "guacd sürücü klasörüne yazamıyor. GUACD_DRIVE_PATH (veya ana bilgisayarın Sürücü Yolu) ve izinlerini kontrol edin.", - "dropToUpload": "Yüklemek için dosyaları sürükleyip bırakın." - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Ekranı Kilitle)", - "winKey": "Windows Tuşu", - "superL": "Süper+L (Kilit Ekranı)", - "superKey": "Süper Anahtar", - "super": "Süper", - "cmdL": "Cmd+L (Kilit Ekranı)", - "cmdKey": "Komut Tuşu", - "cmd": "Komut", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}} (sabitlenmiş - bırakmak için tıklayın)", - "stickyInactive": "{{key}} (sabitlemek için tıklayın)", - "esc": "Escape", - "tab": "Tab", - "home": "Home", - "end": "End", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "Yukarı Ok", - "arrowDown": "Aşağı Ok", - "arrowLeft": "Sol Ok", - "arrowRight": "Sağ Ok", - "fnToggle": "Fonksiyon Tuşları", - "reconnect": "Oturumu Yeniden Bağla", - "collapse": "Araç çubuğunu daralt", - "expand": "Araç çubuğunu genişlet", - "dragHandle": "Yeniden konumlandırmak için sürükle", - "hide": "Bu oturum için gizle. Kalıcı olarak devre dışı bırakmak için Sunucu Ayarları → Genel → Bağlantı Araç Çubuğu'nu açın.", - "zoomIn": "Yakınlaştır", - "zoomOut": "Uzaklaştır", - "resetZoom": "Yakınlaştırmayı sıfırla", - "fitToScreen": "Ekrana uygun", - "switchToTrackpad": "İzleme dörtlüsü moduna geçin (imleci hareket ettirmek için sürükleyin, tıklamak için dokunun)", - "switchToTouch": "Dokunmatik moda geçin (tıklamak istediğiniz yere doğrudan dokunun)" - }, - "disabled": "Uzaktan Masaüstü özelliği kapalı. Yönetici, Ayarlar bölümünden bu özelliği açabilir." - }, - "hosts": { - "nativeRdpOpened": "Windows Uzaktan Masaüstü'nde açıldı", - "nativeRdpFailed": "Windows Uzaktan Masaüstü açılamadı.", - "openNativeRdp": "Windows Uzaktan Masaüstü'nde aç", - "guac": { - "toolbar": "Araç Çubuğu", - "connection": "Bağlantı", - "authentication": "Kimlik doğrulama", - "storedCredential": "Kayıtlı kimlik bilgisi", - "authMethod": "Kimlik doğrulama yöntemi", - "authTypeDirect": "Doğrudan", - "authTypeCredential": "Kimlik bilgisi", - "authTypeNone": "Yok", - "authTypeNoneDesc": "Hiçbir kimlik bilgisi saklanmaz. Her bağlantı kurduğunuzda sizden kullanıcı adı ve şifre istenecektir; bunlar kaydedilmez.", - "selectCredential": "Bir kimlik bilgisi seçin...", - "connectionSettings": "Bağlantı ayarları", - "displaySettings": "Görüntü ayarları", - "audioSettings": "Ses ayarları", - "rdpPerformance": "RDP performansı", - "deviceRedirection": "Cihaz yönlendirme", - "session": "Oturum", - "gateway": "Ağ geçidi", - "remoteApp": "RemoteApp", - "clipboard": "Pano", - "sessionRecording": "Oturum kaydı", - "wakeOnLan": "LAN üzerinden uyandırma", - "vncSettings": "VNC ayarları", - "terminalSettings": "Terminal ayarları", - "rdpPort": "RDP Portu", - "username": "Kullanıcı Adı", - "password": "Parola", - "passwordSaved": "Parola kaydedildi, değiştirmek için tuşlayın.", - "domain": "Etki Alanı", - "securityMode": "Güvenlik Modu", - "width": "Genişlik", - "height": "Yükseklik", - "dpi": "DPI", - "clientName": "İstemci Adı", - "initialProgram": "Başlangıç Programı", - "serverLayout": "Sunucu Düzeni", - "timezone": "Saat Dilimi", - "loadBalanceInfo": "Yük Dengeleme Bilgisi / Çerez", - "loadBalanceInfoDesc": "RDS grubu yük dengelemesi için RD Bağlantı Aracısı çerezi (ör. tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd Proxy", - "guacdHostname": "guacd Sunucusu", - "guacdHostnamePlaceholder": "Genel varsayılan", - "guacdPort": "guacd Portu", - "guacdProxyDesc": "Bu bağlantı için genel guacd örneğini geçersiz kıl. Genel olarak yapılandırılmış guacd'yi kullanmak için boş bırak.", - "gatewayHostname": "Ağ Geçidi Ana Bilgisayar Adı", - "gatewayPort": "Ağ Geçidi Portu", - "gatewayUsername": "Ağ Geçidi Kullanıcı Adı", - "gatewayPassword": "Ağ Geçidi Parolası", - "gatewayDomain": "Ağ Geçidi Etki Alanı", - "remoteAppProgram": "RemoteApp Programı", - "workingDirectory": "Çalışma Dizini", - "arguments": "Argümanlar", - "normalizeLineEndings": "Satır Sonlarını Normalleştir", - "macAddress": "MAC Adresi", - "broadcastAddress": "Yayın Adresi", - "udpPort": "UDP Portu", - "waitTimeS": "Bekleme Süresi (sn)", - "driveName": "Sürücü Adı", - "drivePath": "Sürücü Yolu", - "ignoreCertificate": "Sertifikayı Yoksay", - "ignoreCertificateDesc": "Kendinden imzalı sertifikalara sahip sunuculara bağlantılara izin ver", - "forceLossless": "Kayıpsız Zorla", - "forceLosslessDesc": "Kayıpsız görüntü kodlamasını zorla (daha yüksek kalite, daha fazla bant genişliği)", - "disableAudio": "Sesi Devre Dışı Bırak", - "disableAudioDesc": "Uzak oturumdaki tüm sesleri kapat", - "enableAudioInput": "Ses Girişini Etkinleştir (Mikrofon)", - "enableAudioInputDesc": "Yerel mikrofonu uzak oturuma yönlendir", - "wallpaper": "Duvar Kağıdı", - "wallpaperDesc": "Masaüstü duvar kağıdını göster (devre dışı bırakmak performansı artırır)", - "theming": "Tema", - "themingDesc": "Görsel temaları ve stilleri etkinleştir", - "fontSmoothing": "Yazı Tipi Yumuşatma", - "fontSmoothingDesc": "ClearType yazı tipi işlemeyi etkinleştir", - "fullWindowDrag": "Tam Pencere Sürükleme", - "fullWindowDragDesc": "Sürüklerken pencere içeriğini göster", - "desktopComposition": "Masaüstü Birleştirme", - "desktopCompositionDesc": "Aero cam efektlerini etkinleştir", - "menuAnimations": "Menü Animasyonları", - "menuAnimationsDesc": "Menü solma ve kayma animasyonlarını etkinleştir", - "disableBitmapCaching": "Bitmap Önbelleğini Devre Dışı Bırak", - "disableBitmapCachingDesc": "Bitmap önbelleğini kapat (grafik bozulmalarına yardımcı olabilir)", - "disableOffscreenCaching": "Ekran Dışı Önbelleğini Devre Dışı Bırak", - "disableOffscreenCachingDesc": "Ekran dışı önbelleğini kapat", - "disableGlyphCaching": "Glif Önbelleğini Devre Dışı Bırak", - "disableGlyphCachingDesc": "Glif önbelleğini kapat", - "enableGfx": "GFX'i Etkinleştir", - "enableGfxDesc": "RemoteFX grafik işlem hattını kullan", - "enablePrinting": "Yazdırmayı Etkinleştir", - "enablePrintingDesc": "Yerel yazıcıları uzak oturuma yönlendir", - "enableDriveRedirection": "Sürücü Yönlendirmesini Etkinleştir", - "enableDriveRedirectionDesc": "Yerel bir klasörü uzak oturumda sürücü olarak eşleştir", - "createDrivePath": "Sürücü Yolu Oluştur", - "createDrivePathDesc": "Klasör mevcut değilse otomatik oluştur", - "disableDownload": "İndirmeyi Devre Dışı Bırak", - "disableDownloadDesc": "Uzak oturumdan dosya indirmeyi engelle", - "disableUpload": "Yüklemeyi Devre Dışı Bırak", - "disableUploadDesc": "Uzak oturuma dosya yüklemeyi engelle", - "enableTouch": "Dokunmayı Etkinleştir", - "enableTouchDesc": "Dokunma girişi iletimini etkinleştir", - "consoleSession": "Konsol Oturumu", - "consoleSessionDesc": "Yeni oturum yerine konsola (oturum 0) bağlan", - "sendWolPacket": "WOL Paketi Gönder", - "sendWolPacketDesc": "Bağlanmadan önce bu sunucuyu uyandırmak için sihirli paket gönder", - "disableCopy": "Kopyalamayı Devre Dışı Bırak", - "disableCopyDesc": "Uzak oturumdan metin kopyalamayı engelle", - "disablePaste": "Yapıştırmayı Devre Dışı Bırak", - "disablePasteDesc": "Uzak oturuma metin yapıştırmayı engelle", - "createPathIfMissing": "Yol Yoksa Oluştur", - "createPathIfMissingDesc": "Kayıt dizinini otomatik oluştur", - "excludeOutput": "Çıktıyı Hariç Tut", - "excludeOutputDesc": "Ekran çıktısını kaydetme (yalnızca meta veri)", - "excludeMouse": "Fareyi Hariç Tut", - "excludeMouseDesc": "Fare hareketlerini kaydetme", - "includeKeystrokes": "Tuş Vuruşlarını Dahil Et", - "includeKeystrokesDesc": "Ekran çıktısına ek olarak ham tuş vuruşlarını kaydet", - "vncPort": "VNC Portu", - "vncPassword": "VNC Parolası", - "vncUsernameOptional": "Kullanıcı adı (isteğe bağlı)", - "vncLeaveBlank": "Gerekli değilse boş bırakın", - "cursorMode": "İmleç Modu", - "swapRedBlue": "Kırmızı ve Maviyi Değiştir", - "swapRedBlueDesc": "Kırmızı ve mavi renk kanallarını değiştir (bazı renk sorunlarını giderir)", - "readOnly": "Salt okunur", - "readOnlyDesc": "Hiçbir giriş göndermeden uzak ekranı görüntüle", - "telnetPort": "Telnet Portu", - "terminalType": "Terminal Türü", - "fontName": "Yazı Tipi Adı", - "fontSize": "Yazı Tipi Boyutu", - "colorScheme": "Renk Şeması", - "backspaceKey": "Geri Silme Tuşu", - "colorDepth": "Renk Derinliği", - "resizeMethod": "Yeniden Boyutlandırma Yöntemi", - "driveNamePlaceholder": "Termix Drive", - "options": { - "any": "Herhangi", - "auto": "Otomatik", - "nlaExtended": "NLA Uzatıldı", - "color8": "8 bit", - "color16": "16 bit", - "color24": "24 bit", - "color32": "32 bit", - "displayUpdate": "Ekran Güncellemesi", - "reconnect": "Yeniden Bağlan", - "preserve": "Korumak", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Yerel", - "remote": "Uzak", - "failsafe": "arıza emniyeti", - "blackOnWhite": "Siyah Beyaz", - "whiteOnBlack": "Siyah üzerine beyaz", - "grayOnBlack": "Siyah üzerine gri", - "greenOnBlack": "Siyah üzerine yeşil" - } - }, - "remoteDesktop": "Uzak Masaüstü", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Sanal Ağ", - "unencryptedShell": "Şifrelenmemiş Shell", - "remoteDesktopNotes": "Notlar", - "userDefaultsNote": "Değiştirmediğiniz ayarlar, sunucunuzun varsayılan ayarlarını takip eder.", - "connectionOriginNote": "Masaüstü uygulamasında, RDP, VNC ve Telnet, ana bilgisayar \"Bu cihaz\" olarak ayarlanmadığı sürece uzak sunucuyu kullanır; bu ayar, bu makinenin erişebileceği bir guacd gerektirir. Bunu Uzak Masaüstü ayarlarında veya her ana bilgisayar için guacd Proxy altında ayarlayın.", - "docsLink": "Dokümanları görüntüle", - "connectionOriginGuacamoleNote": "RDP, VNC ve Telnet, \"Bu cihaz\" seçeneğini seçmediğiniz sürece uzak sunucuyu kullanır; bu seçenek için makinenin erişebileceği bir guacd adresine ihtiyaç vardır. Bunu guacd URL ayarında veya her ana bilgisayar için guacd Proxy altında belirleyebilirsiniz." - }, - "onboarding": { - "feature_desktop": "Uzaktan masaüstü", - "feature_desktop_desc": "SSH'de olduğu gibi, RDP, VNC ve Telnet oturumlarını da bir sekmede açın." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Renk Derinliği", - "description": "Varsayılan ayarlarında bırakılan RDP sunucuları için kullanılır." - }, - "resizeMethod": { - "label": "Yeniden Boyutlandırma Yöntemi", - "description": "Uzaktan kumanda ekranının pencere boyutunu nasıl takip ettiği." - }, - "resizeDisplayUpdate": "Ekran Güncellemesi", - "resizeReconnect": "Yeniden Bağlan", - "inherit": "Miras almak", - "on": "Açık", - "off": "Kapalı", - "forceLossless": { - "label": "Kayıpsız Zorla", - "description": "Kayıpsız görüntü kodlamasını zorla (daha yüksek kalite, daha fazla bant genişliği)" - }, - "enableWallpaper": { - "label": "Duvar Kağıdı", - "description": "Masaüstü duvar kağıdını göster (devre dışı bırakmak performansı artırır)" - }, - "enableFontSmoothing": { - "label": "Yazı Tipi Yumuşatma", - "description": "ClearType yazı tipi işlemeyi etkinleştir" - }, - "enableDesktopComposition": { - "label": "Masaüstü Birleştirme", - "description": "Aero cam efektlerini etkinleştir" - }, - "disableAudio": { - "label": "Sesi Devre Dışı Bırak", - "description": "Uzak oturumdaki tüm sesleri kapat" - }, - "enablePrinting": { - "label": "Yazdırmayı Etkinleştir", - "description": "Yerel yazıcıları uzak oturuma yönlendir" - }, - "enableDrive": { - "label": "Sürücü Yönlendirmesini Etkinleştir", - "description": "Yerel bir klasörü uzak oturumda sürücü olarak eşleştir" - }, - "disableCopy": { - "label": "Kopyalamayı Devre Dışı Bırak", - "description": "Uzak oturumdan metin kopyalamayı engelle" - }, - "disablePaste": { - "label": "Yapıştırmayı Devre Dışı Bırak", - "description": "Uzak oturuma metin yapıştırmayı engelle" - }, - "group": "Uzak Masaüstü varsayılan ayarları", - "colorDepth16": "16 bit", - "colorDepth24": "24 bit", - "colorDepth32": "32 bit" - }, - "admin": { - "enabled": { - "label": "Uzaktan Masaüstünü Etkinleştir", - "description": "guacd üzerinden RDP, VNC ve Telnet bağlantılarına izin verin." - }, - "guacdUrl": { - "label": "guacd URL'si", - "description": "Guacd'nin dinlediği yer, host:port şeklindedir. GUACD_URL, GUACD_HOST ve GUACD_PORT ortam değişkenleri bunu geçersiz kılar.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP bağlantı noktası" - }, - "vncPort": { - "label": "VNC bağlantı noktası" - }, - "telnetPort": { - "label": "Telnet bağlantı noktası" - }, - "rdpSecurity": { - "label": "RDP güvenlik modu" - }, - "rdpIgnoreCert": { - "label": "Sertifika hatalarını göz ardı edin" - }, - "guacamoleConfig": { - "label": "guacd ayarları" - }, - "enableToolbar": { - "label": "Uzak masaüstü araç çubuğu", - "description": "RDP, VNC ve Telnet oturumlarında araç çubuğunu göster." - }, - "displayGroup": "Görüntülemek", - "displayDefaultsTab": "Uzak Masaüstü" - } - }, - "palette": { - "connectRdp": "RDP ile bağlantı kurun", - "connectVnc": "VNC ile bağlantı kurun", - "connectTelnet": "Telnet ile bağlantı kurun" - }, - "permissions": { - "sessions": { - "title": "Uzak masaüstü oturumlarını paylaşın", - "description": "Oturum paylaşımı ve iş birliği odalarının bu kullanıcının RDP, VNC ve Telnet oturumlarını diğer kullanıcılara göstermesine izin verin." - } - }, - "errors": { - "remoteServerRequired": "Uzak sunucu gereklidir. Bu bağlantı türünü kullanmak için Ayarlar'dan bir uzak sunucu bağlayın." - }, - "plugin": { - "name": "Uzak Masaüstü" - } -} diff --git a/plugins/remote-desktop/locales/translated/uk_UA.json b/plugins/remote-desktop/locales/translated/uk_UA.json deleted file mode 100644 index 872e6bd32..000000000 --- a/plugins/remote-desktop/locales/translated/uk_UA.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Підключення до сеансу {{type}}...", - "checkingGuacd": "Перевірка служби віддаленого робочого столу...", - "requestingToken": "Запитується токен з’єднання {{type}}...", - "openingSession": "Відкриття сеансу {{type}}...", - "negotiating": "Переговори {{type}} рукостискання...", - "sessionReady": "{{type}} сеанс готовий", - "connectionError": "Помилка підключення", - "connectionFailed": "Помилка підключення", - "failedToConnect": "Не вдалося отримати токен з'єднання", - "hostNotFound": "Хост не знайдено", - "noHostSelected": "Хост не вибрано", - "reconnect": "Знову підключитися", - "retry": "Повторити спробу", - "guacdUnavailable": "Служба віддаленого робочого столу (guacd) недоступна. Переконайтеся, що guacd запущено, доступно та правильно налаштовано в налаштуваннях адміністратора.", - "credentialPromptTitle": "Введіть облікові дані RDP", - "credentialPromptDescription": "Цей хост налаштовано на запит облікових даних під час підключення. Вони використовуються лише для цього сеансу та не зберігаються.", - "connect": "Підключитися", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Файли Диска", - "parent": "Батьківський каталог", - "refresh": "Оновити", - "empty": "Ця папка порожня", - "listFailed": "Не вдалося прочитати цю папку", - "download": "Завантажити", - "downloadFailed": "Не вдалося завантажити {{name}}", - "upload": "Завантажити файли", - "uploading": "Завантаження {{name}}...", - "uploaded": "Завантажено {{name}}", - "uploadFailed": "Не вдалося завантажити {{name}}", - "uploadDisabled": "Завантаження для цього з’єднання вимкнено", - "driveUnavailable": "Увімкніть перенаправлення диска RDP перед завантаженням файлів", - "driveNotWritable": "guacd не може записувати дані в папку диска. Перевірте GUACD_DRIVE_PATH (або шлях до диска хоста) та його дозволи.", - "dropToUpload": "Перетягніть файли для завантаження" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Екран блокування)", - "winKey": "Клавіша Windows", - "superL": "Супер+L (Екран блокування)", - "superKey": "Супер ключ", - "super": "Супер", - "cmdL": "Cmd+L (Екран блокування)", - "cmdKey": "Клавіша команди", - "cmd": "Команда", - "ctrl": "Ctrl", - "alt": "Альтернативний", - "shift": "Зсув", - "win": "Виграй", - "stickyActive": "{{key}} (зафіксовано - натисніть, щоб відпустити)", - "stickyInactive": "{{key}} (натисніть, щоб зафіксувати)", - "esc": "Втеча", - "tab": "Вкладка", - "home": "Дім", - "end": "Кінець", - "pageUp": "Сторінка вгору", - "pageDown": "Сторінка вниз", - "arrowUp": "Стрілка вгору", - "arrowDown": "Стрілка вниз", - "arrowLeft": "Стрілка ліворуч", - "arrowRight": "Стрілка праворуч", - "fnToggle": "Функціональні клавіші", - "reconnect": "Сеанс повторного підключення", - "collapse": "Згорнути панель інструментів", - "expand": "Розгорнути панель інструментів", - "dragHandle": "Перетягніть, щоб змінити положення", - "hide": "Приховати для цього сеансу. Щоб вимкнути його назавжди, відкрийте Параметри хоста → Загальні → Панель інструментів підключення.", - "zoomIn": "Збільшити масштаб", - "zoomOut": "Зменшити масштаб", - "resetZoom": "Скинути масштаб", - "fitToScreen": "Припасувати до екрана", - "switchToTrackpad": "Перехід у режим трекпада (перетягування для переміщення курсора, торкання для клацання)", - "switchToTouch": "Перемикання в сенсорний режим (торкайтеся безпосередньо там, де потрібно клацнути)" - }, - "disabled": "Віддалений робочий стіл вимкнено. Адміністратор може ввімкнути його в налаштуваннях." - }, - "hosts": { - "nativeRdpOpened": "Відкрито у віддаленому робочому столі Windows", - "nativeRdpFailed": "Не вдалося відкрити віддалений робочий стіл Windows", - "openNativeRdp": "Відкрити у віддаленому робочому столі Windows", - "guac": { - "toolbar": "Панель інструментів", - "connection": "З'єднання", - "authentication": "Автентифікація", - "storedCredential": "Збережені облікові дані", - "authMethod": "Метод автентифікації", - "authTypeDirect": "Прямий", - "authTypeCredential": "Посвідчення", - "authTypeNone": "Жоден", - "authTypeNoneDesc": "Облікові дані не зберігаються. Вам буде запропоновано ввести ім’я користувача та пароль щоразу під час підключення; вони не зберігаються.", - "selectCredential": "Виберіть облікові дані...", - "connectionSettings": "Налаштування підключення", - "displaySettings": "Налаштування дисплея", - "audioSettings": "Налаштування аудіо", - "rdpPerformance": "Продуктивність RDP", - "deviceRedirection": "Перенаправлення пристрою", - "session": "Сесія", - "gateway": "Шлюз", - "remoteApp": "Віддалена програма", - "clipboard": "Буфер обміну", - "sessionRecording": "Запис сеансу", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Налаштування VNC", - "terminalSettings": "Налаштування терміналу", - "rdpPort": "RDP-порт", - "username": "Ім'я користувача", - "password": "Пароль", - "passwordSaved": "Пароль збережено, введіть, щоб змінити", - "domain": "Домен", - "securityMode": "Режим безпеки", - "width": "Ширина", - "height": "Висота", - "dpi": "DPI", - "clientName": "Ім'я клієнта", - "initialProgram": "Початкова програма", - "serverLayout": "Макет сервера", - "timezone": "Часовий пояс", - "loadBalanceInfo": "Інформація про балансування навантаження / файли cookie", - "loadBalanceInfoDesc": "Файл cookie брокера підключень до робочого столу для балансування навантаження ферми RDS (наприклад, tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Проксі-сервер guacd", - "guacdHostname": "Хост guacd", - "guacdHostnamePlaceholder": "Глобальний за замовчуванням", - "guacdPort": "Порт GUACD", - "guacdProxyDesc": "Перевизначте глобальний екземпляр guacd для цього з’єднання. Залиште поле порожнім, щоб використовувати глобально налаштований guacd.", - "gatewayHostname": "Ім'я хоста шлюзу", - "gatewayPort": "Порт шлюзу", - "gatewayUsername": "Ім'я користувача шлюзу", - "gatewayPassword": "Пароль шлюзу", - "gatewayDomain": "Домен шлюзу", - "remoteAppProgram": "Програма віддалених додатків", - "workingDirectory": "Робочий каталог", - "arguments": "Аргументи", - "normalizeLineEndings": "Нормалізувати закінчення рядків", - "macAddress": "MAC-адреса", - "broadcastAddress": "Адреса розсилки", - "udpPort": "UDP-порт", - "waitTimeS": "Час очікування (с)", - "driveName": "Назва диска", - "drivePath": "Шлях диска", - "ignoreCertificate": "Ігнорувати сертифікат", - "ignoreCertificateDesc": "Дозволити підключення до хостів із самопідписаними сертифікатами", - "forceLossless": "Примусово без втрат", - "forceLosslessDesc": "Примусове кодування зображень без втрат (вища якість, більша пропускна здатність)", - "disableAudio": "Вимкнути аудіо", - "disableAudioDesc": "Вимкнути весь звук під час віддаленого сеансу", - "enableAudioInput": "Увімкнути аудіовхід (мікрофон)", - "enableAudioInputDesc": "Переадресація локального мікрофона на віддалений сеанс", - "wallpaper": "Шпалери", - "wallpaperDesc": "Показувати шпалери робочого столу (вимикання покращує продуктивність)", - "theming": "Тематизація", - "themingDesc": "Увімкнути візуальні теми та стилі", - "fontSmoothing": "Згладжування шрифтів", - "fontSmoothingDesc": "Увімкнути рендеринг шрифтів ClearType", - "fullWindowDrag": "Повне перетягування вікна", - "fullWindowDragDesc": "Відображати вміст вікна під час перетягування", - "desktopComposition": "Композиція робочого столу", - "desktopCompositionDesc": "Увімкнути ефекти Aero Glass", - "menuAnimations": "Анімації меню", - "menuAnimationsDesc": "Увімкнути анімацію згасання меню та слайдів", - "disableBitmapCaching": "Вимкнути кешування растрових зображень", - "disableBitmapCachingDesc": "Вимкніть кеш растрових зображень (може допомогти з глюками)", - "disableOffscreenCaching": "Вимкнути кешування поза екраном", - "disableOffscreenCachingDesc": "Вимкнути кеш поза екраном", - "disableGlyphCaching": "Вимкнути кешування гліфів", - "disableGlyphCachingDesc": "Вимкнути кеш гліфів", - "enableGfx": "Увімкнути графічні ефекти", - "enableGfxDesc": "Використовуйте графічний конвеєр RemoteFX", - "enablePrinting": "Увімкнути друк", - "enablePrintingDesc": "Перенаправити локальні принтери на віддалений сеанс", - "enableDriveRedirection": "Увімкнути перенаправлення диска", - "enableDriveRedirectionDesc": "Призначення локальної папки як диска у віддаленому сеансі", - "createDrivePath": "Створити шлях до диска", - "createDrivePathDesc": "Автоматично створювати папку, якщо її не існує", - "disableDownload": "Вимкнути завантаження", - "disableDownloadDesc": "Заборонити завантаження файлів з віддаленого сеансу", - "disableUpload": "Вимкнути завантаження", - "disableUploadDesc": "Заборонити завантаження файлів у віддалений сеанс", - "enableTouch": "Увімкнути сенсорний режим", - "enableTouchDesc": "Увімкнути переадресацію сенсорного введення", - "consoleSession": "Сеанс консолі", - "consoleSessionDesc": "Підключення до консолі (сеанс 0) замість нового сеансу", - "sendWolPacket": "Надіслати пакет WOL", - "sendWolPacketDesc": "Надіслати магічний пакет, щоб розбудити цей хост перед підключенням", - "disableCopy": "Вимкнути копіювання", - "disableCopyDesc": "Запобігти копіюванню тексту з віддаленого сеансу", - "disablePaste": "Вимкнути вставку", - "disablePasteDesc": "Запобігти вставці тексту у віддалений сеанс", - "createPathIfMissing": "Створити шлях, якщо він відсутній", - "createPathIfMissingDesc": "Автоматично створити каталог записів", - "excludeOutput": "Виключити вивід", - "excludeOutputDesc": "Не записувати вивід екрана (лише метадані)", - "excludeMouse": "Виключити мишу", - "excludeMouseDesc": "Не записувати рухи миші", - "includeKeystrokes": "Включити натискання клавіш", - "includeKeystrokesDesc": "Записуйте необроблені натискання клавіш на додаток до виводу на екран", - "vncPort": "VNC-порт", - "vncPassword": "Пароль VNC", - "vncUsernameOptional": "Ім'я користувача (необов'язково)", - "vncLeaveBlank": "Залиште порожнім, якщо не потрібно", - "cursorMode": "Режим курсора", - "swapRedBlue": "Поміняйте місцями червоний/синій", - "swapRedBlueDesc": "Поміняти місцями червоний та синій кольорові канали (виправляє деякі проблеми з кольором)", - "readOnly": "Тільки для читання", - "readOnlyDesc": "Перегляд віддаленого екрана без надсилання будь-яких вхідних даних", - "telnetPort": "Порт Telnet", - "terminalType": "Тип терміналу", - "fontName": "Назва шрифту", - "fontSize": "Розмір шрифту", - "colorScheme": "Колірна схема", - "backspaceKey": "Клавіша Backspace", - "colorDepth": "Глибина кольору", - "resizeMethod": "Метод зміни розміру", - "driveNamePlaceholder": "Термікс Драйв", - "options": { - "any": "Будь-який", - "auto": "Авто", - "nlaExtended": "Розширена НЛА", - "color8": "8-бітний", - "color16": "16-бітний", - "color24": "24-бітний", - "color32": "32-бітний", - "displayUpdate": "Оновлення дисплея", - "reconnect": "Знову підключитися", - "preserve": "Зберегти", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Місцевий", - "remote": "Віддалений", - "failsafe": "безвідмовний", - "blackOnWhite": "Чорне на білому", - "whiteOnBlack": "Біле на чорному", - "grayOnBlack": "Сірий на чорному", - "greenOnBlack": "Зелений на чорному" - } - }, - "remoteDesktop": "Віддалений робочий стіл", - "tabRdp": "ПРП", - "tabVnc": "VNC", - "tabTelnet": "Телнет", - "virtualNetwork": "Віртуальна мережа", - "unencryptedShell": "Незашифрована оболонка", - "remoteDesktopNotes": "Нотатки", - "userDefaultsNote": "Налаштування, які ви залишаєте без змін, відповідають налаштуванням вашого хоста за замовчуванням.", - "connectionOriginNote": "У настільному застосунку RDP, VNC та Telnet використовують віддалений сервер, якщо для хоста не налаштовано значення «Цей пристрій», для якого потрібен GUACD, до якого може отримати доступ ця машина. Встановіть його в налаштуваннях віддаленого робочого столу або для кожного хоста в розділі GUACD Proxy.", - "docsLink": "Переглянути документи", - "connectionOriginGuacamoleNote": "RDP, VNC та Telnet використовують віддалений сервер, якщо ви не виберете \"Цей пристрій\", для якого потрібен GUACD, до якого може отримати доступ ця машина. Встановіть його в налаштуваннях URL-адреси GUACD або для кожного хоста в розділі \"Проксі-сервер GUACD\"." - }, - "onboarding": { - "feature_desktop": "Віддалений робочий стіл", - "feature_desktop_desc": "Відкривати сеанси RDP, VNC та Telnet у вкладці, так само, як SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Глибина кольору", - "description": "Використовується для хостів RDP, які залишають його за замовчуванням." - }, - "resizeMethod": { - "label": "Метод зміни розміру", - "description": "Як віддалений екран відповідає розміру вікна." - }, - "resizeDisplayUpdate": "Оновлення дисплея", - "resizeReconnect": "Знову підключитися", - "inherit": "Успадкувати", - "on": "Увімкнено", - "off": "Вимкнено", - "forceLossless": { - "label": "Примусово без втрат", - "description": "Примусове кодування зображень без втрат (вища якість, більша пропускна здатність)" - }, - "enableWallpaper": { - "label": "Шпалери", - "description": "Показувати шпалери робочого столу (вимикання покращує продуктивність)" - }, - "enableFontSmoothing": { - "label": "Згладжування шрифтів", - "description": "Увімкнути рендеринг шрифтів ClearType" - }, - "enableDesktopComposition": { - "label": "Композиція робочого столу", - "description": "Увімкнути ефекти Aero Glass" - }, - "disableAudio": { - "label": "Вимкнути аудіо", - "description": "Вимкнути весь звук під час віддаленого сеансу" - }, - "enablePrinting": { - "label": "Увімкнути друк", - "description": "Перенаправити локальні принтери на віддалений сеанс" - }, - "enableDrive": { - "label": "Увімкнути перенаправлення диска", - "description": "Призначення локальної папки як диска у віддаленому сеансі" - }, - "disableCopy": { - "label": "Вимкнути копіювання", - "description": "Запобігти копіюванню тексту з віддаленого сеансу" - }, - "disablePaste": { - "label": "Вимкнути вставку", - "description": "Запобігти вставці тексту у віддалений сеанс" - }, - "group": "Налаштування віддаленого робочого столу за замовчуванням", - "colorDepth16": "16-бітний", - "colorDepth24": "24-бітний", - "colorDepth32": "32-бітний" - }, - "admin": { - "enabled": { - "label": "Увімкнути віддалений робочий стіл", - "description": "Дозволити з'єднання RDP, VNC та Telnet через guacd." - }, - "guacdUrl": { - "label": "URL-адреса GUACD", - "description": "Де guacd прослуховує, як host:port. Змінні середовища GUACD_URL, GUACD_HOST та GUACD_PORT перевизначають це.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "ПРП" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Телнет" - }, - "rdpPort": { - "label": "RDP-порт" - }, - "vncPort": { - "label": "VNC-порт" - }, - "telnetPort": { - "label": "Порт Telnet" - }, - "rdpSecurity": { - "label": "Режим безпеки RDP" - }, - "rdpIgnoreCert": { - "label": "Ігнорувати помилки сертифіката" - }, - "guacamoleConfig": { - "label": "налаштування GUACD" - }, - "enableToolbar": { - "label": "Панель інструментів віддаленого робочого столу", - "description": "Показувати панель інструментів у сеансах RDP, VNC та Telnet." - }, - "displayGroup": "Дисплей", - "displayDefaultsTab": "Віддалений робочий стіл" - } - }, - "palette": { - "connectRdp": "Зв'язок з RDP", - "connectVnc": "Зв'яжіться з VNC", - "connectTelnet": "З’єднання за допомогою Telnet" - }, - "permissions": { - "sessions": { - "title": "Спільний доступ до сеансів віддаленого робочого столу", - "description": "Дозволити спільний доступ до сеансів та кімнати для співпраці показувати сеанси RDP, VNC та Telnet цього користувача іншим." - } - }, - "errors": { - "remoteServerRequired": "Потрібен віддалений сервер. Підключіться до віддаленого сервера в налаштуваннях, щоб використовувати цей тип підключення." - }, - "plugin": { - "name": "Віддалений робочий стіл" - } -} diff --git a/plugins/remote-desktop/locales/translated/vi_VN.json b/plugins/remote-desktop/locales/translated/vi_VN.json deleted file mode 100644 index 7f9ab8216..000000000 --- a/plugins/remote-desktop/locales/translated/vi_VN.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "Đang kết nối đến phiên {{type}}...", - "checkingGuacd": "Kiểm tra dịch vụ máy tính từ xa...", - "requestingToken": "Yêu cầu mã thông báo kết nối {{type}}...", - "openingSession": "Khai mạc phiên {{type}}...", - "negotiating": "Đàm phán bằng cái bắt tay {{type}}...", - "sessionReady": "{{type}} phiên đã sẵn sàng", - "connectionError": "Lỗi kết nối", - "connectionFailed": "Kết nối thất bại", - "failedToConnect": "Không lấy được mã token kết nối", - "hostNotFound": "Không tìm thấy máy chủ", - "noHostSelected": "Chưa chọn máy chủ", - "reconnect": "Kết nối lại", - "retry": "Thử lại", - "guacdUnavailable": "Dịch vụ máy tính từ xa (guacd) không khả dụng. Vui lòng đảm bảo guacd đang chạy, có thể truy cập và được cấu hình đúng trong cài đặt quản trị.", - "credentialPromptTitle": "Nhập thông tin đăng nhập RDP", - "credentialPromptDescription": "Máy chủ này được thiết lập để yêu cầu thông tin đăng nhập khi kết nối. Thông tin này chỉ được sử dụng cho phiên này và không được lưu lại.", - "connect": "Kết nối", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "Tệp ổ đĩa", - "parent": "Thư mục cha", - "refresh": "Làm mới", - "empty": "Thư mục này trống", - "listFailed": "Không thể đọc thư mục này.", - "download": "Tải xuống", - "downloadFailed": "Tải xuống không thành công {{name}}", - "upload": "Tải lên tệp", - "uploading": "Đang tải lên {{name}}...", - "uploaded": "Đã tải lên {{name}}", - "uploadFailed": "Tải lên không thành công {{name}}", - "uploadDisabled": "Chức năng tải lên đã bị vô hiệu hóa cho kết nối này.", - "driveUnavailable": "Hãy bật tính năng chuyển hướng ổ đĩa RDP trước khi tải lên tệp.", - "driveNotWritable": "guacd không thể ghi vào thư mục ổ đĩa. Hãy kiểm tra GUACD_DRIVE_PATH (hoặc Đường dẫn ổ đĩa của máy chủ) và quyền truy cập của nó.", - "dropToUpload": "Kéo thả tệp để tải lên" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (Khóa màn hình)", - "winKey": "Phím Windows", - "superL": "Super+L (Màn hình khóa)", - "superKey": "Siêu chìa khóa", - "super": "Siêu", - "cmdL": "Cmd+L (Khóa màn hình)", - "cmdKey": "Phím lệnh", - "cmd": "Lệnh", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}} (đang giữ - nhấn để nhả)", - "stickyInactive": "{{key}} (nhấn để giữ)", - "esc": "Escape", - "tab": "Tab", - "home": "Home", - "end": "End", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "Mũi tên lên", - "arrowDown": "Mũi tên xuống", - "arrowLeft": "Mũi tên trái", - "arrowRight": "Mũi tên phải", - "fnToggle": "Phím chức năng", - "reconnect": "Kết nối lại phiên", - "collapse": "Thu gọn thanh công cụ", - "expand": "Mở rộng thanh công cụ", - "dragHandle": "Kéo để di chuyển", - "hide": "Ẩn tùy chọn này trong phiên làm việc hiện tại. Để tắt vĩnh viễn, hãy mở Cài đặt máy chủ → Chung → Thanh công cụ kết nối.", - "zoomIn": "Phóng to", - "zoomOut": "Thu nhỏ", - "resetZoom": "Đặt lại thu phóng", - "fitToScreen": "Vừa với màn hình", - "switchToTrackpad": "Chuyển sang chế độ bàn di chuột (kéo để di chuyển con trỏ, chạm để nhấp chuột)", - "switchToTouch": "Chuyển sang chế độ cảm ứng (chạm trực tiếp vào vị trí bạn muốn nhấp chuột)" - }, - "disabled": "Chức năng Truy cập Máy tính Từ xa (Remote Desktop) đã bị tắt. Quản trị viên có thể bật chức năng này trong phần Cài đặt." - }, - "hosts": { - "nativeRdpOpened": "Đã mở bằng Windows Remote Desktop", - "nativeRdpFailed": "Không thể mở Windows Remote Desktop", - "openNativeRdp": "Mở bằng Windows Remote Desktop", - "guac": { - "toolbar": "Thanh công cụ", - "connection": "Kết nối", - "authentication": "Xác thực", - "storedCredential": "Thông tin xác thực đã lưu", - "authMethod": "Phương thức xác thực", - "authTypeDirect": "Trực tiếp", - "authTypeCredential": "Thông tin xác thực", - "authTypeNone": "Không chọn", - "authTypeNoneDesc": "Thông tin đăng nhập của bạn không được lưu trữ. Bạn sẽ được yêu cầu nhập tên người dùng và mật khẩu mỗi khi kết nối; chúng không được lưu lại.", - "selectCredential": "Chọn thông tin xác thực...", - "connectionSettings": "Cài đặt kết nối", - "displaySettings": "Cài đặt hiển thị", - "audioSettings": "Cài đặt âm thanh", - "rdpPerformance": "Hiệu suất RDP", - "deviceRedirection": "Chuyển hướng thiết bị", - "session": "Phiên làm việc", - "gateway": "Gateway", - "remoteApp": "RemoteApp", - "clipboard": "Clipboard", - "sessionRecording": "Ghi phiên", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "Cài đặt VNC", - "terminalSettings": "Cài đặt Terminal", - "rdpPort": "Cổng RDP", - "username": "Tên người dùng", - "password": "Mật khẩu", - "passwordSaved": "Mật khẩu đã được lưu, nhập để thay đổi", - "domain": "Miền", - "securityMode": "Chế độ bảo mật", - "width": "Chiều rộng", - "height": "Chiều cao", - "dpi": "DPI", - "clientName": "Tên máy khách", - "initialProgram": "Chương trình ban đầu", - "serverLayout": "Bố cục máy chủ", - "timezone": "Múi giờ", - "loadBalanceInfo": "Thông tin cân bằng tải / Cookie", - "loadBalanceInfoDesc": "Cookie RD Connection Broker dùng cho cân bằng tải cụm RDS (vd: tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "Proxy guacd", - "guacdHostname": "Máy chủ guacd", - "guacdHostnamePlaceholder": "Mặc định toàn cục", - "guacdPort": "Cổng guacd", - "guacdProxyDesc": "Ghi đè phiên bản guacd toàn cục cho kết nối này. Để trống để sử dụng guacd được cấu hình toàn cục.", - "gatewayHostname": "Tên máy chủ gateway", - "gatewayPort": "Cổng gateway", - "gatewayUsername": "Tên người dùng gateway", - "gatewayPassword": "Mật khẩu gateway", - "gatewayDomain": "Miền gateway", - "remoteAppProgram": "Chương trình RemoteApp", - "workingDirectory": "Thư mục làm việc", - "arguments": "Tham số", - "normalizeLineEndings": "Chuẩn hóa kết thúc dòng", - "macAddress": "Địa chỉ MAC", - "broadcastAddress": "Địa chỉ broadcast", - "udpPort": "Cổng UDP", - "waitTimeS": "Thời gian chờ (s)", - "driveName": "Tên ổ đĩa", - "drivePath": "Đường dẫn ổ đĩa", - "ignoreCertificate": "Bỏ qua chứng chỉ", - "ignoreCertificateDesc": "Cho phép kết nối đến máy chủ có chứng chỉ tự ký", - "forceLossless": "Ép buộc Lossless", - "forceLosslessDesc": "Ép buộc mã hóa ảnh không mất dữ liệu (chất lượng cao hơn, tốn băng thông)", - "disableAudio": "Tắt âm thanh", - "disableAudioDesc": "Tắt tất cả âm thanh từ phiên từ xa", - "enableAudioInput": "Bật đầu vào âm thanh (micro)", - "enableAudioInputDesc": "Chuyển tiếp micro cục bộ đến phiên từ xa", - "wallpaper": "Hình nền", - "wallpaperDesc": "Hiển thị hình nền desktop (tắt để cải thiện hiệu suất)", - "theming": "Chủ đề", - "themingDesc": "Bật chủ đề và kiểu dáng trực quan", - "fontSmoothing": "Làm mịn phông chữ", - "fontSmoothingDesc": "Bật kết xuất phông chữ ClearType", - "fullWindowDrag": "Kéo toàn bộ cửa sổ", - "fullWindowDragDesc": "Hiển thị nội dung cửa sổ khi kéo", - "desktopComposition": "Tổ hợp màn hình nền", - "desktopCompositionDesc": "Bật hiệu ứng kính Aero", - "menuAnimations": "Hiệu ứng menu", - "menuAnimationsDesc": "Bật hiệu ứng mờ và trượt của menu", - "disableBitmapCaching": "Tắt bộ nhớ đệm bitmap", - "disableBitmapCachingDesc": "Tắt bộ nhớ đệm bitmap (có thể giúp tránh lỗi hiển thị)", - "disableOffscreenCaching": "Tắt bộ nhớ đệm ngoài màn hình", - "disableOffscreenCachingDesc": "Tắt bộ nhớ đệm ngoài màn hình", - "disableGlyphCaching": "Tắt bộ nhớ đệm glyph", - "disableGlyphCachingDesc": "Tắt bộ nhớ đệm glyph", - "enableGfx": "Bật GFX", - "enableGfxDesc": "Sử dụng pipeline đồ họa RemoteFX", - "enablePrinting": "Bật in", - "enablePrintingDesc": "Chuyển hướng máy in cục bộ đến phiên từ xa", - "enableDriveRedirection": "Bật chuyển hướng ổ đĩa", - "enableDriveRedirectionDesc": "Ánh xạ thư mục cục bộ thành ổ đĩa trong phiên từ xa", - "createDrivePath": "Tạo đường dẫn ổ đĩa", - "createDrivePathDesc": "Tự động tạo thư mục nếu chưa tồn tại", - "disableDownload": "Tắt tải xuống", - "disableDownloadDesc": "Ngăn tải file từ phiên từ xa", - "disableUpload": "Tắt tải lên", - "disableUploadDesc": "Ngăn tải file lên phiên từ xa", - "enableTouch": "Bật cảm ứng", - "enableTouchDesc": "Bật chuyển tiếp đầu vào cảm ứng", - "consoleSession": "Phiên console", - "consoleSessionDesc": "Kết nối tới console (phiên 0) thay vì phiên mới", - "sendWolPacket": "Gửi gói WOL", - "sendWolPacketDesc": "Gửi gói magic để đánh thức máy trước khi kết nối", - "disableCopy": "Tắt sao chép", - "disableCopyDesc": "Ngăn sao chép văn bản từ phiên từ xa", - "disablePaste": "Tắt dán", - "disablePasteDesc": "Ngăn dán văn bản vào phiên từ xa", - "createPathIfMissing": "Tạo đường dẫn nếu thiếu", - "createPathIfMissingDesc": "Tự động tạo thư mục ghi hình", - "excludeOutput": "Loại trừ đầu ra", - "excludeOutputDesc": "Không ghi hình màn hình (chỉ metadata)", - "excludeMouse": "Loại trừ chuột", - "excludeMouseDesc": "Không ghi lại chuyển động chuột", - "includeKeystrokes": "Bao gồm thao tác phím", - "includeKeystrokesDesc": "Ghi lại thao tác phím thô cùng với đầu ra màn hình", - "vncPort": "Cổng VNC", - "vncPassword": "Mật khẩu VNC", - "vncUsernameOptional": "Tên người dùng (tùy chọn)", - "vncLeaveBlank": "Để trống nếu không cần", - "cursorMode": "Chế độ con trỏ", - "swapRedBlue": "Hoán đổi Đỏ/Xanh", - "swapRedBlueDesc": "Hoán đổi kênh màu đỏ và xanh (khắc phục một số lỗi màu)", - "readOnly": "Chỉ đọc", - "readOnlyDesc": "Xem màn hình từ xa mà không gửi đầu vào", - "telnetPort": "Cổng Telnet", - "terminalType": "Loại terminal", - "fontName": "Tên phông chữ", - "fontSize": "Cỡ chữ", - "colorScheme": "Bảng màu", - "backspaceKey": "Phím Backspace", - "colorDepth": "Độ sâu màu", - "resizeMethod": "Phương thức thay đổi kích thước", - "driveNamePlaceholder": "Đường Termix", - "options": { - "any": "Bất kì", - "auto": "Tự động", - "nlaExtended": "NLA mở rộng", - "color8": "8-bit", - "color16": "16-bit", - "color24": "24 bit", - "color32": "32-bit", - "displayUpdate": "Cập nhật hiển thị", - "reconnect": "Kết nối lại", - "preserve": "Bảo tồn", - "unix": "Unix (LF)", - "windows": "Windows (CRLF)", - "local": "Cục bộ", - "remote": "Từ xa", - "failsafe": "an toàn", - "blackOnWhite": "Đen trên nền trắng", - "whiteOnBlack": "Trắng trên nền đen", - "grayOnBlack": "Xám trên nền đen", - "greenOnBlack": "Xanh lá cây trên nền đen" - } - }, - "remoteDesktop": "Máy tính từ xa", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "Mạng ảo", - "unencryptedShell": "Shell không mã hóa", - "remoteDesktopNotes": "Ghi chú", - "userDefaultsNote": "Các thiết lập bạn giữ nguyên sẽ tuân theo cài đặt mặc định của máy chủ.", - "connectionOriginNote": "Trên ứng dụng máy tính để bàn, RDP, VNC và Telnet sử dụng máy chủ từ xa trừ khi máy chủ được đặt thành Thiết bị này, điều này yêu cầu một máy ảo mà guacd này có thể truy cập được. Hãy thiết lập một máy ảo trong cài đặt Máy tính từ xa hoặc cho từng máy chủ trong guacd Proxy.", - "docsLink": "Xem tài liệu", - "connectionOriginGuacamoleNote": "RDP, VNC và Telnet sử dụng máy chủ từ xa trừ khi bạn chọn \"Thiết bị này\", tùy chọn này yêu cầu máy ảo guacd mà máy này có thể truy cập được. Hãy thiết lập máy ảo này trong cài đặt URL của guacd hoặc cho từng máy chủ trong phần Proxy của guacd." - }, - "onboarding": { - "feature_desktop": "Máy tính từ xa", - "feature_desktop_desc": "Mở các phiên RDP, VNC và Telnet trong cùng một tab, tương tự như SSH." - }, - "settings": { - "user": { - "colorDepth": { - "label": "Độ sâu màu", - "description": "Được sử dụng cho các máy chủ RDP để chế độ mặc định." - }, - "resizeMethod": { - "label": "Phương thức thay đổi kích thước", - "description": "Màn hình điều khiển từ xa tự động điều chỉnh theo kích thước cửa sổ." - }, - "resizeDisplayUpdate": "Cập nhật hiển thị", - "resizeReconnect": "Kết nối lại", - "inherit": "Kế thừa", - "on": "Bật", - "off": "Tắt", - "forceLossless": { - "label": "Ép buộc Lossless", - "description": "Ép buộc mã hóa ảnh không mất dữ liệu (chất lượng cao hơn, tốn băng thông)" - }, - "enableWallpaper": { - "label": "Hình nền", - "description": "Hiển thị hình nền desktop (tắt để cải thiện hiệu suất)" - }, - "enableFontSmoothing": { - "label": "Làm mịn phông chữ", - "description": "Bật kết xuất phông chữ ClearType" - }, - "enableDesktopComposition": { - "label": "Tổ hợp màn hình nền", - "description": "Bật hiệu ứng kính Aero" - }, - "disableAudio": { - "label": "Tắt âm thanh", - "description": "Tắt tất cả âm thanh từ phiên từ xa" - }, - "enablePrinting": { - "label": "Bật in", - "description": "Chuyển hướng máy in cục bộ đến phiên từ xa" - }, - "enableDrive": { - "label": "Bật chuyển hướng ổ đĩa", - "description": "Ánh xạ thư mục cục bộ thành ổ đĩa trong phiên từ xa" - }, - "disableCopy": { - "label": "Tắt sao chép", - "description": "Ngăn sao chép văn bản từ phiên từ xa" - }, - "disablePaste": { - "label": "Tắt dán", - "description": "Ngăn dán văn bản vào phiên từ xa" - }, - "group": "Cài đặt mặc định của Remote Desktop", - "colorDepth16": "16-bit", - "colorDepth24": "24 bit", - "colorDepth32": "32-bit" - }, - "admin": { - "enabled": { - "label": "Bật tính năng truy cập máy tính từ xa", - "description": "Cho phép các kết nối RDP, VNC và Telnet thông qua guacd." - }, - "guacdUrl": { - "label": "URL guacd", - "description": "Nơi guacd lắng nghe, được định dạng là host:port. Các biến môi trường GUACD_URL, GUACD_HOST và GUACD_PORT sẽ ghi đè lên định dạng này.", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "Cổng RDP" - }, - "vncPort": { - "label": "Cổng VNC" - }, - "telnetPort": { - "label": "Cổng Telnet" - }, - "rdpSecurity": { - "label": "Chế độ bảo mật RDP" - }, - "rdpIgnoreCert": { - "label": "Bỏ qua lỗi chứng chỉ" - }, - "guacamoleConfig": { - "label": "cài đặt guacd" - }, - "enableToolbar": { - "label": "thanh công cụ máy tính từ xa", - "description": "Hiển thị thanh công cụ trong các phiên RDP, VNC và Telnet." - }, - "displayGroup": "Trưng bày", - "displayDefaultsTab": "Máy tính từ xa" - } - }, - "palette": { - "connectRdp": "Kết nối với RDP", - "connectVnc": "Kết nối với VNC", - "connectTelnet": "Kết nối bằng Telnet" - }, - "permissions": { - "sessions": { - "title": "Chia sẻ phiên làm việc từ xa", - "description": "Cho phép chia sẻ phiên và phòng cộng tác hiển thị các phiên RDP, VNC và Telnet của người dùng này cho những người khác." - } - }, - "errors": { - "remoteServerRequired": "Cần có máy chủ từ xa. Kết nối máy chủ từ xa trong Cài đặt để sử dụng loại kết nối này." - }, - "plugin": { - "name": "Máy tính từ xa" - } -} diff --git a/plugins/remote-desktop/locales/translated/zh_CN.json b/plugins/remote-desktop/locales/translated/zh_CN.json deleted file mode 100644 index c69e12836..000000000 --- a/plugins/remote-desktop/locales/translated/zh_CN.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "正在连接到 {{type}} 会话...", - "checkingGuacd": "正在检查远程桌面服务...", - "requestingToken": "Requesting {{type}} connection token...", - "openingSession": "正在开启 {{type}} 会话...", - "negotiating": "Negotiating {{type}} handshake...", - "sessionReady": "{{type}} 会话已准备就绪", - "connectionError": "连接错误", - "connectionFailed": "连接失败", - "failedToConnect": "获取连接令牌失败", - "hostNotFound": "未找到主机", - "noHostSelected": "未选择主机", - "reconnect": "重新连接", - "retry": "重试", - "guacdUnavailable": "远程桌面服务 (guacd) 不可用。请确保 guacd 正在运行并可访问,且在管理设置中正确配置。", - "credentialPromptTitle": "输入 RDP 凭据", - "credentialPromptDescription": "此主机设置为在连接时提示输入凭据。这些凭据仅用于当前会话,不会被保存。", - "connect": "连接", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "驱动器文件", - "parent": "父目录", - "refresh": "刷新", - "empty": "此文件夹为空", - "listFailed": "读取此文件夹失败", - "download": "下载", - "downloadFailed": "下载失败 {{name}}", - "upload": "上传文件", - "uploading": "正在上传 {{name}}...", - "uploaded": "已上传 {{name}}", - "uploadFailed": "上传失败 {{name}}", - "uploadDisabled": "此连接已禁用上传功能", - "driveUnavailable": "上传文件前启用 RDP 驱动器重定向", - "driveNotWritable": "guacd 无法写入驱动器文件夹。请检查 GUACD_DRIVE_PATH(或主机驱动器路径)及其权限。", - "dropToUpload": "拖放文件即可上传" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (锁定屏幕)", - "winKey": "Windows 键", - "superL": "Super+L(锁屏)", - "superKey": "超级密钥", - "super": "极好的", - "cmdL": "Cmd+L(锁屏)", - "cmdKey": "命令键", - "cmd": "命令", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}}(已锁定 - 点击释放)", - "stickyInactive": "{{key}}(点击锁定)", - "esc": "Escape", - "tab": "Tab", - "home": "Home", - "end": "End", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "Arrow Up", - "arrowDown": "Arrow Down", - "arrowLeft": "Arrow Left", - "arrowRight": "Arrow Right", - "fnToggle": "功能键", - "reconnect": "重新连接会话", - "collapse": "折叠工具栏", - "expand": "展开工具栏", - "dragHandle": "拖动以重新定位", - "hide": "本次会话期间隐藏。要永久禁用,请打开“主机设置”→“常规”→“连接工具栏”。", - "zoomIn": "放大", - "zoomOut": "缩小", - "resetZoom": "重置缩放", - "fitToScreen": "适应屏幕", - "switchToTrackpad": "切换到触控板模式(拖动移动光标,轻点进行点击)", - "switchToTouch": "切换到触摸模式(直接点击你想点击的地方)" - }, - "disabled": "远程桌面已关闭。管理员可以在“设置”中启用它。" - }, - "hosts": { - "nativeRdpOpened": "在 Windows 远程桌面中打开", - "nativeRdpFailed": "无法打开 Windows 远程桌面", - "openNativeRdp": "在 Windows 远程桌面中打开", - "guac": { - "toolbar": "工具栏", - "connection": "连接", - "authentication": "认证", - "storedCredential": "已存储凭据", - "authMethod": "认证方法", - "authTypeDirect": "直接", - "authTypeCredential": "凭据", - "authTypeNone": "无", - "authTypeNoneDesc": "不会存储任何凭据。每次连接时,系统都会提示您输入用户名和密码;这些信息不会被保存。", - "selectCredential": "选择凭据...", - "connectionSettings": "连接设置", - "displaySettings": "显示设置", - "audioSettings": "音频设置", - "rdpPerformance": "RDP 性能", - "deviceRedirection": "设备重定向", - "session": "会话", - "gateway": "网关", - "remoteApp": "RemoteApp", - "clipboard": "剪贴板", - "sessionRecording": "会话录制", - "wakeOnLan": "网络唤醒", - "vncSettings": "VNC 设置", - "terminalSettings": "终端设置", - "rdpPort": "RDP 端口", - "username": "用户名", - "password": "密码", - "passwordSaved": "密码已保存,输入即可更改", - "domain": "域", - "securityMode": "安全模式", - "width": "宽度", - "height": "高度", - "dpi": "DPI", - "clientName": "客户端名称", - "initialProgram": "初始程序", - "serverLayout": "服务器布局", - "timezone": "时区", - "loadBalanceInfo": "负载均衡信息 / Cookie", - "loadBalanceInfoDesc": "RD 连接代理 Cookie,用于 RDS 场负载均衡(例如 tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd 代理", - "guacdHostname": "guacd 主机", - "guacdHostnamePlaceholder": "全局默认", - "guacdPort": "guacd 端口", - "guacdProxyDesc": "为此连接覆盖全局 guacd 实例。留空则使用全局配置的 guacd。", - "gatewayHostname": "网关主机名", - "gatewayPort": "网关端口", - "gatewayUsername": "网关用户名", - "gatewayPassword": "网关密码", - "gatewayDomain": "网关域", - "remoteAppProgram": "RemoteApp 程序", - "workingDirectory": "工作目录", - "arguments": "参数", - "normalizeLineEndings": "规范化行尾", - "macAddress": "MAC 地址", - "broadcastAddress": "广播地址", - "udpPort": "UDP 端口", - "waitTimeS": "等待时间(秒)", - "driveName": "驱动器名称", - "drivePath": "驱动器路径", - "ignoreCertificate": "忽略证书", - "ignoreCertificateDesc": "允许连接到具有自签名证书的主机", - "forceLossless": "强制无损", - "forceLosslessDesc": "强制无损图像编码(质量更高,占用更多带宽)", - "disableAudio": "禁用音频", - "disableAudioDesc": "将远程会话的所有音频静音", - "enableAudioInput": "启用音频输入(麦克风)", - "enableAudioInputDesc": "将本地麦克风转发到远程会话", - "wallpaper": "壁纸", - "wallpaperDesc": "显示桌面壁纸(禁用可提高性能)", - "theming": "主题", - "themingDesc": "启用视觉主题和样式", - "fontSmoothing": "字体平滑", - "fontSmoothingDesc": "启用 ClearType 字体渲染", - "fullWindowDrag": "全窗口拖动", - "fullWindowDragDesc": "拖动时显示窗口内容", - "desktopComposition": "桌面合成", - "desktopCompositionDesc": "启用 Aero 玻璃效果", - "menuAnimations": "菜单动画", - "menuAnimationsDesc": "启用菜单淡入淡出与滑动动画", - "disableBitmapCaching": "禁用位图缓存", - "disableBitmapCachingDesc": "关闭位图缓存(可能有助于解决显示故障)", - "disableOffscreenCaching": "禁用离屏缓存", - "disableOffscreenCachingDesc": "关闭离屏缓存", - "disableGlyphCaching": "禁用字形缓存", - "disableGlyphCachingDesc": "关闭字形缓存", - "enableGfx": "启用 GFX", - "enableGfxDesc": "使用 RemoteFX 图形管道", - "enablePrinting": "启用打印", - "enablePrintingDesc": "将本地打印机重定向到远程会话", - "enableDriveRedirection": "启用驱动器重定向", - "enableDriveRedirectionDesc": "将本地文件夹映射为远程会话中的驱动器", - "createDrivePath": "创建驱动器路径", - "createDrivePathDesc": "如果文件夹不存在,则自动创建", - "disableDownload": "禁用下载", - "disableDownloadDesc": "禁止从远程会话下载文件", - "disableUpload": "禁用上传", - "disableUploadDesc": "禁止上传文件到远程会话", - "enableTouch": "启用触控", - "enableTouchDesc": "启用触控输入转发", - "consoleSession": "控制台会话", - "consoleSessionDesc": "连接到控制台(会话 0)而不是新会话", - "sendWolPacket": "发送 WOL 数据包", - "sendWolPacketDesc": "在连接前发送魔术包以唤醒此主机", - "disableCopy": "禁用复制", - "disableCopyDesc": "禁止从远程会话复制文本", - "disablePaste": "禁用粘贴", - "disablePasteDesc": "禁止粘贴文本到远程会话", - "createPathIfMissing": "如果路径不存在则创建", - "createPathIfMissingDesc": "自动创建录制目录", - "excludeOutput": "排除输出", - "excludeOutputDesc": "不录制屏幕输出(仅元数据)", - "excludeMouse": "排除鼠标", - "excludeMouseDesc": "不录制鼠标移动", - "includeKeystrokes": "包含击键", - "includeKeystrokesDesc": "除了屏幕输出外,还录制原始击键", - "vncPort": "VNC 端口", - "vncPassword": "VNC 密码", - "vncUsernameOptional": "用户名 (可选)", - "vncLeaveBlank": "如不需要请留空", - "cursorMode": "光标模式", - "swapRedBlue": "交换红/蓝", - "swapRedBlueDesc": "交换红蓝色彩通道 (可修复某些色彩问题)", - "readOnly": "只读", - "readOnlyDesc": "仅查看远程屏幕,不发送任何输入", - "telnetPort": "Telnet 端口", - "terminalType": "终端类型", - "fontName": "字体名称", - "fontSize": "字体大小", - "colorScheme": "配色方案", - "backspaceKey": "退格键", - "colorDepth": "颜色深度", - "resizeMethod": "调整大小方法", - "driveNamePlaceholder": "特米克斯大道", - "options": { - "any": "任何", - "auto": "自动", - "nlaExtended": "NLA 扩展", - "color8": "8 位", - "color16": "16 位", - "color24": "24 位", - "color32": "32 位", - "displayUpdate": "显示更新", - "reconnect": "重新连接", - "preserve": "保存", - "unix": "Unix(LF)", - "windows": "Windows(CRLF)", - "local": "本地", - "remote": "远程", - "failsafe": "故障安全", - "blackOnWhite": "黑底白字", - "whiteOnBlack": "黑底白字", - "grayOnBlack": "灰色底黑色", - "greenOnBlack": "绿色底黑色" - } - }, - "remoteDesktop": "远程桌面", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "虚拟网络", - "unencryptedShell": "未加密 Shell", - "remoteDesktopNotes": "笔记", - "userDefaultsNote": "如果您的设置保持不变,则遵循主机默认设置。", - "connectionOriginNote": "在桌面应用程序中,RDP、VNC 和 Telnet 会使用远程服务器,除非主机设置为“此设备”。“此设备”需要 guacd 代理,即此计算机可以访问的代理。您可以在远程桌面设置中设置 guacd 代理,也可以为每个主机单独设置。", - "docsLink": "查看文档", - "connectionOriginGuacamoleNote": "RDP、VNC 和 Telnet 默认使用远程服务器,除非您选择“此设备”,这需要一台机器可以访问的 guacd 服务器。您可以在 guacd URL 设置中设置,也可以在 guacd 代理中为每个主机单独设置。" - }, - "onboarding": { - "feature_desktop": "远程桌面", - "feature_desktop_desc": "像 SSH 一样,在标签页中直接打开 RDP、VNC 和 Telnet 会话。" - }, - "settings": { - "user": { - "colorDepth": { - "label": "颜色深度", - "description": "用于那些保留默认设置的 RDP 主机。" - }, - "resizeMethod": { - "label": "调整大小方法", - "description": "远程屏幕如何跟随窗口大小变化。" - }, - "resizeDisplayUpdate": "显示更新", - "resizeReconnect": "重新连接", - "inherit": "继承", - "on": "开启", - "off": "关闭", - "forceLossless": { - "label": "强制无损", - "description": "强制无损图像编码(质量更高,占用更多带宽)" - }, - "enableWallpaper": { - "label": "壁纸", - "description": "显示桌面壁纸(禁用可提高性能)" - }, - "enableFontSmoothing": { - "label": "字体平滑", - "description": "启用 ClearType 字体渲染" - }, - "enableDesktopComposition": { - "label": "桌面合成", - "description": "启用 Aero 玻璃效果" - }, - "disableAudio": { - "label": "禁用音频", - "description": "将远程会话的所有音频静音" - }, - "enablePrinting": { - "label": "启用打印", - "description": "将本地打印机重定向到远程会话" - }, - "enableDrive": { - "label": "启用驱动器重定向", - "description": "将本地文件夹映射为远程会话中的驱动器" - }, - "disableCopy": { - "label": "禁用复制", - "description": "禁止从远程会话复制文本" - }, - "disablePaste": { - "label": "禁用粘贴", - "description": "禁止粘贴文本到远程会话" - }, - "group": "远程桌面默认设置", - "colorDepth16": "16 位", - "colorDepth24": "24 位", - "colorDepth32": "32 位" - }, - "admin": { - "enabled": { - "label": "启用远程桌面", - "description": "允许通过 guacd 连接 RDP、VNC 和 Telnet。" - }, - "guacdUrl": { - "label": "guacd URL", - "description": "guacd 监听的位置,格式为 host:port。GUACD_URL、GUACD_HOST 和 GUACD_PORT 环境变量会覆盖此设置。", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP端口" - }, - "vncPort": { - "label": "VNC端口" - }, - "telnetPort": { - "label": "Telnet端口" - }, - "rdpSecurity": { - "label": "RDP 安全模式" - }, - "rdpIgnoreCert": { - "label": "忽略证书错误" - }, - "guacamoleConfig": { - "label": "guacd 设置" - }, - "enableToolbar": { - "label": "远程桌面工具栏", - "description": "在 RDP、VNC 和 Telnet 会话中显示工具栏。" - }, - "displayGroup": "展示", - "displayDefaultsTab": "远程桌面" - } - }, - "palette": { - "connectRdp": "使用 RDP 连接", - "connectVnc": "通过 VNC 连接", - "connectTelnet": "使用 Telnet 连接" - }, - "permissions": { - "sessions": { - "title": "共享远程桌面会话", - "description": "允许会话共享和协作房间向其他人显示此用户的 RDP、VNC 和 Telnet 会话。" - } - }, - "errors": { - "remoteServerRequired": "需要远程服务器。请在“设置”中连接远程服务器以使用此连接类型。" - }, - "plugin": { - "name": "远程桌面" - } -} diff --git a/plugins/remote-desktop/locales/translated/zh_TW.json b/plugins/remote-desktop/locales/translated/zh_TW.json deleted file mode 100644 index fe6c3c21c..000000000 --- a/plugins/remote-desktop/locales/translated/zh_TW.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "remoteDesktop": { - "connecting": "正在連線至 {{type}} 工作階段…", - "checkingGuacd": "正在檢查遠端桌面服務...", - "requestingToken": "Requesting {{type}} connection token...", - "openingSession": "正在開啟 {{type}} 會話...", - "negotiating": "Negotiating {{type}} handshake...", - "sessionReady": "{{type}} 會話已準備就緒", - "connectionError": "連線錯誤", - "connectionFailed": "連線失敗", - "failedToConnect": "無法取得連線權杖", - "hostNotFound": "找不到主機", - "noHostSelected": "未選擇主機", - "reconnect": "重新連線", - "retry": "重試", - "guacdUnavailable": "遠端桌面服務 (guacd) 無法使用。請確保 guacd 正在執行且可存取,並已在管理員設定中正確設定。", - "credentialPromptTitle": "輸入 RDP 憑證", - "credentialPromptDescription": "此主機設定為在連線時提示輸入憑證。這些憑證僅用於當前會話,不會被儲存。", - "connect": "連線", - "ctrlAltDel": "Ctrl+Alt+Del", - "files": { - "title": "磁碟機檔案", - "parent": "父目錄", - "refresh": "重新整理", - "empty": "此資料夾為空", - "listFailed": "讀取此資料夾失敗", - "download": "下載", - "downloadFailed": "下載失敗 {{name}}", - "upload": "上傳文件", - "uploading": "正在上傳 {{name}}...", - "uploaded": "已上傳 {{name}}", - "uploadFailed": "上傳失敗 {{name}}", - "uploadDisabled": "此連線已停用上傳功能", - "driveUnavailable": "上傳檔案前啟用 RDP 磁碟機重定向", - "driveNotWritable": "guacd 無法寫入磁碟機資料夾。請檢查 GUACD_DRIVE_PATH(或主機磁碟機路徑)及其權限。", - "dropToUpload": "拖放檔案即可上傳" - }, - "toolbar": { - "ctrlAltDel": "Ctrl+Alt+Del", - "winL": "Win+L (鎖定螢幕)", - "winKey": "Windows 鍵", - "superL": "Super+L(鎖定畫面)", - "superKey": "超級鑰匙", - "super": "極佳的", - "cmdL": "Cmd+L(鎖定畫面)", - "cmdKey": "命令鍵", - "cmd": "命令", - "ctrl": "Ctrl", - "alt": "Alt", - "shift": "Shift", - "win": "Win", - "stickyActive": "{{key}} (已鎖定 - 點擊釋放)", - "stickyInactive": "{{key}} (點擊鎖定)", - "esc": "Escape", - "tab": "Tab", - "home": "Home", - "end": "End", - "pageUp": "Page Up", - "pageDown": "Page Down", - "arrowUp": "上箭頭", - "arrowDown": "下箭頭", - "arrowLeft": "左箭頭", - "arrowRight": "右箭頭", - "fnToggle": "功能鍵", - "reconnect": "重新連線工作階段", - "collapse": "收合工具列", - "expand": "展開工具列", - "dragHandle": "拖曳以重新定位", - "hide": "本次會話期間隱藏。若要永久停用,請開啟「主機設定」→「一般」→「連線工具列」。", - "zoomIn": "放大", - "zoomOut": "縮小", - "resetZoom": "重置縮放", - "fitToScreen": "適應螢幕", - "switchToTrackpad": "切換到觸控板模式(拖曳移動遊標,點擊點擊)", - "switchToTouch": "切換到觸控模式(直接點擊你想點擊的地方)" - }, - "disabled": "遠端桌面已關閉。管理員可以在“設定”中啟用它。" - }, - "hosts": { - "nativeRdpOpened": "在 Windows 遠端桌面中開啟", - "nativeRdpFailed": "無法開啟 Windows 遠端桌面", - "openNativeRdp": "在 Windows 遠端桌面中開啟", - "guac": { - "toolbar": "工具列", - "connection": "連線", - "authentication": "驗證", - "storedCredential": "已儲存的憑證", - "authMethod": "驗證方法", - "authTypeDirect": "直接", - "authTypeCredential": "憑證", - "authTypeNone": "取消全選", - "authTypeNoneDesc": "不會儲存任何憑證。每次連線時,系統都會提示您輸入使用者名稱和密碼;這些資訊不會被儲存。", - "selectCredential": "選取憑證…", - "connectionSettings": "連線設定", - "displaySettings": "顯示設定", - "audioSettings": "音訊設定", - "rdpPerformance": "RDP 效能", - "deviceRedirection": "裝置重新導向", - "session": "工作階段", - "gateway": "閘道", - "remoteApp": "RemoteApp", - "clipboard": "剪貼簿", - "sessionRecording": "工作階段錄製", - "wakeOnLan": "Wake-on-LAN", - "vncSettings": "VNC 設定", - "terminalSettings": "終端機設定", - "rdpPort": "RDP 連接埠", - "username": "使用者名稱", - "password": "密碼", - "passwordSaved": "密碼已儲存,輸入即可更改", - "domain": "網域", - "securityMode": "安全模式", - "width": "寬度", - "height": "高度", - "dpi": "DPI", - "clientName": "用戶端名稱", - "initialProgram": "初始程式", - "serverLayout": "伺服器鍵盤配置", - "timezone": "時區", - "loadBalanceInfo": "負載平衡資訊 / Cookie", - "loadBalanceInfoDesc": "用於 RDS 伺服器陣列負載平衡的 RD 連線代理 Cookie (例如 tsv://MS Terminal Services Plugin.1.CollectionName)", - "guacdProxy": "guacd 代理", - "guacdHostname": "guacd 主機", - "guacdHostnamePlaceholder": "全域預設", - "guacdPort": "guacd 連接埠", - "guacdProxyDesc": "覆寫此連線的全域 guacd 執行個體。留空以使用全域設定的 guacd。", - "gatewayHostname": "閘道主機名稱", - "gatewayPort": "閘道連接埠", - "gatewayUsername": "閘道使用者名稱", - "gatewayPassword": "閘道密碼", - "gatewayDomain": "閘道網域", - "remoteAppProgram": "RemoteApp 程式", - "workingDirectory": "工作目錄", - "arguments": "引數", - "normalizeLineEndings": "正規化行尾符號", - "macAddress": "MAC 位址", - "broadcastAddress": "廣播位址", - "udpPort": "UDP 連接埠", - "waitTimeS": "等待時間 (秒)", - "driveName": "磁碟機名稱", - "drivePath": "磁碟機路徑", - "ignoreCertificate": "忽略憑證", - "ignoreCertificateDesc": "允許連線至使用自行簽署憑證的主機", - "forceLossless": "強制無損", - "forceLosslessDesc": "強制使用無損影像編碼(品質較高,頻寬需求較大)", - "disableAudio": "停用音訊", - "disableAudioDesc": "將遠端工作階段的音訊全部靜音", - "enableAudioInput": "啟用音訊輸入(麥克風)", - "enableAudioInputDesc": "將本機麥克風轉送至遠端工作階段", - "wallpaper": "桌面背景", - "wallpaperDesc": "顯示桌面背景(停用可提升效能)", - "theming": "佈景主題", - "themingDesc": "啟用視覺主題與樣式", - "fontSmoothing": "字型平滑處理", - "fontSmoothingDesc": "啟用 ClearType 字型轉譯", - "fullWindowDrag": "全視窗拖曳", - "fullWindowDragDesc": "拖曳時顯示視窗內容", - "desktopComposition": "桌面合成", - "desktopCompositionDesc": "啟用 Aero 玻璃效果", - "menuAnimations": "選單動畫", - "menuAnimationsDesc": "啟用選單淡入淡出及滑動動畫", - "disableBitmapCaching": "停用點陣圖快取", - "disableBitmapCachingDesc": "關閉點陣圖快取(可能有助於解決顯示問題)", - "disableOffscreenCaching": "停用螢幕外快取", - "disableOffscreenCachingDesc": "關閉螢幕外快取", - "disableGlyphCaching": "停用字符快取", - "disableGlyphCachingDesc": "關閉字符快取", - "enableGfx": "啟用 GFX", - "enableGfxDesc": "使用 RemoteFX 圖形管線", - "enablePrinting": "啟用列印功能", - "enablePrintingDesc": "將本機印表機重新導向至遠端工作階段", - "enableDriveRedirection": "啟用磁碟機重新導向", - "enableDriveRedirectionDesc": "在遠端工作階段中將本機資料夾對應為磁碟機", - "createDrivePath": "建立磁碟機路徑", - "createDrivePathDesc": "如果資料夾不存在則自動建立", - "disableDownload": "停用下載", - "disableDownloadDesc": "防止從遠端工作階段下載檔案", - "disableUpload": "停用上傳", - "disableUploadDesc": "防止將檔案上傳至遠端工作階段", - "enableTouch": "啟用觸控", - "enableTouchDesc": "啟用觸控輸入轉送", - "consoleSession": "主控台工作階段", - "consoleSessionDesc": "連線至主控台 (工作階段 0) 而非新工作階段", - "sendWolPacket": "傳送 WOL 封包", - "sendWolPacketDesc": "在連線前傳送魔術封包以喚醒此主機", - "disableCopy": "停用複製", - "disableCopyDesc": "防止從遠端工作階段複製文字", - "disablePaste": "停用貼上", - "disablePasteDesc": "防止將文字貼上至遠端工作階段", - "createPathIfMissing": "若路徑不存在則建立", - "createPathIfMissingDesc": "自動建立錄製目錄", - "excludeOutput": "排除輸出", - "excludeOutputDesc": "不錄製螢幕輸出 (僅中繼資料)", - "excludeMouse": "排除滑鼠", - "excludeMouseDesc": "不錄製滑鼠移動", - "includeKeystrokes": "包含按鍵輸入", - "includeKeystrokesDesc": "除了螢幕輸出外,也錄製原始按鍵輸入", - "vncPort": "VNC 連接埠", - "vncPassword": "VNC 密碼", - "vncUsernameOptional": "使用者名稱 (選用)", - "vncLeaveBlank": "若不需要則留空", - "cursorMode": "游標模式", - "swapRedBlue": "交換紅/藍", - "swapRedBlueDesc": "交換紅色與藍色色版 (修正某些色彩問題)", - "readOnly": "唯讀", - "readOnlyDesc": "僅檢視遠端螢幕,不傳送任何輸入", - "telnetPort": "Telnet 連接埠", - "terminalType": "終端機類型", - "fontName": "字型名稱", - "fontSize": "字型大小", - "colorScheme": "色彩配置", - "backspaceKey": "退格鍵", - "colorDepth": "色彩深度", - "resizeMethod": "調整尺寸方法", - "driveNamePlaceholder": "特米克斯大道", - "options": { - "any": "任何", - "auto": "自動", - "nlaExtended": "NLA 擴展", - "color8": "8 位元", - "color16": "16 位元", - "color24": "24 位元", - "color32": "32 位元", - "displayUpdate": "顯示更新", - "reconnect": "重新連線", - "preserve": "儲存", - "unix": "Unix(LF)", - "windows": "Windows(CRLF)", - "local": "本機", - "remote": "遠端", - "failsafe": "故障安全", - "blackOnWhite": "黑底白字", - "whiteOnBlack": "黑底白字", - "grayOnBlack": "灰色底黑色", - "greenOnBlack": "綠色底黑色" - } - }, - "remoteDesktop": "遠端桌面", - "tabRdp": "RDP", - "tabVnc": "VNC", - "tabTelnet": "Telnet", - "virtualNetwork": "虛擬網路", - "unencryptedShell": "未加密殼層", - "remoteDesktopNotes": "筆記", - "userDefaultsNote": "如果您的設定保持不變,請遵循主機預設設定。", - "connectionOriginNote": "在桌面應用程式中,RDP、VNC 和 Telnet 會使用遠端伺服器,除非主機設定為「此設備」。 「此設備」需要 guacd 代理,即此電腦可以存取的代理。您可以在遠端桌面設定中設定 guacd 代理,也可以為每個主機單獨設定。", - "docsLink": "檢視文件", - "connectionOriginGuacamoleNote": "RDP、VNC 和 Telnet 預設使用遠端伺服器,除非您選擇“此設備”,這需要一台機器可以存取的 guacd 伺服器。您可以在 guacd URL 設定中設置,也可以在 guacd 代理中為每個主機單獨設定。" - }, - "onboarding": { - "feature_desktop": "遠端桌面", - "feature_desktop_desc": "像 SSH 一樣,在同一個標籤頁中開啟 RDP、VNC 和 Telnet 會話。" - }, - "settings": { - "user": { - "colorDepth": { - "label": "色彩深度", - "description": "用於那些保留預設設定的 RDP 主機。" - }, - "resizeMethod": { - "label": "調整尺寸方法", - "description": "遠端螢幕如何跟隨視窗大小變化。" - }, - "resizeDisplayUpdate": "顯示更新", - "resizeReconnect": "重新連線", - "inherit": "繼承", - "on": "開啟", - "off": "關閉", - "forceLossless": { - "label": "強制無損", - "description": "強制使用無損影像編碼(品質較高,頻寬需求較大)" - }, - "enableWallpaper": { - "label": "桌面背景", - "description": "顯示桌面背景(停用可提升效能)" - }, - "enableFontSmoothing": { - "label": "字型平滑處理", - "description": "啟用 ClearType 字型轉譯" - }, - "enableDesktopComposition": { - "label": "桌面合成", - "description": "啟用 Aero 玻璃效果" - }, - "disableAudio": { - "label": "停用音訊", - "description": "將遠端工作階段的音訊全部靜音" - }, - "enablePrinting": { - "label": "啟用列印功能", - "description": "將本機印表機重新導向至遠端工作階段" - }, - "enableDrive": { - "label": "啟用磁碟機重新導向", - "description": "在遠端工作階段中將本機資料夾對應為磁碟機" - }, - "disableCopy": { - "label": "停用複製", - "description": "防止從遠端工作階段複製文字" - }, - "disablePaste": { - "label": "停用貼上", - "description": "防止將文字貼上至遠端工作階段" - }, - "group": "遠端桌面預設設定", - "colorDepth16": "16 位元", - "colorDepth24": "24 位元", - "colorDepth32": "32 位元" - }, - "admin": { - "enabled": { - "label": "啟用遠端桌面", - "description": "允許透過 guacd 連接 RDP、VNC 和 Telnet。" - }, - "guacdUrl": { - "label": "guacd URL", - "description": "guacd 監聽的位置,格式為 host:port。 GUACD_URL、GUACD_HOST 和 GUACD_PORT 環境變數會覆寫此設定。", - "placeholder": "guacd:4822" - } - }, - "host": { - "enableRdp": { - "label": "RDP" - }, - "enableVnc": { - "label": "VNC" - }, - "enableTelnet": { - "label": "Telnet" - }, - "rdpPort": { - "label": "RDP埠" - }, - "vncPort": { - "label": "VNC埠" - }, - "telnetPort": { - "label": "Telnet連接埠" - }, - "rdpSecurity": { - "label": "RDP 安全模式" - }, - "rdpIgnoreCert": { - "label": "忽略證書錯誤" - }, - "guacamoleConfig": { - "label": "guacd 設定" - }, - "enableToolbar": { - "label": "遠端桌面工具列", - "description": "在 RDP、VNC 和 Telnet 會話中顯示工具列。" - }, - "displayGroup": "展示", - "displayDefaultsTab": "遠端桌面" - } - }, - "palette": { - "connectRdp": "使用 RDP 連接", - "connectVnc": "透過 VNC 連接", - "connectTelnet": "使用 Telnet 連接" - }, - "permissions": { - "sessions": { - "title": "共用遠端桌面會話", - "description": "允許會話共用和協作房間向其他人顯示此使用者的 RDP、VNC 和 Telnet 會話。" - } - }, - "errors": { - "remoteServerRequired": "需要遠端伺服器。請在「設定」中連接遠端伺服器以使用此連線類型。" - }, - "plugin": { - "name": "遠端桌面" - } -} diff --git a/plugins/remote-desktop/manifest.json b/plugins/remote-desktop/manifest.json deleted file mode 100644 index 57660e4fa..000000000 --- a/plugins/remote-desktop/manifest.json +++ /dev/null @@ -1,739 +0,0 @@ -{ - "id": "remote-desktop", - "name": "Remote Desktop", - "version": "1.0.0", - "description": "RDP, VNC and Telnet sessions in the browser, with clipboard, file transfer, recording and sharing.", - "author": { - "name": "Termix" - }, - "license": "MIT", - "repository": "https://github.com/Termix-SSH/Termix", - "category": "Terminal", - "icon": "Monitor", - "engine": { - "termix": ">=2.9.0", - "api": "1" - }, - "capabilities": [ - "hosts:read", - "credentials:read", - "credentials:use", - "ssh:connect", - "network:serve", - "desktop:window", - "ui:surface", - "users:impersonate" - ], - "optionalDependencies": { - "session-recording": "^1.0.0", - "tunnels": "^1.0.0" - }, - "provides": [ - { - "service": "sessions.live", - "version": "1.0.0", - "permission": "remote-desktop.sessions", - "names": ["rdp", "vnc", "telnet"] - } - ], - "requires": [ - { - "service": "recordings.writer", - "versionRange": "^1.1.0", - "optional": true - }, - { - "service": "tunnels.access", - "versionRange": "^1.0.0", - "optional": true - } - ], - "contributes": { - "tabs": [ - { - "id": "rdp", - "titleKey": "hosts.tabRdp", - "icon": "Monitor", - "openFrom": ["host-context-menu", "palette"] - }, - { - "id": "vnc", - "titleKey": "hosts.tabVnc", - "icon": "MousePointerClick", - "openFrom": ["host-context-menu", "palette"] - }, - { - "id": "telnet", - "titleKey": "hosts.tabTelnet", - "icon": "Terminal", - "openFrom": ["host-context-menu", "palette"] - } - ], - "actionSlots": [ - { - "id": "remote-desktop.toolbar", - "accepts": ["button"] - } - ], - "protocols": [ - { - "id": "rdp", - "credentialFields": [ - { - "key": "domain" - } - ], - "defaultPort": 3389, - "hostLoginFallback": ["username", "password"] - }, - { - "id": "vnc", - "defaultPort": 5900, - "hostLoginFallback": ["password"] - }, - { - "id": "telnet", - "defaultPort": 23, - "hostLoginFallback": ["password"] - } - ], - "guest": true, - "permissions": [ - { - "name": "sessions", - "titleKey": "permissions.sessions.title", - "descriptionKey": "permissions.sessions.description", - "defaultRoles": ["admin", "user"] - } - ], - "settings": { - "admin": [ - { - "key": "enabled", - "type": "boolean", - "labelKey": "settings.admin.enabled.label", - "descriptionKey": "settings.admin.enabled.description", - "default": true - }, - { - "key": "guacdUrl", - "type": "string", - "labelKey": "settings.admin.guacdUrl.label", - "descriptionKey": "settings.admin.guacdUrl.description", - "placeholderKey": "settings.admin.guacdUrl.placeholder", - "requires": "enabled", - "default": "" - } - ], - "user": [ - { - "key": "colorDepth", - "type": "select", - "labelKey": "settings.user.colorDepth.label", - "descriptionKey": "settings.user.colorDepth.description", - "group": "settings.user.group", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "16", - "labelKey": "settings.user.colorDepth16" - }, - { - "value": "24", - "labelKey": "settings.user.colorDepth24" - }, - { - "value": "32", - "labelKey": "settings.user.colorDepth32" - } - ], - "default": "inherit", - "hidden": true - }, - { - "key": "resizeMethod", - "type": "select", - "labelKey": "settings.user.resizeMethod.label", - "descriptionKey": "settings.user.resizeMethod.description", - "group": "settings.user.group", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "display-update", - "labelKey": "settings.user.resizeDisplayUpdate" - }, - { - "value": "reconnect", - "labelKey": "settings.user.resizeReconnect" - } - ], - "default": "inherit", - "hidden": true - }, - { - "key": "forceLossless", - "type": "select", - "labelKey": "settings.user.forceLossless.label", - "descriptionKey": "settings.user.forceLossless.description", - "group": "settings.user.group", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "hidden": true - }, - { - "key": "enableWallpaper", - "type": "select", - "labelKey": "settings.user.enableWallpaper.label", - "descriptionKey": "settings.user.enableWallpaper.description", - "group": "settings.user.group", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "hidden": true - }, - { - "key": "enableFontSmoothing", - "type": "select", - "labelKey": "settings.user.enableFontSmoothing.label", - "descriptionKey": "settings.user.enableFontSmoothing.description", - "group": "settings.user.group", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "hidden": true - }, - { - "key": "enableDesktopComposition", - "type": "select", - "labelKey": "settings.user.enableDesktopComposition.label", - "descriptionKey": "settings.user.enableDesktopComposition.description", - "group": "settings.user.group", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "hidden": true - }, - { - "key": "disableAudio", - "type": "select", - "labelKey": "settings.user.disableAudio.label", - "descriptionKey": "settings.user.disableAudio.description", - "group": "settings.user.group", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "hidden": true - }, - { - "key": "enablePrinting", - "type": "select", - "labelKey": "settings.user.enablePrinting.label", - "descriptionKey": "settings.user.enablePrinting.description", - "group": "settings.user.group", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "hidden": true - }, - { - "key": "enableDrive", - "type": "select", - "labelKey": "settings.user.enableDrive.label", - "descriptionKey": "settings.user.enableDrive.description", - "group": "settings.user.group", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "hidden": true - }, - { - "key": "disableCopy", - "type": "select", - "labelKey": "settings.user.disableCopy.label", - "descriptionKey": "settings.user.disableCopy.description", - "group": "settings.user.group", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "hidden": true - }, - { - "key": "disablePaste", - "type": "select", - "labelKey": "settings.user.disablePaste.label", - "descriptionKey": "settings.user.disablePaste.description", - "group": "settings.user.group", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "hidden": true - } - ], - "host": { - "fields": [ - { - "key": "enableRdp", - "type": "boolean", - "labelKey": "settings.host.enableRdp.label", - "default": false, - "hidden": true, - "defaultable": false - }, - { - "key": "enableVnc", - "type": "boolean", - "labelKey": "settings.host.enableVnc.label", - "default": false, - "hidden": true, - "defaultable": false - }, - { - "key": "enableTelnet", - "type": "boolean", - "labelKey": "settings.host.enableTelnet.label", - "default": false, - "hidden": true, - "defaultable": false - }, - { - "key": "rdpPort", - "type": "number", - "labelKey": "settings.host.rdpPort.label", - "min": 1, - "max": 65535, - "default": 3389, - "hidden": true, - "defaultable": false - }, - { - "key": "vncPort", - "type": "number", - "labelKey": "settings.host.vncPort.label", - "min": 1, - "max": 65535, - "default": 5900, - "hidden": true, - "defaultable": false - }, - { - "key": "telnetPort", - "type": "number", - "labelKey": "settings.host.telnetPort.label", - "min": 1, - "max": 65535, - "default": 23, - "hidden": true, - "defaultable": false - }, - { - "key": "rdpSecurity", - "type": "string", - "labelKey": "settings.host.rdpSecurity.label", - "default": "", - "hidden": true - }, - { - "key": "rdpIgnoreCert", - "type": "boolean", - "labelKey": "settings.host.rdpIgnoreCert.label", - "default": false, - "hidden": true - }, - { - "key": "guacamoleConfig", - "type": "json", - "labelKey": "settings.host.guacamoleConfig.label", - "default": {}, - "hidden": true, - "shareRead": "edit", - "secretKeys": ["gateway-password"], - "defaultable": false - }, - { - "key": "enableToolbar", - "type": "boolean", - "labelKey": "settings.host.enableToolbar.label", - "descriptionKey": "settings.host.enableToolbar.description", - "default": true, - "hidden": true - }, - { - "key": "colorDepth", - "type": "select", - "labelKey": "settings.user.colorDepth.label", - "descriptionKey": "settings.user.colorDepth.description", - "group": "settings.host.displayGroup", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "16", - "labelKey": "settings.user.colorDepth16" - }, - { - "value": "24", - "labelKey": "settings.user.colorDepth24" - }, - { - "value": "32", - "labelKey": "settings.user.colorDepth32" - } - ], - "default": "inherit", - "personal": true - }, - { - "key": "resizeMethod", - "type": "select", - "labelKey": "settings.user.resizeMethod.label", - "descriptionKey": "settings.user.resizeMethod.description", - "group": "settings.host.displayGroup", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "display-update", - "labelKey": "settings.user.resizeDisplayUpdate" - }, - { - "value": "reconnect", - "labelKey": "settings.user.resizeReconnect" - } - ], - "default": "inherit", - "personal": true - }, - { - "key": "forceLossless", - "type": "select", - "labelKey": "settings.user.forceLossless.label", - "descriptionKey": "settings.user.forceLossless.description", - "group": "settings.host.displayGroup", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "personal": true - }, - { - "key": "enableWallpaper", - "type": "select", - "labelKey": "settings.user.enableWallpaper.label", - "descriptionKey": "settings.user.enableWallpaper.description", - "group": "settings.host.displayGroup", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "personal": true - }, - { - "key": "enableFontSmoothing", - "type": "select", - "labelKey": "settings.user.enableFontSmoothing.label", - "descriptionKey": "settings.user.enableFontSmoothing.description", - "group": "settings.host.displayGroup", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "personal": true - }, - { - "key": "enableDesktopComposition", - "type": "select", - "labelKey": "settings.user.enableDesktopComposition.label", - "descriptionKey": "settings.user.enableDesktopComposition.description", - "group": "settings.host.displayGroup", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "personal": true - }, - { - "key": "disableAudio", - "type": "select", - "labelKey": "settings.user.disableAudio.label", - "descriptionKey": "settings.user.disableAudio.description", - "group": "settings.host.displayGroup", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "personal": true - }, - { - "key": "enablePrinting", - "type": "select", - "labelKey": "settings.user.enablePrinting.label", - "descriptionKey": "settings.user.enablePrinting.description", - "group": "settings.host.displayGroup", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "personal": true - }, - { - "key": "enableDrive", - "type": "select", - "labelKey": "settings.user.enableDrive.label", - "descriptionKey": "settings.user.enableDrive.description", - "group": "settings.host.displayGroup", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "personal": true - }, - { - "key": "disableCopy", - "type": "select", - "labelKey": "settings.user.disableCopy.label", - "descriptionKey": "settings.user.disableCopy.description", - "group": "settings.host.displayGroup", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "personal": true - }, - { - "key": "disablePaste", - "type": "select", - "labelKey": "settings.user.disablePaste.label", - "descriptionKey": "settings.user.disablePaste.description", - "group": "settings.host.displayGroup", - "options": [ - { - "value": "inherit", - "labelKey": "settings.user.inherit" - }, - { - "value": "on", - "labelKey": "settings.user.on" - }, - { - "value": "off", - "labelKey": "settings.user.off" - } - ], - "default": "inherit", - "personal": true - } - ] - } - }, - "uses": ["session.remoteDisplay", "wakeOnLan.macAddress"] - }, - "locales": "locales", - "platforms": ["linux", "win32", "darwin"] -} diff --git a/plugins/remote-desktop/package.json b/plugins/remote-desktop/package.json deleted file mode 100644 index 2f0604f47..000000000 --- a/plugins/remote-desktop/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "@termix-plugin/remote-desktop", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "build": "termix-plugin build", - "test": "termix-plugin test", - "typecheck": "tsc -p .", - "validate": "termix-plugin validate" - }, - "dependencies": { - "guacamole-common-js": "^1.5.0", - "guacamole-lite": "^1.2.0", - "lucide-react": "^1.33.0" - }, - "devDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "@testing-library/dom": "^10.4.2", - "@testing-library/react": "^16.3.3", - "@types/node": "^26.2.0", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", - "jsdom": "^30.0.1", - "vitest": "^4.1.11" - }, - "peerDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "express": "^5.2.1", - "react": "^19.2.8", - "react-i18next": "^17.0.14", - "sonner": "^2.0.8", - "ssh2": "^1.17.0" - }, - "termix": { - "patches": [ - "guacamole-common-js-flush" - ] - } -} diff --git a/plugins/remote-desktop/patches/guacamole-lite.cjs b/plugins/remote-desktop/patches/guacamole-lite.cjs deleted file mode 100644 index 598191ab3..000000000 --- a/plugins/remote-desktop/patches/guacamole-lite.cjs +++ /dev/null @@ -1,396 +0,0 @@ -const fs = require("fs"); -const path = require("path"); - -// Run by `termix-plugin build` with TERMIX_PATCH_ROOT set to the plugin, so -// the package resolves the way the plugin's bundle will resolve it. -function packageDir(name) { - const root = process.env.TERMIX_PATCH_ROOT || process.cwd(); - // Walked by hand: a package whose exports map hides package.json cannot be - // found with require.resolve. - for (let dir = path.resolve(root); ; dir = path.dirname(dir)) { - const candidate = path.join(dir, "node_modules", name); - if (fs.existsSync(path.join(candidate, "package.json"))) return candidate; - if (path.dirname(dir) === dir) return path.join(root, "node_modules", name); - } -} - -const guacdClientPath = path.join( - packageDir("guacamole-lite"), - "lib", - "GuacdClient.js", -); -const cryptPath = path.join(packageDir("guacamole-lite"), "lib", "Crypt.js"); -const clientConnectionPath = path.join( - packageDir("guacamole-lite"), - "lib", - "ClientConnection.js", -); -const serverPath = path.join(packageDir("guacamole-lite"), "lib", "Server.js"); - -if ( - !fs.existsSync(guacdClientPath) || - !fs.existsSync(cryptPath) || - !fs.existsSync(clientConnectionPath) || - !fs.existsSync(serverPath) -) { - console.log("[patch-guacamole-lite] File not found, skipping"); - process.exit(0); -} - -// Every patch below is required for correctness: protocol negotiation, the -// guacd 1.6.0 name handshake, dynamic argument answering, UTF-8 tokens and -// read-only joins. If an upstream release moves an anchor string, silently -// skipping would ship a Termix that looks fine and then drops VNC/RDP sessions -// at runtime, so a missing anchor has to stop the install instead. -function missingAnchor(patch) { - console.error( - `[patch-guacamole-lite] ${patch} anchor not found in guacamole-lite. ` + - "The upstream file has changed and this patch no longer applies — " + - "update scripts/patch-guacamole-lite.cjs to match the new source.", - ); - process.exit(1); -} - -let guacdClientContent = fs.readFileSync(guacdClientPath, "utf8"); -let cryptContent = fs.readFileSync(cryptPath, "utf8"); -let clientConnectionContent = fs.readFileSync(clientConnectionPath, "utf8"); -let serverContent = fs.readFileSync(serverPath, "utf8"); - -// Patch 1: protocol version negotiation. -// guacamole-lite originally only accepted 1.0.0/1.1.0. Support the protocol -// versions Termix can handle, and conservatively answer future 1.x versions as -// VERSION_1_5_0 so guacd still sees support for `require`/`name` without us -// claiming support for unknown instructions. -const oldVersionBlock = - " if (version === '1_0_0' || version === '1_1_0') {\n" + - " protocolVersion = version;\n" + - " } else {\n" + - " protocolVersion = '1_1_0';\n" + - " }"; -const oldPatchedVersionBlock = - " if (version === '1_0_0' || version === '1_1_0' || version === '1_3_0' || version === '1_5_0') {\n" + - " protocolVersion = version;\n" + - " } else {\n" + - " protocolVersion = '1_1_0';\n" + - " }"; -const newVersionBlock = - " if (version === '1_0_0' || version === '1_1_0' || version === '1_3_0' || version === '1_5_0') {\n" + - " protocolVersion = version;\n" + - " } else if (/^1_\\d+_0$/.test(version)) {\n" + - " protocolVersion = '1_5_0';\n" + - " } else {\n" + - " protocolVersion = '1_1_0';\n" + - " }"; - -// Patch 2: timezone instruction must be sent for all protocols >= 1.1.0, not just 1.1.0 -const oldTimezone = "if (protocolVersion === '1_1_0') {"; -const newTimezone = "if (protocolVersion !== '1_0_0') {"; - -// Patch 3: send the `name` handshake instruction for all protocol versions >= 1.1.0. -// The Guacamole protocol added `name` in 1.3.0, but guacd 1.6.0 began requiring it -// during the VNC handshake even when negotiating VERSION_1_1_0, causing connections to -// silently drop right after "User joined". Sending it for all non-1.0.0 sessions is -// harmless (guacd ignores unknown handshake instructions for older versions). See -// Termix-SSH/Support#567 and #734. -const oldConnect = - " this.sendInstruction(['connect'].concat(connectArgs));"; -const oldNameConnect = - " if (protocolVersion === '1_3_0' || protocolVersion === '1_5_0') {\n" + - " this.sendInstruction(['name', this.connectionSettings.name || 'guacamole-lite']);\n" + - " }\n" + - "\n" + - " this.sendInstruction(['connect'].concat(connectArgs));"; -const newConnect = - " if (protocolVersion !== '1_0_0') {\n" + - " this.sendInstruction(['name', this.connectionSettings.name || 'guacamole-lite']);\n" + - " }\n" + - "\n" + - " this.sendInstruction(['connect'].concat(connectArgs));"; - -// Patch 4: answer guacd's dynamic argument requests locally. -// macOS Screen Sharing can request VNC username/password through the -// post-handshake `required`/`require` flow. guacamole-lite forwards those -// instructions to the browser, but Termix already keeps the credentials in the -// server-side token and the browser does not provide an onrequired handler. -const oldSendBuffer = - " this.lastActivity = Date.now();\n" + " this.sendBuffer = '';"; -const newSendBuffer = - " this.lastActivity = Date.now();\n" + - " this.sendBuffer = '';\n" + - " this.nextArgumentStreamIndex = 0;"; - -const oldSendInstructionBlock = - " sendInstruction(instruction) {\n" + - " // convert every element in the instruction array to a string. convert null to an empty string\n" + - " instruction = instruction.map((element) => {\n" + - " if (element === null || element === undefined) {\n" + - " return '';\n" + - " }\n" + - " return String(element);\n" + - " });\n" + - "\n" + - " const instructionString = GuacamoleParser.toInstruction(instruction);\n" + - " this.send(instructionString);\n" + - " }\n"; -const newSendInstructionBlock = - oldSendInstructionBlock + - "\n" + - " sendArgumentValue(name, value) {\n" + - " const stream = this.nextArgumentStreamIndex++;\n" + - " this.sendInstruction(['argv', stream, 'text/plain', name]);\n" + - " this.sendInstruction(['blob', stream, Buffer.from(String(value ?? ''), 'utf8').toString('base64')]);\n" + - " this.sendInstruction(['end', stream]);\n" + - " }\n" + - "\n" + - " sendRequiredArguments(params) {\n" + - " params.forEach((name) => {\n" + - " this.sendArgumentValue(name, this.connectionSettings[name]);\n" + - " });\n" + - " }\n"; - -const oldReadyHandler = - ' // Handle "ready" instruction\n' + - " if (opcode === 'ready') {"; -const newReadyHandler = - " // Handle dynamic argument requests from guacd\n" + - " if (opcode === 'required' || opcode === 'require') {\n" + - " this.sendRequiredArguments(params);\n" + - " return;\n" + - " }\n" + - "\n" + - oldReadyHandler; - -let patched = false; - -if (!guacdClientContent.includes("} else if (/^1_\\d+_0$/.test(version)) {")) { - if (guacdClientContent.includes(oldPatchedVersionBlock)) { - guacdClientContent = guacdClientContent.replace( - oldPatchedVersionBlock, - newVersionBlock, - ); - } else if (guacdClientContent.includes(oldVersionBlock)) { - guacdClientContent = guacdClientContent.replace( - oldVersionBlock, - newVersionBlock, - ); - } else { - missingAnchor("Version check"); - } - patched = true; -} - -if (!guacdClientContent.includes(newTimezone)) { - if (!guacdClientContent.includes(oldTimezone)) { - missingAnchor("Timezone"); - } - guacdClientContent = guacdClientContent.replace(oldTimezone, newTimezone); - patched = true; -} - -if (!guacdClientContent.includes(newConnect)) { - if (guacdClientContent.includes(oldNameConnect)) { - guacdClientContent = guacdClientContent.replace(oldNameConnect, newConnect); - } else if (guacdClientContent.includes(oldConnect)) { - guacdClientContent = guacdClientContent.replace(oldConnect, newConnect); - } else { - missingAnchor("Connect"); - } - patched = true; -} - -if (!guacdClientContent.includes("this.nextArgumentStreamIndex = 0;")) { - if (!guacdClientContent.includes(oldSendBuffer)) { - missingAnchor("Argument stream index"); - } - guacdClientContent = guacdClientContent.replace(oldSendBuffer, newSendBuffer); - patched = true; -} - -if (!guacdClientContent.includes("sendRequiredArguments(params) {")) { - if (!guacdClientContent.includes(oldSendInstructionBlock)) { - missingAnchor("Required argument helper"); - } - guacdClientContent = guacdClientContent.replace( - oldSendInstructionBlock, - newSendInstructionBlock, - ); - patched = true; -} - -if ( - !guacdClientContent.includes("opcode === 'required' || opcode === 'require'") -) { - if (!guacdClientContent.includes(oldReadyHandler)) { - missingAnchor("Required opcode"); - } - guacdClientContent = guacdClientContent.replace( - oldReadyHandler, - newReadyHandler, - ); - patched = true; -} - -// Patch 5: guacamole-lite decrypts token JSON through ASCII/binary strings, -// which corrupts IV/ciphertext bytes and non-ASCII connection settings such as -// RDP/VNC passwords with umlauts. Keep the encrypted fields as Buffers and -// decode the plaintext JSON as UTF-8. -const oldDecryptBlock = - " let encoded = JSON.parse(this.constructor.base64decode(encodedString));\n" + - "\n" + - " encoded.iv = this.constructor.base64decode(encoded.iv);\n" + - " encoded.value = this.constructor.base64decode(encoded.value, 'binary');\n" + - "\n" + - " const decipher = Crypto.createDecipheriv(this.cypher, this.key, encoded.iv);\n" + - "\n" + - " let decrypted = decipher.update(encoded.value, 'binary', 'ascii');\n" + - " decrypted += decipher.final('ascii');"; -const oldPartiallyPatchedDecryptBlock = - " let encoded = JSON.parse(this.constructor.base64decode(encodedString));\n" + - "\n" + - " encoded.iv = this.constructor.base64decode(encoded.iv);\n" + - " encoded.value = this.constructor.base64decode(encoded.value, 'binary');\n" + - "\n" + - " const decipher = Crypto.createDecipheriv(this.cypher, this.key, encoded.iv);\n" + - "\n" + - " let decrypted = decipher.update(encoded.value, 'binary', 'utf8');\n" + - " decrypted += decipher.final('utf8');"; -const newDecryptBlock = - " const encoded = JSON.parse(Buffer.from(encodedString, 'base64').toString('utf8'));\n" + - "\n" + - " const iv = Buffer.from(encoded.iv, 'base64');\n" + - " const value = Buffer.from(encoded.value, 'base64');\n" + - "\n" + - " const decipher = Crypto.createDecipheriv(this.cypher, this.key, iv);\n" + - "\n" + - " let decrypted = decipher.update(value, undefined, 'utf8');\n" + - " decrypted += decipher.final('utf8');"; - -if (!cryptContent.includes(newDecryptBlock)) { - if (cryptContent.includes(oldDecryptBlock)) { - cryptContent = cryptContent.replace(oldDecryptBlock, newDecryptBlock); - } else if (cryptContent.includes(oldPartiallyPatchedDecryptBlock)) { - cryptContent = cryptContent.replace( - oldPartiallyPatchedDecryptBlock, - newDecryptBlock, - ); - } else { - missingAnchor("UTF-8 token decrypt"); - } - patched = true; -} - -// Patch 7: drop client-to-guacd input instructions from read-only session-share -// joins. guacd has no native read-only enforcement in the versions this project -// targets, so Termix must gate here. Denylist (not allowlist) on purpose: an -// unrecognized opcode is far more likely to be protocol plumbing (sync, blob, -// clipboard streams) than a new input vector, so failing open is the safer -// default for a client we already control. -const oldSendMessageToGuacd = - " sendMessageToGuacd(message) {\n" + - " this.lastActivity = Date.now();\n" + - " this.logger.log(LOGLEVEL.DEBUG, '[ >>> # ] Received from WS: ```' + message + '```');\n" + - "\n" + - " if (this.guacdClient) {\n" + - " this.guacdClient.send(message, true);\n" + - " }\n" + - " }"; -const newSendMessageToGuacd = - " sendMessageToGuacd(message) {\n" + - " this.lastActivity = Date.now();\n" + - " this.logger.log(LOGLEVEL.DEBUG, '[ >>> # ] Received from WS: ```' + message + '```');\n" + - "\n" + - " if (this.isReadOnlyJoin() && this.isInputInstruction(message)) {\n" + - " return;\n" + - " }\n" + - "\n" + - " if (this.guacdClient) {\n" + - " this.guacdClient.send(message, true);\n" + - " }\n" + - " }\n" + - "\n" + - " isReadOnlyJoin() {\n" + - " const connection = this.connectionSettings && this.connectionSettings.connection;\n" + - " return !!(connection && connection.join && connection.readOnly === true);\n" + - " }\n" + - "\n" + - " // Termix-only read-only gate, not part of the vendored library: extracts just\n" + - " // the leading opcode from a raw '.,...;' instruction without the\n" + - " // overhead of a full stateful parse.\n" + - " isInputInstruction(message) {\n" + - " const dot = message.indexOf('.');\n" + - " if (dot === -1) return false;\n" + - " const len = parseInt(message.substring(0, dot), 10);\n" + - " if (isNaN(len)) return false;\n" + - " const opcode = message.substring(dot + 1, dot + 1 + len);\n" + - " return ['mouse', 'key', 'touch', 'size'].includes(opcode);\n" + - " }"; - -if (!clientConnectionContent.includes("isReadOnlyJoin()")) { - if (!clientConnectionContent.includes(oldSendMessageToGuacd)) { - missingAnchor("sendMessageToGuacd"); - } - clientConnectionContent = clientConnectionContent.replace( - oldSendMessageToGuacd, - newSendMessageToGuacd, - ); - patched = true; -} - -// Patch 8: mergeConnectionOptions only preserves `join` across the settings -// merge, dropping Termix's `readOnly` flag before sendMessageToGuacd can see it. -const oldPreserveJoin = - " // For join connections, preserve the join property\n" + - " if (this.connectionSettings.connection.join) {\n" + - " compiledSettings.join = this.connectionSettings.connection.join;\n" + - " }"; -const newPreserveJoin = - " // For join connections, preserve the join property\n" + - " if (this.connectionSettings.connection.join) {\n" + - " compiledSettings.join = this.connectionSettings.connection.join;\n" + - " compiledSettings.readOnly = this.connectionSettings.connection.readOnly === true;\n" + - " }"; - -if (!clientConnectionContent.includes("compiledSettings.readOnly")) { - if (!clientConnectionContent.includes(oldPreserveJoin)) { - missingAnchor("join-preserve"); - } - clientConnectionContent = clientConnectionContent.replace( - oldPreserveJoin, - newPreserveJoin, - ); - patched = true; -} - -// Patch 9: ClientConnection closes malformed-token WebSockets in its -// constructor, but Server.newConnection still called connect() afterwards. -// That dereferenced the absent connection settings and turned one bad token -// into an unhandled rejection that could terminate the backend process. -const oldConnectionSetup = - " newConnection.on('ready', async (clientConnection) => {"; -const newConnectionSetup = - " if (!newConnection.connectionSettings || !newConnection.connectionSettings.connection) {\n" + - " return;\n" + - " }\n" + - "\n" + - oldConnectionSetup; - -if (!serverContent.includes("!newConnection.connectionSettings.connection")) { - if (!serverContent.includes(oldConnectionSetup)) { - missingAnchor("invalid-token connection guard"); - } - serverContent = serverContent.replace(oldConnectionSetup, newConnectionSetup); - patched = true; -} - -if (!patched) { - console.log("[patch-guacamole-lite] Already patched"); - process.exit(0); -} - -fs.writeFileSync(guacdClientPath, guacdClientContent); -fs.writeFileSync(cryptPath, cryptContent); -fs.writeFileSync(clientConnectionPath, clientConnectionContent); -fs.writeFileSync(serverPath, serverContent); -console.log( - "[patch-guacamole-lite] Patched protocol negotiation, name handshake, required arguments, UTF-8 token decrypt, read-only joins, and malformed-token handling", -); diff --git a/plugins/remote-desktop/src/backend/drive-settings.ts b/plugins/remote-desktop/src/backend/drive-settings.ts deleted file mode 100644 index 12baa0ca7..000000000 --- a/plugins/remote-desktop/src/backend/drive-settings.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const GUACD_DRIVE_PATH_ENV = "GUACD_DRIVE_PATH"; -const DEFAULT_DRIVE_ROOT = "/drive"; - -/** - * Fills in where guacd keeps the files behind RDP drive redirection. - * - * The folder lives on the guacd host, not on Termix's - with the stock - * compose it is a directory in the shared termix-data volume, configured - * through GUACD_DRIVE_PATH. Each user gets a folder of their own underneath: - * a shared drive would show everyone's uploads to everyone else. A host that - * names its own drive-path keeps it. - */ -export function withDriveSettings( - guacConfig: Record, - userId: string, - env: NodeJS.ProcessEnv = process.env, -): Record { - if (!guacConfig["enable-drive"] || guacConfig["drive-path"]) { - return guacConfig; - } - const root = ( - env[GUACD_DRIVE_PATH_ENV]?.trim() || DEFAULT_DRIVE_ROOT - ).replace(/\/+$/, ""); - return { - ...guacConfig, - "drive-path": `${root}/${userId}`, - "create-drive-path": true, - }; -} diff --git a/plugins/remote-desktop/src/backend/guacamole-server.ts b/plugins/remote-desktop/src/backend/guacamole-server.ts deleted file mode 100644 index 2d2365f6f..000000000 --- a/plugins/remote-desktop/src/backend/guacamole-server.ts +++ /dev/null @@ -1,335 +0,0 @@ -import GuacamoleLite from "guacamole-lite"; -import fs from "fs"; -import path from "path"; -import type { IncomingMessage } from "http"; -import type { Duplex } from "stream"; -import type { - GuacamoleRecordingMetadata, - GuacamoleTokenService, - TermixGuacMeta, -} from "./token-service.js"; -import type { GuacdOptions } from "./guacd-config.js"; -import type { RemoteSessions } from "./sessions.js"; -import { errorMessage, type RemoteDesktopLogger } from "./log.js"; - -/** The recordings.writer service, when the session-recording plugin provides it. */ -export interface RecordingsWriter { - enabledFor: (hostId: number) => Promise; - createFinished: (input: { - hostId: number; - userId: string; - startedAt: string; - endedAt: string; - duration: number | null; - recordingPath: string; - protocol: string; - format: string; - }) => Promise<{ id: number }>; -} - -const DATA_DIR = process.env.DATA_DIR || "./db/data"; - -/** Where the backend reads guacd's recordings from. */ -export function recordingsDir(env: NodeJS.ProcessEnv = process.env): string { - return ( - env.GUACD_RECORDING_BACKEND_PATH || - path.join(DATA_DIR, "session_recordings", "guacamole") - ); -} - -type ClientConnection = { - guacamoleConnectionId?: string; - connectionSettings?: { - connection?: { type?: string; join?: string; readOnly?: boolean }; - recording?: GuacamoleRecordingMetadata; - termixMeta?: TermixGuacMeta; - }; - close?: () => void; -}; - -const clientOptions = ( - tokens: GuacamoleTokenService, - log: RemoteDesktopLogger, -) => ({ - crypt: { - cypher: "AES-256-CBC", - key: tokens.getEncryptionKey(), - }, - log: { - level: "ERRORS", - stdLog: (...args: unknown[]) => log.info(args.join(" ")), - errorLog: (...args: unknown[]) => log.error(args.join(" ")), - }, - allowedUnencryptedConnectionSettings: { - rdp: ["width", "height", "dpi"], - vnc: ["width", "height"], - 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, - audio: ["audio/L16"], - }, - vnc: { - "swap-red-blue": false, - cursor: "remote", - width: 1280, - height: 720, - // macOS Screen Sharing negotiates its VNC security type over several - // round trips (RFB type 30 -> 33/36/2/35) and can fail the first - // attempt; retrying lets guacd's VNC client re-establish instead of - // guacd giving up immediately (Support#1063). - autoretry: 2, - }, - telnet: { - "terminal-type": "xterm-256color", - }, - }, -}); - -export interface GuacamoleServerDeps { - log: RemoteDesktopLogger; - tokens: GuacamoleTokenService; - sessions: RemoteSessions; - guacd: () => GuacdOptions; - recordings: () => RecordingsWriter | null; - /** A socket event has no request behind it, so the row is written as the session's user. */ - asUser: (userId: string, fn: () => Promise) => Promise; - /** Called when a primary session opens; returns what to release on close. */ - onOpen?: (hostId: number) => () => void; -} - -export interface GuacamoleServer { - handleUpgrade: ( - request: IncomingMessage, - socket: Duplex, - head: Buffer, - ) => void; - /** Rebuilds the server so a changed guacd address takes effect. */ - restart: () => void; - /** Closes a session and everyone watching it. */ - endSession: (guacamoleConnectionId: string) => boolean; - close: () => void; -} - -/** - * guacamole-lite in noServer mode: core owns the listening socket and hands - * upgrades from /plugin-ws/remote-desktop/display to its WebSocketServer. - */ -export function createGuacamoleServer( - deps: GuacamoleServerDeps, -): GuacamoleServer { - const { log, sessions } = deps; - let server: GuacamoleLite | null = null; - - const persistRecording = async (connection: ClientConnection) => { - const recording = connection.connectionSettings?.recording; - if (!recording) return; - - const dir = recordingsDir(); - const resolvedPath = path.resolve(dir, recording.path); - if (!resolvedPath.startsWith(`${path.resolve(dir)}${path.sep}`)) return; - - // guacd may flush or rename the recording just after the socket closes. - for ( - let attempt = 0; - attempt < 10 && !fs.existsSync(resolvedPath); - attempt++ - ) { - await new Promise((resolve) => setTimeout(resolve, 100)); - } - if (!fs.existsSync(resolvedPath)) { - log.warn("Guacamole recording file was not found", { - operation: "guac_recording_missing", - hostId: recording.hostId, - path: resolvedPath, - guacdPath: recording.guacdPath ?? dir, - hint: - "guacd writes the recording to guacdPath, the backend reads it from path. " + - "When guacd runs in its own container these must be the same volume: set " + - "GUACD_RECORDING_PATH to guacd's mount point and GUACD_RECORDING_BACKEND_PATH to this one.", - }); - return; - } - - const writer = await deps.asUser(recording.userId, async () => - deps.recordings(), - ); - if (!writer) { - log.warn( - "Session Recording is off; the recording was kept on disk only", - { - operation: "guac_recording_no_writer", - hostId: recording.hostId, - }, - ); - return; - } - - const endedAt = new Date(); - const startedAt = new Date(recording.startedAt); - await deps.asUser(recording.userId, () => - writer.createFinished({ - hostId: recording.hostId, - userId: recording.userId, - startedAt: startedAt.toISOString(), - endedAt: endedAt.toISOString(), - duration: Math.max( - 0, - Math.floor((endedAt.getTime() - startedAt.getTime()) / 1000), - ), - recordingPath: resolvedPath, - protocol: recording.protocol, - format: "guacamole", - }), - ); - }; - - const build = (): GuacamoleLite => { - // guacamole-lite fills in port 8080 unless it sees a server key, and ws - // refuses a port next to noServer, so the port is cleared explicitly. - const guacamole = new GuacamoleLite( - { noServer: true, port: undefined }, - deps.guacd(), - clientOptions(deps.tokens, log), - ); - // It also installs its own SIGTERM and SIGINT handlers. Core decides - // when the process stops and closes this through deactivate instead. - const handlers = guacamole as unknown as { - sigTermHandler?: () => void; - sigIntHandler?: () => void; - }; - if (handlers.sigTermHandler) { - process.off("SIGTERM", handlers.sigTermHandler); - } - if (handlers.sigIntHandler) { - process.off("SIGINT", handlers.sigIntHandler); - } - - guacamole.on("open", (connection: ClientConnection) => { - const meta = connection.connectionSettings?.termixMeta; - const guacamoleConnectionId = connection.guacamoleConnectionId; - const isJoin = !!connection.connectionSettings?.connection?.join; - if (isJoin || !meta || !guacamoleConnectionId) return; - const release = deps.onOpen?.(meta.hostId); - sessions.opened(meta, guacamoleConnectionId, release ? [release] : []); - }); - - guacamole.on("close", (connection: ClientConnection) => { - const meta = connection.connectionSettings?.termixMeta; - const isJoin = !!connection.connectionSettings?.connection?.join; - if (!isJoin && meta) sessions.closed(meta.termixConnectId); - persistRecording(connection).catch((error) => { - log.error("Failed to persist Guacamole recording", { - operation: "guac_recording_persist_error", - error: errorMessage(error), - }); - }); - }); - - guacamole.on("error", (connection: ClientConnection, error: Error) => { - log.error("Guacamole connection error", { - operation: "guac_connection_error", - type: connection.connectionSettings?.connection?.type, - error: errorMessage(error), - }); - }); - - return guacamole; - }; - - const closeServer = () => { - if (!server) return; - try { - server.close(); - } catch (error) { - log.error("Error closing the Guacamole server", { - error: errorMessage(error), - }); - } - server = null; - }; - - server = build(); - - return { - handleUpgrade(request, socket, head) { - // The route is public, so the token is the only credential: refuse - // anything forged, edited or expired before guacamole-lite reads it. - const token = new URL( - request.url ?? "/", - "http://localhost", - ).searchParams.get("token"); - if (!token || !deps.tokens.verifyToken(token)) { - try { - socket.write( - "HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n", - ); - } catch { - // The peer may already be gone. - } - socket.destroy(); - return; - } - const wss = ( - server as unknown as { - webSocketServer?: { - handleUpgrade: ( - request: IncomingMessage, - socket: Duplex, - head: Buffer, - done: (ws: unknown) => void, - ) => void; - emit: (event: string, ...args: unknown[]) => void; - }; - } | null - )?.webSocketServer; - if (!wss) { - socket.destroy(); - return; - } - wss.handleUpgrade(request, socket, head, (ws) => { - wss.emit("connection", ws, request); - }); - }, - - restart() { - if (!server) return; - closeServer(); - server = build(); - }, - - endSession(guacamoleConnectionId) { - const active = ( - server as unknown as { - activeConnections?: Map; - } | null - )?.activeConnections; - if (!active) return false; - let closed = false; - for (const connection of [...active.values()]) { - const join = connection.connectionSettings?.connection?.join; - if ( - connection.guacamoleConnectionId === guacamoleConnectionId || - join === guacamoleConnectionId - ) { - connection.close?.(); - closed = true; - } - } - return closed; - }, - - close: closeServer, - }; -} diff --git a/plugins/remote-desktop/src/backend/guacd-config.ts b/plugins/remote-desktop/src/backend/guacd-config.ts deleted file mode 100644 index 9947ed4d7..000000000 --- a/plugins/remote-desktop/src/backend/guacd-config.ts +++ /dev/null @@ -1,86 +0,0 @@ -export type GuacdOptions = { - host: string; - port: number; -}; - -const DEFAULT_GUACD_OPTIONS: GuacdOptions = { - host: "localhost", - port: 4822, -}; - -function parsePort(value: string | undefined, fallback: number) { - const port = parseInt(value || "", 10); - return Number.isFinite(port) ? port : fallback; -} - -export function parseGuacdUrl( - value: string, - fallback: GuacdOptions = DEFAULT_GUACD_OPTIONS, -): GuacdOptions { - const raw = value.trim(); - if (!raw) { - return fallback; - } - - if (raw.includes("://")) { - try { - const url = new URL(raw); - return { - host: url.hostname || fallback.host, - port: parsePort(url.port, fallback.port), - }; - } catch { - return fallback; - } - } - - const bracketedIpv6 = raw.match(/^\[([^\]]+)\](?::(\d+))?$/); - if (bracketedIpv6) { - return { - host: bracketedIpv6[1], - port: parsePort(bracketedIpv6[2], fallback.port), - }; - } - - const [host, port] = raw.split(":"); - return { - host: host || fallback.host, - port: parsePort(port, fallback.port), - }; -} - -export function getGuacdEnvOptions(): GuacdOptions | null { - if (process.env.GUACD_URL) { - return parseGuacdUrl(process.env.GUACD_URL); - } - - if (!process.env.GUACD_HOST && !process.env.GUACD_PORT) { - return null; - } - - return { - host: process.env.GUACD_HOST || DEFAULT_GUACD_OPTIONS.host, - port: parsePort(process.env.GUACD_PORT, DEFAULT_GUACD_OPTIONS.port), - }; -} - -export function resolveGuacdOptions(dbUrl?: string | null): GuacdOptions { - const envOptions = getGuacdEnvOptions(); - if (envOptions) { - return envOptions; - } - - if (dbUrl) { - return parseGuacdUrl(dbUrl); - } - - return DEFAULT_GUACD_OPTIONS; -} - -export function formatGuacdOptions(options: GuacdOptions): string { - return `${options.host}:${options.port}`; -} - -export function getDefaultGuacdUrl(): string { - return formatGuacdOptions(resolveGuacdOptions()); -} diff --git a/plugins/remote-desktop/src/backend/host-settings.ts b/plugins/remote-desktop/src/backend/host-settings.ts deleted file mode 100644 index 2e4dcaefd..000000000 --- a/plugins/remote-desktop/src/backend/host-settings.ts +++ /dev/null @@ -1,236 +0,0 @@ -import type { PluginContext } from "@termix/plugin-sdk/backend"; - -export type RemoteProtocol = "rdp" | "vnc" | "telnet"; - -export const PLUGIN_ID = "remote-desktop"; - -export interface RemoteDesktopHostSettings { - enableRdp: boolean; - enableVnc: boolean; - enableTelnet: boolean; - rdpPort: number; - vncPort: number; - telnetPort: number; - rdpSecurity: string; - rdpIgnoreCert: boolean; - guacamoleConfig: Record; - enableToolbar: boolean; -} - -export const ENABLE_KEY: Record< - RemoteProtocol, - keyof RemoteDesktopHostSettings -> = { rdp: "enableRdp", vnc: "enableVnc", telnet: "enableTelnet" }; - -export const PORT_KEY: Record = - { rdp: "rdpPort", vnc: "vncPort", telnet: "telnetPort" }; - -export const DEFAULT_PORT: Record = { - rdp: 3389, - vnc: 5900, - telnet: 23, -}; - -export function isRemoteProtocol(value: unknown): value is RemoteProtocol { - return value === "rdp" || value === "vnc" || value === "telnet"; -} - -function asPort(value: unknown): number | undefined { - const port = typeof value === "string" ? Number(value) : value; - return typeof port === "number" && - Number.isInteger(port) && - port > 0 && - port <= 65535 - ? port - : undefined; -} - -function asObject(value: unknown): Record | undefined { - let parsed = value; - if (typeof value === "string") { - try { - parsed = JSON.parse(value); - } catch { - return undefined; - } - } - return parsed && typeof parsed === "object" && !Array.isArray(parsed) - ? (parsed as Record) - : undefined; -} - -function asBool(value: unknown): boolean | undefined { - if (typeof value === "boolean") return value; - if (value === 1 || value === "1" || value === "true") return true; - if (value === 0 || value === "0" || value === "false") return false; - return undefined; -} - -export async function readHostSettings( - ctx: PluginContext, - hostId: number, -): Promise { - const values = await ctx.settings.getAll("host", hostId); - return { - enableRdp: values.enableRdp === true, - enableVnc: values.enableVnc === true, - enableTelnet: values.enableTelnet === true, - rdpPort: asPort(values.rdpPort) ?? DEFAULT_PORT.rdp, - vncPort: asPort(values.vncPort) ?? DEFAULT_PORT.vnc, - telnetPort: asPort(values.telnetPort) ?? DEFAULT_PORT.telnet, - rdpSecurity: - typeof values.rdpSecurity === "string" ? values.rdpSecurity : "", - rdpIgnoreCert: values.rdpIgnoreCert === true, - guacamoleConfig: asObject(values.guacamoleConfig) ?? {}, - enableToolbar: values.enableToolbar !== false, - }; -} - -/** - * Turns an imported host row into this plugin's host settings. - * - * A Termix export carries them under pluginSettings["remote-desktop"]. Older - * exports, other tools and a plugin creating a host (proxmox) use the flat - * fields the host row had before 2.9.0, including the connectionType-only - * shape of hosts from before the per-protocol switches existed. - */ -export function normalizeImportedHost( - raw: Record, -): Record | null { - const nested = - asObject(asObject(raw.pluginSettings)?.[PLUGIN_ID]) ?? - ({} as Record); - const pick = (key: string, legacy?: string) => - nested[key] !== undefined - ? nested[key] - : raw[key] !== undefined - ? raw[key] - : legacy !== undefined - ? raw[legacy] - : undefined; - - const out: Record = {}; - for (const protocol of ["rdp", "vnc", "telnet"] as const) { - const key = ENABLE_KEY[protocol]; - const flag = asBool(pick(key)); - if (flag !== undefined) out[key] = flag; - const port = asPort(pick(PORT_KEY[protocol])); - if (port !== undefined) out[PORT_KEY[protocol]] = port; - } - - const anyFlag = ["enableRdp", "enableVnc", "enableTelnet"].some( - (key) => out[key] === true, - ); - if (!anyFlag && isRemoteProtocol(raw.connectionType)) { - out[ENABLE_KEY[raw.connectionType]] = true; - const port = asPort(raw.port); - if (port !== undefined && out[PORT_KEY[raw.connectionType]] === undefined) { - out[PORT_KEY[raw.connectionType]] = port; - } - } - - const security = pick("rdpSecurity", "security"); - if (typeof security === "string" && security) out.rdpSecurity = security; - const ignoreCert = asBool(pick("rdpIgnoreCert", "ignoreCert")); - if (ignoreCert !== undefined) out.rdpIgnoreCert = ignoreCert; - const config = asObject(pick("guacamoleConfig")); - if (config && Object.keys(config).length > 0) out.guacamoleConfig = config; - const toolbar = asBool(pick("enableToolbar", "enableTerminalToolbar")); - if (toolbar !== undefined) out.enableToolbar = toolbar; - - return Object.keys(out).length > 0 ? out : null; -} - -/** - * Registered as "remote-desktop.hostPayloadLegacy": each protocol's login - * in the flat 2.8 shape (rdpUser, rdpDomain, vncAuthType and so on), for - * Termix-Mobile until it reads protocolAuth. Built from the payload's own - * protocolAuth, which core already reduced for a shared recipient. - */ -export function hostPayloadLegacy( - _values: Record, - host: Record, -): Record | null { - const logins = asObject(host.protocolAuth); - if (!logins) return null; - const out: Record = {}; - for (const protocol of ["rdp", "vnc", "telnet"] as const) { - const login = asObject(logins[protocol]); - if (!login) continue; - out[`${protocol}AuthType`] = login.authType; - if (login.credentialId !== undefined) { - out[`${protocol}CredentialId`] = login.credentialId; - } - if (typeof login.username === "string" && login.username) { - out[`${protocol}User`] = login.username; - } - if (typeof login.hasPassword === "boolean") { - const name = protocol.charAt(0).toUpperCase() + protocol.slice(1); - out[`has${name}Password`] = login.hasPassword; - } - const domain = asObject(login.fields)?.domain; - if (protocol === "rdp" && typeof domain === "string" && domain) { - out.rdpDomain = domain; - out.domain = domain; - } - } - return Object.keys(out).length > 0 ? out : null; -} - -/** Display field key to the guacd parameter it sets. */ -const USER_DEFAULT_PARAMS: Record = { - colorDepth: "color-depth", - resizeMethod: "resize-method", - forceLossless: "force-lossless", - enableWallpaper: "enable-wallpaper", - enableFontSmoothing: "enable-font-smoothing", - enableDesktopComposition: "enable-desktop-composition", - disableAudio: "disable-audio", - enablePrinting: "enable-printing", - enableDrive: "enable-drive", - disableCopy: "disable-copy", - disablePaste: "disable-paste", -}; - -/** - * Display settings as guacd parameters. "inherit" leaves a parameter to the - * host and guacd; on and off become booleans. - */ -export function userDefaultParams( - values: Record, -): Record { - const params: Record = {}; - for (const [key, param] of Object.entries(USER_DEFAULT_PARAMS)) { - const value = values[key]; - if (value === undefined || value === null || value === "inherit") continue; - if (key === "colorDepth") { - const depth = Number(value); - if (Number.isInteger(depth) && depth > 0) params[param] = depth; - } else if (key === "resizeMethod") { - if (typeof value === "string" && value) params[param] = value; - } else if (value === "on" || value === "off") { - params[param] = value === "on"; - } - } - return params; -} - -/** - * The display settings a session runs with, as guacd parameters: the host's - * as this user sees them (their own defaults while the host follows its - * defaults), or with no host, this user's defaults. - */ -export async function readDisplayDefaults( - ctx: PluginContext, - userId: string, - hostId?: number, -): Promise> { - const values: Record = {}; - for (const key of Object.keys(USER_DEFAULT_PARAMS)) { - values[key] = - hostId === undefined - ? await ctx.settings.getHostDefault(userId, key) - : await ctx.settings.getHostFor(hostId, userId, key); - } - return userDefaultParams(values); -} diff --git a/plugins/remote-desktop/src/backend/index.ts b/plugins/remote-desktop/src/backend/index.ts deleted file mode 100644 index 7fc19ebc0..000000000 --- a/plugins/remote-desktop/src/backend/index.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { GuacamoleTokenService } from "./token-service.js"; -import { RemoteSessions } from "./sessions.js"; -import { - createGuacamoleServer, - type RecordingsWriter, -} from "./guacamole-server.js"; -import { resolveGuacdOptions } from "./guacd-config.js"; -import { registerRoutes } from "./routes.js"; -import { createLiveSessions } from "./live-sessions.js"; -import { - hostPayloadLegacy, - normalizeImportedHost, - PORT_KEY, - readHostSettings, -} from "./host-settings.js"; -import { createLogger } from "./log.js"; - -export async function activate(ctx: PluginContext) { - const log = createLogger(ctx.log); - const tokens = new GuacamoleTokenService(log); - const sessions = new RemoteSessions(); - ctx.disposables.add(() => sessions.clear()); - - // The admin setting, unless GUACD_URL / GUACD_HOST / GUACD_PORT override it. - let guacdUrl = (await ctx.settings.get("guacdUrl")) ?? ""; - const guacd = () => resolveGuacdOptions(guacdUrl || undefined); - const enabled = async () => - (await ctx.settings.get("enabled")) !== false; - - // Optional: without session-recording nothing is recorded. - const recordings = (): RecordingsWriter | null => { - const service = - ctx.services.get>("recordings.writer"); - return typeof service.createFinished === "function" && - typeof service.enabledFor === "function" - ? (service as RecordingsWriter) - : null; - }; - - // guacamole-lite prints every new connection to stdout. - const consoleLog = console.log; - console.log = (...args: unknown[]) => { - if ( - typeof args[0] === "string" && - args[0].startsWith("New client connection") - ) - return; - consoleLog(...args); - }; - ctx.disposables.add(() => { - console.log = consoleLog; - }); - - const server = createGuacamoleServer({ - log, - tokens, - sessions, - guacd, - recordings, - asUser: (userId, fn) => ctx.asUser(userId, fn), - onOpen: (hostId) => ctx.hosts.trackSession(hostId), - }); - ctx.disposables.add(() => server.close()); - - ctx.settings.onChange("guacdUrl", (value) => { - guacdUrl = typeof value === "string" ? value : ""; - server.restart(); - }); - - registerRoutes(ctx.http.router(), { - ctx, - log, - tokens, - sessions, - guacd, - enabled, - recordings, - }); - - // guacamole-lite owns its own WebSocketServer, so it takes the raw upgrade. - // Public because a display authenticates with the encrypted, single-use - // token minted by an authenticated route, not with a session JWT. - ctx.ws.upgrade( - "/display", - (request, socket, head) => - server.handleUpgrade( - request as Parameters[0], - socket as Parameters[1], - head as Buffer, - ), - { public: true }, - ); - - for (const protocol of ["rdp", "vnc", "telnet"] as const) { - ctx.services.provide( - "sessions.live", - createLiveSessions(protocol, { - sessions, - tokens, - endSession: server.endSession, - }), - { name: protocol }, - ); - } - - ctx.registry.provide( - "remote-desktop.hostImportNormalizer", - normalizeImportedHost, - ); - ctx.registry.provide("remote-desktop.hostPayloadLegacy", hostPayloadLegacy); - - // Core's status dot pings these hosts on the protocol's own port. - for (const protocol of ["rdp", "vnc", "telnet"] as const) { - ctx.hosts.status.registerPort(protocol, async (hostId) => { - const settings = await readHostSettings(ctx, hostId); - return settings[PORT_KEY[protocol]] as number; - }); - } -} - -export async function deactivate() {} diff --git a/plugins/remote-desktop/src/backend/jump-tunnel-endpoint.ts b/plugins/remote-desktop/src/backend/jump-tunnel-endpoint.ts deleted file mode 100644 index e05f1e29b..000000000 --- a/plugins/remote-desktop/src/backend/jump-tunnel-endpoint.ts +++ /dev/null @@ -1,15 +0,0 @@ -const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]); - -export function resolveJumpTunnelEndpoint( - guacdHost: string, - tunnelHost = process.env.GUACD_TUNNEL_HOST, -): { bindHost: string; advertisedHost: string } { - if (LOOPBACK_HOSTS.has(guacdHost.toLowerCase())) { - return { bindHost: "127.0.0.1", advertisedHost: "127.0.0.1" }; - } - - return { - bindHost: "0.0.0.0", - advertisedHost: tunnelHost?.trim() || "termix", - }; -} diff --git a/plugins/remote-desktop/src/backend/jump-tunnel.ts b/plugins/remote-desktop/src/backend/jump-tunnel.ts deleted file mode 100644 index 9e0377842..000000000 --- a/plugins/remote-desktop/src/backend/jump-tunnel.ts +++ /dev/null @@ -1,145 +0,0 @@ -import net from "net"; -import type { Client } from "ssh2"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import type { RemoteDesktopLogger } from "./log.js"; - -/** The tunnels plugin's service, as ctx.services.get("tunnels.access") returns it. */ -interface TunnelsAccess { - forward( - sourceHostId: number, - target: { targetHost: string; targetPort: number; bindHost?: string }, - options?: { name?: string; idleTimeoutMs?: number }, - ): Promise<{ - bindHost: string; - bindPort: number; - close: () => Promise; - }>; -} - -/** The tunnels plugin keeps these names for on-demand forwards. */ -const TUNNEL_NAME_PREFIX = "web:"; -/** A forward nothing is using closes itself after this long. */ -const FORWARD_IDLE_TIMEOUT_MS = 10 * 60 * 1000; - -export interface JumpTunnel { - port: number; - close: () => void; -} - -export interface JumpTunnelRequest { - jumpHosts: Array<{ hostId: number }>; - targetHost: string; - targetPort: number; - bindHost: string; - /** Names the forward, so each session gets one of its own. */ - connectId: string; -} - -/** - * A local port guacd can dial that reaches the target through the host's - * jump hosts. - * - * One hop is exactly what the tunnels plugin's forward does, so that is used - * when the plugin is running and the user may use it. A chain, or no tunnels - * plugin, goes through ctx.ssh.jumpChain with a listener of our own. Either - * way the caller closes it when the session ends. - */ -export async function openJumpTunnel( - ctx: PluginContext, - log: RemoteDesktopLogger, - request: JumpTunnelRequest, -): Promise { - if (request.jumpHosts.length === 1) { - const viaTunnels = await forwardThroughTunnels(ctx, log, request); - if (viaTunnels) return viaTunnels; - } - return forwardThroughChain(ctx, request); -} - -async function forwardThroughTunnels( - ctx: PluginContext, - log: RemoteDesktopLogger, - request: JumpTunnelRequest, -): Promise { - const tunnels = ctx.services.get>("tunnels.access"); - if (typeof tunnels.forward !== "function") return null; - const hopId = request.jumpHosts[0].hostId; - try { - const handle = await tunnels.forward( - hopId, - { - targetHost: request.targetHost, - targetPort: request.targetPort, - bindHost: request.bindHost, - }, - { - name: `${TUNNEL_NAME_PREFIX}${hopId}:rd-${request.connectId}`, - idleTimeoutMs: FORWARD_IDLE_TIMEOUT_MS, - }, - ); - return { - port: handle.bindPort, - close: () => void handle.close().catch(() => undefined), - }; - } catch (error) { - // A user without the tunnels permission still reaches their host. - log.warn("Tunnels forward failed, using a jump chain instead", { - operation: "guac_tunnels_forward", - hopId, - error: error instanceof Error ? error.message : String(error), - }); - return null; - } -} - -async function forwardThroughChain( - ctx: PluginContext, - request: JumpTunnelRequest, -): Promise { - // The chain dials the first hop through that hop's own SOCKS5 settings; - // the target host's proxy config does not apply to it. - const { client, dispose } = await ctx.ssh.jumpChain( - request.jumpHosts, - ); - - const sockets = new Set(); - const server = net.createServer((socket) => { - sockets.add(socket); - socket.once("close", () => sockets.delete(socket)); - client.forwardOut( - "127.0.0.1", - 0, - request.targetHost, - request.targetPort, - (error, stream) => { - if (error) { - socket.destroy(); - return; - } - socket.pipe(stream).pipe(socket); - }, - ); - }); - - let port: number; - try { - port = await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, request.bindHost, () => - resolve((server.address() as net.AddressInfo).port), - ); - }); - } catch (error) { - await dispose(); - throw error; - } - - return { - port, - close: () => { - for (const socket of sockets) socket.destroy(); - server.close(); - void dispose(); - }, - }; -} diff --git a/plugins/remote-desktop/src/backend/live-sessions.ts b/plugins/remote-desktop/src/backend/live-sessions.ts deleted file mode 100644 index 5f3189f58..000000000 --- a/plugins/remote-desktop/src/backend/live-sessions.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { GuacamoleTokenService } from "./token-service.js"; -import type { RemoteSessions } from "./sessions.js"; -import type { RemoteProtocol } from "./host-settings.js"; - -/** sessions.live v1, as session-sharing reads it (plugins/session-sharing/src/backend/live.ts). */ -export interface LiveSessionInfo { - id: string; - userId: string; - hostId: number; - hostName: string; - isConnected: boolean; - createdAt: number; - tabInstanceId: string | null; -} - -export interface RemoteLiveSessions { - getSession: (sessionId: string) => LiveSessionInfo | null; - ownerEndSession: (sessionId: string, reason: string) => void; - createViewerToken: (sessionId: string, readOnly: boolean) => string; -} - -/** - * One provider per protocol. A session id is guacd's own connection id, - * which is what a viewer's join token names. - */ -export function createLiveSessions( - protocol: RemoteProtocol, - deps: { - sessions: RemoteSessions; - tokens: GuacamoleTokenService; - endSession: (guacamoleConnectionId: string) => boolean; - }, -): RemoteLiveSessions { - const find = (sessionId: string) => { - const session = deps.sessions.byGuacamole(sessionId); - return session && session.protocol === protocol ? session : null; - }; - - return { - getSession(sessionId) { - const session = find(sessionId); - if (!session) return null; - return { - id: session.guacamoleConnectionId, - userId: session.ownerUserId, - hostId: session.hostId, - hostName: session.hostName, - isConnected: true, - createdAt: session.openedAt, - tabInstanceId: session.tabInstanceId, - }; - }, - - ownerEndSession(sessionId) { - if (find(sessionId)) deps.endSession(sessionId); - }, - - createViewerToken(sessionId, readOnly) { - if (!find(sessionId)) { - throw new Error(`No live ${protocol} session ${sessionId}`); - } - return deps.tokens.createJoinToken(sessionId, readOnly); - }, - }; -} diff --git a/plugins/remote-desktop/src/backend/log.ts b/plugins/remote-desktop/src/backend/log.ts deleted file mode 100644 index 010d0f660..000000000 --- a/plugins/remote-desktop/src/backend/log.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { PluginLogger } from "@termix/plugin-sdk/backend"; - -export type Meta = Record; - -export interface RemoteDesktopLogger { - info: (message: string, meta?: Meta) => void; - warn: (message: string, meta?: Meta) => void; - error: (message: string, meta?: Meta) => void; -} - -function withMeta(message: string, meta?: Meta): string { - return meta && Object.keys(meta).length > 0 - ? `${message} ${JSON.stringify(meta)}` - : message; -} - -/** ctx.log with the structured context the old guac logger carried. */ -export function createLogger(log: PluginLogger): RemoteDesktopLogger { - return { - info: (message, meta) => log.info(withMeta(message, meta)), - warn: (message, meta) => log.warn(withMeta(message, meta)), - error: (message, meta) => log.error(withMeta(message, meta)), - }; -} - -export function errorMessage(error: unknown, fallback = "Unknown error") { - if (error instanceof Error && error.message) return error.message; - if (typeof error === "string" && error) return error; - return fallback; -} diff --git a/plugins/remote-desktop/src/backend/macos-vnc-proxy.ts b/plugins/remote-desktop/src/backend/macos-vnc-proxy.ts deleted file mode 100644 index b3c420a82..000000000 --- a/plugins/remote-desktop/src/backend/macos-vnc-proxy.ts +++ /dev/null @@ -1,97 +0,0 @@ -import net from "net"; - -const RFB_BANNER_LENGTH = 12; -const VNC_AUTH_SECURITY_TYPE = 2; -const MACOS_RFB_BANNER = Buffer.from("RFB 003.889\n", "ascii"); -const STANDARD_RFB_BANNER = Buffer.from("RFB 003.008\n", "ascii"); - -export interface VncCompatibilityProxy { - port: number; - close: () => void; -} - -export async function createMacosVncCompatibilityProxy({ - targetHost, - targetPort, - bindHost, -}: { - targetHost: string; - targetPort: number; - bindHost: string; -}): Promise { - const sockets = new Set(); - const server = net.createServer((client) => { - const upstream = net.createConnection(targetPort, targetHost); - sockets.add(client); - sockets.add(upstream); - - const forget = (socket: net.Socket) => sockets.delete(socket); - client.once("close", () => forget(client)); - upstream.once("close", () => forget(upstream)); - client.once("error", () => upstream.destroy()); - upstream.once("error", () => client.destroy()); - client.pipe(upstream); - - let pending = Buffer.alloc(0); - const forwardMacosSecurityTypes = (chunk: Buffer) => { - pending = Buffer.concat([pending, chunk]); - if (pending.length < 1) return; - - const securityTypeCount = pending[0]; - if (securityTypeCount === 0) { - upstream.off("data", forwardMacosSecurityTypes); - client.write(pending); - upstream.pipe(client); - return; - } - if (pending.length < 1 + securityTypeCount) return; - - upstream.off("data", forwardMacosSecurityTypes); - const securityTypes = pending.subarray(1, 1 + securityTypeCount); - if (securityTypes.includes(VNC_AUTH_SECURITY_TYPE)) { - client.write(Buffer.from([1, VNC_AUTH_SECURITY_TYPE])); - } else { - client.write(pending.subarray(0, 1 + securityTypeCount)); - } - client.write(pending.subarray(1 + securityTypeCount)); - upstream.pipe(client); - }; - - const forwardServerBanner = (chunk: Buffer) => { - pending = Buffer.concat([pending, chunk]); - if (pending.length < RFB_BANNER_LENGTH) return; - - upstream.off("data", forwardServerBanner); - const banner = pending.subarray(0, RFB_BANNER_LENGTH); - const remainder = pending.subarray(RFB_BANNER_LENGTH); - if (banner.equals(MACOS_RFB_BANNER)) { - client.write(STANDARD_RFB_BANNER); - pending = Buffer.alloc(0); - upstream.on("data", forwardMacosSecurityTypes); - if (remainder.length > 0) forwardMacosSecurityTypes(remainder); - } else { - client.write(banner); - client.write(remainder); - upstream.pipe(client); - } - }; - upstream.on("data", forwardServerBanner); - }); - - const port = await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, bindHost, () => { - server.off("error", reject); - resolve((server.address() as net.AddressInfo).port); - }); - }); - server.unref(); - - return { - port, - close: () => { - server.close(); - for (const socket of sockets) socket.destroy(); - }, - }; -} diff --git a/plugins/remote-desktop/src/backend/rdp-settings.ts b/plugins/remote-desktop/src/backend/rdp-settings.ts deleted file mode 100644 index 63ada8e14..000000000 --- a/plugins/remote-desktop/src/backend/rdp-settings.ts +++ /dev/null @@ -1,36 +0,0 @@ -interface RdpSettingsInput { - port: number; - domain?: string; - security?: string; - ignoreCert: boolean; - guacConfig: Record; - guacdOverrides: Record; -} - -export function buildRdpSettings({ - port, - domain, - security, - ignoreCert, - guacConfig, - guacdOverrides, -}: RdpSettingsInput): Record { - return { - ...guacConfig, - port, - domain, - ...(security === undefined ? {} : { security }), - "ignore-cert": ignoreCert, - ...guacdOverrides, - }; -} - -export function resolveRdpDomain( - authType: string | null, - promptedDomain: unknown, - storedDomain: string, -): string { - return authType === "none" && typeof promptedDomain === "string" - ? promptedDomain - : storedDomain; -} diff --git a/plugins/remote-desktop/src/backend/recording-settings.ts b/plugins/remote-desktop/src/backend/recording-settings.ts deleted file mode 100644 index 323691691..000000000 --- a/plugins/remote-desktop/src/backend/recording-settings.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Merges Termix's recording bookkeeping into a host's guacd settings. - * - * Location and filename are not the host's to choose: recordings are indexed by - * them for playback, and the backend refuses to read anything outside its - * recordings directory. What a recording *contains* is a host-level decision, so - * those flags are only defaulted, never overwritten. - */ -export function withRecordingSettings( - guacConfig: Record, - recordingPath: string, - recordingName: string, -): Record { - return { - ...guacConfig, - "recording-path": recordingPath, - "recording-name": recordingName, - "create-recording-path": true, - "recording-exclude-output": guacConfig["recording-exclude-output"] ?? false, - "recording-include-keys": guacConfig["recording-include-keys"] ?? true, - }; -} diff --git a/plugins/remote-desktop/src/backend/routes.ts b/plugins/remote-desktop/src/backend/routes.ts deleted file mode 100644 index 71d1dcd73..000000000 --- a/plugins/remote-desktop/src/backend/routes.ts +++ /dev/null @@ -1,655 +0,0 @@ -import crypto from "crypto"; -import net from "net"; -import path from "path"; -import type { Request, Response, Router } from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { - isServerOwnedSetting, - type GuacamoleTokenService, -} from "./token-service.js"; -import type { GuacdOptions } from "./guacd-config.js"; -import type { RemoteSessions } from "./sessions.js"; -import type { RecordingsWriter } from "./guacamole-server.js"; -import { recordingsDir } from "./guacamole-server.js"; -import { withRecordingSettings } from "./recording-settings.js"; -import { withDriveSettings } from "./drive-settings.js"; -import { resolveJumpTunnelEndpoint } from "./jump-tunnel-endpoint.js"; -import { buildRdpSettings, resolveRdpDomain } from "./rdp-settings.js"; -import { createMacosVncCompatibilityProxy } from "./macos-vnc-proxy.js"; -import { openJumpTunnel } from "./jump-tunnel.js"; -import { - DEFAULT_PORT, - ENABLE_KEY, - PORT_KEY, - isRemoteProtocol, - readHostSettings, - readDisplayDefaults, - type RemoteProtocol, -} from "./host-settings.js"; -import { errorMessage, type RemoteDesktopLogger } from "./log.js"; - -export interface RouteDeps { - ctx: PluginContext; - log: RemoteDesktopLogger; - tokens: GuacamoleTokenService; - sessions: RemoteSessions; - guacd: () => GuacdOptions; - enabled: () => Promise; - recordings: () => RecordingsWriter | null; -} - -function actor(ctx: PluginContext, res: Response): string | null { - const userId = ctx.currentActor(); - if (!userId) res.status(401).json({ error: "Authentication required" }); - return userId ?? null; -} - -function probeGuacd({ host, port }: GuacdOptions): Promise { - return new Promise((resolve) => { - const socket = new net.Socket(); - socket.setTimeout(3000); - const done = (ok: boolean) => { - socket.destroy(); - resolve(ok); - }; - socket.once("connect", () => done(true)); - socket.once("timeout", () => done(false)); - socket.once("error", () => done(false)); - socket.connect(port, host); - }); -} - -/** guacd reads these settings as given; the editor stores "auto" for its default. */ -function cleanGuacConfig(raw: Record): { - config: Record; - guacdOverrides: { guacdHost?: string; guacdPort?: number }; -} { - const config: Record = {}; - for (const [key, value] of Object.entries(raw)) { - if (value !== "auto") config[key] = value; - } - const guacdHost = - typeof config["guacd-hostname"] === "string" && config["guacd-hostname"] - ? (config["guacd-hostname"] as string) - : undefined; - const guacdPort = config["guacd-port"] - ? parseInt(String(config["guacd-port"]), 10) || undefined - : undefined; - delete config["guacd-hostname"]; - delete config["guacd-port"]; - if (config.dpi != null) { - const dpi = parseInt(String(config.dpi), 10); - config.dpi = Number.isFinite(dpi) && dpi > 0 ? dpi : undefined; - } - return { - config, - guacdOverrides: { - ...(guacdHost ? { guacdHost } : {}), - ...(guacdPort ? { guacdPort } : {}), - }, - }; -} - -export function registerRoutes(router: Router, deps: RouteDeps): void { - const { ctx, log, tokens, sessions } = deps; - - const refuseWhenOff = async (res: Response): Promise => { - if (await deps.enabled()) return false; - res.status(403).json({ error: "Remote Desktop is turned off" }); - return true; - }; - - /** - * @openapi - * /plugin-api/remote-desktop/connection/{connectId}: - * get: - * summary: Look up guacd's id for a session - * description: Returns the guacamole connection id for a connect request, once guacd has opened it. Only the session's owner gets an answer. - * tags: - * - Remote Desktop - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: connectId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The id, or null while guacd has not opened the session. - * 401: - * description: Not signed in. - */ - router.get("/connection/:connectId", (req: Request, res: Response) => { - const userId = actor(ctx, res); - if (!userId) return; - const session = sessions.byConnect(String(req.params.connectId)); - res.json({ - guacamoleConnectionId: - session?.ownerUserId === userId ? session.guacamoleConnectionId : null, - }); - }); - - /** - * @openapi - * /plugin-api/remote-desktop/token: - * post: - * summary: Mint a connection token for an address - * description: Creates an encrypted guacamole-lite token for connection details the caller supplies, used by Quick Connect. No saved host is involved. - * tags: - * - Remote Desktop - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - type - * - hostname - * properties: - * type: - * type: string - * enum: [rdp, vnc, telnet] - * hostname: - * type: string - * port: - * type: integer - * username: - * type: string - * password: - * type: string - * domain: - * type: string - * responses: - * 200: - * description: The encrypted token. - * 400: - * description: Missing or invalid fields. - * 403: - * description: Remote Desktop is turned off. - * 500: - * description: Server error. - */ - router.post("/token", async (req: Request, res: Response) => { - try { - if (await refuseWhenOff(res)) return; - const { type, hostname, port, username, password, domain, ...raw } = - (req.body ?? {}) as Record; - if (!type || !hostname) { - return res - .status(400) - .json({ error: "Missing required fields: type and hostname" }); - } - if (!isRemoteProtocol(type)) { - return res.status(400).json({ - error: "Invalid connection type. Must be rdp, vnc, or telnet", - }); - } - const options: Record = {}; - for (const [key, value] of Object.entries(raw)) { - // A quick connect names a host, never where guacd is or what paths - // it writes to: those would let any user aim guacd anywhere. - if (isServerOwnedSetting(key)) continue; - if (value !== "auto") options[key] = value; - } - const host = String(hostname); - const user = username ? String(username) : ""; - const pass = password ? String(password) : ""; - const targetPort = Number(port) || DEFAULT_PORT[type]; - const userId = ctx.currentActor(); - if (type === "rdp" && userId) { - for (const [key, value] of Object.entries( - await readDisplayDefaults(ctx, userId), - )) { - if (isServerOwnedSetting(key)) continue; - if (options[key] === undefined) options[key] = value; - } - } - - let token: string; - if (type === "rdp") { - token = tokens.createRdpToken(host, user, pass, { - port: targetPort, - domain: domain ? String(domain) : undefined, - ...options, - }); - } else if (type === "vnc") { - token = tokens.createVncToken(host, user || undefined, pass, { - port: targetPort, - ...options, - }); - } else { - token = tokens.createTelnetToken(host, user, pass, { - port: targetPort, - ...options, - }); - } - res.json({ token }); - } catch (error) { - log.error("Failed to generate a connection token", { - operation: "guac_token_error", - error: errorMessage(error), - }); - res.status(500).json({ error: "Failed to generate connection token" }); - } - }); - - /** - * @openapi - * /plugin-api/remote-desktop/connect-host/{hostId}: - * post: - * summary: Mint a connection token for a saved host - * description: Resolves the host's RDP, VNC or Telnet login for the caller (a shared recipient gets only what was shared with them), opens a jump tunnel when the host has jump hosts, and returns an encrypted guacamole-lite token. - * tags: - * - Remote Desktop - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: hostId - * required: true - * schema: - * type: integer - * requestBody: - * content: - * application/json: - * schema: - * type: object - * properties: - * protocol: - * type: string - * enum: [rdp, vnc, telnet] - * promptedUsername: - * type: string - * promptedPassword: - * type: string - * promptedDomain: - * type: string - * tabInstanceId: - * type: string - * responses: - * 200: - * description: The token and the id to look the session up by. - * 400: - * description: Invalid host id, or the protocol is off for this host. - * 403: - * description: Remote Desktop is turned off. - * 404: - * description: Host not found or no access. - * 500: - * description: The tunnel or the token could not be set up. - */ - router.post("/connect-host/:hostId", async (req: Request, res: Response) => { - try { - const userId = actor(ctx, res); - if (!userId) return; - if (await refuseWhenOff(res)) return; - - const hostId = Number.parseInt(String(req.params.hostId), 10); - if (!hostId) return res.status(400).json({ error: "Invalid host ID" }); - - const body = (req.body ?? {}) as Record; - const settings = await readHostSettings(ctx, hostId); - const requested = body.protocol; - const protocol: RemoteProtocol | undefined = isRemoteProtocol(requested) - ? requested - : (["rdp", "vnc", "telnet"] as const).find( - (candidate) => settings[ENABLE_KEY[candidate]], - ); - if (!protocol) { - return res - .status(400) - .json({ error: "Remote Desktop is not enabled for this host." }); - } - - const target = await ctx.credentials.resolveHostProtocol( - hostId, - protocol, - ); - if (!target) { - return res.status(404).json({ error: "Host not found" }); - } - if (!settings[ENABLE_KEY[protocol]]) { - return res.status(400).json({ - error: `${protocol.toUpperCase()} is not enabled for this host.`, - }); - } - - // The display settings sit under whatever the host's guacd settings say. - const { config, guacdOverrides } = cleanGuacConfig({ - ...(protocol === "rdp" - ? await readDisplayDefaults(ctx, userId, hostId) - : {}), - ...settings.guacamoleConfig, - }); - let guacConfig = config; - - const promptForLogin = - protocol === "rdp" && target.auth.authType === "none"; - const username = promptForLogin - ? String(body.promptedUsername || "") - : target.auth.username; - const password = promptForLogin - ? String(body.promptedPassword || "") - : target.auth.password; - const domain = resolveRdpDomain( - target.auth.authType, - body.promptedDomain, - target.auth.fields.domain ?? "", - ); - - let hostname = target.host.ip; - let port = - (settings[PORT_KEY[protocol]] as number) || DEFAULT_PORT[protocol]; - const termixConnectId = crypto.randomUUID(); - const cleanups: Array<() => void> = []; - const jumpHosts = target.host.jumpHosts; - const needsVncProxy = protocol === "vnc" && !username; - - const endpoint = - jumpHosts.length > 0 || needsVncProxy - ? resolveJumpTunnelEndpoint( - guacdOverrides.guacdHost || deps.guacd().host, - ) - : null; - - try { - if (jumpHosts.length > 0 && endpoint) { - const tunnel = await openJumpTunnel(ctx, log, { - jumpHosts, - targetHost: hostname, - targetPort: port, - bindHost: endpoint.bindHost, - connectId: termixConnectId, - }); - cleanups.push(tunnel.close); - hostname = endpoint.advertisedHost; - port = tunnel.port; - } - - if (needsVncProxy && endpoint) { - const proxy = await createMacosVncCompatibilityProxy({ - targetHost: hostname, - targetPort: port, - bindHost: endpoint.bindHost, - }); - cleanups.push(proxy.close); - hostname = endpoint.advertisedHost; - port = proxy.port; - } - } catch (error) { - for (const cleanup of cleanups) cleanup(); - log.error("Failed to reach the host through its jump hosts", { - operation: "guac_ssh_tunnel_error", - hostId, - error: errorMessage(error), - }); - return res - .status(500) - .json({ error: "Failed to establish SSH tunnel to remote host" }); - } - sessions.park(termixConnectId, cleanups); - - const recordings = deps.recordings(); - const recordingEnabled = - protocol !== "vnc" && - !!recordings && - (await recordings.enabledFor(hostId)); - const recordingName = `${crypto.randomUUID()}.guac`; - const guacdRecordingPath = - process.env.GUACD_RECORDING_PATH || - process.env.GUACD_RECORDING_BACKEND_PATH || - path.resolve(recordingsDir()); - const recording = recordingEnabled - ? { - hostId, - userId, - protocol, - path: recordingName, - guacdPath: guacdRecordingPath, - startedAt: new Date().toISOString(), - } - : undefined; - if (recordingEnabled) { - guacConfig = withRecordingSettings( - guacConfig, - guacdRecordingPath, - recordingName, - ); - } - - const termixMeta = { - termixConnectId, - hostId, - hostName: target.host.name || target.host.ip, - ownerUserId: userId, - protocol, - tabInstanceId: - typeof body.tabInstanceId === "string" ? body.tabInstanceId : null, - }; - - let token: string; - if (protocol === "rdp") { - token = tokens.createRdpToken( - hostname, - username, - password, - buildRdpSettings({ - port, - domain, - security: settings.rdpSecurity || undefined, - ignoreCert: settings.rdpIgnoreCert, - guacConfig: withDriveSettings(guacConfig, userId), - guacdOverrides, - }), - recording, - termixMeta, - ); - } else if (protocol === "vnc") { - token = tokens.createVncToken( - hostname, - username || undefined, - password, - { port, ...guacConfig, ...guacdOverrides }, - recording, - termixMeta, - ); - } else { - token = tokens.createTelnetToken( - hostname, - username, - password, - { port, ...guacConfig, ...guacdOverrides }, - recording, - termixMeta, - ); - } - - await ctx.audit.record({ - action: `${protocol}_connect`, - resourceType: "host", - resourceId: String(hostId), - resourceName: `${hostname}:${port}`, - success: true, - }); - - res.json({ token, termixConnectId, guacamoleConnectionId: null }); - } catch (error) { - log.error("Failed to generate a connection token for a host", { - operation: "guac_host_token_error", - error: errorMessage(error), - }); - res.status(500).json({ error: "Failed to generate connection token" }); - } - }); - - /** - * @openapi - * /plugin-api/remote-desktop/status: - * get: - * summary: Remote Desktop status - * description: Whether Remote Desktop is turned on, and whether guacd answers at the configured address. - * tags: - * - Remote Desktop - * security: - * - bearerAuth: [] - * parameters: - * - in: query - * name: probe - * schema: - * type: string - * description: Set to 0 to skip dialing guacd. - * - in: query - * name: hostId - * schema: - * type: integer - * description: Saved host whose guacd settings should be checked; requires protocol and access to that host. - * - in: query - * name: protocol - * schema: - * type: string - * enum: [rdp, vnc, telnet] - * responses: - * 200: - * description: The status. - * 500: - * description: The check failed. - */ - router.get("/status", async (req: Request, res: Response) => { - try { - const enabled = await deps.enabled(); - let guacd = deps.guacd(); - if (req.query.hostId !== undefined) { - const userId = actor(ctx, res); - if (!userId) return; - const hostId = Number(req.query.hostId); - const protocol = req.query.protocol; - if ( - !Number.isSafeInteger(hostId) || - hostId <= 0 || - !isRemoteProtocol(protocol) - ) { - return res.status(400).json({ error: "Invalid host ID or protocol" }); - } - const target = await ctx.credentials.resolveHostProtocol( - hostId, - protocol, - ); - if (!target) return res.status(404).json({ error: "Host not found" }); - const settings = await readHostSettings(ctx, hostId); - if (!settings[ENABLE_KEY[protocol]]) { - return res - .status(400) - .json({ error: "Protocol is not enabled for this host" }); - } - const { guacdOverrides } = cleanGuacConfig({ - ...(protocol === "rdp" - ? await readDisplayDefaults(ctx, userId, hostId) - : {}), - ...settings.guacamoleConfig, - }); - guacd = { - host: guacdOverrides.guacdHost || guacd.host, - port: guacdOverrides.guacdPort || guacd.port, - }; - } - const probe = enabled && req.query.probe !== "0"; - const reachable = probe ? await probeGuacd(guacd) : false; - res.json({ - enabled, - guacd: { - host: guacd.host, - port: guacd.port, - status: reachable ? "connected" : "disconnected", - }, - websocket: { - path: "/plugin-ws/remote-desktop/display", - status: "running", - }, - }); - } catch (error) { - log.error("Failed to check Remote Desktop status", { - operation: "guac_status_error", - error: errorMessage(error), - }); - res.status(500).json({ error: "Failed to check status" }); - } - }); - - /** - * @openapi - * /plugin-api/remote-desktop/native-rdp: - * get: - * summary: Whether the native RDP client can be opened - * description: True only in the Windows desktop app. - * tags: - * - Remote Desktop - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Availability. - * post: - * summary: Open the native RDP client - * description: Opens the operating system's own RDP client (mstsc) for an address. The password is never passed; the client asks for it. Windows desktop app only. - * tags: - * - Remote Desktop - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - host - * properties: - * host: - * type: string - * port: - * type: integer - * username: - * type: string - * domain: - * type: string - * responses: - * 200: - * description: Whether the client opened. - * 400: - * description: Missing host. - * 404: - * description: Not running in the desktop app. - */ - router.get("/native-rdp", (_req: Request, res: Response) => { - res.json({ - available: ctx.desktop.available() && process.platform === "win32", - }); - }); - - router.post("/native-rdp", async (req: Request, res: Response) => { - if (!ctx.desktop.available()) { - return res.status(404).json({ error: "Only in the desktop app" }); - } - const body = (req.body ?? {}) as Record; - if (typeof body.host !== "string" || !body.host) { - return res.status(400).json({ error: "Missing host" }); - } - try { - const result = await ctx.desktop.launchExternalClient({ - protocol: "rdp", - host: body.host, - port: Number(body.port) || DEFAULT_PORT.rdp, - username: typeof body.username === "string" ? body.username : undefined, - domain: typeof body.domain === "string" ? body.domain : undefined, - }); - res.json(result); - } catch (error) { - res.json({ success: false, error: errorMessage(error) }); - } - }); -} diff --git a/plugins/remote-desktop/src/backend/sessions.ts b/plugins/remote-desktop/src/backend/sessions.ts deleted file mode 100644 index 083cc1e55..000000000 --- a/plugins/remote-desktop/src/backend/sessions.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { TermixGuacMeta } from "./token-service.js"; - -export interface RemoteSession { - guacamoleConnectionId: string; - termixConnectId: string; - hostId: number; - hostName: string; - ownerUserId: string; - protocol: TermixGuacMeta["protocol"]; - tabInstanceId: string | null; - openedAt: number; -} - -type Cleanup = () => void; - -/** How long a minted token's tunnel waits for guacd to connect through it. */ -export const PENDING_TIMEOUT_MS = 5 * 60 * 1000; - -/** - * Live guacd sessions and what each one holds open. - * - * A connect request builds its jump tunnel or VNC proxy before guacd dials - * in, so those resources are parked under the request's connect id and move - * onto the session when it opens. They are released when the session - * closes, when guacd never shows up, or when the plugin stops. - */ -export class RemoteSessions { - private readonly pending = new Map< - string, - { cleanups: Cleanup[]; timer: ReturnType } - >(); - private readonly byConnectId = new Map(); - private readonly byGuacamoleId = new Map(); - private readonly cleanupsByConnectId = new Map(); - - constructor(private readonly pendingTimeoutMs = PENDING_TIMEOUT_MS) {} - - /** Resources for a connection that guacd has not opened yet. */ - park(termixConnectId: string, cleanups: Cleanup[]): void { - if (cleanups.length === 0) return; - const timer = setTimeout(() => { - this.pending.delete(termixConnectId); - run(cleanups); - }, this.pendingTimeoutMs); - timer.unref?.(); - this.pending.set(termixConnectId, { cleanups, timer }); - } - - opened( - meta: TermixGuacMeta, - guacamoleConnectionId: string, - extra: Cleanup[] = [], - ): RemoteSession { - const session: RemoteSession = { - guacamoleConnectionId, - termixConnectId: meta.termixConnectId, - hostId: meta.hostId, - hostName: meta.hostName, - ownerUserId: meta.ownerUserId, - protocol: meta.protocol, - tabInstanceId: meta.tabInstanceId ?? null, - openedAt: Date.now(), - }; - const parked = this.pending.get(meta.termixConnectId); - if (parked) { - clearTimeout(parked.timer); - this.pending.delete(meta.termixConnectId); - } - this.cleanupsByConnectId.set(meta.termixConnectId, [ - ...(parked?.cleanups ?? []), - ...extra, - ]); - this.byConnectId.set(meta.termixConnectId, session); - this.byGuacamoleId.set(guacamoleConnectionId, session); - return session; - } - - closed(termixConnectId: string): void { - const session = this.byConnectId.get(termixConnectId); - if (session) this.byGuacamoleId.delete(session.guacamoleConnectionId); - this.byConnectId.delete(termixConnectId); - run(this.cleanupsByConnectId.get(termixConnectId) ?? []); - this.cleanupsByConnectId.delete(termixConnectId); - } - - byConnect(termixConnectId: string): RemoteSession | null { - return this.byConnectId.get(termixConnectId) ?? null; - } - - byGuacamole(guacamoleConnectionId: string): RemoteSession | null { - return this.byGuacamoleId.get(guacamoleConnectionId) ?? null; - } - - /** Releases everything, for deactivate. */ - clear(): void { - for (const { cleanups, timer } of this.pending.values()) { - clearTimeout(timer); - run(cleanups); - } - this.pending.clear(); - for (const cleanups of this.cleanupsByConnectId.values()) run(cleanups); - this.cleanupsByConnectId.clear(); - this.byConnectId.clear(); - this.byGuacamoleId.clear(); - } -} - -function run(cleanups: Cleanup[]): void { - for (const cleanup of cleanups) { - try { - cleanup(); - } catch { - // One failed close must not keep the rest open. - } - } -} diff --git a/plugins/remote-desktop/src/backend/token-service.ts b/plugins/remote-desktop/src/backend/token-service.ts deleted file mode 100644 index ea007cc5d..000000000 --- a/plugins/remote-desktop/src/backend/token-service.ts +++ /dev/null @@ -1,318 +0,0 @@ -import crypto from "crypto"; - -interface TokenLogger { - warn: (message: string, meta?: Record) => void; - error: (message: string, meta?: Record) => void; -} - -export interface GuacamoleConnectionSettings { - type?: "rdp" | "vnc" | "telnet"; - join?: string; - readOnly?: boolean; - guacdHost?: string; - guacdPort?: number; - settings: { - hostname?: string; - port?: number; - username?: string; - password?: string; - domain?: string; - width?: number; - height?: number; - dpi?: number; - security?: string; - "ignore-cert"?: boolean; - "disable-auth"?: boolean; - "enable-wallpaper"?: boolean; - "enable-drive"?: boolean; - "drive-path"?: string; - "create-drive-path"?: boolean; - "swap-red-blue"?: boolean; - cursor?: string; - "terminal-type"?: string; - [key: string]: unknown; - }; -} - -export interface TermixGuacMeta { - termixConnectId: string; - hostId: number; - hostName: string; - ownerUserId: string; - protocol: "rdp" | "vnc" | "telnet"; - tabInstanceId: string | null; -} - -export interface GuacamoleToken { - connection: GuacamoleConnectionSettings; - recording?: GuacamoleRecordingMetadata; - termixMeta?: TermixGuacMeta; - /** When the token stops opening displays, in ms since the epoch. */ - exp?: number; -} - -export interface GuacamoleRecordingMetadata { - hostId: number; - userId: string; - protocol: "rdp" | "vnc" | "telnet"; - path: string; - /** Directory guacd was told to write into; differs from the backend's view - * when guacd runs in its own container. */ - guacdPath?: string; - startedAt: string; -} - -const CIPHER = "aes-256-cbc"; -const KEY_LENGTH = 32; - -/** - * How long a token opens a display for. Short, because a token for a saved - * host carries its password and ends up in the display URL. - */ -export const TOKEN_TTL_MS = 5 * 60 * 1000; - -/** - * Settings the server decides, never the caller: where guacd is, and every - * path guacd writes or reads on its own filesystem. - */ -export function isServerOwnedSetting(key: string): boolean { - return ( - key.startsWith("guacd") || - key.endsWith("-path") || - key.startsWith("create-") || - key.startsWith("recording-") || - key.startsWith("typescript-") - ); -} - -const silentLogger: TokenLogger = { warn: () => {}, error: () => {} }; - -/** - * Encrypts the connection tokens guacamole-lite decrypts. Built in activate; - * the key is derived from the environment, so a restart keeps it. - */ -export class GuacamoleTokenService { - private encryptionKey: Buffer; - private macKey: Buffer; - - constructor( - private readonly log: TokenLogger = silentLogger, - private readonly now: () => number = Date.now, - ) { - this.encryptionKey = this.initializeKey(); - this.macKey = crypto - .createHmac("sha256", this.encryptionKey) - .update("termix-guacamole-token-mac") - .digest(); - } - - private initializeKey(): Buffer { - const existingKey = process.env.GUACAMOLE_ENCRYPTION_KEY; - if (existingKey) { - if (existingKey.length === 64 && /^[0-9a-fA-F]+$/.test(existingKey)) { - return Buffer.from(existingKey, "hex"); - } - if (existingKey.length === KEY_LENGTH) { - return Buffer.from(existingKey, "utf8"); - } - } - - // A token lives for seconds between issue and connect, so a fresh key - // per boot is enough and nothing is derived from core's own secrets. - return crypto.randomBytes(KEY_LENGTH); - } - - getEncryptionKey(): Buffer { - return this.encryptionKey; - } - - /** - * guacamole-lite only knows AES-CBC, which does not notice tampering, so - * the ciphertext also carries an HMAC that verifyToken checks before - * guacamole-lite ever sees the token. - */ - encryptToken(tokenObject: GuacamoleToken): string { - const iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv(CIPHER, this.encryptionKey, iv); - - let encrypted = cipher.update( - JSON.stringify({ ...tokenObject, exp: this.now() + TOKEN_TTL_MS }), - "utf8", - "base64", - ); - encrypted += cipher.final("base64"); - - const data = { - iv: iv.toString("base64"), - value: encrypted, - mac: this.mac(iv.toString("base64"), encrypted), - }; - - return Buffer.from(JSON.stringify(data)).toString("base64"); - } - - private mac(iv: string, value: string): string { - return crypto - .createHmac("sha256", this.macKey) - .update(`${iv}.${value}`) - .digest("base64"); - } - - /** - * The token if it is one this server minted, untouched and not expired, - * else null. The gate in front of guacamole-lite. - */ - verifyToken(token: string): GuacamoleToken | null { - let data: { iv?: unknown; value?: unknown; mac?: unknown }; - try { - data = JSON.parse(Buffer.from(token, "base64").toString("utf8")); - } catch { - return null; - } - if ( - typeof data?.iv !== "string" || - typeof data.value !== "string" || - typeof data.mac !== "string" - ) { - return null; - } - const expected = Buffer.from(this.mac(data.iv, data.value), "base64"); - const given = Buffer.from(data.mac, "base64"); - if ( - expected.length !== given.length || - !crypto.timingSafeEqual(expected, given) - ) { - return null; - } - const decoded = this.decryptToken(token); - if (!decoded || typeof decoded.exp !== "number") return null; - return decoded.exp > this.now() ? decoded : null; - } - - 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) { - this.log.error("Failed to decrypt guacamole token", { - operation: "guac_token_decrypt_error", - error: error instanceof Error ? error.message : String(error), - }); - return null; - } - } - - createRdpToken( - hostname: string, - username: string, - password: string, - options: Partial & { - guacdHost?: string; - guacdPort?: number; - } = {}, - recording?: GuacamoleRecordingMetadata, - termixMeta?: TermixGuacMeta, - ): string { - const { guacdHost, guacdPort, ...settingsOptions } = options; - const token: GuacamoleToken = { - connection: { - type: "rdp", - ...(guacdHost ? { guacdHost } : {}), - ...(guacdPort ? { guacdPort } : {}), - settings: { - hostname, - ...(username ? { username } : {}), - ...(password ? { password } : {}), - port: 3389, - "ignore-cert": true, - ...(!username && !password ? { "disable-auth": true } : {}), - ...settingsOptions, - }, - }, - recording, - termixMeta, - }; - return this.encryptToken(token); - } - - createVncToken( - hostname: string, - username?: string, - password?: string, - options: Partial & { - guacdHost?: string; - guacdPort?: number; - } = {}, - recording?: GuacamoleRecordingMetadata, - termixMeta?: TermixGuacMeta, - ): string { - const { guacdHost, guacdPort, ...settingsOptions } = options; - const token: GuacamoleToken = { - connection: { - type: "vnc", - ...(guacdHost ? { guacdHost } : {}), - ...(guacdPort ? { guacdPort } : {}), - settings: { - hostname, - ...(username ? { username } : {}), - password, - port: 5900, - ...settingsOptions, - }, - }, - recording, - termixMeta, - }; - return this.encryptToken(token); - } - - createTelnetToken( - hostname: string, - username?: string, - password?: string, - options: Partial & { - guacdHost?: string; - guacdPort?: number; - } = {}, - recording?: GuacamoleRecordingMetadata, - termixMeta?: TermixGuacMeta, - ): string { - const { guacdHost, guacdPort, ...settingsOptions } = options; - const token: GuacamoleToken = { - connection: { - type: "telnet", - ...(guacdHost ? { guacdHost } : {}), - ...(guacdPort ? { guacdPort } : {}), - settings: { - hostname, - username, - password, - port: 23, - ...settingsOptions, - }, - }, - recording, - termixMeta, - }; - return this.encryptToken(token); - } - - // join tokens never carry recording params - only the primary connection's - // token should write recording-path/recording-name to guacd. - createJoinToken(guacamoleConnectionId: string, readOnly: boolean): string { - const token: GuacamoleToken = { - connection: { - join: guacamoleConnectionId, - readOnly, - settings: {}, - }, - }; - return this.encryptToken(token); - } -} diff --git a/plugins/remote-desktop/src/frontend/GuacamoleApp.tsx b/plugins/remote-desktop/src/frontend/GuacamoleApp.tsx deleted file mode 100644 index cee8e5fab..000000000 --- a/plugins/remote-desktop/src/frontend/GuacamoleApp.tsx +++ /dev/null @@ -1,653 +0,0 @@ -import { watchGuacamoleConnectionId } from "./guacamole-session-id"; -import React, { - useState, - useEffect, - useRef, - useCallback, - useImperativeHandle, -} from "react"; -import type Guacamole from "guacamole-common-js"; -import { toast } from "sonner"; -import { - GuacamoleDisplay, - type GuacamoleDisplayHandle, - type GuacamoleTouchMode, -} from "./GuacamoleDisplay.tsx"; -import { getGuacamoleTokenFromHost, getGuacdStatus } from "./guacamole-api"; -import { readConfiguredDimension } from "./guacamole-display-size.ts"; -import { getGuacamoleToken } from "./guacamole-api"; -import { - useHost, - useTranslation, - useConnectionRetry, - logActivity, -} from "@termix/plugin-sdk/frontend"; -import { - Button, - ConnectionLogProvider, - ConnectionScreen, - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - Input, - PasswordInput, - isElectron, - linkedServerUrl, - resolveConnectionOrigin, - useConnectionLog, - type ConnectionOrigin, -} from "@termix/plugin-sdk/ui"; -import type { GuacamoleConfig } from "./guacamole-config"; -import { - errorMessage, - hostRemoteOptions, - type Protocol, - rdpDomain, - type RemoteHostLogin, -} from "./host-remote"; -import { GuacamoleToolbar } from "./GuacamoleToolbar.tsx"; -import { GuacamoleFileBrowser } from "./GuacamoleFileBrowser.tsx"; -import { describeUploadError } from "./guacamole-filesystem.ts"; -import { canUploadToRdpDrive } from "./guacamole-file-drop.ts"; -import { needsRdpCredentialPrompt } from "./rdp-credential-prompt"; - -interface GuacamoleAppProps { - hostId?: string; - tabId?: string; - protocol?: "rdp" | "vnc" | "telnet"; - isVisible?: boolean; - /** A quick-connect host: never saved, so the token is minted from its fields. */ - quickConnectHost?: GuacamoleQuickHost; -} - -/** - * What a session needs from its host. A saved host brings its options; a - * quick-connect host has no row, so it also carries its address and login. - */ -export interface GuacamoleHostConfig { - name?: string; - ip: string; - connectionType?: Protocol; - domain?: string; - guacamoleConfig?: GuacamoleConfig; - rdpAuthType?: string; - authOverrides?: RemoteHostLogin["authOverrides"]; - syncId?: string | null; - connectionOrigin?: "local" | "remote" | null; - showToolbar?: boolean; - port?: number; - username?: string; - password?: string; -} - -export type GuacamoleQuickHost = GuacamoleHostConfig; - -function savedHostConfig( - host: RemoteHostLogin, - protocol: Protocol | undefined, -): GuacamoleHostConfig { - const options = hostRemoteOptions(host); - return { - name: host.name ?? undefined, - ip: host.ip ?? "", - connectionType: - protocol ?? - (options.enableRdp ? "rdp" : options.enableVnc ? "vnc" : "telnet"), - domain: rdpDomain(host), - guacamoleConfig: options.guacamoleConfig, - rdpAuthType: host.protocolAuth?.rdp?.authType, - authOverrides: host.authOverrides, - syncId: host.syncId, - connectionOrigin: host.connectionOrigin, - showToolbar: options.enableToolbar, - }; -} - -export interface GuacamoleAppHandle { - disconnect: () => void; - isConnected: () => boolean; - /** The tab bar's refresh. The shell calls `refresh` on every tab switch. */ - reconnect: () => void; -} - -const GuacamoleApp = React.forwardRef( - function GuacamoleApp( - { hostId, tabId, protocol, isVisible = true, quickConnectHost }, - ref, - ) { - const { t } = useTranslation(); - const savedHost = useHost(quickConnectHost ? undefined : hostId); - const hostConfig: GuacamoleHostConfig | null = quickConnectHost - ? quickConnectHost - : savedHost - ? savedHostConfig(savedHost as RemoteHostLogin, protocol) - : null; - // The host list loads with the shell; a tab restored before it arrives - // waits for it instead of reporting the host missing. - const [waited, setWaited] = useState(false); - useEffect(() => { - const timer = setTimeout(() => setWaited(true), 3000); - return () => clearTimeout(timer); - }, []); - const loading = !hostConfig && !!hostId && !waited; - - if (loading) { - return ( -
- -
- ); - } - - if (!hostConfig || !hostId) { - return ( -
- -
- ); - } - - return ( - - - - ); - }, -); - -interface GuacamoleAppInnerProps { - hostId: number; - hostConfig: GuacamoleHostConfig; - hostName: string; - tabId?: string; - protocol?: "rdp" | "vnc" | "telnet"; - isVisible: boolean; -} - -const GuacamoleAppInner = React.forwardRef< - GuacamoleAppHandle, - GuacamoleAppInnerProps ->(function GuacamoleAppInner( - { hostId, hostConfig, hostName, tabId, protocol, isVisible }, - ref, -) { - const { t } = useTranslation(); - const { addLog, clearLogs } = useConnectionLog(); - const [token, setToken] = useState(null); - const [guacamoleConnectionId, setGuacamoleConnectionId] = useState< - string | null - >(null); - const [sessionLookup, setSessionLookup] = useState<{ - id: string; - origin: ConnectionOrigin; - } | null>(null); - const [error, setError] = useState(null); - const [connectionError, setConnectionError] = useState(null); - const [isDisplayReady, setIsDisplayReady] = useState(false); - const [touchMode, setTouchMode] = useState(() => - typeof window !== "undefined" && - (navigator.maxTouchPoints > 0 || "ontouchstart" in window) - ? "touchscreen" - : null, - ); - useEffect(() => { - if (!isDisplayReady || !sessionLookup) return; - return watchGuacamoleConnectionId( - sessionLookup.id, - sessionLookup.origin, - setGuacamoleConnectionId, - ); - }, [isDisplayReady, sessionLookup]); - const displayRef = useRef(null); - const [displayZoom, setDisplayZoom] = useState(1); - const [filesystem, setFilesystem] = useState(null); - const [fileBrowserOpen, setFileBrowserOpen] = useState(false); - const [toolbarHidden, setToolbarHidden] = useState(false); - const [pendingUploads, setPendingUploads] = useState([]); - - const guacConfig = hostConfig.guacamoleConfig ?? {}; - const allowUpload = guacConfig.disableUpload !== true; - const allowDownload = guacConfig.disableDownload !== true; - - // Prefer the browsable filesystem's current directory. guacd may expose the - // RDP drive only through the connection-level file stream, in which case the - // standard direct upload still lands in the redirected drive. - const handleDropFiles = useCallback( - (files: File[]) => { - if (filesystem) { - setPendingUploads(files); - setFileBrowserOpen(true); - return; - } - - void (async () => { - for (const file of files) { - try { - const display = displayRef.current; - if (!display) throw new Error("RDP session is not ready"); - await display.uploadFile(file); - toast.success( - t("remoteDesktop.files.uploaded", { name: file.name }), - ); - } catch (error) { - toast.error( - describeUploadError(error, (key) => - t(`remoteDesktop.files.${key}`, { name: file.name }), - ), - ); - } - } - })(); - }, - [filesystem, t], - ); - - const handleDropUnavailable = useCallback(() => { - toast.error( - t( - allowUpload - ? "remoteDesktop.files.driveUnavailable" - : "remoteDesktop.files.uploadDisabled", - ), - ); - }, [allowUpload, t]); - - const resolvedProtocolForConnect = (protocol ?? - hostConfig.connectionType ?? - "rdp") as "rdp" | "vnc" | "telnet"; - const needsCredentialPrompt = needsRdpCredentialPrompt({ - protocol: resolvedProtocolForConnect, - rdpAuthType: hostConfig.rdpAuthType, - authOverrides: hostConfig.authOverrides, - }); - - const [promptedCredentials, setPromptedCredentials] = useState<{ - username: string; - password: string; - domain: string; - } | null>(null); - const [promptOpen, setPromptOpen] = useState(needsCredentialPrompt); - const [promptUsername, setPromptUsername] = useState(""); - const [promptPassword, setPromptPassword] = useState(""); - const [promptDomain, setPromptDomain] = useState(hostConfig.domain ?? ""); - - // Assigned below, once handleReconnect exists; the shell's tab refresh calls it. - const reconnectRef = useRef<() => void>(() => {}); - - useImperativeHandle(ref, () => ({ - disconnect: () => displayRef.current?.disconnect(), - isConnected: () => displayRef.current?.isConnected() === true, - reconnect: () => reconnectRef.current(), - })); - - const fetchToken = useCallback(async (): Promise => { - setToken(null); - setIsDisplayReady(false); - setSessionLookup(null); - setGuacamoleConnectionId(null); - setError(null); - - // Outside Electron there is only one backend, so the origin is moot and - // the API layer ignores it; inside, it decides which backend mints the - // token and therefore has to match the one the session will run on. - let resolvedOrigin: ConnectionOrigin = "local"; - - if (isElectron()) { - resolvedOrigin = await resolveConnectionOrigin( - { connectionOrigin: hostConfig.connectionOrigin }, - { defaultRemote: true }, - ); - if (resolvedOrigin === "remote") { - if (!(await linkedServerUrl())) { - throw new Error(t("errors.remoteServerRequired")); - } - } - } - - addLog({ - type: "info", - stage: "guac_guacd", - message: t("remoteDesktop.checkingGuacd"), - }); - const status = await getGuacdStatus(resolvedOrigin, { - hostId, - protocol: resolvedProtocolForConnect, - syncId: hostConfig.syncId, - }); - if (status.enabled === false) { - throw new Error(t("remoteDesktop.disabled")); - } - if (status.guacd.status !== "connected") { - throw new Error(t("remoteDesktop.guacdUnavailable")); - } - - addLog({ - type: "info", - stage: "guac_token", - message: t("remoteDesktop.requestingToken", { - type: resolvedProtocolForConnect.toUpperCase(), - }), - }); - // hostId 0 is a quick-connect host: nothing to look up, mint the token - // straight from what the user typed. It cannot be shared or logged as - // host activity because there is no host row. - const result = - hostId === 0 - ? await getGuacamoleToken( - { - protocol: resolvedProtocolForConnect, - hostname: hostConfig.ip, - port: hostConfig.port, - username: hostConfig.username, - password: hostConfig.password, - domain: hostConfig.domain, - ignoreCert: true, - guacamoleConfig: hostConfig.guacamoleConfig, - }, - resolvedOrigin, - ) - : await getGuacamoleTokenFromHost( - hostId, - resolvedOrigin, - protocol, - promptedCredentials ?? undefined, - hostConfig.syncId, - tabId, - ); - if (result) { - setSessionLookup( - result.termixConnectId - ? { id: result.termixConnectId, origin: resolvedOrigin } - : null, - ); - setToken(result.token); - setGuacamoleConnectionId(result.guacamoleConnectionId ?? null); - if (hostId !== 0) { - logActivity(resolvedProtocolForConnect, hostId, hostName).catch( - () => {}, - ); - } - } - }, [ - hostId, - hostName, - protocol, - promptedCredentials, - resolvedProtocolForConnect, - hostConfig, - tabId, - addLog, - t, - ]); - - const tokenRetry = useConnectionRetry({ - connect: async () => { - try { - await fetchToken(); - tokenRetryRef.current.markConnected(); - } catch (err: unknown) { - const message = errorMessage(err, t("remoteDesktop.failedToConnect")); - setError(message || t("remoteDesktop.failedToConnect")); - addLog({ type: "error", stage: "error", message }); - tokenRetryRef.current.markFailed(); - } - }, - enabled: !needsCredentialPrompt || !!promptedCredentials, - autoStart: false, - }); - const tokenRetryRef = useRef(tokenRetry); - tokenRetryRef.current = tokenRetry; - - useEffect(() => { - if (needsCredentialPrompt && !promptedCredentials) { - setPromptOpen(true); - return; - } - clearLogs(); - tokenRetryRef.current.reset(); - tokenRetryRef.current.retryNow(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hostId, protocol, needsCredentialPrompt, promptedCredentials]); - - const handleReconnect = useCallback(() => { - setConnectionError(null); - setError(null); - setToken(null); - setIsDisplayReady(false); - if (needsCredentialPrompt) { - setPromptedCredentials(null); - setPromptUsername(""); - setPromptPassword(""); - setPromptDomain(hostConfig.domain ?? ""); - setPromptOpen(true); - return; - } - clearLogs(); - tokenRetryRef.current.reset(); - tokenRetryRef.current.retryNow(); - }, [needsCredentialPrompt, hostConfig.domain, clearLogs]); - - reconnectRef.current = handleReconnect; - - if (promptOpen) { - return ( - { - if (!open) setPromptOpen(false); - }} - > - - - - {t("remoteDesktop.credentialPromptTitle")} - - - {t("remoteDesktop.credentialPromptDescription")} - - -
{ - e.preventDefault(); - setPromptedCredentials({ - username: promptUsername, - password: promptPassword, - domain: promptDomain, - }); - setPromptOpen(false); - }} - > -
- - setPromptUsername(e.target.value)} - /> -
-
- - setPromptDomain(e.target.value)} - /> -
-
- - setPromptPassword(e.target.value)} - /> -
-
- -
-
-
-
- ); - } - - if (error || !token) { - return ( -
- -
- ); - } - - const resolvedProtocol = resolvedProtocolForConnect; - const configuredDpi = readConfiguredDimension(guacConfig.dpi); - const configuredWidth = readConfiguredDimension(guacConfig.width); - const configuredHeight = readConfiguredDimension(guacConfig.height); - - return ( -
- {(!isDisplayReady || connectionError) && ( - - )} - setIsDisplayReady(true)} - onError={(err) => { - setConnectionError(err); - addLog({ type: "error", stage: "error", message: err }); - }} - onStageChange={(stage) => { - const type = resolvedProtocol.toUpperCase(); - switch (stage) { - case "guac_connecting": - addLog({ - type: "info", - stage, - message: t("remoteDesktop.openingSession", { type }), - }); - break; - case "guac_handshake": - addLog({ - type: "info", - stage, - message: t("remoteDesktop.negotiating", { type }), - }); - break; - case "guac_ready": - addLog({ - type: "success", - stage, - message: t("remoteDesktop.sessionReady", { type }), - }); - break; - default: - break; - } - }} - onZoomChange={setDisplayZoom} - onFilesystem={setFilesystem} - onDropFiles={handleDropFiles} - onDropUnavailable={handleDropUnavailable} - /> - {filesystem && fileBrowserOpen && ( - setPendingUploads([])} - onClose={() => setFileBrowserOpen(false)} - /> - )} - {hostConfig.showToolbar !== false && !toolbarHidden && ( - setFileBrowserOpen((open) => !open)} - onTouchModeChange={setTouchMode} - zoom={displayZoom} - onHide={() => setToolbarHidden(true)} - slotContext={ - typeof hostId === "number" - ? { - hostId, - sessionId: guacamoleConnectionId, - protocol: resolvedProtocol, - tabInstanceId: tabId, - origin: - sessionLookup?.origin === "remote" ? "remote" : "local", - } - : undefined - } - /> - )} -
- ); -}); - -export default GuacamoleApp; diff --git a/plugins/remote-desktop/src/frontend/GuacamoleDisplay.tsx b/plugins/remote-desktop/src/frontend/GuacamoleDisplay.tsx deleted file mode 100644 index 65f9b0635..000000000 --- a/plugins/remote-desktop/src/frontend/GuacamoleDisplay.tsx +++ /dev/null @@ -1,924 +0,0 @@ -import type React from "react"; -import { - useEffect, - useRef, - useState, - useImperativeHandle, - forwardRef, - useCallback, -} from "react"; -import Guacamole from "guacamole-common-js"; -import { Upload } from "lucide-react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { getGuacamoleToken } from "./guacamole-api"; -import { buildGuacamoleWebSocketBaseUrl } from "./guacamole-websocket-url.ts"; -import { - buildOriginWsUrl, - getBasePath, - isElectron, - resolveConnectionOrigin, - type ConnectionOrigin, - type ConnectionStage, -} from "@termix/plugin-sdk/ui"; -import { errorMessage } from "./host-remote"; -import { guacStateToStage } from "./guacamole-state.ts"; -import { isPasteShortcut, pasteTextToRemote } from "./guacamole-clipboard.ts"; -import { getGuacamoleDisplaySize } from "./guacamole-display-size.ts"; -import { bindPointerInput } from "./guacamole-pointer.ts"; -import { - getFileDropDisposition, - hasDraggedFiles, -} from "./guacamole-file-drop.ts"; -import { - uploadFileToClient, - type GuacamoleFileStreamClient, -} from "./guacamole-filesystem.ts"; -import { clampGuacamoleZoom, stepGuacamoleZoom } from "./guacamole-zoom.ts"; - -export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet"; - -export interface GuacamoleConnectionConfig { - token?: string; - protocol?: GuacamoleConnectionType; - type?: GuacamoleConnectionType; - hostname?: string; - port?: number; - username?: string; - password?: string; - domain?: string; - width?: number; - height?: number; - dpi?: number; - /** - * Which backend serves this session. Supplied by the owning app so the - * websocket dials the same backend that minted the token; omitted by - * shared/collab views, which follow the default for the connection type. - */ - connectionOrigin?: ConnectionOrigin | null; - [key: string]: unknown; -} - -export interface GuacamoleDisplayHandle { - disconnect: () => void; - isConnected: () => boolean; - sendKey: (keysym: number, pressed: boolean) => void; - sendMouse: (x: number, y: number, buttonMask: number) => void; - setClipboard: (data: string) => void; - getFilesystem: () => Guacamole.Object | null; - uploadFile: (file: File) => Promise; - zoomIn: () => number; - zoomOut: () => number; - resetZoom: () => number; -} - -export type GuacamoleTouchMode = "touchscreen" | "touchpad"; - -interface GuacamoleDisplayProps { - connectionConfig: GuacamoleConnectionConfig; - isVisible: boolean; - touchMode?: GuacamoleTouchMode | null; - allowUpload?: boolean; - onConnect?: () => void; - onDisconnect?: () => void; - onError?: (error: string) => void; - onFilesystem?: (filesystem: Guacamole.Object | null) => void; - onDropFiles?: (files: File[]) => void; - onDropUnavailable?: () => void; - onStageChange?: (stage: ConnectionStage) => void; - onZoomChange?: (zoom: number) => void; -} - -export const GuacamoleDisplay = forwardRef< - GuacamoleDisplayHandle, - GuacamoleDisplayProps ->(function GuacamoleDisplay( - { - connectionConfig, - isVisible, - touchMode, - allowUpload = false, - onConnect, - onDisconnect, - onError, - onFilesystem, - onDropFiles, - onDropUnavailable, - onStageChange, - onZoomChange, - }, - ref, -) { - const { t } = useTranslation(); - // The host config pins the session resolution; without it the display follows - // the container. - const hasConfiguredSize = - connectionConfig.width != null && connectionConfig.height != null; - const containerRef = useRef(null); - const displayRef = useRef(null); - const displayElementRef = useRef(null); - const clientRef = useRef(null); - const filesystemRef = useRef(null); - // Held in a ref so tearing down the client can report the loss without - // rebuilding the connect callback whenever the parent re-renders. - const onFilesystemRef = useRef(onFilesystem); - onFilesystemRef.current = onFilesystem; - const onStageChangeRef = useRef(onStageChange); - onStageChangeRef.current = onStageChange; - const keyboardRef = useRef(null); - const unbindPointerRef = useRef<(() => void) | null>(null); - const scaleRef = useRef(1); - const fitScaleRef = useRef(1); - const zoomRef = useRef(1); - const resizeTimeoutRef = useRef(null); - const hasKeyboardFocusRef = useRef(false); - const windowFocusedRef = useRef( - typeof document === "undefined" ? true : document.hasFocus(), - ); - const hasInitiatedRef = useRef(false); - const isMountedRef = useRef(false); - const isConnectingRef = useRef(false); - const [isReady, setIsReady] = useState(false); - const [hasError, setHasError] = useState(false); - // RDP's guacd backend (FreeRDP) can report the Guacamole-level CONNECTED - // state from a single early sync before the actual RDP handshake with the - // remote host has finished negotiating - a handshake that can then still - // fail up to ~30s later. Waiting for a couple of real frame syncs before - // announcing the connection avoids a black screen with no connecting UI - // during that window; VNC/telnet fail before ever reaching CONNECTED, so - // they are unaffected. - const syncCountRef = useRef(0); - - const disconnectClient = useCallback(() => { - unbindPointerRef.current?.(); - unbindPointerRef.current = null; - const client = clientRef.current; - clientRef.current = null; - isConnectingRef.current = false; - if (filesystemRef.current) { - filesystemRef.current = null; - onFilesystemRef.current?.(null); - } - if (!client) return; - - try { - client.disconnect(); - } catch (error) { - console.warn("Failed to disconnect Guacamole client", error); - } - }, []); - - const applyZoom = useCallback( - (nextZoom: number): number => { - const zoom = clampGuacamoleZoom(nextZoom); - zoomRef.current = zoom; - const display = clientRef.current?.getDisplay(); - if (display && displayRef.current) { - const scale = fitScaleRef.current * zoom; - scaleRef.current = scale; - display.scale(scale); - displayRef.current.style.width = `${Math.floor(display.getWidth() * scale)}px`; - displayRef.current.style.height = `${Math.floor(display.getHeight() * scale)}px`; - } - onZoomChange?.(zoom); - return zoom; - }, - [onZoomChange], - ); - - useImperativeHandle(ref, () => ({ - disconnect: disconnectClient, - isConnected: () => isReady && !hasError, - 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(); - } - }, - getFilesystem: () => filesystemRef.current, - uploadFile: (file: File) => { - const client = clientRef.current; - if (!client) return Promise.reject(new Error("RDP session is not ready")); - return uploadFileToClient( - client as unknown as GuacamoleFileStreamClient, - file, - ); - }, - zoomIn: () => applyZoom(stepGuacamoleZoom(zoomRef.current, 1)), - zoomOut: () => applyZoom(stepGuacamoleZoom(zoomRef.current, -1)), - resetZoom: () => applyZoom(1), - })); - - const getWebSocketConnection = useCallback( - async ( - containerWidth: number, - containerHeight: number, - ): Promise<{ url: string; query: string } | null> => { - try { - let token: string; - const connectionProtocol = - connectionConfig.protocol ?? connectionConfig.type; - - // Resolved before the token is minted, not just before the socket is - // opened: the token has to come from whichever backend will serve the - // session, so both steps have to agree on the origin. - const origin = await resolveConnectionOrigin( - { connectionOrigin: connectionConfig.connectionOrigin }, - { defaultRemote: true }, - ); - - if (connectionConfig.token) { - token = connectionConfig.token; - } else { - const data = await getGuacamoleToken( - { - protocol: connectionProtocol ?? "rdp", - hostname: String(connectionConfig.hostname ?? ""), - port: connectionConfig.port, - username: connectionConfig.username, - password: connectionConfig.password, - domain: connectionConfig.domain, - security: - typeof connectionConfig.security === "string" - ? connectionConfig.security - : undefined, - ignoreCert: - typeof connectionConfig.ignoreCert === "boolean" - ? connectionConfig.ignoreCert - : undefined, - guacamoleConfig: connectionConfig.guacamoleConfig as Parameters< - typeof getGuacamoleToken - >[0]["guacamoleConfig"], - }, - origin, - ); - token = data.token; - } - - const displaySize = getGuacamoleDisplaySize( - connectionConfig.width ?? containerWidth ?? 1280, - connectionConfig.height ?? containerHeight ?? 720, - connectionProtocol, - window.devicePixelRatio, - connectionConfig.dpi, - ); - - let wsBase: string | null; - if (isElectron()) { - // includeJwt stays off: a display authenticates with the encrypted - // connection token in the query, not a session. - const target = await buildOriginWsUrl({ - origin, - localPort: 30001, - localPath: "/plugin-ws/remote-desktop/display", - remotePath: "/plugin-ws/remote-desktop/display", - includeJwt: false, - }); - if (!target) { - onError?.(t("errors.remoteServerRequired")); - return null; - } - wsBase = target.url; - } else { - wsBase = buildGuacamoleWebSocketBaseUrl({ - isElectronApp: false, - isEmbeddedApp: false, - basePath: getBasePath(), - location: window.location, - }); - } - - const params = new URLSearchParams({ - token, - width: String(displaySize.width), - height: String(displaySize.height), - }); - if (displaySize.dpi) params.set("dpi", String(displaySize.dpi)); - return { url: wsBase, query: params.toString() }; - } catch (error) { - const message = errorMessage(error); - onError?.(message); - return null; - } - }, - [connectionConfig, onError, t], - ); - - const refreshKeyboardHandlers = useCallback(() => { - const keyboard = keyboardRef.current; - const client = clientRef.current; - const displayElement = displayElementRef.current; - - if (!keyboard) return; - - const documentVisible = - typeof document === "undefined" || document.visibilityState === "visible"; - const displayIsFocused = - !!displayElement && - typeof document !== "undefined" && - document.activeElement === displayElement; - const shouldCaptureInput = - !!client && - !!displayElement && - isVisible && - documentVisible && - windowFocusedRef.current && - (hasKeyboardFocusRef.current || displayIsFocused); - - if (!shouldCaptureInput) { - keyboard.reset(); - keyboard.onkeydown = null; - keyboard.onkeyup = null; - return; - } - - keyboard.onkeydown = (keysym: number) => { - if (!clientRef.current) return; - if (!isVisible || !windowFocusedRef.current) return; - - const activeDisplay = displayElementRef.current; - const stillFocused = - !!activeDisplay && - typeof document !== "undefined" && - document.activeElement === activeDisplay; - - if (!hasKeyboardFocusRef.current && !stillFocused) return; - clientRef.current.sendKeyEvent(1, keysym); - }; - - keyboard.onkeyup = (keysym: number) => { - // Focus loss must still release keys already sent to the remote host. - clientRef.current?.sendKeyEvent(0, keysym); - }; - }, [isVisible]); - - const rescaleDisplay = useCallback( - (immediate: boolean = false) => { - if (!clientRef.current || !containerRef.current) return; - - const performRescale = () => { - if (!clientRef.current || !containerRef.current) return; - - 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 - ) { - fitScaleRef.current = Math.min( - cWidth / displayWidth, - cHeight / displayHeight, - ); - applyZoom(zoomRef.current); - } - }; - - if (immediate) { - performRescale(); - } else { - if (resizeTimeoutRef.current) { - clearTimeout(resizeTimeoutRef.current); - } - resizeTimeoutRef.current = setTimeout(performRescale, 200); - } - }, - [applyZoom], - ); - - const connect = useCallback(async () => { - if (isConnectingRef.current) return; - isConnectingRef.current = true; - setIsReady(false); - setHasError(false); - syncCountRef.current = 0; - - // Let layout settle before measuring without depending on animation frames, - // which may be throttled while Electron windows or tabs are inactive. - await new Promise((resolve) => setTimeout(resolve, 0)); - if (!isMountedRef.current) { - isConnectingRef.current = false; - return; - } - - // The tab's DOM node can still be display:none (and report 0x0) when this - // tab is restored in the background. Measuring then would force the - // window-size fallback, which ignores the tab bar and makes the remote - // resolution too tall (the bottom gets cut off). Poll briefly for a real - // size before connecting so we capture the actual visible viewport. - const measureContainer = () => { - const rect = containerRef.current?.getBoundingClientRect(); - return { width: rect?.width || 0, height: rect?.height || 0 }; - }; - - let { width: containerWidth, height: containerHeight } = measureContainer(); - for ( - let attempt = 0; - (containerWidth < 100 || containerHeight < 100) && attempt < 40; - attempt++ - ) { - await new Promise((resolve) => setTimeout(resolve, 25)); - if (!isMountedRef.current) { - isConnectingRef.current = false; - return; - } - ({ width: containerWidth, height: containerHeight } = measureContainer()); - } - - if (containerWidth < 100 || containerHeight < 100) { - containerWidth = window.innerWidth || 1280; - containerHeight = window.innerHeight || 720; - } - - const wsConnection = await getWebSocketConnection( - containerWidth, - containerHeight, - ); - if (!isMountedRef.current) { - isConnectingRef.current = false; - return; - } - if (!wsConnection) { - isConnectingRef.current = false; - return; - } - - const tunnel = new Guacamole.WebSocketTunnel(wsConnection.url); - const client = new Guacamole.Client(tunnel); - clientRef.current = client; - - const display = client.getDisplay(); - const displayElement = display.getElement(); - displayElementRef.current = displayElement; - - if (displayRef.current) { - displayRef.current.innerHTML = ""; - displayRef.current.appendChild(displayElement); - } - - displayElement.setAttribute("tabindex", "0"); - displayElement.style.outline = "none"; - - // Reading navigator.clipboard outside a user gesture is denied by Safari - // and commonly denied by Chromium. The paste event carries the text under - // the browser's normal permission model, so use it on every browser and - // replace the original shortcut with an ordered clipboard update + Ctrl+V. - displayElement.addEventListener( - "keydown", - (event) => { - if (isPasteShortcut(event)) { - event.stopImmediatePropagation(); - } - }, - true, - ); - displayElement.addEventListener( - "paste", - (event) => { - if (clientRef.current !== client) return; - const text = event.clipboardData?.getData("text/plain"); - if (!text) return; - - event.preventDefault(); - event.stopImmediatePropagation(); - pasteTextToRemote(client, text); - }, - true, - ); - - display.onresize = () => { - if (!isMountedRef.current || clientRef.current !== client) return; - rescaleDisplay(true); - setIsReady(true); - }; - - const protocol = connectionConfig.protocol ?? connectionConfig.type; - if (protocol === "telnet" && isMountedRef.current) { - setIsReady(true); - } - - const sendMouseState = (state: Guacamole.Mouse.State) => { - displayElement.focus({ preventScroll: true }); - const scale = scaleRef.current; - const adjustedState = new Guacamole.Mouse.State( - Math.round(state.x / scale), - Math.round(state.y / scale), - state.left, - state.middle, - state.right, - state.up, - state.down, - ) as Guacamole.Mouse.State; - client.sendMouseState(adjustedState); - }; - - unbindPointerRef.current?.(); - unbindPointerRef.current = bindPointerInput( - displayElement, - touchMode, - sendMouseState, - ); - - const keyboard = new Guacamole.Keyboard(displayElement); - keyboardRef.current = keyboard; - - const handleDisplayFocus = () => { - hasKeyboardFocusRef.current = true; - refreshKeyboardHandlers(); - }; - - const handleDisplayBlur = () => { - hasKeyboardFocusRef.current = false; - refreshKeyboardHandlers(); - }; - - displayElement.addEventListener("focus", handleDisplayFocus); - displayElement.addEventListener("blur", handleDisplayBlur); - displayElement.addEventListener("mousedown", handleDisplayFocus); - displayElement.addEventListener("touchstart", handleDisplayFocus, { - passive: true, - }); - refreshKeyboardHandlers(); - - client.onstatechange = (state: number) => { - if (!isMountedRef.current || clientRef.current !== client) return; - onStageChangeRef.current?.(guacStateToStage(state)); - switch (state) { - case 0: - break; - case 1: - break; - case 2: - break; - case 3: - isConnectingRef.current = false; - if (protocol !== "rdp") { - setIsReady(true); - onConnect?.(); - } - // A configured resolution is the size the session should render at; - // resizing it to the container would discard it. rescaleDisplay still - // fits that fixed display into whatever space is available. - if (!hasConfiguredSize && containerRef.current) { - const rect = containerRef.current.getBoundingClientRect(); - const size = getGuacamoleDisplaySize( - rect.width, - rect.height, - protocol, - window.devicePixelRatio, - connectionConfig.dpi, - ); - client.sendSize(size.width, size.height); - } - rescaleDisplay(false); - break; - case 4: - break; - case 5: - isConnectingRef.current = false; - setIsReady(false); - setHasError(true); - hasKeyboardFocusRef.current = false; - refreshKeyboardHandlers(); - onError?.(t("remoteDesktop.connectionError")); - onDisconnect?.(); - break; - } - }; - - client.onerror = (error: Guacamole.Status) => { - if (!isMountedRef.current || clientRef.current !== client) return; - const errorMessage = error.message || t("remoteDesktop.connectionError"); - setIsReady(false); - setHasError(true); - isConnectingRef.current = false; - onError?.(errorMessage); - }; - - if (protocol === "rdp") { - client.onsync = () => { - if (!isMountedRef.current || clientRef.current !== client) return; - if (syncCountRef.current >= 2) return; - syncCountRef.current += 1; - if (syncCountRef.current >= 2) { - setIsReady(true); - onConnect?.(); - } - }; - } - - 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(() => {}); - }; - } - }; - - client.onaudio = (stream: Guacamole.InputStream, mimetype: string) => { - Guacamole.AudioPlayer.getInstance(stream, mimetype); - }; - - // Only fires when the connection enables drive redirection; guacd exposes - // the redirected drive as a single filesystem object. - client.onfilesystem = (filesystem: Guacamole.Object) => { - if (!isMountedRef.current || clientRef.current !== client) return; - filesystemRef.current = filesystem; - onFilesystemRef.current?.(filesystem); - - filesystem.onundefine = () => { - if (filesystemRef.current !== filesystem) return; - filesystemRef.current = null; - onFilesystemRef.current?.(null); - }; - }; - - client.onfile = ( - stream: Guacamole.InputStream, - mimetype: string, - filename: string, - ) => { - const reader = new Guacamole.BlobReader(stream, mimetype); - reader.onend = () => { - const blob = reader.getBlob(); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); - }; - stream.sendAck("OK", Guacamole.Status.Code.SUCCESS); - }; - - try { - client.connect(wsConnection.query); - } catch (error) { - isConnectingRef.current = false; - if (!isMountedRef.current) return; - setIsReady(false); - setHasError(true); - onError?.(errorMessage(error, t("remoteDesktop.connectionError"))); - } - }, [ - getWebSocketConnection, - onConnect, - onDisconnect, - onError, - refreshKeyboardHandlers, - rescaleDisplay, - connectionConfig.protocol, - connectionConfig.type, - connectionConfig.dpi, - hasConfiguredSize, - touchMode, - t, - ]); - - useEffect(() => { - isMountedRef.current = true; - - if (isVisible && !hasInitiatedRef.current) { - hasInitiatedRef.current = true; - connect(); - } - }, [isVisible, connect]); - - useEffect(() => { - if (!isVisible) { - hasKeyboardFocusRef.current = false; - } - - refreshKeyboardHandlers(); - }, [isVisible, refreshKeyboardHandlers]); - - useEffect(() => { - const handleWindowFocus = () => { - windowFocusedRef.current = true; - refreshKeyboardHandlers(); - }; - - const handleWindowBlur = () => { - windowFocusedRef.current = false; - hasKeyboardFocusRef.current = false; - refreshKeyboardHandlers(); - }; - - const handleVisibilityChange = () => { - windowFocusedRef.current = - document.visibilityState === "visible" && document.hasFocus(); - if (document.visibilityState !== "visible") { - hasKeyboardFocusRef.current = false; - } - refreshKeyboardHandlers(); - }; - - window.addEventListener("focus", handleWindowFocus); - window.addEventListener("blur", handleWindowBlur); - document.addEventListener("visibilitychange", handleVisibilityChange); - - return () => { - window.removeEventListener("focus", handleWindowFocus); - window.removeEventListener("blur", handleWindowBlur); - document.removeEventListener("visibilitychange", handleVisibilityChange); - }; - }, [refreshKeyboardHandlers]); - - useEffect(() => { - return () => { - isMountedRef.current = false; - hasInitiatedRef.current = false; - isConnectingRef.current = false; - if (resizeTimeoutRef.current) { - clearTimeout(resizeTimeoutRef.current); - } - disconnectClient(); - displayElementRef.current = null; - }; - }, [disconnectClient]); - - useEffect(() => { - if (!containerRef.current) return; - - const resizeObserver = new ResizeObserver(() => { - if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current); - resizeTimeoutRef.current = setTimeout(() => { - if (clientRef.current && containerRef.current) { - const rect = containerRef.current.getBoundingClientRect(); - if (rect.width > 0 && rect.height > 0) { - if (!hasConfiguredSize) { - const size = getGuacamoleDisplaySize( - rect.width, - rect.height, - connectionConfig.protocol ?? connectionConfig.type, - window.devicePixelRatio, - connectionConfig.dpi, - ); - clientRef.current.sendSize(size.width, size.height); - } - rescaleDisplay(true); - } - } - }, 150); - }); - - resizeObserver.observe(containerRef.current); - - return () => { - resizeObserver.disconnect(); - }; - }, [ - connectionConfig.dpi, - connectionConfig.protocol, - connectionConfig.type, - hasConfiguredSize, - rescaleDisplay, - ]); - - useEffect(() => { - const container = containerRef.current; - const protocol = connectionConfig.protocol ?? connectionConfig.type; - if (!container || !isReady || protocol !== "vnc") return; - - let pinchDistance = 0; - let pinchZoom = zoomRef.current; - const distance = (touches: TouchList) => - Math.hypot( - touches[0].clientX - touches[1].clientX, - touches[0].clientY - touches[1].clientY, - ); - const onTouchStart = (event: TouchEvent) => { - if (event.touches.length !== 2) return; - event.preventDefault(); - event.stopPropagation(); - pinchDistance = distance(event.touches); - pinchZoom = zoomRef.current; - }; - const onTouchMove = (event: TouchEvent) => { - if (event.touches.length !== 2 || pinchDistance === 0) return; - event.preventDefault(); - event.stopPropagation(); - applyZoom(pinchZoom * (distance(event.touches) / pinchDistance)); - }; - const onTouchEnd = () => { - pinchDistance = 0; - }; - - container.addEventListener("touchstart", onTouchStart, { - passive: false, - capture: true, - }); - container.addEventListener("touchmove", onTouchMove, { - passive: false, - capture: true, - }); - container.addEventListener("touchend", onTouchEnd, { capture: true }); - return () => { - container.removeEventListener("touchstart", onTouchStart, true); - container.removeEventListener("touchmove", onTouchMove, true); - container.removeEventListener("touchend", onTouchEnd, true); - }; - }, [applyZoom, connectionConfig.protocol, connectionConfig.type, isReady]); - - const canDropFiles = allowUpload && onDropFiles != null; - const [isDraggingFiles, setIsDraggingFiles] = useState(false); - // Nested elements fire dragleave as the pointer crosses them, so track depth - // rather than clearing the highlight on the first leave. - const dragDepthRef = useRef(0); - - const handleDragEnter = useCallback( - (event: React.DragEvent) => { - if (!hasDraggedFiles(event.dataTransfer.types)) return; - event.preventDefault(); - if (!canDropFiles) return; - dragDepthRef.current += 1; - setIsDraggingFiles(true); - }, - [canDropFiles], - ); - - const handleDragLeave = useCallback(() => { - dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); - if (dragDepthRef.current === 0) setIsDraggingFiles(false); - }, []); - - const handleDrop = useCallback( - (event: React.DragEvent) => { - if (!hasDraggedFiles(event.dataTransfer.types)) return; - event.preventDefault(); - dragDepthRef.current = 0; - setIsDraggingFiles(false); - - const files = Array.from(event.dataTransfer.files); - const disposition = getFileDropDisposition( - event.dataTransfer.types, - files.length, - canDropFiles, - ); - if (disposition === "upload") onDropFiles?.(files); - if (disposition === "reject") onDropUnavailable?.(); - }, - [canDropFiles, onDropFiles, onDropUnavailable], - ); - - return ( -
{ - if (hasDraggedFiles(event.dataTransfer.types)) event.preventDefault(); - }} - onDragLeave={canDropFiles ? handleDragLeave : undefined} - onDrop={handleDrop} - > -
- - {isDraggingFiles && ( -
-
- - {t("remoteDesktop.files.dropToUpload")} -
-
- )} -
- ); -}); diff --git a/plugins/remote-desktop/src/frontend/GuacamoleFileBrowser.tsx b/plugins/remote-desktop/src/frontend/GuacamoleFileBrowser.tsx deleted file mode 100644 index 9737caa92..000000000 --- a/plugins/remote-desktop/src/frontend/GuacamoleFileBrowser.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { errorMessage } from "./host-remote"; -import { useCallback, useEffect, useRef, useState } from "react"; -import Guacamole from "guacamole-common-js"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { - ArrowUp, - Download, - File as FileIcon, - Folder, - RefreshCw, - Upload, - X, -} from "lucide-react"; -import { toast } from "sonner"; -import { Button } from "@termix/plugin-sdk/ui"; -import { - downloadFile, - listDirectory, - parentPath, - saveBlobAs, - uploadFile, - describeUploadError, - type RemoteFileEntry, -} from "./guacamole-filesystem.ts"; - -interface GuacamoleFileBrowserProps { - filesystem: Guacamole.Object; - allowUpload: boolean; - allowDownload: boolean; - pendingUploads: File[]; - onPendingUploadsHandled: () => void; - onClose: () => void; -} - -export function GuacamoleFileBrowser({ - filesystem, - allowUpload, - allowDownload, - pendingUploads, - onPendingUploadsHandled, - onClose, -}: GuacamoleFileBrowserProps) { - const { t } = useTranslation(); - const [path, setPath] = useState("/"); - const [entries, setEntries] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [busyName, setBusyName] = useState(null); - const fileInputRef = useRef(null); - - const refresh = useCallback( - async (target: string) => { - setLoading(true); - setError(null); - try { - setEntries(await listDirectory(filesystem, target)); - } catch (err) { - setEntries([]); - setError(errorMessage(err, t("remoteDesktop.files.listFailed"))); - } finally { - setLoading(false); - } - }, - [filesystem, t], - ); - - useEffect(() => { - void refresh(path); - }, [refresh, path]); - - const upload = useCallback( - async (files: File[]) => { - for (const file of files) { - setBusyName(file.name); - try { - await uploadFile(filesystem, path, file); - toast.success(t("remoteDesktop.files.uploaded", { name: file.name })); - } catch (err) { - toast.error( - describeUploadError(err, (key) => - t(`remoteDesktop.files.${key}`, { name: file.name }), - ), - ); - } finally { - setBusyName(null); - } - } - void refresh(path); - }, - [filesystem, path, refresh, t], - ); - - // Files dropped on the display land here so they share the browser's current - // directory rather than always going to the drive root. - useEffect(() => { - if (pendingUploads.length === 0) return; - onPendingUploadsHandled(); - if (!allowUpload) { - toast.error(t("remoteDesktop.files.uploadDisabled")); - return; - } - void upload(pendingUploads); - }, [pendingUploads, onPendingUploadsHandled, allowUpload, upload, t]); - - const download = useCallback( - async (entry: RemoteFileEntry) => { - setBusyName(entry.name); - try { - const { blob, filename } = await downloadFile(filesystem, entry.path); - saveBlobAs(blob, filename); - } catch (err) { - toast.error( - err instanceof Error - ? err.message - : t("remoteDesktop.files.downloadFailed", { name: entry.name }), - ); - } finally { - setBusyName(null); - } - }, - [filesystem, t], - ); - - return ( -
-
- - {t("remoteDesktop.files.title")} - - -
- -
- - - {path} - - -
- -
- {loading && ( -

- {t("common.loading")} -

- )} - {!loading && error && ( -

{error}

- )} - {!loading && !error && entries.length === 0 && ( -

- {t("remoteDesktop.files.empty")} -

- )} - {!loading && - !error && - entries.map((entry) => ( -
- {entry.isDirectory ? ( - - ) : ( - - )} - - {!entry.isDirectory && allowDownload && ( - - )} -
- ))} -
- - {allowUpload && ( -
- { - const files = Array.from(e.target.files ?? []); - e.target.value = ""; - if (files.length > 0) void upload(files); - }} - /> - -
- )} -
- ); -} diff --git a/plugins/remote-desktop/src/frontend/GuacamoleToolbar.tsx b/plugins/remote-desktop/src/frontend/GuacamoleToolbar.tsx deleted file mode 100644 index c66bb05f4..000000000 --- a/plugins/remote-desktop/src/frontend/GuacamoleToolbar.tsx +++ /dev/null @@ -1,633 +0,0 @@ -import React, { - useState, - useRef, - useCallback, - useLayoutEffect, - useEffect, -} from "react"; -import { - GripVertical, - Monitor, - ChevronLeft, - ChevronRight, - ChevronUp, - ChevronDown, - ChevronsLeftRight, - FolderOpen, - Touchpad, - MousePointer, - ZoomIn, - ZoomOut, - Scan, - X, -} from "lucide-react"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@termix/plugin-sdk/ui"; -import type { - GuacamoleDisplayHandle, - GuacamoleTouchMode, -} from "./GuacamoleDisplay.tsx"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { ActionSlot } from "@termix/plugin-sdk/ui"; -import { cn } from "@termix/plugin-sdk/ui"; -import { - detectRuntimeMetaKeyFamily, - metaKeyLabels, - type MetaKeyFamily, -} from "./guacamole-meta-key.ts"; - -interface GuacamoleToolbarProps { - displayRef: React.RefObject; - protocol: "rdp" | "vnc" | "telnet"; - touchMode?: GuacamoleTouchMode | null; - hasFilesystem?: boolean; - fileBrowserOpen?: boolean; - onToggleFileBrowser?: () => void; - onTouchModeChange?: (mode: GuacamoleTouchMode) => void; - zoom?: number; - onHide?: () => void; - metaKeyFamily?: MetaKeyFamily; - /** Handed to "remote-desktop.toolbar" actions (the share button). */ - slotContext?: RemoteDesktopToolbarContext; -} - -export const REMOTE_DESKTOP_TOOLBAR_SLOT = "remote-desktop.toolbar"; - -/** What a "remote-desktop.toolbar" action is invoked with. */ -export interface RemoteDesktopToolbarContext { - hostId: number; - /** guacd's connection id, once the session is up. */ - sessionId: string | null; - protocol: "rdp" | "vnc" | "telnet"; - tabInstanceId?: string; - /** The backend the session runs on. */ - origin?: "local" | "remote"; -} - -const MODIFIER_KEYSYMS = { - ctrl: 0xffe3, - alt: 0xffe9, - shift: 0xffe1, - win: 0xffeb, -} as const; - -const FKEY_KEYSYMS = Array.from({ length: 12 }, (_, i) => 0xffbe + i); - -const BTN_BASE = - "flex items-center justify-center gap-1 h-7 px-2 text-[10px] font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors rounded-sm whitespace-nowrap select-none"; - -const BTN_ICON = - "flex items-center justify-center size-7 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors rounded-sm select-none"; - -const SEP = "w-px h-5 bg-border mx-0.5 shrink-0"; - -function TipBtn({ - tooltip, - onClick, - className, - children, -}: { - tooltip: string; - onClick: () => void; - className?: string; - children: React.ReactNode; -}) { - return ( - - - - - - {tooltip} - - - ); -} - -function TipIconBtn({ - tooltip, - onClick, - className, - children, -}: { - tooltip: string; - onClick: () => void; - className?: string; - children: React.ReactNode; -}) { - return ( - - - - - - {tooltip} - - - ); -} - -export const GuacamoleToolbar: React.FC = ({ - displayRef, - protocol, - touchMode, - hasFilesystem = false, - fileBrowserOpen = false, - onToggleFileBrowser, - onTouchModeChange, - zoom = 1, - onHide, - metaKeyFamily, - slotContext, -}) => { - const { t } = useTranslation(); - const meta = metaKeyLabels( - metaKeyFamily ?? detectRuntimeMetaKeyFamily(protocol), - ); - const [position, setPosition] = useState({ x: 0, y: 12 }); - const [collapsed, setCollapsed] = useState(false); - const [showFKeys, setShowFKeys] = useState(false); - const [stickyKeys, setStickyKeys] = useState>({ - [MODIFIER_KEYSYMS.ctrl]: false, - [MODIFIER_KEYSYMS.alt]: false, - [MODIFIER_KEYSYMS.shift]: false, - [MODIFIER_KEYSYMS.win]: false, - }); - - const toolbarRef = useRef(null); - const isDraggingRef = useRef(false); - const dragOriginRef = useRef({ mouseX: 0, mouseY: 0, posX: 0, posY: 0 }); - const [isDragging, setIsDragging] = useState(false); - - useLayoutEffect(() => { - const el = toolbarRef.current; - if (!el) return; - const parent = el.offsetParent as HTMLElement | null; - if (!parent) return; - const parentW = parent.clientWidth; - const toolbarW = el.offsetWidth; - setPosition((p) => ({ ...p, x: Math.max(0, (parentW - toolbarW) / 2) })); - }, [collapsed]); - - useEffect(() => { - if (!isDragging) return; - - const onMove = (e: MouseEvent) => { - if (!isDraggingRef.current) return; - const parent = toolbarRef.current?.offsetParent as HTMLElement | null; - const parentW = parent?.clientWidth ?? Infinity; - const parentH = parent?.clientHeight ?? Infinity; - const toolbarW = toolbarRef.current?.offsetWidth ?? 0; - const toolbarH = toolbarRef.current?.offsetHeight ?? 0; - - const dx = e.clientX - dragOriginRef.current.mouseX; - const dy = e.clientY - dragOriginRef.current.mouseY; - setPosition({ - x: Math.max( - 0, - Math.min(dragOriginRef.current.posX + dx, parentW - toolbarW), - ), - y: Math.max( - 0, - Math.min(dragOriginRef.current.posY + dy, parentH - toolbarH), - ), - }); - }; - - const onUp = () => { - isDraggingRef.current = false; - setIsDragging(false); - document.body.style.userSelect = ""; - }; - - document.addEventListener("mousemove", onMove); - document.addEventListener("mouseup", onUp); - return () => { - document.removeEventListener("mousemove", onMove); - document.removeEventListener("mouseup", onUp); - }; - }, [isDragging]); - - const startDrag = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - isDraggingRef.current = true; - dragOriginRef.current = { - mouseX: e.clientX, - mouseY: e.clientY, - posX: position.x, - posY: position.y, - }; - document.body.style.userSelect = "none"; - setIsDragging(true); - }, - [position], - ); - - const releaseStickyKeys = useCallback(() => { - const display = displayRef.current; - if (!display) return; - setStickyKeys((prev) => { - const next = { ...prev }; - for (const [ksStr, active] of Object.entries(prev)) { - if (active) { - display.sendKey(Number(ksStr), false); - next[Number(ksStr)] = false; - } - } - return next; - }); - }, [displayRef]); - - const sendCombo = useCallback( - (...keysyms: number[]) => { - const display = displayRef.current; - if (!display) return; - for (const k of keysyms) display.sendKey(k, true); - for (const k of [...keysyms].reverse()) display.sendKey(k, false); - releaseStickyKeys(); - }, - [displayRef, releaseStickyKeys], - ); - - const toggleStickyKey = useCallback( - (keysym: number) => { - const display = displayRef.current; - if (!display) return; - setStickyKeys((prev) => { - const isActive = prev[keysym]; - display.sendKey(keysym, !isActive); - return { ...prev, [keysym]: !isActive }; - }); - }, - [displayRef], - ); - - const isRdpVnc = protocol === "rdp" || protocol === "vnc"; - - const containerStyle: React.CSSProperties = { - position: "absolute", - left: position.x, - top: position.y, - zIndex: 20, - }; - - return ( - -
e.preventDefault()} - > - {collapsed ? ( - - -
- -
- -
- - - {t("remoteDesktop.toolbar.expand")} - - - ) : ( -
- {/* Drag handle */} - - - - - - {t("remoteDesktop.toolbar.dragHandle")} - - - - {/* Touch mode toggle — touch devices only */} - {touchMode != null && onTouchModeChange && ( - <> -
- - - - - - {touchMode === "touchscreen" - ? t("remoteDesktop.toolbar.switchToTrackpad") - : t("remoteDesktop.toolbar.switchToTouch")} - - - - )} - - {/* Drive files — only once guacd reports a redirected filesystem */} - {hasFilesystem && onToggleFileBrowser && ( - <> -
- - - - - )} - - {isRdpVnc && ( - <> -
- displayRef.current?.zoomOut()} - > - - - displayRef.current?.resetZoom()} - className="min-w-12 tabular-nums" - > - {Math.round(zoom * 100)}% - - displayRef.current?.zoomIn()} - > - - - displayRef.current?.resetZoom()} - > - - - - )} - - {/* System combos — RDP/VNC only */} - {isRdpVnc && ( - <> -
- sendCombo(0xffe3, 0xffe9, 0xffff)} - > - CAD - - sendCombo(MODIFIER_KEYSYMS.win, 0x006c)} - > - {meta.lock} - - sendCombo(MODIFIER_KEYSYMS.win)} - > - {meta.short} - - - )} - - {/* Sticky modifiers — RDP/VNC only */} - {isRdpVnc && ( - <> -
- {( - [ - [ - "ctrl", - MODIFIER_KEYSYMS.ctrl, - t("remoteDesktop.toolbar.ctrl"), - ], - [ - "alt", - MODIFIER_KEYSYMS.alt, - t("remoteDesktop.toolbar.alt"), - ], - [ - "shift", - MODIFIER_KEYSYMS.shift, - t("remoteDesktop.toolbar.shift"), - ], - ["win", MODIFIER_KEYSYMS.win, t(meta.stickyKey)], - ] as [string, number, string][] - ).map(([key, ks, label]) => ( - - - - - - {stickyKeys[ks] - ? t("remoteDesktop.toolbar.stickyActive", { - key: label, - }) - : t("remoteDesktop.toolbar.stickyInactive", { - key: label, - })} - - - ))} - - )} - - {/* Function key toggle */} -
- setShowFKeys((v) => !v)} - className={cn( - showFKeys && - "bg-primary/15 text-primary border border-primary/30", - )} - > - Fn - - - {/* F1-F12 row */} - {showFKeys && - FKEY_KEYSYMS.map((ks, i) => ( - sendCombo(ks)} - > - F{i + 1} - - ))} - - {/* Navigation */} -
- sendCombo(0xff1b)} - > - Esc - - sendCombo(0xff09)} - > - Tab - - sendCombo(0xff50)} - > - Home - - sendCombo(0xff57)} - > - End - - sendCombo(0xff55)} - > - PgUp - - sendCombo(0xff56)} - > - PgDn - - - {/* Arrow cluster */} -
-
- sendCombo(0xff52)} - > - - -
-
- sendCombo(0xff51)} - > - - - sendCombo(0xff54)} - > - - - sendCombo(0xff53)} - > - - -
-
- - {slotContext && ( - [slotContext]} - /> - )} - - {/* Collapse */} -
- - - - - - {t("remoteDesktop.toolbar.collapse")} - - - {onHide && ( - - - - )} -
- )} -
- - ); -}; diff --git a/plugins/remote-desktop/src/frontend/HostEditorGuacamoleTabs.tsx b/plugins/remote-desktop/src/frontend/HostEditorGuacamoleTabs.tsx deleted file mode 100644 index b4d97f493..000000000 --- a/plugins/remote-desktop/src/frontend/HostEditorGuacamoleTabs.tsx +++ /dev/null @@ -1,1743 +0,0 @@ -import { - HOST_PROTOCOL_SECRET_KEPT, - useTranslation, -} from "@termix/plugin-sdk/frontend"; -import { - FakeSwitch, - Input, - PasswordInput, - SectionCard, - Select2, - SettingRow, -} from "@termix/plugin-sdk/ui"; -import { - Activity, - Copy, - Globe, - Monitor, - Network, - Server, - Settings, - Shield, - Terminal, - Zap, - Cpu, -} from "lucide-react"; -import type { RemoteDesktopForm, RemoteFormSetField } from "./remote-form"; - -type HostEditorForm = RemoteDesktopForm; -type HostEditorSetField = RemoteFormSetField; - -type GuacFieldSetter = (key: string, value: unknown) => void; - -function DocsLinkAction({ href, label }: { href: string; label: string }) { - return ( - - {label} - - ); -} - -export function HostEditorRdpTab({ - form, - setField, - setGuacField, - host, - credentials, -}: { - form: HostEditorForm; - setField: HostEditorSetField; - setGuacField: GuacFieldSetter; - host?: { macAddress?: string | null } | null; - credentials?: { id: string; name: string; username: string }[]; -}) { - const { t } = useTranslation(); - - return ( - <> - } - action={ - - } - > -
-
- - setField("rdpPort", Number(e.target.value))} - className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> -
-
-
- } - > -
-
- - setGuacField("guacd-hostname", e.target.value)} - /> -
-
- - setGuacField("guacd-port", e.target.value)} - className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> -
-

- {t("hosts.guac.guacdProxyDesc")} -

-
-
- - } - > -
-
- -
- {( - [ - "direct", - ...(credentials && credentials.length > 0 - ? (["credential"] as const) - : []), - "none", - ] as const - ).map((m) => ( - - ))} -
-
- {form.rdpAuthType === "none" ? ( -

- {t("hosts.guac.authTypeNoneDesc")} -

- ) : ( -
- {form.rdpAuthType === "credential" && - credentials && - credentials.length > 0 ? ( -
- - - setField("rdpCredentialId", e.target.value) - } - className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - - {credentials.map((c) => ( - - ))} - -
- ) : ( - <> -
- - setField("rdpUser", e.target.value)} - /> -
-
- - { - if (form.rdpPassword === HOST_PROTOCOL_SECRET_KEPT) - setField("rdpPassword", ""); - }} - onChange={(e) => setField("rdpPassword", e.target.value)} - /> -
- - )} -
- - setField("domain", e.target.value)} - /> -
-
- )} -
-
- - } - > -
-
- - setField("security", e.target.value)} - > - - - - - - - -
- - setField("ignoreCert", v)} - /> - -
- - - setGuacField("load-balance-info", e.target.value) - } - /> -

- {t("hosts.guac.loadBalanceInfoDesc")} -

-
-
-
- - } - > -
-
- - setGuacField("color-depth", e.target.value)} - > - - - - - - -
-
-
- - setGuacField("width", e.target.value)} - className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> -
-
- - setGuacField("height", e.target.value)} - className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> -
-
-
- - setGuacField("dpi", e.target.value)} - className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> -
-
- - setGuacField("resize-method", e.target.value)} - > - - - - -
- - setGuacField("force-lossless", v)} - /> - -
-
- - } - > -
- - setGuacField("disable-audio", v)} - /> - - - setGuacField("enable-audio-input", v)} - /> - -
-
- - } - > -
- - setGuacField("enable-wallpaper", v)} - /> - - - setGuacField("enable-theming", v)} - /> - - - setGuacField("enable-font-smoothing", v)} - /> - - - setGuacField("enable-full-window-drag", v)} - /> - - - setGuacField("enable-desktop-composition", v)} - /> - - - setGuacField("enable-menu-animations", v)} - /> - - - setGuacField("disable-bitmap-caching", v)} - /> - - - setGuacField("disable-offscreen-caching", v)} - /> - - - setGuacField("disable-glyph-caching", v)} - /> - - - setGuacField("enable-gfx", v)} - /> - -
-
- - } - > -
- - setGuacField("enable-printing", v)} - /> - - - setGuacField("enable-drive", v)} - /> - -
-
- - setGuacField("drive-name", e.target.value)} - /> -
-
- - setGuacField("drive-path", e.target.value)} - /> -
-
- - setGuacField("create-drive-path", v)} - /> - - - setGuacField("disable-download", v)} - /> - - - setGuacField("disable-upload", v)} - /> - - - setGuacField("enable-touch", v)} - /> - -
-
- - } - > -
-
- - setGuacField("client-name", e.target.value)} - /> -
- - setGuacField("console", v)} - /> - -
- - setGuacField("initial-program", e.target.value)} - /> -
-
- - setGuacField("server-layout", e.target.value)} - > - - - - - - - - - - - - - - -
-
- - setGuacField("timezone", e.target.value)} - /> -
-
-
- - } - > -
-
-
- - - setGuacField("gateway-hostname", e.target.value) - } - /> -
-
- - setGuacField("gateway-port", e.target.value)} - /> -
-
- - - setGuacField("gateway-username", e.target.value) - } - /> -
-
- - - setGuacField("gateway-password", e.target.value) - } - /> -
-
- - setGuacField("gateway-domain", e.target.value)} - /> -
-
-
-
- - } - > -
-
- - setGuacField("remote-app", e.target.value)} - /> -
-
- - setGuacField("remote-app-dir", e.target.value)} - /> -
-
- - setGuacField("remote-app-args", e.target.value)} - /> -
-
-
- - } - > -
-
- - - setGuacField("normalize-clipboard", e.target.value) - } - > - - - - - -
- - setGuacField("disable-copy", v)} - /> - - - setGuacField("disable-paste", v)} - /> - -
-
- - } - action={ - - } - > -
- - setGuacField("create-recording-path", v)} - /> - - - setGuacField("recording-exclude-output", v)} - /> - - - setGuacField("recording-exclude-mouse", v)} - /> - - - setGuacField("recording-include-keys", v)} - /> - -
-
- - } - action={ - - } - > -
- - setGuacField("wol-send-packet", v)} - /> - -
-
- - setGuacField("wol-mac-addr", e.target.value)} - /> -
-
- - - setGuacField("wol-broadcast-addr", e.target.value) - } - /> -
-
- - setGuacField("wol-udp-port", e.target.value)} - /> -
-
- - setGuacField("wol-wait-time", e.target.value)} - /> -
-
-
-
- - ); -} - -export function HostEditorVncTab({ - form, - setField, - setGuacField, - host, - credentials, -}: { - form: HostEditorForm; - setField: HostEditorSetField; - setGuacField: GuacFieldSetter; - host?: { macAddress?: string | null } | null; - credentials?: { id: string; name: string; username: string }[]; -}) { - const { t } = useTranslation(); - - return ( - <> - } - action={ - - } - > -
-
- - setField("vncPort", Number(e.target.value))} - className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> -
-
-
- } - > -
-
- - setGuacField("guacd-hostname", e.target.value)} - /> -
-
- - setGuacField("guacd-port", e.target.value)} - className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> -
-

- {t("hosts.guac.guacdProxyDesc")} -

-
-
- - } - > -
- {credentials && credentials.length > 0 && ( -
- -
- {(["direct", "credential"] as const).map((m) => ( - - ))} -
-
- )} - {form.vncAuthType === "credential" && - credentials && - credentials.length > 0 ? ( -
- - setField("vncCredentialId", e.target.value)} - className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - - {credentials.map((c) => ( - - ))} - -
- ) : ( -
-
- - { - if (form.vncPassword === HOST_PROTOCOL_SECRET_KEPT) - setField("vncPassword", ""); - }} - onChange={(e) => setField("vncPassword", e.target.value)} - /> -
-
- - setField("vncUser", e.target.value)} - /> -
-
- )} -
-
- - } - > -
-
- - setGuacField("color-depth", e.target.value)} - > - - - - - - -
-
-
- - setGuacField("width", e.target.value)} - /> -
-
- - setGuacField("height", e.target.value)} - /> -
-
-
- - setGuacField("resize-method", e.target.value)} - > - - - - -
- - setGuacField("force-lossless", v)} - /> - -
-
- - } - > -
- - setGuacField("disable-audio", v)} - /> - -
-
- - } - > -
-
- - setGuacField("cursor", e.target.value)} - > - - - - -
-
- - setGuacField("server-layout", e.target.value)} - > - - - - - - - - - - - - - - -
- - setGuacField("swap-red-blue", v)} - /> - - - setGuacField("read-only", v)} - /> - -
-
- - } - > -
-
- - - setGuacField("normalize-clipboard", e.target.value) - } - > - - - - - -
- - setGuacField("disable-copy", v)} - /> - - - setGuacField("disable-paste", v)} - /> - -
-
- - } - action={ - - } - > -
- - setGuacField("create-recording-path", v)} - /> - - - setGuacField("recording-exclude-output", v)} - /> - - - setGuacField("recording-exclude-mouse", v)} - /> - - - setGuacField("recording-include-keys", v)} - /> - -
-
- - } - action={ - - } - > -
- - setGuacField("wol-send-packet", v)} - /> - -
-
- - setGuacField("wol-mac-addr", e.target.value)} - /> -
-
- - - setGuacField("wol-broadcast-addr", e.target.value) - } - /> -
-
- - setGuacField("wol-udp-port", e.target.value)} - /> -
-
- - setGuacField("wol-wait-time", e.target.value)} - /> -
-
-
-
- - ); -} - -export function HostEditorTelnetTab({ - form, - setField, - setGuacField, - credentials, -}: { - form: HostEditorForm; - setField: HostEditorSetField; - setGuacField: GuacFieldSetter; - credentials?: { id: string; name: string; username: string }[]; -}) { - const { t } = useTranslation(); - - return ( - <> - } - action={ - - } - > -
-
- - setField("telnetPort", Number(e.target.value))} - className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> -
-
-
- } - > -
-
- - setGuacField("guacd-hostname", e.target.value)} - /> -
-
- - setGuacField("guacd-port", e.target.value)} - className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> -
-

- {t("hosts.guac.guacdProxyDesc")} -

-
-
- } - > -
- {credentials && credentials.length > 0 && ( -
- -
- {(["direct", "credential"] as const).map((m) => ( - - ))} -
-
- )} - {form.telnetAuthType === "credential" && - credentials && - credentials.length > 0 ? ( -
- - setField("telnetCredentialId", e.target.value)} - className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - - {credentials.map((c) => ( - - ))} - -
- ) : ( -
-
- - setField("telnetUser", e.target.value)} - /> -
-
- - { - if (form.telnetPassword === HOST_PROTOCOL_SECRET_KEPT) - setField("telnetPassword", ""); - }} - onChange={(e) => setField("telnetPassword", e.target.value)} - /> -
-
- )} -
-
- - } - > -
-
-
- - setGuacField("width", e.target.value)} - /> -
-
- - setGuacField("height", e.target.value)} - /> -
-
-
-
- - } - > -
-
- - setGuacField("terminal-type", e.target.value)} - > - - - - - - -
-
- - setGuacField("font-name", e.target.value)} - /> -
-
- - - setGuacField("font-size", Number(e.target.value)) - } - /> -
-
- - setGuacField("color-scheme", e.target.value)} - > - - - - - - -
-
- - setGuacField("backspace", e.target.value)} - > - - - - -
-
-
- - } - action={ - - } - > -
- - setGuacField("create-recording-path", v)} - /> - - - setGuacField("recording-exclude-output", v)} - /> - - - setGuacField("recording-include-keys", v)} - /> - -
-
- - ); -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-api.ts b/plugins/remote-desktop/src/frontend/guacamole-api.ts deleted file mode 100644 index 9ec59a906..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-api.ts +++ /dev/null @@ -1,309 +0,0 @@ -import type { PluginApiClient, TermixApp } from "@termix/plugin-sdk/frontend"; -import { - isElectron, - resolveRemoteHostId, - type ConnectionOrigin, -} from "@termix/plugin-sdk/ui"; -import type { GuacamoleConfig } from "./guacamole-config"; -import { errorMessage } from "./host-remote"; - -let app: Pick | null = null; - -/** Set in activate, cleared on deactivate. */ -export function setRemoteDesktopApp(next: Pick | null) { - app = next; -} - -/** - * Picks the backend that will actually serve this session, so the token and - * status come from the same place as the connection. - * - * Outside Electron there is only one backend. Inside it, a host left on - * Default still resolves to the connected server -- asking the embedded - * backend would report the guacd *it* can reach rather than the one serving - * the session. A host explicitly set to "This Device" (Support#1240) is - * served by the embedded backend against the user's own guacd, so its calls - * have to go there instead. - * - * Callers pass the origin explicitly rather than defaulting it: a default - * silently sent a missed call site to the remote server, which fails with a - * bare network error on a desktop that has none configured. - */ -function guacamoleApi(origin: ConnectionOrigin): PluginApiClient { - if (!app) throw new Error("Remote Desktop is not active"); - return app.apiFor(origin); -} - -function apiError(error: unknown, action: string): Error { - return new Error(errorMessage(error, `Failed to ${action}`)); -} - -export interface GuacamoleTokenRequest { - protocol: "rdp" | "vnc" | "telnet"; - hostname: string; - port?: number; - username?: string; - password?: string; - domain?: string; - security?: string; - ignoreCert?: boolean; - guacamoleConfig?: GuacamoleConfig; -} - -export interface GuacamoleTokenResponse { - token: string; - guacamoleConnectionId?: string | null; - termixConnectId?: string; -} - -type GuacamoleConfigSource = { - guacamoleConfig?: string | Record | null; -}; - -export function getGuacamoleDpi( - source?: GuacamoleConfigSource, -): number | undefined { - const config = source?.guacamoleConfig; - if (!config) return undefined; - - let dpi: unknown; - if (typeof config === "string") { - try { - dpi = JSON.parse(config).dpi; - } catch { - return undefined; - } - } else { - dpi = config.dpi; - } - - const parsedDpi = typeof dpi === "string" ? Number(dpi) : dpi; - if ( - typeof parsedDpi !== "number" || - !Number.isFinite(parsedDpi) || - parsedDpi <= 0 - ) { - return undefined; - } - - return Math.trunc(parsedDpi); -} - -function toGuacamoleParams( - config: GuacamoleTokenRequest["guacamoleConfig"], -): Record { - if (!config) return {}; - - const params: Record = {}; - - const mappings: Record = { - 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; - if (typeof value === "boolean") { - params[paramName] = value ? "true" : "false"; - } else { - params[paramName] = value; - } - } - } - - return params; -} - -export async function getGuacamoleToken( - request: GuacamoleTokenRequest, - origin: ConnectionOrigin, -): Promise { - try { - const guacParams = toGuacamoleParams(request.guacamoleConfig); - - const response = await guacamoleApi(origin).post( - "/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 apiError(error, "get a connection token"); - } -} - -export async function getGuacamoleTokenFromHost( - hostId: number, - origin: ConnectionOrigin, - protocol?: "rdp" | "vnc" | "telnet", - promptedCredentials?: { - username?: string; - password?: string; - domain?: string; - }, - syncId?: string | null, - tabInstanceId?: string, -): Promise { - try { - // A locally-originated session is served by the embedded backend, which - // knows the host by its local id -- remapping it onto the remote - // server's id would address the wrong row, or fail outright when no - // server is configured at all. - const useRemote = isElectron() && origin === "remote"; - const remoteHostId = useRemote ? await resolveRemoteHostId(syncId) : null; - if (useRemote && syncId && remoteHostId === null) { - throw new Error("The synced host does not exist on the remote server"); - } - const targetHostId = remoteHostId ?? hostId; - const response = await guacamoleApi(origin).post( - `/connect-host/${targetHostId}`, - { - ...(protocol ? { protocol } : {}), - ...(tabInstanceId ? { tabInstanceId } : {}), - ...(promptedCredentials?.username - ? { promptedUsername: promptedCredentials.username } - : {}), - ...(promptedCredentials?.password - ? { promptedPassword: promptedCredentials.password } - : {}), - ...(promptedCredentials - ? { promptedDomain: promptedCredentials.domain ?? "" } - : {}), - }, - ); - return response.data; - } catch (error) { - throw apiError(error, "get a connection token"); - } -} - -export interface RemoteDesktopStatus { - enabled: boolean; - guacd: { status: string }; -} - -/** `probe: false` skips dialing guacd, for a caller that only needs `enabled`. */ -export async function getGuacdStatus( - origin: ConnectionOrigin, - options: { - probe?: boolean; - hostId?: number; - protocol?: "rdp" | "vnc" | "telnet"; - syncId?: string | null; - } = {}, -): Promise { - const params = new URLSearchParams(); - if (options.probe === false) params.set("probe", "0"); - if (options.hostId) { - const useRemote = isElectron() && origin === "remote"; - const remoteHostId = useRemote - ? await resolveRemoteHostId(options.syncId) - : null; - if (useRemote && options.syncId && remoteHostId === null) { - throw new Error("The synced host does not exist on the remote server"); - } - params.set("hostId", String(remoteHostId ?? options.hostId)); - if (options.protocol) params.set("protocol", options.protocol); - } - const query = params.toString(); - const response = await guacamoleApi(origin).get( - query ? `/status?${query}` : "/status", - ); - return response.data; -} - -export async function getGuacamoleConnectionId( - connectId: string, - origin: ConnectionOrigin, - signal?: AbortSignal, -): Promise { - const response = await guacamoleApi(origin).get<{ - guacamoleConnectionId?: string | null; - }>(`/connection/${encodeURIComponent(connectId)}`, { signal }); - return response.data.guacamoleConnectionId ?? null; -} - -export async function nativeRdpAvailable(): Promise { - if (!isElectron()) return false; - try { - const response = await guacamoleApi("local").get<{ available: boolean }>( - "/native-rdp", - ); - return response.data.available === true; - } catch { - return false; - } -} - -/** Always the embedded backend: it is the one that can reach Electron. */ -export async function openNativeRdp(request: { - host: string; - port?: number; - username?: string; - domain?: string; -}): Promise<{ success: boolean; error?: string }> { - const response = await guacamoleApi("local").post<{ - success: boolean; - error?: string; - }>("/native-rdp", request); - return response.data; -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-clipboard.ts b/plugins/remote-desktop/src/frontend/guacamole-clipboard.ts deleted file mode 100644 index 9fe2f71da..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-clipboard.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Guacamole from "guacamole-common-js"; - -const CONTROL_LEFT_KEYSYM = 0xffe3; -const V_KEYSYM = 0x76; - -interface ClipboardOutputStream { - sendBlob(data: string): void; - sendEnd(): void; -} - -export interface GuacamoleClipboardClient { - createClipboardStream(mimetype: string): ClipboardOutputStream; - sendKeyEvent(pressed: number, keysym: number): void; -} - -export function isPasteShortcut( - event: Pick, -): boolean { - return ( - !event.altKey && - (event.ctrlKey || event.metaKey) && - event.key.toLowerCase() === "v" - ); -} - -export function pasteTextToRemote( - client: GuacamoleClipboardClient, - text: string, -): void { - const stream = client.createClipboardStream("text/plain"); - const writer = new Guacamole.StringWriter(stream); - writer.sendText(text); - writer.sendEnd(); - - client.sendKeyEvent(1, CONTROL_LEFT_KEYSYM); - client.sendKeyEvent(1, V_KEYSYM); - client.sendKeyEvent(0, V_KEYSYM); - client.sendKeyEvent(0, CONTROL_LEFT_KEYSYM); -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-common-js.d.ts b/plugins/remote-desktop/src/frontend/guacamole-common-js.d.ts deleted file mode 100644 index e3ff0a1fd..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-common-js.d.ts +++ /dev/null @@ -1,225 +0,0 @@ -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; - sendSize(width: number, height: number): void; - setClipboard(stream: OutputStream, mimetype: string): void; - createClipboardStream(mimetype: string): OutputStream; - onstatechange: ((state: number) => void) | null; - onerror: ((error: Status) => void) | null; - onsync: ((timestamp: number) => void) | null; - onclipboard: ((stream: InputStream, mimetype: string) => void) | null; - onaudio: ((stream: InputStream, mimetype: string) => void) | null; - onfile: - | ((stream: InputStream, mimetype: string, filename: string) => void) - | null; - onfilesystem: ((filesystem: Object, name: string) => void) | null; - } - - // Mirrors Guacamole.Object: a named collection of streams. Within this - // namespace `Object` refers to this class, not the global one. - class Object { - static readonly ROOT_STREAM: string; - static readonly STREAM_INDEX_MIMETYPE: string; - readonly index: number; - requestInputStream( - name: string, - bodyCallback?: (stream: InputStream, mimetype: string) => void, - ): void; - createOutputStream(mimetype: string, name: string): OutputStream; - onbody: ((stream: InputStream, mimetype: string) => void) | null; - onundefine: (() => void) | null; - } - - class AudioPlayer { - static getInstance( - stream: InputStream, - mimetype: string, - ): AudioPlayer | null; - sync(): void; - } - - 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 SessionRecording { - constructor(source: Blob | Tunnel, refreshInterval?: number); - onload: (() => void) | null; - onerror: ((message: string) => void) | null; - onprogress: ((duration: number, parsedSize: number) => void) | null; - onplay: (() => void) | null; - onpause: (() => void) | null; - onseek: - ((position: number, current: number, total: number) => void) | null; - getDisplay(): Display; - getPosition(): number; - getDuration(): number; - isPlaying(): boolean; - play(): void; - pause(): void; - seek(position: number, callback?: () => void): void; - abort(): void; - } - - 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; - } - - interface MouseEvent { - state: Mouse.State; - preventDefault(): void; - stopPropagation(): void; - } - - class Touchpad { - constructor(element: HTMLElement); - currentState: Mouse.State; - clickTimingThreshold: number; - clickMoveThreshold: number; - scrollThreshold: number; - onEach( - types: string[], - listener: (event: Mouse.MouseEvent) => void, - ): void; - on(type: string, listener: (event: Mouse.MouseEvent) => void): void; - } - - class Touchscreen { - constructor(element: HTMLElement); - currentState: Mouse.State; - clickTimingThreshold: number; - clickMoveThreshold: number; - scrollThreshold: number; - longPressThreshold: number; - onEach( - types: string[], - listener: (event: Mouse.MouseEvent) => void, - ): void; - on(type: string, listener: (event: Mouse.MouseEvent) => void): void; - } - } - - class Keyboard { - constructor(element: Document | HTMLElement); - reset(): void; - onkeydown: ((keysym: number) => void) | null; - onkeyup: ((keysym: number) => void) | null; - } - - class Status { - code: number; - message: string; - isError(): boolean; - static readonly Code: { - SUCCESS: number; - UNSUPPORTED: number; - SERVER_ERROR: number; - SERVER_BUSY: number; - UPSTREAM_TIMEOUT: number; - UPSTREAM_ERROR: number; - RESOURCE_NOT_FOUND: number; - RESOURCE_CONFLICT: number; - RESOURCE_CLOSED: number; - CLIENT_BAD_REQUEST: number; - CLIENT_UNAUTHORIZED: number; - CLIENT_FORBIDDEN: number; - CLIENT_TIMEOUT: number; - }; - } - - class InputStream { - onblob: ((data: string) => void) | null; - onend: (() => void) | null; - sendAck(message: string, code: number): void; - } - - 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; - } - - class BlobReader { - constructor(stream: InputStream, mimetype: string); - getBlob(): Blob; - getLength(): number; - onprogress: ((length: number) => void) | null; - onend: (() => void) | null; - } - - class BlobWriter { - constructor(stream: OutputStream); - sendBlob(blob: Blob): void; - sendEnd(): void; - onack: ((status: Status) => void) | null; - onerror: ((blob: Blob, offset: number, error: Status) => void) | null; - onprogress: ((blob: Blob, offset: number) => void) | null; - oncomplete: ((blob: Blob) => void) | null; - } - } - - export default Guacamole; -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-config.ts b/plugins/remote-desktop/src/frontend/guacamole-config.ts deleted file mode 100644 index 74e7b6320..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-config.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Per-host Guacamole connection settings, as edited in the host editor and - * forwarded to guacd. Field names are camelCase here and translated to the - * hyphenated protocol parameters in toGuacamoleParams(). - */ -export interface GuacamoleConfig { - colorDepth?: number; - width?: number; - height?: number; - dpi?: number; - resizeMethod?: string; - forceLossless?: boolean; - disableAudio?: boolean; - enableAudioInput?: boolean; - enableWallpaper?: boolean; - enableTheming?: boolean; - enableFontSmoothing?: boolean; - enableFullWindowDrag?: boolean; - enableDesktopComposition?: boolean; - enableMenuAnimations?: boolean; - disableBitmapCaching?: boolean; - disableOffscreenCaching?: boolean; - disableGlyphCaching?: boolean; - disableGfx?: boolean; - enablePrinting?: boolean; - printerName?: string; - enableDrive?: boolean; - driveName?: string; - drivePath?: string; - createDrivePath?: boolean; - disableDownload?: boolean; - disableUpload?: boolean; - enableTouch?: boolean; - clientName?: string; - console?: boolean; - initialProgram?: string; - serverLayout?: string; - timezone?: string; - gatewayHostname?: string; - gatewayPort?: number; - gatewayUsername?: string; - gatewayPassword?: string; - gatewayDomain?: string; - remoteApp?: string; - remoteAppDir?: string; - remoteAppArgs?: string; - normalizeClipboard?: string; - disableCopy?: boolean; - disablePaste?: boolean; - cursor?: string; - swapRedBlue?: boolean; - readOnly?: boolean; - recordingPath?: string; - recordingName?: string; - createRecordingPath?: boolean; - recordingExcludeOutput?: boolean; - recordingExcludeMouse?: boolean; - recordingIncludeKeys?: boolean; - wolSendPacket?: boolean; - wolMacAddr?: string; - wolBroadcastAddr?: string; - wolUdpPort?: number; - wolWaitTime?: number; -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-display-size.ts b/plugins/remote-desktop/src/frontend/guacamole-display-size.ts deleted file mode 100644 index 27e34f3a8..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-display-size.ts +++ /dev/null @@ -1,48 +0,0 @@ -const DEFAULT_RDP_DPI = 96; -const MAX_DEVICE_PIXEL_RATIO = 3; - -/** - * Reads a guacamoleConfig display field. The UI stores these as strings, and - * leaves them empty when the size should follow the browser window. - */ -export function readConfiguredDimension(value: unknown): number | undefined { - const parsed = Number(value); - return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; -} - -export interface GuacamoleDisplaySize { - width: number; - height: number; - dpi?: number; - pixelRatio: number; -} - -export function getGuacamoleDisplaySize( - cssWidth: number, - cssHeight: number, - protocol: string | undefined, - devicePixelRatio: number, - configuredDpi?: number, -): GuacamoleDisplaySize { - const isRdp = protocol === "rdp"; - const pixelRatio = isRdp - ? Math.min( - MAX_DEVICE_PIXEL_RATIO, - Math.max(1, Number.isFinite(devicePixelRatio) ? devicePixelRatio : 1), - ) - : 1; - - const size = { - width: Math.max(1, Math.round(cssWidth * pixelRatio)), - height: Math.max(1, Math.round(cssHeight * pixelRatio)), - pixelRatio, - }; - - if (!isRdp) return size; - - const baseDpi = - configuredDpi && Number.isFinite(configuredDpi) && configuredDpi > 0 - ? configuredDpi - : DEFAULT_RDP_DPI; - return { ...size, dpi: Math.round(baseDpi * pixelRatio) }; -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-file-drop.ts b/plugins/remote-desktop/src/frontend/guacamole-file-drop.ts deleted file mode 100644 index b1fe2273c..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-file-drop.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type FileDropDisposition = "ignore" | "reject" | "upload"; - -export function hasDraggedFiles(types: readonly string[]): boolean { - return types.includes("Files"); -} - -export function getFileDropDisposition( - types: readonly string[], - fileCount: number, - canUpload: boolean, -): FileDropDisposition { - if (!hasDraggedFiles(types) || fileCount === 0) return "ignore"; - return canUpload ? "upload" : "reject"; -} - -export function canUploadToRdpDrive( - uploadAllowed: boolean, - driveEnabled: boolean, - hasFilesystem: boolean, -): boolean { - return uploadAllowed && (driveEnabled || hasFilesystem); -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-filesystem.ts b/plugins/remote-desktop/src/frontend/guacamole-filesystem.ts deleted file mode 100644 index a0ae0a1cb..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-filesystem.ts +++ /dev/null @@ -1,223 +0,0 @@ -import Guacamole from "guacamole-common-js"; - -// The bundled Guacamole runtime exposes Client.createFileStream(), but the -// package's older TypeScript declaration omits it. -export interface GuacamoleFileStreamClient { - createFileStream(mimetype: string, filename: string): Guacamole.OutputStream; -} - -// The root stream of a Guacamole.Object maps stream name to mimetype; a stream -// carrying that same mimetype is itself a directory. -export const STREAM_INDEX_MIMETYPE = - "application/vnd.glyptodon.guacamole.stream-index+json"; - -export interface RemoteFileEntry { - name: string; - path: string; - mimetype: string; - isDirectory: boolean; -} - -export function isDirectoryMimetype(mimetype: string): boolean { - return mimetype === STREAM_INDEX_MIMETYPE; -} - -export function joinPath(parent: string, name: string): string { - return parent === "/" ? `/${name}` : `${parent}/${name}`; -} - -export function parentPath(path: string): string { - const cut = path.lastIndexOf("/"); - return cut <= 0 ? "/" : path.slice(0, cut); -} - -export function basename(path: string): string { - return path.slice(path.lastIndexOf("/") + 1) || path; -} - -// guacd returns absolute paths as keys, but a malformed or relative key would -// otherwise produce a broken path on the next descent. -export function parseDirectoryIndex( - json: string, - path: string, -): RemoteFileEntry[] { - const index = JSON.parse(json) as Record; - - return Object.entries(index) - .map(([key, mimetype]) => { - const name = basename(key); - return { - name, - path: key.startsWith("/") ? key : joinPath(path, name), - mimetype, - isDirectory: isDirectoryMimetype(mimetype), - }; - }) - .sort((a, b) => - a.isDirectory === b.isDirectory - ? a.name.localeCompare(b.name) - : a.isDirectory - ? -1 - : 1, - ); -} - -// A refused stream is answered with an error ack that never reaches the body -// callback, so every request needs its own deadline to avoid hanging forever. -const REQUEST_TIMEOUT_MS = 15000; - -function requestStream( - filesystem: Guacamole.Object, - path: string, -): Promise<{ stream: Guacamole.InputStream; mimetype: string }> { - return new Promise((resolve, reject) => { - const timeout = setTimeout( - () => reject(new Error(`Timed out reading ${path}`)), - REQUEST_TIMEOUT_MS, - ); - - filesystem.requestInputStream(path, (stream, mimetype) => { - clearTimeout(timeout); - resolve({ stream, mimetype }); - }); - }); -} - -export async function listDirectory( - filesystem: Guacamole.Object, - path: string, -): Promise { - const { stream, mimetype } = await requestStream(filesystem, path); - - if (!isDirectoryMimetype(mimetype)) { - throw new Error(`${path} is not a directory`); - } - - const json = await new Promise((resolve) => { - const reader = new Guacamole.StringReader(stream); - let text = ""; - reader.ontext = (chunk: string) => { - text += chunk; - }; - reader.onend = () => resolve(text); - }); - - return parseDirectoryIndex(json, path); -} - -export async function downloadFile( - filesystem: Guacamole.Object, - path: string, -): Promise<{ blob: Blob; filename: string }> { - const { stream, mimetype } = await requestStream(filesystem, path); - - if (isDirectoryMimetype(mimetype)) { - throw new Error(`${path} is a directory`); - } - - const blob = await new Promise((resolve) => { - const reader = new Guacamole.BlobReader(stream, mimetype); - reader.onend = () => resolve(reader.getBlob()); - }); - - return { blob, filename: basename(path) }; -} - -export function uploadFile( - filesystem: Guacamole.Object, - directory: string, - file: File, - onProgress?: (sent: number, total: number) => void, -): Promise { - return writeUpload( - filesystem.createOutputStream( - file.type || "application/octet-stream", - joinPath(directory, file.name), - ), - file, - onProgress, - ); -} - -/** - * Uploads through Guacamole's connection-level file stream. RDP exposes this - * path even when guacd does not advertise a browsable filesystem object, so - * display drops must not depend on `Client.onfilesystem` having fired. - */ -export function uploadFileToClient( - client: GuacamoleFileStreamClient, - file: File, - onProgress?: (sent: number, total: number) => void, -): Promise { - return writeUpload(createClientFileStream(client, file), file, onProgress); -} - -export function createClientFileStream( - client: GuacamoleFileStreamClient, - file: File, -): Guacamole.OutputStream { - return client.createFileStream( - file.type || "application/octet-stream", - file.name, - ); -} - -function writeUpload( - stream: Guacamole.OutputStream, - file: File, - onProgress?: (sent: number, total: number) => void, -): Promise { - return new Promise((resolve, reject) => { - const writer = new Guacamole.BlobWriter(stream); - - // A rejected blob stops the writer without firing onerror or oncomplete — - // only the error ack reports it, so without this the upload hangs forever. - writer.onack = (status: Guacamole.Status) => { - if (status.isError()) { - reject( - new Error( - status.message || `Server refused the upload of ${file.name}`, - ), - ); - } - }; - writer.onerror = () => reject(new Error(`Failed to upload ${file.name}`)); - writer.onprogress = (_blob: Blob, offset: number) => - onProgress?.(offset, file.size); - writer.oncomplete = () => { - writer.sendEnd(); - onProgress?.(file.size, file.size); - resolve(); - }; - - writer.sendBlob(file); - }); -} - -export function saveBlobAs(blob: Blob, filename: string): void { - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = filename; - link.click(); - URL.revokeObjectURL(url); -} - -/** - * Turns an upload failure into something a user can act on. guacd answers a - * refused stream with an ack like "FAIL (CANNOT OPEN)" - accurate, but it - * means "the drive folder isn't writable", which is what people need to hear. - */ -export function describeUploadError( - error: unknown, - t: (key: "driveNotWritable" | "driveUnavailable" | "uploadFailed") => string, -): string { - const message = error instanceof Error ? error.message : ""; - if (/cannot open|can't open|permission denied/i.test(message)) { - return t("driveNotWritable"); - } - if (/no fs/i.test(message)) { - return t("driveUnavailable"); - } - return message || t("uploadFailed"); -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-meta-key.ts b/plugins/remote-desktop/src/frontend/guacamole-meta-key.ts deleted file mode 100644 index a9cc446d2..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-meta-key.ts +++ /dev/null @@ -1,82 +0,0 @@ -export type MetaKeyFamily = "win" | "super" | "cmd"; - -export interface MetaKeyLabels { - family: MetaKeyFamily; - short: string; - lock: string; - stickyKey: string; - keyTooltip: string; - lockTooltip: string; -} - -/** - * RDP is a Windows desktop, so the toolbar keeps Windows labels. VNC is used - * for Linux and macOS as often as Windows; branding those sessions with a - * Windows key is wrong, and the local device is the only OS we can see. - */ -export function detectMetaKeyFamily( - protocol: "rdp" | "vnc" | "telnet", - env: { - platform?: string; - userAgent?: string; - userAgentDataPlatform?: string; - } = {}, -): MetaKeyFamily { - if (protocol === "rdp") return "win"; - - const uaPlatform = env.userAgentDataPlatform ?? ""; - const platform = env.platform ?? ""; - const userAgent = env.userAgent ?? ""; - const haystack = `${uaPlatform} ${platform} ${userAgent}`; - - if (/iphone|ipad|ipod|mac/i.test(haystack)) return "cmd"; - if (/win/i.test(haystack)) return "win"; - return "super"; -} - -export function detectRuntimeMetaKeyFamily( - protocol: "rdp" | "vnc" | "telnet", -): MetaKeyFamily { - if (typeof navigator === "undefined") { - return detectMetaKeyFamily(protocol); - } - const uaData = navigator as Navigator & { - userAgentData?: { platform?: string }; - }; - return detectMetaKeyFamily(protocol, { - platform: navigator.platform, - userAgent: navigator.userAgent, - userAgentDataPlatform: uaData.userAgentData?.platform, - }); -} - -export function metaKeyLabels(family: MetaKeyFamily): MetaKeyLabels { - if (family === "cmd") { - return { - family, - short: "Cmd", - lock: "Cmd+L", - stickyKey: "remoteDesktop.toolbar.cmd", - keyTooltip: "remoteDesktop.toolbar.cmdKey", - lockTooltip: "remoteDesktop.toolbar.cmdL", - }; - } - if (family === "super") { - return { - family, - short: "Super", - lock: "Super+L", - stickyKey: "remoteDesktop.toolbar.super", - keyTooltip: "remoteDesktop.toolbar.superKey", - lockTooltip: "remoteDesktop.toolbar.superL", - }; - } - return { - family, - short: "Win", - lock: "Win+L", - stickyKey: "remoteDesktop.toolbar.win", - keyTooltip: "remoteDesktop.toolbar.winKey", - lockTooltip: "remoteDesktop.toolbar.winL", - }; -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-pointer.ts b/plugins/remote-desktop/src/frontend/guacamole-pointer.ts deleted file mode 100644 index 507353a0d..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-pointer.ts +++ /dev/null @@ -1,65 +0,0 @@ -import Guacamole from "guacamole-common-js"; -import { - createScrollCoalescer, - isScrollButtonState, -} from "./guacamole-scroll.js"; - -export type GuacamoleTouchMode = "touchscreen" | "touchpad"; - -/** - * Guacamole.Mouse listens for mousedown/mousemove/mouseup; the Touchscreen and - * Touchpad emulators listen only for touch events. The two sets do not overlap, - * so binding one instead of the other leaves that input dead. - * - * A touch-capable device is not a device without a pointer: laptops with a - * touchscreen report maxTouchPoints > 0 and are still driven by a mouse. The - * physical pointer is therefore always bound, and a touch emulator is layered - * on top when one is selected. - */ -export function bindPointerInput( - element: HTMLElement, - touchMode: GuacamoleTouchMode | null | undefined, - onState: (state: Guacamole.Mouse.State) => void, -): () => void { - const coalescer = createScrollCoalescer((state) => { - onState(state as Guacamole.Mouse.State); - }); - let expectingScrollRelease = false; - - const handleState = (state: Guacamole.Mouse.State) => { - coalescer.notePointer(state); - if (isScrollButtonState(state)) { - coalescer.ingestClick(state.down ? 1 : -1); - expectingScrollRelease = true; - return; - } - if ( - expectingScrollRelease && - !state.left && - !state.middle && - !state.right - ) { - expectingScrollRelease = false; - return; - } - expectingScrollRelease = false; - onState(state); - }; - - const mouse = new Guacamole.Mouse(element); - mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = handleState; - - if (touchMode === "touchscreen") { - const touchscreen = new Guacamole.Mouse.Touchscreen(element); - touchscreen.onEach(["mousedown", "mousemove", "mouseup"], (event) => - handleState(event.state), - ); - } else if (touchMode === "touchpad") { - const touchpad = new Guacamole.Mouse.Touchpad(element); - touchpad.onEach(["mousedown", "mousemove", "mouseup"], (event) => - handleState(event.state), - ); - } - - return () => coalescer.dispose(); -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-scroll.ts b/plugins/remote-desktop/src/frontend/guacamole-scroll.ts deleted file mode 100644 index 9a24d4da4..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-scroll.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * guacamole-common-js turns each wheel tick into one or more mouse-button - * clicks (button 4/5). High-resolution trackpads and the library's duplicate - * wheel/mousewheel listeners can emit dozens of those clicks in a single - * gesture. Each click is two Guacamole instructions, so a laggy path (a - * Cloudflare-proxied remote session especially) queues them and the remote - * desktop keeps scrolling long after the user stopped. - * - * Cap the in-flight click count and space flushes out so scrolling stays - * usable without changing the websocket proxy. - */ -export const MAX_SCROLL_CLICKS_PER_FLUSH = 2; -export const SCROLL_FLUSH_INTERVAL_MS = 32; - -export type ScrollClickDirection = 1 | -1; - -export interface GuacamolePointerState { - x: number; - y: number; - left: boolean; - middle: boolean; - right: boolean; - up: boolean; - down: boolean; -} - -export function clampPendingScrollClicks( - pending: number, - delta: number, - max = MAX_SCROLL_CLICKS_PER_FLUSH, -): number { - return Math.max(-max, Math.min(max, pending + delta)); -} - -export function takeScrollClicks(pending: number): { - clicks: number; - direction: ScrollClickDirection | 0; -} { - if (pending === 0) return { clicks: 0, direction: 0 }; - const direction: ScrollClickDirection = pending < 0 ? -1 : 1; - return { - clicks: Math.min(MAX_SCROLL_CLICKS_PER_FLUSH, Math.abs(pending)), - direction, - }; -} - -function mouseState( - x: number, - y: number, - buttons: Pick, - up: boolean, - down: boolean, -): GuacamolePointerState { - return { - x, - y, - left: buttons.left, - middle: buttons.middle, - right: buttons.right, - up, - down, - }; -} - -export function createScrollCoalescer( - send: (state: GuacamolePointerState) => void, - now: () => number = () => Date.now(), - schedule: (fn: () => void, ms: number) => number = (fn, ms) => - window.setTimeout(fn, ms), - cancel: (id: number) => void = (id) => window.clearTimeout(id), -): { - notePointer: (state: GuacamolePointerState) => void; - ingestClick: (direction: ScrollClickDirection) => void; - dispose: () => void; -} { - let pending = 0; - let timer: number | null = null; - let x = 0; - let y = 0; - let buttons = { left: false, middle: false, right: false }; - let lastFlushAt = 0; - - const flush = () => { - timer = null; - const { clicks, direction } = takeScrollClicks(pending); - pending = 0; - if (clicks === 0 || direction === 0) return; - lastFlushAt = now(); - const up = direction < 0; - const down = direction > 0; - for (let i = 0; i < clicks; i++) { - send(mouseState(x, y, buttons, up, down)); - send(mouseState(x, y, buttons, false, false)); - } - }; - - return { - notePointer(state) { - x = state.x; - y = state.y; - buttons = { - left: state.left, - middle: state.middle, - right: state.right, - }; - }, - ingestClick(direction) { - pending = clampPendingScrollClicks(pending, direction); - const elapsed = now() - lastFlushAt; - if (elapsed >= SCROLL_FLUSH_INTERVAL_MS) { - flush(); - return; - } - if (timer == null) { - timer = schedule(flush, SCROLL_FLUSH_INTERVAL_MS - elapsed); - } - }, - dispose() { - if (timer == null) return; - cancel(timer); - timer = null; - }, - }; -} - -export function isScrollButtonState(state: { - up?: boolean; - down?: boolean; -}): boolean { - return state.up === true || state.down === true; -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-session-id.ts b/plugins/remote-desktop/src/frontend/guacamole-session-id.ts deleted file mode 100644 index 9b173472b..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-session-id.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getGuacamoleConnectionId } from "./guacamole-api"; -import type { ConnectionOrigin } from "@termix/plugin-sdk/ui"; - -export function watchGuacamoleConnectionId( - connectId: string, - origin: ConnectionOrigin, - onReady: (id: string) => void, -): () => void { - const controller = new AbortController(); - let timer: ReturnType | undefined; - let attempts = 0; - const poll = async () => { - try { - const id = await getGuacamoleConnectionId( - connectId, - origin, - controller.signal, - ); - if (controller.signal.aborted) return; - if (id) onReady(id); - else if (++attempts < 20) timer = setTimeout(poll, 500); - } catch { - // Session sharing discovery must not interrupt the remote display. - } - }; - void poll(); - return () => { - controller.abort(); - clearTimeout(timer); - }; -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-state.ts b/plugins/remote-desktop/src/frontend/guacamole-state.ts deleted file mode 100644 index bf8efd923..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-state.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ConnectionStage } from "@termix/plugin-sdk/ui"; - -// Guacamole client states, per guacamole-common-js Guacamole.Client#STATE_*. -const GUAC_STATE_IDLE = 0; -const GUAC_STATE_CONNECTING = 1; -const GUAC_STATE_WAITING = 2; -const GUAC_STATE_CONNECTED = 3; -const GUAC_STATE_DISCONNECTING = 4; -const GUAC_STATE_DISCONNECTED = 5; - -export function guacStateToStage(state: number): ConnectionStage { - switch (state) { - case GUAC_STATE_CONNECTING: - return "guac_connecting"; - case GUAC_STATE_WAITING: - return "guac_handshake"; - case GUAC_STATE_CONNECTED: - return "guac_ready"; - case GUAC_STATE_DISCONNECTING: - case GUAC_STATE_DISCONNECTED: - return "guac_disconnected"; - case GUAC_STATE_IDLE: - default: - return "guac_connecting"; - } -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-websocket-url.ts b/plugins/remote-desktop/src/frontend/guacamole-websocket-url.ts deleted file mode 100644 index 7d1af5ae5..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-websocket-url.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Where the Guacamole display socket lives. - * - * One path in every deployment: the plugin serves it at - * /plugin-ws/remote-desktop/display on the main backend, so there is no - * guacamole port and no separate nginx block any more. Electron with a remote - * server is the only case that targets a different host. - */ -export function buildGuacamoleWebSocketBaseUrl({ - isElectronApp, - isEmbeddedApp, - configuredServerUrl, - basePath, - location, -}: { - isDev?: boolean; - isElectronApp: boolean; - isEmbeddedApp: boolean; - configuredServerUrl?: string; - basePath: string; - location: Pick; -}) { - const route = "/plugin-ws/remote-desktop/display"; - - if (isElectronApp && !isEmbeddedApp && configuredServerUrl) { - const wsProtocol = configuredServerUrl.startsWith("https://") - ? "wss://" - : "ws://"; - const wsHost = configuredServerUrl - .replace(/^https?:\/\//, "") - .replace(/\/$/, ""); - return `${wsProtocol}${wsHost}${route}`; - } - - if (isElectronApp) return `ws://127.0.0.1:30001${route}`; - - const wsProtocol = location.protocol === "https:" ? "wss" : "ws"; - return `${wsProtocol}://${location.host}${basePath}${route}`; -} diff --git a/plugins/remote-desktop/src/frontend/guacamole-zoom.ts b/plugins/remote-desktop/src/frontend/guacamole-zoom.ts deleted file mode 100644 index 8a2fceb27..000000000 --- a/plugins/remote-desktop/src/frontend/guacamole-zoom.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const MIN_GUACAMOLE_ZOOM = 0.5; -export const MAX_GUACAMOLE_ZOOM = 4; -export const GUACAMOLE_ZOOM_STEP = 0.25; - -export function clampGuacamoleZoom(zoom: number): number { - return Math.min(MAX_GUACAMOLE_ZOOM, Math.max(MIN_GUACAMOLE_ZOOM, zoom)); -} - -export function stepGuacamoleZoom(zoom: number, direction: -1 | 1): number { - return clampGuacamoleZoom(zoom + direction * GUACAMOLE_ZOOM_STEP); -} diff --git a/plugins/remote-desktop/src/frontend/host-remote.ts b/plugins/remote-desktop/src/frontend/host-remote.ts deleted file mode 100644 index 22b416c8a..000000000 --- a/plugins/remote-desktop/src/frontend/host-remote.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { - HostProtocolAuthSummary, - PluginHostRecord, - QuickConnectLogin, -} from "@termix/plugin-sdk/frontend"; -import type { GuacamoleConfig } from "./guacamole-config"; - -export const PLUGIN_ID = "remote-desktop"; - -export type Protocol = "rdp" | "vnc" | "telnet"; - -/** This plugin's host settings, as they arrive on host.pluginSettings. */ -export interface RemoteHostOptions { - enableRdp: boolean; - enableVnc: boolean; - enableTelnet: boolean; - rdpPort: number; - vncPort: number; - telnetPort: number; - rdpSecurity: string; - rdpIgnoreCert: boolean; - guacamoleConfig: GuacamoleConfig; - enableToolbar: boolean; -} - -export const ENABLE_KEY = { - rdp: "enableRdp", - vnc: "enableVnc", - telnet: "enableTelnet", -} as const satisfies Record; - -export const PORT_KEY = { - rdp: "rdpPort", - vnc: "vncPort", - telnet: "telnetPort", -} as const satisfies Record; - -export const DEFAULT_PORT: Record = { - rdp: 3389, - vnc: 5900, - telnet: 23, -}; - -/** - * The host fields a session reads. Each protocol's login is core's, under - * protocolAuth, since this plugin declares its protocols in the manifest. - */ -export interface RemoteHostLogin { - id?: number | string; - name?: string | null; - ip?: string; - syncId?: string | null; - connectionOrigin?: "local" | "remote" | null; - protocolAuth?: Record; - quickConnectLogin?: QuickConnectLogin; - authOverrides?: Record< - string, - { - credentialId?: number | string | null; - required?: boolean; - ownerAuthShared?: boolean; - } - >; - pluginSettings?: Record>; -} - -/** The RDP domain a host's saved login carries. */ -export function rdpDomain(host: RemoteHostLogin): string | undefined { - return host.protocolAuth?.rdp?.fields?.domain || undefined; -} - -function port(value: unknown, fallback: number): number { - const parsed = typeof value === "string" ? Number(value) : value; - return typeof parsed === "number" && Number.isInteger(parsed) && parsed > 0 - ? parsed - : fallback; -} - -export function parseGuacamoleConfig(value: unknown): GuacamoleConfig { - if (!value) return {}; - if (typeof value === "string") { - try { - const parsed = JSON.parse(value); - return parsed && typeof parsed === "object" ? parsed : {}; - } catch { - return {}; - } - } - return typeof value === "object" ? (value as GuacamoleConfig) : {}; -} - -export function remoteOptions( - settings: Record | undefined, -): RemoteHostOptions { - const values = settings ?? {}; - return { - enableRdp: values.enableRdp === true, - enableVnc: values.enableVnc === true, - enableTelnet: values.enableTelnet === true, - rdpPort: port(values.rdpPort, DEFAULT_PORT.rdp), - vncPort: port(values.vncPort, DEFAULT_PORT.vnc), - telnetPort: port(values.telnetPort, DEFAULT_PORT.telnet), - rdpSecurity: - typeof values.rdpSecurity === "string" ? values.rdpSecurity : "", - rdpIgnoreCert: values.rdpIgnoreCert === true, - guacamoleConfig: parseGuacamoleConfig(values.guacamoleConfig), - enableToolbar: values.enableToolbar !== false, - }; -} - -export function hostRemoteOptions( - host: - | Pick - | PluginHostRecord - | null - | undefined, -): RemoteHostOptions { - const bag = (host as RemoteHostLogin | null | undefined)?.pluginSettings; - return remoteOptions(bag?.[PLUGIN_ID]); -} - -export function protocolEnabled( - host: - | Pick - | PluginHostRecord - | null - | undefined, - protocol: Protocol, -): boolean { - return hostRemoteOptions(host)[ENABLE_KEY[protocol]]; -} - -/** Quick Connect hosts are never saved; their ids carry this prefix. */ -const QUICK_CONNECT_ID_PREFIX = "quick-connect-"; - -export function isQuickConnectHost(host: { id?: unknown } | null | undefined) { - return String(host?.id ?? "").startsWith(QUICK_CONNECT_ID_PREFIX); -} - -export function errorMessage(error: unknown, fallback = "Unknown error") { - const data = (error as { response?: { data?: { error?: unknown } } }) - ?.response?.data?.error; - if (typeof data === "string" && data) return data; - if (error instanceof Error && error.message) return error.message; - if (typeof error === "string" && error) return error; - return fallback; -} diff --git a/plugins/remote-desktop/src/frontend/index.tsx b/plugins/remote-desktop/src/frontend/index.tsx deleted file mode 100644 index 47d08a3d6..000000000 --- a/plugins/remote-desktop/src/frontend/index.tsx +++ /dev/null @@ -1,457 +0,0 @@ -import { useEffect, useState, type ComponentType, type Ref } from "react"; -import { - Info, - MessagesSquare, - Monitor, - MonitorUp, - MousePointerClick, -} from "lucide-react"; -import { - invokeAction, - useTranslation, - type HostEditorSectionProps, - type PluginHostRecord, - type StandaloneViewProps, - type TabProps, - type TermixApp, -} from "@termix/plugin-sdk/frontend"; -import { - FakeSwitch, - HostFeatureFields, - SectionCard, - SettingRow, - isElectron, -} from "@termix/plugin-sdk/ui"; -import { toast } from "sonner"; -import GuacamoleApp, { type GuacamoleAppHandle } from "./GuacamoleApp"; -import { GuacamoleDisplay } from "./GuacamoleDisplay"; -import { - HostEditorRdpTab, - HostEditorTelnetTab, - HostEditorVncTab, -} from "./HostEditorGuacamoleTabs"; -import { - getGuacamoleTokenFromHost, - getGuacdStatus, - nativeRdpAvailable, - openNativeRdp, - setRemoteDesktopApp, -} from "./guacamole-api"; -import { quickConnectGuacHost } from "./quick-connect-guac-host"; -import { REMOTE_DESKTOP_TOOLBAR_SLOT } from "./GuacamoleToolbar.tsx"; -import { - DEFAULT_PORT, - ENABLE_KEY, - PORT_KEY, - hostRemoteOptions, - isQuickConnectHost, - protocolEnabled, - type Protocol, - rdpDomain, - type RemoteHostLogin, -} from "./host-remote"; -import { remoteDesktopForm } from "./remote-form"; - -const PROTOCOLS: { - id: Protocol; - titleKey: string; - descriptionKey: string; - paletteKey: string; - icon: ComponentType<{ className?: string }>; - priority: number; - order: number; - quickConnect?: { showDomain?: boolean }; -}[] = [ - { - id: "rdp", - titleKey: "hosts.tabRdp", - descriptionKey: "hosts.remoteDesktop", - paletteKey: "palette.connectRdp", - icon: Monitor, - priority: 50, - order: 100, - quickConnect: { showDomain: true }, - }, - { - id: "vnc", - titleKey: "hosts.tabVnc", - descriptionKey: "hosts.virtualNetwork", - paletteKey: "palette.connectVnc", - icon: MousePointerClick, - priority: 40, - order: 110, - quickConnect: {}, - }, - { - id: "telnet", - titleKey: "hosts.tabTelnet", - descriptionKey: "hosts.unencryptedShell", - paletteKey: "palette.connectTelnet", - icon: MessagesSquare, - priority: 30, - order: 120, - }, -]; - -function RemoteDesktopTab({ tab, host, isVisible, handleRef }: TabProps) { - const record: RemoteHostLogin | undefined = host; - return ( - } - hostId={String(host?.id ?? "")} - tabId={tab.id} - protocol={tab.type as Protocol} - isVisible={isVisible} - quickConnectHost={ - record && isQuickConnectHost(record) - ? quickConnectGuacHost(record) - : undefined - } - /> - ); -} - -function RemoteDesktopStandalone({ hostId, view }: StandaloneViewProps) { - return ; -} - -/** What collab rooms and shared-session links draw a stream with. */ -function RemoteDisplay({ - token, - connectionOrigin, - protocol, - isVisible, - onConnect, - onError, -}: { - token: string; - connectionOrigin?: "local" | "remote"; - protocol: Protocol; - isVisible: boolean; - onConnect?: () => void; - onError?: (error: string) => void; -}) { - return ( - - ); -} - -function SectionNotes() { - const { t } = useTranslation(); - return ( - } - > -
-

{t("hosts.userDefaultsNote")}

- {isElectron() &&

{t("hosts.connectionOriginNote")}

} -
-
- ); -} - -function ToolbarCard({ - form, - setField, -}: Pick, "form" | "setField">) { - const { t } = useTranslation(); - return ( - } - > - - setField("enableToolbar", value)} - /> - - - ); -} - -/** - * The Wake-on-LAN address a host keeps, the default for a session's wake - * packet. Null while that plugin is off. - */ -function useWakeOnLanMac(hostId: string | undefined): string | null { - const [mac, setMac] = useState(null); - useEffect(() => { - let active = true; - if (!hostId) { - setMac(null); - return; - } - invokeAction("wakeOnLan.macAddress", hostId) - .then((value) => { - if (active) setMac(typeof value === "string" && value ? value : null); - }) - .catch(() => { - if (active) setMac(null); - }); - return () => { - active = false; - }; - }, [hostId]); - return mac; -} - -function RdpSection(props: HostEditorSectionProps) { - const { form, setField, setGuacField } = remoteDesktopForm(props); - const macAddress = useWakeOnLanMac(props.host?.id); - return ( - <> - - - - - ); -} - -function VncSection(props: HostEditorSectionProps) { - const { form, setField, setGuacField } = remoteDesktopForm(props); - const macAddress = useWakeOnLanMac(props.host?.id); - return ( - <> - - - - - ); -} - -function TelnetSection(props: HostEditorSectionProps) { - const { form, setField, setGuacField } = remoteDesktopForm(props); - return ( - <> - - - - - ); -} - -/** - * The defaults editor's remote desktop section: the display settings and the - * toolbar, which every host follows unless its own guacd settings say - * otherwise. A host edits these in its protocol tabs instead. - */ -function DisplayDefaultsSection(props: HostEditorSectionProps) { - const { t } = useTranslation(); - const { form, setField } = remoteDesktopForm(props); - return ( - <> - } - > - - - - - ); -} - -const SECTIONS: Record> = { - rdp: RdpSection, - vnc: VncSection, - telnet: TelnetSection, -}; - -function registerNativeRdp(app: TermixApp): void { - let disposed = false; - app.onDispose(() => { - disposed = true; - }); - void nativeRdpAvailable().then((available) => { - if (disposed || !available) return; - app.registerHostAction({ - id: "rdp-native", - titleKey: "hosts.openNativeRdp", - icon: MonitorUp, - kind: "open", - order: 105, - when: (host) => protocolEnabled(host, "rdp"), - run: (host) => { - const record: RemoteHostLogin = host; - void openNativeRdp({ - host: String(record.ip ?? ""), - port: hostRemoteOptions(record).rdpPort, - username: record.protocolAuth?.rdp?.username ?? undefined, - domain: rdpDomain(record), - }) - .then((result) => { - if (result.success) toast.success(app.t("hosts.nativeRdpOpened")); - else toast.error(result.error || app.t("hosts.nativeRdpFailed")); - }) - .catch(() => toast.error(app.t("hosts.nativeRdpFailed"))); - }, - }); - }); -} - -function registerHostSurfaces(app: TermixApp): void { - for (const protocol of PROTOCOLS) { - const when = (host: PluginHostRecord) => protocolEnabled(host, protocol.id); - - app.registerHostProtocol({ - id: protocol.id, - settingKey: ENABLE_KEY[protocol.id], - portKey: PORT_KEY[protocol.id], - defaultPort: DEFAULT_PORT[protocol.id], - titleKey: protocol.titleKey, - descriptionKey: protocol.descriptionKey, - connectionOriginNoteKey: "hosts.connectionOriginGuacamoleNote", - icon: protocol.icon, - order: protocol.order, - quickConnect: protocol.quickConnect, - }); - - app.registerHostAction({ - id: protocol.id, - titleKey: protocol.titleKey, - icon: protocol.icon, - kind: "connect", - priority: protocol.priority, - order: protocol.order, - tabType: protocol.id, - copyUrlView: protocol.id, - when, - }); - - app.registerPaletteEntry({ - id: `remote-desktop.${protocol.id}`, - titleKey: protocol.paletteKey, - icon: protocol.icon, - keywords: [protocol.id], - scope: "host", - when: (host) => !!host && when(host), - run: (shell, host) => { - if (host) shell.openTab(host, protocol.id); - }, - }); - - app.registerHostEditorSection({ - id: protocol.id, - group: "top", - titleKey: protocol.titleKey, - icon: protocol.icon, - order: protocol.order / 5, - visible: (protocols) => !!protocols[ENABLE_KEY[protocol.id]], - component: SECTIONS[protocol.id], - }); - } - - app.registerHostEditorSection({ - id: "remote-desktop-display", - group: "top", - titleKey: "settings.host.displayDefaultsTab", - icon: Monitor, - order: 40, - defaults: "only", - component: DisplayDefaultsSection, - }); - - if (isElectron()) registerNativeRdp(app); -} - -export function activate(app: TermixApp): void { - setRemoteDesktopApp(app); - app.onDispose(() => setRemoteDesktopApp(null)); - - // Guest pages only draw shared streams. - app.registerSlotContribution("session.remoteDisplay", { - actionId: "remote-desktop.display", - titleKey: "hosts.tabRdp", - kind: "component", - component: RemoteDisplay as unknown as ComponentType< - Record - >, - }); - if (app.guest) return; - - // The share button lives here, contributed by session sharing. - app.declareActionSlot({ - id: REMOTE_DESKTOP_TOOLBAR_SLOT, - accepts: ["button"], - }); - - // Mints a token for presenting a host in a collab room. - app.registerAction( - "session.remoteDisplay.token", - async (hostId: number, origin: unknown, protocol: Protocol) => { - const response = await getGuacamoleTokenFromHost( - hostId, - origin as Parameters[1], - protocol, - ); - return response.guacamoleConnectionId - ? { - token: response.token, - connectionId: response.guacamoleConnectionId, - } - : null; - }, - ); - - for (const protocol of PROTOCOLS) { - app.registerTab(protocol.id, RemoteDesktopTab, { - icon: protocol.icon, - titleKey: protocol.titleKey, - requiresHost: true, - noHostMessageKey: "remoteDesktop.noHostSelected", - persistent: true, - session: true, - restore: (host) => protocolEnabled(host, protocol.id), - activityTypes: [protocol.id], - standalone: RemoteDesktopStandalone, - preload: () => import("./GuacamoleApp"), - }); - } - - // An admin can turn Remote Desktop off; its ways in go with it. - let disposed = false; - app.onDispose(() => { - disposed = true; - }); - void getGuacdStatus("local", { probe: false }) - .then((status) => status.enabled !== false) - .catch(() => true) - .then((enabled) => { - if (!disposed && enabled) registerHostSurfaces(app); - }); - - app.registerSlotContribution("onboarding.features", { - actionId: "remote-desktop.feature", - titleKey: "onboarding.feature_desktop", - descriptionKey: "onboarding.feature_desktop_desc", - icon: Monitor, - }); -} diff --git a/plugins/remote-desktop/src/frontend/quick-connect-guac-host.ts b/plugins/remote-desktop/src/frontend/quick-connect-guac-host.ts deleted file mode 100644 index 42eda507c..000000000 --- a/plugins/remote-desktop/src/frontend/quick-connect-guac-host.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { GuacamoleQuickHost } from "./GuacamoleApp"; -import { - hostRemoteOptions, - type Protocol, - type RemoteHostLogin, -} from "./host-remote"; - -/** The slice of a quick-connect host that GuacamoleApp mints a token from. */ -export function quickConnectGuacHost( - host: RemoteHostLogin, -): GuacamoleQuickHost { - const options = hostRemoteOptions(host); - const connectionType: Protocol = options.enableVnc ? "vnc" : "rdp"; - const login = - host.quickConnectLogin?.protocol === connectionType - ? host.quickConnectLogin - : undefined; - return { - name: host.name ?? undefined, - ip: host.ip ?? "", - connectionType, - domain: login?.fields?.domain, - port: connectionType === "vnc" ? options.vncPort : options.rdpPort, - rdpAuthType: "direct", - username: login?.username, - password: login?.password, - }; -} diff --git a/plugins/remote-desktop/src/frontend/rdp-credential-prompt.ts b/plugins/remote-desktop/src/frontend/rdp-credential-prompt.ts deleted file mode 100644 index 80eb154e7..000000000 --- a/plugins/remote-desktop/src/frontend/rdp-credential-prompt.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { RemoteHostLogin } from "./host-remote"; - -export function needsRdpCredentialPrompt({ - protocol, - rdpAuthType, - authOverrides, -}: { - protocol: "rdp" | "vnc" | "telnet"; - rdpAuthType?: string; - authOverrides?: RemoteHostLogin["authOverrides"]; -}): boolean { - return ( - protocol === "rdp" && - rdpAuthType === "none" && - !authOverrides?.rdp?.credentialId - ); -} diff --git a/plugins/remote-desktop/src/frontend/remote-form.ts b/plugins/remote-desktop/src/frontend/remote-form.ts deleted file mode 100644 index ad801826a..000000000 --- a/plugins/remote-desktop/src/frontend/remote-form.ts +++ /dev/null @@ -1,183 +0,0 @@ -import type { - HostEditorSectionProps, - HostProtocolAuthForm, -} from "@termix/plugin-sdk/frontend"; -import { PLUGIN_ID, remoteOptions, type Protocol } from "./host-remote"; - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -/** - * The host editor fields the RDP, VNC and Telnet tabs edit. The logins are - * core's, under form.protocolAuth (this plugin declares the protocols in its - * manifest); the options (ports, security, guacd settings) are this plugin's - * host settings. Both are presented here under the names the tabs use. - */ -export interface RemoteDesktopForm { - domain: string; - security: string; - ignoreCert: boolean; - rdpPort: number; - vncPort: number; - telnetPort: number; - guacamoleConfig: Record; - enableToolbar: boolean; - rdpAuthType: "direct" | "credential" | "none"; - rdpCredentialId: string; - rdpUser: string; - rdpPassword: string; - vncAuthType: "direct" | "credential"; - vncCredentialId: string; - vncUser: string; - vncPassword: string; - telnetAuthType: "direct" | "credential"; - telnetCredentialId: string; - telnetUser: string; - telnetPassword: string; -} - -export type RemoteFormSetField = ( - key: K, - value: RemoteDesktopForm[K], -) => void; - -/** Tab field name to this plugin's host setting key. */ -const SETTING_FOR_FIELD: Partial> = { - rdpPort: "rdpPort", - vncPort: "vncPort", - telnetPort: "telnetPort", - security: "rdpSecurity", - ignoreCert: "rdpIgnoreCert", - enableToolbar: "enableToolbar", -}; - -type LoginPart = "authType" | "credentialId" | "username" | "password"; - -/** Tab field name to the protocol login it edits. */ -const LOGIN_FOR_FIELD: Partial< - Record -> = { - rdpAuthType: ["rdp", "authType"], - rdpCredentialId: ["rdp", "credentialId"], - rdpUser: ["rdp", "username"], - rdpPassword: ["rdp", "password"], - domain: ["rdp", "domain"], - vncAuthType: ["vnc", "authType"], - vncCredentialId: ["vnc", "credentialId"], - vncUser: ["vnc", "username"], - vncPassword: ["vnc", "password"], - telnetAuthType: ["telnet", "authType"], - telnetCredentialId: ["telnet", "credentialId"], - telnetUser: ["telnet", "username"], - telnetPassword: ["telnet", "password"], -}; - -type PluginSettingsBag = Record>; -type LoginBag = Record; - -const EMPTY_LOGIN: HostProtocolAuthForm = { - authType: "direct", - credentialId: "", - username: "", - password: "", - fields: {}, -}; - -export function loginOf( - bag: LoginBag | undefined, - protocol: Protocol, -): HostProtocolAuthForm { - const login = bag?.[protocol]; - return login ? { ...EMPTY_LOGIN, ...login } : EMPTY_LOGIN; -} - -function patchSettings( - current: Record, - patch: (settings: Record) => Record, -): Record { - const bag = (current.pluginSettings as PluginSettingsBag | undefined) ?? {}; - const mine = bag[PLUGIN_ID] ?? {}; - return { - pluginSettings: { ...bag, [PLUGIN_ID]: { ...mine, ...patch(mine) } }, - }; -} - -/** The form patch that sets one part of a protocol's login. */ -export function patchLogin( - current: Record, - protocol: Protocol, - part: LoginPart | "domain", - value: unknown, -): Record { - const bag = (current.protocolAuth as LoginBag | undefined) ?? {}; - const login = loginOf(bag, protocol); - const next: HostProtocolAuthForm = - part === "domain" - ? { ...login, fields: { ...login.fields, domain: String(value ?? "") } } - : ({ ...login, [part]: value } as HostProtocolAuthForm); - return { protocolAuth: { ...bag, [protocol]: next } }; -} - -export function remoteDesktopForm(props: HostEditorSectionProps): { - form: RemoteDesktopForm; - setField: RemoteFormSetField; - setGuacField: (key: string, value: unknown) => void; -} { - const bag = props.form?.pluginSettings as PluginSettingsBag | undefined; - const logins = props.form?.protocolAuth as LoginBag | undefined; - const options = remoteOptions(bag?.[PLUGIN_ID]); - const rdp = loginOf(logins, "rdp"); - const vnc = loginOf(logins, "vnc"); - const telnet = loginOf(logins, "telnet"); - const form: RemoteDesktopForm = { - rdpPort: options.rdpPort, - vncPort: options.vncPort, - telnetPort: options.telnetPort, - security: options.rdpSecurity, - ignoreCert: options.rdpIgnoreCert, - guacamoleConfig: options.guacamoleConfig as Record, - enableToolbar: options.enableToolbar, - domain: rdp.fields.domain ?? "", - rdpAuthType: rdp.authType, - rdpCredentialId: rdp.credentialId, - rdpUser: rdp.username, - rdpPassword: rdp.password, - vncAuthType: vnc.authType === "credential" ? "credential" : "direct", - vncCredentialId: vnc.credentialId, - vncUser: vnc.username, - vncPassword: vnc.password, - telnetAuthType: telnet.authType === "credential" ? "credential" : "direct", - telnetCredentialId: telnet.credentialId, - telnetUser: telnet.username, - telnetPassword: telnet.password, - }; - - const setField: RemoteFormSetField = (key, value) => { - const login = LOGIN_FOR_FIELD[key]; - if (login) { - props.updateForm((current) => - patchLogin(current, login[0], login[1], value), - ); - return; - } - const setting = SETTING_FOR_FIELD[key]; - if (!setting) { - props.setField(key, value); - return; - } - props.updateForm((current) => - patchSettings(current, () => ({ [setting]: value })), - ); - }; - - const setGuacField = (key: string, value: unknown) => - props.updateForm((current) => - patchSettings(current, (mine) => ({ - guacamoleConfig: { - ...remoteOptions(mine).guacamoleConfig, - [key]: value, - }, - })), - ); - - return { form, setField, setGuacField }; -} diff --git a/plugins/remote-desktop/tests/backend/drive-settings.test.ts b/plugins/remote-desktop/tests/backend/drive-settings.test.ts deleted file mode 100644 index 68a5a3fdc..000000000 --- a/plugins/remote-desktop/tests/backend/drive-settings.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { withDriveSettings } from "../../src/backend/drive-settings.js"; - -describe("withDriveSettings", () => { - it("gives each user a folder under GUACD_DRIVE_PATH and creates it", () => { - expect( - withDriveSettings({ "enable-drive": true }, "user-1", { - GUACD_DRIVE_PATH: "/termix-data/rdp-drive/", - }), - ).toEqual({ - "enable-drive": true, - "drive-path": "/termix-data/rdp-drive/user-1", - "create-drive-path": true, - }); - }); - - it("falls back to /drive when the environment says nothing", () => { - expect( - withDriveSettings({ "enable-drive": true }, "user-1", {}), - ).toMatchObject({ "drive-path": "/drive/user-1" }); - }); - - it("leaves a host-chosen drive-path alone", () => { - const config = { "enable-drive": true, "drive-path": "/mnt/share" }; - expect( - withDriveSettings(config, "user-1", { GUACD_DRIVE_PATH: "/x" }), - ).toBe(config); - }); - - it("does nothing when the drive is not enabled", () => { - const config = { "enable-drive": false }; - expect(withDriveSettings(config, "user-1")).toBe(config); - }); -}); diff --git a/plugins/remote-desktop/tests/backend/host-settings.test.ts b/plugins/remote-desktop/tests/backend/host-settings.test.ts deleted file mode 100644 index 95a427ae3..000000000 --- a/plugins/remote-desktop/tests/backend/host-settings.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - hostPayloadLegacy, - normalizeImportedHost, - userDefaultParams, -} from "../../src/backend/host-settings.js"; - -describe("normalizeImportedHost", () => { - it("reads a Termix export's pluginSettings", () => { - expect( - normalizeImportedHost({ - pluginSettings: { - "remote-desktop": { - enableVnc: true, - vncPort: 5901, - guacamoleConfig: { cursor: "local" }, - }, - }, - }), - ).toEqual({ - enableVnc: true, - vncPort: 5901, - guacamoleConfig: { cursor: "local" }, - }); - }); - - it("reads the flat fields hosts had before 2.9.0", () => { - expect( - normalizeImportedHost({ - enableRdp: true, - rdpPort: "3390", - security: "nla", - ignoreCert: 1, - guacamoleConfig: JSON.stringify({ dpi: 120 }), - enableTerminalToolbar: false, - }), - ).toEqual({ - enableRdp: true, - rdpPort: 3390, - rdpSecurity: "nla", - rdpIgnoreCert: true, - guacamoleConfig: { dpi: 120 }, - enableToolbar: false, - }); - }); - - it("turns a connectionType-only host into its switch and port", () => { - expect( - normalizeImportedHost({ connectionType: "telnet", port: 2323 }), - ).toEqual({ enableTelnet: true, telnetPort: 2323 }); - }); - - it("writes nothing for a plain SSH row", () => { - expect( - normalizeImportedHost({ connectionType: "ssh", port: 22 }), - ).toBeNull(); - }); - - it("drops a port out of range", () => { - expect(normalizeImportedHost({ enableRdp: true, rdpPort: 70000 })).toEqual({ - enableRdp: true, - }); - }); -}); - -describe("userDefaultParams", () => { - it("maps the user settings onto guacd parameters, skipping inherit", () => { - expect( - userDefaultParams({ - colorDepth: "24", - resizeMethod: "reconnect", - disableCopy: "on", - enableWallpaper: "off", - enableDrive: "inherit", - }), - ).toEqual({ - "color-depth": 24, - "resize-method": "reconnect", - "disable-copy": true, - "enable-wallpaper": false, - }); - }); -}); - -describe("hostPayloadLegacy", () => { - it("puts each login back in its 2.8 flat shape for older clients", () => { - expect( - hostPayloadLegacy( - {}, - { - protocolAuth: { - rdp: { - authType: "direct", - credentialId: null, - username: "admin", - fields: { domain: "CORP" }, - hasPassword: true, - }, - vnc: { authType: "credential", credentialId: 4 }, - }, - }, - ), - ).toEqual({ - rdpAuthType: "direct", - rdpCredentialId: null, - rdpUser: "admin", - hasRdpPassword: true, - rdpDomain: "CORP", - domain: "CORP", - vncAuthType: "credential", - vncCredentialId: 4, - }); - }); - - it("adds nothing for a host without logins", () => { - expect(hostPayloadLegacy({}, {})).toBeNull(); - }); -}); diff --git a/plugins/remote-desktop/tests/backend/jump-tunnel-endpoint.test.ts b/plugins/remote-desktop/tests/backend/jump-tunnel-endpoint.test.ts deleted file mode 100644 index af017f402..000000000 --- a/plugins/remote-desktop/tests/backend/jump-tunnel-endpoint.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { resolveJumpTunnelEndpoint } from "../../src/backend/jump-tunnel-endpoint.js"; - -describe("resolveJumpTunnelEndpoint", () => { - it("keeps an in-process guacd tunnel on loopback", () => { - expect(resolveJumpTunnelEndpoint("localhost")).toEqual({ - bindHost: "127.0.0.1", - advertisedHost: "127.0.0.1", - }); - }); - - it("exposes the tunnel to a separate guacd container", () => { - expect(resolveJumpTunnelEndpoint("guacd")).toEqual({ - bindHost: "0.0.0.0", - advertisedHost: "termix", - }); - }); - - it("supports a custom backend hostname for external guacd", () => { - expect( - resolveJumpTunnelEndpoint("guacd.example", "termix-backend"), - ).toEqual({ - bindHost: "0.0.0.0", - advertisedHost: "termix-backend", - }); - }); -}); diff --git a/plugins/remote-desktop/tests/backend/macos-vnc-proxy.test.ts b/plugins/remote-desktop/tests/backend/macos-vnc-proxy.test.ts deleted file mode 100644 index 6e097a6f0..000000000 --- a/plugins/remote-desktop/tests/backend/macos-vnc-proxy.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import net from "net"; -import { afterEach, describe, expect, it } from "vitest"; -import { createMacosVncCompatibilityProxy } from "../../src/backend/macos-vnc-proxy.js"; - -const closers: Array<() => void> = []; - -afterEach(() => { - for (const close of closers.splice(0)) close(); -}); - -async function listen(server: net.Server): Promise { - return new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", () => { - resolve((server.address() as net.AddressInfo).port); - }); - }); -} - -async function read(socket: net.Socket, length: number): Promise { - return new Promise((resolve, reject) => { - let received = Buffer.alloc(0); - const onData = (chunk: Buffer) => { - received = Buffer.concat([received, chunk]); - if (received.length < length) return; - socket.off("data", onData); - resolve(received); - }; - socket.on("data", onData); - socket.once("error", reject); - }); -} - -describe("createMacosVncCompatibilityProxy", () => { - it("normalizes Apple's private RFB banner and forces classic VNC auth", async () => { - let clientBanner = ""; - let selectedSecurityType = 0; - const target = net.createServer((socket) => { - socket.write("RFB 003."); - socket.write("889\n"); - socket.once("data", (data) => { - clientBanner = data.toString("ascii"); - socket.write(Buffer.from([5, 30, 33])); - socket.write(Buffer.from([36, 2, 35])); - socket.once("data", (selection) => { - selectedSecurityType = Number(selection[0]); - socket.write("desktop-data"); - }); - }); - }); - const targetPort = await listen(target); - closers.push(() => target.close()); - - const proxy = await createMacosVncCompatibilityProxy({ - targetHost: "127.0.0.1", - targetPort, - bindHost: "127.0.0.1", - }); - closers.push(proxy.close); - - const client = net.createConnection(proxy.port, "127.0.0.1"); - closers.push(() => client.destroy()); - expect((await read(client, 12)).subarray(0, 12).toString("ascii")).toBe( - "RFB 003.008\n", - ); - client.write("RFB 003.008\n"); - expect([...(await read(client, 2))]).toEqual([1, 2]); - client.write(Buffer.from([2])); - expect((await read(client, 12)).toString("ascii")).toBe("desktop-data"); - expect(clientBanner).toBe("RFB 003.008\n"); - expect(selectedSecurityType).toBe(2); - }); - - it("passes standard RFB negotiation through unchanged", async () => { - const target = net.createServer((socket) => { - socket.write("RFB 003.008\n"); - socket.once("data", () => socket.write(Buffer.from([2, 30, 2]))); - }); - const targetPort = await listen(target); - closers.push(() => target.close()); - const proxy = await createMacosVncCompatibilityProxy({ - targetHost: "127.0.0.1", - targetPort, - bindHost: "127.0.0.1", - }); - closers.push(proxy.close); - - const client = net.createConnection(proxy.port, "127.0.0.1"); - closers.push(() => client.destroy()); - expect((await read(client, 12)).toString("ascii")).toBe("RFB 003.008\n"); - client.write("RFB 003.008\n"); - expect([...(await read(client, 3))]).toEqual([2, 30, 2]); - }); - - it("passes Apple's security types through when VNC auth is unavailable", async () => { - const target = net.createServer((socket) => { - socket.write("RFB 003.889\n"); - socket.once("data", () => socket.write(Buffer.from([2, 30, 33]))); - }); - const targetPort = await listen(target); - closers.push(() => target.close()); - const proxy = await createMacosVncCompatibilityProxy({ - targetHost: "127.0.0.1", - targetPort, - bindHost: "127.0.0.1", - }); - closers.push(proxy.close); - - const client = net.createConnection(proxy.port, "127.0.0.1"); - closers.push(() => client.destroy()); - expect((await read(client, 12)).toString("ascii")).toBe("RFB 003.008\n"); - client.write("RFB 003.008\n"); - expect([...(await read(client, 3))]).toEqual([2, 30, 33]); - }); -}); diff --git a/plugins/remote-desktop/tests/backend/rdp-settings.test.ts b/plugins/remote-desktop/tests/backend/rdp-settings.test.ts deleted file mode 100644 index 2738c78d9..000000000 --- a/plugins/remote-desktop/tests/backend/rdp-settings.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - buildRdpSettings, - resolveRdpDomain, -} from "../../src/backend/rdp-settings.js"; - -describe("buildRdpSettings", () => { - it("keeps saved RDP settings authoritative over stale advanced config", () => { - expect( - buildRdpSettings({ - port: 3390, - domain: "EXAMPLE", - security: "nla", - ignoreCert: true, - guacConfig: { - port: 3389, - domain: "OLD", - security: "rdp", - "ignore-cert": false, - "color-depth": 24, - }, - guacdOverrides: { guacdHost: "guacd.internal" }, - }), - ).toEqual({ - port: 3390, - domain: "EXAMPLE", - security: "nla", - "ignore-cert": true, - "color-depth": 24, - guacdHost: "guacd.internal", - }); - }); - - it("preserves an advanced security value when no saved value exists", () => { - expect( - buildRdpSettings({ - port: 3389, - ignoreCert: false, - guacConfig: { security: "tls" }, - guacdOverrides: {}, - }).security, - ).toBe("tls"); - }); - - it("uses the prompted domain for prompt-on-connect authentication", () => { - expect(resolveRdpDomain("none", "EXAMPLE", "OLD")).toBe("EXAMPLE"); - expect(resolveRdpDomain("none", "", "OLD")).toBe(""); - }); - - it("keeps the stored domain for saved authentication", () => { - expect(resolveRdpDomain("direct", "EXAMPLE", "SAVED")).toBe("SAVED"); - expect(resolveRdpDomain("none", undefined, "SAVED")).toBe("SAVED"); - }); -}); diff --git a/plugins/remote-desktop/tests/backend/recording-settings.test.ts b/plugins/remote-desktop/tests/backend/recording-settings.test.ts deleted file mode 100644 index 8504e5f75..000000000 --- a/plugins/remote-desktop/tests/backend/recording-settings.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { withRecordingSettings } from "../../src/backend/recording-settings.js"; - -const PATH = "/app/data/session_recordings/guacamole"; -const NAME = "b7e6c0f2-0000-4000-8000-000000000000.guac"; - -describe("withRecordingSettings", () => { - it("takes ownership of the location and filename", () => { - const merged = withRecordingSettings( - { - "recording-path": "/var/lib/termix/recordings", - "recording-name": "${GUAC_USERNAME}-${GUAC_DATE}", - "create-recording-path": false, - }, - PATH, - NAME, - ); - - expect(merged).toMatchObject({ - "recording-path": PATH, - "recording-name": NAME, - "create-recording-path": true, - }); - }); - - it("defaults the content flags when the host has no opinion", () => { - expect(withRecordingSettings({}, PATH, NAME)).toMatchObject({ - "recording-exclude-output": false, - "recording-include-keys": true, - }); - }); - - it("keeps the host's content flags, including the falsy ones", () => { - const merged = withRecordingSettings( - { - "recording-exclude-output": true, - "recording-include-keys": false, - }, - PATH, - NAME, - ); - - expect(merged).toMatchObject({ - "recording-exclude-output": true, - "recording-include-keys": false, - }); - }); - - it("leaves unrelated settings alone", () => { - const merged = withRecordingSettings( - { "recording-exclude-mouse": true, width: "1920" }, - PATH, - NAME, - ); - - expect(merged).toMatchObject({ - "recording-exclude-mouse": true, - width: "1920", - }); - }); - - it("does not mutate the settings it was given", () => { - const original = { "recording-path": "/tmp/mine" }; - withRecordingSettings(original, PATH, NAME); - - expect(original).toEqual({ "recording-path": "/tmp/mine" }); - }); -}); diff --git a/plugins/remote-desktop/tests/backend/routes.test.ts b/plugins/remote-desktop/tests/backend/routes.test.ts deleted file mode 100644 index c7571bca9..000000000 --- a/plugins/remote-desktop/tests/backend/routes.test.ts +++ /dev/null @@ -1,420 +0,0 @@ -import http from "node:http"; -import net, { type AddressInfo } from "node:net"; -import express, { type Router } from "express"; -import { afterEach, describe, expect, it } from "vitest"; -import { - createMockCtx, - type MockPluginContext, -} from "@termix/plugin-sdk/testing"; -import type { - PluginContext, - PluginProtocolTarget, -} from "@termix/plugin-sdk/backend"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import manifestJson from "../../manifest.json"; -import { activate } from "../../src/backend/index.js"; -import { GuacamoleTokenService } from "../../src/backend/token-service.js"; - -const manifest = manifestJson as unknown as PluginManifest; - -// Without a fixed key each token service picks its own. -process.env.GUACAMOLE_ENCRYPTION_KEY = "ab".repeat(32); - -function target( - overrides: Partial = {}, -): PluginProtocolTarget { - return { - host: { - id: 7, - name: "Win box", - ip: "10.0.0.7", - port: 22, - ownerUserId: "user-1", - jumpHosts: [], - }, - shared: false, - auth: { - authType: "direct", - username: "admin", - password: "secret", - fields: { domain: "CORP" }, - ...overrides, - }, - }; -} - -interface Server { - mock: MockPluginContext; - request: ( - method: string, - path: string, - body?: unknown, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ) => Promise<{ status: number; body: any }>; - close: () => Promise; -} - -let server: Server | null = null; - -afterEach(async () => { - await server?.close(); - server = null; -}); - -async function start( - options: { - protocolTargets?: Record; - services?: Record; - capabilities?: string[]; - } = {}, -): Promise { - let router: Router | null = null; - const mock = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: options.capabilities ?? manifest.capabilities, - router: () => (router = express.Router()), - protocolTargets: options.protocolTargets, - services: options.services, - }); - await activate(mock.ctx as PluginContext); - - const app = express(); - app.use(express.json()); - app.use((req, _res, next) => { - mock.setActor(req.header("x-test-user") ?? "user-1"); - next(); - }); - app.use((req, res, next) => router!(req, res, next)); - const httpServer = http.createServer(app); - await new Promise((resolve) => httpServer.listen(0, resolve)); - const { port } = httpServer.address() as AddressInfo; - - return { - mock, - async request(method, path, body) { - const response = await fetch(`http://127.0.0.1:${port}${path}`, { - method, - headers: body ? { "content-type": "application/json" } : {}, - body: body ? JSON.stringify(body) : undefined, - }); - const text = await response.text(); - return { status: response.status, body: text ? JSON.parse(text) : null }; - }, - async close() { - await new Promise((resolve) => httpServer.close(() => resolve())); - for (const dispose of [...mock.disposals].reverse()) await dispose(); - }, - }; -} - -const tokens = new GuacamoleTokenService(); - -describe("POST /connect-host/:hostId", () => { - it("mints a token from the resolved login and the host's own settings", async () => { - server = await start({ protocolTargets: { "7:rdp": target() } }); - await server.mock.ctx.settings.setHost(7, "enableRdp", true); - await server.mock.ctx.settings.setHost(7, "rdpPort", 3390); - await server.mock.ctx.settings.setHost(7, "rdpSecurity", "nla"); - await server.mock.ctx.settings.setHost(7, "guacamoleConfig", { - "color-depth": 16, - "enable-wallpaper": "auto", - }); - - const response = await server.request("POST", "/connect-host/7", { - protocol: "rdp", - }); - - expect(response.status).toBe(200); - expect(response.body.termixConnectId).toEqual(expect.any(String)); - const token = tokens.decryptToken(response.body.token); - expect(token?.connection.settings).toMatchObject({ - hostname: "10.0.0.7", - port: 3390, - username: "admin", - password: "secret", - domain: "CORP", - security: "nla", - "color-depth": 16, - }); - expect(token?.connection.settings).not.toHaveProperty("enable-wallpaper"); - expect(token?.termixMeta).toMatchObject({ - hostId: 7, - hostName: "Win box", - protocol: "rdp", - }); - expect(server.mock.audits.at(-1)).toMatchObject({ - action: "rdp_connect", - success: true, - }); - }); - - it("puts the host's display settings under its own guacd settings", async () => { - server = await start({ protocolTargets: { "7:rdp": target() } }); - await server.mock.ctx.settings.setHost(7, "enableRdp", true); - await server.mock.ctx.settings.setHost(7, "guacamoleConfig", { - "color-depth": 32, - }); - // What the host follows from its defaults. - await server.mock.ctx.settings.setHost(7, "colorDepth", "16"); - await server.mock.ctx.settings.setHost(7, "disableCopy", "on"); - - const response = await server.request("POST", "/connect-host/7", { - protocol: "rdp", - }); - const settings = tokens.decryptToken(response.body.token)?.connection - .settings; - expect(settings?.["color-depth"]).toBe(32); - expect(settings?.["disable-copy"]).toBe(true); - }); - - it("uses the prompted login when the host asks at connect time", async () => { - server = await start({ - protocolTargets: { "7:rdp": target({ authType: "none", username: "" }) }, - }); - await server.mock.ctx.settings.setHost(7, "enableRdp", true); - - const response = await server.request("POST", "/connect-host/7", { - protocol: "rdp", - promptedUsername: "bob", - promptedPassword: "pw", - promptedDomain: "", - }); - const settings = tokens.decryptToken(response.body.token)?.connection - .settings; - expect(settings).toMatchObject({ username: "bob", password: "pw" }); - }); - - it("refuses a protocol the host has switched off", async () => { - server = await start({ protocolTargets: { "7:vnc": target() } }); - const response = await server.request("POST", "/connect-host/7", { - protocol: "vnc", - }); - expect(response.status).toBe(400); - }); - - it("answers 404 when the caller cannot reach the host", async () => { - server = await start(); - await server.mock.ctx.settings.setHost(7, "enableRdp", true); - const response = await server.request("POST", "/connect-host/7", { - protocol: "rdp", - }); - expect(response.status).toBe(404); - }); - - it("refuses everything while an admin has turned it off", async () => { - server = await start({ protocolTargets: { "7:rdp": target() } }); - await server.mock.ctx.settings.set("enabled", false); - await server.mock.ctx.settings.setHost(7, "enableRdp", true); - - expect( - (await server.request("POST", "/connect-host/7", { protocol: "rdp" })) - .status, - ).toBe(403); - expect( - ( - await server.request("POST", "/token", { - type: "rdp", - hostname: "10.0.0.9", - }) - ).status, - ).toBe(403); - const status = await server.request("GET", "/status?probe=0"); - expect(status.body.enabled).toBe(false); - }); - - it("records only when session recording is on for the host", async () => { - const enabledFor = async (hostId: number) => hostId === 7; - server = await start({ - protocolTargets: { "7:rdp": target() }, - services: { - "recordings.writer": { - enabledFor, - createFinished: async () => ({ id: 1 }), - }, - }, - }); - await server.mock.ctx.settings.setHost(7, "enableRdp", true); - const response = await server.request("POST", "/connect-host/7", { - protocol: "rdp", - }); - const token = tokens.decryptToken(response.body.token); - expect(token?.recording).toMatchObject({ hostId: 7, protocol: "rdp" }); - expect(token?.connection.settings["recording-name"]).toEqual( - expect.any(String), - ); - }); -}); - -describe("the rest of the routes", () => { - it("mints a quick connect token with the caller's defaults", async () => { - server = await start(); - await server.mock.ctx.settings.setUser("user-1", "colorDepth", "24"); - const response = await server.request("POST", "/token", { - type: "rdp", - hostname: "10.0.0.9", - username: "u", - password: "p", - }); - expect(response.status).toBe(200); - expect( - tokens.decryptToken(response.body.token)?.connection.settings, - ).toMatchObject({ hostname: "10.0.0.9", port: 3389, "color-depth": 24 }); - }); - - it("ignores guacd and path settings a quick connect tries to set", async () => { - server = await start(); - const response = await server.request("POST", "/token", { - type: "rdp", - hostname: "10.0.0.9", - guacdHost: "attacker.example", - guacdPort: 4822, - "drive-path": "/etc", - "recording-path": "/tmp/x", - width: 800, - }); - expect(response.status).toBe(200); - const token = tokens.decryptToken(response.body.token); - expect(token?.connection.guacdHost).toBeUndefined(); - expect(token?.connection.guacdPort).toBeUndefined(); - expect(token?.connection.settings["drive-path"]).toBeUndefined(); - expect(token?.connection.settings["recording-path"]).toBeUndefined(); - expect(token?.connection.settings.width).toBe(800); - }); - - it("offers native RDP only in the desktop app", async () => { - server = await start(); - expect((await server.request("GET", "/native-rdp")).body).toEqual({ - available: false, - }); - expect( - (await server.request("POST", "/native-rdp", { host: "10.0.0.7" })) - .status, - ).toBe(404); - }); - - it("does not hand out another user's session id", async () => { - server = await start(); - const response = await server.request("GET", "/connection/unknown"); - expect(response.body).toEqual({ guacamoleConnectionId: null }); - }); -}); - -describe("activate", () => { - it("fails closed without network:serve", async () => { - await expect( - start({ - capabilities: manifest.capabilities.filter( - (capability) => capability !== "network:serve", - ), - }), - ).rejects.toThrow(/network:serve/); - }); - - it("cannot read a host's login without credentials:read", async () => { - server = await start({ - protocolTargets: { "7:rdp": target() }, - capabilities: manifest.capabilities.filter( - (capability) => capability !== "credentials:read", - ), - }); - await server.mock.ctx.settings.setHost(7, "enableRdp", true); - const response = await server.request("POST", "/connect-host/7", { - protocol: "rdp", - }); - expect(response.status).toBe(500); - expect(server.mock.checked).toContain("credentials:read"); - }); - - it("provides sessions.live for rdp, vnc and telnet", async () => { - server = await start(); - expect([...server.mock.services.keys()].sort()).toEqual( - expect.arrayContaining([ - "sessions.live#rdp", - "sessions.live#telnet", - "sessions.live#vnc", - ]), - ); - }); -}); - -describe("saved host guacd status", () => { - it("probes the same override used by the connection token", async () => { - const guacd = net.createServer((socket) => socket.end()); - await new Promise((resolve) => guacd.listen(0, "127.0.0.1", resolve)); - try { - const port = (guacd.address() as AddressInfo).port; - server = await start({ protocolTargets: { "7:rdp": target() } }); - await server.mock.ctx.settings.setHost(7, "enableRdp", true); - await server.mock.ctx.settings.setHost(7, "guacamoleConfig", { - "guacd-hostname": "127.0.0.1", - "guacd-port": String(port), - }); - const status = await server.request( - "GET", - "/status?hostId=7&protocol=rdp", - ); - expect(status.status).toBe(200); - expect(status.body.guacd).toEqual({ - host: "127.0.0.1", - port, - status: "connected", - }); - const result = await server.request("POST", "/connect-host/7", { - protocol: "rdp", - }); - expect(tokens.decryptToken(result.body.token)?.connection).toMatchObject({ - guacdHost: status.body.guacd.host, - guacdPort: status.body.guacd.port, - }); - } finally { - await new Promise((resolve) => guacd.close(() => resolve())); - } - }); - - it("keeps the global port for a host-only override and global config when absent", async () => { - server = await start({ protocolTargets: { "7:rdp": target() } }); - await server.mock.ctx.settings.setHost(7, "enableRdp", true); - const global = await server.request("GET", "/status?probe=0"); - const inherited = await server.request( - "GET", - "/status?probe=0&hostId=7&protocol=rdp", - ); - expect(inherited.body.guacd).toEqual(global.body.guacd); - await server.mock.ctx.settings.setHost(7, "guacamoleConfig", { - "guacd-hostname": "custom.example", - }); - const overridden = await server.request( - "GET", - "/status?probe=0&hostId=7&protocol=rdp", - ); - expect(overridden.body.guacd).toEqual({ - ...global.body.guacd, - host: "custom.example", - }); - }); - - it("refuses a host the caller cannot resolve", async () => { - server = await start(); - await server.mock.ctx.settings.setHost(7, "enableRdp", true); - const result = await server.request("GET", "/status?hostId=7&protocol=rdp"); - expect(result.status).toBe(404); - expect(result.body.guacd).toBeUndefined(); - }); - - it("refuses disabled protocols", async () => { - server = await start({ protocolTargets: { "7:rdp": target() } }); - const result = await server.request("GET", "/status?hostId=7&protocol=rdp"); - expect(result.status).toBe(400); - }); - - it.each([ - "hostId=bad&protocol=rdp", - "hostId=0&protocol=rdp", - "hostId=7&protocol=ssh", - "hostId=7", - ])("rejects invalid queries: %s", async (query) => { - server = await start(); - expect((await server.request("GET", `/status?${query}`)).status).toBe(400); - }); -}); diff --git a/plugins/remote-desktop/tests/backend/sessions.test.ts b/plugins/remote-desktop/tests/backend/sessions.test.ts deleted file mode 100644 index 1a9716e90..000000000 --- a/plugins/remote-desktop/tests/backend/sessions.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { RemoteSessions } from "../../src/backend/sessions.js"; -import { createLiveSessions } from "../../src/backend/live-sessions.js"; -import { GuacamoleTokenService } from "../../src/backend/token-service.js"; - -const meta = { - termixConnectId: "c1", - hostId: 7, - hostName: "Win box", - ownerUserId: "user-1", - protocol: "rdp" as const, - tabInstanceId: "tab-1", -}; - -afterEach(() => { - vi.useRealTimers(); -}); - -describe("RemoteSessions", () => { - it("moves parked resources onto the session and releases them on close", () => { - const sessions = new RemoteSessions(); - const tunnel = vi.fn(); - const tracking = vi.fn(); - sessions.park("c1", [tunnel]); - sessions.opened(meta, "g1", [tracking]); - - expect(sessions.byConnect("c1")?.guacamoleConnectionId).toBe("g1"); - expect(sessions.byGuacamole("g1")?.hostName).toBe("Win box"); - expect(tunnel).not.toHaveBeenCalled(); - - sessions.closed("c1"); - expect(tunnel).toHaveBeenCalledOnce(); - expect(tracking).toHaveBeenCalledOnce(); - expect(sessions.byGuacamole("g1")).toBeNull(); - }); - - it.each(["vnc", "rdp"] as const)( - "keeps an opened %s session's proxy and jump tunnel alive beyond one hour", - (protocol) => { - vi.useFakeTimers(); - const sessions = new RemoteSessions(); - const proxy = vi.fn(); - const tunnel = vi.fn(); - sessions.park("c1", [proxy, tunnel]); - sessions.opened({ ...meta, protocol }, "g1"); - vi.advanceTimersByTime(2 * 60 * 60 * 1000); - expect(proxy).not.toHaveBeenCalled(); - expect(tunnel).not.toHaveBeenCalled(); - expect(sessions.byConnect("c1")?.guacamoleConnectionId).toBe("g1"); - sessions.closed("c1"); - sessions.closed("c1"); - expect(proxy).toHaveBeenCalledOnce(); - expect(tunnel).toHaveBeenCalledOnce(); - }, - ); - - it("releases a tunnel guacd never used", () => { - vi.useFakeTimers(); - const sessions = new RemoteSessions(1000); - const tunnel = vi.fn(); - sessions.park("c1", [tunnel]); - vi.advanceTimersByTime(1000); - expect(tunnel).toHaveBeenCalledOnce(); - }); - - it("releases everything on clear, and one failing close does not stop the rest", () => { - const sessions = new RemoteSessions(); - const failing = vi.fn(() => { - throw new Error("already closed"); - }); - const pending = vi.fn(); - const open = vi.fn(); - sessions.park("c2", [failing, pending]); - sessions.park("c1", [open]); - sessions.opened(meta, "g1"); - sessions.clear(); - expect(pending).toHaveBeenCalledOnce(); - expect(open).toHaveBeenCalledOnce(); - }); -}); - -describe("sessions.live providers", () => { - const tokens = new GuacamoleTokenService(); - - it("describes a live session of its own protocol only", () => { - const sessions = new RemoteSessions(); - sessions.opened(meta, "g1"); - const deps = { sessions, tokens, endSession: vi.fn(() => true) }; - const rdp = createLiveSessions("rdp", deps); - const vnc = createLiveSessions("vnc", deps); - - expect(rdp.getSession("g1")).toMatchObject({ - id: "g1", - userId: "user-1", - hostId: 7, - hostName: "Win box", - isConnected: true, - tabInstanceId: "tab-1", - }); - expect(vnc.getSession("g1")).toBeNull(); - }); - - it("mints a join token for a viewer and refuses an unknown session", () => { - const sessions = new RemoteSessions(); - sessions.opened(meta, "g1"); - const rdp = createLiveSessions("rdp", { - sessions, - tokens, - endSession: vi.fn(() => true), - }); - - const token = tokens.decryptToken(rdp.createViewerToken("g1", true)); - expect(token?.connection).toMatchObject({ join: "g1", readOnly: true }); - expect(token?.recording).toBeUndefined(); - expect(() => rdp.createViewerToken("nope", false)).toThrow(); - }); - - it("ends the session for its owner", () => { - const sessions = new RemoteSessions(); - sessions.opened(meta, "g1"); - const endSession = vi.fn(() => true); - const rdp = createLiveSessions("rdp", { sessions, tokens, endSession }); - rdp.ownerEndSession("g1", "owner ended"); - expect(endSession).toHaveBeenCalledWith("g1"); - }); -}); diff --git a/plugins/remote-desktop/tests/backend/token-security.test.ts b/plugins/remote-desktop/tests/backend/token-security.test.ts deleted file mode 100644 index 75e86bde2..000000000 --- a/plugins/remote-desktop/tests/backend/token-security.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * A display token is the only credential on the public /display socket, so - * it has to be one this server minted, unedited and recent. - */ - -import { describe, expect, it } from "vitest"; -import { - GuacamoleTokenService, - TOKEN_TTL_MS, - isServerOwnedSetting, -} from "../../src/backend/token-service.js"; - -process.env.GUACAMOLE_ENCRYPTION_KEY = "ab".repeat(32); - -function decode(token: string) { - return JSON.parse(Buffer.from(token, "base64").toString("utf8")) as { - iv: string; - value: string; - mac: string; - }; -} - -function encode(data: unknown): string { - return Buffer.from(JSON.stringify(data)).toString("base64"); -} - -describe("verifyToken", () => { - it("accepts a fresh token it minted", () => { - const tokens = new GuacamoleTokenService(); - const token = tokens.createRdpToken("10.0.0.7", "admin", "secret"); - expect(tokens.verifyToken(token)?.connection.settings.hostname).toBe( - "10.0.0.7", - ); - }); - - it("refuses an edited ciphertext or IV", () => { - const tokens = new GuacamoleTokenService(); - const data = decode(tokens.createRdpToken("10.0.0.7", "admin", "secret")); - const iv = Buffer.from(data.iv, "base64"); - iv[0] ^= 0xff; - expect( - tokens.verifyToken(encode({ ...data, iv: iv.toString("base64") })), - ).toBeNull(); - const ciphertext = Buffer.from(data.value, "base64"); - ciphertext[0] ^= 0xff; - expect( - tokens.verifyToken( - encode({ ...data, value: ciphertext.toString("base64") }), - ), - ).toBeNull(); - }); - - it("refuses a token with no MAC, as guacamole-lite alone would accept", () => { - const tokens = new GuacamoleTokenService(); - const { iv, value } = decode(tokens.createRdpToken("h", "u", "p")); - expect(tokens.verifyToken(encode({ iv, value }))).toBeNull(); - }); - - it("refuses a token once it is older than the TTL", () => { - let now = 1_000_000; - const tokens = new GuacamoleTokenService(undefined, () => now); - const token = tokens.createJoinToken("session-1", true); - now += TOKEN_TTL_MS - 1; - expect(tokens.verifyToken(token)).not.toBeNull(); - now += 2; - expect(tokens.verifyToken(token)).toBeNull(); - }); - - it("refuses garbage", () => { - const tokens = new GuacamoleTokenService(); - expect(tokens.verifyToken("not-a-token")).toBeNull(); - expect(tokens.verifyToken(encode({ iv: 1 }))).toBeNull(); - }); -}); - -describe("isServerOwnedSetting", () => { - it.each([ - "guacdHost", - "guacdPort", - "drive-path", - "create-drive-path", - "recording-path", - "recording-name", - "create-recording-path", - "typescript-path", - "sftp-private-key-path", - ])("keeps %s for the server", (key) => { - expect(isServerOwnedSetting(key)).toBe(true); - }); - - it.each(["width", "security", "ignore-cert", "color-depth", "enable-drive"])( - "lets a caller set %s", - (key) => { - expect(isServerOwnedSetting(key)).toBe(false); - }, - ); -}); diff --git a/plugins/remote-desktop/tests/backend/token-service.test.ts b/plugins/remote-desktop/tests/backend/token-service.test.ts deleted file mode 100644 index ffb6d3511..000000000 --- a/plugins/remote-desktop/tests/backend/token-service.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { GuacamoleTokenService } from "../../src/backend/token-service.js"; - -describe("GuacamoleTokenService", () => { - const tokenService = new GuacamoleTokenService(); - - it("disables RDP pre-authentication when no credentials are configured", () => { - const token = tokenService.createRdpToken("windows.example.test", "", ""); - const decrypted = tokenService.decryptToken(token); - - expect(decrypted?.connection.settings).toMatchObject({ - hostname: "windows.example.test", - port: 3389, - "ignore-cert": true, - "disable-auth": true, - }); - expect(decrypted?.connection.settings.username).toBeUndefined(); - expect(decrypted?.connection.settings.password).toBeUndefined(); - }); - - it("keeps normal RDP credential authentication unchanged", () => { - const token = tokenService.createRdpToken( - "windows.example.test", - "Administrator", - "secret", - ); - const decrypted = tokenService.decryptToken(token); - - expect(decrypted?.connection.settings).toMatchObject({ - hostname: "windows.example.test", - username: "Administrator", - password: "secret", - port: 3389, - }); - expect(decrypted?.connection.settings["disable-auth"]).toBeUndefined(); - }); - - it("preserves recording metadata outside guacd connection settings", () => { - const recording = { - hostId: 42, - userId: "user-1", - protocol: "vnc" as const, - path: "recording.guac", - startedAt: "2026-07-14T00:00:00.000Z", - }; - const token = tokenService.createVncToken( - "vnc.example.test", - "user", - "secret", - {}, - recording, - ); - - expect(tokenService.decryptToken(token)?.recording).toEqual(recording); - }); - - it("preserves termixMeta through the encrypt/decrypt round trip", () => { - const termixMeta = { - termixConnectId: "connect-1", - hostId: 7, - hostName: "windows", - ownerUserId: "user-1", - protocol: "rdp" as const, - tabInstanceId: null, - }; - const token = tokenService.createRdpToken( - "windows.example.test", - "Administrator", - "secret", - {}, - undefined, - termixMeta, - ); - - expect(tokenService.decryptToken(token)?.termixMeta).toEqual(termixMeta); - }); - - it("createJoinToken sets connection.join, not connection.type", () => { - const token = tokenService.createJoinToken("guacd-conn-123", true); - const decrypted = tokenService.decryptToken(token); - - expect(decrypted?.connection.join).toBe("guacd-conn-123"); - expect(decrypted?.connection.type).toBeUndefined(); - expect(decrypted?.connection.readOnly).toBe(true); - }); - - it("createJoinToken round-trips a read-write join through decryptToken", () => { - const token = tokenService.createJoinToken("guacd-conn-456", false); - const decrypted = tokenService.decryptToken(token); - - expect(decrypted?.connection.join).toBe("guacd-conn-456"); - expect(decrypted?.connection.readOnly).toBe(false); - expect(decrypted?.recording).toBeUndefined(); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/GuacamoleToolbar.test.tsx b/plugins/remote-desktop/tests/frontend/GuacamoleToolbar.test.tsx deleted file mode 100644 index 06e265d30..000000000 --- a/plugins/remote-desktop/tests/frontend/GuacamoleToolbar.test.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React from "react"; -import { fireEvent, render } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; -import { GuacamoleToolbar } from "../../src/frontend/GuacamoleToolbar.js"; -import type { GuacamoleDisplayHandle } from "../../src/frontend/GuacamoleDisplay.js"; - -vi.mock("react-i18next", async (importOriginal) => ({ - ...(await importOriginal()), - useTranslation: () => ({ t: (key: string) => key }), -})); - -describe("GuacamoleToolbar Windows key", () => { - it("sends Super_L for Win shortcuts and the sticky modifier", () => { - const sendKey = vi.fn(); - const displayRef = { - current: { - disconnect: vi.fn(), - isConnected: () => true, - sendKey, - sendMouse: vi.fn(), - setClipboard: vi.fn(), - getFilesystem: () => null, - uploadFile: async () => {}, - zoomIn: vi.fn(() => 1.25), - zoomOut: vi.fn(() => 0.75), - resetZoom: vi.fn(() => 1), - } satisfies GuacamoleDisplayHandle, - } as React.RefObject; - const { getByText } = render( - , - ); - - fireEvent.click(getByText("Win+L")); - expect(sendKey.mock.calls).toEqual([ - [0xffeb, true], - [0x006c, true], - [0x006c, false], - [0xffeb, false], - ]); - - sendKey.mockClear(); - fireEvent.click(getByText("Win")); - expect(sendKey.mock.calls).toEqual([ - [0xffeb, true], - [0xffeb, false], - ]); - - sendKey.mockClear(); - fireEvent.click(getByText("remoteDesktop.toolbar.win")); - expect(sendKey).toHaveBeenCalledWith(0xffeb, true); - }); - - it.each(["vnc", "rdp"] as const)( - "exposes working zoom controls for %s", - (protocol) => { - const zoomIn = vi.fn(() => 1.25); - const zoomOut = vi.fn(() => 0.75); - const resetZoom = vi.fn(() => 1); - const displayRef = { - current: { - disconnect: vi.fn(), - isConnected: () => true, - sendKey: vi.fn(), - sendMouse: vi.fn(), - setClipboard: vi.fn(), - getFilesystem: () => null, - uploadFile: async () => {}, - zoomIn, - zoomOut, - resetZoom, - } satisfies GuacamoleDisplayHandle, - } as React.RefObject; - const { getByLabelText, getByText } = render( - , - ); - - fireEvent.click(getByLabelText("remoteDesktop.toolbar.zoomOut")); - fireEvent.click(getByLabelText("remoteDesktop.toolbar.zoomIn")); - fireEvent.click(getByText("125%")); - expect(zoomOut).toHaveBeenCalledOnce(); - expect(zoomIn).toHaveBeenCalledOnce(); - expect(resetZoom).toHaveBeenCalledOnce(); - }, - ); - - it("offers an explicit session-only hide action", () => { - const onHide = vi.fn(); - const displayRef = { - current: { - disconnect: vi.fn(), - isConnected: () => true, - sendKey: vi.fn(), - sendMouse: vi.fn(), - setClipboard: vi.fn(), - getFilesystem: () => null, - uploadFile: vi.fn(), - zoomIn: vi.fn(() => 1.25), - zoomOut: vi.fn(() => 0.75), - resetZoom: vi.fn(() => 1), - } satisfies GuacamoleDisplayHandle, - } as React.RefObject; - - const { getByLabelText } = render( - , - ); - - fireEvent.click(getByLabelText("remoteDesktop.toolbar.hide")); - expect(onHide).toHaveBeenCalledOnce(); - }); - - it("labels the VNC meta key Super instead of Windows", () => { - const displayRef = { - current: { - disconnect: vi.fn(), - isConnected: () => true, - sendKey: vi.fn(), - sendMouse: vi.fn(), - setClipboard: vi.fn(), - getFilesystem: () => null, - uploadFile: vi.fn(), - zoomIn: vi.fn(() => 1.25), - zoomOut: vi.fn(() => 0.75), - resetZoom: vi.fn(() => 1), - } satisfies GuacamoleDisplayHandle, - } as React.RefObject; - - const { getByText, queryByText } = render( - , - ); - - expect(getByText("Super+L")).toBeTruthy(); - expect(getByText("Super")).toBeTruthy(); - expect(queryByText("Win+L")).toBeNull(); - expect(queryByText("Win")).toBeNull(); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/activate.test.tsx b/plugins/remote-desktop/tests/frontend/activate.test.tsx deleted file mode 100644 index b9b00717c..000000000 --- a/plugins/remote-desktop/tests/frontend/activate.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - renderWithApp, - type RenderedPluginApp, -} from "@termix/plugin-sdk/testing"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import * as plugin from "../../src/frontend/index"; -import manifestJson from "../../manifest.json"; -import locales from "../../locales/en.json"; - -const manifest = manifestJson as unknown as PluginManifest; - -const declared = (views?: Array<{ id: string }>) => - (views ?? []).map((view) => view.id).sort(); - -let rendered: RenderedPluginApp | null = null; - -afterEach(async () => { - await rendered?.deactivate(); - rendered = null; -}); - -describe(`${manifest.id} activate`, () => { - it("activates and registers only views its manifest declares", async () => { - rendered = await renderWithApp(plugin, { manifest, locales }); - const contributes = manifest.contributes as unknown as Record< - string, - Array<{ id: string }> | undefined - >; - const tabs = declared(contributes.tabs); - // A rail panel may reuse one of the plugin's tab ids. - const panels = [...declared(contributes.panels), ...tabs]; - const cards = declared(contributes.dashboardCards); - for (const id of rendered.registered.tabs()) expect(tabs).toContain(id); - for (const id of rendered.registered.panels()) expect(panels).toContain(id); - for (const id of rendered.registered.dashboardCards()) { - expect(cards).toContain(id); - } - }); - - it("removes everything it registered on deactivate", async () => { - const app = await renderWithApp(plugin, { manifest, locales }); - await app.deactivate(); - expect(app.registered.tabs()).toEqual([]); - expect(app.registered.panels()).toEqual([]); - expect(app.registered.railItems()).toEqual([]); - expect(app.registered.hostActions()).toEqual([]); - expect(app.registered.hostProtocols()).toEqual([]); - expect(app.registered.hostEditorSections()).toEqual([]); - expect(app.registered.dashboardCards()).toEqual([]); - expect(app.registered.settingsComponents()).toEqual([]); - expect(app.registered.actions()).toEqual([]); - }); - - it("adds RDP, VNC and Telnet as host protocols while turned on", async () => { - rendered = await renderWithApp(plugin, { - manifest, - locales, - api: { get: async () => ({ data: { enabled: true } }) } as never, - }); - await vi.waitFor(() => - expect(rendered!.registered.hostProtocols().sort()).toEqual([ - "rdp", - "telnet", - "vnc", - ]), - ); - expect(rendered.registered.hostActions().map((a) => a.id)).toEqual( - expect.arrayContaining(["rdp", "vnc", "telnet"]), - ); - }); - - it("offers no way in while an admin has turned it off", async () => { - rendered = await renderWithApp(plugin, { - manifest, - locales, - api: { get: async () => ({ data: { enabled: false } }) } as never, - }); - await new Promise((resolve) => setTimeout(resolve, 20)); - expect(rendered.registered.hostProtocols()).toEqual([]); - expect(rendered.registered.hostActions()).toEqual([]); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-api.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-api.test.ts deleted file mode 100644 index bfab4c050..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-api.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { describe, expect, it, vi, beforeEach } from "vitest"; - -const authApiMock = vi.hoisted(() => ({ - get: vi.fn(async () => ({ data: { guacd: { status: "disconnected" } } })), - post: vi.fn(async () => ({ data: { token: "local-token" } })), -})); -const remoteApiMock = vi.hoisted(() => ({ - get: vi.fn(async () => ({ data: { guacd: { status: "connected" } } })), - post: vi.fn(async () => ({ data: { token: "remote-token" } })), -})); -const isElectronMock = vi.hoisted(() => vi.fn(() => false)); -const resolveRemoteHostIdMock = vi.hoisted(() => vi.fn()); - -vi.mock("@termix/plugin-sdk/ui", () => ({ - isElectron: isElectronMock, - resolveRemoteHostId: resolveRemoteHostIdMock, -})); - -import { - setRemoteDesktopApp, - getGuacdStatus, - getGuacamoleConnectionId, - getGuacamoleTokenFromHost, -} from "../../src/frontend/guacamole-api"; - -// Mirrors app.apiFor: the remote server only for a remote origin in the -// desktop app, the ordinary client otherwise. -setRemoteDesktopApp({ - apiFor: (origin) => - (origin === "remote" && isElectronMock() - ? remoteApiMock - : authApiMock) as never, -}); - -beforeEach(() => { - authApiMock.get.mockClear(); - authApiMock.post.mockClear(); - remoteApiMock.get.mockClear(); - remoteApiMock.post.mockClear(); - resolveRemoteHostIdMock.mockReset(); -}); - -describe("guacamole API origin", () => { - it("uses the shared instance in the browser", async () => { - isElectronMock.mockReturnValue(false); - - await getGuacdStatus("remote"); - await getGuacamoleTokenFromHost(9, "remote", "vnc"); - - expect(authApiMock.get).toHaveBeenCalledWith("/status"); - expect(authApiMock.post).toHaveBeenCalledOnce(); - expect(remoteApiMock.get).not.toHaveBeenCalled(); - expect(remoteApiMock.post).not.toHaveBeenCalled(); - }); - - it("uses the connected remote server in the desktop app", async () => { - isElectronMock.mockReturnValue(true); - - // The embedded backend has no guacd, so asking it reports "disconnected" - // even when the connected server can serve the session. - const status = await getGuacdStatus("remote"); - const token = await getGuacamoleTokenFromHost(9, "remote", "vnc"); - - expect(status.guacd.status).toBe("connected"); - expect(token.token).toBe("remote-token"); - expect(remoteApiMock.get).toHaveBeenCalledWith("/status"); - expect(remoteApiMock.post).toHaveBeenCalledOnce(); - expect(authApiMock.get).not.toHaveBeenCalled(); - expect(authApiMock.post).not.toHaveBeenCalled(); - }); - - it("sends the connect-host payload unchanged to the remote server", async () => { - isElectronMock.mockReturnValue(true); - resolveRemoteHostIdMock.mockResolvedValue(41); - - await getGuacamoleTokenFromHost( - 9, - "remote", - "rdp", - { - username: "admin", - password: "secret", - domain: "EXAMPLE", - }, - "host-sync-id", - ); - - expect(remoteApiMock.post).toHaveBeenCalledWith("/connect-host/41", { - protocol: "rdp", - promptedUsername: "admin", - promptedPassword: "secret", - promptedDomain: "EXAMPLE", - }); - expect(resolveRemoteHostIdMock).toHaveBeenCalledWith("host-sync-id"); - }); - - it("sends an empty prompted domain for local RDP accounts", async () => { - isElectronMock.mockReturnValue(false); - - await getGuacamoleTokenFromHost(9, "local", "rdp", { - username: "local-admin", - password: "secret", - domain: "", - }); - - expect(authApiMock.post).toHaveBeenCalledWith("/connect-host/9", { - protocol: "rdp", - promptedUsername: "local-admin", - promptedPassword: "secret", - promptedDomain: "", - }); - }); - - // Support#1240: a host set to "This Device" is served by the embedded - // backend, so its token has to come from there too -- and its id must not - // be remapped onto a remote server that may not even be configured. - it("asks the embedded backend when the host originates locally", async () => { - isElectronMock.mockReturnValue(true); - - const token = await getGuacamoleTokenFromHost(9, "local", "rdp"); - - expect(token.token).toBe("local-token"); - expect(authApiMock.post).toHaveBeenCalledWith("/connect-host/9", { - protocol: "rdp", - }); - expect(remoteApiMock.post).not.toHaveBeenCalled(); - expect(resolveRemoteHostIdMock).not.toHaveBeenCalled(); - }); - - it("reports the embedded backend's own guacd for a local origin", async () => { - isElectronMock.mockReturnValue(true); - - const status = await getGuacdStatus("local"); - - expect(status.guacd.status).toBe("disconnected"); - expect(authApiMock.get).toHaveBeenCalledWith("/status"); - expect(remoteApiMock.get).not.toHaveBeenCalled(); - }); - - it("uses the remote server for an explicitly remote origin", async () => { - isElectronMock.mockReturnValue(true); - - await getGuacamoleTokenFromHost(9, "remote", "rdp"); - - expect(remoteApiMock.post).toHaveBeenCalledOnce(); - expect(authApiMock.post).not.toHaveBeenCalled(); - }); - - it("does not fall back to a colliding local id when sync resolution fails", async () => { - isElectronMock.mockReturnValue(true); - resolveRemoteHostIdMock.mockResolvedValue(null); - - await expect( - getGuacamoleTokenFromHost( - 9, - "remote", - "rdp", - undefined, - "missing-sync-id", - ), - ).rejects.toThrow("The synced host does not exist on the remote server"); - expect(remoteApiMock.post).not.toHaveBeenCalled(); - }); -}); - -it("looks up the session on the backend that issued its token", async () => { - isElectronMock.mockReturnValue(true); - const signal = new AbortController().signal; - await getGuacamoleConnectionId("connect/id", "remote", signal); - expect(remoteApiMock.get).toHaveBeenCalledWith("/connection/connect%2Fid", { - signal, - }); - expect(authApiMock.get).not.toHaveBeenCalled(); -}); - -describe("saved host status routing", () => { - it("maps the local host ID before probing the remote backend", async () => { - isElectronMock.mockReturnValue(true); - resolveRemoteHostIdMock.mockResolvedValue(41); - await getGuacdStatus("remote", { - hostId: 9, - protocol: "rdp", - syncId: "sync-9", - }); - expect(resolveRemoteHostIdMock).toHaveBeenCalledWith("sync-9"); - expect(remoteApiMock.get).toHaveBeenCalledWith( - "/status?hostId=41&protocol=rdp", - ); - expect(authApiMock.get).not.toHaveBeenCalled(); - }); - - it("keeps local IDs for local connections", async () => { - isElectronMock.mockReturnValue(true); - await getGuacdStatus("local", { - hostId: 9, - protocol: "vnc", - syncId: "sync-9", - }); - expect(authApiMock.get).toHaveBeenCalledWith( - "/status?hostId=9&protocol=vnc", - ); - expect(resolveRemoteHostIdMock).not.toHaveBeenCalled(); - }); - - it("does not probe an unrelated remote ID when a synced host is missing", async () => { - isElectronMock.mockReturnValue(true); - resolveRemoteHostIdMock.mockResolvedValue(null); - await expect( - getGuacdStatus("remote", { - hostId: 9, - protocol: "rdp", - syncId: "missing", - }), - ).rejects.toThrow("does not exist"); - expect(remoteApiMock.get).not.toHaveBeenCalled(); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-clipboard.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-clipboard.test.ts deleted file mode 100644 index 2c298bfff..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-clipboard.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { - isPasteShortcut, - pasteTextToRemote, - type GuacamoleClipboardClient, -} from "../../src/frontend/guacamole-clipboard.js"; - -describe("Guacamole clipboard paste", () => { - it("recognizes Ctrl+V and Command+V without intercepting Alt+V", () => { - expect( - isPasteShortcut({ - key: "v", - ctrlKey: true, - metaKey: false, - altKey: false, - }), - ).toBe(true); - expect( - isPasteShortcut({ - key: "V", - ctrlKey: false, - metaKey: true, - altKey: false, - }), - ).toBe(true); - expect( - isPasteShortcut({ - key: "v", - ctrlKey: true, - metaKey: false, - altKey: true, - }), - ).toBe(false); - }); - - it("updates the remote clipboard before sending Ctrl+V", () => { - const events: string[] = []; - const client: GuacamoleClipboardClient = { - createClipboardStream: vi.fn((mimetype: string) => { - events.push(`stream:${mimetype}`); - return { - sendBlob: () => events.push("blob"), - sendEnd: () => events.push("end"), - }; - }), - sendKeyEvent: vi.fn((pressed: number, keysym: number) => { - events.push(`key:${pressed}:${keysym}`); - }), - }; - - pasteTextToRemote(client, "Firefox clipboard"); - - expect(events).toEqual([ - "stream:text/plain", - "blob", - "end", - "key:1:65507", - "key:1:118", - "key:0:118", - "key:0:65507", - ]); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-display-size.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-display-size.test.ts deleted file mode 100644 index 0a1338fbb..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-display-size.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - getGuacamoleDisplaySize, - readConfiguredDimension, -} from "../../src/frontend/guacamole-display-size"; - -describe("getGuacamoleDisplaySize", () => { - it("requests native pixels and matching DPI for HiDPI RDP", () => { - expect(getGuacamoleDisplaySize(1280, 720, "rdp", 2)).toEqual({ - width: 2560, - height: 1440, - dpi: 192, - pixelRatio: 2, - }); - }); - - it("scales a configured RDP DPI with the device pixel ratio", () => { - expect(getGuacamoleDisplaySize(1000, 600, "rdp", 1.5, 120)).toEqual({ - width: 1500, - height: 900, - dpi: 180, - pixelRatio: 1.5, - }); - }); - - it("leaves non-RDP protocols at CSS-pixel dimensions", () => { - expect(getGuacamoleDisplaySize(1280, 720, "vnc", 2)).toEqual({ - width: 1280, - height: 720, - pixelRatio: 1, - }); - }); - - it("caps pathological pixel ratios to protect remote session size", () => { - expect(getGuacamoleDisplaySize(400, 800, "rdp", 4)).toEqual({ - width: 1200, - height: 2400, - dpi: 288, - pixelRatio: 3, - }); - }); - - it("uses a configured resolution in place of the container size", () => { - expect( - getGuacamoleDisplaySize(800, 600, "rdp", 1, undefined), - ).toMatchObject({ width: 800, height: 600 }); - expect( - getGuacamoleDisplaySize(1920, 1080, "rdp", 1, undefined), - ).toMatchObject({ width: 1920, height: 1080 }); - }); -}); - -describe("readConfiguredDimension", () => { - it("accepts the strings the host editor stores", () => { - expect(readConfiguredDimension("1920")).toBe(1920); - expect(readConfiguredDimension(1080)).toBe(1080); - }); - - it("treats an unset or unusable value as 'follow the container'", () => { - expect(readConfiguredDimension("")).toBeUndefined(); - expect(readConfiguredDimension(undefined)).toBeUndefined(); - expect(readConfiguredDimension(null)).toBeUndefined(); - expect(readConfiguredDimension("auto")).toBeUndefined(); - expect(readConfiguredDimension("0")).toBeUndefined(); - expect(readConfiguredDimension("-1080")).toBeUndefined(); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-file-drop.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-file-drop.test.ts deleted file mode 100644 index cf407025d..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-file-drop.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - canUploadToRdpDrive, - getFileDropDisposition, - hasDraggedFiles, -} from "../../src/frontend/guacamole-file-drop.ts"; - -describe("Guacamole file drop", () => { - it("recognizes external file drags", () => { - expect(hasDraggedFiles(["Files"])).toBe(true); - expect(hasDraggedFiles(["text/plain"])).toBe(false); - }); - - it("rejects files when upload is unavailable instead of ignoring them", () => { - expect(getFileDropDisposition(["Files"], 1, false)).toBe("reject"); - expect(getFileDropDisposition(["Files"], 1, true)).toBe("upload"); - }); - - it("accepts an enabled RDP drive before a filesystem object is advertised", () => { - expect(canUploadToRdpDrive(true, true, false)).toBe(true); - expect(canUploadToRdpDrive(true, false, true)).toBe(true); - expect(canUploadToRdpDrive(true, false, false)).toBe(false); - expect(canUploadToRdpDrive(false, true, true)).toBe(false); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-filesystem.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-filesystem.test.ts deleted file mode 100644 index ce7c535af..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-filesystem.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { describeUploadError } from "../../src/frontend/guacamole-filesystem"; - -const t = (key: string) => `#${key}`; - -describe("describeUploadError", () => { - it("explains guacd's refusal to open the target file as a drive-folder problem", () => { - expect(describeUploadError(new Error("FAIL (CANNOT OPEN)"), t)).toBe( - "#driveNotWritable", - ); - expect(describeUploadError(new Error("FAIL (NO FS)"), t)).toBe( - "#driveUnavailable", - ); - }); - - it("passes other messages through and falls back for unknown errors", () => { - expect(describeUploadError(new Error("disk full"), t)).toBe("disk full"); - expect(describeUploadError(undefined, t)).toBe("#uploadFailed"); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-key-release.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-key-release.test.ts deleted file mode 100644 index bdfcb05ac..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-key-release.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -/// -import ts from "typescript"; -import Guacamole from "guacamole-common-js"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import displaySource from "../../src/frontend/GuacamoleDisplay.tsx?raw"; - -// Exercise the display's actual focus callback with Guacamole's real keyboard. -const source = ts.createSourceFile( - "GuacamoleDisplay.tsx", - displaySource, - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.TSX, -); -let callback: string | undefined; -function visit(node: ts.Node) { - if ( - ts.isVariableDeclaration(node) && - node.name.getText(source) === "refreshKeyboardHandlers" && - node.initializer && - ts.isCallExpression(node.initializer) - ) { - callback = node.initializer.arguments[0].getText(source); - } - ts.forEachChild(node, visit); -} -visit(source); -if (!callback) throw new Error("Missing refreshKeyboardHandlers callback"); -const runRefresh = new Function( - "state", - ts.transpileModule( - `const { keyboardRef, clientRef, displayElementRef, isVisible, windowFocusedRef, hasKeyboardFocusRef } = state; - (${callback})();`, - { compilerOptions: { target: ts.ScriptTarget.ES2022 } }, - ).outputText, -) as (state: Record) => void; - -afterEach(() => { - vi.restoreAllMocks(); - document.body.replaceChildren(); -}); - -describe("remote keyboard focus loss", () => { - it.each(["window", "display", "hidden", "tab"])( - "releases held modifiers when losing %s focus", - (reason) => { - const display = document.createElement("div"); - display.tabIndex = 0; - document.body.append(display); - display.focus(); - vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); - const keyboard = new Guacamole.Keyboard(display) as Guacamole.Keyboard & { - press(keysym: number): boolean; - release(keysym: number): void; - pressed: Record; - }; - const sendKeyEvent = vi.fn(); - const state = { - keyboardRef: { current: keyboard }, - clientRef: { current: { sendKeyEvent } }, - displayElementRef: { current: display }, - isVisible: true, - windowFocusedRef: { current: true }, - hasKeyboardFocusRef: { current: true }, - }; - const refresh = () => runRefresh(state); - refresh(); - const keys = [0xffeb, 0xffe3, 0xffe9]; // Windows, Ctrl, Alt - keys.forEach((key) => keyboard.press(key)); - expect(sendKeyEvent.mock.calls).toEqual(keys.map((key) => [1, key])); - - if (reason === "window") state.windowFocusedRef.current = false; - if (reason === "tab") state.isVisible = false; - if (reason === "hidden") { - vi.spyOn(document, "visibilityState", "get").mockReturnValue("hidden"); - } - if (reason === "display") { - state.hasKeyboardFocusRef.current = false; - display.blur(); - } - refresh(); - expect(sendKeyEvent.mock.calls.slice(3)).toEqual( - [...keys].sort((a, b) => a - b).map((key) => [0, key]), - ); - expect(Object.keys(keyboard.pressed)).toEqual([]); - expect(keyboard.onkeydown).toBeNull(); - expect(keyboard.onkeyup).toBeNull(); - keyboard.press(0x6c); - keyboard.reset(); - expect(sendKeyEvent).toHaveBeenCalledTimes(6); - - state.isVisible = true; - state.windowFocusedRef.current = true; - state.hasKeyboardFocusRef.current = true; - vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible"); - display.focus(); - refresh(); - keyboard.press(0x6c); - keyboard.release(0x6c); - expect(sendKeyEvent.mock.calls.slice(6)).toEqual([ - [1, 0x6c], - [0, 0x6c], - ]); - }, - ); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-meta-key.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-meta-key.test.ts deleted file mode 100644 index 7fa6d15ff..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-meta-key.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - detectMetaKeyFamily, - metaKeyLabels, -} from "../../src/frontend/guacamole-meta-key.js"; - -describe("guacamole toolbar meta key labels", () => { - it("keeps Windows labels for RDP regardless of the local device", () => { - expect( - detectMetaKeyFamily("rdp", { - platform: "MacIntel", - userAgent: "Macintosh; Intel Mac OS X", - userAgentDataPlatform: "macOS", - }), - ).toBe("win"); - }); - - it("labels VNC with the local device's modifier instead of Windows", () => { - expect( - detectMetaKeyFamily("vnc", { - platform: "MacIntel", - userAgent: "Macintosh; Intel Mac OS X", - userAgentDataPlatform: "macOS", - }), - ).toBe("cmd"); - expect( - detectMetaKeyFamily("vnc", { - platform: "Linux x86_64", - userAgent: "X11; Linux x86_64", - userAgentDataPlatform: "Linux", - }), - ).toBe("super"); - expect( - detectMetaKeyFamily("vnc", { - platform: "Win32", - userAgent: "Windows NT 10.0", - userAgentDataPlatform: "Windows", - }), - ).toBe("win"); - }); - - it("exposes Super/Cmd button copy for non-Windows VNC sessions", () => { - expect(metaKeyLabels("super").short).toBe("Super"); - expect(metaKeyLabels("super").lock).toBe("Super+L"); - expect(metaKeyLabels("cmd").short).toBe("Cmd"); - expect(metaKeyLabels("cmd").lock).toBe("Cmd+L"); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-pointer.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-pointer.test.ts deleted file mode 100644 index dcb7c54d5..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-pointer.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { bindPointerInput } from "../../src/frontend/guacamole-pointer.js"; - -function listenedEvents(): { - element: HTMLElement; - events: () => string[]; -} { - const element = document.createElement("div"); - const seen: string[] = []; - const original = element.addEventListener.bind(element); - element.addEventListener = ((type: string, ...rest: unknown[]) => { - seen.push(type); - return (original as (t: string, ...r: unknown[]) => void)(type, ...rest); - }) as typeof element.addEventListener; - return { element, events: () => seen }; -} - -describe("bindPointerInput", () => { - // A touchscreen laptop reports maxTouchPoints > 0 and still has a mouse. - // Binding only the touch emulator left the pointer dead on those machines. - it.each([ - ["no touch mode", null], - ["touchscreen", "touchscreen" as const], - ["touchpad", "touchpad" as const], - ])("keeps the physical pointer bound with %s", (_label, touchMode) => { - const { element, events } = listenedEvents(); - - bindPointerInput(element, touchMode, vi.fn()); - - expect(events()).toContain("mousedown"); - expect(events()).toContain("mousemove"); - expect(events()).toContain("mouseup"); - }); - - it("adds touch listeners only when a touch mode is selected", () => { - const withoutTouch = listenedEvents(); - bindPointerInput(withoutTouch.element, null, vi.fn()); - const plainTouchCount = withoutTouch - .events() - .filter((e) => e.startsWith("touch")).length; - - const withTouch = listenedEvents(); - bindPointerInput(withTouch.element, "touchscreen", vi.fn()); - const touchModeCount = withTouch - .events() - .filter((e) => e.startsWith("touch")).length; - - expect(touchModeCount).toBeGreaterThan(plainTouchCount); - }); - - it("does not forward a wheel flood as individual remote scroll clicks", () => { - vi.useFakeTimers(); - const element = document.createElement("div"); - const onState = vi.fn(); - const dispose = bindPointerInput(element, null, onState); - - for (let i = 0; i < 30; i++) { - element.dispatchEvent( - new WheelEvent("wheel", { - deltaY: 120, - deltaMode: 0, - bubbles: true, - cancelable: true, - }), - ); - } - - const presses = onState.mock.calls.filter( - ([state]) => state.up === true || state.down === true, - ); - expect(presses.length).toBeLessThanOrEqual(2); - - vi.advanceTimersByTime(40); - const laterPresses = onState.mock.calls.filter( - ([state]) => state.up === true || state.down === true, - ); - expect(laterPresses.length).toBeLessThanOrEqual(4); - - dispose(); - vi.useRealTimers(); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-scroll.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-scroll.test.ts deleted file mode 100644 index 8c5665151..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-scroll.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { - clampPendingScrollClicks, - createScrollCoalescer, - isScrollButtonState, - MAX_SCROLL_CLICKS_PER_FLUSH, - SCROLL_FLUSH_INTERVAL_MS, - takeScrollClicks, -} from "../../src/frontend/guacamole-scroll.js"; - -describe("guacamole scroll coalescing", () => { - it("caps a click flood so a laggy tunnel cannot queue a backlog", () => { - expect(clampPendingScrollClicks(0, 40)).toBe(MAX_SCROLL_CLICKS_PER_FLUSH); - expect(clampPendingScrollClicks(0, -40)).toBe(-MAX_SCROLL_CLICKS_PER_FLUSH); - expect(takeScrollClicks(20)).toEqual({ - clicks: MAX_SCROLL_CLICKS_PER_FLUSH, - direction: 1, - }); - }); - - it("sends the first notch immediately and drops later clicks in the same burst", () => { - const send = vi.fn(); - let now = 1_000; - const scheduled: Array<{ fn: () => void; ms: number }> = []; - const coalescer = createScrollCoalescer( - send, - () => now, - (fn, ms) => { - scheduled.push({ fn, ms }); - return scheduled.length; - }, - () => {}, - ); - - coalescer.notePointer({ - x: 12, - y: 34, - left: false, - middle: false, - right: false, - up: false, - down: false, - }); - for (let i = 0; i < 20; i++) coalescer.ingestClick(1); - - const firstBurst = send.mock.calls.map(([state]) => state); - expect(firstBurst).toEqual([ - { - x: 12, - y: 34, - left: false, - middle: false, - right: false, - up: false, - down: true, - }, - { - x: 12, - y: 34, - left: false, - middle: false, - right: false, - up: false, - down: false, - }, - ]); - - send.mockClear(); - now += SCROLL_FLUSH_INTERVAL_MS; - expect(scheduled).toHaveLength(1); - scheduled[0].fn(); - expect(send.mock.calls).toHaveLength(MAX_SCROLL_CLICKS_PER_FLUSH * 2); - coalescer.dispose(); - }); - - it("recognizes only the scroll buttons as coalesced input", () => { - expect(isScrollButtonState({ up: true, down: false })).toBe(true); - expect(isScrollButtonState({ up: false, down: true })).toBe(true); - expect(isScrollButtonState({ up: false, down: false })).toBe(false); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-session-id.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-session-id.test.ts deleted file mode 100644 index 27cc872cd..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-session-id.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { afterEach, beforeEach, expect, it, vi } from "vitest"; -const fetchId = vi.hoisted(() => vi.fn()); -vi.mock("../../src/frontend/guacamole-api", () => ({ - getGuacamoleConnectionId: fetchId, -})); -import { watchGuacamoleConnectionId } from "../../src/frontend/guacamole-session-id"; -beforeEach(() => { - vi.useFakeTimers(); - fetchId.mockReset(); -}); -afterEach(() => vi.useRealTimers()); -it("discovers the sharing ID after handshake without changing the connection origin", async () => { - fetchId.mockResolvedValueOnce(null).mockResolvedValueOnce("guacd-id"); - const ready = vi.fn(); - const stop = watchGuacamoleConnectionId("connect-id", "remote", ready); - await vi.advanceTimersByTimeAsync(500); - expect(fetchId).toHaveBeenLastCalledWith( - "connect-id", - "remote", - expect.any(AbortSignal), - ); - expect(ready).toHaveBeenCalledExactlyOnceWith("guacd-id"); - stop(); -}); -it("ignores a late response after disconnect or replacement", async () => { - let resolve!: (id: string) => void; - fetchId.mockReturnValue( - new Promise((done) => { - resolve = done; - }), - ); - const ready = vi.fn(); - const stop = watchGuacamoleConnectionId("old", "local", ready); - stop(); - resolve("old-guacd-id"); - await vi.advanceTimersByTimeAsync(1000); - expect(ready).not.toHaveBeenCalled(); - expect(fetchId.mock.calls[0][2].aborted).toBe(true); -}); -it("bounds discovery when the session never opens", async () => { - fetchId.mockResolvedValue(null); - const stop = watchGuacamoleConnectionId("missing", "local", vi.fn()); - await vi.advanceTimersByTimeAsync(30000); - expect(fetchId).toHaveBeenCalledTimes(20); - stop(); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-websocket-url.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-websocket-url.test.ts deleted file mode 100644 index 81cb19fe3..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-websocket-url.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { buildGuacamoleWebSocketBaseUrl } from "../../src/frontend/guacamole-websocket-url.js"; - -const httpsLocation = { - protocol: "https:", - host: "termix.example.com", -} as Location; - -// The display socket moved off port 30008 to /plugin-ws/remote-desktop/display -// on the main backend, so every case below asserts the same one path. Dev is -// no longer a special case: Vite proxies the backend, so there is nothing to -// point at a separate port. -const ROUTE = "/plugin-ws/remote-desktop/display"; - -describe("buildGuacamoleWebSocketBaseUrl", () => { - it("uses the same origin in production web builds", () => { - expect( - buildGuacamoleWebSocketBaseUrl({ - isElectronApp: false, - isEmbeddedApp: false, - basePath: "", - location: httpsLocation, - }), - ).toBe(`wss://termix.example.com${ROUTE}`); - }); - - it("preserves the runtime base path for proxied web deployments", () => { - expect( - buildGuacamoleWebSocketBaseUrl({ - isElectronApp: false, - isEmbeddedApp: false, - basePath: "/termix", - location: httpsLocation, - }), - ).toBe(`wss://termix.example.com/termix${ROUTE}`); - }); - - it("targets the embedded backend for electron running locally", () => { - expect( - buildGuacamoleWebSocketBaseUrl({ - isElectronApp: true, - isEmbeddedApp: true, - basePath: "/termix", - location: httpsLocation, - }), - ).toBe(`ws://127.0.0.1:30001${ROUTE}`); - }); - - it("uses the configured remote server URL for electron remote mode", () => { - expect( - buildGuacamoleWebSocketBaseUrl({ - isElectronApp: true, - isEmbeddedApp: false, - configuredServerUrl: "https://termix.example.com/termix/", - basePath: "", - location: httpsLocation, - }), - ).toBe(`wss://termix.example.com/termix${ROUTE}`); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/guacamole-zoom.test.ts b/plugins/remote-desktop/tests/frontend/guacamole-zoom.test.ts deleted file mode 100644 index 413d15e0a..000000000 --- a/plugins/remote-desktop/tests/frontend/guacamole-zoom.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - clampGuacamoleZoom, - stepGuacamoleZoom, -} from "../../src/frontend/guacamole-zoom.js"; - -describe("guacamole zoom", () => { - it("steps in predictable quarter increments", () => { - expect(stepGuacamoleZoom(1, 1)).toBe(1.25); - expect(stepGuacamoleZoom(1, -1)).toBe(0.75); - }); - - it("keeps toolbar and pinch zoom within usable bounds", () => { - expect(clampGuacamoleZoom(0.1)).toBe(0.5); - expect(clampGuacamoleZoom(8)).toBe(4); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/quick-connect-guac-host.test.ts b/plugins/remote-desktop/tests/frontend/quick-connect-guac-host.test.ts deleted file mode 100644 index 4a5829d2c..000000000 --- a/plugins/remote-desktop/tests/frontend/quick-connect-guac-host.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { quickConnectGuacHost } from "../../src/frontend/quick-connect-guac-host"; - -// The shape core's Quick Connect builds for a plugin protocol: the switch and -// port in this plugin's host settings, the unsaved login in quickConnectLogin. -function quickHost(protocol: "rdp" | "vnc", extra: Record) { - return { - id: "quick-connect-1", - name: "admin@10.0.0.2", - ip: "10.0.0.2", - pluginSettings: { - "remote-desktop": { - [protocol === "rdp" ? "enableRdp" : "enableVnc"]: true, - [protocol === "rdp" ? "rdpPort" : "vncPort"]: - protocol === "rdp" ? 3390 : 5901, - }, - }, - ...extra, - }; -} - -describe("quickConnectGuacHost", () => { - it("carries an RDP quick connect's address and credentials", () => { - expect( - quickConnectGuacHost( - quickHost("rdp", { - quickConnectLogin: { - protocol: "rdp", - username: "admin", - password: "pw", - fields: { domain: "CORP" }, - }, - }), - ), - ).toMatchObject({ - ip: "10.0.0.2", - connectionType: "rdp", - port: 3390, - username: "admin", - password: "pw", - domain: "CORP", - }); - }); - - it("takes a VNC quick connect's login from the VNC fields", () => { - expect( - quickConnectGuacHost( - quickHost("vnc", { - quickConnectLogin: { protocol: "vnc", password: "vncpw" }, - }), - ), - ).toMatchObject({ - connectionType: "vnc", - port: 5901, - password: "vncpw", - }); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/rdp-credential-prompt.test.ts b/plugins/remote-desktop/tests/frontend/rdp-credential-prompt.test.ts deleted file mode 100644 index 6170dcdf1..000000000 --- a/plugins/remote-desktop/tests/frontend/rdp-credential-prompt.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { needsRdpCredentialPrompt } from "../../src/frontend/rdp-credential-prompt"; - -describe("needsRdpCredentialPrompt", () => { - it("prompts recipients when RDP has no saved authentication", () => { - expect( - needsRdpCredentialPrompt({ protocol: "rdp", rdpAuthType: "none" }), - ).toBe(true); - }); - - it("does not prompt when the recipient has a personal override", () => { - expect( - needsRdpCredentialPrompt({ - protocol: "rdp", - rdpAuthType: "none", - authOverrides: { - rdp: { credentialId: 7, required: false, ownerAuthShared: true }, - }, - }), - ).toBe(false); - }); - - it("does not prompt for other protocols", () => { - expect( - needsRdpCredentialPrompt({ protocol: "vnc", rdpAuthType: "none" }), - ).toBe(false); - }); -}); diff --git a/plugins/remote-desktop/tests/frontend/remote-form.test.ts b/plugins/remote-desktop/tests/frontend/remote-form.test.ts deleted file mode 100644 index 6fca9c30b..000000000 --- a/plugins/remote-desktop/tests/frontend/remote-form.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, expect, it } from "vitest"; -import type { HostEditorSectionProps } from "@termix/plugin-sdk/frontend"; -import { - loginOf, - patchLogin, - remoteDesktopForm, -} from "../../src/frontend/remote-form"; - -function sectionProps(form: Record) { - const state = { form: { ...form } }; - const props = { - form: state.form, - setField: (key: string, value: unknown) => { - state.form = { ...state.form, [key]: value }; - }, - updateForm: ( - patch: (current: Record) => Record, - ) => { - state.form = { ...state.form, ...patch(state.form) }; - }, - } as unknown as HostEditorSectionProps; - return { props, state }; -} - -describe("remote desktop editor form", () => { - it("reads each protocol's login from the host form's protocolAuth", () => { - const { props } = sectionProps({ - protocolAuth: { - rdp: { - authType: "credential", - credentialId: "4", - username: "", - password: "", - fields: { domain: "CORP" }, - }, - vnc: { - authType: "direct", - credentialId: "", - username: "", - password: "saved", - fields: {}, - }, - }, - }); - const { form } = remoteDesktopForm(props); - expect(form).toMatchObject({ - rdpAuthType: "credential", - rdpCredentialId: "4", - domain: "CORP", - vncPassword: "saved", - telnetAuthType: "direct", - telnetUser: "", - }); - }); - - it("writes the tabs' login fields into protocolAuth", () => { - const { props, state } = sectionProps({}); - const { setField } = remoteDesktopForm(props); - setField("rdpUser", "admin"); - setField("domain", "CORP"); - setField("telnetAuthType", "credential"); - - expect(state.form.protocolAuth).toEqual({ - rdp: { - authType: "direct", - credentialId: "", - username: "admin", - password: "", - fields: { domain: "CORP" }, - }, - telnet: { - authType: "credential", - credentialId: "", - username: "", - password: "", - fields: {}, - }, - }); - }); - - it("keeps other protocols when one changes", () => { - const current = { - protocolAuth: { vnc: { ...loginOf(undefined, "vnc"), username: "v" } }, - }; - const next = patchLogin(current, "rdp", "password", "pw"); - expect(next.protocolAuth).toMatchObject({ - vnc: { username: "v" }, - rdp: { password: "pw" }, - }); - }); -}); diff --git a/plugins/remote-desktop/tsconfig.json b/plugins/remote-desktop/tsconfig.json deleted file mode 100644 index 7b35314c3..000000000 --- a/plugins/remote-desktop/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@termix/plugin-sdk/tsconfig.plugin.json", - "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] -} diff --git a/plugins/remote-desktop/vitest.config.ts b/plugins/remote-desktop/vitest.config.ts deleted file mode 100644 index 31ba004fc..000000000 --- a/plugins/remote-desktop/vitest.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; - -export default pluginVitestConfig(import.meta.url); diff --git a/plugins/secret-sources/CHANGELOG.md b/plugins/secret-sources/CHANGELOG.md deleted file mode 100644 index 3e2bbb6b0..000000000 --- a/plugins/secret-sources/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -## 1.0.0 - -First release, bundled with Termix 2.9.0. - -Fetches host passwords and keys from 1Password when you connect, using op:// references, so they are not stored in Termix. diff --git a/plugins/secret-sources/README.md b/plugins/secret-sources/README.md deleted file mode 100644 index 933ee3975..000000000 --- a/plugins/secret-sources/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Secret Sources - -Fetch host secrets from 1Password when you connect instead of storing them in Termix. - -## Features - -- Connect a 1Password Connect server as a secret source. -- Use `op://` references in a host's password or key fields instead of the secret itself. -- Secrets are fetched each time you connect and are not stored in Termix. - -## Settings - -### Admin - -- Private endpoint allowlist: private or loopback hosts a secret source may reach, one per line. A self-hosted 1Password Connect server usually needs its address listed here. - -## Development - -```bash -npm run build # build into dist/ -npm run test # run this plugin's tests -npm run typecheck # type-check this plugin -``` diff --git a/plugins/secret-sources/locales/en.json b/plugins/secret-sources/locales/en.json deleted file mode 100644 index 42dfe8c50..000000000 --- a/plugins/secret-sources/locales/en.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "You can paste a 1Password reference (op://vault/item/field) instead of the secret. It is fetched from your secret source when connecting.", - "manage": "Manage secret sources", - "title": "Secret sources", - "description": "External password managers Termix reads secrets from at connect time. Currently 1Password Connect (self-hosted). The access token is stored encrypted with your data key.", - "new": "New source", - "nameLabel": "Friendly name", - "urlLabel": "Connect server URL", - "tokenLabel": "Connect access token", - "tokenKeep": "Leave empty to keep the current token", - "sharedLabel": "Share with all users (admins only, resolves while you are signed in)", - "shared": "shared", - "test": "Test", - "testOk": "Connected, {{count}} vault(s) visible", - "testFailed": "Connection test failed", - "required": "Name, server URL and token are required", - "saved": "Secret source saved", - "deleted": "Secret source deleted", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Private endpoint allowlist", - "description": "Private or loopback hosts a secret source may reach, one per line. A self-hosted 1Password Connect server usually needs its own address listed here." - } - } - }, - "plugin": { - "name": "Secret Sources" - } -} diff --git a/plugins/secret-sources/locales/translated/af_ZA.json b/plugins/secret-sources/locales/translated/af_ZA.json deleted file mode 100644 index 989bfca7c..000000000 --- a/plugins/secret-sources/locales/translated/af_ZA.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Jy kan 'n 1Password-verwysing (op://vault/item/field) in plaas van die geheim plak. Dit word van jou geheime bron afgehaal wanneer jy koppel.", - "manage": "Bestuur geheime bronne", - "title": "Geheime bronne", - "description": "Eksterne wagwoordbestuurders Termix lees geheime vanaf wanneer dit verbind word. Tans 1Password Connect (selfgehuisves). Die toegangstoken word geïnkripteer met jou datasleutel gestoor.", - "new": "Nuwe bron", - "nameLabel": "Vriendelike naam", - "urlLabel": "Koppel bediener URL", - "tokenLabel": "Koppel toegangstoken", - "tokenKeep": "Los leeg om die huidige teken te behou", - "sharedLabel": "Deel met alle gebruikers (slegs administrateurs, word opgelos terwyl jy aangemeld is)", - "shared": "gedeel", - "test": "Toets", - "testOk": "Gekoppel, {{count}} kluis(se) sigbaar", - "testFailed": "Verbindingstoets het misluk", - "required": "Naam, bediener-URL en token word vereis", - "saved": "Geheime bron gestoor", - "deleted": "Geheime bron verwyder", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Privaat eindpunt-toelaatlys", - "description": "Privaat of teruglusgashere wat 'n geheime bron mag bereik, een per lyn. 'n Selfgehosteerde 1Password Connect-bediener benodig gewoonlik sy eie adres wat hier gelys word." - } - } - }, - "plugin": { - "name": "Geheime Bronne" - } -} diff --git a/plugins/secret-sources/locales/translated/ar_SA.json b/plugins/secret-sources/locales/translated/ar_SA.json deleted file mode 100644 index d2f225fb2..000000000 --- a/plugins/secret-sources/locales/translated/ar_SA.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "يمكنك لصق مرجع 1Password (op://vault/item/field) بدلاً من كلمة المرور السرية. يتم جلبها من مصدر كلمة المرور السرية عند الاتصال.", - "manage": "إدارة المصادر السرية", - "title": "مصادر سرية", - "description": "يقرأ برنامج إدارة كلمات المرور الخارجي Termix البيانات السرية عند الاتصال. حاليًا، نستخدم 1Password Connect (مُستضاف ذاتيًا). يتم تخزين رمز الوصول مُشفّرًا باستخدام مفتاح البيانات الخاص بك.", - "new": "مصدر جديد", - "nameLabel": "اسم ودود", - "urlLabel": "عنوان URL لخادم الاتصال", - "tokenLabel": "رمز الوصول للاتصال", - "tokenKeep": "اتركه فارغًا للاحتفاظ بالرمز المميز الحالي", - "sharedLabel": "مشاركة مع جميع المستخدمين (للمسؤولين فقط، يتم حل المشكلة أثناء تسجيل دخولك)", - "shared": "مشترك", - "test": "اختبار", - "testOk": "متصل، {{count}} الخزائن مرئية", - "testFailed": "فشل اختبار الاتصال", - "required": "الاسم وعنوان URL للخادم والرمز المميز مطلوبة", - "saved": "تم حفظ المصدر السري", - "deleted": "تم حذف المصدر السري", - "settings": { - "admin": { - "privateEndpoints": { - "label": "قائمة السماح لنقاط النهاية الخاصة", - "description": "قد يصل مصدر سري إلى مضيفات خاصة أو مضيفات ذات اتصال داخلي، واحد لكل سطر. عادةً ما يحتاج خادم 1Password Connect المستضاف ذاتيًا إلى عنوانه الخاص المدرج هنا." - } - } - }, - "plugin": { - "name": "مصادر سرية" - } -} diff --git a/plugins/secret-sources/locales/translated/bg_BG.json b/plugins/secret-sources/locales/translated/bg_BG.json deleted file mode 100644 index db7c08d02..000000000 --- a/plugins/secret-sources/locales/translated/bg_BG.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Можете да поставите препратка към 1Password (op://vault/item/field) вместо тайната. Тя се извлича от вашия източник на тайна при свързване.", - "manage": "Управлявайте тайните източници", - "title": "Тайни източници", - "description": "Външни мениджъри на пароли Termix четат тайни по време на свързване. В момента 1Password Connect (самостоятелно хостван). Токенът за достъп се съхранява криптиран с вашия ключ за данни.", - "new": "Нов източник", - "nameLabel": "Приятелско име", - "urlLabel": "URL адрес на сървъра за свързване", - "tokenLabel": "Свързване на токен за достъп", - "tokenKeep": "Оставете празно, за да запазите текущия токен", - "sharedLabel": "Споделяне с всички потребители (само за администратори, разрешава се, докато сте влезли)", - "shared": "споделено", - "test": "Тест", - "testOk": "Свързано, {{count}} видими трезори", - "testFailed": "Тестът за връзка не бе успешен", - "required": "Име, URL адрес на сървъра и токен са задължителни", - "saved": "Секретният източник е запазен", - "deleted": "Секретният източник е изтрит", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Списък с разрешени частни крайни точки", - "description": "Частни или loopback хостове, до които може да достигне таен източник, по един на ред. Самостоятелно хостван 1Password Connect сървър обикновено се нуждае от собствен адрес, посочен тук." - } - } - }, - "plugin": { - "name": "Тайни източници" - } -} diff --git a/plugins/secret-sources/locales/translated/bn_BD.json b/plugins/secret-sources/locales/translated/bn_BD.json deleted file mode 100644 index 80a1cdfca..000000000 --- a/plugins/secret-sources/locales/translated/bn_BD.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "আপনি সিক্রেটের পরিবর্তে একটি 1Password রেফারেন্স (op://vault/item/field) পেস্ট করতে পারেন। সংযোগ করার সময় এটি আপনার সিক্রেট সোর্স থেকে সংগ্রহ করা হয়।", - "manage": "গোপন উৎস পরিচালনা করুন", - "title": "গোপন সূত্র", - "description": "এক্সটার্নাল পাসওয়ার্ড ম্যানেজার Termix কানেক্ট করার সময় সিক্রেট ডেটা পড়ে নেয়। বর্তমানে ব্যবহৃত হয় 1Password Connect (সেলফ-হোস্টেড)। অ্যাক্সেস টোকেনটি আপনার ডেটা কী দিয়ে এনক্রিপ্ট করে সংরক্ষণ করা হয়।", - "new": "নতুন উৎস", - "nameLabel": "বন্ধুত্বপূর্ণ নাম", - "urlLabel": "সার্ভার URL সংযোগ করুন", - "tokenLabel": "অ্যাক্সেস টোকেন সংযুক্ত করুন", - "tokenKeep": "বর্তমান টোকেনটি রাখতে খালি রাখুন।", - "sharedLabel": "সকল ব্যবহারকারীর সাথে শেয়ার করুন (শুধুমাত্র অ্যাডমিনদের জন্য, আপনি সাইন ইন থাকা অবস্থায় কার্যকর হবে)", - "shared": "ভাগ করা হয়েছে", - "test": "পরীক্ষা", - "testOk": "সংযুক্ত, {{count}} টি ভল্ট দৃশ্যমান", - "testFailed": "সংযোগ পরীক্ষা ব্যর্থ হয়েছে", - "required": "নাম, সার্ভার ইউআরএল এবং টোকেন প্রয়োজন।", - "saved": "গোপন উৎস সংরক্ষিত", - "deleted": "গোপন সূত্র মুছে ফেলা হয়েছে", - "settings": { - "admin": { - "privateEndpoints": { - "label": "ব্যক্তিগত এন্ডপয়েন্ট অনুমোদিত তালিকা", - "description": "একটি গোপন উৎস যেসব প্রাইভেট বা লুপব্যাক হোস্টে পৌঁছাতে পারে, প্রতি লাইনে একটি করে। একটি সেলফ-হোস্টেড 1Password Connect সার্ভারের জন্য সাধারণত এখানে তার নিজস্ব ঠিকানা তালিকাভুক্ত করার প্রয়োজন হয়।" - } - } - }, - "plugin": { - "name": "গোপন সূত্র" - } -} diff --git a/plugins/secret-sources/locales/translated/ca_ES.json b/plugins/secret-sources/locales/translated/ca_ES.json deleted file mode 100644 index 5c1d9db70..000000000 --- a/plugins/secret-sources/locales/translated/ca_ES.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Podeu enganxar una referència 1Password (op://vault/item/field) en comptes del secret. S'obté de la font del secret quan us connecteu.", - "manage": "Gestiona fonts secretes", - "title": "Fonts secretes", - "description": "Els gestors de contrasenyes externes Termix llegeixen els secrets en el moment de la connexió. Actualment 1Password Connect (autoallotjat). El testimoni d'accés s'emmagatzema xifrat amb la vostra clau de dades.", - "new": "Nova font", - "nameLabel": "Nom amigable", - "urlLabel": "URL de connexió del servidor", - "tokenLabel": "Connecta el testimoni d'accés", - "tokenKeep": "Deixeu-ho en blanc per mantenir el testimoni actual", - "sharedLabel": "Comparteix amb tots els usuaris (només administradors, resol mentre tens la sessió iniciada)", - "shared": "compartit", - "test": "Prova", - "testOk": "Connectat, {{count}} volta(es) visible(s)", - "testFailed": "La prova de connexió ha fallat", - "required": "El nom, l'URL del servidor i el testimoni són obligatoris", - "saved": "Font secreta desada", - "deleted": "Font secreta suprimida", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Llista permesa de punts finals privats", - "description": "Amfitrions privats o de bucle que una font secreta pot assolir, un per línia. Un servidor 1Password Connect autoallotjat normalment necessita la seva pròpia adreça llistada aquí." - } - } - }, - "plugin": { - "name": "Fonts secretes" - } -} diff --git a/plugins/secret-sources/locales/translated/cs_CZ.json b/plugins/secret-sources/locales/translated/cs_CZ.json deleted file mode 100644 index ea934a074..000000000 --- a/plugins/secret-sources/locales/translated/cs_CZ.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Místo tajného hesla můžete vložit odkaz na heslo 1Password (op://vault/item/field). Ten se načte ze zdroje vašeho tajného hesla při připojování.", - "manage": "Správa tajných zdrojů", - "title": "Tajné zdroje", - "description": "Externí správci hesel Termix čte tajné hesla v době připojení. Aktuálně 1Password Connect (samostatně hostovaný). Přístupový token je uložen zašifrovaný s vaším datovým klíčem.", - "new": "Nový zdroj", - "nameLabel": "Přátelský název", - "urlLabel": "URL adresa serveru pro připojení", - "tokenLabel": "Připojit přístupový token", - "tokenKeep": "Pro zachování aktuálního tokenu ponechte prázdné.", - "sharedLabel": "Sdílet se všemi uživateli (pouze pro administrátory, řeší se, když jste přihlášeni)", - "shared": "sdílené", - "test": "Test", - "testOk": "Připojeno, viditelné {{count}} úložiště", - "testFailed": "Test připojení selhal", - "required": "Jméno, URL adresa serveru a token jsou povinné", - "saved": "Tajný zdroj uložen", - "deleted": "Tajný zdroj smazán", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Seznam povolených soukromých koncových bodů", - "description": "Soukromé nebo zpětně loopbackové hostitele, na které může tajný zdroj dosáhnout, jeden na řádek. Samostatně hostovaný server 1Password Connect zde obvykle potřebuje uvedenou vlastní adresu." - } - } - }, - "plugin": { - "name": "Tajné zdroje" - } -} diff --git a/plugins/secret-sources/locales/translated/da_DK.json b/plugins/secret-sources/locales/translated/da_DK.json deleted file mode 100644 index 8b16eb6ed..000000000 --- a/plugins/secret-sources/locales/translated/da_DK.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Du kan indsætte en 1Password-reference (op://vault/item/field) i stedet for hemmeligheden. Den hentes fra din hemmelige kilde, når du opretter forbindelse.", - "manage": "Administrer hemmelige kilder", - "title": "Hemmelige kilder", - "description": "Eksterne adgangskodeadministratorer. Termix læser hemmeligheder fra ved tilslutning. I øjeblikket 1Password Connect (selvhostet). Adgangstokenet gemmes krypteret med din datanøgle.", - "new": "Ny kilde", - "nameLabel": "Venligt navn", - "urlLabel": "URL til forbindelsesserver", - "tokenLabel": "Forbind adgangstoken", - "tokenKeep": "Lad feltet stå tomt for at beholde det aktuelle token", - "sharedLabel": "Del med alle brugere (kun administratorer, løses mens du er logget ind)", - "shared": "delt", - "test": "Prøve", - "testOk": "Forbundet, {{count}} boks(e) synlig(e)", - "testFailed": "Forbindelsestest mislykkedes", - "required": "Navn, server-URL og token er påkrævet", - "saved": "Hemmelig kilde gemt", - "deleted": "Hemmelig kilde slettet", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Tilladelsesliste for privat slutpunkt", - "description": "Private eller loopback-værter, som en hemmelig kilde kan nå, én pr. linje. En selvhostet 1Password Connect-server skal normalt have sin egen adresse angivet her." - } - } - }, - "plugin": { - "name": "Hemmelige kilder" - } -} diff --git a/plugins/secret-sources/locales/translated/de_DE.json b/plugins/secret-sources/locales/translated/de_DE.json deleted file mode 100644 index 041bd2338..000000000 --- a/plugins/secret-sources/locales/translated/de_DE.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Sie können anstelle des Geheimnisses eine 1Password-Referenz (op://vault/item/field) einfügen. Diese wird beim Verbindungsaufbau aus Ihrer Geheimnisquelle abgerufen.", - "manage": "Geheime Quellen verwalten", - "title": "Geheime Quellen", - "description": "Externe Passwortmanager wie Termix lesen die Geheimnisse beim Verbindungsaufbau. Aktuell wird 1Password Connect (selbst gehostet) verwendet. Das Zugriffstoken wird mit Ihrem Datenschlüssel verschlüsselt gespeichert.", - "new": "Neue Quelle", - "nameLabel": "Freundlicher Name", - "urlLabel": "Verbindungsserver-URL", - "tokenLabel": "Zugriffstoken verbinden", - "tokenKeep": "Lassen Sie dieses Feld leer, um das aktuelle Token beizubehalten.", - "sharedLabel": "Mit allen Benutzern teilen (nur für Administratoren, wird ausgeführt, solange Sie angemeldet sind)", - "shared": "geteilt", - "test": "Test", - "testOk": "Verbunden, {{count}} Tresor(e) sichtbar", - "testFailed": "Verbindungstest fehlgeschlagen", - "required": "Name, Server-URL und Token werden benötigt.", - "saved": "Geheime Quelle gerettet", - "deleted": "Geheime Quelle gelöscht", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Zulassungsliste für private Endpunkte", - "description": "Private oder Loopback-Hosts, die eine geheime Quelle erreichen kann, jeweils einer pro Zeile. Ein selbst gehosteter 1Password Connect-Server benötigt in der Regel eine eigene Adresse, die hier aufgeführt wird." - } - } - }, - "plugin": { - "name": "Geheime Quellen" - } -} diff --git a/plugins/secret-sources/locales/translated/el_GR.json b/plugins/secret-sources/locales/translated/el_GR.json deleted file mode 100644 index 2a8a52f60..000000000 --- a/plugins/secret-sources/locales/translated/el_GR.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Μπορείτε να επικολλήσετε μια αναφορά 1Password (op://vault/item/field) αντί για το μυστικό. Λαμβάνεται από την μυστική σας πηγή κατά τη σύνδεση.", - "manage": "Διαχείριση μυστικών πηγών", - "title": "Μυστικές πηγές", - "description": "Οι εξωτερικοί διαχειριστές κωδικών πρόσβασης Termix διαβάζουν μυστικά κατά τη στιγμή της σύνδεσης. Προς το παρόν 1Password Connect (αυτοφιλοξενείται). Το διακριτικό πρόσβασης αποθηκεύεται κρυπτογραφημένο με το κλειδί δεδομένων σας.", - "new": "Νέα πηγή", - "nameLabel": "Φιλικό όνομα", - "urlLabel": "Σύνδεση URL διακομιστή", - "tokenLabel": "Σύνδεση διακριτικού πρόσβασης", - "tokenKeep": "Αφήστε κενό για να διατηρήσετε το τρέχον διακριτικό", - "sharedLabel": "Κοινή χρήση με όλους τους χρήστες (μόνο για διαχειριστές, επιλύεται ενώ είστε συνδεδεμένοι)", - "shared": "κοινόχρηστο", - "test": "Δοκιμή", - "testOk": "Συνδεδεμένο, {{count}} ορατό(α) θησαυροφυλάκιο(α)", - "testFailed": "Η δοκιμή σύνδεσης απέτυχε.", - "required": "Απαιτούνται όνομα, διεύθυνση URL διακομιστή και διακριτικό", - "saved": "Η μυστική πηγή αποθηκεύτηκε", - "deleted": "Η μυστική πηγή διαγράφηκε", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Λίστα επιτρεπόμενων ιδιωτικών τελικών σημείων", - "description": "Ιδιωτικοί ή loopback hosts στους οποίους μπορεί να φτάσει μια μυστική πηγή, ένας ανά γραμμή. Ένας αυτοφιλοξενούμενος διακομιστής 1Password Connect συνήθως χρειάζεται τη δική του διεύθυνση να αναφέρεται εδώ." - } - } - }, - "plugin": { - "name": "Μυστικές Πηγές" - } -} diff --git a/plugins/secret-sources/locales/translated/es_ES.json b/plugins/secret-sources/locales/translated/es_ES.json deleted file mode 100644 index 6b68a042b..000000000 --- a/plugins/secret-sources/locales/translated/es_ES.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Puedes pegar una referencia de 1Password (op://vault/item/field) en lugar de la clave secreta. Esta se obtiene de tu fuente de claves secretas al conectarte.", - "manage": "Gestionar fuentes secretas", - "title": "Fuentes secretas", - "description": "Termix lee los secretos de los gestores de contraseñas externos al conectarse. Actualmente, utiliza 1Password Connect (autoalojado). El token de acceso se almacena cifrado con su clave de datos.", - "new": "Nueva fuente", - "nameLabel": "Nombre amistoso", - "urlLabel": "URL del servidor de conexión", - "tokenLabel": "Token de acceso de Connect", - "tokenKeep": "Déjelo vacío para conservar el token actual.", - "sharedLabel": "Compartir con todos los usuarios (solo administradores, se resuelve mientras estés conectado).", - "shared": "compartido", - "test": "Probar", - "testOk": "Conectado(s), {{count}} bóveda(s) visible(s)", - "testFailed": "La prueba de conexión falló.", - "required": "Se requieren nombre, URL del servidor y token.", - "saved": "Fuente secreta guardada", - "deleted": "Fuente secreta eliminada", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Lista de permitidos de puntos finales privados", - "description": "Hosts privados o de bucle invertido a los que puede acceder una fuente secreta, uno por línea. Un servidor 1Password Connect autohospedado generalmente necesita su propia dirección IP que aparezca aquí." - } - } - }, - "plugin": { - "name": "Fuentes secretas" - } -} diff --git a/plugins/secret-sources/locales/translated/fi_FI.json b/plugins/secret-sources/locales/translated/fi_FI.json deleted file mode 100644 index 49c1fe064..000000000 --- a/plugins/secret-sources/locales/translated/fi_FI.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Voit liittää 1Password-viitteen (op://vault/item/field) salaisuuden sijaan. Se noudetaan salaisuuden lähteestäsi yhteyden muodostamisen yhteydessä.", - "manage": "Hallitse salaisia lähteitä", - "title": "Salaiset lähteet", - "description": "Ulkoiset salasananhallintaohjelmat Termix lukevat salaisuudet yhteyden muodostamisen yhteydessä. Tällä hetkellä käytössä on 1Password Connect (itse isännöity). Käyttöoikeustunnus tallennetaan salattuna dataavaimellasi.", - "new": "Uusi lähde", - "nameLabel": "Ystävällinen nimi", - "urlLabel": "Yhdistä palvelimen URL-osoite", - "tokenLabel": "Yhdistä käyttöoikeustunnus", - "tokenKeep": "Jätä tyhjäksi säilyttääksesi nykyisen tunnuksen", - "sharedLabel": "Jaa kaikkien käyttäjien kanssa (vain järjestelmänvalvojille, ratkeaa, kun olet kirjautuneena sisään)", - "shared": "jaettu", - "test": "Testata", - "testOk": "Yhdistetty, {{count}} holvia näkyvissä", - "testFailed": "Yhteystesti epäonnistui", - "required": "Nimi, palvelimen URL-osoite ja tunnus vaaditaan", - "saved": "Salainen lähde tallennettu", - "deleted": "Salainen lähde poistettu", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Yksityisten päätepisteiden sallittujen luettelo", - "description": "Yksityiset tai loopback-isännät, joihin salainen lähde voi tavoittaa, yksi riviä kohden. Itse isännöity 1Password Connect -palvelin tarvitsee yleensä oman osoitteensa, joka luetellaan tässä." - } - } - }, - "plugin": { - "name": "Salaiset lähteet" - } -} diff --git a/plugins/secret-sources/locales/translated/fr_FR.json b/plugins/secret-sources/locales/translated/fr_FR.json deleted file mode 100644 index a7500946f..000000000 --- a/plugins/secret-sources/locales/translated/fr_FR.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Vous pouvez coller une référence 1Password (op://vault/item/field) à la place du mot de passe. Elle sera récupérée depuis votre source de mots de passe lors de la connexion.", - "manage": "Gérer les sources secrètes", - "title": "Sources secrètes", - "description": "Termix utilise des gestionnaires de mots de passe externes pour lire les secrets lors de la connexion. Actuellement, il s'agit de 1Password Connect (hébergé sur place). Le jeton d'accès est stocké chiffré avec votre clé de données.", - "new": "Nouvelle source", - "nameLabel": "Nom amical", - "urlLabel": "URL du serveur de connexion", - "tokenLabel": "Jeton d'accès Connect", - "tokenKeep": "Laissez vide pour conserver le jeton actuel", - "sharedLabel": "Partager avec tous les utilisateurs (administrateurs uniquement, résolution effectuée lorsque vous êtes connecté)", - "shared": "partagé", - "test": "Tester", - "testOk": "Connecté, {{count}} coffre(s) visible(s)", - "testFailed": "Le test de connexion a échoué.", - "required": "Le nom, l'URL du serveur et le jeton sont requis.", - "saved": "Source secrète sauvegardée", - "deleted": "Source secrète supprimée", - "settings": { - "admin": { - "privateEndpoints": { - "label": "liste blanche des points de terminaison privés", - "description": "Hôtes privés ou de bouclage auxquels une source secrète peut accéder, un par ligne. Un serveur 1Password Connect auto-hébergé nécessite généralement sa propre adresse indiquée ici." - } - } - }, - "plugin": { - "name": "Sources secrètes" - } -} diff --git a/plugins/secret-sources/locales/translated/he_IL.json b/plugins/secret-sources/locales/translated/he_IL.json deleted file mode 100644 index eb42bfcee..000000000 --- a/plugins/secret-sources/locales/translated/he_IL.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "ניתן להדביק הפניה ל-1Password (op://vault/item/field) במקום הסוד. היא נלקחת ממקור הסוד שלך בעת ההתחברות.", - "manage": "ניהול מקורות סודיים", - "title": "מקורות סודיים", - "description": "מנהלי סיסמאות חיצוניים של Termix קוראים סודות מ- בזמן ההתחברות. נכון לעכשיו, 1Password Connect (מארח עצמי). אסימון הגישה מאוחסן מוצפן עם מפתח הנתונים שלך.", - "new": "מקור חדש", - "nameLabel": "שם ידידותי", - "urlLabel": "כתובת URL של שרת התחברות", - "tokenLabel": "אסימון גישה להתחברות", - "tokenKeep": "השאר ריק כדי לשמור את האסימון הנוכחי", - "sharedLabel": "שתף עם כל המשתמשים (מנהלים בלבד, נפתר בזמן שאתה מחובר)", - "shared": "מְשׁוּתָף", - "test": "מִבְחָן", - "testOk": "מחובר, כספת/ות {{count}} גלויה/יות", - "testFailed": "בדיקת החיבור נכשלה", - "required": "נדרשים שם, כתובת URL של השרת וטופס", - "saved": "מקור סודי נשמר", - "deleted": "מקור סודי נמחק", - "settings": { - "admin": { - "privateEndpoints": { - "label": "רשימת היתרים לנקודות קצה פרטיות", - "description": "מארחים פרטיים או לולאה-חוזרים אליהם מקור סודי עשוי להגיע, אחד בכל שורה. שרת 1Password Connect המתארח באופן עצמאי זקוק בדרך כלל לכתובת משלו הרשומה כאן." - } - } - }, - "plugin": { - "name": "מקורות סודיים" - } -} diff --git a/plugins/secret-sources/locales/translated/hi_IN.json b/plugins/secret-sources/locales/translated/hi_IN.json deleted file mode 100644 index 6ac2950ab..000000000 --- a/plugins/secret-sources/locales/translated/hi_IN.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "आप गुप्त पासवर्ड के स्थान पर 1Password संदर्भ (op://vault/item/field) पेस्ट कर सकते हैं। कनेक्ट करते समय यह आपके गुप्त पासवर्ड स्रोत से प्राप्त किया जाता है।", - "manage": "गुप्त स्रोतों का प्रबंधन करें", - "title": "गुप्त सूत्रों", - "description": "टर्मिक्स कनेक्ट होने के समय बाहरी पासवर्ड मैनेजर से गुप्त जानकारी पढ़ता है। वर्तमान में 1Password Connect (स्वयं-होस्टेड) का उपयोग किया जा रहा है। एक्सेस टोकन आपके डेटा कुंजी के साथ एन्क्रिप्टेड रूप में संग्रहीत किया जाता है।", - "new": "नए स्रोत", - "nameLabel": "दोस्ताना नाम", - "urlLabel": "सर्वर यूआरएल से कनेक्ट करें", - "tokenLabel": "एक्सेस टोकन कनेक्ट करें", - "tokenKeep": "वर्तमान टोकन को बनाए रखने के लिए इसे खाली छोड़ दें", - "sharedLabel": "सभी उपयोगकर्ताओं के साथ साझा करें (केवल व्यवस्थापकों के लिए, आपके लॉग इन होने पर ही समस्या का समाधान हो जाएगा)", - "shared": "साझा", - "test": "परीक्षण", - "testOk": "कनेक्टेड, {{count}} वॉल्ट दिखाई दे रहे हैं", - "testFailed": "कनेक्शन परीक्षण विफल रहा", - "required": "नाम, सर्वर यूआरएल और टोकन आवश्यक हैं।", - "saved": "गुप्त स्रोत सुरक्षित", - "deleted": "गुप्त सूत्र हटा दिया गया", - "settings": { - "admin": { - "privateEndpoints": { - "label": "निजी एंडपॉइंट अनुमति सूची", - "description": "निजी या लूपबैक होस्ट तक एक गुप्त स्रोत पहुंच सकता है, प्रति पंक्ति एक। एक स्व-होस्टेड 1Password Connect सर्वर को आमतौर पर यहां अपना पता सूचीबद्ध करने की आवश्यकता होती है।" - } - } - }, - "plugin": { - "name": "गुप्त सूत्रों" - } -} diff --git a/plugins/secret-sources/locales/translated/hu_HU.json b/plugins/secret-sources/locales/translated/hu_HU.json deleted file mode 100644 index 2ebc65e7f..000000000 --- a/plugins/secret-sources/locales/translated/hu_HU.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "A titkos kód helyett beilleszthetsz egy 1Password hivatkozást (op://vault/item/field). A titkos kód a csatlakozáskor a forrásból lesz lekérve.", - "manage": "Titkos források kezelése", - "title": "Titkos források", - "description": "Külső jelszókezelők: A Termix a csatlakozáskor olvassa be a titkos kódokat. Jelenleg az 1Password Connect (saját tárhelyen). A hozzáférési token titkosítva tárolódik az adatkulccsal.", - "new": "Új forrás", - "nameLabel": "Barátságos név", - "urlLabel": "Kapcsolódási szerver URL-címe", - "tokenLabel": "Hozzáférési token csatlakoztatása", - "tokenKeep": "Hagyja üresen az aktuális token megtartásához", - "sharedLabel": "Megosztás az összes felhasználóval (csak rendszergazdák, bejelentkezés után oldódik meg)", - "shared": "megosztott", - "test": "Teszt", - "testOk": "Csatlakoztatva, {{count}} trezor látható", - "testFailed": "A kapcsolat tesztje sikertelen", - "required": "Név, szerver URL és token megadása kötelező", - "saved": "Titkos forrás mentve", - "deleted": "Titkos forrás törölve", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Privát végpontok engedélyezőlistája", - "description": "Privát vagy visszacsatolásos hosztok, amelyeket egy titkos forrás elérhet, soronként egy. Egy saját hosztolt 1Password Connect szervernek általában saját címre van szüksége, amely itt szerepel." - } - } - }, - "plugin": { - "name": "Titkos források" - } -} diff --git a/plugins/secret-sources/locales/translated/id_ID.json b/plugins/secret-sources/locales/translated/id_ID.json deleted file mode 100644 index de935ff9e..000000000 --- a/plugins/secret-sources/locales/translated/id_ID.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Anda dapat menempelkan referensi 1Password (op://vault/item/field) sebagai pengganti rahasia. Referensi tersebut akan diambil dari sumber rahasia Anda saat terhubung.", - "manage": "Mengelola sumber rahasia", - "title": "Sumber rahasia", - "description": "Termix membaca rahasia dari pengelola kata sandi eksternal pada saat koneksi. Saat ini menggunakan 1Password Connect (yang dihosting sendiri). Token akses disimpan dalam bentuk terenkripsi dengan kunci data Anda.", - "new": "Sumber baru", - "nameLabel": "Nama yang ramah", - "urlLabel": "Hubungkan URL server", - "tokenLabel": "Hubungkan token akses", - "tokenKeep": "Biarkan kosong untuk mempertahankan token saat ini.", - "sharedLabel": "Bagikan dengan semua pengguna (hanya admin, akan aktif saat Anda masuk)", - "shared": "dibagikan", - "test": "Tes", - "testOk": "Terhubung, {{count}} brankas terlihat", - "testFailed": "Pengujian koneksi gagal", - "required": "Nama, URL server, dan token wajib diisi.", - "saved": "Sumber rahasia tersimpan", - "deleted": "Sumber rahasia telah dihapus.", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Daftar izin titik akhir privat", - "description": "Host privat atau loopback yang dapat dijangkau oleh sumber rahasia, satu per baris. Server 1Password Connect yang dihosting sendiri biasanya memerlukan alamatnya sendiri yang tercantum di sini." - } - } - }, - "plugin": { - "name": "Sumber Rahasia" - } -} diff --git a/plugins/secret-sources/locales/translated/it_IT.json b/plugins/secret-sources/locales/translated/it_IT.json deleted file mode 100644 index f76294067..000000000 --- a/plugins/secret-sources/locales/translated/it_IT.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Al posto del segreto, puoi incollare un riferimento a 1Password (op://vault/item/field). Questo verrà recuperato dalla tua fonte di segreti al momento della connessione.", - "manage": "Gestire le fonti segrete", - "title": "Fonti segrete", - "description": "I gestori di password esterni Termix leggono le credenziali al momento della connessione. Attualmente, 1Password Connect (self-hosted) è il gestore di password esterno. Il token di accesso viene memorizzato crittografato insieme alla chiave dati.", - "new": "Nuova fonte", - "nameLabel": "Nome amichevole", - "urlLabel": "URL del server di connessione", - "tokenLabel": "Connetti il token di accesso", - "tokenKeep": "Lascia vuoto per mantenere il token corrente", - "sharedLabel": "Condividi con tutti gli utenti (solo amministratori, la modifica si applica solo se si è connessi)", - "shared": "condiviso", - "test": "Test", - "testOk": "Connesso, {{count}} caveau visibile(i)", - "testFailed": "Test di connessione fallito", - "required": "Sono richiesti nome, URL del server e token.", - "saved": "fonte segreta salvata", - "deleted": "Fonte segreta eliminata", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Lista di endpoint privati consentiti", - "description": "Host privati o di loopback raggiungibili da una fonte segreta, uno per riga. Un server 1Password Connect self-hosted di solito necessita di un proprio indirizzo elencato qui." - } - } - }, - "plugin": { - "name": "Fonti segrete" - } -} diff --git a/plugins/secret-sources/locales/translated/ja_JP.json b/plugins/secret-sources/locales/translated/ja_JP.json deleted file mode 100644 index d82e39f40..000000000 --- a/plugins/secret-sources/locales/translated/ja_JP.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "シークレットの代わりに、1Passwordの参照(op://vault/item/field)を貼り付けることができます。これは接続時にシークレットソースから取得されます。", - "manage": "秘密情報源の管理", - "title": "秘密の情報源", - "description": "Termixは、接続時に外部パスワードマネージャーから秘密情報を読み取ります。現在、1Password Connect(セルフホスト型)に対応しています。アクセストークンは、お客様のデータキーで暗号化されて保存されます。", - "new": "新しい情報源", - "nameLabel": "親しみやすい名前", - "urlLabel": "接続サーバーURL", - "tokenLabel": "接続アクセストークン", - "tokenKeep": "現在のトークンを保持するには空欄のままにしてください。", - "sharedLabel": "すべてのユーザーと共有(管理者のみ、ログイン中に解決されます)", - "shared": "共有", - "test": "テスト", - "testOk": "接続済み、 {{count}} 個の保管庫が表示されています", - "testFailed": "接続テストに失敗しました", - "required": "名前、サーバーURL、トークンは必須です。", - "saved": "秘密の情報源が保存されました", - "deleted": "秘密情報源を削除", - "settings": { - "admin": { - "privateEndpoints": { - "label": "プライベートエンドポイントの許可リスト", - "description": "秘密情報源がアクセスできるプライベートホストまたはループバックホストを、1行に1つずつ記述します。自己ホスト型の1Password Connectサーバーの場合、通常はここに独自のIPアドレスを記載する必要があります。" - } - } - }, - "plugin": { - "name": "秘密情報源" - } -} diff --git a/plugins/secret-sources/locales/translated/ko_KR.json b/plugins/secret-sources/locales/translated/ko_KR.json deleted file mode 100644 index 14582ee9c..000000000 --- a/plugins/secret-sources/locales/translated/ko_KR.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "비밀 키 대신 1Password 참조(op://vault/item/field)를 붙여넣을 수 있습니다. 연결 시 비밀 키 소스에서 해당 참조를 가져옵니다.", - "manage": "비밀 소스를 관리하세요", - "title": "비밀 정보원", - "description": "Termix는 외부 암호 관리자(현재 1Password Connect(자체 호스팅))에서 연결 시 비밀 키를 읽어옵니다. 액세스 토큰은 데이터 키로 암호화되어 저장됩니다.", - "new": "새로운 소스", - "nameLabel": "친근한 이름", - "urlLabel": "서버 URL에 연결", - "tokenLabel": "연결 액세스 토큰", - "tokenKeep": "현재 토큰을 유지하려면 비워 두십시오.", - "sharedLabel": "모든 사용자와 공유 (관리자만 가능, 로그인 상태에서 해결됨)", - "shared": "공유", - "test": "테스트", - "testOk": "연결됨, {{count}} 금고가 보입니다", - "testFailed": "연결 테스트 실패", - "required": "이름, 서버 URL 및 토큰은 필수 입력 사항입니다.", - "saved": "비밀 소스 저장됨", - "deleted": "비밀 소스 삭제됨", - "settings": { - "admin": { - "privateEndpoints": { - "label": "개인 엔드포인트 허용 목록", - "description": "비밀 소스가 접근할 수 있는 개인 또는 루프백 호스트를 한 줄에 하나씩 나열합니다. 자체 호스팅 1Password Connect 서버는 일반적으로 여기에 자체 주소를 나열해야 합니다." - } - } - }, - "plugin": { - "name": "비밀 정보원" - } -} diff --git a/plugins/secret-sources/locales/translated/nl_NL.json b/plugins/secret-sources/locales/translated/nl_NL.json deleted file mode 100644 index 45d1af905..000000000 --- a/plugins/secret-sources/locales/translated/nl_NL.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Je kunt in plaats van het geheim een 1Password-referentie (op://vault/item/field) plakken. Deze wordt opgehaald uit je geheime bron wanneer je verbinding maakt.", - "manage": "Beheer geheime bronnen", - "title": "Geheime bronnen", - "description": "Externe wachtwoordmanagers die Termix gebruikt, lezen de geheimen uit tijdens het verbinden. Momenteel is dat 1Password Connect (zelf gehost). Het toegangstoken wordt versleuteld opgeslagen met uw gegevenssleutel.", - "new": "Nieuwe bron", - "nameLabel": "Vriendelijke naam", - "urlLabel": "Verbindingsserver-URL", - "tokenLabel": "Verbindingstoegangstoken", - "tokenKeep": "Laat dit veld leeg om het huidige token te behouden.", - "sharedLabel": "Delen met alle gebruikers (alleen beheerders, wordt opgelost terwijl u bent ingelogd)", - "shared": "gedeeld", - "test": "Test", - "testOk": "Verbonden, {{count}} kluis(en) zichtbaar", - "testFailed": "Verbindingstest mislukt", - "required": "Naam, server-URL en token zijn vereist.", - "saved": "Geheime bron opgeslagen", - "deleted": "Geheime bron verwijderd", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Toegestane lijst voor privé-eindpunten", - "description": "Privé- of loopback-hosts die een geheime bron kunnen bereiken, één per regel. Een zelfgehoste 1Password Connect-server heeft meestal een eigen adres nodig dat hier vermeld moet worden." - } - } - }, - "plugin": { - "name": "Geheime bronnen" - } -} diff --git a/plugins/secret-sources/locales/translated/no_NO.json b/plugins/secret-sources/locales/translated/no_NO.json deleted file mode 100644 index cab1424cf..000000000 --- a/plugins/secret-sources/locales/translated/no_NO.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Du kan lime inn en 1Password-referanse (op://vault/item/field) i stedet for hemmeligheten. Den hentes fra hemmelighetskilden din når du kobler til.", - "manage": "Administrer hemmelige kilder", - "title": "Hemmelige kilder", - "description": "Eksterne passordbehandlere Termix leser hemmeligheter fra ved tilkobling. For tiden 1Password Connect (selvhostet). Tilgangstokenet lagres kryptert med datanøkkelen din.", - "new": "Ny kilde", - "nameLabel": "Vennlig navn", - "urlLabel": "URL-adresse for tilkobling av server", - "tokenLabel": "Koble til tilgangstoken", - "tokenKeep": "La stå tomt for å beholde gjeldende token", - "sharedLabel": "Del med alle brukere (kun administratorer, løses mens du er logget på)", - "shared": "delt", - "test": "Test", - "testOk": "Tilkoblet, {{count}} hvelv synlig(e)", - "testFailed": "Tilkoblingstesten mislyktes", - "required": "Navn, server-URL og token er obligatorisk", - "saved": "Hemmelig kilde lagret", - "deleted": "Hemmelig kilde slettet", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Tillatelsesliste for privat endepunkt", - "description": "Private verter eller loopback-verter en hemmelig kilde kan nå, én per linje. En selvhostet 1Password Connect-server trenger vanligvis sin egen adresse oppført her." - } - } - }, - "plugin": { - "name": "Hemmelige kilder" - } -} diff --git a/plugins/secret-sources/locales/translated/pl_PL.json b/plugins/secret-sources/locales/translated/pl_PL.json deleted file mode 100644 index e8dc3812f..000000000 --- a/plugins/secret-sources/locales/translated/pl_PL.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Zamiast klucza tajnego możesz wkleić odnośnik do 1Password (op://vault/item/field). Zostanie on pobrany ze źródła klucza tajnego podczas łączenia.", - "manage": "Zarządzaj tajnymi źródłami", - "title": "Tajne źródła", - "description": "Zewnętrzny menedżer haseł Termix odczytuje sekrety w momencie połączenia. Obecnie 1Password Connect (hostowany samodzielnie). Token dostępu jest przechowywany w postaci zaszyfrowanej z Twoim kluczem danych.", - "new": "Nowe źródło", - "nameLabel": "Przyjazna nazwa", - "urlLabel": "Połącz adres URL serwera", - "tokenLabel": "Połącz token dostępu", - "tokenKeep": "Pozostaw puste, aby zachować bieżący token", - "sharedLabel": "Udostępnij wszystkim użytkownikom (tylko administratorzy, rozwiązuje się, gdy jesteś zalogowany)", - "shared": "współdzielony", - "test": "Test", - "testOk": "Połączono, {{count}} widocznych skarbców", - "testFailed": "Test połączenia nie powiódł się", - "required": "Wymagane są nazwa, adres URL serwera i token", - "saved": "Tajne źródło uratowane", - "deleted": "Usunięto tajne źródło", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Prywatna lista dozwolonych punktów końcowych", - "description": "Prywatne lub zwrotne hosty, do których może dotrzeć tajne źródło, po jednym na linię. Samodzielnie hostowany serwer 1Password Connect zazwyczaj wymaga podania tutaj własnego adresu." - } - } - }, - "plugin": { - "name": "Tajne źródła" - } -} diff --git a/plugins/secret-sources/locales/translated/pt_BR.json b/plugins/secret-sources/locales/translated/pt_BR.json deleted file mode 100644 index 74d3f04f1..000000000 --- a/plugins/secret-sources/locales/translated/pt_BR.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Você pode colar uma referência do 1Password (op://vault/item/field) em vez do segredo. Ela é obtida da sua fonte de segredos ao conectar.", - "manage": "Gerenciar fontes secretas", - "title": "Fontes secretas", - "description": "O Termix lê os segredos de gerenciadores de senhas externos no momento da conexão. Atualmente, utiliza o 1Password Connect (hospedado localmente). O token de acesso é armazenado criptografado com sua chave de dados.", - "new": "Nova fonte", - "nameLabel": "Nome amigável", - "urlLabel": "URL de conexão do servidor", - "tokenLabel": "Token de acesso Connect", - "tokenKeep": "Deixe em branco para manter o token atual.", - "sharedLabel": "Compartilhar com todos os usuários (somente administradores, funciona enquanto você estiver conectado)", - "shared": "compartilhado", - "test": "Teste", - "testOk": "Conectado, {{count}} cofre(s) visível(is)", - "testFailed": "Teste de conexão falhou", - "required": "Nome, URL do servidor e token são obrigatórios.", - "saved": "Fonte secreta salva", - "deleted": "Fonte secreta excluída", - "settings": { - "admin": { - "privateEndpoints": { - "label": "lista de permissões de endpoints privados", - "description": "Hosts privados ou de loopback que uma fonte secreta pode acessar, um por linha. Um servidor 1Password Connect autohospedado geralmente precisa ter seu próprio endereço listado aqui." - } - } - }, - "plugin": { - "name": "Fontes secretas" - } -} diff --git a/plugins/secret-sources/locales/translated/pt_PT.json b/plugins/secret-sources/locales/translated/pt_PT.json deleted file mode 100644 index 82ba6a8c6..000000000 --- a/plugins/secret-sources/locales/translated/pt_PT.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Pode colar uma referência do 1Password (op://vault/item/field) em vez do segredo. É obtida a partir da sua fonte de segredos ao conectar.", - "manage": "Gerir fontes secretas", - "title": "Fontes secretas", - "description": "O Termix lê os segredos dos gestores de palavras-passe externos no momento da ligação. Atualmente utiliza o 1Password Connect (alojado localmente). O token de acesso é armazenado encriptado com a sua chave de dados.", - "new": "Nova fonte", - "nameLabel": "Nome amigável", - "urlLabel": "URL de ligação do servidor", - "tokenLabel": "Token de acesso Connect", - "tokenKeep": "Deixe em branco para manter o token atual.", - "sharedLabel": "Partilhar com todos os utilizadores (apenas administradores, funciona enquanto estiver ligado)", - "shared": "partilhado", - "test": "Testar", - "testOk": "Ligado, {{count}} cofre(s) visível(is)", - "testFailed": "Teste de ligação falhou", - "required": "Nome, URL do servidor e token são obrigatórios.", - "saved": "Fonte secreta guardada", - "deleted": "Fonte secreta excluída", - "settings": { - "admin": { - "privateEndpoints": { - "label": "lista de permissões de endpoints privados", - "description": "Hosts privados ou de loopback a que uma fonte secreta pode aceder, um por linha. Um servidor 1Password Connect auto-hospedado precisa geralmente de ter o seu próprio endereço listado aqui." - } - } - }, - "plugin": { - "name": "Fontes secretas" - } -} diff --git a/plugins/secret-sources/locales/translated/ro_RO.json b/plugins/secret-sources/locales/translated/ro_RO.json deleted file mode 100644 index 7d85a859d..000000000 --- a/plugins/secret-sources/locales/translated/ro_RO.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Puteți lipi o referință 1Password (op://vault/item/field) în loc de secret. Aceasta este preluată din sursa secretului dvs. la conectare.", - "manage": "Gestionați sursele secrete", - "title": "Surse secrete", - "description": "Managerii de parole externi Termix citesc secretele la momentul conectării. În prezent, 1Password Connect (găzduit automat). Tokenul de acces este stocat criptat cu cheia dvs. de date.", - "new": "Sursă nouă", - "nameLabel": "Nume prietenos", - "urlLabel": "Conectați adresa URL a serverului", - "tokenLabel": "Conectare jeton de acces", - "tokenKeep": "Lăsați gol pentru a păstra token-ul curent", - "sharedLabel": "Partajează cu toți utilizatorii (doar pentru administratori, se rezolvă în timp ce ești conectat)", - "shared": "partajat", - "test": "Test", - "testOk": "Conectat, {{count}} seif(uri) vizibil(e)", - "testFailed": "Testul de conexiune a eșuat", - "required": "Numele, adresa URL a serverului și tokenul sunt obligatorii", - "saved": "Sursă secretă salvată", - "deleted": "Sursa secretă a fost ștearsă", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Listă permisă pentru puncte de terminare private", - "description": "Gazde private sau loopback la care poate ajunge o sursă secretă, câte una pe linie. Un server 1Password Connect auto-găzduit are nevoie de obicei de propria adresă listată aici." - } - } - }, - "plugin": { - "name": "Surse secrete" - } -} diff --git a/plugins/secret-sources/locales/translated/ru_RU.json b/plugins/secret-sources/locales/translated/ru_RU.json deleted file mode 100644 index 1d67eeb90..000000000 --- a/plugins/secret-sources/locales/translated/ru_RU.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Вместо секретного ключа можно вставить ссылку на 1Password (op://vault/item/field). Она будет получена из вашего источника секретных ключей при подключении.", - "manage": "Управление секретными источниками", - "title": "Секретные источники", - "description": "Внешние менеджеры паролей Termix считывают секретные ключи во время подключения. В настоящее время используется 1Password Connect (самостоятельно размещенный). Токен доступа хранится в зашифрованном виде с помощью вашего ключа данных.", - "new": "Новый источник", - "nameLabel": "Дружелюбное имя", - "urlLabel": "URL-адрес сервера подключения", - "tokenLabel": "Токен доступа для подключения", - "tokenKeep": "Оставьте поле пустым, чтобы сохранить текущий токен.", - "sharedLabel": "Предоставить доступ всем пользователям (только администраторам, проблема решается при авторизации).", - "shared": "общий", - "test": "Проверить", - "testOk": "Подключено, {{count}} хранилище(я) видны", - "testFailed": "Проверка соединения не удалась.", - "required": "Необходимо указать имя, URL-адрес сервера и токен.", - "saved": "Секретный источник сохранен", - "deleted": "Секретный источник удален", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Список разрешенных частных конечных точек", - "description": "Доступ к секретному источнику может осуществляться через частные или замыкающие хосты, по одному на строку. Самостоятельно размещенному серверу 1Password Connect обычно требуется собственный адрес, указанный здесь." - } - } - }, - "plugin": { - "name": "Секретные источники" - } -} diff --git a/plugins/secret-sources/locales/translated/sr_SP.json b/plugins/secret-sources/locales/translated/sr_SP.json deleted file mode 100644 index 684a5b533..000000000 --- a/plugins/secret-sources/locales/translated/sr_SP.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Можете да налепите референцу 1Password (op://vault/item/field) уместо тајне лозинке. Она се преузима из вашег тајног извора приликом повезивања.", - "manage": "Управљајте тајним изворима", - "title": "Тајни извори", - "description": "Спољни менаџери лозинки Termix чита тајне податке у време повезивања. Тренутно 1Password Connect (самостални хост). Приступни токен се чува шифровано помоћу вашег кључа података.", - "new": "Нови извор", - "nameLabel": "Пријатељско име", - "urlLabel": "URL адреса сервера за повезивање", - "tokenLabel": "Токен за приступ повезивању", - "tokenKeep": "Оставите празно да бисте задржали тренутни токен", - "sharedLabel": "Дели са свим корисницима (само за администраторе, решава се док сте пријављени)", - "shared": "дељено", - "test": "Тест", - "testOk": "Повезано, видљивих {{count}} трезора", - "testFailed": "Тест везе није успео", - "required": "Потребно је име, URL сервера и токен", - "saved": "Тајни извор сачуван", - "deleted": "Тајни извор је обрисан", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Листа дозвољених приватних крајњих тачака", - "description": "Приватни или loopback хостови које тајни извор може досегнути, по један у сваком реду. Самостално хостовани 1Password Connect сервер обично захтева сопствену адресу наведену овде." - } - } - }, - "plugin": { - "name": "Тајни извори" - } -} diff --git a/plugins/secret-sources/locales/translated/sv_SE.json b/plugins/secret-sources/locales/translated/sv_SE.json deleted file mode 100644 index 9b661dfa7..000000000 --- a/plugins/secret-sources/locales/translated/sv_SE.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Du kan klistra in en 1Password-referens (op://vault/item/field) istället för hemligheten. Den hämtas från din hemliga källa när du ansluter.", - "manage": "Hantera hemliga källor", - "title": "Hemliga källor", - "description": "Externa lösenordshanterare Termix läser hemligheter från vid anslutningstillfället. För närvarande 1Password Connect (egenhostad). Åtkomsttoken lagras krypterad med din datanyckel.", - "new": "Ny källa", - "nameLabel": "Vänligt namn", - "urlLabel": "Anslut server-URL", - "tokenLabel": "Anslut åtkomsttoken", - "tokenKeep": "Lämna tomt för att behålla den aktuella token", - "sharedLabel": "Dela med alla användare (endast administratörer, löses medan du är inloggad)", - "shared": "delad", - "test": "Testa", - "testOk": "Ansluten, {{count}} valv synliga", - "testFailed": "Anslutningstestet misslyckades", - "required": "Namn, server-URL och token krävs", - "saved": "Hemlig källa sparad", - "deleted": "Hemlig källa raderad", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Tillåtelselista för privat slutpunkt", - "description": "Privata eller loopback-värdar som en hemlig källa kan nå, en per rad. En självhostad 1Password Connect-server behöver vanligtvis sin egen adress listad här." - } - } - }, - "plugin": { - "name": "Hemliga källor" - } -} diff --git a/plugins/secret-sources/locales/translated/th_TH.json b/plugins/secret-sources/locales/translated/th_TH.json deleted file mode 100644 index 3f8710882..000000000 --- a/plugins/secret-sources/locales/translated/th_TH.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "คุณสามารถวางข้อมูลอ้างอิง 1Password (op://vault/item/field) แทนรหัสลับได้ ระบบจะดึงข้อมูลนี้จากแหล่งข้อมูลลับของคุณเมื่อเชื่อมต่อ", - "manage": "จัดการแหล่งข้อมูลลับ", - "title": "แหล่งข่าวลับ", - "description": "โปรแกรมจัดการรหัสผ่านภายนอก Termix จะอ่านข้อมูลลับจากอุปกรณ์เมื่อเชื่อมต่อ ปัจจุบันคือ 1Password Connect (แบบติดตั้งเอง) โทเค็นการเข้าถึงจะถูกจัดเก็บในรูปแบบเข้ารหัสพร้อมกับคีย์ข้อมูลของคุณ", - "new": "แหล่งข้อมูลใหม่", - "nameLabel": "ชื่อที่เป็นมิตร", - "urlLabel": "เชื่อมต่อ URL ของเซิร์ฟเวอร์", - "tokenLabel": "เชื่อมต่อโทเค็นการเข้าถึง", - "tokenKeep": "เว้นว่างไว้เพื่อคงโทเค็นปัจจุบันไว้", - "sharedLabel": "แชร์กับผู้ใช้ทั้งหมด (เฉพาะผู้ดูแลระบบเท่านั้น ใช้งานได้เฉพาะเมื่อคุณเข้าสู่ระบบอยู่)", - "shared": "แชร์", - "test": "ทดสอบ", - "testOk": "เชื่อมต่อแล้ว, มองเห็นห้องนิรภัย {{count}} ห้อง", - "testFailed": "การทดสอบการเชื่อมต่อล้มเหลว", - "required": "จำเป็นต้องระบุชื่อ ที่อยู่ URL ของเซิร์ฟเวอร์ และโทเค็น", - "saved": "แหล่งข่าวลับถูกบันทึกไว้แล้ว", - "deleted": "แหล่งข่าวลับถูกลบแล้ว", - "settings": { - "admin": { - "privateEndpoints": { - "label": "รายการอนุญาตปลายทางส่วนตัว", - "description": "โฮสต์ส่วนตัวหรือโฮสต์แบบลูปแบ็กที่แหล่งข้อมูลลับสามารถเข้าถึงได้ หนึ่งโฮสต์ต่อบรรทัด โดยปกติแล้วเซิร์ฟเวอร์ 1Password Connect ที่ติดตั้งเองจะต้องมีที่อยู่ของตัวเองระบุไว้ที่นี่" - } - } - }, - "plugin": { - "name": "แหล่งข่าวลับ" - } -} diff --git a/plugins/secret-sources/locales/translated/tr_TR.json b/plugins/secret-sources/locales/translated/tr_TR.json deleted file mode 100644 index 0cb5beded..000000000 --- a/plugins/secret-sources/locales/translated/tr_TR.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Gizli anahtar yerine 1Password referansını (op://vault/item/field) yapıştırabilirsiniz. Bağlantı kurulurken gizli anahtar kaynağınızdan alınır.", - "manage": "Gizli kaynakları yönetin", - "title": "Gizli kaynaklar", - "description": "Termix, bağlantı kurulduğunda harici parola yöneticilerinden gizli bilgileri okur. Şu anda 1Password Connect (kendi sunucunuzda barındırılan) kullanılmaktadır. Erişim belirteci, veri anahtarınızla şifrelenmiş olarak saklanır.", - "new": "Yeni kaynak", - "nameLabel": "Dostça isim", - "urlLabel": "Sunucu URL'sine bağlanın", - "tokenLabel": "Bağlantı erişim belirteci", - "tokenKeep": "Geçerli belirteci korumak için boş bırakın.", - "sharedLabel": "Tüm kullanıcılarla paylaş (yalnızca yöneticiler, oturumunuz açıkken çözümlenir)", - "shared": "paylaşılan", - "test": "Test Et", - "testOk": "Bağlı, {{count}} kasa görünür durumda", - "testFailed": "Bağlantı testi başarısız oldu.", - "required": "İsim, sunucu URL'si ve belirteç (token) gereklidir.", - "saved": "Gizli kaynak kaydedildi.", - "deleted": "Gizli kaynak silindi", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Özel uç nokta izin listesi", - "description": "Gizli bir kaynağın erişebileceği özel veya geri döngü sunucuları, her satıra bir tane. Kendi kendine barındırılan bir 1Password Connect sunucusunun genellikle burada listelenen kendi adresine ihtiyacı vardır." - } - } - }, - "plugin": { - "name": "Gizli Kaynaklar" - } -} diff --git a/plugins/secret-sources/locales/translated/uk_UA.json b/plugins/secret-sources/locales/translated/uk_UA.json deleted file mode 100644 index 008dd27c4..000000000 --- a/plugins/secret-sources/locales/translated/uk_UA.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Ви можете вставити посилання на 1Password (op://vault/item/field) замість секретного коду. Воно отримується з вашого секретного джерела під час підключення.", - "manage": "Керування секретними джерелами", - "title": "Секретні джерела", - "description": "Зовнішні менеджери паролів Termix зчитують секрети під час підключення. Наразі це 1Password Connect (власний хостинг). Токен доступу зберігається в зашифрованому вигляді з вашим ключем даних.", - "new": "Нове джерело", - "nameLabel": "Зрозуміле ім'я", - "urlLabel": "URL-адреса сервера підключення", - "tokenLabel": "Підключити токен доступу", - "tokenKeep": "Залиште поле порожнім, щоб зберегти поточний токен", - "sharedLabel": "Поділитися з усіма користувачами (лише для адміністраторів, вирішується, коли ви ввійшли в систему)", - "shared": "спільний", - "test": "Тест", - "testOk": "Підключено, видимих сховищ {{count}}", - "testFailed": "Перевірка з'єднання не вдалася", - "required": "Ім'я, URL-адреса сервера та токен обов'язкові", - "saved": "Секретне джерело збережено", - "deleted": "Секретне джерело видалено", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Список дозволених приватних кінцевих точок", - "description": "Приватні або петлеві хости, до яких може звернутися секретне джерело, по одному на рядок. Власному серверу 1Password Connect зазвичай потрібна власна адреса, вказана тут." - } - } - }, - "plugin": { - "name": "Секретні джерела" - } -} diff --git a/plugins/secret-sources/locales/translated/vi_VN.json b/plugins/secret-sources/locales/translated/vi_VN.json deleted file mode 100644 index 4ce0f0460..000000000 --- a/plugins/secret-sources/locales/translated/vi_VN.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "Bạn có thể dán mã tham chiếu 1Password (op://vault/item/field) thay cho mã bí mật. Mã này sẽ được lấy từ nguồn mã bí mật của bạn khi kết nối.", - "manage": "Quản lý các nguồn tin mật", - "title": "Nguồn tin bí mật", - "description": "Termix đọc các mã bí mật từ các trình quản lý mật khẩu bên ngoài khi kết nối. Hiện tại là 1Password Connect (tự lưu trữ). Mã truy cập được lưu trữ dưới dạng mã hóa cùng với khóa dữ liệu của bạn.", - "new": "Nguồn tin mới", - "nameLabel": "Tên thân thiện", - "urlLabel": "URL kết nối máy chủ", - "tokenLabel": "Mã truy cập kết nối", - "tokenKeep": "Để trống nếu muốn giữ nguyên mã thông báo hiện tại.", - "sharedLabel": "Chia sẻ với tất cả người dùng (chỉ quản trị viên, thao tác này sẽ được thực hiện khi bạn đang đăng nhập)", - "shared": "được chia sẻ", - "test": "Kiểm tra", - "testOk": "Đã kết nối, {{count}} kho lưu trữ đang hiển thị", - "testFailed": "Kiểm tra kết nối thất bại", - "required": "Tên người dùng, URL máy chủ và mã thông báo là bắt buộc.", - "saved": "Nguồn bí mật đã được lưu", - "deleted": "Nguồn tin bí mật đã bị xóa.", - "settings": { - "admin": { - "privateEndpoints": { - "label": "Danh sách cho phép điểm cuối riêng tư", - "description": "Các máy chủ riêng tư hoặc máy chủ vòng lặp mà nguồn bí mật có thể truy cập được, mỗi dòng một máy chủ. Máy chủ 1Password Connect tự lưu trữ thường cần có địa chỉ riêng được liệt kê ở đây." - } - } - }, - "plugin": { - "name": "Nguồn tin bí mật" - } -} diff --git a/plugins/secret-sources/locales/translated/zh_CN.json b/plugins/secret-sources/locales/translated/zh_CN.json deleted file mode 100644 index eb631472b..000000000 --- a/plugins/secret-sources/locales/translated/zh_CN.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "您可以粘贴 1Password 引用(op://vault/item/field)而不是密钥。连接时,系统会从您的密钥源中获取该引用。", - "manage": "管理秘密来源", - "title": "秘密消息来源", - "description": "外部密码管理器 Termix 会在连接时读取密钥。目前支持 1Password Connect(自托管)。访问令牌会使用您的数据密钥进行加密存储。", - "new": "新来源", - "nameLabel": "友好名称", - "urlLabel": "连接服务器 URL", - "tokenLabel": "连接访问令牌", - "tokenKeep": "留空以保留当前令牌", - "sharedLabel": "共享给所有用户(仅限管理员,登录后生效)", - "shared": "已共享", - "test": "测试", - "testOk": "已连接, {{count}} 保险库可见", - "testFailed": "连接测试失败", - "required": "需要提供名称、服务器 URL 和令牌。", - "saved": "秘密来源已保存", - "deleted": "秘密来源已删除", - "settings": { - "admin": { - "privateEndpoints": { - "label": "私有端点允许列表", - "description": "私有或回环地址,秘密来源可以访问,每行一个。自托管的 1Password Connect 服务器通常需要在此处列出其自己的地址。" - } - } - }, - "plugin": { - "name": "秘密消息来源" - } -} diff --git a/plugins/secret-sources/locales/translated/zh_TW.json b/plugins/secret-sources/locales/translated/zh_TW.json deleted file mode 100644 index 8233ccca8..000000000 --- a/plugins/secret-sources/locales/translated/zh_TW.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "hint": "您可以貼上 1Password 引用(op://vault/item/field)而不是金鑰。連線時,系統會從您的金鑰來源取得該引用。", - "manage": "管理秘密來源", - "title": "秘密消息來源", - "description": "外部密碼管理器 Termix 會在連線時讀取金鑰。目前支援 1Password Connect(自架)。存取令牌會使用您的資料密鑰進行加密儲存。", - "new": "新來源", - "nameLabel": "友善名稱", - "urlLabel": "連接伺服器 URL", - "tokenLabel": "連接訪問令牌", - "tokenKeep": "留空以保留當前令牌", - "sharedLabel": "分享給所有使用者(僅限管理員,登入後生效)", - "shared": "已共用", - "test": "測試", - "testOk": "已連接, {{count}} 保險庫可見", - "testFailed": "連線測試失敗", - "required": "需要提供名稱、伺服器 URL 和令牌。", - "saved": "秘密來源已儲存", - "deleted": "秘密來源已刪除", - "settings": { - "admin": { - "privateEndpoints": { - "label": "私有端點允許列表", - "description": "私有或回環位址,秘密來源可以訪問,每行一個。自託管的 1Password Connect 伺服器通常需要在此列出自己的位址。" - } - } - }, - "plugin": { - "name": "秘密消息來源" - } -} diff --git a/plugins/secret-sources/manifest.json b/plugins/secret-sources/manifest.json deleted file mode 100644 index 0ed2eef67..000000000 --- a/plugins/secret-sources/manifest.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "secret-sources", - "name": "Secret Sources", - "version": "1.0.0", - "description": "Fetch host secrets from 1Password when you connect instead of storing them in Termix.", - "author": { - "name": "Termix" - }, - "license": "MIT", - "repository": "https://github.com/Termix-SSH/Termix", - "category": "Access & Security", - "icon": "KeyRound", - "engine": { - "termix": ">=2.9.0", - "api": "1" - }, - "capabilities": [ - "db:own", - "secrets:own", - "network:outbound", - "network:serve", - "auth:provide", - "ui:surface", - "users:impersonate" - ], - "contributes": { - "auth": { - "secretSchemes": ["op"] - }, - "settings": { - "admin": [ - { - "key": "privateEndpoints", - "type": "textarea", - "labelKey": "settings.admin.privateEndpoints.label", - "descriptionKey": "settings.admin.privateEndpoints.description", - "default": "localhost\n127.0.0.1\n::1\nhost.docker.internal" - } - ] - } - }, - "locales": "locales", - "platforms": ["linux", "win32", "darwin"] -} diff --git a/plugins/secret-sources/migrations/mysql/0001_adopt_secret_sources.sql b/plugins/secret-sources/migrations/mysql/0001_adopt_secret_sources.sql deleted file mode 100644 index 50c253185..000000000 --- a/plugins/secret-sources/migrations/mysql/0001_adopt_secret_sources.sql +++ /dev/null @@ -1,15 +0,0 @@ --- secret-sources 0001: adopt_secret_sources --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS `secret_sources` ( - `id` varchar(255) PRIMARY KEY NOT NULL, - `user_id` varchar(255) NOT NULL, - `name` text NOT NULL, - `kind` text NOT NULL DEFAULT ('onepassword-connect'), - `base_url` text NOT NULL, - `shared` boolean NOT NULL DEFAULT false, - `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE -); -RENAME TABLE `secret_sources` TO `p_secret_sources_sources`; diff --git a/plugins/secret-sources/migrations/postgres/0001_adopt_secret_sources.sql b/plugins/secret-sources/migrations/postgres/0001_adopt_secret_sources.sql deleted file mode 100644 index ae426dc7a..000000000 --- a/plugins/secret-sources/migrations/postgres/0001_adopt_secret_sources.sql +++ /dev/null @@ -1,16 +0,0 @@ --- secret-sources 0001: adopt_secret_sources --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS "secret_sources" ( - "id" varchar(255) PRIMARY KEY NOT NULL, - "user_id" varchar(255) NOT NULL, - "name" text NOT NULL, - "kind" text NOT NULL DEFAULT 'onepassword-connect', - "base_url" text NOT NULL, - "shared" boolean NOT NULL DEFAULT false, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS "idx_secret_sources_user" ON "secret_sources" ("user_id"); -ALTER TABLE "secret_sources" RENAME TO "p_secret_sources_sources"; diff --git a/plugins/secret-sources/migrations/snapshot.json b/plugins/secret-sources/migrations/snapshot.json deleted file mode 100644 index 02ae92905..000000000 --- a/plugins/secret-sources/migrations/snapshot.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "version": 1, - "tables": { - "sources": { - "columns": { - "id": { - "type": "varchar", - "length": 255, - "primaryKey": true, - "notNull": true - }, - "userId": { - "type": "refUser", - "length": 255, - "notNull": true - }, - "name": { - "type": "text", - "notNull": true - }, - "kind": { - "type": "text", - "notNull": true, - "defaultValue": "onepassword-connect" - }, - "baseUrl": { - "type": "text", - "notNull": true - }, - "shared": { - "type": "boolean", - "notNull": true, - "defaultValue": false - }, - "createdAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - }, - "updatedAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - } - }, - "indexes": [ - { - "name": "idx_secret_sources_user", - "columns": ["userId"] - } - ], - "adopts": "secret_sources" - } - } -} diff --git a/plugins/secret-sources/migrations/sqlite/0001_adopt_secret_sources.sql b/plugins/secret-sources/migrations/sqlite/0001_adopt_secret_sources.sql deleted file mode 100644 index ddb2d0105..000000000 --- a/plugins/secret-sources/migrations/sqlite/0001_adopt_secret_sources.sql +++ /dev/null @@ -1,16 +0,0 @@ --- secret-sources 0001: adopt_secret_sources --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS "secret_sources" ( - "id" text PRIMARY KEY NOT NULL, - "user_id" text NOT NULL, - "name" text NOT NULL, - "kind" text NOT NULL DEFAULT 'onepassword-connect', - "base_url" text NOT NULL, - "shared" integer NOT NULL DEFAULT 0, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS "idx_secret_sources_user" ON "secret_sources" ("user_id"); -ALTER TABLE "secret_sources" RENAME TO "p_secret_sources_sources"; diff --git a/plugins/secret-sources/package.json b/plugins/secret-sources/package.json deleted file mode 100644 index 11dfff5d7..000000000 --- a/plugins/secret-sources/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@termix-plugin/secret-sources", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "build": "termix-plugin build", - "test": "termix-plugin test", - "typecheck": "tsc -p .", - "validate": "termix-plugin validate" - }, - "dependencies": { - "lucide-react": "^1.33.0" - }, - "devDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "@testing-library/dom": "^10.4.2", - "@testing-library/react": "^16.3.3", - "@types/node": "^26.2.0", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", - "better-sqlite3": "^13.0.3", - "jsdom": "^30.0.1", - "vitest": "^4.1.11" - }, - "peerDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "drizzle-orm": "^0.45.2", - "express": "^5.2.1", - "react": "^19.2.8", - "sonner": "^2.0.8" - } -} diff --git a/plugins/secret-sources/src/backend/egress.ts b/plugins/secret-sources/src/backend/egress.ts deleted file mode 100644 index 58fae605e..000000000 --- a/plugins/secret-sources/src/backend/egress.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Where a secret source (a self-hosted 1Password Connect server) may be - * reached. Every request goes through ctx.fetch, which refuses private and - * loopback addresses unless the exact host is listed here, admin-only and - * install-wide. - */ - -export const DEFAULT_PRIVATE_ALLOWLIST = [ - "localhost", - "127.0.0.1", - "::1", - "host.docker.internal", -]; - -/** The admin setting is one host per line. A line that is not a bare host is ignored. */ -const HOST_PATTERN = /^[a-z0-9._:-]+$/; - -export function parseAllowlist(raw: unknown): string[] { - if (typeof raw !== "string") return [...DEFAULT_PRIVATE_ALLOWLIST]; - const hosts: string[] = []; - for (const line of raw.split(/[\r\n,]+/)) { - const host = line.trim().toLowerCase(); - if (host && HOST_PATTERN.test(host) && !hosts.includes(host)) { - hosts.push(host); - } - } - return hosts; -} diff --git a/plugins/secret-sources/src/backend/index.ts b/plugins/secret-sources/src/backend/index.ts deleted file mode 100644 index fd8d388a1..000000000 --- a/plugins/secret-sources/src/backend/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Router } from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { sources } from "./tables.js"; -import { createSecretSourceRepository } from "./repository.js"; -import { createTokenStore } from "./token-store.js"; -import { registerSecretSourceRoutes } from "./routes.js"; -import { registerOnePasswordResolver } from "./resolver.js"; - -export async function activate(ctx: PluginContext) { - const table = await ctx.db.define(sources); - const repository = createSecretSourceRepository(ctx.db, table); - const tokenStore = createTokenStore(ctx); - - registerSecretSourceRoutes( - ctx.http.router(), - ctx, - repository, - tokenStore, - ); - registerOnePasswordResolver(ctx, repository, tokenStore); - - ctx.log.info("Secret sources routes mounted at /plugin-api/secret-sources"); -} - -export async function deactivate() { - // Everything above was registered through ctx and is disposed by core. -} diff --git a/plugins/secret-sources/src/backend/onepassword-connect.ts b/plugins/secret-sources/src/backend/onepassword-connect.ts deleted file mode 100644 index 977905eb8..000000000 --- a/plugins/secret-sources/src/backend/onepassword-connect.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { PluginFetch } from "@termix/plugin-sdk/backend"; - -/** - * The slice of the 1Password Connect REST API needed to resolve a secret - * reference: find the vault, find the item, read one field. - * https://developer.1password.com/docs/connect/api-reference - */ - -export interface SecretReference { - vault: string; - item: string; - field: string; -} - -const FETCH_TIMEOUT_MS = 10_000; - -/** op://// (a trailing ?query, as in ?ssh-format=openssh, is ignored) */ -export function parseSecretReference(raw: string): SecretReference | null { - const match = /^op:\/\/([^/]+)\/([^/]+)\/([^/?]+)(?:\?.*)?$/.exec(raw.trim()); - if (!match) return null; - const [, vault, item, field] = match.map((part) => decodeURIComponent(part)); - return { vault, item, field }; -} - -export interface ConnectSource { - baseUrl: string; - token: string; - allowedPrivateHosts: readonly string[]; -} - -async function connectGet( - fetch: PluginFetch, - source: ConnectSource, - path: string, -): Promise { - const base = source.baseUrl.replace(/\/+$/, ""); - const response = await fetch(`${base}${path}`, { - method: "GET", - headers: { Authorization: `Bearer ${source.token}` }, - timeoutMs: FETCH_TIMEOUT_MS, - allowPrivateHosts: source.allowedPrivateHosts, - }); - if (!response.ok) { - throw new Error( - `1Password Connect ${path} failed: HTTP ${response.status}`, - ); - } - return (await response.json()) as T; -} - -function eqFilter(value: string): string { - return encodeURIComponent(`title eq "${value.replace(/"/g, '\\"')}"`); -} - -interface ConnectVault { - id: string; - name: string; -} -interface ConnectItemSummary { - id: string; - title: string; -} -interface ConnectField { - id: string; - label?: string; - purpose?: string; - type?: string; - value?: string; -} - -/** Reachability + token validity: the vault list needs a valid token. */ -export async function testConnectSource( - fetch: PluginFetch, - source: ConnectSource, -): Promise { - const vaults = await connectGet(fetch, source, "/v1/vaults"); - return vaults.length; -} - -const byIdOrName = - (name: string) => - (candidate: { id: string; name?: string; title?: string }) => - candidate.id === name || - (candidate.name ?? candidate.title ?? "").toLowerCase() === - name.toLowerCase(); - -export async function resolveConnectReference( - fetch: PluginFetch, - source: ConnectSource, - ref: SecretReference, -): Promise { - const vaults = await connectGet( - fetch, - source, - `/v1/vaults?filter=${eqFilter(ref.vault)}`, - ); - const vault = - vaults.find(byIdOrName(ref.vault)) ?? - (await connectGet(fetch, source, "/v1/vaults")).find( - byIdOrName(ref.vault), - ); - if (!vault) throw new Error(`1Password vault "${ref.vault}" not found`); - - const items = await connectGet( - fetch, - source, - `/v1/vaults/${vault.id}/items?filter=${eqFilter(ref.item)}`, - ); - const summary = - items.find(byIdOrName(ref.item)) ?? - (ref.item.length >= 26 ? { id: ref.item, title: ref.item } : undefined); - if (!summary) { - throw new Error(`1Password item "${ref.item}" not found in "${ref.vault}"`); - } - - const item = await connectGet<{ fields?: ConnectField[] }>( - fetch, - source, - `/v1/vaults/${vault.id}/items/${summary.id}`, - ); - const wanted = ref.field.toLowerCase(); - const field = (item.fields ?? []).find( - (f) => - f.id === ref.field || - f.label?.toLowerCase() === wanted || - f.purpose?.toLowerCase() === wanted || - (wanted === "private key" && f.id === "private_key") || - (wanted === "private_key" && f.label?.toLowerCase() === "private key"), - ); - if (!field || field.value === undefined) { - throw new Error( - `1Password field "${ref.field}" not found on "${ref.item}"`, - ); - } - return field.value; -} diff --git a/plugins/secret-sources/src/backend/repository.ts b/plugins/secret-sources/src/backend/repository.ts deleted file mode 100644 index ebfb067ec..000000000 --- a/plugins/secret-sources/src/backend/repository.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { eq, or } from "drizzle-orm"; -import type { PluginDatabase } from "@termix/plugin-sdk/backend"; - -export type SecretSourceKind = "onepassword-connect"; - -export interface SecretSourceRecord { - id: string; - userId: string; - name: string; - kind: SecretSourceKind; - baseUrl: string; - shared: boolean; - createdAt: string; - updatedAt: string; -} - -export interface SecretSourceCreateInput { - id: string; - userId: string; - name: string; - kind: SecretSourceKind; - baseUrl: string; - shared: boolean; -} - -export interface SecretSourceUpdateInput { - name?: string; - baseUrl?: string; - shared?: boolean; -} - -/* eslint-disable @typescript-eslint/no-explicit-any */ -// The table comes from ctx.db.define, which the SDK hands back untyped, and -// the drizzle handle is the server's own. Typed at this module's edge instead. -type Table = any; -type Drizzle = any; -/* eslint-enable @typescript-eslint/no-explicit-any */ - -export type SecretSourceRepository = ReturnType< - typeof createSecretSourceRepository ->; - -/** - * Per-user secret source storage. The token is never in this table - it - * lives in ctx.secrets under "source:". A row with an id the caller - * chose is written without RETURNING so it runs the same on all three - * engines. - */ -export function createSecretSourceRepository(db: PluginDatabase, table: Table) { - const client = () => db.client(); - - async function findById(id: string): Promise { - const drizzle = await client(); - const rows = await drizzle - .select() - .from(table) - .where(eq(table.id, id)) - .limit(1); - return (rows[0] as SecretSourceRecord) ?? null; - } - - return { - findById, - - async listVisibleToUser(userId: string): Promise { - const drizzle = await client(); - return drizzle - .select() - .from(table) - .where(or(eq(table.userId, userId), eq(table.shared, true))); - }, - - async create( - input: SecretSourceCreateInput, - now = new Date().toISOString(), - ): Promise { - const drizzle = await client(); - await drizzle.insert(table).values({ - id: input.id, - userId: input.userId, - name: input.name, - kind: input.kind, - baseUrl: input.baseUrl, - shared: input.shared, - createdAt: now, - updatedAt: now, - }); - await db.persist(); - return (await findById(input.id)) as SecretSourceRecord; - }, - - async update( - row: SecretSourceRecord, - input: SecretSourceUpdateInput, - now = new Date().toISOString(), - ): Promise { - const drizzle = await client(); - await drizzle - .update(table) - .set({ - ...(input.name !== undefined ? { name: input.name } : {}), - ...(input.baseUrl !== undefined ? { baseUrl: input.baseUrl } : {}), - ...(input.shared !== undefined ? { shared: input.shared } : {}), - updatedAt: now, - }) - .where(eq(table.id, row.id)); - await db.persist(); - }, - - async deleteById(id: string): Promise { - const drizzle = await client(); - await drizzle.delete(table).where(eq(table.id, id)); - await db.persist(); - }, - }; -} diff --git a/plugins/secret-sources/src/backend/resolver.ts b/plugins/secret-sources/src/backend/resolver.ts deleted file mode 100644 index a79f39013..000000000 --- a/plugins/secret-sources/src/backend/resolver.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import type { SecretSourceRepository } from "./repository.js"; -import type { TokenStore } from "./token-store.js"; -import { parseAllowlist } from "./egress.js"; -import { - parseSecretReference, - resolveConnectReference, -} from "./onepassword-connect.js"; - -/** The user's own source first, else a shared one. */ -async function pickSecretSource( - repository: SecretSourceRepository, - userId: string, -) { - const sources = await repository.listVisibleToUser(userId); - return ( - sources.find((source) => source.userId === userId) ?? sources[0] ?? null - ); -} - -/** - * The "op" scheme resolver: turns "op://vault/item/field" into the actual - * secret, using whichever source the acting user can see (their own first, - * else a shared one). - */ -export function registerOnePasswordResolver( - ctx: PluginContext, - repository: SecretSourceRepository, - tokenStore: TokenStore, -): void { - ctx.credentials.registerSecretResolver("op", async (userId, reference) => { - const ref = parseSecretReference(reference); - if (!ref) throw new Error(`Invalid secret reference: ${reference}`); - - const source = await pickSecretSource(repository, userId); - if (!source) { - throw new Error( - "This host uses a secret reference but no secret source is configured", - ); - } - - const token = await tokenStore.getForOwner(source.id, source.userId); - if (!token) { - throw new Error( - "The secret source owner's data is locked; they need to sign in first", - ); - } - - return resolveConnectReference( - ctx.fetch, - { - baseUrl: source.baseUrl, - token, - allowedPrivateHosts: parseAllowlist( - await ctx.settings.get("privateEndpoints"), - ), - }, - ref, - ); - }); -} diff --git a/plugins/secret-sources/src/backend/routes.ts b/plugins/secret-sources/src/backend/routes.ts deleted file mode 100644 index a804c79de..000000000 --- a/plugins/secret-sources/src/backend/routes.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { randomUUID } from "node:crypto"; -import type { Request, Response, Router } from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import type { SecretSourceRepository } from "./repository.js"; -import type { TokenStore } from "./token-store.js"; -import { testConnectSource } from "./onepassword-connect.js"; -import { parseAllowlist } from "./egress.js"; - -const KINDS = ["onepassword-connect"] as const; - -function isNonEmptyString(value: unknown): value is string { - return typeof value === "string" && value.trim().length > 0; -} - -function validBaseUrl(raw: unknown): string | null { - if (!isNonEmptyString(raw)) return null; - try { - const url = new URL(raw.trim()); - if (!["http:", "https:"].includes(url.protocol)) return null; - return url.toString().replace(/\/+$/, ""); - } catch { - return null; - } -} - -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - -export function registerSecretSourceRoutes( - router: Router, - ctx: PluginContext, - repository: SecretSourceRepository, - tokenStore: TokenStore, -): void { - const isAdmin = () => ctx.rbac.has("admin.plugins.manage"); - // Secret sources are credentials, so they follow core's credential - // permissions. ctx.rbac.require only takes this plugin's own names. - const requireCredentials = - (action: "view" | "create" | "edit" | "delete") => - (req: Request, res: Response, next: () => void) => { - void ctx.rbac.has(`credentials.${action}`).then((allowed) => { - if (allowed) next(); - else { - res.status(403).json({ - error: "Insufficient permissions", - required: `credentials.${action}`, - }); - } - }); - }; - const allowedPrivateHosts = async () => - parseAllowlist(await ctx.settings.get("privateEndpoints")); - - /** - * @openapi - * /plugin-api/secret-sources: - * get: - * summary: List secret sources visible to the caller (own + shared) - * tags: - * - Secret Sources - */ - router.get( - "/", - requireCredentials("view") as never, - async (req: Request, res: Response) => { - const userId = ctx.currentActor()!; - try { - const rows = await repository.listVisibleToUser(userId); - res.json({ - sources: rows.map((row) => ({ - ...row, - owned: row.userId === userId, - })), - }); - } catch (error) { - ctx.log.error("Failed to list secret sources", error); - res.status(500).json({ error: "Failed to list secret sources" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/secret-sources: - * post: - * summary: Create a secret source (1Password Connect) - * description: Sharing a source with every user requires admin. The token is encrypted with the owner's data key. - * tags: - * - Secret Sources - */ - router.post( - "/", - requireCredentials("create") as never, - async (req: Request, res: Response) => { - const userId = ctx.currentActor()!; - const { - name, - kind = "onepassword-connect", - baseUrl, - token, - shared, - } = req.body ?? {}; - const url = validBaseUrl(baseUrl); - if (!isNonEmptyString(name) || !url || !isNonEmptyString(token)) { - return res.status(400).json({ - error: "name, a valid http(s) baseUrl and token are required", - }); - } - if (!(KINDS as readonly string[]).includes(kind)) { - return res - .status(400) - .json({ error: "Unsupported secret source kind" }); - } - if (shared === true && !(await isAdmin())) { - return res - .status(403) - .json({ error: "Only admins can share a secret source" }); - } - try { - const id = randomUUID(); - const created = await repository.create({ - id, - userId, - name: name.trim(), - kind, - baseUrl: url, - shared: shared === true, - }); - await tokenStore.set(id, token.trim()); - await ctx.audit.record({ - action: "secret_source_create", - resourceType: "secret_source", - resourceId: created.id, - resourceName: created.name, - success: true, - }); - res.json({ source: { ...created, owned: true } }); - } catch (error) { - ctx.log.error("Failed to create secret source", error); - res.status(500).json({ error: "Failed to create secret source" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/secret-sources/{id}: - * put: - * summary: Update a secret source (owner only; omit token to keep it) - * tags: - * - Secret Sources - */ - router.put( - "/:id", - requireCredentials("edit") as never, - async (req: Request, res: Response) => { - const userId = ctx.currentActor()!; - const { name, baseUrl, token, shared } = req.body ?? {}; - try { - const source = await repository.findById(String(req.params.id)); - if (!source) { - return res.status(404).json({ error: "Secret source not found" }); - } - if (source.userId !== userId) { - return res - .status(403) - .json({ error: "Only the owner can change this source" }); - } - const url = baseUrl === undefined ? undefined : validBaseUrl(baseUrl); - if (baseUrl !== undefined && !url) { - return res.status(400).json({ error: "baseUrl must be http(s)" }); - } - if (shared === true && !source.shared && !(await isAdmin())) { - return res - .status(403) - .json({ error: "Only admins can share a secret source" }); - } - await repository.update(source, { - ...(isNonEmptyString(name) ? { name: name.trim() } : {}), - ...(url ? { baseUrl: url } : {}), - ...(typeof shared === "boolean" ? { shared } : {}), - }); - if (isNonEmptyString(token)) { - await tokenStore.set(source.id, token.trim()); - } - res.json({ success: true }); - } catch (error) { - ctx.log.error("Failed to update secret source", error); - res.status(500).json({ error: "Failed to update secret source" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/secret-sources/{id}: - * delete: - * summary: Delete a secret source (owner only) - * tags: - * - Secret Sources - */ - router.delete( - "/:id", - requireCredentials("delete") as never, - async (req: Request, res: Response) => { - const userId = ctx.currentActor()!; - try { - const source = await repository.findById(String(req.params.id)); - if (!source) { - return res.status(404).json({ error: "Secret source not found" }); - } - if (source.userId !== userId) { - return res - .status(403) - .json({ error: "Only the owner can change this source" }); - } - await repository.deleteById(source.id); - await tokenStore.clear(source.id); - await ctx.audit.record({ - action: "secret_source_delete", - resourceType: "secret_source", - resourceId: source.id, - resourceName: source.name, - success: true, - }); - res.json({ success: true }); - } catch (error) { - ctx.log.error("Failed to delete secret source", error); - res.status(500).json({ error: "Failed to delete secret source" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/secret-sources/{id}/test: - * post: - * summary: Check that the source is reachable and the token is accepted - * tags: - * - Secret Sources - */ - router.post( - "/:id/test", - requireCredentials("view") as never, - async (req: Request, res: Response) => { - const userId = ctx.currentActor()!; - try { - const source = await repository.findById(String(req.params.id)); - if (!source || (source.userId !== userId && !source.shared)) { - return res.status(404).json({ error: "Secret source not found" }); - } - const token = await tokenStore.getForOwner(source.id, source.userId); - if (!token) { - return res - .status(200) - .json({ ok: false, error: "No token stored for this source" }); - } - const vaults = await testConnectSource(ctx.fetch, { - baseUrl: source.baseUrl, - token, - allowedPrivateHosts: await allowedPrivateHosts(), - }); - res.json({ ok: true, vaults }); - } catch (error) { - res.status(200).json({ ok: false, error: errorMessage(error) }); - } - }, - ); -} diff --git a/plugins/secret-sources/src/backend/tables.ts b/plugins/secret-sources/src/backend/tables.ts deleted file mode 100644 index 4b5fb2d3a..000000000 --- a/plugins/secret-sources/src/backend/tables.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - adoptLegacyTable, - boolean, - defineTable, - refUser, - text, - timestamp, - varchar, -} from "@termix/plugin-sdk/db"; - -/** - * Adopted from core's secret_sources, so column and index names are the - * legacy ones and the rename carries every row across. - * - * The access token is not a column here: it lives in ctx.secrets under - * "source:", the same move the ai plugin made for provider API keys. A - * 2.8 database still has a token column, which the boot migration empties - * after moving the value. - */ -export const sources = adoptLegacyTable( - "secret_sources", - defineTable( - "sources", - { - id: varchar().primaryKey(), - userId: refUser(), - name: text().notNull(), - // "onepassword-connect" for now; the reference syntax is per kind. - kind: text().notNull().default("onepassword-connect"), - baseUrl: text().notNull(), - // Visible to every user; the token still decrypts with the owner's key. - shared: boolean().notNull().default(false), - createdAt: timestamp().notNull().defaultNow(), - updatedAt: timestamp().notNull().defaultNow(), - }, - { - indexes: [{ name: "idx_secret_sources_user", columns: ["userId"] }], - }, - ), -); - -export const tables = [sources]; diff --git a/plugins/secret-sources/src/backend/token-store.ts b/plugins/secret-sources/src/backend/token-store.ts deleted file mode 100644 index 4d04b8371..000000000 --- a/plugins/secret-sources/src/backend/token-store.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { PluginContext } from "@termix/plugin-sdk/backend"; - -/** - * The access token for one source, kept out of the sources table and in - * ctx.secrets instead, the same move the ai plugin made for provider API - * keys: encrypted with the installation key rather than the owner's data - * key, so it is readable whenever the plugin runs rather than only while the - * owner is signed in. A shared source's token is still only ever handed to - * its owner's part of the resolve, so a read for any other acting user runs - * as the owner through ctx.asUser. - */ - -function tokenKey(sourceId: string): string { - return `source:${sourceId}`; -} - -export function createTokenStore(ctx: PluginContext) { - return { - async set(sourceId: string, token: string): Promise { - await ctx.secrets.set(tokenKey(sourceId), token); - }, - - async clear(sourceId: string): Promise { - await ctx.secrets.delete(tokenKey(sourceId)); - }, - - /** Reads the token as the source's owner, whoever is currently acting. */ - async getForOwner( - sourceId: string, - ownerId: string, - ): Promise { - return ctx.asUser(ownerId, () => ctx.secrets.get(tokenKey(sourceId))); - }, - }; -} - -export type TokenStore = ReturnType; diff --git a/plugins/secret-sources/src/frontend/SecretReferenceHint.tsx b/plugins/secret-sources/src/frontend/SecretReferenceHint.tsx deleted file mode 100644 index 66a45b914..000000000 --- a/plugins/secret-sources/src/frontend/SecretReferenceHint.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useTranslation } from "@termix/plugin-sdk/frontend"; - -/** One line under a secret field: references are allowed, here is where to set them up. */ -export function SecretReferenceHint({ onManage }: { onManage: () => void }) { - const { t } = useTranslation(); - return ( -

- {t("hint")}{" "} - -

- ); -} diff --git a/plugins/secret-sources/src/frontend/SecretSourceManager.tsx b/plugins/secret-sources/src/frontend/SecretSourceManager.tsx deleted file mode 100644 index cc0542dfe..000000000 --- a/plugins/secret-sources/src/frontend/SecretSourceManager.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { KeyRound, Pencil, Plus, Trash2, X } from "lucide-react"; -import { toast } from "sonner"; -import { Button, Input, PasswordInput } from "@termix/plugin-sdk/ui"; -import { usePluginApi, useTranslation } from "@termix/plugin-sdk/frontend"; -import { - createSecretSourcesApi, - type SecretSource, -} from "./secret-sources-api"; - -function errorMessage(error: unknown): string { - if (error && typeof error === "object" && "message" in error) { - return String((error as { message?: unknown }).message ?? ""); - } - return String(error); -} - -type FormState = { - id?: string; - name: string; - baseUrl: string; - token: string; - shared: boolean; -}; - -const emptyForm: FormState = { - name: "", - baseUrl: "", - token: "", - shared: false, -}; - -export function SecretSourceManager({ onClose }: { onClose: () => void }) { - const { t } = useTranslation(); - const pluginApi = usePluginApi(); - const api = useMemo(() => createSecretSourcesApi(pluginApi), [pluginApi]); - const [sources, setSources] = useState([]); - const [form, setForm] = useState(null); - const [saving, setSaving] = useState(false); - const [testing, setTesting] = useState(null); - - const reload = useCallback(async () => { - try { - setSources(await api.list()); - } catch (e) { - toast.error(errorMessage(e)); - } - }, [api]); - - useEffect(() => { - void reload(); - }, [reload]); - - const setField = (key: K, value: FormState[K]) => - setForm((prev) => (prev ? { ...prev, [key]: value } : prev)); - - const handleSave = async () => { - if (!form) return; - if ( - !form.name.trim() || - !form.baseUrl.trim() || - (!form.id && !form.token) - ) { - toast.error(t("required")); - return; - } - setSaving(true); - try { - if (form.id) { - await api.update(form.id, { - name: form.name, - baseUrl: form.baseUrl, - shared: form.shared, - ...(form.token ? { token: form.token } : {}), - }); - } else { - await api.create({ - name: form.name, - baseUrl: form.baseUrl, - token: form.token, - shared: form.shared, - }); - } - toast.success(t("saved")); - setForm(null); - await reload(); - } catch (e) { - toast.error(errorMessage(e)); - } finally { - setSaving(false); - } - }; - - const handleDelete = async (source: SecretSource) => { - try { - await api.remove(source.id); - toast.success(t("deleted")); - await reload(); - } catch (e) { - toast.error(errorMessage(e)); - } - }; - - const handleTest = async (source: SecretSource) => { - setTesting(source.id); - try { - const result = await api.test(source.id); - if (result.ok) { - toast.success(t("testOk", { count: result.vaults ?? 0 })); - } else { - toast.error(result.error ?? t("testFailed")); - } - } catch (e) { - toast.error(errorMessage(e)); - } finally { - setTesting(null); - } - }; - - return ( -
-
- - {t("title")} - - -
-

{t("description")}

- - {!form && ( - <> - {sources.map((source) => ( -
- -
-
- {source.name} - {source.shared && ( - - {t("shared")} - - )} -
-
- {source.baseUrl} -
-
- - {source.owned && ( - <> - - - - )} -
- ))} - - - )} - - {form && ( -
-
-
- - setField("name", e.target.value)} - /> -
-
- - setField("baseUrl", e.target.value)} - /> -
-
-
- - setField("token", e.target.value)} - /> -
- -
- - -
-
- )} -
- ); -} diff --git a/plugins/secret-sources/src/frontend/index.tsx b/plugins/secret-sources/src/frontend/index.tsx deleted file mode 100644 index c22106bff..000000000 --- a/plugins/secret-sources/src/frontend/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type { TermixApp } from "@termix/plugin-sdk/frontend"; -import { SecretSourceManager } from "./SecretSourceManager"; -import { SecretReferenceHint } from "./SecretReferenceHint"; - -export function activate(app: TermixApp): void { - app.registerComponent("credentials.secretHint", SecretReferenceHint); - app.registerComponent("credentials.secretManager", SecretSourceManager); -} diff --git a/plugins/secret-sources/src/frontend/secret-sources-api.ts b/plugins/secret-sources/src/frontend/secret-sources-api.ts deleted file mode 100644 index 315d2460b..000000000 --- a/plugins/secret-sources/src/frontend/secret-sources-api.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; - -export type SecretSourceKind = "onepassword-connect"; - -export interface SecretSource { - id: string; - name: string; - kind: SecretSourceKind; - baseUrl: string; - shared: boolean; - owned: boolean; - createdAt: string; - updatedAt: string; -} - -export interface SecretSourceInput { - name: string; - baseUrl: string; - token: string; - shared?: boolean; -} - -export interface SecretSourceUpdateInput { - name?: string; - baseUrl?: string; - token?: string; - shared?: boolean; -} - -/** The secret-sources routes, through the plugin's own client. */ -export function createSecretSourcesApi(api: PluginApiClient) { - const data = async (request: Promise<{ data: T }>) => (await request).data; - - return { - list: () => - data(api.get<{ sources: SecretSource[] }>("/")).then((r) => r.sources), - create: (body: SecretSourceInput) => - data(api.post<{ source: SecretSource }>("/", body)).then((r) => r.source), - update: (id: string, body: SecretSourceUpdateInput) => - data(api.put<{ success: boolean }>(`/${id}`, body)), - remove: (id: string) => data(api.delete<{ success: boolean }>(`/${id}`)), - test: (id: string) => - data( - api.post<{ ok: boolean; vaults?: number; error?: string }>( - `/${id}/test`, - ), - ), - }; -} - -export type SecretSourcesApi = ReturnType; diff --git a/plugins/secret-sources/tests/backend/helpers.ts b/plugins/secret-sources/tests/backend/helpers.ts deleted file mode 100644 index 5f63f6922..000000000 --- a/plugins/secret-sources/tests/backend/helpers.ts +++ /dev/null @@ -1,103 +0,0 @@ -import http from "node:http"; -import type { AddressInfo } from "node:net"; -import { fileURLToPath } from "node:url"; -import express, { type Router } from "express"; -import { - createMockCtx, - createTestDb, - type MockPluginContext, - type TestDb, -} from "@termix/plugin-sdk/testing"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import manifestJson from "../../manifest.json"; - -export const pluginDir = fileURLToPath(new URL("../..", import.meta.url)); -export const manifest = manifestJson as unknown as PluginManifest; - -export const ALL_PERMISSIONS = [ - "credentials.view", - "credentials.create", - "credentials.edit", - "credentials.delete", -]; - -export interface TestServer { - db: TestDb; - mock: MockPluginContext; - request: ( - method: string, - path: string, - options?: { user?: string; body?: unknown }, - // Route bodies vary per test; asserting on them is the point. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ) => Promise<{ status: number; body: any }>; - close: () => Promise; -} - -/** - * The plugin activated against a real in-memory database, its router mounted - * on an express app the way core mounts it: JSON parsed, the acting user set - * from the request, everything else left to the plugin. - */ -export async function startServer( - options: { - permissions?: string[]; - users?: string[]; - fetch?: (url: string, init?: unknown) => Promise; - } = {}, -): Promise { - const db = await createTestDb(pluginDir); - for (const user of options.users ?? ["user-1", "user-2"]) { - db.sqlite - .prepare("INSERT INTO users (id, username) VALUES (?, ?)") - .run(user, user); - } - - let router: Router | null = null; - const mock = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: manifest.capabilities, - db: db.database, - router: () => (router = express.Router()), - permissions: options.permissions ?? ALL_PERMISSIONS, - fetch: options.fetch as never, - }); - - const { activate } = await import("../../src/backend/index.js"); - await activate(mock.ctx); - - const app = express(); - app.use(express.json()); - app.use((req, _res, next) => { - mock.setActor(req.header("x-test-user") ?? undefined); - next(); - }); - app.use((req, res, next) => router!(req, res, next)); - - const server = http.createServer(app); - await new Promise((resolve) => server.listen(0, resolve)); - const { port } = server.address() as AddressInfo; - - return { - db, - mock, - async request(method, path, { user = "user-1", body: given } = {}) { - const body = method === "GET" ? undefined : given; - const response = await fetch(`http://127.0.0.1:${port}${path}`, { - method, - headers: { - "x-test-user": user, - ...(body !== undefined ? { "content-type": "application/json" } : {}), - }, - body: body !== undefined ? JSON.stringify(body) : undefined, - }); - const text = await response.text(); - return { status: response.status, body: text ? JSON.parse(text) : null }; - }, - async close() { - await new Promise((resolve) => server.close(() => resolve())); - db.close(); - }, - }; -} diff --git a/plugins/secret-sources/tests/backend/onepassword-connect.test.ts b/plugins/secret-sources/tests/backend/onepassword-connect.test.ts deleted file mode 100644 index cb720efe9..000000000 --- a/plugins/secret-sources/tests/backend/onepassword-connect.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { - parseSecretReference, - resolveConnectReference, - testConnectSource, -} from "../../src/backend/onepassword-connect.js"; -import type { PluginFetch } from "@termix/plugin-sdk/backend"; - -function jsonResponse(body: unknown, ok = true, status = 200): Response { - return { - ok, - status, - json: async () => body, - } as unknown as Response; -} - -describe("1Password secret references", () => { - it("parses op://vault/item/field, ignoring a query suffix", () => { - expect(parseSecretReference("op://Infra/prod-db/password")).toEqual({ - vault: "Infra", - item: "prod-db", - field: "password", - }); - expect( - parseSecretReference( - "op://Infra/deploy key/private key?ssh-format=openssh", - ), - ).toEqual({ vault: "Infra", item: "deploy key", field: "private key" }); - expect(parseSecretReference("op://Infra/only-two")).toBeNull(); - expect(parseSecretReference("https://x")).toBeNull(); - }); -}); - -describe("testConnectSource", () => { - it("returns the vault count when the token is valid", async () => { - const fetch = vi.fn().mockResolvedValue( - jsonResponse([ - { id: "v1", name: "Infra" }, - { id: "v2", name: "Dev" }, - ]), - ) as unknown as PluginFetch; - - const count = await testConnectSource(fetch, { - baseUrl: "https://connect.internal", - token: "tok", - allowedPrivateHosts: ["connect.internal"], - }); - expect(count).toBe(2); - expect(fetch).toHaveBeenCalledWith( - "https://connect.internal/v1/vaults", - expect.objectContaining({ - headers: { Authorization: "Bearer tok" }, - allowPrivateHosts: ["connect.internal"], - }), - ); - }); - - it("throws when the server answers with a non-2xx status", async () => { - const fetch = vi - .fn() - .mockResolvedValue( - jsonResponse([], false, 401), - ) as unknown as PluginFetch; - - await expect( - testConnectSource(fetch, { - baseUrl: "https://connect.internal", - token: "bad", - allowedPrivateHosts: [], - }), - ).rejects.toThrow(/HTTP 401/); - }); -}); - -describe("resolveConnectReference", () => { - it("resolves a field by label across vault, item and field lookups", async () => { - const fetch = vi - .fn() - .mockResolvedValueOnce(jsonResponse([{ id: "vid", name: "Infra" }])) - .mockResolvedValueOnce(jsonResponse([{ id: "iid", title: "prod-db" }])) - .mockResolvedValueOnce( - jsonResponse({ - fields: [{ id: "f1", label: "password", value: "s3cret" }], - }), - ) as unknown as PluginFetch; - - const value = await resolveConnectReference( - fetch, - { - baseUrl: "https://connect.internal", - token: "tok", - allowedPrivateHosts: [], - }, - { vault: "Infra", item: "prod-db", field: "password" }, - ); - expect(value).toBe("s3cret"); - }); - - it("fails clearly when the vault is not found", async () => { - const fetch = vi - .fn() - .mockResolvedValueOnce(jsonResponse([])) - .mockResolvedValueOnce(jsonResponse([])) as unknown as PluginFetch; - - await expect( - resolveConnectReference( - fetch, - { - baseUrl: "https://connect.internal", - token: "tok", - allowedPrivateHosts: [], - }, - { vault: "Missing", item: "x", field: "y" }, - ), - ).rejects.toThrow(/vault "Missing" not found/); - }); -}); diff --git a/plugins/secret-sources/tests/backend/resolver.test.ts b/plugins/secret-sources/tests/backend/resolver.test.ts deleted file mode 100644 index 005dc21a6..000000000 --- a/plugins/secret-sources/tests/backend/resolver.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { fileURLToPath } from "node:url"; -import { describe, expect, it, vi } from "vitest"; -import { createMockCtx, createTestDb } from "@termix/plugin-sdk/testing"; -import { createSecretSourceRepository } from "../../src/backend/repository.js"; -import { createTokenStore } from "../../src/backend/token-store.js"; -import { registerOnePasswordResolver } from "../../src/backend/resolver.js"; -import { sources } from "../../src/backend/tables.js"; -import manifestJson from "../../manifest.json"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; - -const manifest = manifestJson as unknown as PluginManifest; -const pluginDir = fileURLToPath(new URL("../..", import.meta.url)); - -function jsonResponse(body: unknown): Response { - return { - ok: true, - status: 200, - json: async () => body, - } as unknown as Response; -} - -async function setUp(fetchMock: ReturnType) { - const db = await createTestDb(pluginDir); - db.sqlite - .prepare("INSERT INTO users (id, username) VALUES (?, ?)") - .run("owner-1", "owner-1"); - - const mock = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: manifest.capabilities, - db: db.database, - actor: "owner-1", - fetch: fetchMock as never, - }); - - const table = await mock.ctx.db.define(sources); - const repository = createSecretSourceRepository(mock.ctx.db, table as never); - const tokenStore = createTokenStore(mock.ctx); - registerOnePasswordResolver(mock.ctx, repository, tokenStore); - - const resolve = mock.auth.secretResolvers.get("op"); - if (!resolve) throw new Error("resolver was not registered"); - - return { db, repository, tokenStore, resolve }; -} - -describe("op:// secret resolver", () => { - it("registers under the declared scheme", async () => { - const { resolve } = await setUp(vi.fn()); - expect(typeof resolve).toBe("function"); - }); - - it("resolves through the owner's source", async () => { - const fetchMock = vi - .fn() - .mockResolvedValueOnce(jsonResponse([{ id: "vid", name: "Infra" }])) - .mockResolvedValueOnce(jsonResponse([{ id: "iid", title: "prod-db" }])) - .mockResolvedValueOnce( - jsonResponse({ - fields: [{ id: "f1", label: "password", value: "s3cret" }], - }), - ); - const { repository, tokenStore, resolve } = await setUp(fetchMock); - - const source = await repository.create({ - id: "src-1", - userId: "owner-1", - name: "n", - kind: "onepassword-connect", - baseUrl: "https://connect.internal", - shared: false, - }); - await tokenStore.set(source.id, "tok"); - - const value = await resolve("owner-1", "op://Infra/prod-db/password"); - expect(value).toBe("s3cret"); - }); - - it("fails clearly with no configured source", async () => { - const { resolve } = await setUp(vi.fn()); - await expect( - resolve("nobody", "op://Infra/prod-db/password"), - ).rejects.toThrow(/no secret source is configured/); - }); - - it("rejects a malformed reference", async () => { - const { resolve } = await setUp(vi.fn()); - await expect(resolve("owner-1", "op://only-two")).rejects.toThrow( - /Invalid secret reference/, - ); - }); -}); diff --git a/plugins/secret-sources/tests/backend/routes.test.ts b/plugins/secret-sources/tests/backend/routes.test.ts deleted file mode 100644 index db3dc83ae..000000000 --- a/plugins/secret-sources/tests/backend/routes.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { startServer, type TestServer } from "./helpers.js"; - -function jsonResponse(body: unknown, ok = true, status = 200): Response { - return { ok, status, json: async () => body } as unknown as Response; -} - -let server: TestServer; - -afterEach(async () => { - await server?.close(); -}); - -describe("secret-sources routes", () => { - beforeEach(async () => { - server = await startServer(); - }); - - it("refuses a request without the matching core permission", async () => { - await server.close(); - server = await startServer({ permissions: [] }); - const res = await server.request("GET", "/"); - expect(res.status).toBe(403); - }); - - it("creates, lists and hides the token from the owner's own list", async () => { - const create = await server.request("POST", "/", { - body: { - name: "Team 1Password", - baseUrl: "https://connect.internal", - token: "s3cret-token", - }, - }); - expect(create.status).toBe(200); - expect(create.body.source).toMatchObject({ - name: "Team 1Password", - baseUrl: "https://connect.internal", - owned: true, - shared: false, - }); - expect(create.body.source.token).toBeUndefined(); - - const list = await server.request("GET", "/"); - expect(list.body.sources).toHaveLength(1); - expect(list.body.sources[0].owned).toBe(true); - }); - - it("rejects a create missing a required field", async () => { - const res = await server.request("POST", "/", { - body: { name: "", baseUrl: "https://x", token: "t" }, - }); - expect(res.status).toBe(400); - }); - - it("rejects an http(s)-only baseUrl", async () => { - const res = await server.request("POST", "/", { - body: { name: "n", baseUrl: "ftp://x", token: "t" }, - }); - expect(res.status).toBe(400); - }); - - it("refuses shared: true for a non-admin", async () => { - const res = await server.request("POST", "/", { - body: { - name: "n", - baseUrl: "https://connect.internal", - token: "t", - shared: true, - }, - }); - expect(res.status).toBe(403); - }); - - it("allows shared: true for an admin", async () => { - await server.close(); - server = await startServer({ - permissions: [ - "credentials.view", - "credentials.create", - "credentials.edit", - "credentials.delete", - "admin.plugins.manage", - ], - }); - const res = await server.request("POST", "/", { - body: { - name: "n", - baseUrl: "https://connect.internal", - token: "t", - shared: true, - }, - }); - expect(res.status).toBe(200); - expect(res.body.source.shared).toBe(true); - }); - - it("shows a shared source to another user, unowned", async () => { - await server.close(); - server = await startServer({ - permissions: [ - "credentials.view", - "credentials.create", - "credentials.edit", - "credentials.delete", - "admin.plugins.manage", - ], - }); - await server.request("POST", "/", { - body: { name: "n", baseUrl: "https://x", token: "t", shared: true }, - }); - const list = await server.request("GET", "/", { user: "user-2" }); - expect(list.body.sources).toHaveLength(1); - expect(list.body.sources[0].owned).toBe(false); - }); - - it("refuses an update or delete from a non-owner", async () => { - const created = await server.request("POST", "/", { - body: { name: "n", baseUrl: "https://x", token: "t" }, - }); - const id = created.body.source.id; - - const update = await server.request("PUT", `/${id}`, { - user: "user-2", - body: { name: "hijacked" }, - }); - expect(update.status).toBe(403); - - const del = await server.request("DELETE", `/${id}`, { user: "user-2" }); - expect(del.status).toBe(403); - }); - - it("updates without a token keeping the existing one, and deletes cleanly", async () => { - const created = await server.request("POST", "/", { - body: { name: "n", baseUrl: "https://x", token: "orig-token" }, - }); - const id = created.body.source.id; - - const update = await server.request("PUT", `/${id}`, { - body: { name: "renamed" }, - }); - expect(update.status).toBe(200); - - const test = await server.request("POST", `/${id}/test`); - // Reaches out through ctx.fetch, unstubbed here, so it fails softly. - expect(test.status).toBe(200); - expect(test.body.ok).toBe(false); - - const del = await server.request("DELETE", `/${id}`); - expect(del.status).toBe(200); - - const list = await server.request("GET", "/"); - expect(list.body.sources).toHaveLength(0); - }); - - it("tests a source through ctx.fetch and reports the vault count", async () => { - await server.close(); - const fetchMock = vi - .fn() - .mockResolvedValue(jsonResponse([{ id: "v1", name: "Infra" }])); - server = await startServer({ fetch: fetchMock }); - - const created = await server.request("POST", "/", { - body: { name: "n", baseUrl: "https://connect.internal", token: "tok" }, - }); - const id = created.body.source.id; - - const test = await server.request("POST", `/${id}/test`); - expect(test.status).toBe(200); - expect(test.body).toEqual({ ok: true, vaults: 1 }); - expect(fetchMock).toHaveBeenCalledWith( - "https://connect.internal/v1/vaults", - expect.objectContaining({ - headers: { Authorization: "Bearer tok" }, - }), - ); - }); -}); diff --git a/plugins/secret-sources/tests/backend/token-store.test.ts b/plugins/secret-sources/tests/backend/token-store.test.ts deleted file mode 100644 index ce934cf50..000000000 --- a/plugins/secret-sources/tests/backend/token-store.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createMockCtx } from "@termix/plugin-sdk/testing"; -import { createTokenStore } from "../../src/backend/token-store.js"; -import manifestJson from "../../manifest.json"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; - -const manifest = manifestJson as unknown as PluginManifest; - -function mockCtx(actor?: string) { - return createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: manifest.capabilities, - actor, - }); -} - -describe("token store", () => { - it("stores and reads back a token for the acting user", async () => { - const { ctx } = mockCtx("owner-1"); - const store = createTokenStore(ctx); - - await store.set("src-1", "s3cret"); - await expect(store.getForOwner("src-1", "owner-1")).resolves.toBe("s3cret"); - }); - - it("reads the owner's token for a caller currently acting as someone else", async () => { - const { ctx, setActor } = mockCtx("owner-1"); - const store = createTokenStore(ctx); - await store.set("src-1", "s3cret"); - - // A shared source's test/resolve runs as the recipient viewing it. - setActor("recipient-1"); - await expect(store.getForOwner("src-1", "owner-1")).resolves.toBe("s3cret"); - // ctx.asUser restores the previous actor once it returns. - expect(ctx.currentActor()).toBe("recipient-1"); - }); - - it("clears a token", async () => { - const { ctx } = mockCtx("owner-1"); - const store = createTokenStore(ctx); - await store.set("src-1", "s3cret"); - await store.clear("src-1"); - await expect(store.getForOwner("src-1", "owner-1")).resolves.toBeNull(); - }); -}); diff --git a/plugins/secret-sources/tests/frontend/activate.test.tsx b/plugins/secret-sources/tests/frontend/activate.test.tsx deleted file mode 100644 index a2b9d87f6..000000000 --- a/plugins/secret-sources/tests/frontend/activate.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import { - renderWithApp, - type RenderedPluginApp, -} from "@termix/plugin-sdk/testing"; -import { PluginComponent } from "@termix/plugin-sdk/ui"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import * as plugin from "../../src/frontend/index"; -import manifestJson from "../../manifest.json"; -import locales from "../../locales/en.json"; - -const manifest = manifestJson as unknown as PluginManifest; - -let rendered: RenderedPluginApp | null = null; - -afterEach(async () => { - await rendered?.deactivate(); - rendered = null; -}); - -describe(`${manifest.id} activate`, () => { - it("registers the hint and manager components", async () => { - rendered = await renderWithApp(plugin, { manifest, locales }); - render( - fallback} - />, - ); - expect(await screen.findByText(locales.hint)).toBeTruthy(); - }); - - it("renders the manager and can be closed", async () => { - rendered = await renderWithApp(plugin, { - manifest, - locales, - api: { - get: vi.fn(async () => ({ data: { sources: [] } })), - } as never, - }); - const onClose = vi.fn(); - render( - , - ); - expect(await screen.findByText(locales.title)).toBeTruthy(); - }); - - it("removes both components on deactivate", async () => { - const app = await renderWithApp(plugin, { manifest, locales }); - await app.deactivate(); - render( - gone} - />, - ); - expect(await screen.findByText("gone")).toBeTruthy(); - }); -}); diff --git a/plugins/secret-sources/tsconfig.json b/plugins/secret-sources/tsconfig.json deleted file mode 100644 index 7b35314c3..000000000 --- a/plugins/secret-sources/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@termix/plugin-sdk/tsconfig.plugin.json", - "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] -} diff --git a/plugins/secret-sources/vitest.config.ts b/plugins/secret-sources/vitest.config.ts deleted file mode 100644 index 31ba004fc..000000000 --- a/plugins/secret-sources/vitest.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; - -export default pluginVitestConfig(import.meta.url); diff --git a/plugins/serial/CHANGELOG.md b/plugins/serial/CHANGELOG.md deleted file mode 100644 index c55fc4d0e..000000000 --- a/plugins/serial/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -## 1.0.0 - -First release, bundled with Termix 2.9.0. - -Opens a serial console to a device plugged into your computer. The desktop app talks to the device directly, and the browser uses Web Serial. diff --git a/plugins/serial/README.md b/plugins/serial/README.md deleted file mode 100644 index 57680eb47..000000000 --- a/plugins/serial/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Serial - -Open a serial console to a device plugged into your computer. - -## Features - -- Open a serial console to a device plugged into your computer. -- The desktop app talks to the device directly. -- In a browser, the plugin uses Web Serial. - -## Setup - -The desktop app uses the `serialport` npm package, which has a native binary. The build does not bundle it. - -## Development - -```bash -npm run build # build into dist/ -npm run test # run this plugin's tests -npm run typecheck # type-check this plugin -``` diff --git a/plugins/serial/locales/en.json b/plugins/serial/locales/en.json deleted file mode 100644 index bf4b7de0c..000000000 --- a/plugins/serial/locales/en.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Serial" - }, - "serial": { - "title": "Serial", - "portLabel": "Port", - "portPlaceholder": "/dev/ttyUSB0 or COM3", - "baudRateLabel": "Baud Rate", - "dataBitsLabel": "Data", - "stopBitsLabel": "Stop", - "parityLabel": "Parity", - "parityNone": "None", - "parityEven": "Even", - "parityOdd": "Odd", - "connect": "Connect to Serial", - "disconnect": "Disconnect", - "refreshPorts": "Refresh ports", - "connected": "Connected to {{path}} at {{baud}} baud", - "disconnected": "Serial port disconnected", - "connectionError": "Failed to open serial port", - "wsError": "WebSocket error", - "errorNoServerUrl": "No server URL configured", - "notSupportedTitle": "Serial Not Supported", - "notSupported": "Serial connections require a browser with Web Serial API support (Chrome, Edge, or Firefox 151+), or the Termix desktop app.", - "hideHint": "You can hide the Serial tab under User Profile > Appearance > Sidebar > Navigation.", - "browserPickerHint": "Click Connect and your browser will open a port picker to choose the device." - } -} diff --git a/plugins/serial/locales/translated/af_ZA.json b/plugins/serial/locales/translated/af_ZA.json deleted file mode 100644 index 06690e526..000000000 --- a/plugins/serial/locales/translated/af_ZA.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Reeks" - }, - "serial": { - "title": "Reeks", - "portLabel": "Hawe", - "portPlaceholder": "/dev/ttyUSB0 of COM3", - "baudRateLabel": "Baudtempo", - "dataBitsLabel": "Data", - "stopBitsLabel": "Stop", - "parityLabel": "Pariteit", - "parityNone": "Geen", - "parityEven": "Selfs", - "parityOdd": "Vreemd", - "connect": "Koppel aan serieel", - "disconnect": "Ontkoppel", - "refreshPorts": "Verfris poorte", - "connected": "Verbind aan {{path}} teen {{baud}} baud", - "disconnected": "Seriepoort ontkoppel", - "connectionError": "Kon nie die seriepoort oopmaak nie", - "wsError": "WebSocket-fout", - "errorNoServerUrl": "Geen bediener-URL gekonfigureer nie", - "notSupportedTitle": "Serienommer word nie ondersteun nie", - "notSupported": "Seriële verbindings vereis 'n blaaier met Web Serial API-ondersteuning (Chrome, Edge of Firefox 151+), of die Termix-lessenaarprogram.", - "hideHint": "Jy kan die Seriële oortjie versteek onder Gebruikersprofiel > Voorkoms > Sybalk > Navigasie.", - "browserPickerHint": "Klik op Verbind en jou blaaier sal 'n poortkieser oopmaak om die toestel te kies." - } -} diff --git a/plugins/serial/locales/translated/ar_SA.json b/plugins/serial/locales/translated/ar_SA.json deleted file mode 100644 index aea5ec5ac..000000000 --- a/plugins/serial/locales/translated/ar_SA.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "تسلسلي" - }, - "serial": { - "title": "منفذ تسلسلي", - "portLabel": "المنفذ", - "portPlaceholder": "/dev/ttyUSB0 or COM3", - "baudRateLabel": "معدل الباود", - "dataBitsLabel": "البيانات", - "stopBitsLabel": "التوقف", - "parityLabel": "التكافؤ", - "parityNone": "بلا", - "parityEven": "زوجي", - "parityOdd": "فردي", - "connect": "اتصال", - "disconnect": "قطع الاتصال", - "refreshPorts": "تحديث المنافذ", - "connected": "متصل بـ {{path}} بسرعة {{baud}} باود", - "disconnected": "تم قطع الاتصال بالمنفذ التسلسلي", - "connectionError": "فشل في فتح المنفذ التسلسلي", - "wsError": "خطأ في WebSocket", - "errorNoServerUrl": "لم يتم تكوين عنوان URL للخادم", - "notSupportedTitle": "المنفذ التسلسلي غير مدعوم", - "notSupported": "تتطلب الاتصالات التسلسلية متصفحًا يدعم Web Serial API (Chrome، أو Edge، أو Firefox 151+)، أو تطبيق Termix لسطح المكتب.", - "hideHint": "يمكنك إخفاء تبويب المنفذ التسلسلي من الملف الشخصي > المظهر > الشريط الجانبي > التنقل.", - "browserPickerHint": "انقر على Connect وسيفتح متصفحك نافذة اختيار المنفذ لتحديد الجهاز." - } -} diff --git a/plugins/serial/locales/translated/bg_BG.json b/plugins/serial/locales/translated/bg_BG.json deleted file mode 100644 index 39c9de261..000000000 --- a/plugins/serial/locales/translated/bg_BG.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Сериен" - }, - "serial": { - "title": "Сериен", - "portLabel": "Порт", - "portPlaceholder": "/dev/ttyUSB0 или COM3", - "baudRateLabel": "Скорост на бодове", - "dataBitsLabel": "Данни", - "stopBitsLabel": "Спри", - "parityLabel": "Паритет", - "parityNone": "Няма", - "parityEven": "Дори", - "parityOdd": "Странно", - "connect": "Свързване към сериен", - "disconnect": "Изключване", - "refreshPorts": "Обновяване на портове", - "connected": "Свързан към {{path}} при {{baud}} бод", - "disconnected": "Серийният порт е прекъснат", - "connectionError": "Неуспешно отваряне на серийния порт", - "wsError": "Грешка в WebSocket", - "errorNoServerUrl": "Няма конфигуриран URL адрес на сървъра", - "notSupportedTitle": "Серийният номер не се поддържа", - "notSupported": "Серийните връзки изискват браузър с поддръжка на Web Serial API (Chrome, Edge или Firefox 151+) или настолното приложение Termix.", - "hideHint": "Можете да скриете раздела „Сериен номер“ в Потребителски профил > Външен вид > Странична лента > Навигация.", - "browserPickerHint": "Щракнете върху „Свързване“ и браузърът ви ще отвори инструмент за избор на порт, за да изберете устройството." - } -} diff --git a/plugins/serial/locales/translated/bn_BD.json b/plugins/serial/locales/translated/bn_BD.json deleted file mode 100644 index d739155e9..000000000 --- a/plugins/serial/locales/translated/bn_BD.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "সিরিয়াল" - }, - "serial": { - "title": "সিরিয়াল", - "portLabel": "বন্দর", - "portPlaceholder": "/dev/ttyUSB0 অথবা COM3", - "baudRateLabel": "বড রেট", - "dataBitsLabel": "ডেটা", - "stopBitsLabel": "থামুন", - "parityLabel": "সমতা", - "parityNone": "কোনোটিই না", - "parityEven": "এমনকি", - "parityOdd": "অদ্ভুত", - "connect": "সিরিয়ালের সাথে সংযোগ করুন", - "disconnect": "সংযোগ বিচ্ছিন্ন করুন", - "refreshPorts": "পোর্টগুলি রিফ্রেশ করুন", - "connected": "{{path}} এর সাথে {{baud}} বাউডে সংযুক্ত।", - "disconnected": "সিরিয়াল পোর্ট সংযোগ বিচ্ছিন্ন", - "connectionError": "সিরিয়াল পোর্ট খুলতে ব্যর্থ হয়েছে", - "wsError": "ওয়েব সকেট ত্রুটি", - "errorNoServerUrl": "কোন সার্ভার URL কনফিগার করা নেই", - "notSupportedTitle": "সিরিয়াল সমর্থিত নয়", - "notSupported": "সিরিয়াল সংযোগের জন্য ওয়েব সিরিয়াল এপিআই সমর্থনসহ একটি ব্রাউজার (ক্রোম, এজ, বা ফায়ারফক্স ১৫১+), অথবা টারমিক্স ডেস্কটপ অ্যাপ প্রয়োজন।", - "hideHint": "আপনি ইউজার প্রোফাইল > অ্যাপিয়ারেন্স > সাইডবার > নেভিগেশন-এর অধীনে সিরিয়াল ট্যাবটি লুকাতে পারেন।", - "browserPickerHint": "কানেক্ট-এ ক্লিক করলে আপনার ব্রাউজারে ডিভাইসটি বেছে নেওয়ার জন্য একটি পোর্ট পিকার খুলবে।" - } -} diff --git a/plugins/serial/locales/translated/ca_ES.json b/plugins/serial/locales/translated/ca_ES.json deleted file mode 100644 index 37d7cbda8..000000000 --- a/plugins/serial/locales/translated/ca_ES.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Sèrie" - }, - "serial": { - "title": "Sèrie", - "portLabel": "Port", - "portPlaceholder": "/dev/ttyUSB0 o COM3", - "baudRateLabel": "velocitat de bauds", - "dataBitsLabel": "Dades", - "stopBitsLabel": "Atura", - "parityLabel": "Paritat", - "parityNone": "Cap", - "parityEven": "Fins i tot", - "parityOdd": "Estrany", - "connect": "Connecta't al serial", - "disconnect": "Desconnecta", - "refreshPorts": "Actualitza els ports", - "connected": "Connectat a {{path}} a {{baud}} baud", - "disconnected": "Port sèrie desconnectat", - "connectionError": "No s'ha pogut obrir el port sèrie", - "wsError": "Error de WebSocket", - "errorNoServerUrl": "No s'ha configurat cap URL del servidor", - "notSupportedTitle": "Número de sèrie no compatible", - "notSupported": "Les connexions en sèrie requereixen un navegador compatible amb l'API Web Serial (Chrome, Edge o Firefox 151+) o l'aplicació d'escriptori Termix.", - "hideHint": "Podeu amagar la pestanya Sèrie a Perfil d'usuari > Aparença > Barra lateral > Navegació.", - "browserPickerHint": "Feu clic a Connecta i el navegador obrirà un selector de ports per triar el dispositiu." - } -} diff --git a/plugins/serial/locales/translated/cs_CZ.json b/plugins/serial/locales/translated/cs_CZ.json deleted file mode 100644 index 8a6cbd512..000000000 --- a/plugins/serial/locales/translated/cs_CZ.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Seriál" - }, - "serial": { - "title": "Seriál", - "portLabel": "Port", - "portPlaceholder": "/dev/ttyUSB0 nebo COM3", - "baudRateLabel": "Přenosová rychlost", - "dataBitsLabel": "Data", - "stopBitsLabel": "Zastávka", - "parityLabel": "Parita", - "parityNone": "Žádný", - "parityEven": "Dokonce", - "parityOdd": "Zvláštní", - "connect": "Připojení k sériovému", - "disconnect": "Odpojit", - "refreshPorts": "Obnovit porty", - "connected": "Připojeno k {{path}} rychlostí {{baud}} baudu", - "disconnected": "Sériový port odpojen", - "connectionError": "Nepodařilo se otevřít sériový port", - "wsError": "Chyba WebSocketu", - "errorNoServerUrl": "Není nakonfigurována žádná adresa URL serveru", - "notSupportedTitle": "Sériové číslo není podporováno", - "notSupported": "Sériová připojení vyžadují prohlížeč s podporou webového sériového API (Chrome, Edge nebo Firefox 151+) nebo desktopovou aplikaci Termix.", - "hideHint": "Kartu Sériové číslo můžete skrýt v části Profil uživatele > Vzhled > Postranní panel > Navigace.", - "browserPickerHint": "Klikněte na tlačítko Připojit a prohlížeč otevře výběr portu pro výběr zařízení." - } -} diff --git a/plugins/serial/locales/translated/da_DK.json b/plugins/serial/locales/translated/da_DK.json deleted file mode 100644 index 82bf10916..000000000 --- a/plugins/serial/locales/translated/da_DK.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Seriel" - }, - "serial": { - "title": "Seriel", - "portLabel": "Havn", - "portPlaceholder": "/dev/ttyUSB0 eller COM3", - "baudRateLabel": "Baudhastighed", - "dataBitsLabel": "Data", - "stopBitsLabel": "Stop", - "parityLabel": "Paritet", - "parityNone": "Ingen", - "parityEven": "Endog", - "parityOdd": "Ulige", - "connect": "Tilslut til seriel", - "disconnect": "Afbryde", - "refreshPorts": "Opdater porte", - "connected": "Forbundet til {{path}} ved {{baud}} baud", - "disconnected": "Seriel port afbrudt", - "connectionError": "Kunne ikke åbne seriel port", - "wsError": "WebSocket-fejl", - "errorNoServerUrl": "Ingen server-URL konfigureret", - "notSupportedTitle": "Seriel understøttes ikke", - "notSupported": "Serielle forbindelser kræver en browser med Web Serial API-understøttelse (Chrome, Edge eller Firefox 151+) eller Termix-desktopappen.", - "hideHint": "Du kan skjule fanen Seriel under Brugerprofil > Udseende > Sidebjælke > Navigation.", - "browserPickerHint": "Klik på Opret forbindelse, og din browser åbner en portvælger, hvor du kan vælge enheden." - } -} diff --git a/plugins/serial/locales/translated/de_DE.json b/plugins/serial/locales/translated/de_DE.json deleted file mode 100644 index 73b196602..000000000 --- a/plugins/serial/locales/translated/de_DE.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Seriell" - }, - "serial": { - "title": "Seriell", - "portLabel": "Port", - "portPlaceholder": "/dev/ttyUSB0 oder COM3", - "baudRateLabel": "Baudrate", - "dataBitsLabel": "Daten", - "stopBitsLabel": "Stop", - "parityLabel": "Parität", - "parityNone": "Keine", - "parityEven": "Gerade", - "parityOdd": "Ungerade", - "connect": "Mit Seriell verbinden", - "disconnect": "Trennen", - "refreshPorts": "Ports aktualisieren", - "connected": "Verbunden mit {{path}} bei {{baud}} Baud", - "disconnected": "Serielle Schnittstelle getrennt", - "connectionError": "Fehler beim Öffnen der seriellen Schnittstelle", - "wsError": "WebSocket-Fehler", - "errorNoServerUrl": "Keine Server-URL konfiguriert", - "notSupportedTitle": "Seriell nicht unterstützt", - "notSupported": "Serielle Verbindungen erfordern einen Browser mit Web Serial API-Unterstützung (Chrome, Edge oder Firefox 151+) oder die Termix-Desktop-App.", - "hideHint": "Sie können den Seriell-Tab unter Benutzerprofil > Erscheinungsbild > Seitenleiste > Navigation ausblenden.", - "browserPickerHint": "Klicken Sie auf Verbinden, und Ihr Browser öffnet eine Port-Auswahl, um das Gerät auszuwählen." - } -} diff --git a/plugins/serial/locales/translated/el_GR.json b/plugins/serial/locales/translated/el_GR.json deleted file mode 100644 index c46a38c78..000000000 --- a/plugins/serial/locales/translated/el_GR.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Σειράς" - }, - "serial": { - "title": "Σειράς", - "portLabel": "Λιμάνι", - "portPlaceholder": "/dev/ttyUSB0 ή COM3", - "baudRateLabel": "Ρυθμός Baud", - "dataBitsLabel": "Δεδομένα", - "stopBitsLabel": "Στάση", - "parityLabel": "Ισοτιμία", - "parityNone": "Κανένας", - "parityEven": "Ακόμη και", - "parityOdd": "Περιττός", - "connect": "Σύνδεση σε σειριακή σύνδεση", - "disconnect": "Αποσυνδέω", - "refreshPorts": "Ανανέωση θυρών", - "connected": "Συνδεδεμένο με {{path}} σε {{baud}} baud", - "disconnected": "Η σειριακή θύρα αποσυνδέθηκε", - "connectionError": "Αποτυχία ανοίγματος σειριακής θύρας", - "wsError": "Σφάλμα WebSocket", - "errorNoServerUrl": "Δεν έχει ρυθμιστεί διεύθυνση URL διακομιστή", - "notSupportedTitle": "Σειριακή Δεν Υποστηρίζεται", - "notSupported": "Οι σειριακές συνδέσεις απαιτούν ένα πρόγραμμα περιήγησης με υποστήριξη Web Serial API (Chrome, Edge ή Firefox 151+) ή την εφαρμογή Termix για υπολογιστές.", - "hideHint": "Μπορείτε να αποκρύψετε την καρτέλα Σειριακός αριθμός στην περιοχή Προφίλ χρήστη > Εμφάνιση > Πλαϊνή μπάρα > Πλοήγηση.", - "browserPickerHint": "Κάντε κλικ στην επιλογή Σύνδεση και το πρόγραμμα περιήγησής σας θα ανοίξει ένα πρόγραμμα επιλογής θυρών για να επιλέξετε τη συσκευή." - } -} diff --git a/plugins/serial/locales/translated/es_ES.json b/plugins/serial/locales/translated/es_ES.json deleted file mode 100644 index 8bce3a3c3..000000000 --- a/plugins/serial/locales/translated/es_ES.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Serial" - }, - "serial": { - "title": "Serial", - "portLabel": "Puerto", - "portPlaceholder": "/dev/ttyUSB0 or COM3", - "baudRateLabel": "Baudios", - "dataBitsLabel": "Datos", - "stopBitsLabel": "Parada", - "parityLabel": "Paridad", - "parityNone": "Ninguna", - "parityEven": "Par", - "parityOdd": "Impar", - "connect": "Conectar a Serial", - "disconnect": "Desconectar", - "refreshPorts": "Actualizar puertos", - "connected": "Conectado a {{path}} a {{baud}} baudios", - "disconnected": "Puerto serie desconectado", - "connectionError": "Error al abrir el puerto serie", - "wsError": "Error WebSocket", - "errorNoServerUrl": "No se ha configurado URL de servidor", - "notSupportedTitle": "Serial no compatible", - "notSupported": "Las conexiones serie requieren un navegador compatible con Web Serial API (Chrome, Edge o Firefox 151+), o la aplicación de escritorio Termix.", - "hideHint": "Puedes ocultar la pestaña Serial en Perfil de usuario > Apariencia > Barra lateral > Navegación.", - "browserPickerHint": "Haz clic en Conectar y el navegador abrirá un selector de puertos para elegir el dispositivo." - } -} diff --git a/plugins/serial/locales/translated/fi_FI.json b/plugins/serial/locales/translated/fi_FI.json deleted file mode 100644 index 0f3583153..000000000 --- a/plugins/serial/locales/translated/fi_FI.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Sarjanumero" - }, - "serial": { - "title": "Sarjanumero", - "portLabel": "Portti", - "portPlaceholder": "/dev/ttyUSB0 tai COM3", - "baudRateLabel": "Baud-nopeus", - "dataBitsLabel": "Data", - "stopBitsLabel": "Stop", - "parityLabel": "Pariteetti", - "parityNone": "Ei mitään", - "parityEven": "Jopa", - "parityOdd": "Pariton", - "connect": "Yhdistä sarjaporttiin", - "disconnect": "Katkaise yhteys", - "refreshPorts": "Päivitä portit", - "connected": "Yhdistetty {{path}} -laitteeseen {{baud}} baudilla", - "disconnected": "Sarjaportti irrotettu", - "connectionError": "Sarjaportin avaaminen epäonnistui", - "wsError": "WebSocket-virhe", - "errorNoServerUrl": "Palvelimen URL-osoitetta ei ole määritetty", - "notSupportedTitle": "Sarjanumeroa ei tueta", - "notSupported": "Sarjaliikenne vaatii selaimen, jossa on Web Serial API -tuki (Chrome, Edge tai Firefox 151+), tai Termix-työpöytäsovelluksen.", - "hideHint": "Voit piilottaa Sarja-välilehden kohdassa Käyttäjäprofiili > Ulkoasu > Sivupalkki > Navigointi.", - "browserPickerHint": "Napsauta Yhdistä, niin selaimesi avaa porttivalitsimen laitteen valitsemiseksi." - } -} diff --git a/plugins/serial/locales/translated/fr_FR.json b/plugins/serial/locales/translated/fr_FR.json deleted file mode 100644 index 89b9c973b..000000000 --- a/plugins/serial/locales/translated/fr_FR.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Série" - }, - "serial": { - "title": "Série", - "portLabel": "Port", - "portPlaceholder": "/dev/ttyUSB0 or COM3", - "baudRateLabel": "Débit en bauds", - "dataBitsLabel": "Données", - "stopBitsLabel": "Stop", - "parityLabel": "Parité", - "parityNone": "Aucune", - "parityEven": "Paire", - "parityOdd": "Impaire", - "connect": "Connecter", - "disconnect": "Déconnecter", - "refreshPorts": "Actualiser les ports", - "connected": "Connecté à {{path}} à {{baud}} bauds", - "disconnected": "Port série déconnecté", - "connectionError": "Échec de l'ouverture du port série", - "wsError": "Erreur WebSocket", - "errorNoServerUrl": "Aucune URL de serveur configurée", - "notSupportedTitle": "Port série non pris en charge", - "notSupported": "Les connexions série nécessitent un navigateur prenant en charge l'API Web Serial (Chrome, Edge, ou Firefox 151+), ou l'application de bureau Termix.", - "hideHint": "Vous pouvez masquer l'onglet Série sous Profil utilisateur > Apparence > Barre latérale > Navigation.", - "browserPickerHint": "Cliquez sur Connexion et votre navigateur ouvrira un sélecteur de port pour choisir le périphérique." - } -} diff --git a/plugins/serial/locales/translated/he_IL.json b/plugins/serial/locales/translated/he_IL.json deleted file mode 100644 index 619822ae5..000000000 --- a/plugins/serial/locales/translated/he_IL.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "סִדרָתִי" - }, - "serial": { - "title": "סִדרָתִי", - "portLabel": "נָמָל", - "portPlaceholder": "/dev/ttyUSB0 או COM3", - "baudRateLabel": "קצב באוד", - "dataBitsLabel": "נְתוּנִים", - "stopBitsLabel": "לְהַפְסִיק", - "parityLabel": "שִׁוּוּי", - "parityNone": "אַף לֹא אֶחָד", - "parityEven": "אֲפִילוּ", - "parityOdd": "מְשׁוּנֶה", - "connect": "חיבור סריאלי", - "disconnect": "לְנַתֵק", - "refreshPorts": "רענון פורטים", - "connected": "מחובר ל- {{path}} ב- {{baud}} באוד", - "disconnected": "יציאה טורית מנותקת", - "connectionError": "נכשלה פתיחת הפורט הטורי", - "wsError": "שגיאת WebSocket", - "errorNoServerUrl": "לא הוגדרה כתובת URL של שרת", - "notSupportedTitle": "סריאלי לא נתמך", - "notSupported": "חיבורים טוריים דורשים דפדפן עם תמיכה ב-Web Serial API (Chrome, Edge או Firefox 151+), או את אפליקציית שולחן העבודה Termix.", - "hideHint": "ניתן להסתיר את הכרטיסייה 'סדרתי' תחת פרופיל משתמש > מראה > סרגל צד > ניווט.", - "browserPickerHint": "לחץ על התחבר והדפדפן שלך יפתח בורר פורטים לבחירת המכשיר." - } -} diff --git a/plugins/serial/locales/translated/hi_IN.json b/plugins/serial/locales/translated/hi_IN.json deleted file mode 100644 index a289a4155..000000000 --- a/plugins/serial/locales/translated/hi_IN.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "सीरियल" - }, - "serial": { - "title": "सीरियल", - "portLabel": "पोर्ट", - "portPlaceholder": "/dev/ttyUSB0 or COM3", - "baudRateLabel": "बॉड दर", - "dataBitsLabel": "डेटा", - "stopBitsLabel": "स्टॉप", - "parityLabel": "पैरिटी", - "parityNone": "कोई नहीं", - "parityEven": "सम", - "parityOdd": "विषम", - "connect": "सीरियल कनेक्ट करें", - "disconnect": "डिस्कनेक्ट करें", - "refreshPorts": "पोर्ट रीफ़्रेश करें", - "connected": "{{path}} से {{baud}} बॉड पर कनेक्टेड", - "disconnected": "सीरियल पोर्ट डिस्कनेक्ट हो गया", - "connectionError": "सीरियल पोर्ट खोलने में विफल", - "wsError": "WebSocket त्रुटि", - "errorNoServerUrl": "कोई सर्वर URL कॉन्फ़िगर नहीं है", - "notSupportedTitle": "सीरियल समर्थित नहीं", - "notSupported": "सीरियल कनेक्शन के लिए Web Serial API समर्थन वाले ब्राउज़र (Chrome, Edge, या Firefox 151+) या Termix डेस्कटॉप ऐप की आवश्यकता है।", - "hideHint": "आप सीरियल टैब को User Profile > Appearance > Sidebar > Navigation में छिपा सकते हैं।", - "browserPickerHint": "Connect पर क्लिक करें और आपका ब्राउज़र डिवाइस चुनने के लिए एक पोर्ट पिकर खोलेगा।" - } -} diff --git a/plugins/serial/locales/translated/hu_HU.json b/plugins/serial/locales/translated/hu_HU.json deleted file mode 100644 index cf3fbf123..000000000 --- a/plugins/serial/locales/translated/hu_HU.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Sorozatszám" - }, - "serial": { - "title": "Sorozatszám", - "portLabel": "Kikötő", - "portPlaceholder": "/dev/ttyUSB0 vagy COM3", - "baudRateLabel": "Baud sebesség", - "dataBitsLabel": "Adat", - "stopBitsLabel": "Stop", - "parityLabel": "Paritás", - "parityNone": "Egyik sem", - "parityEven": "Még", - "parityOdd": "Páratlan", - "connect": "Csatlakozás soros porthoz", - "disconnect": "Leválasztás", - "refreshPorts": "Frissítse a portokat", - "connected": "{{path}} -hoz csatlakoztatva {{baud}} baud sebességgel", - "disconnected": "Soros port leválasztva", - "connectionError": "Nem sikerült megnyitni a soros portot", - "wsError": "WebSocket hiba", - "errorNoServerUrl": "Nincs konfigurálva szerver URL", - "notSupportedTitle": "Sorozatszám Nem támogatott", - "notSupported": "A soros kapcsolatokhoz Web Serial API-t támogató böngésző (Chrome, Edge vagy Firefox 151+), vagy a Termix asztali alkalmazás szükséges.", - "hideHint": "A Sorozat fület elrejtheted a Felhasználói profil > Megjelenés > Oldalsáv > Navigáció alatt.", - "browserPickerHint": "Kattintson a Csatlakozás gombra, és a böngészője megnyit egy portválasztót az eszköz kiválasztásához." - } -} diff --git a/plugins/serial/locales/translated/id_ID.json b/plugins/serial/locales/translated/id_ID.json deleted file mode 100644 index b44424ed3..000000000 --- a/plugins/serial/locales/translated/id_ID.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Serial" - }, - "serial": { - "title": "Serial", - "portLabel": "Pelabuhan", - "portPlaceholder": "/dev/ttyUSB0 atau COM3", - "baudRateLabel": "Kecepatan Baud", - "dataBitsLabel": "Data", - "stopBitsLabel": "Berhenti", - "parityLabel": "Keseimbangan", - "parityNone": "Tidak ada", - "parityEven": "Bahkan", - "parityOdd": "Aneh", - "connect": "Hubungkan ke Serial", - "disconnect": "Memutuskan", - "refreshPorts": "Penyegaran port", - "connected": "Terhubung ke {{path}} pada kecepatan {{baud}} baud", - "disconnected": "Port serial terputus", - "connectionError": "Gagal membuka port serial", - "wsError": "Kesalahan WebSocket", - "errorNoServerUrl": "Tidak ada URL server yang dikonfigurasi.", - "notSupportedTitle": "Nomor Seri Tidak Didukung", - "notSupported": "Koneksi serial memerlukan browser dengan dukungan Web Serial API (Chrome, Edge, atau Firefox 151+), atau aplikasi desktop Termix.", - "hideHint": "Anda dapat menyembunyikan tab Serial di bawah Profil Pengguna > Tampilan > Bilah Samping > Navigasi.", - "browserPickerHint": "Klik Sambungkan dan browser Anda akan membuka pemilih port untuk memilih perangkat." - } -} diff --git a/plugins/serial/locales/translated/it_IT.json b/plugins/serial/locales/translated/it_IT.json deleted file mode 100644 index 5d3940e45..000000000 --- a/plugins/serial/locales/translated/it_IT.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Seriale" - }, - "serial": { - "title": "Seriale", - "portLabel": "Porta", - "portPlaceholder": "/dev/ttyUSB0 or COM3", - "baudRateLabel": "Baud rate", - "dataBitsLabel": "Dati", - "stopBitsLabel": "Stop", - "parityLabel": "Parità", - "parityNone": "Nessuna", - "parityEven": "Pari", - "parityOdd": "Dispari", - "connect": "Connetti alla Seriale", - "disconnect": "Disconnetti", - "refreshPorts": "Aggiorna porte", - "connected": "Connesso a {{path}} a {{baud}} baud", - "disconnected": "Porta seriale disconnessa", - "connectionError": "Impossibile aprire la porta seriale", - "wsError": "Errore WebSocket", - "errorNoServerUrl": "Nessun URL del server configurato", - "notSupportedTitle": "Seriale non supportato", - "notSupported": "Le connessioni seriali richiedono un browser con supporto Web Serial API (Chrome, Edge o Firefox 151+), oppure l'app desktop Termix.", - "hideHint": "Puoi nascondere la scheda Seriale in Profilo Utente > Aspetto > Barra laterale > Navigazione.", - "browserPickerHint": "Fai clic su Connetti e il browser aprirà un selettore di porte per scegliere il dispositivo." - } -} diff --git a/plugins/serial/locales/translated/ja_JP.json b/plugins/serial/locales/translated/ja_JP.json deleted file mode 100644 index ba2ede59d..000000000 --- a/plugins/serial/locales/translated/ja_JP.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "シリアル" - }, - "serial": { - "title": "シリアル", - "portLabel": "ポート", - "portPlaceholder": "/dev/ttyUSB0 または COM3", - "baudRateLabel": "ボーレート", - "dataBitsLabel": "データ", - "stopBitsLabel": "ストップ", - "parityLabel": "パリティ", - "parityNone": "なし", - "parityEven": "偶数", - "parityOdd": "奇数", - "connect": "シリアルに接続", - "disconnect": "切断", - "refreshPorts": "ポートを更新", - "connected": "{{path}} に {{baud}} ボーで接続しました", - "disconnected": "シリアルポートが切断されました", - "connectionError": "シリアルポートを開けませんでした", - "wsError": "WebSocket エラー", - "errorNoServerUrl": "サーバー URL が設定されていません", - "notSupportedTitle": "シリアル非対応", - "notSupported": "シリアル接続には Web Serial API をサポートするブラウザ (Chrome, Edge, Firefox 151+) または Termix デスクトップアプリが必要です。", - "hideHint": "シリアルタブは「ユーザープロファイル > 外観 > サイドバー > ナビゲーション」で非表示にできます。", - "browserPickerHint": "接続をクリックすると、ブラウザがポートピッカーを開き、デバイスを選択できます。" - } -} diff --git a/plugins/serial/locales/translated/ko_KR.json b/plugins/serial/locales/translated/ko_KR.json deleted file mode 100644 index 51b81e612..000000000 --- a/plugins/serial/locales/translated/ko_KR.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "시리얼" - }, - "serial": { - "title": "시리얼", - "portLabel": "포트", - "portPlaceholder": "/dev/ttyUSB0 or COM3", - "baudRateLabel": "보레이트", - "dataBitsLabel": "데이터", - "stopBitsLabel": "스톱", - "parityLabel": "패리티", - "parityNone": "없음", - "parityEven": "짝수", - "parityOdd": "홀수", - "connect": "시리얼 연결", - "disconnect": "연결 해제", - "refreshPorts": "포트 새로 고침", - "connected": "{{path}}에 {{baud}} 보로 연결됨", - "disconnected": "시리얼 포트 연결 해제됨", - "connectionError": "시리얼 포트를 열지 못했습니다.", - "wsError": "WebSocket 오류", - "errorNoServerUrl": "서버 URL이 구성되지 않았습니다.", - "notSupportedTitle": "시리얼 미지원", - "notSupported": "시리얼 연결에는 Web Serial API를 지원하는 브라우저(Chrome, Edge, Firefox 151+) 또는 Termix 데스크톱 앱이 필요합니다.", - "hideHint": "Serial 탭은 사용자 프로필 > 모양 > 사이드바 > 탐색에서 숨길 수 있습니다.", - "browserPickerHint": "연결 버튼을 클릭하면 브라우저에서 장치 선택을 위한 포트 선택기가 열립니다." - } -} diff --git a/plugins/serial/locales/translated/nl_NL.json b/plugins/serial/locales/translated/nl_NL.json deleted file mode 100644 index 2c46f23bc..000000000 --- a/plugins/serial/locales/translated/nl_NL.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Serieel" - }, - "serial": { - "title": "Serieel", - "portLabel": "Haven", - "portPlaceholder": "/dev/ttyUSB0 of COM3", - "baudRateLabel": "Baudrate", - "dataBitsLabel": "Gegevens", - "stopBitsLabel": "Stop", - "parityLabel": "Pariteit", - "parityNone": "Geen", - "parityEven": "Zelfs", - "parityOdd": "Vreemd", - "connect": "Verbinden via serieel", - "disconnect": "Verbinding verbreken", - "refreshPorts": "Vernieuw poorten", - "connected": "Verbonden met {{path}} met {{baud}} baud", - "disconnected": "Seriële poort losgekoppeld", - "connectionError": "Het openen van de seriële poort is mislukt.", - "wsError": "WebSocket-fout", - "errorNoServerUrl": "Geen server-URL geconfigureerd", - "notSupportedTitle": "Serienummer niet ondersteund", - "notSupported": "Voor seriële verbindingen is een browser met Web Serial API-ondersteuning vereist (Chrome, Edge of Firefox 151+), of de Termix-desktopapp.", - "hideHint": "Je kunt het tabblad 'Serienummer' verbergen via Gebruikersprofiel > Uiterlijk > Zijbalk > Navigatie.", - "browserPickerHint": "Klik op Verbinden en uw browser opent een poortkiezer waarmee u het apparaat kunt selecteren." - } -} diff --git a/plugins/serial/locales/translated/no_NO.json b/plugins/serial/locales/translated/no_NO.json deleted file mode 100644 index 068df0e16..000000000 --- a/plugins/serial/locales/translated/no_NO.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Seriell" - }, - "serial": { - "title": "Seriell", - "portLabel": "Havn", - "portPlaceholder": "/dev/ttyUSB0 eller COM3", - "baudRateLabel": "Baudhastighet", - "dataBitsLabel": "Data", - "stopBitsLabel": "Stopp", - "parityLabel": "Paritet", - "parityNone": "Ingen", - "parityEven": "Til og med", - "parityOdd": "Merkelig", - "connect": "Koble til seriell", - "disconnect": "Koble fra", - "refreshPorts": "Oppdater porter", - "connected": "Koblet til {{path}} ved {{baud}} baud", - "disconnected": "Seriell port frakoblet", - "connectionError": "Klarte ikke å åpne serieporten", - "wsError": "WebSocket-feil", - "errorNoServerUrl": "Ingen server-URL konfigurert", - "notSupportedTitle": "Serienummer støttes ikke", - "notSupported": "Serielle tilkoblinger krever en nettleser med støtte for Web Serial API (Chrome, Edge eller Firefox 151+), eller Termix-skrivebordsappen.", - "hideHint": "Du kan skjule Seriell-fanen under Brukerprofil > Utseende > Sidefelt > Navigasjon.", - "browserPickerHint": "Klikk på Koble til, så åpner nettleseren din en portvelger der du kan velge enheten." - } -} diff --git a/plugins/serial/locales/translated/pl_PL.json b/plugins/serial/locales/translated/pl_PL.json deleted file mode 100644 index ce7105580..000000000 --- a/plugins/serial/locales/translated/pl_PL.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Port szeregowy" - }, - "serial": { - "title": "Port szeregowy", - "portLabel": "Port", - "portPlaceholder": "/dev/ttyUSB0 lub COM3", - "baudRateLabel": "Szybkość transmisji", - "dataBitsLabel": "Dane", - "stopBitsLabel": "Stop", - "parityLabel": "Parzystość", - "parityNone": "Brak", - "parityEven": "Parzysta", - "parityOdd": "Nieparzysta", - "connect": "Połącz z portem szeregowym", - "disconnect": "Odłącz", - "refreshPorts": "Odśwież porty", - "connected": "Połączono z {{path}} z szybkością {{baud}} bodów", - "disconnected": "Port szeregowy rozłączony", - "connectionError": "Nie udało się otworzyć portu szeregowego", - "wsError": "Błąd WebSocket", - "errorNoServerUrl": "Brak skonfigurowanego adresu URL serwera", - "notSupportedTitle": "Port szeregowy nieobsługiwany", - "notSupported": "Połączenia szeregowe wymagają przeglądarki obsługującej Web Serial API (Chrome, Edge lub Firefox 151+) albo aplikacji desktopowej Termix.", - "hideHint": "Kartę Port szeregowy można ukryć w: Profil użytkownika > Wygląd > Pasek boczny > Nawigacja.", - "browserPickerHint": "Kliknij Połącz, a przeglądarka otworzy okno wyboru portu, w którym wskażesz urządzenie." - } -} diff --git a/plugins/serial/locales/translated/pt_BR.json b/plugins/serial/locales/translated/pt_BR.json deleted file mode 100644 index 83c97828d..000000000 --- a/plugins/serial/locales/translated/pt_BR.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Serial" - }, - "serial": { - "title": "Serial", - "portLabel": "Porta", - "portPlaceholder": "/dev/ttyUSB0 ou COM3", - "baudRateLabel": "Taxa de transmissão (Baud rate)", - "dataBitsLabel": "Dados", - "stopBitsLabel": "Parar", - "parityLabel": "Paridade", - "parityNone": "Nenhum", - "parityEven": "Até", - "parityOdd": "Chance", - "connect": "Conectar à porta serial", - "disconnect": "Desconectar", - "refreshPorts": "Atualizar portas", - "connected": "Conectado a {{path}} na taxa de transmissão de {{baud}}", - "disconnected": "Porta serial desconectada", - "connectionError": "Falha ao abrir a porta serial", - "wsError": "Erro no WebSocket", - "errorNoServerUrl": "Nenhuma URL de servidor configurada", - "notSupportedTitle": "Serial não suportado", - "notSupported": "As conexões seriais exigem um navegador com suporte à API Web Serial (Chrome, Edge ou Firefox 151+) ou o aplicativo Termix para desktop.", - "hideHint": "Você pode ocultar a guia Serial em Perfil do Usuário > Aparência > Barra lateral > Navegação.", - "browserPickerHint": "Clique em Conectar e seu navegador abrirá um seletor de portas para que você escolha o dispositivo." - } -} diff --git a/plugins/serial/locales/translated/pt_PT.json b/plugins/serial/locales/translated/pt_PT.json deleted file mode 100644 index 11a6d758c..000000000 --- a/plugins/serial/locales/translated/pt_PT.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Serial" - }, - "serial": { - "title": "Serial", - "portLabel": "Porta", - "portPlaceholder": "/dev/ttyUSB0 ou COM3", - "baudRateLabel": "Taxa Baud", - "dataBitsLabel": "Dados", - "stopBitsLabel": "Stop", - "parityLabel": "Paridade", - "parityNone": "Nenhuma", - "parityEven": "Par", - "parityOdd": "Ímpar", - "connect": "Ligar à Serial", - "disconnect": "Desligar", - "refreshPorts": "Atualizar portas", - "connected": "Ligado a {{path}} a {{baud}} baud", - "disconnected": "Porta série desligada", - "connectionError": "Falha ao abrir a porta série", - "wsError": "Erro de WebSocket", - "errorNoServerUrl": "Nenhum URL de servidor configurado", - "notSupportedTitle": "Série não suportada", - "notSupported": "As ligações série requerem um navegador com suporte à Web Serial API (Chrome, Edge ou Firefox 151+) ou a aplicação de ambiente de trabalho Termix.", - "hideHint": "Pode ocultar o separador Série em Perfil do Utilizador > Aparência > Barra Lateral > Navegação.", - "browserPickerHint": "Clique em Ligar e o seu navegador abrirá um seletor de porta para escolher o dispositivo." - } -} diff --git a/plugins/serial/locales/translated/ro_RO.json b/plugins/serial/locales/translated/ro_RO.json deleted file mode 100644 index fc1f7b0a5..000000000 --- a/plugins/serial/locales/translated/ro_RO.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Serial" - }, - "serial": { - "title": "Serial", - "portLabel": "Port", - "portPlaceholder": "/dev/ttyUSB0 sau COM3", - "baudRateLabel": "Rată de baud", - "dataBitsLabel": "Date", - "stopBitsLabel": "Stop", - "parityLabel": "Paritate", - "parityNone": "Nici unul", - "parityEven": "Chiar", - "parityOdd": "Ciudat", - "connect": "Conectare la serial", - "disconnect": "Deconecta", - "refreshPorts": "Reîmprospătați porturile", - "connected": "Conectat la {{path}} la {{baud}} baud", - "disconnected": "Portul serial deconectat", - "connectionError": "Nu s-a putut deschide portul serial", - "wsError": "Eroare WebSocket", - "errorNoServerUrl": "Nu este configurată nicio adresă URL a serverului", - "notSupportedTitle": "Număr de serie neacceptat", - "notSupported": "Conexiunile seriale necesită un browser cu suport Web Serial API (Chrome, Edge sau Firefox 151+) sau aplicația desktop Termix.", - "hideHint": "Puteți ascunde fila Serial din Profil utilizator > Aspect > Bara laterală > Navigare.", - "browserPickerHint": "Faceți clic pe Conectare și browserul dvs. va deschide un selector de porturi pentru a alege dispozitivul." - } -} diff --git a/plugins/serial/locales/translated/ru_RU.json b/plugins/serial/locales/translated/ru_RU.json deleted file mode 100644 index 9f99fbfa1..000000000 --- a/plugins/serial/locales/translated/ru_RU.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Последовательный порт" - }, - "serial": { - "title": "Последовательный порт", - "portLabel": "Порт", - "portPlaceholder": "/dev/ttyUSB0 or COM3", - "baudRateLabel": "Скорость передачи", - "dataBitsLabel": "Биты данных", - "stopBitsLabel": "Стоп-биты", - "parityLabel": "Чётность", - "parityNone": "Нет", - "parityEven": "Чётный", - "parityOdd": "Нечётный", - "connect": "Подключиться", - "disconnect": "Отключиться", - "refreshPorts": "Обновить порты", - "connected": "Подключено к {{path}} на скорости {{baud}} бод", - "disconnected": "Последовательный порт отключен", - "connectionError": "Ошибка открытия последовательного порта", - "wsError": "Ошибка WebSocket", - "errorNoServerUrl": "Не настроен URL сервера", - "notSupportedTitle": "Последовательный порт не поддерживается", - "notSupported": "Последовательные подключения требуют браузер с поддержкой Web Serial API (Chrome, Edge или Firefox 151+) или десктопное приложение Termix.", - "hideHint": "Вы можете скрыть вкладку «Serial» в разделе «Профиль пользователя» > «Оформление» > «Боковая панель» > «Навигация».", - "browserPickerHint": "Нажмите «Подключиться», и браузер откроет окно выбора порта для выбора устройства." - } -} diff --git a/plugins/serial/locales/translated/sr_SP.json b/plugins/serial/locales/translated/sr_SP.json deleted file mode 100644 index 0f7d87efe..000000000 --- a/plugins/serial/locales/translated/sr_SP.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Серијски" - }, - "serial": { - "title": "Серијски", - "portLabel": "Лука", - "portPlaceholder": "/dev/ttyUSB0 или COM3", - "baudRateLabel": "Брзина преноса података", - "dataBitsLabel": "Подаци", - "stopBitsLabel": "Заустави", - "parityLabel": "Паритет", - "parityNone": "Ниједан", - "parityEven": "Чак", - "parityOdd": "Чудно", - "connect": "Повежи се са серијским", - "disconnect": "Прекини везу", - "refreshPorts": "Освежи портове", - "connected": "Повезано са {{path}} брзином {{baud}} бауда", - "disconnected": "Серијски порт је искључен", - "connectionError": "Отварање серијског порта није успело", - "wsError": "Грешка WebSocket-а", - "errorNoServerUrl": "Није конфигурисан URL сервера", - "notSupportedTitle": "Серијски број није подржан", - "notSupported": "Серијске везе захтевају прегледач са подршком за Web Serial API (Chrome, Edge или Firefox 151+) или Termix десктоп апликацију.", - "hideHint": "Картицу Серијски број можете сакрити под Кориснички профил > Изглед > Бочна трака > Навигација.", - "browserPickerHint": "Кликните на Повежи се и ваш прегледач ће отворити бирач портова за избор уређаја." - } -} diff --git a/plugins/serial/locales/translated/sv_SE.json b/plugins/serial/locales/translated/sv_SE.json deleted file mode 100644 index 3b565d4bd..000000000 --- a/plugins/serial/locales/translated/sv_SE.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Serie" - }, - "serial": { - "title": "Serie", - "portLabel": "Hamn", - "portPlaceholder": "/dev/ttyUSB0 eller COM3", - "baudRateLabel": "Baudhastighet", - "dataBitsLabel": "Data", - "stopBitsLabel": "Stopp", - "parityLabel": "Paritet", - "parityNone": "Ingen", - "parityEven": "Även", - "parityOdd": "Udda", - "connect": "Anslut till seriellt", - "disconnect": "Koppla från", - "refreshPorts": "Uppdatera portar", - "connected": "Ansluten till {{path}} vid {{baud}} baud", - "disconnected": "Seriell port frånkopplad", - "connectionError": "Misslyckades med att öppna serieporten", - "wsError": "WebSocket-fel", - "errorNoServerUrl": "Ingen server-URL konfigurerad", - "notSupportedTitle": "Seriellt stöds inte", - "notSupported": "Seriella anslutningar kräver en webbläsare med stöd för Web Serial API (Chrome, Edge eller Firefox 151+), eller Termix-skrivbordsappen.", - "hideHint": "Du kan dölja fliken Seriell under Användarprofil > Utseende > Sidofält > Navigering.", - "browserPickerHint": "Klicka på Anslut så öppnar din webbläsare en portväljare där du kan välja enheten." - } -} diff --git a/plugins/serial/locales/translated/th_TH.json b/plugins/serial/locales/translated/th_TH.json deleted file mode 100644 index 4c74c23dc..000000000 --- a/plugins/serial/locales/translated/th_TH.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "ซีเรียล" - }, - "serial": { - "title": "ซีเรียล", - "portLabel": "ท่าเรือ", - "portPlaceholder": "/dev/ttyUSB0 หรือ COM3", - "baudRateLabel": "อัตราบิต", - "dataBitsLabel": "ข้อมูล", - "stopBitsLabel": "หยุด", - "parityLabel": "ความเท่าเทียมกัน", - "parityNone": "ไม่มี", - "parityEven": "สม่ำเสมอ", - "parityOdd": "แปลก", - "connect": "เชื่อมต่อกับพอร์ตอนุกรม", - "disconnect": "ตัดการเชื่อมต่อ", - "refreshPorts": "พอร์ตรีเฟรช", - "connected": "เชื่อมต่อกับ {{path}} ที่ความเร็ว {{baud}} บอด", - "disconnected": "พอร์ตอนุกรมถูกตัดการเชื่อมต่อ", - "connectionError": "ไม่สามารถเปิดพอร์ตอนุกรมได้", - "wsError": "ข้อผิดพลาด WebSocket", - "errorNoServerUrl": "ไม่ได้กำหนดค่า URL เซิร์ฟเวอร์", - "notSupportedTitle": "ไม่รองรับหมายเลขซีเรียล", - "notSupported": "การเชื่อมต่อแบบอนุกรมจำเป็นต้องใช้เบราว์เซอร์ที่รองรับ Web Serial API (Chrome, Edge หรือ Firefox เวอร์ชัน 151 ขึ้นไป) หรือแอปพลิเคชัน Termix บนเดสก์ท็อป", - "hideHint": "คุณสามารถซ่อนแท็บ Serial ได้ที่ โปรไฟล์ผู้ใช้ > ลักษณะที่ปรากฏ > แถบด้านข้าง > การนำทาง", - "browserPickerHint": "คลิก \"เชื่อมต่อ\" แล้วเบราว์เซอร์ของคุณจะเปิดตัวเลือกพอร์ตเพื่อเลือกอุปกรณ์" - } -} diff --git a/plugins/serial/locales/translated/tr_TR.json b/plugins/serial/locales/translated/tr_TR.json deleted file mode 100644 index 30edc85c7..000000000 --- a/plugins/serial/locales/translated/tr_TR.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Seri" - }, - "serial": { - "title": "Seri", - "portLabel": "Port", - "portPlaceholder": "/dev/ttyUSB0 veya COM3", - "baudRateLabel": "Baud Hızı", - "dataBitsLabel": "Veri", - "stopBitsLabel": "Durdurma", - "parityLabel": "Eşlik", - "parityNone": "Yok", - "parityEven": "Çift", - "parityOdd": "Tek", - "connect": "Seri Bağlantıya Bağlan", - "disconnect": "Bağlantıyı Kes", - "refreshPorts": "Portları yenile", - "connected": "{{path}} ile {{baud}} baud hızında bağlandı.", - "disconnected": "Seri port bağlantısı kesildi.", - "connectionError": "Seri bağlantı noktası açılamadı", - "wsError": "WebSocket hatası", - "errorNoServerUrl": "Sunucu URL'si yapılandırılmamış", - "notSupportedTitle": "Seri Bağlantı Desteklenmiyor", - "notSupported": "Seri bağlantılar, Web Serial API desteği olan bir tarayıcı (Chrome, Edge veya Firefox 151+) ya da Termix masaüstü uygulaması gerektirir.", - "hideHint": "Seri sekmesini Kullanıcı Profili > Görünüm > Kenar Çubuğu > Gezinme altından gizleyebilirsiniz.", - "browserPickerHint": "Bağlan'a tıklayın, tarayıcınız cihazı seçmek için bir bağlantı noktası seçici açacaktır." - } -} diff --git a/plugins/serial/locales/translated/uk_UA.json b/plugins/serial/locales/translated/uk_UA.json deleted file mode 100644 index 3487ac1f5..000000000 --- a/plugins/serial/locales/translated/uk_UA.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Серійний" - }, - "serial": { - "title": "Серійний", - "portLabel": "Порт", - "portPlaceholder": "/dev/ttyUSB0 або COM3", - "baudRateLabel": "Швидкість передачі даних", - "dataBitsLabel": "Дані", - "stopBitsLabel": "СТІЙ", - "parityLabel": "Паритет", - "parityNone": "Жоден", - "parityEven": "Навіть", - "parityOdd": "Дивний", - "connect": "Підключення до послідовного порту", - "disconnect": "Відключитися", - "refreshPorts": "Оновити порти", - "connected": "Підключено до {{path}} зі швидкістю {{baud}} бод", - "disconnected": "Послідовний порт відключено", - "connectionError": "Не вдалося відкрити послідовний порт", - "wsError": "Помилка WebSocket", - "errorNoServerUrl": "URL-адреса сервера не налаштована", - "notSupportedTitle": "Серійний номер не підтримується", - "notSupported": "Для послідовних з’єднань потрібен браузер із підтримкою Web Serial API (Chrome, Edge або Firefox 151+) або настільний додаток Termix.", - "hideHint": "Ви можете приховати вкладку «Серійний номер» у розділі «Профіль користувача» > «Зовнішній вигляд» > «Бічна панель» > «Навігація».", - "browserPickerHint": "Натисніть «Підключитися», і ваш браузер відкриє вибір порту для вибору пристрою." - } -} diff --git a/plugins/serial/locales/translated/vi_VN.json b/plugins/serial/locales/translated/vi_VN.json deleted file mode 100644 index a6140974c..000000000 --- a/plugins/serial/locales/translated/vi_VN.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "Nối tiếp" - }, - "serial": { - "title": "Cổng nối tiếp", - "portLabel": "Cổng", - "portPlaceholder": "/dev/ttyUSB0 or COM3", - "baudRateLabel": "Tốc độ Baud", - "dataBitsLabel": "Dữ liệu", - "stopBitsLabel": "Bit dừng", - "parityLabel": "Kiểm tra chẵn lẻ", - "parityNone": "Không", - "parityEven": "Chẵn", - "parityOdd": "Lẻ", - "connect": "Kết nối cổng nối tiếp", - "disconnect": "Ngắt kết nối", - "refreshPorts": "Làm mới cổng", - "connected": "Đã kết nối tới {{path}} với tốc độ {{baud}} baud", - "disconnected": "Đã ngắt kết nối cổng nối tiếp", - "connectionError": "Không thể mở cổng nối tiếp", - "wsError": "Lỗi WebSocket", - "errorNoServerUrl": "Chưa cấu hình URL máy chủ", - "notSupportedTitle": "Không hỗ trợ Serial", - "notSupported": "Kết nối nối tiếp yêu cầu trình duyệt hỗ trợ Web Serial API (Chrome, Edge, hoặc Firefox 151+), hoặc ứng dụng Termix desktop.", - "hideHint": "Bạn có thể ẩn tab Serial trong Hồ sơ người dùng > Giao diện > Thanh bên > Điều hướng.", - "browserPickerHint": "Nhấn Kết nối và trình duyệt sẽ mở bộ chọn cổng để chọn thiết bị." - } -} diff --git a/plugins/serial/locales/translated/zh_CN.json b/plugins/serial/locales/translated/zh_CN.json deleted file mode 100644 index 2fdf4476f..000000000 --- a/plugins/serial/locales/translated/zh_CN.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "串口" - }, - "serial": { - "title": "串口", - "portLabel": "端口", - "portPlaceholder": "/dev/ttyUSB0 或 COM3", - "baudRateLabel": "波特率", - "dataBitsLabel": "数据", - "stopBitsLabel": "停止位", - "parityLabel": "奇偶校验", - "parityNone": "无", - "parityEven": "偶校验", - "parityOdd": "奇校验", - "connect": "连接串口", - "disconnect": "断开", - "refreshPorts": "刷新端口", - "connected": "已连接到 {{path}},波特率 {{baud}}", - "disconnected": "串口已断开", - "connectionError": "打开串口失败", - "wsError": "WebSocket 错误", - "errorNoServerUrl": "未配置服务器 URL", - "notSupportedTitle": "不支持串口", - "notSupported": "串口连接需要支持 Web Serial API 的浏览器(Chrome、Edge 或 Firefox 151+),或者 Termix 桌面应用。", - "hideHint": "您可以在用户资料 > 外观 > 侧边栏 > 导航中隐藏串口标签页。", - "browserPickerHint": "点击连接,您的浏览器将打开端口选择器以选择设备。" - } -} diff --git a/plugins/serial/locales/translated/zh_TW.json b/plugins/serial/locales/translated/zh_TW.json deleted file mode 100644 index 1258a15c0..000000000 --- a/plugins/serial/locales/translated/zh_TW.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "nav": { - "serial": "序列" - }, - "serial": { - "title": "序列", - "portLabel": "連接埠", - "portPlaceholder": "/dev/ttyUSB0 or COM3", - "baudRateLabel": "鮑率", - "dataBitsLabel": "資料位元", - "stopBitsLabel": "停止位元", - "parityLabel": "同位檢查", - "parityNone": "無", - "parityEven": "偶", - "parityOdd": "奇", - "connect": "連線至序列埠", - "disconnect": "中斷連線", - "refreshPorts": "重新整理連接埠", - "connected": "已連線至 {{path}},鮑率 {{baud}}", - "disconnected": "序列埠已中斷連線", - "connectionError": "開啟序列埠失敗", - "wsError": "WebSocket 錯誤", - "errorNoServerUrl": "未設定伺服器網址", - "notSupportedTitle": "不支援序列埠", - "notSupported": "序列連線需要支援 Web Serial API 的瀏覽器 (Chrome、Edge 或 Firefox 151+) 或 Termix 桌面應用程式。", - "hideHint": "您可以在「使用者設定檔 > 外觀 > 側邊欄 > 導覽」中隱藏序列索引標籤。", - "browserPickerHint": "按一下連線,瀏覽器會開啟連接埠選擇器讓您選取裝置。" - } -} diff --git a/plugins/serial/manifest.json b/plugins/serial/manifest.json deleted file mode 100644 index bca945f96..000000000 --- a/plugins/serial/manifest.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "serial", - "name": "Serial", - "version": "1.0.0", - "description": "Open a serial console to a device plugged into your computer.", - "author": { - "name": "Termix" - }, - "license": "MIT", - "repository": "https://github.com/Termix-SSH/Termix", - "category": "Terminal", - "icon": "Usb", - "engine": { - "termix": ">=2.9.0", - "api": "1" - }, - "capabilities": ["network:serve", "device:serial", "ui:surface"], - "nativeDependencies": ["serialport"], - "locales": "locales", - "platforms": ["linux", "win32", "darwin"], - "contributes": { - "tabs": [ - { - "id": "serial", - "titleKey": "nav.serial", - "icon": "Usb", - "openFrom": ["rail"] - } - ], - "panels": [ - { - "id": "serial", - "titleKey": "nav.serial", - "icon": "Usb" - } - ], - "uses": ["terminal.resolveTheme"] - }, - "desktop": "local" -} diff --git a/plugins/serial/package.json b/plugins/serial/package.json deleted file mode 100644 index 314fec69f..000000000 --- a/plugins/serial/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "@termix-plugin/serial", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "build": "termix-plugin build", - "test": "termix-plugin test", - "typecheck": "tsc -p .", - "validate": "termix-plugin validate" - }, - "dependencies": { - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", - "lucide-react": "^1.33.0", - "react-xtermjs": "^1.0.10", - "serialport": "^13.0.0" - }, - "devDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "@testing-library/dom": "^10.4.2", - "@testing-library/react": "^16.3.3", - "@types/node": "^26.2.0", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", - "jsdom": "^30.0.1", - "vitest": "^4.1.11" - }, - "peerDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "react": "^19.2.8", - "ws": "^8.21.3" - }, - "termix": { - "patches": [ - "xterm-android-ime" - ] - }, - "overrides": { - "react-xtermjs": { - "@xterm/xterm": "$@xterm/xterm" - } - } -} diff --git a/plugins/serial/src/backend/index.ts b/plugins/serial/src/backend/index.ts deleted file mode 100644 index 17f46a0a9..000000000 --- a/plugins/serial/src/backend/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { createSerialSession } from "./session.js"; - -export async function activate(ctx: PluginContext): Promise { - ctx.ws.route("/console", createSerialSession(ctx)); - ctx.log.info("Serial console mounted at /plugin-ws/serial/console"); -} - -export async function deactivate(): Promise { - // The WS route and every open socket are torn down by core: see - // src/backend/plugins/ws.ts. Nothing here outlives activate(). -} diff --git a/plugins/serial/src/backend/session.ts b/plugins/serial/src/backend/session.ts deleted file mode 100644 index b869f7ac0..000000000 --- a/plugins/serial/src/backend/session.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { WebSocket } from "ws"; -import { SerialPort } from "serialport"; -import type { - PluginContext, - PluginWebSocketConnection, -} from "@termix/plugin-sdk/backend"; -import { parseWsMessage } from "./ws-message.js"; - -interface SerialConnectData { - path: string; - baudRate: number; - dataBits?: 5 | 6 | 7 | 8; - stopBits?: 1 | 2; - parity?: "none" | "even" | "odd"; -} - -/** - * Handles one /plugin-ws/serial/console connection. Auth already ran (core's - * ctx.ws.route authenticates before this is called), so every message here - * only has to decide whether to touch the actual hardware. - */ -export function createSerialSession(ctx: PluginContext) { - return async (connection: PluginWebSocketConnection): Promise => { - const ws = connection.socket as WebSocket; - let port: SerialPort | null = null; - - const send = (msg: object) => { - if (ws.readyState === ws.OPEN) { - ws.send(JSON.stringify(msg)); - } - }; - - const cleanup = () => { - if (port?.isOpen) { - port.close(); - } - port = null; - }; - - if (!connection.isDataUnlocked()) { - send({ type: "error", data: "Data locked" }); - ws.close(1008, "Data access required"); - return; - } - - ws.on("message", async (raw) => { - let type: string; - let data: unknown; - try { - ({ type, data } = parseWsMessage(raw)); - } catch { - return; - } - - if (!connection.isDataUnlocked()) { - send({ type: "error", data: "Data access expired" }); - ws.close(1008, "Data access expired"); - return; - } - - try { - switch (type) { - case "list_ports": { - await ctx.capabilities.require("device:serial"); - try { - const ports = await SerialPort.list(); - send({ type: "ports_list", data: ports }); - } catch (err) { - send({ - type: "error", - data: errorMessage(err, "Failed to list ports"), - }); - } - break; - } - - case "connect": { - await ctx.capabilities.require("device:serial"); - - if (port?.isOpen) { - port.close(); - port = null; - } - - const cfg = data as SerialConnectData; - if (!cfg?.path || !cfg?.baudRate) { - send({ type: "error", data: "Missing port path or baud rate" }); - break; - } - - try { - const opened = new SerialPort({ - path: cfg.path, - baudRate: cfg.baudRate, - dataBits: cfg.dataBits ?? 8, - stopBits: cfg.stopBits ?? 1, - parity: cfg.parity ?? "none", - autoOpen: false, - }); - port = opened; - - opened.open((err) => { - if (err) { - ctx.log.error(`Serial port open failed for ${cfg.path}`, err); - send({ type: "error", data: err.message }); - port = null; - return; - } - ctx.log.info( - `Serial port opened: ${cfg.path} at ${cfg.baudRate} baud`, - ); - send({ type: "connected" }); - }); - - opened.on("data", (chunk: Buffer) => { - send({ type: "data", data: chunk.toString("binary") }); - }); - - opened.on("error", (err) => { - send({ type: "error", data: err.message }); - }); - - opened.on("close", () => { - send({ type: "disconnected" }); - port = null; - }); - } catch (err) { - send({ - type: "error", - data: errorMessage(err, "Failed to open serial port"), - }); - } - break; - } - - case "input": { - if (!port?.isOpen) break; - const input = typeof data === "string" ? data : ""; - if (!input) break; - port.write(Buffer.from(input, "binary"), (err) => { - if (err) send({ type: "error", data: err.message }); - }); - break; - } - - case "disconnect": { - cleanup(); - send({ type: "disconnected" }); - break; - } - } - } catch (err) { - ctx.log.error("Error handling serial WebSocket message", err); - send({ - type: "error", - data: - err instanceof Error ? err.message : "Failed to process message", - }); - } - }); - - ws.on("close", cleanup); - ws.on("error", cleanup); - }; -} - -function errorMessage(err: unknown, fallback: string): string { - return err instanceof Error ? err.message : fallback; -} diff --git a/plugins/serial/src/backend/ws-message.ts b/plugins/serial/src/backend/ws-message.ts deleted file mode 100644 index ad8907d01..000000000 --- a/plugins/serial/src/backend/ws-message.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { RawData } from "ws"; - -// Control messages here are tiny (a path, a baud rate) or raw terminal input, -// bounded by what a user can type or paste, so anything larger is abuse. -const MAX_MESSAGE_BYTES = 1024 * 1024; - -function rawByteLength(raw: RawData): number { - if (Buffer.isBuffer(raw)) return raw.length; - if (Array.isArray(raw)) - return raw.reduce((sum, part) => sum + part.length, 0); - if (raw instanceof ArrayBuffer) return raw.byteLength; - return 0; -} - -/** Parses a WebSocket frame into `{ type, data }`, throwing on anything malformed. */ -export function parseWsMessage(raw: RawData): { type: string; data: unknown } { - if (rawByteLength(raw) > MAX_MESSAGE_BYTES) { - throw new Error("Message too large"); - } - - let parsed: unknown; - try { - parsed = JSON.parse(raw.toString()); - } catch { - throw new Error("Invalid JSON"); - } - - if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { - throw new Error("Message must be a JSON object"); - } - - const { type, data } = parsed as { type?: unknown; data?: unknown }; - if (typeof type !== "string") { - throw new Error("Message type must be a string"); - } - - return { type, data }; -} diff --git a/plugins/serial/src/frontend/Serial.tsx b/plugins/serial/src/frontend/Serial.tsx deleted file mode 100644 index 6be1b9717..000000000 --- a/plugins/serial/src/frontend/Serial.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import { - useEffect, - useRef, - useCallback, - forwardRef, - useImperativeHandle, -} from "react"; -import { useXTerm } from "react-xtermjs"; -import { FitAddon } from "@xterm/addon-fit"; -import { TriangleAlert } from "lucide-react"; -import { - invokeAction, - useTranslation, - useTheme, -} from "@termix/plugin-sdk/frontend"; -import type { SerialConfig, SerialHandle } from "./types.js"; -import { isElectron } from "./electron.js"; -import { resolveSerialWsUrl } from "./transport.js"; - -interface SerialLook { - colors: Record; - fontFamily: string; - fontSize: number; -} - -const FALLBACK_COLORS = { - background: "#0c0d0b", - foreground: "#fafafa", - cursor: "#fafafa", - cursorAccent: "#0c0d0b", -}; -const FALLBACK_FONT = '"SF Mono", Consolas, "Liberation Mono", monospace'; - -type WebSerialPort = { - open(options: { - baudRate: number; - dataBits?: number; - stopBits?: number; - parity?: string; - }): Promise; - close(): Promise; - readable: ReadableStream | null; - writable: WritableStream | null; -}; - -interface SerialProps { - config: SerialConfig; - isVisible: boolean; - instanceId: string; -} - -export const Serial = forwardRef(function Serial( - { config, isVisible, instanceId }, - ref, -) { - const { t } = useTranslation(); - const { theme: appTheme } = useTheme(); - const { instance: terminal, ref: xtermRef } = useXTerm(); - const fitAddonRef = useRef(null); - const wsRef = useRef(null); - const connectedRef = useRef(false); - const webSerialReaderRef = useRef(null); - const webSerialWriterRef = useRef(null); - const webSerialPortRef = useRef(null); - const containerRef = useRef(null); - - const useWebSerial = !isElectron(); - - const write = useCallback( - (text: string) => { - terminal?.write(text); - }, - [terminal], - ); - - useEffect(() => { - if (!terminal) return; - let active = true; - terminal.options.theme = { ...FALLBACK_COLORS }; - terminal.options.fontFamily = FALLBACK_FONT; - terminal.options.fontSize = 14; - // The user's terminal look from the SSH terminal, when it is running. - void invokeAction("terminal.resolveTheme", { appTheme }) - .then((look) => { - const resolved = look as SerialLook | undefined; - if (!active || !resolved) return; - terminal.options.theme = { ...resolved.colors }; - terminal.options.fontFamily = resolved.fontFamily; - terminal.options.fontSize = resolved.fontSize; - fitAddonRef.current?.fit(); - }) - .catch(() => {}); - return () => { - active = false; - }; - }, [terminal, appTheme]); - - // ── WebSocket (Electron) path ────────────────────────────────────────── - - const disconnectWs = useCallback(() => { - if (wsRef.current) { - wsRef.current.onclose = null; - wsRef.current.close(); - wsRef.current = null; - } - connectedRef.current = false; - }, []); - - const connectWs = useCallback(async () => { - disconnectWs(); - const target = await resolveSerialWsUrl(); - if (!target) { - write(`\r\n\x1b[31m${t("serial.errorNoServerUrl")}\x1b[0m\r\n`); - return; - } - - const ws = new WebSocket(target.url, target.protocols); - wsRef.current = ws; - - ws.onopen = () => { - ws.send(JSON.stringify({ type: "connect", data: config })); - }; - - ws.onmessage = (ev) => { - try { - const msg = JSON.parse(ev.data as string) as { - type: string; - data?: unknown; - }; - switch (msg.type) { - case "connected": - connectedRef.current = true; - write( - `\x1b[32m${t("serial.connected", { path: config.path, baud: config.baudRate })}\x1b[0m\r\n`, - ); - break; - case "data": - if (typeof msg.data === "string") write(msg.data); - break; - case "disconnected": - connectedRef.current = false; - write(`\r\n\x1b[33m${t("serial.disconnected")}\x1b[0m\r\n`); - break; - case "error": - write( - `\r\n\x1b[31m${t("serial.connectionError")}: ${msg.data as string}\x1b[0m\r\n`, - ); - break; - } - } catch { - // ignore malformed messages - } - }; - - ws.onclose = () => { - connectedRef.current = false; - }; - - ws.onerror = () => { - write(`\r\n\x1b[31m${t("serial.wsError")}\x1b[0m\r\n`); - }; - }, [config, disconnectWs, t, write]); - - // ── Web Serial API path ──────────────────────────────────────────────── - - const disconnectWebSerial = useCallback(async () => { - try { - webSerialReaderRef.current?.cancel(); - webSerialWriterRef.current?.releaseLock(); - await webSerialPortRef.current?.close(); - } catch { - // best-effort - } - webSerialReaderRef.current = null; - webSerialWriterRef.current = null; - webSerialPortRef.current = null; - connectedRef.current = false; - }, []); - - const connectWebSerial = useCallback(async () => { - await disconnectWebSerial(); - - if (!("serial" in navigator)) { - write(`\r\n\x1b[31m${t("serial.notSupported")}\x1b[0m\r\n`); - return; - } - - try { - const serial = navigator.serial as { - requestPort(): Promise; - }; - const port = await serial.requestPort(); - await port.open({ - baudRate: config.baudRate, - dataBits: config.dataBits, - stopBits: config.stopBits, - parity: config.parity === "none" ? "none" : config.parity, - }); - - webSerialPortRef.current = port; - connectedRef.current = true; - write( - `\x1b[32m${t("serial.connected", { path: config.path || "serial", baud: config.baudRate })}\x1b[0m\r\n`, - ); - - const reader = port.readable!.getReader(); - webSerialReaderRef.current = reader; - const decoder = new TextDecoder(); - - (async () => { - try { - for (;;) { - const { value, done } = await reader.read(); - if (done) break; - write(decoder.decode(value)); - } - } catch { - // port closed - } finally { - reader.releaseLock(); - connectedRef.current = false; - write(`\r\n\x1b[33m${t("serial.disconnected")}\x1b[0m\r\n`); - } - })(); - - const writer = port.writable!.getWriter(); - webSerialWriterRef.current = writer; - } catch (err) { - if (err instanceof Error && err.name !== "NotFoundError") { - write( - `\r\n\x1b[31m${t("serial.connectionError")}: ${err.message}\x1b[0m\r\n`, - ); - } - } - }, [config, disconnectWebSerial, t, write]); - - // ── Unified connect/disconnect ───────────────────────────────────────── - - const connect = useCallback(() => { - if (useWebSerial) { - connectWebSerial(); - } else { - void connectWs(); - } - }, [useWebSerial, connectWebSerial, connectWs]); - - const disconnect = useCallback(() => { - if (useWebSerial) { - disconnectWebSerial(); - } else { - if (wsRef.current?.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ type: "disconnect" })); - } - disconnectWs(); - } - }, [useWebSerial, disconnectWebSerial, disconnectWs]); - - const reconnect = useCallback(() => { - disconnect(); - connect(); - }, [disconnect, connect]); - - useImperativeHandle(ref, () => ({ - connect, - disconnect, - reconnect, - isConnected: () => connectedRef.current, - sendInput: (data: string) => { - if (useWebSerial) { - const encoder = new TextEncoder(); - webSerialWriterRef.current?.write(encoder.encode(data)).catch(() => {}); - } else if (wsRef.current?.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ type: "input", data })); - } - }, - })); - - // ── Terminal setup ───────────────────────────────────────────────────── - - useEffect(() => { - if (!terminal) return; - - const fitAddon = new FitAddon(); - fitAddonRef.current = fitAddon; - terminal.loadAddon(fitAddon); - terminal.options.cursorBlink = true; - terminal.options.scrollback = 10000; - - terminal.onData((data) => { - if (!connectedRef.current) return; - if (useWebSerial) { - const encoder = new TextEncoder(); - webSerialWriterRef.current?.write(encoder.encode(data)).catch(() => {}); - } else if (wsRef.current?.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ type: "input", data })); - } - }); - - connect(); - - return () => { - disconnect(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [terminal, instanceId]); - - // ── Fit on visibility change ─────────────────────────────────────────── - - useEffect(() => { - if (!isVisible || !fitAddonRef.current) return; - try { - fitAddonRef.current.fit(); - } catch { - // ignore - } - }, [isVisible]); - - // ── ResizeObserver ───────────────────────────────────────────────────── - - useEffect(() => { - if (!containerRef.current || !terminal) return; - const observer = new ResizeObserver(() => { - try { - fitAddonRef.current?.fit(); - } catch { - // ignore - } - }); - observer.observe(containerRef.current); - return () => observer.disconnect(); - }, [terminal]); - - // ── Not supported (Firefox etc.) ────────────────────────────────────── - - if (useWebSerial && !("serial" in navigator)) { - return ( -
-
- -
-
- - {t("serial.notSupportedTitle")} - - - {t("serial.notSupported")} - -
-
- ); - } - - return ( -
-
-
- ); -}); diff --git a/plugins/serial/src/frontend/SerialPanel.tsx b/plugins/serial/src/frontend/SerialPanel.tsx deleted file mode 100644 index 41317b1c3..000000000 --- a/plugins/serial/src/frontend/SerialPanel.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { RefreshCw, Usb, TriangleAlert } from "lucide-react"; -import { Input, Select2 } from "@termix/plugin-sdk/ui"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { SerialConfig } from "./types.js"; -import { isElectron } from "./electron.js"; -import { resolveSerialWsUrl } from "./transport.js"; - -const BAUD_RATES = [ - 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, - 921600, -]; - -interface SerialPanelProps { - onConnect: (config: SerialConfig) => void; -} - -// In Chrome/Edge the Web Serial API shows its own native port picker on requestPort(), -// so getPorts() only returns previously-granted ports. We never need the user to type -// a path — the browser handles selection at connect time. -const useWebSerial = !isElectron() && "serial" in navigator; -const serialUnsupported = !isElectron() && !("serial" in navigator); - -export function SerialPanel({ onConnect }: SerialPanelProps) { - const { t } = useTranslation(); - const [path, setPath] = useState(""); - const [baudRate, setBaudRate] = useState(115200); - const [dataBits, setDataBits] = useState<5 | 6 | 7 | 8>(8); - const [stopBits, setStopBits] = useState<1 | 2>(1); - const [parity, setParity] = useState<"none" | "even" | "odd">("none"); - // Electron only: ports discovered from the backend - const [availablePorts, setAvailablePorts] = useState([]); - const [loadingPorts, setLoadingPorts] = useState(false); - - const refreshPorts = useCallback(async () => { - if (!isElectron()) return; - setLoadingPorts(true); - const target = await resolveSerialWsUrl(); - if (!target) { - setLoadingPorts(false); - return; - } - - const ws = new WebSocket(target.url, target.protocols); - ws.onopen = () => ws.send(JSON.stringify({ type: "list_ports" })); - ws.onmessage = (ev) => { - try { - const msg = JSON.parse(ev.data as string) as { - type: string; - data?: { path: string }[]; - }; - if (msg.type === "ports_list" && Array.isArray(msg.data)) { - const paths = msg.data.map((p) => p.path); - setAvailablePorts(paths); - setPath((current) => current || (paths[0] ?? current)); - } - } catch { - // ignore - } finally { - ws.close(); - setLoadingPorts(false); - } - }; - ws.onerror = () => setLoadingPorts(false); - }, []); - - useEffect(() => { - if (isElectron()) void refreshPorts(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const connect = () => { - // For Web Serial, path is irrelevant — the browser picker handles it. - // We pass an empty string; Serial.tsx calls requestPort() itself. - if (useWebSerial) { - onConnect({ path: "", baudRate, dataBits, stopBits, parity }); - return; - } - const finalPath = path.trim(); - if (!finalPath) return; - onConnect({ path: finalPath, baudRate, dataBits, stopBits, parity }); - }; - - // ── Not supported (Firefox, Safari, etc.) ───────────────────────────── - - if (serialUnsupported) { - return ( -
-
-
-
- - - {t("serial.notSupportedTitle")} - -
-

- {t("serial.notSupported")} -

-

- {t("serial.hideHint")} -

-
-
-
- ); - } - - // ── Web Serial (Chrome / Edge) ───────────────────────────────────────── - - if (useWebSerial) { - return ( -
-
-

- {t("serial.browserPickerHint")} -

- -
- - setBaudRate(Number(e.target.value))} - className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - {BAUD_RATES.map((r) => ( - - ))} - -
- -
-
- - - setDataBits(Number(e.target.value) as 5 | 6 | 7 | 8) - } - className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - {([5, 6, 7, 8] as const).map((b) => ( - - ))} - -
- -
- - setStopBits(Number(e.target.value) as 1 | 2)} - className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - - - -
- -
- - - setParity(e.target.value as "none" | "even" | "odd") - } - className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - - - - -
-
- - -
-
- ); - } - - // ── Electron (backend serialport) ───────────────────────────────────── - - return ( -
-
-
-
- - -
- {availablePorts.length > 0 && ( - setPath(e.target.value)} - className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - - {availablePorts.map((p) => ( - - ))} - - )} - setPath(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") connect(); - }} - className="h-7 text-xs font-mono" - /> -
- -
- - setBaudRate(Number(e.target.value))} - className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - {BAUD_RATES.map((r) => ( - - ))} - -
- -
-
- - - setDataBits(Number(e.target.value) as 5 | 6 | 7 | 8) - } - className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - {([5, 6, 7, 8] as const).map((b) => ( - - ))} - -
- -
- - setStopBits(Number(e.target.value) as 1 | 2)} - className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - - - -
- -
- - - setParity(e.target.value as "none" | "even" | "odd") - } - className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" - > - - - - -
-
- - -
-
- ); -} diff --git a/plugins/serial/src/frontend/electron.ts b/plugins/serial/src/frontend/electron.ts deleted file mode 100644 index 7cfbd6d90..000000000 --- a/plugins/serial/src/frontend/electron.ts +++ /dev/null @@ -1,16 +0,0 @@ -type ElectronWindow = Window & - typeof globalThis & { - IS_ELECTRON?: boolean; - electronAPI?: { isElectron?: boolean }; - }; - -/** Serial is always local: the device is physically attached to this desktop machine. */ -export function isElectron(): boolean { - if (typeof window === "undefined") return false; - const win = window as ElectronWindow; - return ( - win.IS_ELECTRON === true || - !!win.electronAPI || - win.electronAPI?.isElectron === true - ); -} diff --git a/plugins/serial/src/frontend/index.tsx b/plugins/serial/src/frontend/index.tsx deleted file mode 100644 index fa4daf74c..000000000 --- a/plugins/serial/src/frontend/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -// The terminal surfaces need xterm's own stylesheet. -import "@xterm/xterm/css/xterm.css"; -import type { Ref } from "react"; -import { Usb } from "lucide-react"; -import type { - PanelProps, - PluginHostRecord, - TabProps, - TermixApp, -} from "@termix/plugin-sdk/frontend"; -import { Serial } from "./Serial.js"; -import { SerialPanel } from "./SerialPanel.js"; -import { setSerialWsUrl } from "./transport.js"; -import type { SerialConfig, SerialHandle } from "./types.js"; - -/** - * A per-connection synthetic host, never a real DB host. The shell only - * opens a genuinely hostless (host: null) tab as a singleton, and a serial - * console is not one: a user can have several open at once, each with its - * own device and settings. A distinct id per connection is what keeps them - * separate tabs. - */ -function buildPseudoHost(config: SerialConfig): PluginHostRecord { - return { - id: `serial-${Date.now()}`, - name: config.path - ? `${config.path} (${config.baudRate})` - : `Serial (${config.baudRate})`, - ip: "", - port: 0, - }; -} - -function SerialPanelView({ shell }: PanelProps) { - return ( - { - shell.openTab(buildPseudoHost(config), "serial", { - forceNewTab: true, - data: { serialConfig: config }, - }); - }} - /> - ); -} - -function SerialTabView({ tab, isVisible, handleRef }: TabProps) { - const config = tab.data?.serialConfig as SerialConfig | undefined; - if (!config) return null; - return ( - } - config={config} - isVisible={isVisible} - instanceId={tab.id} - /> - ); -} - -export function activate(app: TermixApp): void { - setSerialWsUrl(app.wsUrl); - app.onDispose(() => setSerialWsUrl(null)); - - app.registerRailItem({ - id: "serial", - icon: Usb, - titleKey: "nav.serial", - after: "quick-connect", - separatorAfter: true, - }); - - app.registerPanel("serial", SerialPanelView); - - app.registerTab("serial", SerialTabView, { - icon: Usb, - titleKey: "nav.serial", - session: true, - // Reconnecting to a COM port automatically after a restart is not a - // workflow anyone asked for, and there's no host to restore against - // anyway, so a serial tab is a local, ephemeral session like the - // desktop app's local terminal rather than something workspaces saves. - inLayouts: false, - }); -} diff --git a/plugins/serial/src/frontend/transport.ts b/plugins/serial/src/frontend/transport.ts deleted file mode 100644 index 79fd3ce25..000000000 --- a/plugins/serial/src/frontend/transport.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { TermixApp } from "@termix/plugin-sdk/frontend"; - -/** - * The live app.wsUrl, for components that are defined at module scope rather - * than inside activate(). Set on activate, cleared on dispose, the same - * pattern the tunnels plugin uses for app.api. - */ -let wsUrl: TermixApp["wsUrl"] | null = null; - -export function setSerialWsUrl(next: TermixApp["wsUrl"] | null): void { - wsUrl = next; -} - -/** - * The console's WebSocket URL and auth subprotocols. No origin option: a - * serial device is physically attached to this desktop machine, so it - * always dials the embedded local backend rather than a connected remote - * sync server (app.wsUrl's default when no origin is given). - */ -export async function resolveSerialWsUrl() { - if (!wsUrl) return null; - return wsUrl("/console"); -} diff --git a/plugins/serial/src/frontend/types.ts b/plugins/serial/src/frontend/types.ts deleted file mode 100644 index 32be6649e..000000000 --- a/plugins/serial/src/frontend/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type SerialConfig = { - path: string; - baudRate: number; - dataBits: 5 | 6 | 7 | 8; - stopBits: 1 | 2; - parity: "none" | "even" | "odd"; -}; - -export interface SerialHandle { - connect: () => void; - disconnect: () => void; - reconnect: () => void; - isConnected: () => boolean; - sendInput: (data: string) => void; -} diff --git a/plugins/serial/tests/backend/activate.test.ts b/plugins/serial/tests/backend/activate.test.ts deleted file mode 100644 index dcb3b36fe..000000000 --- a/plugins/serial/tests/backend/activate.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { - createMockCtx, - type MockPluginContext, -} from "@termix/plugin-sdk/testing"; -import { PluginCapabilityError } from "@termix/plugin-sdk/backend"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import manifestJson from "../../manifest.json"; -import { activate } from "../../src/backend/index.js"; - -const manifest = manifestJson as unknown as PluginManifest; - -describe("serial activate", () => { - it("mounts the console WS route at /console with every declared capability", async () => { - const mock: MockPluginContext = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: manifest.capabilities, - }); - - await activate(mock.ctx); - - expect(mock.wsRoutes).toMatchObject([{ path: "/console", raw: false }]); - }); - - it("refuses to mount without network:serve", async () => { - const mock: MockPluginContext = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: manifest.capabilities.filter((c) => c !== "network:serve"), - }); - - await expect(activate(mock.ctx)).rejects.toBeInstanceOf( - PluginCapabilityError, - ); - }); -}); diff --git a/plugins/serial/tests/backend/session.test.ts b/plugins/serial/tests/backend/session.test.ts deleted file mode 100644 index 2677b0299..000000000 --- a/plugins/serial/tests/backend/session.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { EventEmitter } from "node:events"; -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { - createMockCtx, - type MockPluginContext, -} from "@termix/plugin-sdk/testing"; -import type { PluginWebSocketConnection } from "@termix/plugin-sdk/backend"; -import { createSerialSession } from "../../src/backend/session.js"; - -// vi.mock's factory is hoisted above every import, including node:events, so -// FakeSerialPort gets its own minimal emitter rather than extending it. -const { listMock, openMock, FakeSerialPort } = vi.hoisted(() => { - const listMock = vi.fn(async () => [{ path: "/dev/ttyUSB0" }]); - const openMock = vi.fn(); - - class FakeSerialPort { - isOpen = false; - private listeners = new Map void>>(); - constructor(public options: Record) {} - on(event: string, listener: (...args: unknown[]) => void) { - const set = this.listeners.get(event) ?? new Set(); - set.add(listener); - this.listeners.set(event, set); - return this; - } - private emit(event: string, ...args: unknown[]) { - for (const listener of this.listeners.get(event) ?? []) listener(...args); - } - open(cb: (err?: Error) => void) { - openMock(this.options); - this.isOpen = true; - cb(); - } - write(_data: Buffer, cb: (err?: Error) => void) { - cb(); - } - close() { - this.isOpen = false; - this.emit("close"); - } - static list = listMock; - } - - return { listMock, openMock, FakeSerialPort }; -}); - -vi.mock("serialport", () => ({ SerialPort: FakeSerialPort })); - -class FakeSocket extends EventEmitter { - readyState = 1; - OPEN = 1; - sent: unknown[] = []; - send(msg: string) { - this.sent.push(JSON.parse(msg)); - } - close(_code?: number, _reason?: string) { - this.readyState = 3; - } -} - -function fakeConnection( - socket: FakeSocket, - isDataUnlocked: () => boolean = () => true, -): PluginWebSocketConnection { - return { - userId: "user-1", - request: {}, - socket, - isDataUnlocked, - } as unknown as PluginWebSocketConnection; -} - -async function deliver(socket: FakeSocket, message: object) { - socket.emit("message", Buffer.from(JSON.stringify(message))); - // Let the async message handler's microtasks settle. - await new Promise((resolve) => setTimeout(resolve, 0)); -} - -describe("serial session", () => { - let mock: MockPluginContext; - - beforeEach(() => { - listMock.mockClear(); - openMock.mockClear(); - }); - - it("refuses list_ports and connect without device:serial", async () => { - mock = createMockCtx({ pluginId: "serial", capabilities: [] }); - const socket = new FakeSocket(); - await createSerialSession(mock.ctx)(fakeConnection(socket)); - - await deliver(socket, { type: "list_ports" }); - expect(listMock).not.toHaveBeenCalled(); - expect(socket.sent).toEqual([expect.objectContaining({ type: "error" })]); - - socket.sent.length = 0; - await deliver(socket, { - type: "connect", - data: { path: "/dev/ttyUSB0", baudRate: 9600 }, - }); - expect(openMock).not.toHaveBeenCalled(); - expect(socket.sent).toEqual([expect.objectContaining({ type: "error" })]); - }); - - it("lists ports once granted device:serial", async () => { - mock = createMockCtx({ - pluginId: "serial", - capabilities: ["network:serve", "device:serial"], - }); - const socket = new FakeSocket(); - await createSerialSession(mock.ctx)(fakeConnection(socket)); - - await deliver(socket, { type: "list_ports" }); - - expect(listMock).toHaveBeenCalledTimes(1); - expect(socket.sent).toEqual([ - { type: "ports_list", data: [{ path: "/dev/ttyUSB0" }] }, - ]); - }); - - it("opens, writes to and closes a port through connect/input/disconnect", async () => { - mock = createMockCtx({ - pluginId: "serial", - capabilities: ["network:serve", "device:serial"], - }); - const socket = new FakeSocket(); - await createSerialSession(mock.ctx)(fakeConnection(socket)); - - await deliver(socket, { - type: "connect", - data: { path: "/dev/ttyUSB0", baudRate: 115200 }, - }); - expect(openMock).toHaveBeenCalledWith( - expect.objectContaining({ path: "/dev/ttyUSB0", baudRate: 115200 }), - ); - expect(socket.sent).toContainEqual({ type: "connected" }); - - socket.sent.length = 0; - await deliver(socket, { type: "input", data: "AT\r\n" }); - // write() succeeds silently; nothing is echoed back until the port emits data. - - socket.sent.length = 0; - await deliver(socket, { type: "disconnect" }); - expect(socket.sent).toContainEqual({ type: "disconnected" }); - }); - - it("rejects a connect with no path or baud rate without touching the port", async () => { - mock = createMockCtx({ - pluginId: "serial", - capabilities: ["network:serve", "device:serial"], - }); - const socket = new FakeSocket(); - await createSerialSession(mock.ctx)(fakeConnection(socket)); - - await deliver(socket, { type: "connect", data: { path: "" } }); - - expect(openMock).not.toHaveBeenCalled(); - expect(socket.sent).toEqual([expect.objectContaining({ type: "error" })]); - }); - - it("closes the socket at connect time when data is locked", async () => { - mock = createMockCtx({ - pluginId: "serial", - capabilities: ["network:serve", "device:serial"], - }); - const socket = new FakeSocket(); - await createSerialSession(mock.ctx)(fakeConnection(socket, () => false)); - - expect(socket.sent).toEqual([expect.objectContaining({ type: "error" })]); - }); - - it("refuses a message once data locks mid-session", async () => { - mock = createMockCtx({ - pluginId: "serial", - capabilities: ["network:serve", "device:serial"], - }); - const socket = new FakeSocket(); - let unlocked = true; - await createSerialSession(mock.ctx)(fakeConnection(socket, () => unlocked)); - - unlocked = false; - await deliver(socket, { type: "list_ports" }); - - expect(listMock).not.toHaveBeenCalled(); - expect(socket.sent).toEqual([expect.objectContaining({ type: "error" })]); - }); -}); diff --git a/plugins/serial/tests/frontend/activate.test.tsx b/plugins/serial/tests/frontend/activate.test.tsx deleted file mode 100644 index c395d306b..000000000 --- a/plugins/serial/tests/frontend/activate.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { screen } from "@testing-library/react"; -import { - renderWithApp, - type RenderedPluginApp, -} from "@termix/plugin-sdk/testing"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import * as plugin from "../../src/frontend/index"; -import manifestJson from "../../manifest.json"; -import locales from "../../locales/en.json"; - -const manifest = manifestJson as unknown as PluginManifest; - -const declared = (views?: Array<{ id: string }>) => - (views ?? []).map((view) => view.id).sort(); - -let rendered: RenderedPluginApp | null = null; - -afterEach(async () => { - await rendered?.deactivate(); - rendered = null; -}); - -describe(`${manifest.id} activate`, () => { - it("activates and registers only views its manifest declares", async () => { - rendered = await renderWithApp(plugin, { manifest, locales }); - const contributes = manifest.contributes as unknown as Record< - string, - Array<{ id: string }> | undefined - >; - const tabs = declared(contributes.tabs); - const panels = [...declared(contributes.panels), ...tabs]; - - expect(rendered.registered.tabs()).toEqual(["serial"]); - for (const id of rendered.registered.tabs()) expect(tabs).toContain(id); - for (const id of rendered.registered.panels()) expect(panels).toContain(id); - expect(rendered.registered.railItems().map((item) => item.id)).toContain( - "serial", - ); - }); - - it("removes everything it registered on deactivate", async () => { - const app = await renderWithApp(plugin, { manifest, locales }); - await app.deactivate(); - expect(app.registered.tabs()).toEqual([]); - expect(app.registered.panels()).toEqual([]); - expect(app.registered.railItems()).toEqual([]); - }); - - it("renders the connect form in the rail panel", async () => { - rendered = await renderWithApp(plugin, { manifest, locales }); - rendered.renderPanel("serial", { active: true }); - // jsdom has no Web Serial API and isn't Electron, so the panel falls - // back to its "not supported" message rather than a live form. - expect( - await screen.findByText(locales.serial.notSupportedTitle), - ).toBeTruthy(); - }); -}); diff --git a/plugins/serial/tsconfig.json b/plugins/serial/tsconfig.json deleted file mode 100644 index 7b35314c3..000000000 --- a/plugins/serial/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@termix/plugin-sdk/tsconfig.plugin.json", - "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] -} diff --git a/plugins/serial/vitest.config.ts b/plugins/serial/vitest.config.ts deleted file mode 100644 index 31ba004fc..000000000 --- a/plugins/serial/vitest.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; - -export default pluginVitestConfig(import.meta.url); diff --git a/plugins/session-recording/CHANGELOG.md b/plugins/session-recording/CHANGELOG.md deleted file mode 100644 index a4e735e92..000000000 --- a/plugins/session-recording/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -## 1.0.0 - -First release, bundled with Termix 2.9.0. - -Records SSH terminal sessions so you can play them back, copy their text or download them. Old recordings are removed after a set number of days. diff --git a/plugins/session-recording/README.md b/plugins/session-recording/README.md deleted file mode 100644 index cd4e575c2..000000000 --- a/plugins/session-recording/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Session Recording - -Record terminal sessions and play them back or download them later. - -## Features - -- Records SSH terminal sessions as they happen. -- Play a recording back, skip through it and copy its text. -- Download a recording as a file or as plain text. -- Removes recordings older than a set number of days. -- Keeps recordings when a user is deleted, with the user's name removed. - -## Settings - -### Admin - -- Retention (days): remove recordings older than this. Checked at startup and once a day. - -### Host - -- Enable session recording: record sessions on this host. - -## Permissions - -- `session-recording.view`: View and download session recordings. Admins and users have it by default. - -## Services - -Provides to other plugins: - -- `recordings.writer`: start a recording and add to it as a session runs, or save a recording that is already finished. - -## Development - -```bash -npm run build # build into dist/ -npm run test # run this plugin's tests -npm run typecheck # type-check this plugin -``` diff --git a/plugins/session-recording/locales/en.json b/plugins/session-recording/locales/en.json deleted file mode 100644 index ccd600232..000000000 --- a/plugins/session-recording/locales/en.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Session Logs" - }, - "hosts": { - "enableSessionRecording": "Session Recording", - "enableSessionRecordingDesc": "Record terminal session output for later review" - }, - "permissions": { - "view": { - "title": "View session recordings", - "description": "See and play back your own session recordings." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Retention (days)", - "description": "How long a session recording is kept before it is deleted automatically." - } - }, - "host": { - "enable": { - "label": "Enable session recording", - "description": "Record terminal output for this host so you can play it back or download it later." - } - } - }, - "terminalStatus": { - "recording": "Recording" - }, - "sessionLogs": { - "title": "Session Logs", - "noLogs": "No session logs yet", - "noLogsDesc": "Enable session logging on a host to start recording", - "duration": "Duration", - "viewLog": "View log", - "downloadLog": "Download", - "downloadAsText": "Download as text", - "deleteLog": "Delete", - "confirmDelete": "Delete this session log?", - "confirmDeleteDesc": "This action cannot be undone.", - "copyContent": "Copy", - "copied": "Copied!", - "loadError": "Failed to load session logs", - "deleteError": "Failed to delete session log", - "filterByHost": "Filter by host...", - "back": "Back", - "empty": "(empty)", - "noResults": "No results for \"{{query}}\"", - "hostFallback": "Host {{id}}", - "sessionFallback": "Session {{id}}" - }, - "player": { - "timeline": "Recording timeline", - "speed": "Playback speed", - "play": "Play recording", - "pause": "Pause recording" - }, - "plugin": { - "name": "Session Recording" - } -} diff --git a/plugins/session-recording/locales/translated/af_ZA.json b/plugins/session-recording/locales/translated/af_ZA.json deleted file mode 100644 index 753c4fdd2..000000000 --- a/plugins/session-recording/locales/translated/af_ZA.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Sessielogboeke" - }, - "hosts": { - "enableSessionRecording": "Sessie-opname", - "enableSessionRecordingDesc": "Neem terminaalsessie-uitvoer op vir latere hersiening" - }, - "permissions": { - "view": { - "title": "Bekyk sessie-opnames", - "description": "Sien en speel jou eie sessie-opnames terug." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Behoud (dae)", - "description": "Hoe lank 'n sessie-opname gehou word voordat dit outomaties uitgevee word." - } - }, - "host": { - "enable": { - "label": "Aktiveer sessie-opname", - "description": "Neem terminaaluitvoer vir hierdie gasheer op sodat jy dit later kan terugspeel of aflaai." - } - } - }, - "terminalStatus": { - "recording": "Opname" - }, - "sessionLogs": { - "title": "Sessielogboeke", - "noLogs": "Geen sessielogboeke nog nie", - "noLogsDesc": "Aktiveer sessie-aanmelding op 'n gasheer om opname te begin", - "duration": "Duur", - "viewLog": "Bekyk logboek", - "downloadLog": "Laai af", - "downloadAsText": "Laai af as teks", - "deleteLog": "Vee uit", - "confirmDelete": "Vee hierdie sessielog uit?", - "confirmDeleteDesc": "Hierdie aksie kan nie ongedaan gemaak word nie.", - "copyContent": "Kopieer", - "copied": "Gekopieer!", - "loadError": "Kon nie sessielogboeke laai nie", - "deleteError": "Kon nie sessielogboek uitvee nie", - "filterByHost": "Filtreer volgens gasheer...", - "back": "Terug", - "empty": "(leeg)", - "noResults": "Geen resultate vir \"{{query}}\"", - "hostFallback": "Gasheer {{id}}", - "sessionFallback": "Sessie {{id}}" - }, - "player": { - "timeline": "Opnametydlyn", - "speed": "Terugspeelspoed", - "play": "Speel opname", - "pause": "Onderbreek opname" - }, - "plugin": { - "name": "Sessie-opname" - } -} diff --git a/plugins/session-recording/locales/translated/ar_SA.json b/plugins/session-recording/locales/translated/ar_SA.json deleted file mode 100644 index fd1c1ffa6..000000000 --- a/plugins/session-recording/locales/translated/ar_SA.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "سجلات الجلسات" - }, - "hosts": { - "enableSessionRecording": "تسجيل الجلسة", - "enableSessionRecordingDesc": "تسجيل مخرجات الجلسة الطرفية للمراجعة لاحقاً" - }, - "permissions": { - "view": { - "title": "عرض تسجيلات الجلسات", - "description": "شاهد واستمع إلى تسجيلات جلساتك الخاصة." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "مدة الاحتفاظ (بالأيام)", - "description": "ما هي المدة التي يتم فيها الاحتفاظ بتسجيل الجلسة قبل حذفه تلقائيًا؟" - } - }, - "host": { - "enable": { - "label": "تفعيل تسجيل الجلسة", - "description": "سجل مخرجات الطرفية لهذا المضيف حتى تتمكن من تشغيلها أو تنزيلها لاحقًا." - } - } - }, - "terminalStatus": { - "recording": "تسجيل" - }, - "sessionLogs": { - "title": "سجلات الجلسات", - "noLogs": "لا توجد سجلات جلسات بعد", - "noLogsDesc": "فعّل تسجيل الجلسات على مضيف لبدء التسجيل", - "duration": "المدة", - "viewLog": "عرض السجل", - "downloadLog": "تنزيل", - "downloadAsText": "تنزيل كنص", - "deleteLog": "حذف", - "confirmDelete": "حذف سجل الجلسة هذا؟", - "confirmDeleteDesc": "لا يمكن التراجع عن هذا الإجراء.", - "copyContent": "نسخ", - "copied": "تم النسخ!", - "loadError": "فشل في تحميل سجلات الجلسات", - "deleteError": "فشل في حذف سجل الجلسة", - "filterByHost": "تصفية حسب المضيف...", - "back": "رجوع", - "empty": "(فارغ)", - "noResults": "لا توجد نتائج للبحث عن \"{{query}}\"", - "hostFallback": "المضيف {{id}}", - "sessionFallback": "الجلسة {{id}}" - }, - "player": { - "timeline": "الجدول الزمني للتسجيل", - "speed": "سرعة التشغيل", - "play": "تشغيل التسجيل", - "pause": "إيقاف التسجيل مؤقتًا" - }, - "plugin": { - "name": "تسجيل الجلسة" - } -} diff --git a/plugins/session-recording/locales/translated/bg_BG.json b/plugins/session-recording/locales/translated/bg_BG.json deleted file mode 100644 index 377fa22f1..000000000 --- a/plugins/session-recording/locales/translated/bg_BG.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Дневници на сесии" - }, - "hosts": { - "enableSessionRecording": "Запис на сесия", - "enableSessionRecordingDesc": "Записване на изхода от терминалната сесия за по-късен преглед" - }, - "permissions": { - "view": { - "title": "Преглед на записите от сесиите", - "description": "Вижте и възпроизведете собствените си записи от сесиите." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Задържане (дни)", - "description": "Колко дълго се съхранява записът на сесия, преди да бъде изтрит автоматично." - } - }, - "host": { - "enable": { - "label": "Активиране на запис на сесия", - "description": "Запишете изхода от терминала за този хост, за да можете да го възпроизведете или изтеглите по-късно." - } - } - }, - "terminalStatus": { - "recording": "Запис" - }, - "sessionLogs": { - "title": "Дневници на сесии", - "noLogs": "Все още няма логове на сесии", - "noLogsDesc": "Активирайте регистрирането на сесията на хост, за да започнете записа", - "duration": "Продължителност", - "viewLog": "Преглед на дневника", - "downloadLog": "Изтегляне", - "downloadAsText": "Изтегляне като текст", - "deleteLog": "Изтриване", - "confirmDelete": "Да се изтрие ли този дневник на сесията?", - "confirmDeleteDesc": "Това действие не може да бъде отменено.", - "copyContent": "Копиране", - "copied": "Копирано!", - "loadError": "Зареждането на лог файловете на сесията не бе успешно", - "deleteError": "Неуспешно изтриване на дневника на сесията", - "filterByHost": "Филтриране по хост...", - "back": "Обратно", - "empty": "(празно)", - "noResults": "Няма резултати за „{{query}}“", - "hostFallback": "Домакин {{id}}", - "sessionFallback": "Сесия {{id}}" - }, - "player": { - "timeline": "Хронология на записа", - "speed": "Скорост на възпроизвеждане", - "play": "Пускане на запис", - "pause": "Пауза на записа" - }, - "plugin": { - "name": "Запис на сесия" - } -} diff --git a/plugins/session-recording/locales/translated/bn_BD.json b/plugins/session-recording/locales/translated/bn_BD.json deleted file mode 100644 index dab93a35d..000000000 --- a/plugins/session-recording/locales/translated/bn_BD.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "সেশন লগ" - }, - "hosts": { - "enableSessionRecording": "সেশন রেকর্ডিং", - "enableSessionRecordingDesc": "পরবর্তী পর্যালোচনার জন্য টার্মিনাল সেশনের আউটপুট রেকর্ড করুন।" - }, - "permissions": { - "view": { - "title": "সেশন রেকর্ডিং দেখুন", - "description": "আপনার নিজের সেশন রেকর্ডিংগুলো দেখুন এবং প্লেব্যাক করুন।" - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "ধারণক্ষমতা (দিন)", - "description": "একটি সেশন রেকর্ডিং স্বয়ংক্রিয়ভাবে মুছে যাওয়ার আগে কতক্ষণ সংরক্ষিত থাকে।" - } - }, - "host": { - "enable": { - "label": "সেশন রেকর্ডিং সক্ষম করুন", - "description": "এই হোস্টের জন্য টার্মিনাল আউটপুট রেকর্ড করুন, যাতে আপনি পরে এটি প্লেব্যাক বা ডাউনলোড করতে পারেন।" - } - } - }, - "terminalStatus": { - "recording": "রেকর্ডিং" - }, - "sessionLogs": { - "title": "সেশন লগ", - "noLogs": "এখনো কোনো সেশন লগ নেই", - "noLogsDesc": "রেকর্ডিং শুরু করতে কোনো হোস্টে সেশন লগিং চালু করুন।", - "duration": "সময়কাল", - "viewLog": "লগ দেখুন", - "downloadLog": "ডাউনলোড করুন", - "downloadAsText": "টেক্সট হিসেবে ডাউনলোড করুন", - "deleteLog": "মুছে ফেলুন", - "confirmDelete": "এই সেশন লগটি মুছে ফেলবেন?", - "confirmDeleteDesc": "এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না।", - "copyContent": "অনুলিপি", - "copied": "অনুলিপি করা হয়েছে!", - "loadError": "সেশন লগ লোড করতে ব্যর্থ হয়েছে", - "deleteError": "সেশন লগ মুছে ফেলতে ব্যর্থ হয়েছে", - "filterByHost": "হোস্ট দ্বারা ফিল্টার করুন...", - "back": "ফিরে যান", - "empty": "(খালি)", - "noResults": "\"{{query}} \" এর জন্য কোন ফলাফল পাওয়া যায়নি", - "hostFallback": "হোস্ট {{id}}", - "sessionFallback": "সেশন {{id}}" - }, - "player": { - "timeline": "রেকর্ডিং টাইমলাইন", - "speed": "প্লেব্যাক গতি", - "play": "রেকর্ডিং চালান", - "pause": "রেকর্ডিং থামান" - }, - "plugin": { - "name": "সেশন রেকর্ডিং" - } -} diff --git a/plugins/session-recording/locales/translated/ca_ES.json b/plugins/session-recording/locales/translated/ca_ES.json deleted file mode 100644 index e0fc30ab5..000000000 --- a/plugins/session-recording/locales/translated/ca_ES.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Registres de sessió" - }, - "hosts": { - "enableSessionRecording": "Gravació de sessions", - "enableSessionRecordingDesc": "Enregistra la sortida de la sessió de terminal per a una revisió posterior" - }, - "permissions": { - "view": { - "title": "Veure enregistraments de sessions", - "description": "Veure i reproduir els enregistraments de les teves pròpies sessions." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Retenció (dies)", - "description": "Quant de temps es conserva una gravació de sessió abans que s'elimini automàticament." - } - }, - "host": { - "enable": { - "label": "Activa l'enregistrament de la sessió", - "description": "Enregistra la sortida del terminal per a aquest host per poder reproduir-la o descarregar-la més tard." - } - } - }, - "terminalStatus": { - "recording": "Enregistrament" - }, - "sessionLogs": { - "title": "Registres de sessió", - "noLogs": "Encara no hi ha registres de sessió", - "noLogsDesc": "Habilita el registre de sessions en un amfitrió per iniciar l'enregistrament", - "duration": "Durada", - "viewLog": "Veure registre", - "downloadLog": "Descarrega", - "downloadAsText": "Descarrega com a text", - "deleteLog": "Suprimeix", - "confirmDelete": "Voleu suprimir aquest registre de sessió?", - "confirmDeleteDesc": "Aquesta acció no es pot desfer.", - "copyContent": "Còpia", - "copied": "Copiat!", - "loadError": "No s'han pogut carregar els registres de sessió", - "deleteError": "No s'ha pogut suprimir el registre de sessió", - "filterByHost": "Filtra per amfitrió...", - "back": "Enrere", - "empty": "(buit)", - "noResults": "No hi ha resultats per a \"{{query}}\"", - "hostFallback": "Amfitrió {{id}}", - "sessionFallback": "Sessió {{id}}" - }, - "player": { - "timeline": "Cronologia de gravació", - "speed": "Velocitat de reproducció", - "play": "Reprodueix la gravació", - "pause": "Pausa l'enregistrament" - }, - "plugin": { - "name": "Gravació de sessions" - } -} diff --git a/plugins/session-recording/locales/translated/cs_CZ.json b/plugins/session-recording/locales/translated/cs_CZ.json deleted file mode 100644 index c367c61b5..000000000 --- a/plugins/session-recording/locales/translated/cs_CZ.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Protokoly relací" - }, - "hosts": { - "enableSessionRecording": "Nahrávání relace", - "enableSessionRecordingDesc": "Zaznamenejte výstup terminálové relace pro pozdější kontrolu" - }, - "permissions": { - "view": { - "title": "Zobrazit záznamy relací", - "description": "Prohlížejte a přehrávejte si vlastní nahrávky z relací." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Retenční doba (dny)", - "description": "Jak dlouho je záznam relace uchováván, než je automaticky smazán." - } - }, - "host": { - "enable": { - "label": "Povolit nahrávání relace", - "description": "Nahrajte výstup terminálu pro tento hostitel, abyste si ho mohli později přehrát nebo stáhnout." - } - } - }, - "terminalStatus": { - "recording": "Záznam" - }, - "sessionLogs": { - "title": "Protokoly relací", - "noLogs": "Zatím žádné protokoly relací", - "noLogsDesc": "Povolte protokolování relace na hostiteli pro zahájení nahrávání", - "duration": "Trvání", - "viewLog": "Zobrazit protokol", - "downloadLog": "Stáhnout", - "downloadAsText": "Stáhnout jako text", - "deleteLog": "Vymazat", - "confirmDelete": "Smazat tento protokol relace?", - "confirmDeleteDesc": "Tuto akci nelze vrátit zpět.", - "copyContent": "Kopie", - "copied": "Zkopírováno!", - "loadError": "Nepodařilo se načíst protokoly relací", - "deleteError": "Nepodařilo se smazat protokol relace", - "filterByHost": "Filtrovat podle hostitele...", - "back": "Zadní", - "empty": "(prázdný)", - "noResults": "Žádné výsledky pro výraz „{{query}}“", - "hostFallback": "Hostitel {{id}}", - "sessionFallback": "Relace {{id}}" - }, - "player": { - "timeline": "Časová osa nahrávání", - "speed": "Rychlost přehrávání", - "play": "Přehrát nahrávku", - "pause": "Pozastavit nahrávání" - }, - "plugin": { - "name": "Nahrávání relace" - } -} diff --git a/plugins/session-recording/locales/translated/da_DK.json b/plugins/session-recording/locales/translated/da_DK.json deleted file mode 100644 index ebb434407..000000000 --- a/plugins/session-recording/locales/translated/da_DK.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Sessionslogfiler" - }, - "hosts": { - "enableSessionRecording": "Sessionsoptagelse", - "enableSessionRecordingDesc": "Optag output fra terminalsessionen til senere gennemgang" - }, - "permissions": { - "view": { - "title": "Se sessionsoptagelser", - "description": "Se og afspil dine egne sessionsoptagelser." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Opbevaring (dage)", - "description": "Hvor længe en sessionsoptagelse gemmes, før den slettes automatisk." - } - }, - "host": { - "enable": { - "label": "Aktivér sessionsoptagelse", - "description": "Optag terminaloutput for denne vært, så du kan afspille det eller downloade det senere." - } - } - }, - "terminalStatus": { - "recording": "Indspilning" - }, - "sessionLogs": { - "title": "Sessionslogfiler", - "noLogs": "Ingen sessionslogfiler endnu", - "noLogsDesc": "Aktivér sessionslogning på en vært for at starte optagelse", - "duration": "Varighed", - "viewLog": "Se log", - "downloadLog": "Download", - "downloadAsText": "Download som tekst", - "deleteLog": "Slet", - "confirmDelete": "Slet denne sessionslog?", - "confirmDeleteDesc": "Denne handling kan ikke fortrydes.", - "copyContent": "Kopi", - "copied": "Kopieret!", - "loadError": "Kunne ikke indlæse sessionslogfiler", - "deleteError": "Kunne ikke slette sessionsloggen", - "filterByHost": "Filtrer efter vært...", - "back": "Tilbage", - "empty": "(tom)", - "noResults": "Ingen resultater for \"{{query}}\"", - "hostFallback": "Vært {{id}}", - "sessionFallback": "Session {{id}}" - }, - "player": { - "timeline": "Optagelsestidslinje", - "speed": "Afspilningshastighed", - "play": "Afspil optagelse", - "pause": "Pause optagelse" - }, - "plugin": { - "name": "Sessionsoptagelse" - } -} diff --git a/plugins/session-recording/locales/translated/de_DE.json b/plugins/session-recording/locales/translated/de_DE.json deleted file mode 100644 index f28ee09e3..000000000 --- a/plugins/session-recording/locales/translated/de_DE.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Sitzungsprotokolle" - }, - "hosts": { - "enableSessionRecording": "Sitzungsaufzeichnung", - "enableSessionRecordingDesc": "Terminal-Sitzungsausgabe zur späteren Überprüfung aufzeichnen" - }, - "permissions": { - "view": { - "title": "Sitzungsaufzeichnungen ansehen", - "description": "Sehen und spielen Sie Ihre eigenen Session-Aufnahmen ab." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Aufbewahrungsdauer (Tage)", - "description": "Wie lange eine Sitzungsaufzeichnung aufbewahrt wird, bevor sie automatisch gelöscht wird." - } - }, - "host": { - "enable": { - "label": "Sitzungsaufzeichnung aktivieren", - "description": "Zeichnen Sie die Terminalausgabe dieses Hosts auf, damit Sie sie später wiedergeben oder herunterladen können." - } - } - }, - "terminalStatus": { - "recording": "Aufnahme" - }, - "sessionLogs": { - "title": "Sitzungsprotokolle", - "noLogs": "Noch keine Sitzungsprotokolle", - "noLogsDesc": "Aktivieren Sie die Sitzungsprotokollierung auf einem Host, um die Aufzeichnung zu starten", - "duration": "Dauer", - "viewLog": "Protokoll anzeigen", - "downloadLog": "Herunterladen", - "downloadAsText": "Als Text herunterladen", - "deleteLog": "Löschen", - "confirmDelete": "Dieses Sitzungsprotokoll löschen?", - "confirmDeleteDesc": "Diese Aktion kann nicht rückgängig gemacht werden.", - "copyContent": "Kopieren", - "copied": "Kopiert!", - "loadError": "Fehler beim Laden der Sitzungsprotokolle", - "deleteError": "Fehler beim Löschen des Sitzungsprotokolls", - "filterByHost": "Nach Host filtern...", - "back": "Zurück", - "empty": "(leer)", - "noResults": "Keine Ergebnisse für \"{{query}}\"", - "hostFallback": "Host {{id}}", - "sessionFallback": "Sitzung {{id}}" - }, - "player": { - "timeline": "Aufnahmezeitplan", - "speed": "Wiedergabegeschwindigkeit", - "play": "Wiedergabe", - "pause": "Aufnahme pausieren" - }, - "plugin": { - "name": "Sitzungsaufzeichnung" - } -} diff --git a/plugins/session-recording/locales/translated/el_GR.json b/plugins/session-recording/locales/translated/el_GR.json deleted file mode 100644 index e64d16aa0..000000000 --- a/plugins/session-recording/locales/translated/el_GR.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Αρχεία καταγραφής συνεδρίας" - }, - "hosts": { - "enableSessionRecording": "Ηχογράφηση συνεδρίας", - "enableSessionRecordingDesc": "Καταγραφή εξόδου συνεδρίας τερματικού για μελλοντική αναθεώρηση" - }, - "permissions": { - "view": { - "title": "Δείτε τις ηχογραφήσεις της συνεδρίας", - "description": "Δείτε και αναπαράγετε τις δικές σας ηχογραφήσεις συνεδρίας." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Διατήρηση (ημέρες)", - "description": "Για πόσο χρονικό διάστημα διατηρείται μια εγγραφή συνεδρίας πριν διαγραφεί αυτόματα." - } - }, - "host": { - "enable": { - "label": "Ενεργοποίηση εγγραφής συνεδρίας", - "description": "Εγγράψτε την έξοδο του τερματικού για αυτόν τον κεντρικό υπολογιστή, ώστε να μπορείτε να την αναπαράγετε ή να την κατεβάσετε αργότερα." - } - } - }, - "terminalStatus": { - "recording": "Εγγραφή" - }, - "sessionLogs": { - "title": "Αρχεία καταγραφής συνεδρίας", - "noLogs": "Δεν υπάρχουν ακόμη αρχεία καταγραφής συνεδρίας", - "noLogsDesc": "Ενεργοποίηση καταγραφής περιόδου λειτουργίας σε έναν κεντρικό υπολογιστή για έναρξη εγγραφής", - "duration": "Διάρκεια", - "viewLog": "Προβολή αρχείου καταγραφής", - "downloadLog": "Λήψη", - "downloadAsText": "Λήψη ως κείμενο", - "deleteLog": "Διαγράφω", - "confirmDelete": "Διαγραφή αυτού του αρχείου καταγραφής περιόδου σύνδεσης;", - "confirmDeleteDesc": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", - "copyContent": "Αντίγραφο", - "copied": "Αντιγράφηκε!", - "loadError": "Αποτυχία φόρτωσης αρχείων καταγραφής περιόδου λειτουργίας", - "deleteError": "Η διαγραφή του αρχείου καταγραφής περιόδου λειτουργίας απέτυχε", - "filterByHost": "Φιλτράρισμα κατά κεντρικό υπολογιστή...", - "back": "Πίσω", - "empty": "(αδειάζω)", - "noResults": "Δεν υπάρχουν αποτελέσματα για \"{{query}}\"", - "hostFallback": "Κεντρικός υπολογιστής {{id}}", - "sessionFallback": "Συνεδρία {{id}}" - }, - "player": { - "timeline": "Χρονολόγιο καταγραφής", - "speed": "Ταχύτητα αναπαραγωγής", - "play": "Αναπαραγωγή ηχογράφησης", - "pause": "Παύση εγγραφής" - }, - "plugin": { - "name": "Ηχογράφηση συνεδρίας" - } -} diff --git a/plugins/session-recording/locales/translated/es_ES.json b/plugins/session-recording/locales/translated/es_ES.json deleted file mode 100644 index e76f061f0..000000000 --- a/plugins/session-recording/locales/translated/es_ES.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Registros de sesión" - }, - "hosts": { - "enableSessionRecording": "Grabación de sesión", - "enableSessionRecordingDesc": "Registrar la salida de la sesión de terminal para su revisión posterior" - }, - "permissions": { - "view": { - "title": "Ver las grabaciones de las sesiones", - "description": "Visualiza y reproduce las grabaciones de tus propias sesiones." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Retención (días)", - "description": "Cuánto tiempo se conserva la grabación de una sesión antes de que se elimine automáticamente." - } - }, - "host": { - "enable": { - "label": "Habilitar la grabación de la sesión", - "description": "Graba la salida de la terminal de este host para que puedas reproducirla o descargarla más tarde." - } - } - }, - "terminalStatus": { - "recording": "Grabación" - }, - "sessionLogs": { - "title": "Registros de sesión", - "noLogs": "Aún no hay registros de sesión", - "noLogsDesc": "Activa el registro de sesiones en un host para empezar a grabar", - "duration": "Duración", - "viewLog": "Ver registro", - "downloadLog": "Descargar", - "downloadAsText": "Descargar como texto", - "deleteLog": "Eliminar", - "confirmDelete": "¿Eliminar este registro de sesión?", - "confirmDeleteDesc": "Esta acción no se puede deshacer.", - "copyContent": "Copiar", - "copied": "¡Copiado!", - "loadError": "Error al cargar los registros de sesión", - "deleteError": "Error al eliminar el registro de sesión", - "filterByHost": "Filtrar por host...", - "back": "Volver", - "empty": "(vacío)", - "noResults": "No se encontraron resultados para \"{{query}}\"", - "hostFallback": "Anfitrión {{id}}", - "sessionFallback": "Sesión {{id}}" - }, - "player": { - "timeline": "Cronología de la grabación", - "speed": "Velocidad de reproducción", - "play": "Reproducir grabación", - "pause": "Pausar la grabación" - }, - "plugin": { - "name": "Grabación de sesión" - } -} diff --git a/plugins/session-recording/locales/translated/fi_FI.json b/plugins/session-recording/locales/translated/fi_FI.json deleted file mode 100644 index f84076651..000000000 --- a/plugins/session-recording/locales/translated/fi_FI.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Istuntolokit" - }, - "hosts": { - "enableSessionRecording": "Istunnon tallennus", - "enableSessionRecordingDesc": "Tallenna pääteistunnon tuloste myöhempää tarkastelua varten" - }, - "permissions": { - "view": { - "title": "Näytä istuntotallenteet", - "description": "Katsele ja toista omia istuntotallenteitasi." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Säilytys (päivää)", - "description": "Kuinka kauan istuntotallennetta säilytetään ennen kuin se poistetaan automaattisesti." - } - }, - "host": { - "enable": { - "label": "Ota istunnon tallennus käyttöön", - "description": "Tallenna tämän isännän päätelaitteen lähtö, jotta voit toistaa sen tai ladata sen myöhemmin." - } - } - }, - "terminalStatus": { - "recording": "Äänitys" - }, - "sessionLogs": { - "title": "Istuntolokit", - "noLogs": "Ei vielä istuntolokeja", - "noLogsDesc": "Ota istunnon lokikirjaus käyttöön isännässä aloittaaksesi tallennuksen", - "duration": "Kesto", - "viewLog": "Näytä loki", - "downloadLog": "Lataa", - "downloadAsText": "Lataa tekstinä", - "deleteLog": "Poistaa", - "confirmDelete": "Poistetaanko tämä istuntoloki?", - "confirmDeleteDesc": "Tätä toimintoa ei voi perua.", - "copyContent": "Kopioida", - "copied": "Kopioitu!", - "loadError": "Istuntolokien lataaminen epäonnistui", - "deleteError": "Istuntolokitietojen poistaminen epäonnistui", - "filterByHost": "Suodata isännän mukaan...", - "back": "Takaisin", - "empty": "(tyhjä)", - "noResults": "Ei tuloksia haulle \"{{query}}\"", - "hostFallback": "Isäntä {{id}}", - "sessionFallback": "Istunto {{id}}" - }, - "player": { - "timeline": "Tallennusaikajana", - "speed": "Toistonopeus", - "play": "Toista tallenne", - "pause": "Keskeytä tallennus" - }, - "plugin": { - "name": "Istunnon tallennus" - } -} diff --git a/plugins/session-recording/locales/translated/fr_FR.json b/plugins/session-recording/locales/translated/fr_FR.json deleted file mode 100644 index 21c16ff6a..000000000 --- a/plugins/session-recording/locales/translated/fr_FR.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Journaux de session" - }, - "hosts": { - "enableSessionRecording": "Enregistrement de session", - "enableSessionRecordingDesc": "Enregistrer la sortie de la session terminal pour une consultation ultérieure" - }, - "permissions": { - "view": { - "title": "Consulter les enregistrements des sessions", - "description": "Consultez et relisez vos propres enregistrements de session." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Rétention (jours)", - "description": "Durée de conservation d'un enregistrement de session avant sa suppression automatique." - } - }, - "host": { - "enable": { - "label": "Activer l'enregistrement de session", - "description": "Enregistrez la sortie du terminal pour cet hôte afin de pouvoir la relire ou la télécharger ultérieurement." - } - } - }, - "terminalStatus": { - "recording": "Enregistrement" - }, - "sessionLogs": { - "title": "Journaux de session", - "noLogs": "Aucun journal de session pour l’instant", - "noLogsDesc": "Activez la journalisation des sessions sur un hôte pour démarrer l’enregistrement", - "duration": "Durée", - "viewLog": "Afficher le journal", - "downloadLog": "Télécharger", - "downloadAsText": "Télécharger au format texte", - "deleteLog": "Supprimer", - "confirmDelete": "Supprimer ce journal de session ?", - "confirmDeleteDesc": "Cette action est irréversible.", - "copyContent": "Copier", - "copied": "Copié !", - "loadError": "Échec du chargement des journaux de session", - "deleteError": "Échec de la suppression du journal de session", - "filterByHost": "Filtrer par hôte...", - "back": "Retour", - "empty": "(vide)", - "noResults": "Aucun résultat pour «{{query}}»", - "hostFallback": "Hôte {{id}}", - "sessionFallback": "Session {{id}}" - }, - "player": { - "timeline": "Chronologie de l'enregistrement", - "speed": "Vitesse de lecture", - "play": "Lire l'enregistrement", - "pause": "Mettre en pause l'enregistrement" - }, - "plugin": { - "name": "Enregistrement de session" - } -} diff --git a/plugins/session-recording/locales/translated/he_IL.json b/plugins/session-recording/locales/translated/he_IL.json deleted file mode 100644 index 0a112ec1b..000000000 --- a/plugins/session-recording/locales/translated/he_IL.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "יומני סשן" - }, - "hosts": { - "enableSessionRecording": "הקלטת סשן", - "enableSessionRecordingDesc": "הקלט את פלט הפעלת הטרמינל לצורך סקירה מאוחרת יותר" - }, - "permissions": { - "view": { - "title": "צפה בהקלטות של הפגישה", - "description": "צפה והפעל את הקלטות ההקלטה שלך." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "שמירה (ימים)", - "description": "כמה זמן נשמרת הקלטת סשן לפני שהיא נמחקת אוטומטית." - } - }, - "host": { - "enable": { - "label": "הפעל הקלטת סשן", - "description": "הקלט את פלט הטרמינל עבור מארח זה כדי שתוכל להשמיע אותו או להוריד אותו מאוחר יותר." - } - } - }, - "terminalStatus": { - "recording": "הַקלָטָה" - }, - "sessionLogs": { - "title": "יומני סשן", - "noLogs": "אין עדיין יומני סשן", - "noLogsDesc": "הפעל רישום סשן במארח כדי להתחיל להקליט", - "duration": "מֶשֶׁך", - "viewLog": "הצג יומן", - "downloadLog": "הורדה", - "downloadAsText": "הורד כטקסט", - "deleteLog": "לִמְחוֹק", - "confirmDelete": "למחוק את יומן הפעילויות הזה?", - "confirmDeleteDesc": "לא ניתן לבטל פעולה זו.", - "copyContent": "לְהַעְתִיק", - "copied": "מוּעֲתָק!", - "loadError": "טעינת יומני הסשן נכשלה", - "deleteError": "נכשלה מחיקת יומן ההפעלה", - "filterByHost": "סנן לפי מארח...", - "back": "בְּחֲזָרָה", - "empty": "(רֵיק)", - "noResults": "אין תוצאות עבור \"{{query}}\"", - "hostFallback": "מארח {{id}}", - "sessionFallback": "סשן {{id}}" - }, - "player": { - "timeline": "ציר זמן של הקלטה", - "speed": "מהירות השמעה", - "play": "הפעלת הקלטה", - "pause": "השהיית ההקלטה" - }, - "plugin": { - "name": "הקלטת סשן" - } -} diff --git a/plugins/session-recording/locales/translated/hi_IN.json b/plugins/session-recording/locales/translated/hi_IN.json deleted file mode 100644 index e19f80fea..000000000 --- a/plugins/session-recording/locales/translated/hi_IN.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "सत्र लॉग्स" - }, - "hosts": { - "enableSessionRecording": "सत्र रिकॉर्डिंग", - "enableSessionRecordingDesc": "बाद में समीक्षा के लिए टर्मिनल सत्र का आउटपुट रिकॉर्ड करें" - }, - "permissions": { - "view": { - "title": "सेशन की रिकॉर्डिंग देखें", - "description": "अपने सेशन की रिकॉर्डिंग देखें और उन्हें दोबारा चलाएं।" - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "प्रतिधारण अवधि (दिनों में)", - "description": "सेशन रिकॉर्डिंग को स्वचालित रूप से डिलीट होने से पहले कितने समय तक रखा जाता है।" - } - }, - "host": { - "enable": { - "label": "सेशन रिकॉर्डिंग चालू करें", - "description": "इस होस्ट के टर्मिनल आउटपुट को रिकॉर्ड करें ताकि आप इसे बाद में प्लेबैक कर सकें या डाउनलोड कर सकें।" - } - } - }, - "terminalStatus": { - "recording": "रिकॉर्डिंग" - }, - "sessionLogs": { - "title": "सेशन लॉग्स", - "noLogs": "अभी तक कोई सेशन लॉग नहीं", - "noLogsDesc": "रिकॉर्डिंग शुरू करने के लिए किसी होस्ट पर सेशन लॉगिंग सक्षम करें", - "duration": "अवधि", - "viewLog": "लॉग देखें", - "downloadLog": "डाउनलोड करें", - "downloadAsText": "टेक्स्ट के रूप में डाउनलोड करें", - "deleteLog": "हटाएं", - "confirmDelete": "क्या इस सेशन लॉग को हटाएं?", - "confirmDeleteDesc": "यह क्रिया वापस नहीं ली जा सकती।", - "copyContent": "कॉपी करें", - "copied": "कॉपी हो गया!", - "loadError": "सेशन लॉग लोड करने में विफल", - "deleteError": "सेशन लॉग हटाने में विफल", - "filterByHost": "होस्ट द्वारा फ़िल्टर करें...", - "back": "वापस", - "empty": "(खाली)", - "noResults": "\"{{query}} \" के लिए कोई परिणाम नहीं मिले", - "hostFallback": "मेज़बान {{id}}", - "sessionFallback": "सत्र {{id}}" - }, - "player": { - "timeline": "रिकॉर्डिंग समयरेखा", - "speed": "प्लेबैक गति", - "play": "रिकॉर्डिंग चलाएं", - "pause": "रिकॉर्डिंग रोकें" - }, - "plugin": { - "name": "सत्र रिकॉर्डिंग" - } -} diff --git a/plugins/session-recording/locales/translated/hu_HU.json b/plugins/session-recording/locales/translated/hu_HU.json deleted file mode 100644 index 1d916f092..000000000 --- a/plugins/session-recording/locales/translated/hu_HU.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Munkamenet-naplók" - }, - "hosts": { - "enableSessionRecording": "Munkamenet-felvétel", - "enableSessionRecordingDesc": "Terminál munkamenet kimenetének rögzítése későbbi áttekintés céljából" - }, - "permissions": { - "view": { - "title": "Munkamenet-felvételek megtekintése", - "description": "Tekintse meg és játssza le saját felvételeit." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Megtartás (nap)", - "description": "Mennyi ideig tárolják a munkamenet-felvételt, mielőtt automatikusan törlődne." - } - }, - "host": { - "enable": { - "label": "Munkamenet-rögzítés engedélyezése", - "description": "Rögzítse a terminál kimenetét ehhez a gazdagéphez, hogy később lejátszhassa vagy letölthesse." - } - } - }, - "terminalStatus": { - "recording": "Felvétel" - }, - "sessionLogs": { - "title": "Munkamenet-naplók", - "noLogs": "Még nincsenek munkamenet-naplók", - "noLogsDesc": "A felvétel megkezdéséhez engedélyezze a munkamenet-naplózást egy gazdagépen", - "duration": "Időtartam", - "viewLog": "Napló megtekintése", - "downloadLog": "Letöltés", - "downloadAsText": "Letöltés szövegként", - "deleteLog": "Töröl", - "confirmDelete": "Törli ezt a munkamenet-naplót?", - "confirmDeleteDesc": "Ez a művelet nem vonható vissza.", - "copyContent": "Másolat", - "copied": "Másolva!", - "loadError": "Nem sikerült betölteni a munkamenet-naplókat", - "deleteError": "Nem sikerült törölni a munkamenet-naplót", - "filterByHost": "Szűrés hoszt szerint...", - "back": "Vissza", - "empty": "(üres)", - "noResults": "Nincs találat a(z) „{{query}} ” kifejezésre", - "hostFallback": "Gazdagép {{id}}", - "sessionFallback": "Munkamenet {{id}}" - }, - "player": { - "timeline": "Felvételi idővonal", - "speed": "Lejátszási sebesség", - "play": "Felvétel lejátszása", - "pause": "Felvétel szüneteltetése" - }, - "plugin": { - "name": "Munkamenet-felvétel" - } -} diff --git a/plugins/session-recording/locales/translated/id_ID.json b/plugins/session-recording/locales/translated/id_ID.json deleted file mode 100644 index eff33aebd..000000000 --- a/plugins/session-recording/locales/translated/id_ID.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Log Sesi" - }, - "hosts": { - "enableSessionRecording": "Rekaman Sesi", - "enableSessionRecordingDesc": "Rekam output sesi terminal untuk ditinjau nanti." - }, - "permissions": { - "view": { - "title": "Lihat rekaman sesi", - "description": "Lihat dan putar kembali rekaman sesi Anda sendiri." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Retensi (hari)", - "description": "Berapa lama rekaman sesi disimpan sebelum dihapus secara otomatis." - } - }, - "host": { - "enable": { - "label": "Aktifkan perekaman sesi", - "description": "Rekam output terminal untuk host ini agar Anda dapat memutarnya kembali atau mengunduhnya nanti." - } - } - }, - "terminalStatus": { - "recording": "Rekaman" - }, - "sessionLogs": { - "title": "Log Sesi", - "noLogs": "Belum ada log sesi.", - "noLogsDesc": "Aktifkan pencatatan sesi pada host untuk memulai perekaman.", - "duration": "Lamanya", - "viewLog": "Lihat log", - "downloadLog": "Unduh", - "downloadAsText": "Unduh sebagai teks", - "deleteLog": "Menghapus", - "confirmDelete": "Hapus log sesi ini?", - "confirmDeleteDesc": "Tindakan ini tidak dapat dibatalkan.", - "copyContent": "Menyalin", - "copied": "Disalin!", - "loadError": "Gagal memuat log sesi", - "deleteError": "Gagal menghapus log sesi", - "filterByHost": "Saring berdasarkan host...", - "back": "Kembali", - "empty": "(kosong)", - "noResults": "Tidak ada hasil untuk \"{{query}}\"", - "hostFallback": "Tuan rumah {{id}}", - "sessionFallback": "Sesi {{id}}" - }, - "player": { - "timeline": "Garis waktu perekaman", - "speed": "Kecepatan pemutaran", - "play": "Putar rekaman", - "pause": "Jeda perekaman" - }, - "plugin": { - "name": "Rekaman Sesi" - } -} diff --git a/plugins/session-recording/locales/translated/it_IT.json b/plugins/session-recording/locales/translated/it_IT.json deleted file mode 100644 index 91a4cc936..000000000 --- a/plugins/session-recording/locales/translated/it_IT.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Log di sessione" - }, - "hosts": { - "enableSessionRecording": "Registrazione sessione", - "enableSessionRecordingDesc": "Registra l'output della sessione del terminale per la revisione successiva" - }, - "permissions": { - "view": { - "title": "Visualizza le registrazioni delle sessioni", - "description": "Visualizza e riproduci le registrazioni delle tue sessioni." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Tempo di ritenzione (giorni)", - "description": "Per quanto tempo viene conservata la registrazione di una sessione prima di essere eliminata automaticamente." - } - }, - "host": { - "enable": { - "label": "Abilita la registrazione della sessione", - "description": "Registra l'output del terminale per questo host in modo da poterlo riprodurre o scaricare in seguito." - } - } - }, - "terminalStatus": { - "recording": "Registrazione" - }, - "sessionLogs": { - "title": "Registri di sessione", - "noLogs": "Ancora nessun registro di sessione", - "noLogsDesc": "Abilita la registrazione delle sessioni su un host per iniziare a registrare", - "duration": "Durata", - "viewLog": "Visualizza registro", - "downloadLog": "Scarica", - "downloadAsText": "Scarica come testo", - "deleteLog": "Elimina", - "confirmDelete": "Eliminare questo registro di sessione?", - "confirmDeleteDesc": "Questa azione non può essere annullata.", - "copyContent": "Copia", - "copied": "Copiato!", - "loadError": "Impossibile caricare i registri di sessione", - "deleteError": "Impossibile eliminare il registro di sessione", - "filterByHost": "Filtra per host...", - "back": "Indietro", - "empty": "(vuoto)", - "noResults": "Nessun risultato per \"{{query}}\"", - "hostFallback": "Ospite {{id}}", - "sessionFallback": "Sessione {{id}}" - }, - "player": { - "timeline": "Cronologia delle registrazioni", - "speed": "Velocità di riproduzione", - "play": "Riproduci la registrazione", - "pause": "Metti in pausa la registrazione" - }, - "plugin": { - "name": "Registrazione sessione" - } -} diff --git a/plugins/session-recording/locales/translated/ja_JP.json b/plugins/session-recording/locales/translated/ja_JP.json deleted file mode 100644 index 8d052e552..000000000 --- a/plugins/session-recording/locales/translated/ja_JP.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "セッションログ" - }, - "hosts": { - "enableSessionRecording": "セッション記録", - "enableSessionRecordingDesc": "後で確認できるようにターミナルセッションの出力を記録します" - }, - "permissions": { - "view": { - "title": "セッションの録画を見る", - "description": "自分のセッション録音を視聴・再生できます。" - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "保持期間(日数)", - "description": "セッション記録が自動的に削除されるまでの保存期間。" - } - }, - "host": { - "enable": { - "label": "セッション記録を有効にする", - "description": "このホストの端末出力を記録しておけば、後で再生したりダウンロードしたりできます。" - } - } - }, - "terminalStatus": { - "recording": "録音" - }, - "sessionLogs": { - "title": "セッションログ", - "noLogs": "セッションログはまだありません", - "noLogsDesc": "ホストでセッションログを有効にして記録を開始してください", - "duration": "所要時間", - "viewLog": "ログを表示", - "downloadLog": "ダウンロード", - "downloadAsText": "テキストとしてダウンロード", - "deleteLog": "削除", - "confirmDelete": "このセッションログを削除しますか?", - "confirmDeleteDesc": "この操作は元に戻せません。", - "copyContent": "コピー", - "copied": "コピーしました!", - "loadError": "セッションログの読み込みに失敗しました", - "deleteError": "セッションログの削除に失敗しました", - "filterByHost": "ホストでフィルター...", - "back": "戻る", - "empty": "(空)", - "noResults": "「{{query}}」の検索結果はありません", - "hostFallback": "ホスト {{id}}", - "sessionFallback": "セッション {{id}}" - }, - "player": { - "timeline": "録音タイムライン", - "speed": "再生速度", - "play": "録音を再生する", - "pause": "録画を一時停止" - }, - "plugin": { - "name": "セッション記録" - } -} diff --git a/plugins/session-recording/locales/translated/ko_KR.json b/plugins/session-recording/locales/translated/ko_KR.json deleted file mode 100644 index 35b3cb937..000000000 --- a/plugins/session-recording/locales/translated/ko_KR.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "세션 로그" - }, - "hosts": { - "enableSessionRecording": "세션 녹화", - "enableSessionRecordingDesc": "나중에 검토할 수 있도록 터미널 세션 출력을 기록합니다" - }, - "permissions": { - "view": { - "title": "세션 녹화 영상을 시청하세요", - "description": "자신이 녹음한 세션 내용을 보고 재생해 보세요." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "보유 기간(일)", - "description": "세션 녹화 파일이 자동으로 삭제되기 전까지 보관되는 기간은 얼마인가요?" - } - }, - "host": { - "enable": { - "label": "세션 녹화를 활성화합니다.", - "description": "이 호스트의 터미널 출력을 기록하여 나중에 재생하거나 다운로드할 수 있도록 합니다." - } - } - }, - "terminalStatus": { - "recording": "녹음" - }, - "sessionLogs": { - "title": "세션 로그", - "noLogs": "아직 세션 로그 없음", - "noLogsDesc": "호스트에서 세션 로깅을 활성화하여 기록 시작", - "duration": "지속 시간", - "viewLog": "로그 보기", - "downloadLog": "다운로드", - "downloadAsText": "텍스트 파일로 다운로드", - "deleteLog": "삭제", - "confirmDelete": "이 세션 로그를 삭제하시겠습니까?", - "confirmDeleteDesc": "이 작업은 취소할 수 없습니다.", - "copyContent": "복사", - "copied": "복사됨!", - "loadError": "세션 로그 로드 실패", - "deleteError": "세션 로그 삭제 실패", - "filterByHost": "호스트로 필터링...", - "back": "뒤로", - "empty": "(비어 있음)", - "noResults": "\"{{query}} \"에 대한 검색 결과가 없습니다.", - "hostFallback": "호스트 {{id}}", - "sessionFallback": "세션 {{id}}" - }, - "player": { - "timeline": "녹음 타임라인", - "speed": "재생 속도", - "play": "재생 녹음", - "pause": "녹화 일시 중지" - }, - "plugin": { - "name": "세션 녹화" - } -} diff --git a/plugins/session-recording/locales/translated/nl_NL.json b/plugins/session-recording/locales/translated/nl_NL.json deleted file mode 100644 index 48d3900f6..000000000 --- a/plugins/session-recording/locales/translated/nl_NL.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Sessielogboeken" - }, - "hosts": { - "enableSessionRecording": "Sessieopname", - "enableSessionRecordingDesc": "Sla de uitvoer van de terminalsessie op voor latere beoordeling." - }, - "permissions": { - "view": { - "title": "Bekijk sessieopnames", - "description": "Bekijk en speel je eigen sessieopnames af." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Retentie (dagen)", - "description": "Hoe lang een sessieopname wordt bewaard voordat deze automatisch wordt verwijderd." - } - }, - "host": { - "enable": { - "label": "Sessieopname inschakelen", - "description": "Neem de terminaluitvoer van deze host op, zodat u deze later kunt afspelen of downloaden." - } - } - }, - "terminalStatus": { - "recording": "Opname" - }, - "sessionLogs": { - "title": "Sessielogboeken", - "noLogs": "Nog geen sessielogboeken", - "noLogsDesc": "Schakel sessielogging in op een host om te beginnen met opnemen.", - "duration": "Duur", - "viewLog": "Logboek bekijken", - "downloadLog": "Download", - "downloadAsText": "Downloaden als tekst", - "deleteLog": "Verwijderen", - "confirmDelete": "Dit sessielogboek verwijderen?", - "confirmDeleteDesc": "Deze actie kan niet ongedaan gemaakt worden.", - "copyContent": "Kopiëren", - "copied": "Gekopieerd!", - "loadError": "Het laden van de sessielogboeken is mislukt.", - "deleteError": "Het verwijderen van het sessielogboek is mislukt.", - "filterByHost": "Filteren op host...", - "back": "Rug", - "empty": "(leeg)", - "noResults": "Geen resultaten voor \"{{query}}\"", - "hostFallback": "Host {{id}}", - "sessionFallback": "Sessie {{id}}" - }, - "player": { - "timeline": "Opnametijdlijn", - "speed": "Afspeelsnelheid", - "play": "Speel opname af", - "pause": "Opname pauzeren" - }, - "plugin": { - "name": "Sessieopname" - } -} diff --git a/plugins/session-recording/locales/translated/no_NO.json b/plugins/session-recording/locales/translated/no_NO.json deleted file mode 100644 index c3d76953c..000000000 --- a/plugins/session-recording/locales/translated/no_NO.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Øktlogger" - }, - "hosts": { - "enableSessionRecording": "Øktopptak", - "enableSessionRecordingDesc": "Ta opp utdata fra terminaløkten for senere gjennomgang" - }, - "permissions": { - "view": { - "title": "Se opptak av økter", - "description": "Se og spill av dine egne øktopptak." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Oppbevaring (dager)", - "description": "Hvor lenge et øktopptak lagres før det slettes automatisk." - } - }, - "host": { - "enable": { - "label": "Aktiver øktopptak", - "description": "Spill inn terminalutdata for denne verten, slik at du kan spille det av eller laste det ned senere." - } - } - }, - "terminalStatus": { - "recording": "Innspilling" - }, - "sessionLogs": { - "title": "Øktlogger", - "noLogs": "Ingen øktlogger ennå", - "noLogsDesc": "Aktiver øktlogging på en vert for å starte opptaket", - "duration": "Varighet", - "viewLog": "Vis logg", - "downloadLog": "Last ned", - "downloadAsText": "Last ned som tekst", - "deleteLog": "Slett", - "confirmDelete": "Slette denne øktloggen?", - "confirmDeleteDesc": "Denne handlingen kan ikke angres.", - "copyContent": "Kopier", - "copied": "Kopiert!", - "loadError": "Kunne ikke laste inn øktlogger", - "deleteError": "Kunne ikke slette øktloggen", - "filterByHost": "Filtrer etter vert...", - "back": "Tilbake", - "empty": "(tømme)", - "noResults": "Ingen resultater for «{{query}}»", - "hostFallback": "Vert {{id}}", - "sessionFallback": "Økt {{id}}" - }, - "player": { - "timeline": "Tidslinje for opptak", - "speed": "Avspillingshastighet", - "play": "Spill av opptaket", - "pause": "Sett opptaket på pause" - }, - "plugin": { - "name": "Øktopptak" - } -} diff --git a/plugins/session-recording/locales/translated/pl_PL.json b/plugins/session-recording/locales/translated/pl_PL.json deleted file mode 100644 index b3cc20ca6..000000000 --- a/plugins/session-recording/locales/translated/pl_PL.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Logi sesji" - }, - "hosts": { - "enableSessionRecording": "Nagrywanie sesji", - "enableSessionRecordingDesc": "Zapisuj wyjście sesji terminala do późniejszego przejrzenia" - }, - "permissions": { - "view": { - "title": "Zobacz nagrania sesji", - "description": "Oglądaj i odtwarzaj własne nagrania sesji." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Retencja (dni)", - "description": "Jak długo przechowywane jest nagranie sesji, zanim zostanie automatycznie usunięte." - } - }, - "host": { - "enable": { - "label": "Włącz nagrywanie sesji", - "description": "Nagraj dane wyjściowe terminala dla tego hosta, aby móc je później odtworzyć lub pobrać." - } - } - }, - "terminalStatus": { - "recording": "Nagranie" - }, - "sessionLogs": { - "title": "Logi sesji", - "noLogs": "Brak logów sesji", - "noLogsDesc": "Włącz rejestrowanie sesji na hoście, aby rozpocząć nagrywanie", - "duration": "Czas trwania", - "viewLog": "Wyświetl log", - "downloadLog": "Pobierz", - "downloadAsText": "Pobierz jako tekst", - "deleteLog": "Usuń", - "confirmDelete": "Usunąć ten log sesji?", - "confirmDeleteDesc": "Tej czynności nie można cofnąć.", - "copyContent": "Kopiuj", - "copied": "Skopiowano!", - "loadError": "Nie udało się załadować logów sesji", - "deleteError": "Nie udało się usunąć logu sesji", - "filterByHost": "Filtruj według hosta...", - "back": "Wstecz", - "empty": "(pusty)", - "noResults": "Brak wyników dla „{{query}}”", - "hostFallback": "Gospodarz {{id}}", - "sessionFallback": "Sesja {{id}}" - }, - "player": { - "timeline": "Oś czasu nagrywania", - "speed": "Prędkość odtwarzania", - "play": "Odtwórz nagranie", - "pause": "Wstrzymaj nagrywanie" - }, - "plugin": { - "name": "Nagrywanie sesji" - } -} diff --git a/plugins/session-recording/locales/translated/pt_BR.json b/plugins/session-recording/locales/translated/pt_BR.json deleted file mode 100644 index 8ba3adff7..000000000 --- a/plugins/session-recording/locales/translated/pt_BR.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Logs de sessão" - }, - "hosts": { - "enableSessionRecording": "Gravação da sessão", - "enableSessionRecordingDesc": "Registre a saída da sessão do terminal para revisão posterior." - }, - "permissions": { - "view": { - "title": "Veja as gravações das sessões", - "description": "Veja e reproduza as gravações das suas próprias sessões." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Retenção (dias)", - "description": "Por quanto tempo uma gravação de sessão é mantida antes de ser excluída automaticamente." - } - }, - "host": { - "enable": { - "label": "Ativar gravação de sessão", - "description": "Grave a saída do terminal para este host para que você possa reproduzi-la ou baixá-la posteriormente." - } - } - }, - "terminalStatus": { - "recording": "Gravação" - }, - "sessionLogs": { - "title": "Logs de sessão", - "noLogs": "Ainda não há registros de sessão.", - "noLogsDesc": "Habilite o registro de sessão em um host para iniciar a gravação.", - "duration": "Duração", - "viewLog": "Ver registro", - "downloadLog": "Download", - "downloadAsText": "Baixar como texto", - "deleteLog": "Apagar", - "confirmDelete": "Excluir este registro de sessão?", - "confirmDeleteDesc": "Esta ação não pode ser desfeita.", - "copyContent": "Copiar", - "copied": "Copiado!", - "loadError": "Falha ao carregar os registros da sessão.", - "deleteError": "Falha ao excluir o registro da sessão", - "filterByHost": "Filtrar por host...", - "back": "Voltar", - "empty": "(vazio)", - "noResults": "Nenhum resultado para \"{{query}}\"", - "hostFallback": "Anfitrião {{id}}", - "sessionFallback": "Sessão {{id}}" - }, - "player": { - "timeline": "Cronograma de gravação", - "speed": "Velocidade de reprodução", - "play": "Reproduzir gravação", - "pause": "Pausar gravação" - }, - "plugin": { - "name": "Gravação da sessão" - } -} diff --git a/plugins/session-recording/locales/translated/pt_PT.json b/plugins/session-recording/locales/translated/pt_PT.json deleted file mode 100644 index 15bed6948..000000000 --- a/plugins/session-recording/locales/translated/pt_PT.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Registos de Sessão" - }, - "hosts": { - "enableSessionRecording": "Gravação de Sessão", - "enableSessionRecordingDesc": "Gravar a saída da sessão de terminal para revisão posterior" - }, - "permissions": { - "view": { - "title": "Veja as gravações das sessões", - "description": "Veja e reproduza as gravações das suas próprias sessões." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Retenção (dias)", - "description": "Durante quanto tempo é mantida uma gravação de sessão antes de ser automaticamente eliminada." - } - }, - "host": { - "enable": { - "label": "Ativar gravação de sessão", - "description": "Grave a saída do terminal para este host para que possa reproduzi-la ou descarregá-la posteriormente." - } - } - }, - "terminalStatus": { - "recording": "Gravação" - }, - "sessionLogs": { - "title": "Registos de Sessão", - "noLogs": "Ainda sem registos de sessão", - "noLogsDesc": "Ative o registo de sessão num host para começar a gravar", - "duration": "Duração", - "viewLog": "Ver registo", - "downloadLog": "Transferir", - "downloadAsText": "Descarregar como texto", - "deleteLog": "Eliminar", - "confirmDelete": "Eliminar este registo de sessão?", - "confirmDeleteDesc": "Esta ação não pode ser desfeita.", - "copyContent": "Copiar", - "copied": "Copiado!", - "loadError": "Falha ao carregar registos de sessão", - "deleteError": "Falha ao eliminar registo de sessão", - "filterByHost": "Filtrar por host...", - "back": "Voltar", - "empty": "(vazio)", - "noResults": "Nenhum resultado para \"{{query}}\"", - "hostFallback": "Anfitrião {{id}}", - "sessionFallback": "Sessão {{id}}" - }, - "player": { - "timeline": "Cronograma de gravação", - "speed": "Velocidade de reprodução", - "play": "Reproduzir gravação", - "pause": "Pausar gravação" - }, - "plugin": { - "name": "Gravação de Sessão" - } -} diff --git a/plugins/session-recording/locales/translated/ro_RO.json b/plugins/session-recording/locales/translated/ro_RO.json deleted file mode 100644 index aec38d031..000000000 --- a/plugins/session-recording/locales/translated/ro_RO.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Jurnalele de sesiune" - }, - "hosts": { - "enableSessionRecording": "Înregistrarea sesiunii", - "enableSessionRecordingDesc": "Înregistrați ieșirea sesiunii terminale pentru o revizuire ulterioară" - }, - "permissions": { - "view": { - "title": "Vizualizați înregistrările sesiunilor", - "description": "Vizualizați și redați propriile înregistrări ale sesiunilor." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Retenție (zile)", - "description": "Cât timp este păstrată o înregistrare a sesiunii înainte de a fi ștearsă automat." - } - }, - "host": { - "enable": { - "label": "Activează înregistrarea sesiunii", - "description": "Înregistrați ieșirea terminalului pentru această gazdă, astfel încât să o puteți reda sau descărca mai târziu." - } - } - }, - "terminalStatus": { - "recording": "Înregistrare" - }, - "sessionLogs": { - "title": "Jurnalele de sesiune", - "noLogs": "Încă nu există jurnale de sesiune", - "noLogsDesc": "Activați înregistrarea în jurnal pe o gazdă pentru a începe înregistrarea", - "duration": "Durată", - "viewLog": "Vizualizare jurnal", - "downloadLog": "Descărcare", - "downloadAsText": "Descărcați ca text", - "deleteLog": "Şterge", - "confirmDelete": "Ștergeți acest jurnal de sesiune?", - "confirmDeleteDesc": "Această acțiune nu poate fi anulată.", - "copyContent": "Copie", - "copied": "Copiat!", - "loadError": "Nu s-au putut încărca jurnalele de sesiune", - "deleteError": "Nu s-a putut șterge jurnalul sesiunii", - "filterByHost": "Filtrare după gazdă...", - "back": "Spate", - "empty": "(gol)", - "noResults": "Niciun rezultat pentru „{{query}}”", - "hostFallback": "Gazdă {{id}}", - "sessionFallback": "Sesiune {{id}}" - }, - "player": { - "timeline": "Cronologia înregistrării", - "speed": "Viteză de redare", - "play": "Redă înregistrarea", - "pause": "Întrerupe înregistrarea" - }, - "plugin": { - "name": "Înregistrarea sesiunii" - } -} diff --git a/plugins/session-recording/locales/translated/ru_RU.json b/plugins/session-recording/locales/translated/ru_RU.json deleted file mode 100644 index b8eea342d..000000000 --- a/plugins/session-recording/locales/translated/ru_RU.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Журналы сессий" - }, - "hosts": { - "enableSessionRecording": "Запись сессии", - "enableSessionRecordingDesc": "Записывать вывод терминальной сессии для последующего просмотра" - }, - "permissions": { - "view": { - "title": "Просмотреть записи сессий", - "description": "Просматривайте и воспроизводите записи своих собственных сессий." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Срок хранения (дни)", - "description": "Как долго хранится запись сеанса, прежде чем она будет автоматически удалена." - } - }, - "host": { - "enable": { - "label": "Включить запись сеанса", - "description": "Запишите вывод терминала для этого хоста, чтобы вы могли воспроизвести его или загрузить позже." - } - } - }, - "terminalStatus": { - "recording": "Запись" - }, - "sessionLogs": { - "title": "Журналы сеансов", - "noLogs": "Пока нет журналов сеансов", - "noLogsDesc": "Включите ведение журнала сеансов на хосте, чтобы начать запись", - "duration": "Продолжительность", - "viewLog": "Просмотреть журнал", - "downloadLog": "Скачать", - "downloadAsText": "Скачать как текст", - "deleteLog": "Удалить", - "confirmDelete": "Удалить этот журнал сеансов?", - "confirmDeleteDesc": "Это действие нельзя отменить.", - "copyContent": "Копировать", - "copied": "Скопировано!", - "loadError": "Не удалось загрузить журналы сеансов", - "deleteError": "Не удалось удалить журнал сеанса", - "filterByHost": "Фильтровать по хосту...", - "back": "Назад", - "empty": "(пусто)", - "noResults": "Результаты поиска по запросу \"{{query}} отсутствуют", - "hostFallback": "Хост {{id}}", - "sessionFallback": "Сессия {{id}}" - }, - "player": { - "timeline": "Хронология записи", - "speed": "скорость воспроизведения", - "play": "Воспроизвести запись", - "pause": "Приостановить запись" - }, - "plugin": { - "name": "Запись сессии" - } -} diff --git a/plugins/session-recording/locales/translated/sr_SP.json b/plugins/session-recording/locales/translated/sr_SP.json deleted file mode 100644 index 499822fe2..000000000 --- a/plugins/session-recording/locales/translated/sr_SP.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Записи сесија" - }, - "hosts": { - "enableSessionRecording": "Снимање сесије", - "enableSessionRecordingDesc": "Снимите излаз терминалне сесије за каснији преглед" - }, - "permissions": { - "view": { - "title": "Погледајте снимке сесије", - "description": "Погледајте и репродукујте снимке сопствених сесија." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Задржавање (дани)", - "description": "Колико дуго се снимак сесије чува пре него што се аутоматски обрише." - } - }, - "host": { - "enable": { - "label": "Омогући снимање сесије", - "description": "Снимите излаз терминала за овај хост како бисте га могли касније репродуковати или преузети." - } - } - }, - "terminalStatus": { - "recording": "Снимање" - }, - "sessionLogs": { - "title": "Записи сесија", - "noLogs": "Још нема евиденција сесија", - "noLogsDesc": "Омогућите евидентирање сесије на хосту да бисте започели снимање", - "duration": "Трајање", - "viewLog": "Прикажи дневник", - "downloadLog": "Преузми", - "downloadAsText": "Преузми као текст", - "deleteLog": "Обриши", - "confirmDelete": "Да ли желите да обришете овај дневник сесије?", - "confirmDeleteDesc": "Ова радња се не може поништити.", - "copyContent": "Копија", - "copied": "Копирано!", - "loadError": "Учитавање евиденције сесије није успело", - "deleteError": "Брисање дневника сесије није успело", - "filterByHost": "Филтрирај по хосту...", - "back": "Назад", - "empty": "(празно)", - "noResults": "Нема резултата за „{{query}}“", - "hostFallback": "Домаћин {{id}}", - "sessionFallback": "Сесија {{id}}" - }, - "player": { - "timeline": "Временска линија снимања", - "speed": "Брзина репродукције", - "play": "Пусти снимак", - "pause": "Паузирај снимање" - }, - "plugin": { - "name": "Снимање сесије" - } -} diff --git a/plugins/session-recording/locales/translated/sv_SE.json b/plugins/session-recording/locales/translated/sv_SE.json deleted file mode 100644 index 6c6ada844..000000000 --- a/plugins/session-recording/locales/translated/sv_SE.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Sessionsloggar" - }, - "hosts": { - "enableSessionRecording": "Sessionsinspelning", - "enableSessionRecordingDesc": "Spela in utdata från terminalsessionen för senare granskning" - }, - "permissions": { - "view": { - "title": "Visa inspelningar av sessionen", - "description": "Se och spela upp dina egna inspelningar av sessioner." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Lagringstid (dagar)", - "description": "Hur länge en sessionsinspelning sparas innan den raderas automatiskt." - } - }, - "host": { - "enable": { - "label": "Aktivera sessionsinspelning", - "description": "Spela in terminalutdata för den här värden så att du kan spela upp eller ladda ner den senare." - } - } - }, - "terminalStatus": { - "recording": "Inspelning" - }, - "sessionLogs": { - "title": "Sessionsloggar", - "noLogs": "Inga sessionsloggar ännu", - "noLogsDesc": "Aktivera sessionsloggning på en värd för att starta inspelning", - "duration": "Varaktighet", - "viewLog": "Visa logg", - "downloadLog": "Ladda ner", - "downloadAsText": "Ladda ner som text", - "deleteLog": "Radera", - "confirmDelete": "Radera den här sessionsloggen?", - "confirmDeleteDesc": "Den här åtgärden kan inte ångras.", - "copyContent": "Kopiera", - "copied": "Kopierad!", - "loadError": "Misslyckades med att läsa in sessionsloggar", - "deleteError": "Misslyckades med att radera sessionsloggen", - "filterByHost": "Filtrera efter värd...", - "back": "Tillbaka", - "empty": "(tömma)", - "noResults": "Inga resultat för \"{{query}}\"", - "hostFallback": "Värd {{id}}", - "sessionFallback": "Session {{id}}" - }, - "player": { - "timeline": "Inspelningstidslinje", - "speed": "Uppspelningshastighet", - "play": "Spela upp inspelningen", - "pause": "Pausa inspelningen" - }, - "plugin": { - "name": "Sessionsinspelning" - } -} diff --git a/plugins/session-recording/locales/translated/th_TH.json b/plugins/session-recording/locales/translated/th_TH.json deleted file mode 100644 index 1a163659f..000000000 --- a/plugins/session-recording/locales/translated/th_TH.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "บันทึกเซสชัน" - }, - "hosts": { - "enableSessionRecording": "การบันทึกการประชุม", - "enableSessionRecordingDesc": "บันทึกผลลัพธ์จากเทอร์มินัลเพื่อตรวจสอบในภายหลัง" - }, - "permissions": { - "view": { - "title": "ดูบันทึกการประชุม", - "description": "ดูและเล่นบันทึกการประชุมของคุณเองได้" - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "การเก็บรักษา (วัน)", - "description": "ระบบจะเก็บไฟล์บันทึกการประชุมไว้นานแค่ไหนก่อนที่จะลบออกโดยอัตโนมัติ" - } - }, - "host": { - "enable": { - "label": "เปิดใช้งานการบันทึกเซสชัน", - "description": "บันทึกข้อมูลที่แสดงบนเทอร์มินัลของโฮสต์นี้ เพื่อให้คุณสามารถเล่นซ้ำหรือดาวน์โหลดได้ในภายหลัง" - } - } - }, - "terminalStatus": { - "recording": "การบันทึก" - }, - "sessionLogs": { - "title": "บันทึกเซสชัน", - "noLogs": "ยังไม่มีบันทึกการใช้งานใดๆ", - "noLogsDesc": "เปิดใช้งานการบันทึกเซสชันบนโฮสต์เพื่อเริ่มการบันทึก", - "duration": "ระยะเวลา", - "viewLog": "ดูบันทึก", - "downloadLog": "ดาวน์โหลด", - "downloadAsText": "ดาวน์โหลดเป็นข้อความ", - "deleteLog": "ลบ", - "confirmDelete": "ลบบันทึกการใช้งานนี้หรือไม่?", - "confirmDeleteDesc": "การกระทำนี้ไม่สามารถย้อนกลับได้", - "copyContent": "สำเนา", - "copied": "คัดลอกแล้ว!", - "loadError": "ไม่สามารถโหลดบันทึกเซสชันได้", - "deleteError": "ไม่สามารถลบบันทึกเซสชันได้", - "filterByHost": "กรองตามโฮสต์...", - "back": "กลับ", - "empty": "(ว่างเปล่า)", - "noResults": "ไม่พบผลลัพธ์สำหรับ \"{{query}}\"", - "hostFallback": "โฮสต์ {{id}}", - "sessionFallback": "เซสชั่น {{id}}" - }, - "player": { - "timeline": "ลำดับเวลาการบันทึก", - "speed": "ความเร็วในการเล่น", - "play": "เล่นการบันทึก", - "pause": "หยุดการบันทึกชั่วคราว" - }, - "plugin": { - "name": "การบันทึกการประชุม" - } -} diff --git a/plugins/session-recording/locales/translated/tr_TR.json b/plugins/session-recording/locales/translated/tr_TR.json deleted file mode 100644 index 84d0425f4..000000000 --- a/plugins/session-recording/locales/translated/tr_TR.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Oturum Kayıtları" - }, - "hosts": { - "enableSessionRecording": "Oturum kaydı", - "enableSessionRecordingDesc": "Daha sonra gözden geçirmek için terminal oturumu çıktısını kaydet" - }, - "permissions": { - "view": { - "title": "Oturum kayıtlarını görüntüleyin", - "description": "Kendi oturum kayıtlarınızı görüntüleyin ve tekrar oynatın." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Saklama süresi (gün)", - "description": "Oturum kaydının otomatik olarak silinmeden önce ne kadar süreyle saklandığı." - } - }, - "host": { - "enable": { - "label": "Oturum kaydını etkinleştirin", - "description": "Bu sunucu için terminal çıktısını kaydedin, böylece daha sonra tekrar oynatabilir veya indirebilirsiniz." - } - } - }, - "terminalStatus": { - "recording": "Kayıt" - }, - "sessionLogs": { - "title": "Oturum Günlükleri", - "noLogs": "Henüz oturum günlüğü yok", - "noLogsDesc": "Kayda başlamak için bir sunucuda oturum günlüğünü etkinleştirin", - "duration": "Süre", - "viewLog": "Günlüğü görüntüle", - "downloadLog": "İndir", - "downloadAsText": "Metin olarak indir", - "deleteLog": "Sil", - "confirmDelete": "Bu oturum günlüğü silinsin mi?", - "confirmDeleteDesc": "Bu işlem geri alınamaz.", - "copyContent": "Kopyala", - "copied": "Kopyalandı!", - "loadError": "Oturum günlükleri yüklenemedi", - "deleteError": "Oturum günlüğü silinemedi", - "filterByHost": "Sunucuya göre filtrele...", - "back": "Geri", - "empty": "(boş)", - "noResults": "\"{{query}} \" için sonuç bulunamadı.", - "hostFallback": "Sunucu {{id}}", - "sessionFallback": "Oturum {{id}}" - }, - "player": { - "timeline": "Kayıt zaman çizelgesi", - "speed": "Oynatma hızı", - "play": "Kaydı oynat", - "pause": "Kaydı duraklat" - }, - "plugin": { - "name": "Oturum kaydı" - } -} diff --git a/plugins/session-recording/locales/translated/uk_UA.json b/plugins/session-recording/locales/translated/uk_UA.json deleted file mode 100644 index fb1fa4792..000000000 --- a/plugins/session-recording/locales/translated/uk_UA.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Журнали сеансів" - }, - "hosts": { - "enableSessionRecording": "Запис сеансу", - "enableSessionRecordingDesc": "Запис виводу сеансу терміналу для подальшого перегляду" - }, - "permissions": { - "view": { - "title": "Переглянути записи сеансу", - "description": "Переглядайте та відтворюйте власні записи сеансів." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Утримання (дні)", - "description": "Як довго зберігається запис сеансу, перш ніж його буде автоматично видалено." - } - }, - "host": { - "enable": { - "label": "Увімкнути запис сеансу", - "description": "Запишіть вивід терміналу для цього хоста, щоб ви могли його відтворити або завантажити пізніше." - } - } - }, - "terminalStatus": { - "recording": "Запис" - }, - "sessionLogs": { - "title": "Журнали сеансів", - "noLogs": "Журналів сеансів поки що немає", - "noLogsDesc": "Увімкнути ведення журналу сеансу на хості, щоб розпочати запис", - "duration": "Тривалість", - "viewLog": "Переглянути журнал", - "downloadLog": "Завантажити", - "downloadAsText": "Завантажити як текст", - "deleteLog": "Видалити", - "confirmDelete": "Видалити цей журнал сеансу?", - "confirmDeleteDesc": "Цю дію не можна скасувати.", - "copyContent": "Копіювати", - "copied": "Скопійовано!", - "loadError": "Не вдалося завантажити журнали сеансу", - "deleteError": "Не вдалося видалити журнал сеансу", - "filterByHost": "Фільтрувати за хостом...", - "back": "Назад", - "empty": "(порожній)", - "noResults": "Немає результатів для запиту \"{{query}}\"", - "hostFallback": "Хост {{id}}", - "sessionFallback": "Сесія {{id}}" - }, - "player": { - "timeline": "Хронологія запису", - "speed": "Швидкість відтворення", - "play": "Відтворити запис", - "pause": "Призупинити запис" - }, - "plugin": { - "name": "Запис сеансу" - } -} diff --git a/plugins/session-recording/locales/translated/vi_VN.json b/plugins/session-recording/locales/translated/vi_VN.json deleted file mode 100644 index ef5910054..000000000 --- a/plugins/session-recording/locales/translated/vi_VN.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "Nhật ký phiên" - }, - "hosts": { - "enableSessionRecording": "Ghi phiên", - "enableSessionRecordingDesc": "Ghi lại đầu ra phiên terminal để xem lại sau" - }, - "permissions": { - "view": { - "title": "Xem bản ghi lại các buổi họp", - "description": "Xem và phát lại các bản ghi âm buổi tập của chính bạn." - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "Thời gian lưu giữ (ngày)", - "description": "Thời gian lưu giữ bản ghi phiên trước khi nó tự động bị xóa." - } - }, - "host": { - "enable": { - "label": "Bật tính năng ghi lại phiên", - "description": "Ghi lại thông báo lỗi trên terminal của máy chủ này để bạn có thể phát lại hoặc tải xuống sau." - } - } - }, - "terminalStatus": { - "recording": "Ghi âm" - }, - "sessionLogs": { - "title": "Nhật ký phiên", - "noLogs": "Chưa có nhật ký phiên nào", - "noLogsDesc": "Bật ghi nhật ký phiên trên máy chủ để bắt đầu ghi", - "duration": "Thời lượng", - "viewLog": "Xem nhật ký", - "downloadLog": "Tải xuống", - "downloadAsText": "Tải xuống dưới dạng văn bản", - "deleteLog": "Xóa", - "confirmDelete": "Xóa nhật ký phiên này?", - "confirmDeleteDesc": "Hành động này không thể hoàn tác.", - "copyContent": "Sao chép", - "copied": "Đã sao chép!", - "loadError": "Tải nhật ký phiên thất bại", - "deleteError": "Xóa nhật ký phiên thất bại", - "filterByHost": "Lọc theo máy chủ...", - "back": "Quay lại", - "empty": "(trống)", - "noResults": "Không tìm thấy kết quả cho \"{{query}}\"", - "hostFallback": "Máy chủ {{id}}", - "sessionFallback": "Phiên {{id}}" - }, - "player": { - "timeline": "Lịch trình ghi âm", - "speed": "Tốc độ phát lại", - "play": "Phát bản ghi âm", - "pause": "Tạm dừng ghi âm" - }, - "plugin": { - "name": "Ghi phiên" - } -} diff --git a/plugins/session-recording/locales/translated/zh_CN.json b/plugins/session-recording/locales/translated/zh_CN.json deleted file mode 100644 index 8d9e5ff51..000000000 --- a/plugins/session-recording/locales/translated/zh_CN.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "会话日志" - }, - "hosts": { - "enableSessionRecording": "会话录制", - "enableSessionRecordingDesc": "记录终端会话输出以供日后查看" - }, - "permissions": { - "view": { - "title": "查看会议录像", - "description": "查看并回放您自己的会话录像。" - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "保留时间(天)", - "description": "会话录像在自动删除前会保留多长时间?" - } - }, - "host": { - "enable": { - "label": "启用会话录制", - "description": "记录此主机的终端输出,以便稍后回放或下载。" - } - } - }, - "terminalStatus": { - "recording": "记录" - }, - "sessionLogs": { - "title": "会话日志", - "noLogs": "暂无会话日志", - "noLogsDesc": "在主机上启用会话日志以开始记录", - "duration": "持续时间", - "viewLog": "查看日志", - "downloadLog": "下载", - "downloadAsText": "下载为文本", - "deleteLog": "删除", - "confirmDelete": "删除此会话日志?", - "confirmDeleteDesc": "此操作无法撤消。", - "copyContent": "复制", - "copied": "已复制!", - "loadError": "加载会话日志失败", - "deleteError": "删除会话日志失败", - "filterByHost": "按主机筛选...", - "back": "返回", - "empty": "(空)", - "noResults": "没有找到“{{query}}”的结果", - "hostFallback": "主机 {{id}}", - "sessionFallback": "会话 {{id}}" - }, - "player": { - "timeline": "录制时间线", - "speed": "播放速度", - "play": "播放录音", - "pause": "暂停录制" - }, - "plugin": { - "name": "会话录制" - } -} diff --git a/plugins/session-recording/locales/translated/zh_TW.json b/plugins/session-recording/locales/translated/zh_TW.json deleted file mode 100644 index af50d6099..000000000 --- a/plugins/session-recording/locales/translated/zh_TW.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "nav": { - "sessionLogs": "工作階段記錄" - }, - "hosts": { - "enableSessionRecording": "工作階段錄製", - "enableSessionRecordingDesc": "記錄終端機工作階段輸出以供日後檢視" - }, - "permissions": { - "view": { - "title": "查看會議錄影", - "description": "查看並回放您自己的會話錄影。" - } - }, - "settings": { - "admin": { - "retentionDays": { - "label": "保留時間(天)", - "description": "會話錄影在自動刪除前會保留多久?" - } - }, - "host": { - "enable": { - "label": "啟用會話錄製", - "description": "記錄此主機的終端輸出,以便稍後重播或下載。" - } - } - }, - "terminalStatus": { - "recording": "記錄" - }, - "sessionLogs": { - "title": "工作階段記錄", - "noLogs": "尚無任何工作階段記錄", - "noLogsDesc": "在主機上啟用工作階段記錄以開始錄製", - "duration": "持續時間", - "viewLog": "檢視記錄", - "downloadLog": "下載", - "downloadAsText": "下載為文字", - "deleteLog": "刪除", - "confirmDelete": "刪除此工作階段記錄?", - "confirmDeleteDesc": "此動作無法復原。", - "copyContent": "複製", - "copied": "已複製!", - "loadError": "載入工作階段記錄失敗", - "deleteError": "刪除工作階段記錄失敗", - "filterByHost": "依主機篩選...", - "back": "返回", - "empty": "(空的)", - "noResults": "沒有找到「{{query}}」的結果", - "hostFallback": "主機 {{id}}", - "sessionFallback": "會話 {{id}}" - }, - "player": { - "timeline": "錄製時間軸", - "speed": "播放速度", - "play": "播放錄音", - "pause": "暫停錄製" - }, - "plugin": { - "name": "工作階段錄製" - } -} diff --git a/plugins/session-recording/manifest.json b/plugins/session-recording/manifest.json deleted file mode 100644 index bf19a807f..000000000 --- a/plugins/session-recording/manifest.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "id": "session-recording", - "name": "Session Recording", - "version": "1.0.0", - "description": "Record terminal sessions and play them back or download them later.", - "author": { - "name": "Termix" - }, - "license": "MIT", - "repository": "https://github.com/Termix-SSH/Termix", - "category": "Terminal", - "icon": "ScrollText", - "engine": { - "termix": ">=2.9.0", - "api": "1" - }, - "capabilities": [ - "hosts:read", - "db:own", - "files:own", - "network:serve", - "ui:surface", - "events:core", - "db:core-refs" - ], - "provides": [ - { - "service": "recordings.writer", - "version": "1.1.0", - "permission": "session-recording.view" - } - ], - "contributes": { - "tabs": [ - { - "id": "session-logs", - "titleKey": "nav.sessionLogs", - "icon": "ScrollText", - "openFrom": ["rail"] - } - ], - "permissions": [ - { - "name": "view", - "titleKey": "permissions.view.title", - "descriptionKey": "permissions.view.description", - "defaultRoles": ["admin", "user"] - } - ], - "settings": { - "admin": [ - { - "key": "retentionDays", - "type": "number", - "labelKey": "settings.admin.retentionDays.label", - "descriptionKey": "settings.admin.retentionDays.description", - "min": 1, - "max": 3650, - "default": 30 - } - ], - "host": { - "editorGroup": "ssh", - "editorOrder": 85, - "enableKey": "enableSessionRecording", - "enableLabelKey": "settings.host.enable.label", - "enableDescriptionKey": "settings.host.enable.description", - "fields": [] - } - }, - "hostCapability": { - "key": "enableSessionRecording", - "labelKey": "hosts.enableSessionRecording", - "editorTab": "general" - }, - "uses": ["terminal.toolbarStatus"] - }, - "locales": "locales", - "platforms": ["linux", "win32", "darwin"] -} diff --git a/plugins/session-recording/migrations/mysql/0001_adopt_session_recordings.sql b/plugins/session-recording/migrations/mysql/0001_adopt_session_recordings.sql deleted file mode 100644 index b0760ddf7..000000000 --- a/plugins/session-recording/migrations/mysql/0001_adopt_session_recordings.sql +++ /dev/null @@ -1,22 +0,0 @@ --- session-recording 0001: adopt_session_recordings --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS `session_recordings` ( - `id` int AUTO_INCREMENT PRIMARY KEY, - `host_id` int NOT NULL, - `user_id` varchar(255), - `username` text, - `access_id` int, - `started_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), - `ended_at` text, - `duration` int, - `commands` text, - `dangerous_actions` text, - `recording_path` text, - `protocol` text NOT NULL DEFAULT ('ssh'), - `format` text NOT NULL DEFAULT ('text'), - `terminated_by_owner` boolean DEFAULT false, - `termination_reason` text, - FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE -); -RENAME TABLE `session_recordings` TO `p_session_recording_session_recordings`; diff --git a/plugins/session-recording/migrations/postgres/0001_adopt_session_recordings.sql b/plugins/session-recording/migrations/postgres/0001_adopt_session_recordings.sql deleted file mode 100644 index 4cd436667..000000000 --- a/plugins/session-recording/migrations/postgres/0001_adopt_session_recordings.sql +++ /dev/null @@ -1,24 +0,0 @@ --- session-recording 0001: adopt_session_recordings --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS "session_recordings" ( - "id" serial PRIMARY KEY, - "host_id" integer NOT NULL, - "user_id" varchar(255), - "username" text, - "access_id" integer, - "started_at" varchar(255) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "ended_at" text, - "duration" integer, - "commands" text, - "dangerous_actions" text, - "recording_path" text, - "protocol" text NOT NULL DEFAULT 'ssh', - "format" text NOT NULL DEFAULT 'text', - "terminated_by_owner" boolean DEFAULT false, - "termination_reason" text, - FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS "idx_session_recordings_user_started" ON "session_recordings" ("user_id", "started_at"); -CREATE INDEX IF NOT EXISTS "idx_session_recordings_host" ON "session_recordings" ("host_id"); -ALTER TABLE "session_recordings" RENAME TO "p_session_recording_session_recordings"; diff --git a/plugins/session-recording/migrations/snapshot.json b/plugins/session-recording/migrations/snapshot.json deleted file mode 100644 index add1a0836..000000000 --- a/plugins/session-recording/migrations/snapshot.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "version": 1, - "tables": { - "session_recordings": { - "columns": { - "id": { - "type": "id", - "primaryKey": true, - "notNull": true - }, - "hostId": { - "type": "refHost", - "notNull": true - }, - "userId": { - "type": "varchar", - "length": 255 - }, - "username": { - "type": "text" - }, - "accessId": { - "type": "integer" - }, - "startedAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - }, - "endedAt": { - "type": "timestamp" - }, - "duration": { - "type": "integer" - }, - "commands": { - "type": "text" - }, - "dangerousActions": { - "type": "text" - }, - "recordingPath": { - "type": "text" - }, - "protocol": { - "type": "text", - "notNull": true, - "defaultValue": "ssh" - }, - "format": { - "type": "text", - "notNull": true, - "defaultValue": "text" - }, - "terminatedByOwner": { - "type": "boolean", - "defaultValue": false - }, - "terminationReason": { - "type": "text" - } - }, - "indexes": [ - { - "name": "idx_session_recordings_user_started", - "columns": ["userId", "startedAt"] - }, - { - "name": "idx_session_recordings_host", - "columns": ["hostId"] - } - ], - "adopts": "session_recordings" - } - } -} diff --git a/plugins/session-recording/migrations/sqlite/0001_adopt_session_recordings.sql b/plugins/session-recording/migrations/sqlite/0001_adopt_session_recordings.sql deleted file mode 100644 index 93f9ba02f..000000000 --- a/plugins/session-recording/migrations/sqlite/0001_adopt_session_recordings.sql +++ /dev/null @@ -1,24 +0,0 @@ --- session-recording 0001: adopt_session_recordings --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS "session_recordings" ( - "id" integer PRIMARY KEY AUTOINCREMENT, - "host_id" integer NOT NULL, - "user_id" text, - "username" text, - "access_id" integer, - "started_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "ended_at" text, - "duration" integer, - "commands" text, - "dangerous_actions" text, - "recording_path" text, - "protocol" text NOT NULL DEFAULT 'ssh', - "format" text NOT NULL DEFAULT 'text', - "terminated_by_owner" integer DEFAULT 0, - "termination_reason" text, - FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS "idx_session_recordings_user_started" ON "session_recordings" ("user_id", "started_at"); -CREATE INDEX IF NOT EXISTS "idx_session_recordings_host" ON "session_recordings" ("host_id"); -ALTER TABLE "session_recordings" RENAME TO "p_session_recording_session_recordings"; diff --git a/plugins/session-recording/package.json b/plugins/session-recording/package.json deleted file mode 100644 index ce4fb7e10..000000000 --- a/plugins/session-recording/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@termix-plugin/session-recording", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "build": "termix-plugin build", - "test": "termix-plugin test", - "typecheck": "tsc -p .", - "validate": "termix-plugin validate" - }, - "dependencies": { - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", - "guacamole-common-js": "^1.5.0", - "lucide-react": "^1.33.0" - }, - "devDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "@types/node": "^26.2.0", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", - "better-sqlite3": "^13.0.3", - "jsdom": "^30.0.1", - "vitest": "^4.1.11" - }, - "peerDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "drizzle-orm": "^0.45.2", - "express": "^5.2.1", - "react": "^19.2.8" - }, - "termix": { - "patches": [ - "xterm-android-ime", - "guacamole-common-js-flush" - ] - } -} diff --git a/plugins/session-recording/src/backend/files.ts b/plugins/session-recording/src/backend/files.ts deleted file mode 100644 index 32da411a8..000000000 --- a/plugins/session-recording/src/backend/files.ts +++ /dev/null @@ -1,39 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; - -export function isInside(file: string, dir: string): boolean { - return path.resolve(file).startsWith(`${path.resolve(dir)}${path.sep}`); -} - -/** - * Moves a finished recording someone else wrote (guacd's, in remote - * desktop's recordings folder) under this plugin's data folder, which is the - * only place playback and retention read from. Returns where it ended up; - * a file that is not there is left alone. - */ -export async function adoptRecordingFile( - source: string, - dataDir: string, -): Promise { - if (isInside(source, dataDir)) return source; - const target = path.join( - dataDir, - "session_recordings", - path.basename(path.dirname(source)), - path.basename(source), - ); - try { - await fs.access(source); - } catch { - return source; - } - await fs.mkdir(path.dirname(target), { recursive: true }); - try { - await fs.rename(source, target); - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== "EXDEV") throw error; - await fs.copyFile(source, target); - await fs.unlink(source); - } - return target; -} diff --git a/plugins/session-recording/src/backend/index.ts b/plugins/session-recording/src/backend/index.ts deleted file mode 100644 index 8df126870..000000000 --- a/plugins/session-recording/src/backend/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Router } from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { sessionRecordings } from "./tables.js"; -import { createSessionRecordingRepository } from "./repository.js"; -import { createRecordingsWriter, type RecordingsWriterV1 } from "./service.js"; -import { registerSessionRecordingRoutes } from "./routes.js"; -import { startRetentionSweep } from "./retention.js"; - -export type { RecordingsWriterV1, RecordingSink } from "./service.js"; - -export async function activate(ctx: PluginContext) { - const table = await ctx.db.define(sessionRecordings); - const repository = createSessionRecordingRepository(ctx.db, table, ctx.hosts); - - ctx.http - .router() - .use( - registerSessionRecordingRoutes(ctx, repository, () => - ctx.files.dataDir(), - ), - ); - - ctx.services.provide( - "recordings.writer", - createRecordingsWriter(ctx, repository), - ); - - // A recording is evidence about the host as much as the person, so it - // outlives the account: the row's userId is cleared rather than cascading. - ctx.events.on("user.deleted", (payload) => { - const { userId } = payload as { userId: string }; - void repository.anonymizeByUserId(userId); - }); - - startRetentionSweep(ctx, repository, () => ctx.files.dataDir()); - - ctx.log.info( - "Session Recording routes mounted at /plugin-api/session-recording", - ); -} - -export async function deactivate() {} diff --git a/plugins/session-recording/src/backend/repository.ts b/plugins/session-recording/src/backend/repository.ts deleted file mode 100644 index 7a3779f68..000000000 --- a/plugins/session-recording/src/backend/repository.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { and, desc, eq, inArray, lt } from "drizzle-orm"; -import type { PluginDatabase, PluginHosts } from "@termix/plugin-sdk/backend"; - -/* eslint-disable @typescript-eslint/no-explicit-any */ -// The table comes from ctx.db.define, which the SDK hands back untyped, and -// the drizzle handle is the server's own. Typed at this module's edge instead. -type Table = any; -type Drizzle = any; -/* eslint-enable @typescript-eslint/no-explicit-any */ - -export interface SessionRecordingRecord { - id: number; - hostId: number; - userId: string | null; - username: string | null; - startedAt: string; - endedAt: string | null; - duration: number | null; - recordingPath: string | null; - protocol: string; - format: string | null; - terminatedByOwner: boolean | null; - terminationReason: string | null; -} - -export interface SessionRecordingListRecord { - id: number; - hostId: number; - userId: string | null; - startedAt: string; - endedAt: string | null; - duration: number | null; - recordingPath: string | null; - protocol: string; - format: string | null; - username: string | null; - hostName: string | null; - hostIp: string | null; -} - -export interface SessionRecordingCreateInput { - hostId: number; - userId: string; - username?: string | null; - startedAt: string; - endedAt?: string | null; - duration?: number | null; - recordingPath?: string | null; - protocol?: string | null; - format?: string | null; - terminatedByOwner?: boolean | null; - terminationReason?: string | null; -} - -export type SessionRecordingRepository = ReturnType< - typeof createSessionRecordingRepository ->; - -/** - * Written without RETURNING so it runs the same on all three engines: an - * insert is read back by a marker key it was given, an update by its id. - */ -export function createSessionRecordingRepository( - db: PluginDatabase, - table: Table, - hosts: PluginHosts, -) { - const client = () => db.client(); - - async function usernameFor(userId: string): Promise { - const { users } = await db.refs<{ users: Table }>(); - const drizzle = await client(); - const rows = await drizzle - .select({ username: users.username }) - .from(users) - .where(eq(users.id, userId)); - return rows[0]?.username ?? null; - } - - async function withHostNames( - rows: SessionRecordingListRecord[], - ): Promise { - const list = await hosts.list(); - const byId = new Map(list.map((host) => [host.id, host])); - return rows.map((row) => { - const host = byId.get(row.hostId); - return { ...row, hostName: host?.name ?? null, hostIp: host?.ip ?? null }; - }); - } - - return { - usernameFor, - - /** - * Inserts a row and returns it. recordingPath is the caller's read-back - * key: every caller sets it to something unique (the file path or a - * session id) before insert, since MySQL has no RETURNING to read a new - * id back with. - */ - async create( - input: SessionRecordingCreateInput & { recordingPath: string }, - ): Promise { - const drizzle = await client(); - await drizzle.insert(table).values(input); - const rows = await drizzle - .select() - .from(table) - .where(eq(table.recordingPath, input.recordingPath)) - .orderBy(desc(table.id)) - .limit(1); - await db.persist(); - return rows[0]; - }, - - async updateEnded( - id: number, - input: { - endedAt: string; - duration: number | null; - terminatedByOwner?: boolean; - terminationReason?: string | null; - }, - ): Promise { - const drizzle = await client(); - await drizzle.update(table).set(input).where(eq(table.id, id)); - await db.persist(); - }, - - async findByIdForUser( - userId: string, - id: number, - ): Promise { - const drizzle = await client(); - const rows = await drizzle - .select() - .from(table) - .where(and(eq(table.id, id), eq(table.userId, userId))) - .limit(1); - return rows[0] ?? null; - }, - - async findPathByIdForUser( - userId: string, - id: number, - ): Promise<{ - id: number; - recordingPath: string | null; - format: string | null; - } | null> { - const drizzle = await client(); - const rows = await drizzle - .select({ - id: table.id, - recordingPath: table.recordingPath, - format: table.format, - }) - .from(table) - .where(and(eq(table.id, id), eq(table.userId, userId))) - .limit(1); - return rows[0] ?? null; - }, - - async listByUserIdWithHost( - userId: string, - ): Promise { - const drizzle = await client(); - const rows = (await drizzle - .select() - .from(table) - .where(eq(table.userId, userId)) - .orderBy(desc(table.startedAt))) as SessionRecordingListRecord[]; - return withHostNames(rows); - }, - - async listPathsOlderThan( - cutoff: string, - ): Promise> { - const drizzle = await client(); - return drizzle - .select({ id: table.id, recordingPath: table.recordingPath }) - .from(table) - .where(lt(table.startedAt, cutoff)); - }, - - async deleteForUser(userId: string, id: number): Promise { - const drizzle = await client(); - const existing = await drizzle - .select({ id: table.id }) - .from(table) - .where(and(eq(table.id, id), eq(table.userId, userId))) - .limit(1); - if (existing.length === 0) return false; - await drizzle - .delete(table) - .where(and(eq(table.id, id), eq(table.userId, userId))); - await db.persist(); - return true; - }, - - async deleteById(id: number): Promise { - const drizzle = await client(); - const existing = await drizzle - .select({ id: table.id }) - .from(table) - .where(eq(table.id, id)) - .limit(1); - if (existing.length === 0) return false; - await drizzle.delete(table).where(eq(table.id, id)); - await db.persist(); - return true; - }, - - async deleteByHostId(hostId: number): Promise { - const drizzle = await client(); - await drizzle.delete(table).where(eq(table.hostId, hostId)); - await db.persist(); - }, - - async deleteByHostIds(hostIds: number[]): Promise { - if (hostIds.length === 0) return; - const drizzle = await client(); - await drizzle.delete(table).where(inArray(table.hostId, hostIds)); - await db.persist(); - }, - - /** - * Detaches recordings from a user being deleted instead of removing them. - * A recording is evidence about the host as much as about the person, and - * the file stays on disk regardless of the row. - */ - async anonymizeByUserId(userId: string): Promise { - const drizzle = await client(); - await drizzle - .update(table) - .set({ userId: null }) - .where(eq(table.userId, userId)); - await db.persist(); - }, - }; -} diff --git a/plugins/session-recording/src/backend/retention.ts b/plugins/session-recording/src/backend/retention.ts deleted file mode 100644 index df10b49f7..000000000 --- a/plugins/session-recording/src/backend/retention.ts +++ /dev/null @@ -1,64 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import type { SessionRecordingRepository } from "./repository.js"; - -const DEFAULT_RETENTION_DAYS = 30; -const PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000; - -async function getRetentionDays(ctx: PluginContext): Promise { - const configured = await ctx.settings.get("retentionDays"); - if (typeof configured === "number" && configured >= 1 && configured <= 3650) { - return configured; - } - return DEFAULT_RETENTION_DAYS; -} - -async function pruneOldRecordings( - ctx: PluginContext, - repository: SessionRecordingRepository, - dataDir: () => Promise, -): Promise { - try { - const retentionDays = await getRetentionDays(ctx); - const cutoff = new Date( - Date.now() - retentionDays * 24 * 60 * 60 * 1000, - ).toISOString(); - const old = await repository.listPathsOlderThan(cutoff); - if (old.length === 0) return; - - const base = `${path.resolve(await dataDir())}${path.sep}`; - for (const row of old) { - if (row.recordingPath) { - const resolved = path.resolve(row.recordingPath); - if (resolved.startsWith(base)) { - await fs.unlink(resolved).catch(() => {}); - } - } - await repository.deleteById(row.id); - } - - ctx.log.info(`Pruned ${old.length} old session recording(s)`); - } catch (error) { - ctx.log.warn( - `Failed to prune old session recordings: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } -} - -/** Runs the retention sweep at startup and every 24 hours. */ -export function startRetentionSweep( - ctx: PluginContext, - repository: SessionRecordingRepository, - dataDir: () => Promise, -): void { - void pruneOldRecordings(ctx, repository, dataDir); - const timer = setInterval( - () => void pruneOldRecordings(ctx, repository, dataDir), - PRUNE_INTERVAL_MS, - ); - timer.unref?.(); - ctx.disposables.add(() => clearInterval(timer)); -} diff --git a/plugins/session-recording/src/backend/routes.ts b/plugins/session-recording/src/backend/routes.ts deleted file mode 100644 index 705ec4c0b..000000000 --- a/plugins/session-recording/src/backend/routes.ts +++ /dev/null @@ -1,253 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import express, { type Request, type Response, type Router } from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import type { SessionRecordingRepository } from "./repository.js"; - -function actorId(ctx: PluginContext): string | undefined { - return ctx.currentActor(); -} - -/** - * @openapi - * /plugin-api/session-recording/: - * get: - * summary: List session recordings - * description: Returns every terminal session recording for the authenticated user. - * tags: - * - Session Recording - * responses: - * 200: - * description: List of session recordings. - * 500: - * description: Failed to fetch session recordings. - */ -function registerList( - router: Router, - ctx: PluginContext, - repository: SessionRecordingRepository, -): void { - router.get("/", async (_req: Request, res: Response) => { - const userId = actorId(ctx); - if (!userId) return res.status(401).json({ error: "Not authenticated" }); - try { - const rows = await repository.listByUserIdWithHost(userId); - const records = rows.map((row) => { - let sizeBytes: number | null = null; - if (row.recordingPath) { - try { - sizeBytes = fs.statSync(row.recordingPath).size; - } catch { - // file may have been removed - } - } - return { ...row, sizeBytes }; - }); - res.json({ logs: records }); - } catch (error) { - ctx.log.error( - "Failed to fetch session recordings", - error instanceof Error ? error : new Error(String(error)), - ); - res.status(500).json({ error: "Failed to fetch session recordings" }); - } - }); -} - -/** - * @openapi - * /plugin-api/session-recording/{id}: - * get: - * summary: Get session recording metadata - * description: Returns metadata for a single session recording. - * tags: - * - Session Recording - * parameters: - * - in: path - * name: id - * required: true - * schema: { type: integer } - * responses: - * 200: - * description: Session recording metadata. - * 404: - * description: Session recording not found. - * 500: - * description: Failed to fetch session recording. - */ -function registerGet( - router: Router, - ctx: PluginContext, - repository: SessionRecordingRepository, -): void { - router.get("/:id", async (req: Request, res: Response) => { - const userId = actorId(ctx); - if (!userId) return res.status(401).json({ error: "Not authenticated" }); - const rawId = Array.isArray(req.params.id) - ? req.params.id[0] - : req.params.id; - const id = parseInt(rawId, 10); - if (isNaN(id)) return res.status(400).json({ error: "Invalid id" }); - - try { - const row = await repository.findByIdForUser(userId, id); - if (!row) return res.status(404).json({ error: "Not found" }); - res.json({ log: row }); - } catch (error) { - ctx.log.error( - "Failed to fetch session recording", - error instanceof Error ? error : new Error(String(error)), - ); - res.status(500).json({ error: "Failed to fetch session recording" }); - } - }); -} - -/** - * @openapi - * /plugin-api/session-recording/{id}/content: - * get: - * summary: Get session recording content - * description: Returns the raw content of a session recording file. - * tags: - * - Session Recording - * parameters: - * - in: path - * name: id - * required: true - * schema: { type: integer } - * responses: - * 200: - * description: Raw recording content. - * 404: - * description: Session recording or file not found. - * 500: - * description: Failed to read session recording. - */ -function registerContent( - router: Router, - ctx: PluginContext, - repository: SessionRecordingRepository, - dataDir: () => Promise, -): void { - router.get("/:id/content", async (req: Request, res: Response) => { - const userId = actorId(ctx); - if (!userId) return res.status(401).json({ error: "Not authenticated" }); - const rawId = Array.isArray(req.params.id) - ? req.params.id[0] - : req.params.id; - const id = parseInt(rawId, 10); - if (isNaN(id)) return res.status(400).json({ error: "Invalid id" }); - - try { - const row = await repository.findPathByIdForUser(userId, id); - if (!row) return res.status(404).json({ error: "Not found" }); - - const filePath = row.recordingPath; - if (!filePath) - return res.status(404).json({ error: "No recording file" }); - - const resolvedPath = path.resolve(filePath); - const base = `${path.resolve(await dataDir())}${path.sep}`; - if (!resolvedPath.startsWith(base)) { - return res.status(403).json({ error: "Forbidden" }); - } - if (!fs.existsSync(resolvedPath)) { - return res.status(404).json({ error: "File not found" }); - } - - const content = await fs.promises.readFile(resolvedPath); - const format = - row.format ?? (filePath.endsWith(".cast") ? "asciicast" : "text"); - const contentType = - format === "guacamole" - ? "application/vnd.apache.guacamole.recording" - : format === "asciicast" - ? "application/x-asciicast" - : "text/plain"; - res.type(contentType).send(content); - } catch (error) { - ctx.log.error( - "Failed to read session recording content", - error instanceof Error ? error : new Error(String(error)), - ); - res.status(500).json({ error: "Failed to read session recording" }); - } - }); -} - -/** - * @openapi - * /plugin-api/session-recording/{id}: - * delete: - * summary: Delete session recording - * description: Deletes a session recording and its file. - * tags: - * - Session Recording - * parameters: - * - in: path - * name: id - * required: true - * schema: { type: integer } - * responses: - * 200: - * description: Session recording deleted. - * 404: - * description: Session recording not found. - * 500: - * description: Failed to delete session recording. - */ -function registerDelete( - router: Router, - ctx: PluginContext, - repository: SessionRecordingRepository, - dataDir: () => Promise, -): void { - router.delete("/:id", async (req: Request, res: Response) => { - const userId = actorId(ctx); - if (!userId) return res.status(401).json({ error: "Not authenticated" }); - const rawId = Array.isArray(req.params.id) - ? req.params.id[0] - : req.params.id; - const id = parseInt(rawId, 10); - if (isNaN(id)) return res.status(400).json({ error: "Invalid id" }); - - try { - const row = await repository.findPathByIdForUser(userId, id); - if (!row) return res.status(404).json({ error: "Not found" }); - - const filePath = row.recordingPath; - const deleted = await repository.deleteForUser(userId, id); - if (!deleted) return res.status(404).json({ error: "Not found" }); - - if (filePath) { - const resolvedPath = path.resolve(filePath); - const base = `${path.resolve(await dataDir())}${path.sep}`; - if (resolvedPath.startsWith(base) && fs.existsSync(resolvedPath)) { - await fs.promises.unlink(resolvedPath).catch(() => {}); - } - } - - res.json({ success: true }); - } catch (error) { - ctx.log.error( - "Failed to delete session recording", - error instanceof Error ? error : new Error(String(error)), - ); - res.status(500).json({ error: "Failed to delete session recording" }); - } - }); -} - -export function registerSessionRecordingRoutes( - ctx: PluginContext, - repository: SessionRecordingRepository, - dataDir: () => Promise, -): Router { - const router = express.Router(); - registerList(router, ctx, repository); - registerGet(router, ctx, repository); - registerContent(router, ctx, repository, dataDir); - registerDelete(router, ctx, repository, dataDir); - return router; -} diff --git a/plugins/session-recording/src/backend/service.ts b/plugins/session-recording/src/backend/service.ts deleted file mode 100644 index 73e313e72..000000000 --- a/plugins/session-recording/src/backend/service.ts +++ /dev/null @@ -1,133 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import type { SessionRecordingRepository } from "./repository.js"; -import { adoptRecordingFile } from "./files.js"; - -/** - * recordings.writer v1, the service ssh-terminal calls to record sessions. - * 1.1 added enabledFor, for a caller that records outside this plugin. - * - * One call per already-batched chunk (ssh-terminal coalesces on a 300ms - * trailing edge, issue #1049): mkdir and writeFile on the first append, - * appendFile after that, never per chunk. - */ -export interface RecordingSink { - append: (chunk: string) => Promise; - persist: (summary: { - endedAt: number; - durationSeconds: number; - terminatedByOwner: boolean; - terminationReason: string | null; - }) => Promise; - discard: () => void; -} - -export interface RecordingsWriterV1 { - /** - * Whether the host's recording switch is on, for a caller that has to set - * up recording before a session exists (remote desktop tells guacd). - */ - enabledFor: (hostId: number) => Promise; - /** Null when recording is off for this user or host. */ - open: (meta: { - sessionId: string; - hostId: number; - userId: string; - protocol: "ssh"; - format: "asciicast"; - startedAt: number; - }) => Promise; - /** - * Inserts a row for a recording a caller already wrote to disk itself - * (remote desktop's guacd recordings), rather than streaming through - * open()/append(). The caller already decided to record. The file is - * moved under this plugin's data folder, where playback reads it. - */ - createFinished: (input: { - hostId: number; - userId: string; - startedAt: string; - endedAt: string; - duration: number | null; - recordingPath: string; - protocol: string; - format: string; - }) => Promise<{ id: number }>; -} - -export function createRecordingsWriter( - ctx: PluginContext, - repository: SessionRecordingRepository, -): RecordingsWriterV1 { - const enabledFor = async (hostId: number) => - (await ctx.settings.getHost(hostId, "enableSessionRecording")) !== - false; - - return { - enabledFor, - - async open(meta) { - if (!(await enabledFor(meta.hostId))) return null; - - const dataDir = await ctx.files.dataDir(); - const dir = path.join(dataDir, "session_logs", meta.userId); - const filePath = path.join(dir, `${meta.sessionId}.cast`); - let wroteFirst = false; - let discarded = false; - - const username = await repository.usernameFor(meta.userId); - const row = await repository.create({ - hostId: meta.hostId, - userId: meta.userId, - username, - startedAt: new Date(meta.startedAt).toISOString(), - recordingPath: filePath, - protocol: meta.protocol, - format: meta.format, - }); - - return { - async append(chunk: string) { - if (discarded) return; - if (!wroteFirst) { - await fs.mkdir(dir, { recursive: true }); - await fs.writeFile(filePath, chunk); - wroteFirst = true; - } else { - await fs.appendFile(filePath, chunk); - } - }, - - async persist(summary) { - if (discarded) return; - await repository.updateEnded(row.id, { - endedAt: new Date(summary.endedAt).toISOString(), - duration: summary.durationSeconds, - terminatedByOwner: summary.terminatedByOwner, - terminationReason: summary.terminationReason, - }); - }, - - discard() { - discarded = true; - void repository.deleteById(row.id).catch(() => {}); - }, - }; - }, - - async createFinished(input) { - const username = await repository.usernameFor(input.userId); - const recordingPath = await adoptRecordingFile( - input.recordingPath, - await ctx.files.dataDir(), - ).catch(() => input.recordingPath); - const row = await repository.create({ - ...input, - recordingPath, - username, - }); - return { id: row.id }; - }, - }; -} diff --git a/plugins/session-recording/src/backend/tables.ts b/plugins/session-recording/src/backend/tables.ts deleted file mode 100644 index bb32940a6..000000000 --- a/plugins/session-recording/src/backend/tables.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - adoptLegacyTable, - boolean, - defineTable, - id, - integer, - refHost, - text, - timestamp, - varchar, -} from "@termix/plugin-sdk/db"; - -/** - * userId is a plain column, not refUser(): a recording is evidence about the - * host as much as the person, so it outlives the account. The plugin listens - * for user.deleted and clears userId itself instead of cascading. username - * keeps an anonymized row attributable. - */ -export const sessionRecordings = adoptLegacyTable( - "session_recordings", - defineTable( - "session_recordings", - { - id: id(), - hostId: refHost(), - userId: varchar(), - username: text(), - accessId: integer(), - startedAt: timestamp().notNull().defaultNow(), - endedAt: timestamp(), - duration: integer(), - commands: text(), - dangerousActions: text(), - recordingPath: text(), - protocol: text().notNull().default("ssh"), - format: text().notNull().default("text"), - terminatedByOwner: boolean().default(false), - terminationReason: text(), - }, - { - indexes: [ - { - name: "idx_session_recordings_user_started", - columns: ["userId", "startedAt"], - }, - { name: "idx_session_recordings_host", columns: ["hostId"] }, - ], - }, - ), -); - -export const tables = [sessionRecordings]; diff --git a/plugins/session-recording/src/frontend/RecordingStatus.tsx b/plugins/session-recording/src/frontend/RecordingStatus.tsx deleted file mode 100644 index c8ee32dbe..000000000 --- a/plugins/session-recording/src/frontend/RecordingStatus.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Circle } from "lucide-react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; - -/** What ssh-terminal hands a "terminal.toolbarStatus" component. */ -interface TerminalToolbarStatusProps { - host?: { pluginSettings?: Record> }; - isConnected?: boolean; -} - -export function RecordingStatus(props: Record) { - const { isConnected } = props as TerminalToolbarStatusProps; - const { t } = useTranslation(); - if (!isConnected) return null; - - return ( -
- - {t("terminalStatus.recording")} -
- ); -} diff --git a/plugins/session-recording/src/frontend/SessionLogsPanel.tsx b/plugins/session-recording/src/frontend/SessionLogsPanel.tsx deleted file mode 100644 index 857a241da..000000000 --- a/plugins/session-recording/src/frontend/SessionLogsPanel.tsx +++ /dev/null @@ -1,632 +0,0 @@ -import { - useState, - useEffect, - useCallback, - useMemo, - useRef, - type ReactNode, -} from "react"; -import { - useTranslation, - useToast, - usePluginApi, - usePermission, -} from "@termix/plugin-sdk/frontend"; -import { - Input, - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, - copyToClipboard, -} from "@termix/plugin-sdk/ui"; -import { - ArrowLeft, - Check, - Copy, - Download, - Eye, - FileText, - Loader2, - ScrollText, - Search, - X, -} from "lucide-react"; -import { SessionRecordingPlayer } from "./SessionRecordingPlayer"; -import { asciicastToPlainText, parseAsciicast } from "./asciicast"; -import { - createSessionRecordingApi, - type SessionLogRecord, -} from "./session-recording-api"; - -function useAdaptivePolling( - fn: () => Promise, - options: { minIntervalMs: number; maxIntervalMs: number }, - enabled: boolean, -) { - const fnRef = useRef(fn); - fnRef.current = fn; - - useEffect(() => { - if (!enabled) return; - let interval = options.minIntervalMs; - let stable = 0; - let cancelled = false; - let timer: ReturnType; - - const tick = async () => { - if (cancelled) return; - try { - const changed = await fnRef.current(); - if (changed) { - interval = options.minIntervalMs; - stable = 0; - } else { - stable++; - if (stable >= 3) { - interval = Math.min(interval * 1.5, options.maxIntervalMs); - } - } - } catch { - // keep polling - } - if (!cancelled) timer = setTimeout(tick, interval); - }; - - timer = setTimeout(tick, interval); - return () => { - cancelled = true; - clearTimeout(timer); - }; - }, [enabled, options.minIntervalMs, options.maxIntervalMs]); -} - -function formatDuration(seconds: number | null): string { - if (seconds == null) return "--"; - if (seconds < 60) return `${seconds}s`; - const m = Math.floor(seconds / 60); - const s = seconds % 60; - return s > 0 ? `${m}m ${s}s` : `${m}m`; -} - -function formatBytes(bytes: number | null): string { - if (bytes == null || bytes === 0) return "--"; - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; -} - -function formatDate(iso: string): string { - const d = new Date(iso); - return d.toLocaleString(undefined, { - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); -} - -function buildFilename(log: SessionLogRecord): string { - const host = (log.hostName ?? log.hostIp ?? "session") - .replace(/[^a-zA-Z0-9._-]/g, "_") - .slice(0, 40); - const d = new Date(log.startedAt); - const y = d.getFullYear(); - const m = String(d.getMonth() + 1).padStart(2, "0"); - const day = String(d.getDate()).padStart(2, "0"); - const h = String(d.getHours()).padStart(2, "0"); - const min = String(d.getMinutes()).padStart(2, "0"); - const s = String(d.getSeconds()).padStart(2, "0"); - const extension = - log.format === "guacamole" - ? "guac" - : log.format === "asciicast" - ? "cast" - : "log"; - return `${host}_${y}-${m}-${day}_${h}-${min}-${s}.${extension}`; -} - -function buildTextFilename(log: SessionLogRecord): string { - return buildFilename(log).replace(/\.(cast|guac|log)$/, ".txt"); -} - -async function extractPlainText( - log: SessionLogRecord, - blob: Blob, -): Promise { - if (log.format === "guacamole") return null; - const source = await blob.text(); - if (log.format === "text") return source; - return asciicastToPlainText(parseAsciicast(source)); -} - -function SectionHeader({ label, count }: { label: string; count: number }) { - return ( -
- - {label} - - - {count} - -
- ); -} - -function LogMeta({ - items, - className, -}: { - items: (string | null | undefined)[]; - className?: string; -}) { - return ( - - {items.filter(Boolean).map((item, i) => ( - - {item} - - ))} - - ); -} - -const ACTION_BUTTON = - "size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"; - -function ActionButton({ - label, - onClick, - destructive, - children, -}: { - label: string; - onClick: () => void; - destructive?: boolean; - children: ReactNode; -}) { - return ( - - - - - - {label} - - - ); -} - -function LogRow({ - log, - onView, - onDownload, - onDownloadText, - onDelete, -}: { - log: SessionLogRecord; - onView: () => void; - onDownload: () => void; - onDownloadText: () => void; - onDelete: () => void; -}) { - const { t } = useTranslation(); - const hostLabel = - log.hostName ?? - log.hostIp ?? - t("sessionLogs.hostFallback", { id: log.hostId }); - - return ( -
-
- -
- -
- - {hostLabel} - - -
- - -
- - - - {log.format === "asciicast" && ( - - - - )} - - - - - - -
-
-
- ); -} - -export function SessionLogsPanel() { - const { t } = useTranslation(); - const toast = useToast(); - const pluginApi = usePluginApi(); - const api = useMemo(() => createSessionRecordingApi(pluginApi), [pluginApi]); - const canView = usePermission("view"); - const [logs, setLogs] = useState([]); - const [loading, setLoading] = useState(true); - const [filter, setFilter] = useState(""); - const [viewLog, setViewLog] = useState(null); - const [viewContent, setViewContent] = useState(""); - const [viewBlob, setViewBlob] = useState(null); - const [viewText, setViewText] = useState(null); - const [viewLoading, setViewLoading] = useState(false); - const [deleteTarget, setDeleteTarget] = useState( - null, - ); - const [deleting, setDeleting] = useState(false); - const [copied, setCopied] = useState(false); - const logsRef = useRef(logs); - logsRef.current = logs; - - const load = useCallback( - async (initial = false) => { - if (initial) setLoading(true); - try { - const fresh = await api.list(); - const changed = - logsRef.current.length !== fresh.length || - logsRef.current.some((log, i) => log.id !== fresh[i]?.id); - if (changed) { - logsRef.current = fresh; - setLogs(fresh); - } - return changed; - } catch (error) { - if (initial) { - toast.error(t("sessionLogs.loadError")); - return false; - } - throw error; - } finally { - if (initial) setLoading(false); - } - }, - [api, t, toast], - ); - - useEffect(() => { - if (!canView) return; - void load(true); - }, [canView, load]); - - useAdaptivePolling( - () => load(false), - { - minIntervalMs: 5_000, - maxIntervalMs: 30_000, - }, - canView, - ); - - const q = filter.trim().toLowerCase(); - const filtered = q - ? logs.filter((l) => - `${l.hostName ?? ""} ${l.hostIp ?? ""} ${l.username ?? ""} ${l.protocol}` - .toLowerCase() - .includes(q), - ) - : logs; - - const handleView = async (log: SessionLogRecord) => { - setViewLog(log); - setViewContent(""); - setViewBlob(null); - setViewText(null); - setViewLoading(true); - try { - if (log.format === "text") { - const text = await api.getContent(log.id); - setViewContent(text); - setViewText(text); - } else { - const blob = await api.getBlob(log.id); - setViewBlob(blob); - setViewText(await extractPlainText(log, blob)); - } - } catch { - toast.error(t("sessionLogs.loadError")); - } finally { - setViewLoading(false); - } - }; - - const downloadBlob = (blob: Blob, filename: string) => { - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); - }; - - const handleDownload = async (log: SessionLogRecord) => { - try { - downloadBlob(await api.getBlob(log.id), buildFilename(log)); - } catch { - toast.error(t("sessionLogs.loadError")); - } - }; - - const handleDownloadText = async (log: SessionLogRecord) => { - try { - const text = - log.id === viewLog?.id && viewText != null - ? viewText - : await extractPlainText(log, await api.getBlob(log.id)); - if (text == null) { - toast.error(t("sessionLogs.loadError")); - return; - } - downloadBlob( - new Blob([text], { type: "text/plain" }), - buildTextFilename(log), - ); - } catch { - toast.error(t("sessionLogs.loadError")); - } - }; - - const handleDelete = async () => { - if (!deleteTarget) return; - setDeleting(true); - try { - await api.delete(deleteTarget.id); - setLogs((prev) => prev.filter((l) => l.id !== deleteTarget.id)); - setDeleteTarget(null); - } catch { - toast.error(t("sessionLogs.deleteError")); - } finally { - setDeleting(false); - } - }; - - const handleCopy = async () => { - if (!viewText) return; - const ok = await copyToClipboard(viewText); - if (ok) { - setCopied(true); - setTimeout(() => setCopied(false), 1500); - } else { - toast.error(t("common.copyFailed")); - } - }; - - if (loading) { - return ( -
- -
- ); - } - - // Inline log viewer - if (viewLog) { - const hostLabel = - viewLog.hostName ?? - viewLog.hostIp ?? - t("sessionLogs.hostFallback", { id: viewLog.hostId }); - return ( -
- {/* Viewer header */} -
- -
- - {hostLabel} - - -
- -
- {viewText != null && ( - - {copied ? ( - - ) : ( - - )} - - )} - {viewLog.format === "asciicast" && ( - handleDownloadText(viewLog)} - > - - - )} - handleDownload(viewLog)} - > - - -
-
-
- - {/* Log content */} -
- {viewLoading ? ( -
- -
- ) : viewBlob ? ( - - ) : ( -
-              {viewContent || t("sessionLogs.empty")}
-            
- )} -
-
- ); - } - - return ( -
-
- {logs.length === 0 ? ( -
-
- -
-
- - {t("sessionLogs.noLogs")} - - - {t("sessionLogs.noLogsDesc")} - -
-
- ) : ( - <> -
- - setFilter(e.target.value)} - className="pl-8 h-7 text-xs" - /> -
- - {filtered.length === 0 ? ( -
- - {t("sessionLogs.noResults", { query: filter })} - -
- ) : ( -
- - {filtered.map((log) => ( - handleView(log)} - onDownload={() => handleDownload(log)} - onDownloadText={() => handleDownloadText(log)} - onDelete={() => setDeleteTarget(log)} - /> - ))} -
- )} - - )} -
- - {/* Inline delete confirmation - positioned against the relative parent in AppShell */} - {deleteTarget && ( -
-
-

- {t("sessionLogs.confirmDelete")} -

-
- - {deleteTarget.hostName ?? - deleteTarget.hostIp ?? - t("sessionLogs.sessionFallback", { id: deleteTarget.id })} - - {formatDate(deleteTarget.startedAt)} -
-
- - -
-
-
- )} -
- ); -} diff --git a/plugins/session-recording/src/frontend/SessionRecordingPlayer.tsx b/plugins/session-recording/src/frontend/SessionRecordingPlayer.tsx deleted file mode 100644 index be7642c0f..000000000 --- a/plugins/session-recording/src/frontend/SessionRecordingPlayer.tsx +++ /dev/null @@ -1,375 +0,0 @@ -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { Pause, Play } from "lucide-react"; -import { Terminal } from "@xterm/xterm"; -import Guacamole from "guacamole-common-js"; -import "@xterm/xterm/css/xterm.css"; -import { parseAsciicast, type Asciicast } from "./asciicast"; -import type { SessionLogRecord } from "./session-recording-api"; - -const SPEEDS = [0.5, 1, 2, 4]; - -function formatPosition(seconds: number) { - const minutes = Math.floor(seconds / 60); - return `${minutes}:${String(Math.floor(seconds % 60)).padStart(2, "0")}`; -} - -function getErrorMessage(error: unknown, fallback: string): string { - return error instanceof Error ? error.message : fallback; -} - -function SeekBar({ - position, - duration, - onSeek, -}: { - position: number; - duration: number; - onSeek: (position: number) => void; -}) { - const { t } = useTranslation(); - const barRef = useRef(null); - const [dragging, setDragging] = useState(false); - const progress = duration > 0 ? Math.min(position / duration, 1) : 0; - - const seekFromPointer = (clientX: number) => { - const rect = barRef.current?.getBoundingClientRect(); - if (!rect || rect.width === 0 || duration <= 0) return; - const ratio = Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1); - onSeek(ratio * duration); - }; - - return ( -
{ - event.currentTarget.setPointerCapture(event.pointerId); - setDragging(true); - seekFromPointer(event.clientX); - }} - onPointerMove={(event) => { - if (dragging) seekFromPointer(event.clientX); - }} - onPointerUp={(event) => { - event.currentTarget.releasePointerCapture(event.pointerId); - setDragging(false); - }} - onKeyDown={(event) => { - if (event.key === "ArrowLeft") onSeek(Math.max(position - 5, 0)); - if (event.key === "ArrowRight") - onSeek(Math.min(position + 5, duration)); - }} - > -
-
-
-
- ); -} - -function PlaybackControls({ - playing, - position, - duration, - speed, - onToggle, - onSeek, - onSpeed, -}: { - playing: boolean; - position: number; - duration: number; - speed: number; - onToggle: () => void; - onSeek: (position: number) => void; - onSpeed: (speed: number) => void; -}) { - const { t } = useTranslation(); - return ( -
- -
- - - {formatPosition(Math.min(position, duration))} /{" "} - {formatPosition(duration)} - -
- {SPEEDS.map((value) => ( - - ))} -
-
-
- ); -} - -function AsciicastPlayer({ blob }: { blob: Blob }) { - const containerRef = useRef(null); - const screenRef = useRef(null); - const terminalRef = useRef(null); - const recordingRef = useRef(null); - const positionRef = useRef(0); - const eventIndexRef = useRef(0); - const [position, setPosition] = useState(0); - const [duration, setDuration] = useState(0); - const [speed, setSpeed] = useState(1); - const [playing, setPlaying] = useState(false); - const [error, setError] = useState(""); - - const renderAt = useCallback((nextPosition: number) => { - const terminal = terminalRef.current; - const recording = recordingRef.current; - if (!terminal || !recording) return; - if (nextPosition < positionRef.current) { - terminal.reset(); - terminal.resize(recording.width, recording.height); - eventIndexRef.current = 0; - } - while (eventIndexRef.current < recording.events.length) { - const [time, type, data] = recording.events[eventIndexRef.current]; - if (time > nextPosition) break; - if (type === "o") terminal.write(data); - if (type === "r") { - const [cols, rows] = data.split("x").map(Number); - if (cols > 0 && rows > 0) terminal.resize(cols, rows); - } - eventIndexRef.current++; - } - positionRef.current = nextPosition; - setPosition(nextPosition); - }, []); - - useEffect(() => { - const container = containerRef.current; - const screen = screenRef.current; - if (!container || !screen) return; - const terminal = new Terminal({ - cursorBlink: false, - disableStdin: true, - convertEol: false, - fontSize: 12, - theme: { background: "#09090b" }, - }); - terminal.open(screen); - terminalRef.current = terminal; - // The recording keeps its own size, scaled down to fit the panel. - const fit = () => { - const width = screen.offsetWidth; - const height = screen.offsetHeight; - if (!width || !height) return; - const scale = Math.min( - 1, - container.clientWidth / width, - container.clientHeight / height, - ); - screen.style.transform = `scale(${scale})`; - }; - const observer = new ResizeObserver(fit); - observer.observe(container); - observer.observe(screen); - const resizeListener = terminal.onResize(() => requestAnimationFrame(fit)); - - blob - .text() - .then((source) => { - const recording = parseAsciicast(source); - recordingRef.current = recording; - setDuration(recording.duration); - terminal.resize(recording.width, recording.height); - renderAt(0); - }) - .catch((reason) => setError(getErrorMessage(reason, String(reason)))); - - return () => { - observer.disconnect(); - resizeListener.dispose(); - terminal.dispose(); - terminalRef.current = null; - }; - }, [blob, renderAt]); - - useEffect(() => { - if (!playing || !recordingRef.current) return; - let frame = 0; - let previous = performance.now(); - const tick = (now: number) => { - const next = Math.min( - duration, - positionRef.current + ((now - previous) / 1000) * speed, - ); - previous = now; - renderAt(next); - if (next >= duration) setPlaying(false); - else frame = requestAnimationFrame(tick); - }; - frame = requestAnimationFrame(tick); - return () => cancelAnimationFrame(frame); - }, [duration, playing, renderAt, speed]); - - if (error) return
{error}
; - return ( -
-
-
-
- { - if (!playing && position >= duration) renderAt(0); - setPlaying((value) => !value); - }} - onSeek={renderAt} - onSpeed={setSpeed} - /> -
- ); -} - -function GuacamolePlayer({ blob }: { blob: Blob }) { - const containerRef = useRef(null); - const recordingRef = useRef(null); - const positionRef = useRef(0); - const [position, setPosition] = useState(0); - const [duration, setDuration] = useState(0); - const [speed, setSpeed] = useState(1); - const [playing, setPlaying] = useState(false); - const [error, setError] = useState(""); - - useEffect(() => { - if (!containerRef.current) return; - const recording = new Guacamole.SessionRecording(blob, 100); - recordingRef.current = recording; - const display = recording.getDisplay(); - const element = display.getElement(); - containerRef.current.replaceChildren(element); - const fit = () => { - const width = display.getWidth(); - if (width && containerRef.current) { - display.scale(containerRef.current.clientWidth / width); - } - }; - const observer = new ResizeObserver(fit); - observer.observe(containerRef.current); - recording.onload = () => { - setDuration(recording.getDuration() / 1000); - fit(); - }; - recording.onprogress = (nextDuration) => setDuration(nextDuration / 1000); - recording.onseek = (nextPosition) => { - positionRef.current = nextPosition / 1000; - setPosition(positionRef.current); - }; - recording.onerror = setError; - - return () => { - observer.disconnect(); - recording.abort(); - recordingRef.current = null; - }; - }, [blob]); - - useEffect(() => { - const recording = recordingRef.current; - if (!recording || !playing) return; - recording.pause(); - let previous = performance.now(); - let nextPosition = positionRef.current; - const interval = window.setInterval(() => { - const now = performance.now(); - nextPosition = Math.min( - duration, - nextPosition + ((now - previous) / 1000) * speed, - ); - previous = now; - recording.seek(nextPosition * 1000); - if (nextPosition >= duration) setPlaying(false); - }, 100); - return () => clearInterval(interval); - }, [duration, playing, speed]); - - if (error) return
{error}
; - return ( -
-
- { - const recording = recordingRef.current; - if (!recording) return; - if (!playing && position >= duration) recording.seek(0); - setPlaying((value) => !value); - }} - onSeek={(nextPosition) => { - recordingRef.current?.seek(nextPosition * 1000); - positionRef.current = nextPosition; - setPosition(nextPosition); - }} - onSpeed={setSpeed} - /> -
- ); -} - -export function SessionRecordingPlayer({ - log, - blob, -}: { - log: SessionLogRecord; - blob: Blob; -}) { - return log.format === "guacamole" ? ( - - ) : ( - - ); -} diff --git a/plugins/session-recording/src/frontend/asciicast.ts b/plugins/session-recording/src/frontend/asciicast.ts deleted file mode 100644 index 62d872f96..000000000 --- a/plugins/session-recording/src/frontend/asciicast.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Terminal } from "@xterm/xterm"; - -export type AsciicastEvent = [ - time: number, - type: "i" | "o" | "r", - data: string, -]; - -export type Asciicast = { - width: number; - height: number; - duration: number; - events: AsciicastEvent[]; -}; - -export function parseAsciicast(source: string): Asciicast { - const lines = source.trim().split("\n"); - const header = JSON.parse(lines.shift() || "{}") as { - version?: number; - width?: number; - height?: number; - }; - if (header.version !== 2) throw new Error("Unsupported asciicast version"); - - const events = lines - .filter(Boolean) - .map((line) => JSON.parse(line) as AsciicastEvent) - .filter( - ([time, type, data]) => - Number.isFinite(time) && - (type === "i" || type === "o" || type === "r") && - typeof data === "string", - ); - - return { - width: header.width || 80, - height: header.height || 24, - duration: events.at(-1)?.[0] || 0, - events, - }; -} - -function writeAsync(terminal: Terminal, data: string): Promise { - return new Promise((resolve) => terminal.write(data, resolve)); -} - -// Replays the recording into an offscreen terminal and reads back its -// rendered buffer, so cursor movement/overwrites resolve the same way -// they would on screen instead of leaving raw ANSI escapes in the text. -export async function asciicastToPlainText( - recording: Asciicast, -): Promise { - const terminal = new Terminal({ - cols: recording.width, - rows: recording.height, - allowProposedApi: true, - }); - - try { - for (const [, type, data] of recording.events) { - if (type === "o") await writeAsync(terminal, data); - if (type === "r") { - const [cols, rows] = data.split("x").map(Number); - if (cols > 0 && rows > 0) terminal.resize(cols, rows); - } - } - - const buffer = terminal.buffer.active; - const lines: string[] = []; - for (let i = 0; i < buffer.length; i++) { - lines.push(buffer.getLine(i)?.translateToString(true) ?? ""); - } - while (lines.length > 0 && lines.at(-1) === "") lines.pop(); - return lines.join("\n"); - } finally { - terminal.dispose(); - } -} diff --git a/plugins/session-recording/src/frontend/index.tsx b/plugins/session-recording/src/frontend/index.tsx deleted file mode 100644 index 35b66413d..000000000 --- a/plugins/session-recording/src/frontend/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ScrollText } from "lucide-react"; -import type { TermixApp } from "@termix/plugin-sdk/frontend"; -import { SessionLogsPanel } from "./SessionLogsPanel"; -import { RecordingStatus } from "./RecordingStatus"; - -export function activate(app: TermixApp): void { - app.registerRailItem({ - id: "session-logs", - icon: ScrollText, - titleKey: "nav.sessionLogs", - kind: "panel", - promotable: true, - rightDockable: true, - separatorAfter: true, - permission: "view", - }); - - app.registerPanel("session-logs", () => ); - - app.registerTab("session-logs", () => , { - icon: ScrollText, - titleKey: "nav.sessionLogs", - hostless: true, - }); - - app.registerSlotContribution("terminal.toolbarStatus", { - actionId: "session-recording.terminalStatus", - titleKey: "nav.sessionLogs", - kind: "component", - component: RecordingStatus, - when: (context) => { - const host = context.host as - | { pluginSettings?: Record> } - | undefined; - const enabled = - host?.pluginSettings?.["session-recording"]?.enableSessionRecording; - return enabled !== false; - }, - }); -} - -export function deactivate(): void {} diff --git a/plugins/session-recording/src/frontend/session-recording-api.ts b/plugins/session-recording/src/frontend/session-recording-api.ts deleted file mode 100644 index 17e6b9c7d..000000000 --- a/plugins/session-recording/src/frontend/session-recording-api.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; - -export type SessionLogRecord = { - id: number; - hostId: number; - userId: string | null; - startedAt: string; - endedAt: string | null; - duration: number | null; - recordingPath: string | null; - hostName: string | null; - hostIp: string | null; - sizeBytes: number | null; - protocol: "ssh" | "rdp" | "vnc" | "telnet"; - format: "text" | "asciicast" | "guacamole"; - username: string | null; -}; - -export function createSessionRecordingApi(api: PluginApiClient) { - return { - async list(): Promise { - const response = await api.get<{ logs: SessionLogRecord[] }>("/"); - return response.data.logs; - }, - - async getBlob(id: number): Promise { - const response = await api.get(`/${id}/content`, { - responseType: "blob", - }); - return response.data; - }, - - async getContent(id: number): Promise { - const response = await api.get(`/${id}/content`, { - responseType: "text", - }); - return response.data; - }, - - async delete(id: number): Promise { - await api.delete(`/${id}`); - }, - }; -} diff --git a/plugins/session-recording/tests/backend/helpers.ts b/plugins/session-recording/tests/backend/helpers.ts deleted file mode 100644 index 9f5334bf5..000000000 --- a/plugins/session-recording/tests/backend/helpers.ts +++ /dev/null @@ -1,128 +0,0 @@ -import http from "node:http"; -import type { AddressInfo } from "node:net"; -import { fileURLToPath } from "node:url"; -import express, { type Router } from "express"; -import { - createMockCtx, - createTestDb, - type MockPluginContext, - type TestDb, -} from "@termix/plugin-sdk/testing"; -import type { PluginHostSummary } from "@termix/plugin-sdk/backend"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import manifestJson from "../../manifest.json"; - -export const pluginDir = fileURLToPath(new URL("../..", import.meta.url)); -export const manifest = manifestJson as unknown as PluginManifest; - -export function host( - overrides: Partial = {}, -): PluginHostSummary { - return { - id: 7, - userId: "user-1", - name: "web", - ip: "10.0.0.5", - port: 22, - username: "root", - tags: null, - folder: null, - authType: "password", - ...overrides, - }; -} - -export interface TestServer { - db: TestDb; - mock: MockPluginContext; - request: ( - method: string, - path: string, - options?: { user?: string; body?: unknown }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ) => Promise<{ status: number; body: any }>; - requestBinary: ( - method: string, - path: string, - options?: { user?: string }, - ) => Promise<{ status: number; body: ArrayBuffer; contentType: string }>; - close: () => Promise; -} - -export async function startServer( - options: { - permissions?: string[]; - users?: string[]; - hosts?: PluginHostSummary[]; - } = {}, -): Promise { - const db = await createTestDb(pluginDir); - for (const user of options.users ?? ["user-1", "user-2"]) { - db.sqlite - .prepare("INSERT INTO users (id, username) VALUES (?, ?)") - .run(user, user); - } - for (const h of options.hosts ?? [host()]) { - db.sqlite.prepare("INSERT INTO ssh_data (id) VALUES (?)").run(h.id); - } - - let router: Router | null = null; - const mock = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: manifest.capabilities, - db: db.database, - router: () => (router = express.Router()), - permissions: options.permissions ?? ["session-recording.view"], - hosts: options.hosts ?? [host()], - }); - - const { activate } = await import("../../src/backend/index.js"); - await activate(mock.ctx); - - const app = express(); - app.use(express.json()); - app.use((req, _res, next) => { - mock.setActor(req.header("x-test-user") ?? undefined); - next(); - }); - app.use((req, res, next) => router!(req, res, next)); - - const server = http.createServer(app); - await new Promise((resolve) => server.listen(0, resolve)); - const { port } = server.address() as AddressInfo; - - return { - db, - mock, - async request(method, path, { user = "user-1", body: given } = {}) { - const body = method === "GET" ? undefined : given; - const response = await fetch(`http://127.0.0.1:${port}${path}`, { - method, - headers: { - "x-test-user": user, - ...(body !== undefined ? { "content-type": "application/json" } : {}), - }, - body: body !== undefined ? JSON.stringify(body) : undefined, - }); - const text = await response.text(); - return { status: response.status, body: text ? JSON.parse(text) : null }; - }, - async requestBinary(method, path, { user = "user-1" } = {}) { - const response = await fetch(`http://127.0.0.1:${port}${path}`, { - method, - headers: { "x-test-user": user }, - }); - return { - status: response.status, - body: await response.arrayBuffer(), - contentType: response.headers.get("content-type") ?? "", - }; - }, - async close() { - for (const dispose of [...mock.disposals].reverse()) await dispose(); - await new Promise((resolve) => server.close(() => resolve())); - db.close(); - }, - }; -} diff --git a/plugins/session-recording/tests/backend/migrations.test.ts b/plugins/session-recording/tests/backend/migrations.test.ts deleted file mode 100644 index f817fab03..000000000 --- a/plugins/session-recording/tests/backend/migrations.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing"; -import { pluginDir } from "./helpers"; - -// session_recordings as core's SQLite bootstrap created it before 2.9.0. -const LEGACY_DDL = ` - CREATE TABLE session_recordings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL, - user_id TEXT, - username TEXT, - access_id INTEGER, - started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - ended_at TEXT, - duration INTEGER, - commands TEXT, - dangerous_actions TEXT, - recording_path TEXT, - protocol TEXT NOT NULL DEFAULT 'ssh', - format TEXT NOT NULL DEFAULT 'text', - terminated_by_owner INTEGER DEFAULT 0, - termination_reason TEXT, - FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL - ); -`; - -let db: TestDb | null = null; - -afterEach(() => { - db?.close(); - db = null; -}); - -const tableExists = (name: string) => - !!db!.sqlite - .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") - .get(name); - -describe("adopting session_recordings", () => { - it("keeps every existing row when the legacy table is there", async () => { - db = await createTestDb(pluginDir, { - before: (sqlite) => { - sqlite.exec("INSERT INTO users (id) VALUES ('user-1')"); - sqlite.exec("INSERT INTO ssh_data (id) VALUES (7)"); - sqlite.exec(LEGACY_DDL); - sqlite - .prepare( - "INSERT INTO session_recordings (host_id, user_id, username, recording_path, protocol, format) VALUES (?, ?, ?, ?, ?, ?)", - ) - .run( - 7, - "user-1", - "user-1", - "/data/session_logs/user-1/a.cast", - "ssh", - "asciicast", - ); - }, - }); - - expect(db.applied).toEqual(["0001_adopt_session_recordings"]); - expect(tableExists("session_recordings")).toBe(false); - expect(tableExists("p_session_recording_session_recordings")).toBe(true); - - expect( - db.sqlite - .prepare( - "SELECT host_id, user_id, recording_path, protocol, format FROM p_session_recording_session_recordings", - ) - .all(), - ).toEqual([ - { - host_id: 7, - user_id: "user-1", - recording_path: "/data/session_logs/user-1/a.cast", - protocol: "ssh", - format: "asciicast", - }, - ]); - }); - - it("creates the table fresh when there is no legacy one", async () => { - db = await createTestDb(pluginDir); - - expect(tableExists("session_recordings")).toBe(false); - expect(tableExists("p_session_recording_session_recordings")).toBe(true); - expect( - db.sqlite - .prepare("SELECT * FROM p_session_recording_session_recordings") - .all(), - ).toEqual([]); - }); - - it("cascades on host delete", async () => { - db = await createTestDb(pluginDir, { - before: (sqlite) => { - sqlite.exec("INSERT INTO users (id) VALUES ('user-1')"); - sqlite.exec("INSERT INTO ssh_data (id) VALUES (7)"); - }, - }); - db.sqlite - .prepare( - "INSERT INTO p_session_recording_session_recordings (host_id, user_id, protocol, format) VALUES (?, ?, ?, ?)", - ) - .run(7, "user-1", "ssh", "asciicast"); - - db.sqlite.exec("DELETE FROM ssh_data WHERE id = 7"); - - expect( - db.sqlite - .prepare("SELECT * FROM p_session_recording_session_recordings") - .all(), - ).toEqual([]); - }); - - it("keeps a row whose user was deleted, with userId cleared", async () => { - db = await createTestDb(pluginDir, { - before: (sqlite) => { - sqlite.exec("INSERT INTO users (id) VALUES ('user-1')"); - sqlite.exec("INSERT INTO ssh_data (id) VALUES (7)"); - }, - }); - db.sqlite - .prepare( - "INSERT INTO p_session_recording_session_recordings (host_id, user_id, protocol, format) VALUES (?, ?, ?, ?)", - ) - .run(7, "user-1", "ssh", "asciicast"); - - // userId is a plain column, not refUser(): deleting the user must not - // cascade delete the row (the plugin clears userId itself on - // user.deleted instead). - db.sqlite.exec("DELETE FROM users WHERE id = 'user-1'"); - - expect( - db.sqlite - .prepare("SELECT host_id FROM p_session_recording_session_recordings") - .all(), - ).toEqual([{ host_id: 7 }]); - }); -}); diff --git a/plugins/session-recording/tests/backend/repository.test.ts b/plugins/session-recording/tests/backend/repository.test.ts deleted file mode 100644 index 59a0cc435..000000000 --- a/plugins/session-recording/tests/backend/repository.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing"; -import { pluginDir } from "./helpers"; -import { createSessionRecordingRepository } from "../../src/backend/repository"; -import { sessionRecordings } from "../../src/backend/tables"; - -let db: TestDb | null = null; - -afterEach(() => { - db?.close(); - db = null; -}); - -const fakeHosts = { - list: async () => [ - { - id: 7, - userId: "user-1", - name: "web", - ip: "10.0.0.5", - port: 22, - username: "root", - tags: null, - folder: null, - authType: "password", - }, - ], - get: async () => null, - checkAccess: async () => ({ - hasAccess: false, - isOwner: false, - isShared: false, - }), - create: async () => { - throw new Error("not used"); - }, - update: async () => null, - listOwned: async () => [], - share: async () => ({ hostId: 0, shared: false }), - listUsers: async () => [], - listRoles: async () => [], - trackSession: () => () => {}, - recordActivity: async () => {}, -}; - -async function setup() { - db = await createTestDb(pluginDir); - db.sqlite - .prepare("INSERT INTO users (id, username) VALUES (?, ?)") - .run("user-1", "alice"); - db.sqlite.prepare("INSERT INTO ssh_data (id) VALUES (?)").run(7); - const table = await db.database.define(sessionRecordings); - return createSessionRecordingRepository( - db.database, - table, - fakeHosts as never, - ); -} - -describe("session recording repository", () => { - it("creates a row and reads it back by recordingPath", async () => { - const repo = await setup(); - const row = await repo.create({ - hostId: 7, - userId: "user-1", - username: "alice", - startedAt: new Date().toISOString(), - recordingPath: "/data/session_logs/user-1/a.cast", - protocol: "ssh", - format: "asciicast", - }); - - expect(row.id).toBeGreaterThan(0); - expect(row.recordingPath).toBe("/data/session_logs/user-1/a.cast"); - expect(db!.persisted).toBeGreaterThan(0); - }); - - it("lists a user's recordings with host name and ip attached", async () => { - const repo = await setup(); - await repo.create({ - hostId: 7, - userId: "user-1", - startedAt: new Date().toISOString(), - recordingPath: "/data/session_logs/user-1/a.cast", - protocol: "ssh", - format: "asciicast", - }); - - const rows = await repo.listByUserIdWithHost("user-1"); - expect(rows).toHaveLength(1); - expect(rows[0].hostName).toBe("web"); - expect(rows[0].hostIp).toBe("10.0.0.5"); - }); - - it("finds and deletes only the owner's row", async () => { - const repo = await setup(); - const row = await repo.create({ - hostId: 7, - userId: "user-1", - startedAt: new Date().toISOString(), - recordingPath: "/data/session_logs/user-1/a.cast", - protocol: "ssh", - format: "asciicast", - }); - - expect(await repo.findByIdForUser("user-2", row.id)).toBeNull(); - expect(await repo.deleteForUser("user-2", row.id)).toBe(false); - expect(await repo.deleteForUser("user-1", row.id)).toBe(true); - expect(await repo.findByIdForUser("user-1", row.id)).toBeNull(); - }); - - it("anonymizes a user's rows instead of deleting them", async () => { - const repo = await setup(); - const row = await repo.create({ - hostId: 7, - userId: "user-1", - startedAt: new Date().toISOString(), - recordingPath: "/data/session_logs/user-1/a.cast", - protocol: "ssh", - format: "asciicast", - }); - - await repo.anonymizeByUserId("user-1"); - - const rows = await repo.listPathsOlderThan( - new Date(Date.now() + 60_000).toISOString(), - ); - expect(rows.map((r) => r.id)).toContain(row.id); - expect(await repo.findByIdForUser("user-1", row.id)).toBeNull(); - }); - - it("lists recordings older than a cutoff for retention pruning", async () => { - const repo = await setup(); - await repo.create({ - hostId: 7, - userId: "user-1", - startedAt: "2020-01-01T00:00:00.000Z", - recordingPath: "/data/session_logs/user-1/old.cast", - protocol: "ssh", - format: "asciicast", - }); - - const old = await repo.listPathsOlderThan("2021-01-01T00:00:00.000Z"); - expect(old).toHaveLength(1); - - const none = await repo.listPathsOlderThan("2019-01-01T00:00:00.000Z"); - expect(none).toHaveLength(0); - }); -}); diff --git a/plugins/session-recording/tests/backend/routes.test.ts b/plugins/session-recording/tests/backend/routes.test.ts deleted file mode 100644 index e4ee2eccc..000000000 --- a/plugins/session-recording/tests/backend/routes.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { startServer, type TestServer } from "./helpers"; - -let server: TestServer | null = null; -let dataDir: string; - -beforeEach(async () => { - dataDir = await fs.mkdtemp( - path.join(os.tmpdir(), "session-recording-routes-"), - ); -}); - -afterEach(async () => { - await server?.close(); - server = null; - await fs.rm(dataDir, { recursive: true, force: true }); -}); - -describe("session recording routes", () => { - it("401s without an authenticated actor", async () => { - server = await startServer(); - const res = await server.request("GET", "/", { user: "" }); - expect(res.status).toBe(401); - }); - - it("lists only the caller's own recordings", async () => { - server = await startServer(); - - const table = await server.mock.ctx.db.client<{ - insert: (t: unknown) => { - values: (v: unknown) => Promise; - }; - }>(); - void table; - - // Seed through the repository the way activate() wired it: easiest is a - // direct HTTP round trip isn't available for writes (only ssh-terminal - // and remote-desktop write through the service), so seed the table - // directly. - server.db.sqlite - .prepare( - `INSERT INTO p_session_recording_session_recordings - (host_id, user_id, recording_path, protocol, format, started_at) - VALUES (?, ?, ?, ?, ?, ?)`, - ) - .run( - 7, - "user-1", - "/tmp/a.cast", - "ssh", - "asciicast", - new Date().toISOString(), - ); - server.db.sqlite - .prepare( - `INSERT INTO p_session_recording_session_recordings - (host_id, user_id, recording_path, protocol, format, started_at) - VALUES (?, ?, ?, ?, ?, ?)`, - ) - .run( - 7, - "user-2", - "/tmp/b.cast", - "ssh", - "asciicast", - new Date().toISOString(), - ); - - const res = await server.request("GET", "/", { user: "user-1" }); - expect(res.status).toBe(200); - expect(res.body.logs).toHaveLength(1); - expect(res.body.logs[0].userId).toBe("user-1"); - }); - - it("404s getting another user's recording", async () => { - server = await startServer(); - server.db.sqlite - .prepare( - `INSERT INTO p_session_recording_session_recordings - (host_id, user_id, recording_path, protocol, format, started_at) - VALUES (?, ?, ?, ?, ?, ?)`, - ) - .run( - 7, - "user-2", - "/tmp/b.cast", - "ssh", - "asciicast", - new Date().toISOString(), - ); - - const res = await server.request("GET", "/1", { user: "user-1" }); - expect(res.status).toBe(404); - }); - - it("serves file content within the plugin's data directory", async () => { - server = await startServer(); - const filePath = path.join(dataDir, "session_logs", "user-1", "a.cast"); - await fs.mkdir(path.dirname(filePath), { recursive: true }); - await fs.writeFile(filePath, '{"version":2}\n[0,"o","hi"]\n'); - - server.db.sqlite - .prepare( - `INSERT INTO p_session_recording_session_recordings - (host_id, user_id, recording_path, protocol, format, started_at) - VALUES (?, ?, ?, ?, ?, ?)`, - ) - .run(7, "user-1", filePath, "ssh", "asciicast", new Date().toISOString()); - - // Point ctx.files.dataDir() at the temp dir for this request. - ( - server.mock.ctx as unknown as { - files: { dataDir: () => Promise }; - } - ).files = { dataDir: async () => dataDir }; - - const res = await server.requestBinary("GET", "/1/content", { - user: "user-1", - }); - expect(res.status).toBe(200); - expect(res.contentType).toContain("asciicast"); - }); - - it("refuses a path outside the plugin's data directory", async () => { - server = await startServer(); - server.db.sqlite - .prepare( - `INSERT INTO p_session_recording_session_recordings - (host_id, user_id, recording_path, protocol, format, started_at) - VALUES (?, ?, ?, ?, ?, ?)`, - ) - .run( - 7, - "user-1", - "/etc/passwd", - "ssh", - "asciicast", - new Date().toISOString(), - ); - - ( - server.mock.ctx as unknown as { - files: { dataDir: () => Promise }; - } - ).files = { dataDir: async () => dataDir }; - - const res = await server.requestBinary("GET", "/1/content", { - user: "user-1", - }); - expect(res.status).toBe(403); - }); - - it("deletes only the caller's own recording", async () => { - server = await startServer(); - server.db.sqlite - .prepare( - `INSERT INTO p_session_recording_session_recordings - (host_id, user_id, recording_path, protocol, format, started_at) - VALUES (?, ?, ?, ?, ?, ?)`, - ) - .run( - 7, - "user-1", - "/tmp/a.cast", - "ssh", - "asciicast", - new Date().toISOString(), - ); - - const denied = await server.request("DELETE", "/1", { user: "user-2" }); - expect(denied.status).toBe(404); - - const ok = await server.request("DELETE", "/1", { user: "user-1" }); - expect(ok.status).toBe(200); - }); -}); diff --git a/plugins/session-recording/tests/backend/service.test.ts b/plugins/session-recording/tests/backend/service.test.ts deleted file mode 100644 index 5b8522729..000000000 --- a/plugins/session-recording/tests/backend/service.test.ts +++ /dev/null @@ -1,267 +0,0 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { - createMockCtx, - createTestDb, - type MockPluginContext, - type TestDb, -} from "@termix/plugin-sdk/testing"; -import manifestJson from "../../manifest.json"; -import { pluginDir } from "./helpers"; -import { createSessionRecordingRepository } from "../../src/backend/repository"; -import { sessionRecordings } from "../../src/backend/tables"; -import { createRecordingsWriter } from "../../src/backend/service"; - -const manifest = - manifestJson as unknown as import("@termix/plugin-sdk/manifest").PluginManifest; - -let db: TestDb | null = null; -let dataDir: string; - -beforeEach(async () => { - dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "session-recording-")); -}); - -afterEach(async () => { - db?.close(); - db = null; - await fs.rm(dataDir, { recursive: true, force: true }); -}); - -async function setup(): Promise<{ - mock: MockPluginContext; - writer: ReturnType; -}> { - db = await createTestDb(pluginDir); - db.sqlite - .prepare("INSERT INTO users (id, username) VALUES (?, ?)") - .run("user-1", "alice"); - db.sqlite.prepare("INSERT INTO ssh_data (id) VALUES (?)").run(7); - const table = await db.database.define(sessionRecordings); - const hosts = { - list: async () => [], - get: async () => null, - checkAccess: async () => ({ - hasAccess: false, - isOwner: false, - isShared: false, - }), - create: async () => { - throw new Error("not used"); - }, - update: async () => null, - listOwned: async () => [], - share: async () => ({ hostId: 0, shared: false }), - listUsers: async () => [], - listRoles: async () => [], - trackSession: () => () => {}, - recordActivity: async () => {}, - }; - const repo = createSessionRecordingRepository( - db.database, - table, - hosts as never, - ); - - const mock = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: manifest.capabilities, - db: db.database, - }); - // Point ctx.files.dataDir() at a real temp dir for this test. - (mock.ctx as unknown as { files: { dataDir: () => Promise } }).files = - { - dataDir: async () => dataDir, - }; - - const writer = createRecordingsWriter(mock.ctx, repo); - return { mock, writer }; -} - -describe("recordings.writer", () => { - it("is null when the host has recording switched off", async () => { - const { mock, writer } = await setup(); - await mock.ctx.settings.setHost(7, "enableSessionRecording", false); - - const sink = await writer.open({ - sessionId: "s1", - hostId: 7, - userId: "user-1", - protocol: "ssh", - format: "asciicast", - startedAt: Date.now(), - }); - - expect(sink).toBeNull(); - expect(await writer.enabledFor(7)).toBe(false); - expect(await fs.readdir(dataDir)).toEqual([]); - expect( - db!.sqlite - .prepare( - "SELECT COUNT(*) AS count FROM p_session_recording_session_recordings", - ) - .get(), - ).toEqual({ count: 0 }); - }); - - it("keeps a saved off switch after recreating the writer without affecting another host", async () => { - const { mock } = await setup(); - db!.sqlite.prepare("INSERT INTO ssh_data (id) VALUES (?)").run(8); - await mock.ctx.settings.setHost(7, "enableSessionRecording", false); - await mock.ctx.settings.setHost(8, "enableSessionRecording", true); - const table = await db!.database.define(sessionRecordings); - const repository = createSessionRecordingRepository( - db!.database, - table, - mock.ctx.hosts, - ); - const reopened = createRecordingsWriter(mock.ctx, repository); - for (const hostId of [7, 8]) { - const sink = await reopened.open({ - sessionId: `host-${hostId}`, - hostId, - userId: "user-1", - protocol: "ssh", - format: "asciicast", - startedAt: Date.now(), - }); - if (hostId === 7) expect(sink).toBeNull(); - else { - expect(sink).not.toBeNull(); - await sink!.append("marker\n"); - } - } - expect( - await fs.readdir(path.join(dataDir, "session_logs", "user-1")), - ).toEqual(["host-8.cast"]); - expect( - db!.sqlite - .prepare("SELECT host_id FROM p_session_recording_session_recordings") - .all(), - ).toEqual([{ host_id: 8 }]); - }); - - it("writes the first append with writeFile and later ones with appendFile", async () => { - const { writer } = await setup(); - const sink = await writer.open({ - sessionId: "s1", - hostId: 7, - userId: "user-1", - protocol: "ssh", - format: "asciicast", - startedAt: Date.now(), - }); - expect(sink).not.toBeNull(); - - await sink!.append("chunk-one\n"); - await sink!.append("chunk-two\n"); - - const filePath = path.join(dataDir, "session_logs", "user-1", "s1.cast"); - const content = await fs.readFile(filePath, "utf-8"); - expect(content).toBe("chunk-one\nchunk-two\n"); - }); - - it("persist writes the summary onto the row", async () => { - const { writer } = await setup(); - const sink = await writer.open({ - sessionId: "s1", - hostId: 7, - userId: "user-1", - protocol: "ssh", - format: "asciicast", - startedAt: Date.now(), - }); - await sink!.append("x\n"); - - const endedAt = Date.now(); - await sink!.persist({ - endedAt, - durationSeconds: 42, - terminatedByOwner: true, - terminationReason: null, - }); - - // Not thrown means it wrote successfully; deeper row assertions belong - // to repository.test.ts. - }); - - it("discard removes the row and further writes are no-ops", async () => { - const { writer } = await setup(); - const sink = await writer.open({ - sessionId: "s1", - hostId: 7, - userId: "user-1", - protocol: "ssh", - format: "asciicast", - startedAt: Date.now(), - }); - - sink!.discard(); - await sink!.append("should not write"); - - const filePath = path.join(dataDir, "session_logs", "user-1", "s1.cast"); - await expect(fs.access(filePath)).rejects.toThrow(); - }); - - it("createFinished inserts a row for an already-written recording", async () => { - const { writer } = await setup(); - const row = await writer.createFinished({ - hostId: 7, - userId: "user-1", - startedAt: new Date().toISOString(), - endedAt: new Date().toISOString(), - duration: 10, - recordingPath: "/data/session_recordings/guacamole/x.guac", - protocol: "rdp", - format: "guacamole", - }); - - expect(row.id).toBeGreaterThan(0); - }); - - it("createFinished moves guacd's file under the data folder, where playback reads it", async () => { - const { mock, writer } = await setup(); - const guacdDir = await fs.mkdtemp(path.join(os.tmpdir(), "guacd-")); - const source = path.join(guacdDir, "guacamole", "rdp-1.guac"); - await fs.mkdir(path.dirname(source), { recursive: true }); - await fs.writeFile(source, "4.size,1.0;"); - - const row = await writer.createFinished({ - hostId: 7, - userId: "user-1", - startedAt: new Date().toISOString(), - endedAt: new Date().toISOString(), - duration: 10, - recordingPath: source, - protocol: "rdp", - format: "guacamole", - }); - - const target = path.join( - await mock.ctx.files.dataDir(), - "session_recordings", - "guacamole", - "rdp-1.guac", - ); - await expect(fs.readFile(target, "utf8")).resolves.toBe("4.size,1.0;"); - await expect(fs.access(source)).rejects.toThrow(); - const stored = db!.sqlite - .prepare( - "SELECT recording_path FROM p_session_recording_session_recordings WHERE id = ?", - ) - .get(row.id) as { recording_path: string }; - expect(stored.recording_path).toBe(target); - await fs.rm(guacdDir, { recursive: true, force: true }); - }); - - it("enabledFor follows the host's recording switch", async () => { - const { mock, writer } = await setup(); - await mock.ctx.settings.setHost(7, "enableSessionRecording", true); - await expect(writer.enabledFor(7)).resolves.toBe(true); - await mock.ctx.settings.setHost(7, "enableSessionRecording", false); - await expect(writer.enabledFor(7)).resolves.toBe(false); - }); -}); diff --git a/plugins/session-recording/tests/frontend/activate.test.tsx b/plugins/session-recording/tests/frontend/activate.test.tsx deleted file mode 100644 index 4589ad2e3..000000000 --- a/plugins/session-recording/tests/frontend/activate.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; -import { - renderWithApp, - type RenderedPluginApp, -} from "@termix/plugin-sdk/testing"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import * as plugin from "../../src/frontend/index"; -import manifestJson from "../../manifest.json"; -import locales from "../../locales/en.json"; - -const manifest = manifestJson as unknown as PluginManifest; - -let rendered: RenderedPluginApp | null = null; - -afterEach(async () => { - await rendered?.deactivate(); - rendered = null; -}); - -function stubApi(overrides: Partial = {}): PluginApiClient { - return { - get: vi.fn(async () => ({ data: { logs: [] } })), - post: vi.fn(async () => ({ data: {} })), - put: vi.fn(async () => ({ data: {} })), - patch: vi.fn(async () => ({ data: {} })), - delete: vi.fn(async () => ({ data: {} })), - ...overrides, - } as PluginApiClient; -} - -describe(`${manifest.id} activate`, () => { - it("registers only views its manifest declares", async () => { - rendered = await renderWithApp(plugin, { - manifest, - locales, - api: stubApi(), - }); - - expect(rendered.registered.tabs()).toEqual(["session-logs"]); - expect(rendered.registered.panels()).toEqual(["session-logs"]); - expect(rendered.registered.railItems()).toEqual([ - expect.objectContaining({ id: "session-logs" }), - ]); - expect(rendered.registered.slot("terminal.toolbarStatus")).toEqual([ - "session-recording.terminalStatus", - ]); - }); - - it("removes everything it registered on deactivate", async () => { - const app = await renderWithApp(plugin, { - manifest, - locales, - api: stubApi(), - }); - await app.deactivate(); - expect(app.registered.tabs()).toEqual([]); - expect(app.registered.panels()).toEqual([]); - expect(app.registered.railItems()).toEqual([]); - }); - - it("renders the panel without crashing", async () => { - rendered = await renderWithApp(plugin, { - manifest, - locales, - api: stubApi(), - }); - const element = rendered.renderPanel("session-logs"); - expect(element).toBeTruthy(); - }); -}); diff --git a/plugins/session-recording/tests/frontend/asciicast.test.ts b/plugins/session-recording/tests/frontend/asciicast.test.ts deleted file mode 100644 index 1f7656837..000000000 --- a/plugins/session-recording/tests/frontend/asciicast.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { parseAsciicast } from "../../src/frontend/asciicast"; - -describe("parseAsciicast", () => { - it("parses terminal dimensions and timed input/output", () => { - const recording = parseAsciicast( - '{"version":2,"width":120,"height":30}\n[0.1,"o","hello"]\n[1.5,"i","ls\\r"]\n[2,"r","100x40"]\n', - ); - expect(recording.width).toBe(120); - expect(recording.height).toBe(30); - expect(recording.duration).toBe(2); - expect(recording.events).toHaveLength(3); - }); - - it("rejects unsupported recording versions", () => { - expect(() => parseAsciicast('{"version":1}\n')).toThrow( - "Unsupported asciicast version", - ); - }); -}); diff --git a/plugins/session-recording/tsconfig.json b/plugins/session-recording/tsconfig.json deleted file mode 100644 index 7b35314c3..000000000 --- a/plugins/session-recording/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@termix/plugin-sdk/tsconfig.plugin.json", - "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] -} diff --git a/plugins/session-recording/vitest.config.ts b/plugins/session-recording/vitest.config.ts deleted file mode 100644 index 31ba004fc..000000000 --- a/plugins/session-recording/vitest.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; - -export default pluginVitestConfig(import.meta.url); diff --git a/plugins/session-sharing/CHANGELOG.md b/plugins/session-sharing/CHANGELOG.md deleted file mode 100644 index f9e465f00..000000000 --- a/plugins/session-sharing/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -## 1.0.0 - -First release, bundled with Termix 2.9.0. - -Share a live terminal or remote desktop session by link or with another user, read-only or with input. Collaboration rooms let a presenter show sessions to a group. diff --git a/plugins/session-sharing/README.md b/plugins/session-sharing/README.md deleted file mode 100644 index 6b253d9e9..000000000 --- a/plugins/session-sharing/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Session Sharing - -Share a live terminal or remote desktop session by link or with another user, and present sessions in collaboration rooms. - -## Features - -- Share a live terminal or remote desktop session by link or with one user. -- Pick read-only or read and write, and set when the share expires. -- Collaboration rooms where a presenter shows a session to a group and can hand control to a member. -- Guest links for people without a Termix account. -- The share button is in the terminal and remote desktop toolbars. - -## Settings - -### Admin - -- Allow Session Sharing: turn sharing on or off for everyone. - -### Host - -- Allow Session Sharing: allow sharing sessions on this host. - -## Permissions - -- `session-sharing.use`: Share sessions and join rooms. Admins and users have it by default. - -## Services - -Provides to other plugins: - -- `sessions.sharing`: join a shared session or room from the terminal. - -Uses from other plugins: - -- `sessions.live` to reach live SSH, RDP, VNC and Telnet sessions. - -## Development - -```bash -npm run build # build into dist/ -npm run test # run this plugin's tests -npm run typecheck # type-check this plugin -``` diff --git a/plugins/session-sharing/locales/en.json b/plugins/session-sharing/locales/en.json deleted file mode 100644 index 1b9763f07..000000000 --- a/plugins/session-sharing/locales/en.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Connecting to shared session...", - "linkInvalid": "This share link is invalid, expired, or has been revoked", - "rateLimited": "Too many attempts, please try again shortly", - "sessionEnded": "This session has ended", - "readOnlyBadge": "View only", - "ownerLabel": "Host owner" - }, - "modalTitle": "Share session", - "shareButton": "Share", - "notReadyToShare": "Session is not ready to share yet", - "modeTab": { - "link": "Link", - "user": "User" - }, - "linkModeDescription": "Anyone with this link can join, no account required.", - "userModeDescription": "Share with a specific user who already has access to this host. If they do not have access yet, share the host with them first or use a link instead. Once shared, the session appears in their Connections tab.", - "permissionLevel": { - "label": "Permission level", - "readOnly": "Read-only", - "readOnlyDescription": "Can watch the session live but cannot type or interact.", - "readWrite": "Read-write", - "readWriteDescription": "Can type and interact with the session just like the owner." - }, - "expiryLabel": "Link expiry", - "createLinkButton": "Create link", - "createShareButton": "Share with user", - "searchUsersPlaceholder": "Search users...", - "noUsersFound": "No users found", - "linkCreated": "Share link created", - "linkNeedsServer": "Share links only work for sessions on a Termix server. Connect this host through your linked server to share it with a link.", - "linkCopied": "Link copied to clipboard", - "copyLink": "Copy link", - "shareCreated": "Session shared. It will appear in their Connections tab.", - "shareFailed": "Failed to create share", - "userLacksHostAccess": "That user does not have access to this host yet. Share the host with them first, or use a link instead.", - "activeShares": "Active shares", - "noActiveShares": "No active shares for this session", - "revoke": "Revoke", - "revokeConfirmTitle": "Revoke this share?", - "revokeConfirmDescription": "Anyone using this share will lose access immediately.", - "revoked": "Share revoked", - "revokeFailed": "Failed to revoke share", - "joinCount": "{{count}} join", - "joinCount_other": "{{count}} joins", - "expiresAt": "Expires {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "User: {{username}}", - "loadSharesFailed": "Failed to load active shares", - "remoteDisplayUnavailable": "Remote desktop is not available right now.", - "sharedWithMe": "Sessions shared with me", - "customHoursPlaceholder": "Hours until access expires" - }, - "collab": { - "createRoom": "New room", - "roomName": "Room name", - "persistentRoom": "Persistent room", - "persistentRoomHint": "Persistent rooms stay listed after the meeting ends and can be reused.", - "noRooms": "No rooms yet. Create one to start a meeting.", - "openRoom": "Open", - "endRoom": "End meeting", - "leaveRoom": "Leave", - "deleteMember": "Remove", - "removeMember": "Remove {{name}}", - "removeMemberTitle": "Remove this member?", - "removeMemberDescription": "{{name}} will lose access to this room immediately. Any active SSH viewer will be disconnected.", - "invite": "Invite", - "inviteTitle": "Invite members", - "members": "Members", - "membersPanel": "Room members and control requests", - "membersWithCount": "Members ({{count}})", - "guestsWithCount": "Guests ({{count}})", - "removeGuests": "Remove guests", - "anonymousGuest": "Guest", - "online": "online", - "offline": "offline", - "you": "(you)", - "hostBadge": "Host", - "presenterBadge": "Presenting", - "present": "Present", - "presentTitle": "Take the stage", - "presentHost": "Host", - "searchHosts": "Search hosts", - "noHostsFound": "No available hosts found.", - "presentProtocol": "Protocol", - "stopPresenting": "Stop presenting", - "takeOver": "Take over", - "takeOverConfirmTitle": "Replace the current presentation?", - "takeOverDescription": "{{name}} is presenting now. Taking over will disconnect everyone from that stage.", - "emptyStage": "Nobody is presenting. Take the stage to share a session.", - "stageLoading": "Connecting to the stage...", - "stageEnded": "The presentation ended", - "roomEnded": "This meeting has ended", - "presenterLabel": "{{name}} is presenting", - "guestLink": "Guest link", - "guestLinkOn": "Guest link is on. Anyone with the link can watch the stage.", - "guestLinkOff": "Guest link is off.", - "copyLink": "Copy link", - "rotateLink": "Rotate link", - "disableLink": "Disable link", - "rotateLinkConfirmTitle": "Rotate the guest link?", - "rotateLinkDescription": "The current link will stop working immediately. Connected SSH guests will be disconnected.", - "disableLinkConfirmTitle": "Disable the guest link?", - "disableLinkDescription": "Anonymous SSH viewers will be disconnected immediately and the current link will stop working.", - "linkCopied": "Link copied", - "roles": "Roles", - "users": "Users", - "searchUsers": "Search users", - "endConfirmTitle": "End this meeting?", - "endDescription": "{{name}} will end for everyone and cannot be reopened.", - "endPersistentDescription": "The active presentation in {{name}} will stop. The room remains available for reuse.", - "deleteRoom": "Delete room", - "deleteConfirmTitle": "Delete this room?", - "deleteDescription": "{{name}} will be permanently deleted for everyone and cannot be undone.", - "invitedTo": "You were invited to \"{{name}}\"", - "guest": { - "title": "Meeting", - "waiting": "Waiting for the presenter...", - "linkInvalid": "This guest link is invalid or the meeting has ended" - }, - "reopenFromPanel": "This meeting tab expired. Reopen the room from the Meetings panel.", - "retry": "Retry", - "requestControl": "Request control", - "cancelControlRequest": "Cancel request", - "controlQueue": "Control requests", - "noControlRequests": "No pending requests.", - "grantControlTo": "Give control to {{name}}", - "dismissControlRequest": "Dismiss {{name}}'s control request", - "releaseControl": "Release control", - "grantControl": "Give control", - "revokeControl": "Take back control", - "controlRequestedBy": "{{name}} asked for control", - "grant": "Grant", - "controlBadge": "In control", - "youArePresenting": "You are presenting in another tab. Stop presenting to hand the stage over.", - "created": "Room created", - "invited": "Members invited", - "roomTab": "Meeting", - "serverChanged": "The meeting server changed. Reopen this meeting from the Meetings panel.", - "hostNotSynced": "Sync this host to the meeting server before presenting.", - "linkRequiresServer": "Link this desktop to a Termix server and create the meeting there to share a web link." - }, - "nav": { - "collab": "Meetings" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Allow Session Sharing", - "description": "Allow live terminal, RDP, VNC, and Telnet sessions to be shared instance-wide. Overrides every per-host sharing toggle when disabled." - } - }, - "host": { - "allowSessionSharing": { - "label": "Allow Session Sharing", - "description": "Let live sessions on this host be shared via link or with other users" - } - } - }, - "permissions": { - "use": { - "title": "Share sessions and join rooms", - "description": "Share live sessions by link or with other users, and create and join collaboration rooms." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Never", - "oneHour": "1 hour", - "oneDay": "24 hours", - "sevenDays": "7 days", - "thirtyDays": "30 days", - "custom": "Custom" - } - } - }, - "connections": { - "sectionSharedWithMe": "Shared with me", - "sharedBy": "Shared by {{username}}", - "join": "Join", - "sharedSessionLabel": "{{hostName}} (shared)" - }, - "plugin": { - "name": "Session Sharing" - } -} diff --git a/plugins/session-sharing/locales/translated/af_ZA.json b/plugins/session-sharing/locales/translated/af_ZA.json deleted file mode 100644 index d2f9b8d5b..000000000 --- a/plugins/session-sharing/locales/translated/af_ZA.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Verbind met gedeelde sessie...", - "linkInvalid": "Hierdie deelskakel is ongeldig, het verval of is herroep", - "rateLimited": "Te veel pogings, probeer asseblief weer binnekort", - "sessionEnded": "Hierdie sessie is verby", - "readOnlyBadge": "Slegs kyk", - "ownerLabel": "Gasheer-eienaar" - }, - "modalTitle": "Deel sessie", - "shareButton": "Deel", - "notReadyToShare": "Sessie is nog nie gereed om te deel nie", - "modeTab": { - "link": "Skakel", - "user": "Gebruiker" - }, - "linkModeDescription": "Enigiemand met hierdie skakel kan aansluit, geen rekening nodig nie.", - "userModeDescription": "Deel met 'n spesifieke gebruiker wat reeds toegang tot hierdie gasheer het. Indien hulle nog nie toegang het nie, deel die gasheer eers met hulle of gebruik eerder 'n skakel. Sodra dit gedeel is, verskyn die sessie in hul Verbindings-oortjie.", - "permissionLevel": { - "label": "Toestemmingsvlak", - "readOnly": "Leesalleen", - "readOnlyDescription": "Kan die sessie regstreeks kyk, maar kan nie tik of interaksie hê nie.", - "readWrite": "Lees-skryf", - "readWriteDescription": "Kan tik en met die sessie interaksie hê net soos die eienaar." - }, - "expiryLabel": "Skakelvervaldatum", - "createLinkButton": "Skep skakel", - "createShareButton": "Deel met gebruiker", - "searchUsersPlaceholder": "Soek gebruikers...", - "noUsersFound": "Geen gebruikers gevind nie", - "linkCreated": "Deel skakel geskep", - "linkNeedsServer": "Deelskakels werk slegs vir sessies op 'n Termix-bediener. Koppel hierdie gasheer deur jou gekoppelde bediener om dit met 'n skakel te deel.", - "linkCopied": "Skakel gekopieer na knipbord", - "copyLink": "Kopieer skakel", - "shareCreated": "Sessie gedeel. Dit sal in hul Verbindings-oortjie verskyn.", - "shareFailed": "Kon nie deel skep nie", - "userLacksHostAccess": "Daardie gebruiker het nog nie toegang tot hierdie gasheer nie. Deel die gasheer eers met hulle, of gebruik eerder 'n skakel.", - "activeShares": "Aktiewe aandele", - "noActiveShares": "Geen aktiewe delings vir hierdie sessie nie", - "revoke": "Herroep", - "revokeConfirmTitle": "Herroep hierdie deling?", - "revokeConfirmDescription": "Enigiemand wat hierdie deel gebruik, sal onmiddellik toegang verloor.", - "revoked": "Deel herroep", - "revokeFailed": "Kon nie deel herroep nie", - "joinCount": "{{count}} sluit aan", - "joinCount_other": "{{count}} sluit aan", - "expiresAt": "Vervaldatum {{date}}", - "linkShareBadge": "Skakel", - "userShareBadge": "Gebruiker: {{username}}", - "loadSharesFailed": "Kon nie aktiewe delings laai nie", - "remoteDisplayUnavailable": "Afstandswerkskerm is tans nie beskikbaar nie.", - "sharedWithMe": "Sessies met my gedeel", - "customHoursPlaceholder": "Ure totdat toegang verval" - }, - "collab": { - "createRoom": "Nuwe kamer", - "roomName": "Kamernaam", - "persistentRoom": "Permanente kamer", - "persistentRoomHint": "Permanente kamers bly gelys nadat die vergadering eindig en kan hergebruik word.", - "noRooms": "Geen kamers nog nie. Skep een om 'n vergadering te begin.", - "openRoom": "Oop", - "endRoom": "Beëindig vergadering", - "leaveRoom": "Vertrek", - "deleteMember": "Verwyder", - "removeMember": "Verwyder {{name}}", - "removeMemberTitle": "Verwyder hierdie lid?", - "removeMemberDescription": "{{name}} sal onmiddellik toegang tot hierdie kamer verloor. Enige aktiewe SSH-kyker sal ontkoppel word.", - "invite": "Nooi uit", - "inviteTitle": "Nooi lede uit", - "members": "Lede", - "membersPanel": "Kamerlede en beheerversoeke", - "membersWithCount": "Lede ({{count}})", - "guestsWithCount": "Gaste ({{count}})", - "removeGuests": "Verwyder gaste", - "anonymousGuest": "Gas", - "online": "aanlyn", - "offline": "vanlyn", - "you": "(jy)", - "hostBadge": "Gasheer", - "presenterBadge": "Aanbieding", - "present": "Aanwesig", - "presentTitle": "Neem die verhoog", - "presentHost": "Gasheer", - "searchHosts": "Soek gashere", - "noHostsFound": "Geen beskikbare gashere gevind nie.", - "presentProtocol": "Protokol", - "stopPresenting": "Hou op met aanbied", - "takeOver": "Neem oor", - "takeOverConfirmTitle": "Vervang die huidige aanbieding?", - "takeOverDescription": "{{name}} bied nou aan. Oorname sal almal van daardie stadium ontkoppel.", - "emptyStage": "Niemand bied aan nie. Neem die verhoog om 'n sessie te deel.", - "stageLoading": "Verbind met die verhoog...", - "stageEnded": "Die aanbieding het geëindig", - "roomEnded": "Hierdie vergadering het geëindig", - "presenterLabel": "{{name}} bied aan", - "guestLink": "Gas skakel", - "guestLinkOn": "Gasskakel is aan. Enigiemand met die skakel kan die verhoog kyk.", - "guestLinkOff": "Gas skakel is af.", - "copyLink": "Kopieer skakel", - "rotateLink": "Roteer skakel", - "disableLink": "Deaktiveer skakel", - "rotateLinkConfirmTitle": "Draai die gasskakel?", - "rotateLinkDescription": "Die huidige skakel sal onmiddellik ophou werk. Gekoppelde SSH-gaste sal ontkoppel word.", - "disableLinkConfirmTitle": "Deaktiveer die gasskakel?", - "disableLinkDescription": "Anonieme SSH-kykers sal onmiddellik ontkoppel word en die huidige skakel sal ophou werk.", - "linkCopied": "Skakel gekopieer", - "roles": "Rolle", - "users": "Gebruikers", - "searchUsers": "Soek gebruikers", - "endConfirmTitle": "Beëindig hierdie vergadering?", - "endDescription": "{{name}} sal vir almal eindig en kan nie weer heropen word nie.", - "endPersistentDescription": "Die aktiewe aanbieding in {{name}} sal stop. Die kamer bly beskikbaar vir hergebruik.", - "deleteRoom": "Vee kamer uit", - "deleteConfirmTitle": "Vee hierdie kamer uit?", - "deleteDescription": "{{name}} sal permanent vir almal uitgevee word en kan nie ongedaan gemaak word nie.", - "invitedTo": "Jy is genooi na \"{{name}}\"", - "guest": { - "title": "Vergadering", - "waiting": "Wag vir die aanbieder...", - "linkInvalid": "Hierdie gasskakel is ongeldig of die vergadering het geëindig" - }, - "reopenFromPanel": "Hierdie vergadering-oortjie het verval. Maak die kamer weer oop vanaf die Vergaderings-paneel.", - "retry": "Probeer weer", - "requestControl": "Versoekbeheer", - "cancelControlRequest": "Kanselleer versoek", - "controlQueue": "Beheerversoeke", - "noControlRequests": "Geen hangende versoeke nie.", - "grantControlTo": "Gee beheer aan {{name}}", - "dismissControlRequest": "Maak {{name}}se beheerversoek toe", - "releaseControl": "Vrystellingsbeheer", - "grantControl": "Gee beheer", - "revokeControl": "Neem beheer terug", - "controlRequestedBy": "{{name}} het om beheer gevra", - "grant": "Toekenning", - "controlBadge": "In beheer", - "youArePresenting": "Jy bied aan in 'n ander oortjie. Stop die aanbieding om die verhoog oor te dra.", - "created": "Kamer geskep", - "invited": "Lede genooi", - "roomTab": "Vergadering", - "serverChanged": "Die vergaderingsbediener het verander. Maak hierdie vergadering weer oop vanaf die Vergaderingspaneel.", - "hostNotSynced": "Sinkroniseer hierdie gasheer met die vergaderingbediener voor die aanbieding.", - "linkRequiresServer": "Koppel hierdie lessenaar aan 'n Termix-bediener en skep die vergadering daar om 'n webskakel te deel." - }, - "nav": { - "collab": "Vergaderings" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Laat Sessiedeling Toe", - "description": "Laat lewendige terminaal-, RDP-, VNC- en Telnet-sessies toe om instansiewyd gedeel te word. Oorskryf elke deelskakelaar per gasheer wanneer dit gedeaktiveer is." - } - }, - "host": { - "allowSessionSharing": { - "label": "Laat Sessiedeling Toe", - "description": "Laat regstreekse sessies op hierdie gasheer via skakel of met ander gebruikers gedeel word" - } - } - }, - "permissions": { - "use": { - "title": "Deel sessies en sluit aan by kamers", - "description": "Deel regstreekse sessies per skakel of met ander gebruikers, en skep en sluit aan by samewerkingskamers." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Nooit", - "oneHour": "1 uur", - "oneDay": "24 uur", - "sevenDays": "7 dae", - "thirtyDays": "30 dae", - "custom": "Pasgemaak" - } - } - }, - "connections": { - "sectionSharedWithMe": "Met my gedeel", - "sharedBy": "Gedeel deur {{username}}", - "join": "Sluit aan", - "sharedSessionLabel": "{{hostName}} (gedeel)" - }, - "plugin": { - "name": "Sessiedeling" - } -} diff --git a/plugins/session-sharing/locales/translated/ar_SA.json b/plugins/session-sharing/locales/translated/ar_SA.json deleted file mode 100644 index 1b2cdc27c..000000000 --- a/plugins/session-sharing/locales/translated/ar_SA.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "جارٍ الاتصال بالجلسة المشتركة...", - "linkInvalid": "رابط المشاركة هذا غير صالح أو منتهي الصلاحية أو تم إلغاؤه", - "rateLimited": "عدد المحاولات كبير جدًا، يرجى المحاولة مرة أخرى قريبًا", - "sessionEnded": "انتهت هذه الجلسة", - "readOnlyBadge": "للعرض فقط", - "ownerLabel": "مالك المضيف" - }, - "modalTitle": "جلسة مشاركة", - "shareButton": "مشاركة", - "notReadyToShare": "الجلسة غير جاهزة للمشاركة بعد", - "modeTab": { - "link": "وصلة", - "user": "المستخدم" - }, - "linkModeDescription": "يمكن لأي شخص لديه هذا الرابط الانضمام، ولا يلزم وجود حساب.", - "userModeDescription": "شارك الملف مع مستخدم محدد لديه صلاحية الوصول إلى هذا المضيف. إذا لم يكن لديه صلاحية الوصول بعد، فشارك معه المضيف أولاً أو استخدم رابطًا. بمجرد المشاركة، ستظهر الجلسة في علامة تبويب \"الاتصالات\" الخاصة به.", - "permissionLevel": { - "label": "مستوى الصلاحية", - "readOnly": "للقراءة فقط", - "readOnlyDescription": "يمكنني مشاهدة الجلسة مباشرة ولكن لا يمكنني الكتابة أو التفاعل.", - "readWrite": "القراءة والكتابة", - "readWriteDescription": "يمكنه الكتابة والتفاعل مع الجلسة تمامًا مثل المالك." - }, - "expiryLabel": "انتهاء صلاحية الرابط", - "createLinkButton": "إنشاء رابط", - "createShareButton": "مشاركة مع المستخدم", - "searchUsersPlaceholder": "ابحث عن المستخدمين...", - "noUsersFound": "لم يتم العثور على مستخدمين", - "linkCreated": "تم إنشاء رابط المشاركة", - "linkNeedsServer": "لا تعمل روابط المشاركة إلا مع الجلسات الموجودة على خادم Termix. قم بتوصيل هذا المضيف عبر خادمك المرتبط لمشاركته باستخدام رابط.", - "linkCopied": "تم نسخ الرابط إلى الحافظة", - "copyLink": "نسخ الوصلة", - "shareCreated": "تمت مشاركة الجلسة. ستظهر في علامة تبويب \"الاتصالات\" الخاصة بهم.", - "shareFailed": "فشل إنشاء المشاركة", - "userLacksHostAccess": "لا يملك هذا المستخدم صلاحية الوصول إلى هذا المضيف بعد. شارك المضيف معه أولاً، أو استخدم رابطاً بدلاً من ذلك.", - "activeShares": "الأسهم النشطة", - "noActiveShares": "لا توجد أسهم نشطة لهذه الجلسة", - "revoke": "إبطال", - "revokeConfirmTitle": "إلغاء هذه الحصة؟", - "revokeConfirmDescription": "أي شخص يستخدم هذا الملف المشترك سيفقد الوصول إليه فوراً.", - "revoked": "تم إلغاء الحصة", - "revokeFailed": "فشل في إلغاء المشاركة", - "joinCount": "{{count}} انضم", - "joinCount_other": "{{count}} ينضم", - "expiresAt": "تنتهي في {{date}}", - "linkShareBadge": "وصلة", - "userShareBadge": "المستخدم: {{username}}", - "loadSharesFailed": "فشل تحميل المشاركات النشطة", - "remoteDisplayUnavailable": "خدمة سطح المكتب البعيد غير متاحة حاليًا.", - "sharedWithMe": "الجلسات التي تمت مشاركتها معي", - "customHoursPlaceholder": "عدد الساعات حتى انتهاء صلاحية الوصول" - }, - "collab": { - "createRoom": "غرف جديدة", - "roomName": "اسم الغرفة", - "persistentRoom": "غرفة مستمرة", - "persistentRoomHint": "تبقى الغرف الدائمة مدرجة بعد انتهاء الاجتماع ويمكن إعادة استخدامها.", - "noRooms": "لا توجد غرف اجتماعات بعد. أنشئ غرفة لبدء اجتماع.", - "openRoom": "فتح", - "endRoom": "انتهاء الاجتماع", - "leaveRoom": "يترك", - "deleteMember": "إزالة", - "removeMember": "إزالة {{name}}", - "removeMemberTitle": "هل تريد حذف هذا العضو؟", - "removeMemberDescription": "سيفقد المستخدم {{name}} إمكانية الوصول إلى هذه الغرفة فورًا. وسيتم فصل أي برنامج عرض SSH نشط.", - "invite": "يدعو", - "inviteTitle": "دعوة الأعضاء", - "members": "أعضاء", - "membersPanel": "أعضاء الغرفة وطلبات التحكم", - "membersWithCount": "الأعضاء ({{count}})", - "guestsWithCount": "الضيوف ({{count}})", - "removeGuests": "إزالة الضيوف", - "anonymousGuest": "ضيف", - "online": "متصل", - "offline": "غير متصل", - "you": "(أنت)", - "hostBadge": "المضيف", - "presenterBadge": "تقديم", - "present": "حاضر", - "presentTitle": "اصعد إلى المسرح", - "presentHost": "المضيف", - "searchHosts": "ابحث عن مضيفين", - "noHostsFound": "لم يتم العثور على مضيفين متاحين.", - "presentProtocol": "البروتوكول", - "stopPresenting": "توقف عن التقديم", - "takeOver": "تولي زمام الأمور", - "takeOverConfirmTitle": "هل تريد استبدال العرض التقديمي الحالي؟", - "takeOverDescription": "{{name}} يقدم العرض الآن. سيؤدي الاستحواذ على المنصة إلى فصل الجميع عنها.", - "emptyStage": "لا يوجد مقدمون. تفضلوا بالصعود إلى المنصة لمشاركة جلسة.", - "stageLoading": "الاتصال بالمسرح...", - "stageEnded": "انتهى العرض التقديمي", - "roomEnded": "انتهى هذا الاجتماع", - "presenterLabel": "{{name}} يقدم", - "guestLink": "رابط الضيف", - "guestLinkOn": "رابط الضيوف مُفعّل. يمكن لأي شخص لديه الرابط مشاهدة العرض.", - "guestLinkOff": "رابط الضيف معطل.", - "copyLink": "نسخ الوصلة", - "rotateLink": "تدوير الرابط", - "disableLink": "تعطيل الرابط", - "rotateLinkConfirmTitle": "هل تريد تدوير رابط الضيف؟", - "rotateLinkDescription": "سيتوقف الرابط الحالي عن العمل فوراً. سيتم فصل الضيوف المتصلين عبر SSH.", - "disableLinkConfirmTitle": "هل تريد تعطيل رابط الضيف؟", - "disableLinkDescription": "سيتم قطع اتصال عارضي SSH المجهولين على الفور وسيتوقف الرابط الحالي عن العمل.", - "linkCopied": "تم نسخ الرابط", - "roles": "الأدوار", - "users": "المستخدمون", - "searchUsers": "ابحث عن المستخدمين", - "endConfirmTitle": "هل ننهي هذا الاجتماع؟", - "endDescription": "سينتهي {{name}} للجميع ولن يكون من الممكن إعادة فتحه.", - "endPersistentDescription": "سيتوقف العرض التقديمي النشط في {{name}} . وستبقى الغرفة متاحة لإعادة الاستخدام.", - "deleteRoom": "حذف الغرفة", - "deleteConfirmTitle": "هل تريد حذف هذه الغرفة؟", - "deleteDescription": "سيتم حذف {{name}} نهائياً للجميع ولا يمكن التراجع عنه.", - "invitedTo": "لقد تمت دعوتك إلى \"{{name}}\"", - "guest": { - "title": "مقابلة", - "waiting": "في انتظار المذيع...", - "linkInvalid": "رابط الضيف هذا غير صالح أو انتهى الاجتماع" - }, - "reopenFromPanel": "انتهت صلاحية علامة تبويب هذا الاجتماع. أعد فتح الغرفة من لوحة الاجتماعات.", - "retry": "إعادة المحاولة", - "requestControl": "التحكم في الطلبات", - "cancelControlRequest": "إلغاء الطلب", - "controlQueue": "طلبات التحكم", - "noControlRequests": "لا توجد طلبات معلقة.", - "grantControlTo": "قم بمنح التحكم إلى {{name}}", - "dismissControlRequest": "رفض طلب التحكم {{name}}", - "releaseControl": "التحكم في الإصدار", - "grantControl": "امنح السيطرة", - "revokeControl": "استعد السيطرة", - "controlRequestedBy": "طلب {{name}} التحكم", - "grant": "منحة", - "controlBadge": "مسيطر", - "youArePresenting": "أنت تعرض المحتوى في علامة تبويب أخرى. توقف عن العرض لتسليم المنصة.", - "created": "تم إنشاء الغرفة", - "invited": "الأعضاء المدعوون", - "roomTab": "مقابلة", - "serverChanged": "تم تغيير خادم الاجتماع. أعد فتح هذا الاجتماع من لوحة الاجتماعات.", - "hostNotSynced": "قم بمزامنة هذا المضيف مع خادم الاجتماع قبل العرض التقديمي.", - "linkRequiresServer": "قم بربط هذا الجهاز بخادم Termix وقم بإنشاء الاجتماع هناك لمشاركة رابط الويب." - }, - "nav": { - "collab": "الاجتماعات" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "السماح بمشاركة الجلسة", - "description": "يسمح هذا الخيار بمشاركة جلسات الطرفية المباشرة، وجلسات RDP، وVNC، وTelnet على مستوى الجهاز. ويتجاوز هذا الخيار جميع إعدادات المشاركة الخاصة بكل مضيف عند تعطيله." - } - }, - "host": { - "allowSessionSharing": { - "label": "السماح بمشاركة الجلسة", - "description": "يمكن مشاركة الجلسات المباشرة على هذا المضيف عبر رابط أو مع مستخدمين آخرين" - } - } - }, - "permissions": { - "use": { - "title": "شارك الجلسات وانضم إلى الغرف", - "description": "شارك الجلسات المباشرة عبر رابط أو مع مستخدمين آخرين، وقم بإنشاء غرف تعاون والانضمام إليها." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "أبدًا", - "oneHour": "ساعة واحدة", - "oneDay": "24 ساعة", - "sevenDays": "7 أيام", - "thirtyDays": "30 يومًا", - "custom": "مخصص" - } - } - }, - "connections": { - "sectionSharedWithMe": "تمت مشاركته معي", - "sharedBy": "تمت المشاركة بواسطة {{username}}", - "join": "ينضم", - "sharedSessionLabel": "{{hostName}} (مشترك)" - }, - "plugin": { - "name": "مشاركة الجلسة" - } -} diff --git a/plugins/session-sharing/locales/translated/bg_BG.json b/plugins/session-sharing/locales/translated/bg_BG.json deleted file mode 100644 index 92885cb53..000000000 --- a/plugins/session-sharing/locales/translated/bg_BG.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Свързване със споделена сесия...", - "linkInvalid": "Този линк за споделяне е невалиден, изтекъл или е отменен", - "rateLimited": "Твърде много опити, моля опитайте отново скоро", - "sessionEnded": "Тази сесия приключи", - "readOnlyBadge": "Само преглед", - "ownerLabel": "Собственик на домакин" - }, - "modalTitle": "Споделяне на сесия", - "shareButton": "Споделяне", - "notReadyToShare": "Сесията все още не е готова за споделяне", - "modeTab": { - "link": "Връзка", - "user": "Потребител" - }, - "linkModeDescription": "Всеки с този линк може да се присъедини, не е необходим акаунт.", - "userModeDescription": "Споделете с конкретен потребител, който вече има достъп до този хост. Ако все още няма достъп, първо споделете хоста с него или използвайте връзка. След като бъде споделена, сесията се показва в раздела му „Връзки“.", - "permissionLevel": { - "label": "Ниво на разрешение", - "readOnly": "Само за четене", - "readOnlyDescription": "Може да гледа сесията на живо, но не може да пише или да взаимодейства.", - "readWrite": "Четене и запис", - "readWriteDescription": "Може да пише и да взаимодейства със сесията точно като собственика." - }, - "expiryLabel": "Изтичане на срока на валидност на връзката", - "createLinkButton": "Създаване на връзка", - "createShareButton": "Споделяне с потребителя", - "searchUsersPlaceholder": "Търсене на потребители...", - "noUsersFound": "Не са намерени потребители", - "linkCreated": "Линкът за споделяне е създаден", - "linkNeedsServer": "Линковете за споделяне работят само за сесии на Termix сървър. Свържете този хост чрез свързания си сървър, за да го споделите с линк.", - "linkCopied": "Връзката е копирана в буферната памет", - "copyLink": "Копиране на връзката", - "shareCreated": "Сесията е споделена. Тя ще се покаже в раздела им „Връзки“.", - "shareFailed": "Създаването на споделено съдържание не бе успешно", - "userLacksHostAccess": "Тозият потребител все още няма достъп до този хост. Първо споделете хоста с него или използвайте линк.", - "activeShares": "Активни акции", - "noActiveShares": "Няма активни споделяния за тази сесия", - "revoke": "Отмяна", - "revokeConfirmTitle": "Да се отмени ли това споделяне?", - "revokeConfirmDescription": "Всеки, който използва това споделяне, ще загуби достъп незабавно.", - "revoked": "Акцията е отменена", - "revokeFailed": "Неуспешно отменяне на споделянето", - "joinCount": "{{count}} присъединяване", - "joinCount_other": "{{count}} се присъединява", - "expiresAt": "Изтича {{date}}", - "linkShareBadge": "Връзка", - "userShareBadge": "Потребител: {{username}}", - "loadSharesFailed": "Зареждането на активните споделяния не бе успешно", - "remoteDisplayUnavailable": "Отдалеченият работен плот не е наличен в момента.", - "sharedWithMe": "Сесии, споделени с мен", - "customHoursPlaceholder": "Часове до изтичане на срока на достъп" - }, - "collab": { - "createRoom": "Нова стая", - "roomName": "Име на стаята", - "persistentRoom": "Постоянна стая", - "persistentRoomHint": "Постоянните стаи остават в списъка след края на срещата и могат да бъдат използвани повторно.", - "noRooms": "Все още няма стаи. Създайте една, за да започнете среща.", - "openRoom": "Отворено", - "endRoom": "Край на срещата", - "leaveRoom": "Оставете", - "deleteMember": "Премахване", - "removeMember": "Премахване на {{name}}", - "removeMemberTitle": "Да се премахне ли този член?", - "removeMemberDescription": "{{name}} ще загуби достъп до тази стая незабавно. Всеки активен SSH зрител ще бъде прекъснат.", - "invite": "Покани", - "inviteTitle": "Поканете членове", - "members": "Членове", - "membersPanel": "Членове на стаята и заявки за контрол", - "membersWithCount": "Членове ({{count}})", - "guestsWithCount": "Гости ({{count}})", - "removeGuests": "Премахване на гости", - "anonymousGuest": "Гост", - "online": "онлайн", - "offline": "офлайн", - "you": "(ти)", - "hostBadge": "Домакин", - "presenterBadge": "Представяне", - "present": "Настояще", - "presentTitle": "Излезте на сцената", - "presentHost": "Домакин", - "searchHosts": "Търсене на хостове", - "noHostsFound": "Няма намерени налични хостове.", - "presentProtocol": "Протокол", - "stopPresenting": "Спиране на представянето", - "takeOver": "Поемете контрола", - "takeOverConfirmTitle": "Да се замени ли текущата презентация?", - "takeOverDescription": "{{name}} представя сега. Поемането на водещия ще изключи всички от тази сцена.", - "emptyStage": "Никой не представя. Излезте на сцената, за да споделите сесия.", - "stageLoading": "Свързване със сцената...", - "stageEnded": "Презентацията приключи", - "roomEnded": "Тази среща приключи", - "presenterLabel": "{{name}} представя", - "guestLink": "Връзка за гости", - "guestLinkOn": "Връзката за гости е включена. Всеки с връзката може да гледа сцената.", - "guestLinkOff": "Връзката за гости е изключена.", - "copyLink": "Копиране на връзката", - "rotateLink": "Завъртане на връзката", - "disableLink": "Деактивиране на връзката", - "rotateLinkConfirmTitle": "Да се завърти ли линкът за гости?", - "rotateLinkDescription": "Текущата връзка ще спре да работи незабавно. Свързаните SSH гости ще бъдат прекъснати.", - "disableLinkConfirmTitle": "Да се деактивира ли връзката за гости?", - "disableLinkDescription": "Анонимните SSH зрители ще бъдат незабавно прекъснати и текущата връзка ще спре да работи.", - "linkCopied": "Връзката е копирана", - "roles": "Роли", - "users": "Потребители", - "searchUsers": "Търсене на потребители", - "endConfirmTitle": "Да се прекрати ли тази среща?", - "endDescription": "{{name}} ще приключи за всички и не може да бъде отворено отново.", - "endPersistentDescription": "Активната презентация в {{name}} ще спре. Стаята остава достъпна за повторна употреба.", - "deleteRoom": "Изтриване на стая", - "deleteConfirmTitle": "Да се изтрие ли тази стая?", - "deleteDescription": "{{name}} ще бъде изтрито за постоянно за всички и действието не може да бъде отменено.", - "invitedTo": "Поканиха ви в „{{name}}“", - "guest": { - "title": "Среща", - "waiting": "В очакване на водещия...", - "linkInvalid": "Този линк за гости е невалиден или срещата е приключила" - }, - "reopenFromPanel": "Този раздел за среща е с изтекъл срок. Отворете отново стаята от панела „Срещи“.", - "retry": "Опитай отново", - "requestControl": "Заявка за контрол", - "cancelControlRequest": "Отмяна на заявката", - "controlQueue": "Заявки за контрол", - "noControlRequests": "Няма чакащи заявки.", - "grantControlTo": "Дайте контрол на {{name}}", - "dismissControlRequest": "Отхвърляне на заявката за контрол на {{name}}", - "releaseControl": "Контрол на освобождаването", - "grantControl": "Дайте контрол", - "revokeControl": "Върнете си контрола", - "controlRequestedBy": "{{name}} поиска контрол", - "grant": "Грант", - "controlBadge": "В контрол", - "youArePresenting": "Презентирате в друг раздел. Спрете презентацията, за да предадете сцената.", - "created": "Стаята е създадена", - "invited": "Поканени членове", - "roomTab": "Среща", - "serverChanged": "Сървърът за срещата се промени. Отворете отново тази среща от панела „Срещи“.", - "hostNotSynced": "Синхронизирайте този хост със сървъра за срещи преди представяне.", - "linkRequiresServer": "Свържете този работен плот със Termix сървър и създайте срещата там, за да споделите уеб връзка." - }, - "nav": { - "collab": "Срещи" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Разрешаване на споделяне на сесия", - "description": "Разрешава споделянето на сесии на живо на терминал, RDP, VNC и Telnet в целия екземпляр. Заменя всяко превключвател за споделяне на хост, когато е деактивиран." - } - }, - "host": { - "allowSessionSharing": { - "label": "Разрешаване на споделяне на сесия", - "description": "Позволете споделянето на сесиите на живо на този хост чрез линк или с други потребители" - } - } - }, - "permissions": { - "use": { - "title": "Споделяне на сесии и присъединяване към стаи", - "description": "Споделяйте сесии на живо чрез линк или с други потребители, както и създавайте и се присъединявайте към стаи за сътрудничество." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Никога", - "oneHour": "1 час", - "oneDay": "24 часа", - "sevenDays": "7 дни", - "thirtyDays": "30 дни", - "custom": "Персонализирано" - } - } - }, - "connections": { - "sectionSharedWithMe": "Споделено с мен", - "sharedBy": "Споделено от {{username}}", - "join": "Присъединете се", - "sharedSessionLabel": "{{hostName}} (споделено)" - }, - "plugin": { - "name": "Споделяне на сесия" - } -} diff --git a/plugins/session-sharing/locales/translated/bn_BD.json b/plugins/session-sharing/locales/translated/bn_BD.json deleted file mode 100644 index e4dc0868f..000000000 --- a/plugins/session-sharing/locales/translated/bn_BD.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "শেয়ার করা সেশনে সংযোগ স্থাপন করা হচ্ছে...", - "linkInvalid": "এই শেয়ার লিঙ্কটি অবৈধ, মেয়াদোত্তীর্ণ, বা বাতিল করা হয়েছে।", - "rateLimited": "অনেকবার চেষ্টা করা হয়েছে, অনুগ্রহ করে কিছুক্ষণ পরে আবার চেষ্টা করুন।", - "sessionEnded": "এই অধিবেশন শেষ হয়েছে", - "readOnlyBadge": "শুধুমাত্র দেখার জন্য", - "ownerLabel": "হোস্ট মালিক" - }, - "modalTitle": "সেশন শেয়ার করুন", - "shareButton": "শেয়ার", - "notReadyToShare": "সেশনটি এখনও শেয়ার করার জন্য প্রস্তুত নয়।", - "modeTab": { - "link": "লিঙ্ক", - "user": "ব্যবহারকারী" - }, - "linkModeDescription": "এই লিঙ্কের মাধ্যমে যে কেউ যোগ দিতে পারবেন, কোনো অ্যাকাউন্টের প্রয়োজন নেই।", - "userModeDescription": "এমন একজন নির্দিষ্ট ব্যবহারকারীর সাথে শেয়ার করুন যার এই হোস্টে আগে থেকেই অ্যাক্সেস আছে। যদি তাদের এখনও অ্যাক্সেস না থাকে, তাহলে প্রথমে তাদের সাথে হোস্টটি শেয়ার করুন অথবা এর পরিবর্তে একটি লিঙ্ক ব্যবহার করুন। শেয়ার করা হয়ে গেলে, সেশনটি তাদের 'কানেকশনস' ট্যাবে দেখা যাবে।", - "permissionLevel": { - "label": "অনুমতি স্তর", - "readOnly": "শুধুমাত্র পঠনযোগ্য", - "readOnlyDescription": "সেশনটি সরাসরি দেখা যাবে কিন্তু টাইপ বা অংশগ্রহণ করা যাবে না।", - "readWrite": "পড়া-লেখা", - "readWriteDescription": "মালিকের মতোই সেশনে টাইপ করতে এবং ইন্টারঅ্যাক্ট করতে পারবেন।" - }, - "expiryLabel": "লিঙ্কের মেয়াদ শেষ", - "createLinkButton": "লিঙ্ক তৈরি করুন", - "createShareButton": "ব্যবহারকারীর সাথে শেয়ার করুন", - "searchUsersPlaceholder": "ব্যবহারকারীদের অনুসন্ধান করুন...", - "noUsersFound": "কোন ব্যবহারকারী খুঁজে পাওয়া যায়নি", - "linkCreated": "শেয়ার লিঙ্ক তৈরি করা হয়েছে", - "linkNeedsServer": "শেয়ার লিঙ্ক শুধুমাত্র টারমিক্স সার্ভারের সেশনের জন্যই কাজ করে। লিঙ্কের মাধ্যমে শেয়ার করতে আপনার লিঙ্কড সার্ভারের মাধ্যমে এই হোস্টটি সংযুক্ত করুন।", - "linkCopied": "লিঙ্ক ক্লিপবোর্ডে কপি করা হয়েছে", - "copyLink": "লিঙ্ক কপি করুন", - "shareCreated": "সেশনটি শেয়ার করা হয়েছে। এটি তাদের কানেকশনস ট্যাবে দেখা যাবে।", - "shareFailed": "শেয়ার তৈরি করতে ব্যর্থ হয়েছে", - "userLacksHostAccess": "ওই ব্যবহারকারীর এখনও এই হোস্টে অ্যাক্সেস নেই। প্রথমে তাদের সাথে হোস্টটি শেয়ার করুন, অথবা এর পরিবর্তে একটি লিঙ্ক ব্যবহার করুন।", - "activeShares": "সক্রিয় শেয়ার", - "noActiveShares": "এই সেশনের জন্য কোনো সক্রিয় শেয়ার নেই", - "revoke": "বাতিল করুন", - "revokeConfirmTitle": "এই শেয়ারটি প্রত্যাহার করবেন?", - "revokeConfirmDescription": "যে কেউ এই শেয়ারটি ব্যবহার করলে, তিনি অবিলম্বে অ্যাক্সেস হারাবেন।", - "revoked": "শেয়ার প্রত্যাহার করা হয়েছে", - "revokeFailed": "শেয়ার প্রত্যাহার করতে ব্যর্থ হয়েছে", - "joinCount": "{{count}} যোগদান করুন", - "joinCount_other": "{{count}} যোগদান করে", - "expiresAt": "মেয়াদ শেষ হবে {{date}}", - "linkShareBadge": "লিঙ্ক", - "userShareBadge": "ব্যবহারকারী: {{username}}", - "loadSharesFailed": "সক্রিয় শেয়ারগুলি লোড করতে ব্যর্থ হয়েছে", - "remoteDisplayUnavailable": "এই মুহূর্তে রিমোট ডেস্কটপ উপলব্ধ নেই।", - "sharedWithMe": "আমার সাথে শেয়ার করা সেশনগুলি", - "customHoursPlaceholder": "অ্যাক্সেসের মেয়াদ শেষ হতে বাকি ঘন্টা" - }, - "collab": { - "createRoom": "নতুন ঘর", - "roomName": "রুমের নাম", - "persistentRoom": "স্থায়ী কক্ষ", - "persistentRoomHint": "মিটিং শেষ হওয়ার পরেও স্থায়ী রুমগুলো তালিকাভুক্ত থাকে এবং পুনরায় ব্যবহার করা যায়।", - "noRooms": "এখনো কোনো রুম নেই। মিটিং শুরু করতে একটি রুম তৈরি করুন।", - "openRoom": "খোলা", - "endRoom": "সভা শেষ করুন", - "leaveRoom": "ছেড়ে দিন", - "deleteMember": "অপসারণ করুন", - "removeMember": "{{name}} মুছে ফেলুন", - "removeMemberTitle": "এই সদস্যকে অপসারণ করবেন?", - "removeMemberDescription": "{{name}} অবিলম্বে এই রুমে প্রবেশাধিকার হারাবে। যেকোনো সক্রিয় SSH ভিউয়ার সংযোগ বিচ্ছিন্ন হয়ে যাবে।", - "invite": "আমন্ত্রণ", - "inviteTitle": "সদস্যদের আমন্ত্রণ জানান", - "members": "সদস্যরা", - "membersPanel": "রুমের সদস্য এবং নিয়ন্ত্রণ অনুরোধ", - "membersWithCount": "সদস্যগণ ({{count}})", - "guestsWithCount": "অতিথিরা ({{count}})", - "removeGuests": "অতিথিদের সরিয়ে দিন", - "anonymousGuest": "অতিথি", - "online": "অনলাইন", - "offline": "অফলাইন", - "you": "(তুমি)", - "hostBadge": "হোস্ট", - "presenterBadge": "উপস্থাপন করা হচ্ছে", - "present": "বর্তমান", - "presentTitle": "মঞ্চে উঠুন", - "presentHost": "হোস্ট", - "searchHosts": "হোস্ট অনুসন্ধান করুন", - "noHostsFound": "কোনো উপলব্ধ হোস্ট খুঁজে পাওয়া যায়নি।", - "presentProtocol": "প্রোটোকল", - "stopPresenting": "উপস্থাপন করা বন্ধ করুন", - "takeOver": "দায়িত্ব গ্রহণ করুন", - "takeOverConfirmTitle": "বর্তমান উপস্থাপনাটি প্রতিস্থাপন করবেন?", - "takeOverDescription": "{{name}} এখন উপস্থাপনা করছে। দায়িত্ব গ্রহণ করলে সবাই সেই মঞ্চ থেকে বিচ্ছিন্ন হয়ে যাবে।", - "emptyStage": "কেউ উপস্থাপনা করছেন না। একটি সেশন পরিচালনা করতে মঞ্চে আসুন।", - "stageLoading": "মঞ্চের সাথে সংযোগ স্থাপন...", - "stageEnded": "উপস্থাপনা শেষ হলো।", - "roomEnded": "এই সভা শেষ হয়েছে।", - "presenterLabel": "{{name}} উপস্থাপন করছে", - "guestLink": "অতিথি লিঙ্ক", - "guestLinkOn": "গেস্ট লিঙ্ক চালু আছে। লিঙ্কটি ব্যবহার করে যে কেউ মঞ্চটি দেখতে পারবেন।", - "guestLinkOff": "গেস্ট লিঙ্ক বন্ধ আছে।", - "copyLink": "লিঙ্ক কপি করুন", - "rotateLink": "লিঙ্ক ঘোরান", - "disableLink": "লিঙ্ক নিষ্ক্রিয় করুন", - "rotateLinkConfirmTitle": "গেস্ট লিঙ্কটি ঘোরান?", - "rotateLinkDescription": "বর্তমান সংযোগটি অবিলম্বে কাজ করা বন্ধ করে দেবে। সংযুক্ত SSH গেস্টদের সংযোগ বিচ্ছিন্ন করা হবে।", - "disableLinkConfirmTitle": "গেস্ট লিঙ্কটি নিষ্ক্রিয় করবেন?", - "disableLinkDescription": "বেনামী SSH দর্শকদের সংযোগ অবিলম্বে বিচ্ছিন্ন করা হবে এবং বর্তমান লিঙ্কটি কাজ করা বন্ধ করে দেবে।", - "linkCopied": "লিঙ্ক কপি করা হয়েছে", - "roles": "ভূমিকা", - "users": "ব্যবহারকারীরা", - "searchUsers": "ব্যবহারকারীদের অনুসন্ধান করুন", - "endConfirmTitle": "এই সভা শেষ করব?", - "endDescription": "{{name}} সকলের জন্য শেষ হয়ে যাবে এবং পুনরায় খোলা যাবে না।", - "endPersistentDescription": "{{name}} -এ চলমান উপস্থাপনাটি বন্ধ হয়ে যাবে। রুমটি পুনরায় ব্যবহারের জন্য উপলব্ধ থাকবে।", - "deleteRoom": "রুম মুছুন", - "deleteConfirmTitle": "এই রুমটা মুছে ফেলবেন?", - "deleteDescription": "{{name}} সকলের জন্য স্থায়ীভাবে মুছে ফেলা হবে এবং এটি আর পূর্বাবস্থায় ফেরানো যাবে না।", - "invitedTo": "তোমাকে \"{{name}} \" এ আমন্ত্রণ জানানো হয়েছিল", - "guest": { - "title": "সভা", - "waiting": "উপস্থাপকের জন্য অপেক্ষা করছি...", - "linkInvalid": "এই অতিথি লিঙ্কটি অবৈধ অথবা মিটিং শেষ হয়ে গেছে।" - }, - "reopenFromPanel": "এই মিটিং ট্যাবটির মেয়াদ শেষ হয়ে গেছে। মিটিং প্যানেল থেকে রুমটি পুনরায় খুলুন।", - "retry": "পুনরায় চেষ্টা করুন", - "requestControl": "অনুরোধ নিয়ন্ত্রণ", - "cancelControlRequest": "অনুরোধ বাতিল করুন", - "controlQueue": "নিয়ন্ত্রণ অনুরোধ", - "noControlRequests": "কোনো অপেক্ষমান অনুরোধ নেই।", - "grantControlTo": "{{name}}-কে নিয়ন্ত্রণ দিন", - "dismissControlRequest": "{{name}}এর নিয়ন্ত্রণ অনুরোধ খারিজ করুন", - "releaseControl": "রিলিজ নিয়ন্ত্রণ", - "grantControl": "নিয়ন্ত্রণ দিন", - "revokeControl": "নিয়ন্ত্রণ ফিরিয়ে নিন", - "controlRequestedBy": "{{name}} নিয়ন্ত্রণের জন্য অনুরোধ করেছে", - "grant": "অনুদান", - "controlBadge": "নিয়ন্ত্রণে", - "youArePresenting": "আপনি অন্য একটি ট্যাবে প্রেজেন্টেশন দিচ্ছেন। মঞ্চ হস্তান্তর করার জন্য প্রেজেন্টেশন বন্ধ করুন।", - "created": "রুম তৈরি করা হয়েছে", - "invited": "সদস্যদের আমন্ত্রণ জানানো হয়েছে", - "roomTab": "সভা", - "serverChanged": "মিটিং সার্ভার পরিবর্তিত হয়েছে। মিটিং প্যানেল থেকে এই মিটিংটি পুনরায় খুলুন।", - "hostNotSynced": "উপস্থাপনা করার আগে এই হোস্টকে মিটিং সার্ভারের সাথে সিঙ্ক করুন।", - "linkRequiresServer": "ওয়েব লিঙ্ক শেয়ার করার জন্য এই ডেস্কটপটিকে একটি টারমিক্স সার্ভারের সাথে লিঙ্ক করুন এবং সেখানে মিটিংটি তৈরি করুন।" - }, - "nav": { - "collab": "সভা" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "সেশন শেয়ারিং অনুমোদন করুন", - "description": "লাইভ টার্মিনাল, RDP, VNC, এবং টেলনেট সেশনগুলোকে ইনস্ট্যান্স-ব্যাপী শেয়ার করার অনুমতি দিন। এটি নিষ্ক্রিয় করা হলে, প্রতি-হোস্ট শেয়ারিং টগলকে ওভাররাইড করে।" - } - }, - "host": { - "allowSessionSharing": { - "label": "সেশন শেয়ারিং অনুমোদন করুন", - "description": "এই হোস্টের লাইভ সেশনগুলো লিঙ্কের মাধ্যমে বা অন্যান্য ব্যবহারকারীদের সাথে শেয়ার করা যাবে।" - } - } - }, - "permissions": { - "use": { - "title": "সেশন শেয়ার করুন এবং রুমে যোগ দিন", - "description": "লিঙ্কের মাধ্যমে বা অন্যান্য ব্যবহারকারীদের সাথে লাইভ সেশন শেয়ার করুন, এবং কোলাবোরেশন রুম তৈরি করে তাতে যোগদান করুন।" - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "কখনো না", - "oneHour": "১ ঘন্টা", - "oneDay": "২৪ ঘন্টা", - "sevenDays": "৭ দিন", - "thirtyDays": "৩০ দিন", - "custom": "কাস্টম" - } - } - }, - "connections": { - "sectionSharedWithMe": "আমার সাথে শেয়ার করা হয়েছে", - "sharedBy": "{{username}} দ্বারা শেয়ার করা হয়েছে", - "join": "যোগদান করুন", - "sharedSessionLabel": "{{hostName}} (ভাগ করা)" - }, - "plugin": { - "name": "সেশন শেয়ারিং" - } -} diff --git a/plugins/session-sharing/locales/translated/ca_ES.json b/plugins/session-sharing/locales/translated/ca_ES.json deleted file mode 100644 index 2e139ce26..000000000 --- a/plugins/session-sharing/locales/translated/ca_ES.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Connectant a la sessió compartida...", - "linkInvalid": "Aquest enllaç per compartir no és vàlid, ha caducat o s'ha revocat.", - "rateLimited": "Massa intents, si us plau, torneu-ho a intentar en breu", - "sessionEnded": "Aquesta sessió ha finalitzat", - "readOnlyBadge": "Només visualització", - "ownerLabel": "Propietari de l'amfitrió" - }, - "modalTitle": "Comparteix la sessió", - "shareButton": "Comparteix", - "notReadyToShare": "La sessió encara no està a punt per compartir", - "modeTab": { - "link": "Enllaç", - "user": "Usuari" - }, - "linkModeDescription": "Qualsevol persona amb aquest enllaç s'hi pot unir, no cal tenir cap compte.", - "userModeDescription": "Comparteix amb un usuari específic que ja tingui accés a aquest amfitrió. Si encara no hi té accés, comparteix primer l'amfitrió amb ell o utilitza un enllaç. Un cop compartida, la sessió apareix a la pestanya Connexions.", - "permissionLevel": { - "label": "Nivell de permís", - "readOnly": "Només lectura", - "readOnlyDescription": "Puc veure la sessió en directe però no puc escriure ni interactuar.", - "readWrite": "Lectura i escriptura", - "readWriteDescription": "Pot escriure i interactuar amb la sessió igual que el propietari." - }, - "expiryLabel": "Caducitat de l'enllaç", - "createLinkButton": "Crea un enllaç", - "createShareButton": "Comparteix amb l'usuari", - "searchUsersPlaceholder": "Cerca usuaris...", - "noUsersFound": "No s'han trobat usuaris", - "linkCreated": "Enllaç de compartició creat", - "linkNeedsServer": "Els enllaços per compartir només funcionen per a sessions en un servidor Termix. Connecteu aquest amfitrió a través del vostre servidor enllaçat per compartir-lo amb un enllaç.", - "linkCopied": "Enllaç copiat al porta-retalls", - "copyLink": "Copia l'enllaç", - "shareCreated": "Sessió compartida. Apareixerà a la pestanya Connexions.", - "shareFailed": "No s'ha pogut crear la compartició", - "userLacksHostAccess": "Aquest usuari encara no té accés a aquest amfitrió. Primer comparteix l'amfitrió amb ell o utilitza un enllaç.", - "activeShares": "Accions actives", - "noActiveShares": "No hi ha cap compartició activa per a aquesta sessió", - "revoke": "Revocar", - "revokeConfirmTitle": "Voleu revocar aquesta compartició?", - "revokeConfirmDescription": "Qualsevol persona que utilitzi aquesta opció compartida perdrà l'accés immediatament.", - "revoked": "Compartir revocat", - "revokeFailed": "No s'ha pogut revocar la compartició", - "joinCount": "{{count}} uneix-te", - "joinCount_other": "{{count}} s'uneix", - "expiresAt": "Caduca {{date}}", - "linkShareBadge": "Enllaç", - "userShareBadge": "Usuari: {{username}}", - "loadSharesFailed": "No s'han pogut carregar les comparticions actives", - "remoteDisplayUnavailable": "L'escriptori remot no està disponible ara mateix.", - "sharedWithMe": "Sessions compartides amb mi", - "customHoursPlaceholder": "Hores fins que caduca l'accés" - }, - "collab": { - "createRoom": "Nova habitació", - "roomName": "Nom de l'habitació", - "persistentRoom": "Sala persistent", - "persistentRoomHint": "Les sales persistents romanen a la llista després que finalitzi la reunió i es poden reutilitzar.", - "noRooms": "Encara no hi ha cap sala. Crea'n una per iniciar una reunió.", - "openRoom": "Obre", - "endRoom": "Finalitza la reunió", - "leaveRoom": "Marxar", - "deleteMember": "Elimina", - "removeMember": "Elimina {{name}}", - "removeMemberTitle": "Voleu eliminar aquest membre?", - "removeMemberDescription": "{{name}} perdrà l'accés a aquesta sala immediatament. Qualsevol visor SSH actiu es desconnectarà.", - "invite": "Convidar", - "inviteTitle": "Convida membres", - "members": "Membres", - "membersPanel": "Membres de la sala i sol·licituds de control", - "membersWithCount": "Membres ({{count}})", - "guestsWithCount": "Convidats ({{count}})", - "removeGuests": "Elimina els convidats", - "anonymousGuest": "Convidat", - "online": "en línia", - "offline": "fora de línia", - "you": "(tu)", - "hostBadge": "Amfitrió", - "presenterBadge": "Presentant", - "present": "Present", - "presentTitle": "Puja a l'escenari", - "presentHost": "Amfitrió", - "searchHosts": "Cerca amfitrions", - "noHostsFound": "No s'han trobat amfitrions disponibles.", - "presentProtocol": "Protocol", - "stopPresenting": "Atura la presentació", - "takeOver": "Prendre el control", - "takeOverConfirmTitle": "Voleu substituir la presentació actual?", - "takeOverDescription": "{{name}} està presentant ara. Prendre el control desconnectarà tothom d'aquesta etapa.", - "emptyStage": "Ningú fa una presentació. Puja a l'escenari per compartir una sessió.", - "stageLoading": "Connexió amb l'escenari...", - "stageEnded": "La presentació va acabar", - "roomEnded": "Aquesta reunió ha acabat", - "presenterLabel": "{{name}} presenta", - "guestLink": "Enllaç de convidat", - "guestLinkOn": "L'enllaç de convidat està activat. Qualsevol persona que tingui l'enllaç pot veure l'escenari.", - "guestLinkOff": "L'enllaç de convidat està desactivat.", - "copyLink": "Copia l'enllaç", - "rotateLink": "Gira l'enllaç", - "disableLink": "Desactiva l'enllaç", - "rotateLinkConfirmTitle": "Voleu girar l'enllaç de convidat?", - "rotateLinkDescription": "L'enllaç actual deixarà de funcionar immediatament. Els convidats SSH connectats es desconnectaran.", - "disableLinkConfirmTitle": "Voleu desactivar l'enllaç de convidat?", - "disableLinkDescription": "Els visors SSH anònims es desconnectaran immediatament i l'enllaç actual deixarà de funcionar.", - "linkCopied": "Enllaç copiat", - "roles": "Rols", - "users": "Usuaris", - "searchUsers": "Cerca usuaris", - "endConfirmTitle": "Voleu finalitzar aquesta reunió?", - "endDescription": "{{name}} finalitzarà per a tothom i no es podrà tornar a obrir.", - "endPersistentDescription": "La presentació activa a {{name}} s'aturarà. La sala romandrà disponible per a reutilització.", - "deleteRoom": "Suprimeix l'habitació", - "deleteConfirmTitle": "Voleu suprimir aquesta sala?", - "deleteDescription": "{{name}} s'eliminarà permanentment per a tothom i no es podrà desfer.", - "invitedTo": "Has estat convidat a \"{{name}}\"", - "guest": { - "title": "Reunió", - "waiting": "Esperant el presentador...", - "linkInvalid": "Aquest enllaç de convidat no és vàlid o la reunió ha finalitzat." - }, - "reopenFromPanel": "Aquesta pestanya de reunió ha caducat. Torna a obrir la sala des del tauler Reunions.", - "retry": "Torna-ho a intentar", - "requestControl": "Sol·licitud de control", - "cancelControlRequest": "Cancel·la la sol·licitud", - "controlQueue": "Sol·licituds de control", - "noControlRequests": "No hi ha sol·licituds pendents.", - "grantControlTo": "Dóna el control a {{name}}", - "dismissControlRequest": "Ignora la sol·licitud de control de {{name}}", - "releaseControl": "Control de llançament", - "grantControl": "Donar control", - "revokeControl": "Recupera el control", - "controlRequestedBy": "{{name}} va demanar control", - "grant": "Subvenció", - "controlBadge": "En control", - "youArePresenting": "Esteu presentant en una altra pestanya. Deixeu de presentar per cedir l'escenari.", - "created": "Sala creada", - "invited": "Membres convidats", - "roomTab": "Reunió", - "serverChanged": "El servidor de reunions ha canviat. Torna a obrir aquesta reunió des del tauler Reunions.", - "hostNotSynced": "Sincronitza aquest amfitrió amb el servidor de reunions abans de presentar.", - "linkRequiresServer": "Enllaça aquest escriptori a un servidor de Termix i crea-hi la reunió per compartir un enllaç web." - }, - "nav": { - "collab": "Reunions" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Permetre la compartició de sessions", - "description": "Permet que les sessions de terminal, RDP, VNC i Telnet en directe es comparteixin a tota la instància. Anul·la tots els activables i desactivables per compartir per host quan estan desactivats." - } - }, - "host": { - "allowSessionSharing": { - "label": "Permetre la compartició de sessions", - "description": "Permet que les sessions en directe d'aquest amfitrió es comparteixin mitjançant un enllaç o amb altres usuaris" - } - } - }, - "permissions": { - "use": { - "title": "Comparteix sessions i uneix-te a sales", - "description": "Comparteix sessions en directe mitjançant un enllaç o amb altres usuaris, i crea sales de col·laboració i uneix-te-hi." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Mai", - "oneHour": "1 hora", - "oneDay": "24 hores", - "sevenDays": "7 dies", - "thirtyDays": "30 dies", - "custom": "Personalitzat" - } - } - }, - "connections": { - "sectionSharedWithMe": "Compartit amb mi", - "sharedBy": "Compartit per {{username}}", - "join": "Uneix-te", - "sharedSessionLabel": "{{hostName}} (compartit)" - }, - "plugin": { - "name": "Compartició de sessions" - } -} diff --git a/plugins/session-sharing/locales/translated/cs_CZ.json b/plugins/session-sharing/locales/translated/cs_CZ.json deleted file mode 100644 index 693c13763..000000000 --- a/plugins/session-sharing/locales/translated/cs_CZ.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Připojování ke sdílené relaci...", - "linkInvalid": "Tento odkaz pro sdílení je neplatný, jeho platnost vypršela nebo byl zrušen.", - "rateLimited": "Příliš mnoho pokusů, zkuste to prosím znovu brzy", - "sessionEnded": "Tato relace skončila", - "readOnlyBadge": "Pouze zobrazení", - "ownerLabel": "Majitel hostitele" - }, - "modalTitle": "Sdílení relace", - "shareButton": "Podíl", - "notReadyToShare": "Relace ještě není připravena ke sdílení", - "modeTab": { - "link": "Odkaz", - "user": "Uživatel" - }, - "linkModeDescription": "Připojit se může kdokoli s tímto odkazem, účet není potřeba.", - "userModeDescription": "Sdílejte s konkrétním uživatelem, který již má k tomuto hostiteli přístup. Pokud ještě nemá přístup, nejprve s ním hostitele sdílejte nebo použijte odkaz. Po sdílení se relace zobrazí na jeho kartě Připojení.", - "permissionLevel": { - "label": "Úroveň oprávnění", - "readOnly": "Pouze pro čtení", - "readOnlyDescription": "Může sledovat relaci živě, ale nemůže psát ani interagovat.", - "readWrite": "Čtení a zápis", - "readWriteDescription": "Může psát a interagovat s relací stejně jako její vlastník." - }, - "expiryLabel": "Platnost vypršení platnosti odkazu", - "createLinkButton": "Vytvořit odkaz", - "createShareButton": "Sdílet s uživatelem", - "searchUsersPlaceholder": "Hledat uživatele...", - "noUsersFound": "Nenaleženi žádní uživatelé", - "linkCreated": "Odkaz pro sdílení vytvořen", - "linkNeedsServer": "Sdílení odkazů funguje pouze pro relace na serveru Termix. Pro sdílení tohoto hostitele pomocí odkazu jej připojte přes propojený server.", - "linkCopied": "Odkaz zkopírován do schránky", - "copyLink": "Kopírovat odkaz", - "shareCreated": "Relace sdílena. Zobrazí se na jejich kartě Připojení.", - "shareFailed": "Vytvoření sdílené položky se nezdařilo", - "userLacksHostAccess": "Tento uživatel zatím nemá přístup k tomuto hostiteli. Nejprve s ním hostitele sdílejte nebo použijte odkaz.", - "activeShares": "Aktivní akcie", - "noActiveShares": "Pro tuto relaci nejsou žádné aktivní sdílení", - "revoke": "Zrušit", - "revokeConfirmTitle": "Zrušit toto sdílení?", - "revokeConfirmDescription": "Každý, kdo tuto sdílenou složku používá, okamžitě ztratí přístup.", - "revoked": "Podíl zrušen", - "revokeFailed": "Zrušení sdílení se nezdařilo", - "joinCount": "{{count}} připojit se", - "joinCount_other": "{{count}} se připojí", - "expiresAt": "Platnost vyprší {{date}}", - "linkShareBadge": "Odkaz", - "userShareBadge": "Uživatel: {{username}}", - "loadSharesFailed": "Nepodařilo se načíst aktivní sdílení", - "remoteDisplayUnavailable": "Vzdálená plocha momentálně není k dispozici.", - "sharedWithMe": "Sezení sdílená se mnou", - "customHoursPlaceholder": "Hodiny do vypršení přístupu" - }, - "collab": { - "createRoom": "Nový pokoj", - "roomName": "Název místnosti", - "persistentRoom": "Trvalá místnost", - "persistentRoomHint": "Trvalé místnosti zůstanou uvedeny i po skončení schůzky a lze je znovu použít.", - "noRooms": "Zatím žádné místnosti. Vytvořte si novou a začněte schůzku.", - "openRoom": "OTEVŘENO", - "endRoom": "Ukončit schůzku", - "leaveRoom": "Dovolená", - "deleteMember": "Odstranit", - "removeMember": "Odebrat {{name}}", - "removeMemberTitle": "Odebrat tohoto člena?", - "removeMemberDescription": "{{name}} okamžitě ztratí přístup k této místnosti. Jakýkoli aktivní SSH prohlížeč bude odpojen.", - "invite": "Pozvat", - "inviteTitle": "Pozvat členy", - "members": "Členové", - "membersPanel": "Členové místnosti a požadavky na ovládání", - "membersWithCount": "Členové ({{count}})", - "guestsWithCount": "Hosté ({{count}})", - "removeGuests": "Odebrat hosty", - "anonymousGuest": "Host", - "online": "online", - "offline": "offline", - "you": "(Vy)", - "hostBadge": "Hostitel", - "presenterBadge": "Prezentace", - "present": "Současnost", - "presentTitle": "Vyjdi na pódium", - "presentHost": "Hostitel", - "searchHosts": "Vyhledávání hostitelů", - "noHostsFound": "Nebyli nalezeni žádní dostupní hostitelé.", - "presentProtocol": "Protokol", - "stopPresenting": "Zastavit prezentaci", - "takeOver": "Převzít", - "takeOverConfirmTitle": "Nahradit aktuální prezentaci?", - "takeOverDescription": "{{name}} právě vystupuje. Převzetí vystoupení odpojí všechny od dané scény.", - "emptyStage": "Nikdo neprezentuje. Vydejte se na pódium a sdílejte prezentaci.", - "stageLoading": "Připojení k pódiu...", - "stageEnded": "Prezentace skončila", - "roomEnded": "Tato schůzka skončila", - "presenterLabel": "{{name}} prezentuje", - "guestLink": "Odkaz pro hosty", - "guestLinkOn": "Odkaz pro hosty je zapnutý. Kdokoli s odkazem může sledovat pódium.", - "guestLinkOff": "Hostovský odkaz je vypnutý.", - "copyLink": "Kopírovat odkaz", - "rotateLink": "Otočit odkaz", - "disableLink": "Zakázat odkaz", - "rotateLinkConfirmTitle": "Otočit odkaz pro hosty?", - "rotateLinkDescription": "Aktuální odkaz okamžitě přestane fungovat. Připojení hosté přes SSH budou odpojeni.", - "disableLinkConfirmTitle": "Zakázat odkaz pro hosty?", - "disableLinkDescription": "Anonymní uživatelé SSH budou okamžitě odpojeni a aktuální odkaz přestane fungovat.", - "linkCopied": "Odkaz zkopírován", - "roles": "Role", - "users": "Uživatelé", - "searchUsers": "Vyhledat uživatele", - "endConfirmTitle": "Ukončit tuto schůzku?", - "endDescription": "{{name}} bude pro všechny ukončeno a nebude možné jej znovu otevřít.", - "endPersistentDescription": "Aktivní prezentace v {{name}} se zastaví. Místnost zůstane k dispozici pro opětovné použití.", - "deleteRoom": "Smazat místnost", - "deleteConfirmTitle": "Smazat tuto místnost?", - "deleteDescription": "{{name}} bude trvale smazáno pro všechny a nelze to vrátit zpět.", - "invitedTo": "Byli jste pozváni na „{{name}}“", - "guest": { - "title": "Zasedání", - "waiting": "Čekání na moderátora...", - "linkInvalid": "Tento odkaz pro hosty je neplatný nebo schůzka skončila." - }, - "reopenFromPanel": "Platnost této karty schůzky vypršela. Znovu otevřete místnost z panelu Schůzky.", - "retry": "Zkusit znovu", - "requestControl": "Požádat o kontrolu", - "cancelControlRequest": "Zrušit požadavek", - "controlQueue": "Žádosti o kontrolu", - "noControlRequests": "Žádné čekající žádosti.", - "grantControlTo": "Předejte kontrolu uživateli {{name}}", - "dismissControlRequest": "Zamítnout požadavek uživatele {{name}}na kontrolu", - "releaseControl": "Ovládání uvolnění", - "grantControl": "Předejte kontrolu", - "revokeControl": "Převezměte zpět kontrolu", - "controlRequestedBy": "{{name}} požádal o kontrolu", - "grant": "Grant", - "controlBadge": "Pod kontrolou", - "youArePresenting": "Prezentujete na jiné kartě. Ukončete prezentaci, abyste mohli předat scénu.", - "created": "Místnost vytvořena", - "invited": "Pozvaní členové", - "roomTab": "Zasedání", - "serverChanged": "Server schůzky se změnil. Znovu otevřete tuto schůzku z panelu Schůzky.", - "hostNotSynced": "Před prezentací synchronizujte tohoto hostitele se serverem schůzek.", - "linkRequiresServer": "Propojte tuto plochu se serverem Termix a vytvořte tam schůzku pro sdílení webového odkazu." - }, - "nav": { - "collab": "Schůzky" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Povolit sdílení relace", - "description": "Povolit sdílení relací živého terminálu, RDP, VNC a Telnet v rámci celé instance. Pokud je tato možnost zakázána, přepíše všechny přepínače sdílení pro jednotlivé hostitele." - } - }, - "host": { - "allowSessionSharing": { - "label": "Povolit sdílení relace", - "description": "Umožnit sdílení živých relací na tomto hostiteli prostřednictvím odkazu nebo s ostatními uživateli" - } - } - }, - "permissions": { - "use": { - "title": "Sdílení relací a připojení k místnostem", - "description": "Sdílejte živé relace prostřednictvím odkazu nebo s ostatními uživateli a vytvářejte a připojujte se k místnostem pro spolupráci." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Nikdy", - "oneHour": "1 hodina", - "oneDay": "24 hodin", - "sevenDays": "7 dní", - "thirtyDays": "30 dní", - "custom": "Zvyk" - } - } - }, - "connections": { - "sectionSharedWithMe": "Sdíleno se mnou", - "sharedBy": "Sdíleno uživatelem {{username}}", - "join": "Připojit se", - "sharedSessionLabel": "{{hostName}} (sdílené)" - }, - "plugin": { - "name": "Sdílení relace" - } -} diff --git a/plugins/session-sharing/locales/translated/da_DK.json b/plugins/session-sharing/locales/translated/da_DK.json deleted file mode 100644 index 69080675c..000000000 --- a/plugins/session-sharing/locales/translated/da_DK.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Forbinder til delt session...", - "linkInvalid": "Dette delingslink er ugyldigt, udløbet eller er blevet tilbagekaldt", - "rateLimited": "For mange forsøg, prøv igen om lidt", - "sessionEnded": "Denne session er afsluttet", - "readOnlyBadge": "Kun visning", - "ownerLabel": "Værtsejer" - }, - "modalTitle": "Del session", - "shareButton": "Dele", - "notReadyToShare": "Sessionen er ikke klar til deling endnu", - "modeTab": { - "link": "Forbindelse", - "user": "Bruger" - }, - "linkModeDescription": "Alle med dette link kan tilmelde sig, ingen konto kræves.", - "userModeDescription": "Del med en bestemt bruger, der allerede har adgang til denne vært. Hvis de ikke har adgang endnu, skal du først dele værten med dem eller bruge et link i stedet. Når sessionen er delt, vises den under deres fane Forbindelser.", - "permissionLevel": { - "label": "Tilladelsesniveau", - "readOnly": "Skrivebeskyttet", - "readOnlyDescription": "Kan se sessionen live, men kan ikke skrive eller interagere.", - "readWrite": "Læs-skriv", - "readWriteDescription": "Kan skrive og interagere med sessionen ligesom ejeren." - }, - "expiryLabel": "Linkudløb", - "createLinkButton": "Opret link", - "createShareButton": "Del med brugeren", - "searchUsersPlaceholder": "Søg efter brugere...", - "noUsersFound": "Ingen brugere fundet", - "linkCreated": "Delingslink oprettet", - "linkNeedsServer": "Delingslinks fungerer kun for sessioner på en Termix-server. Forbind denne vært via din linkede server for at dele den med et link.", - "linkCopied": "Link kopieret til udklipsholder", - "copyLink": "Kopiér link", - "shareCreated": "Session delt. Den vises i fanen Forbindelser.", - "shareFailed": "Kunne ikke oprette deling", - "userLacksHostAccess": "Den bruger har endnu ikke adgang til denne vært. Del værten med dem først, eller brug et link i stedet.", - "activeShares": "Aktive aktier", - "noActiveShares": "Ingen aktive delinger for denne session", - "revoke": "Tilbagekald", - "revokeConfirmTitle": "Tilbagekald denne deling?", - "revokeConfirmDescription": "Enhver, der bruger denne deling, mister adgangen med det samme.", - "revoked": "Deling tilbagekaldt", - "revokeFailed": "Delingen kunne ikke tilbagekaldes", - "joinCount": "{{count}} tilmeld dig", - "joinCount_other": "{{count}} deltager", - "expiresAt": "Udløber {{date}}", - "linkShareBadge": "Forbindelse", - "userShareBadge": "Bruger: {{username}}", - "loadSharesFailed": "Kunne ikke indlæse aktive delinger", - "remoteDisplayUnavailable": "Fjernskrivebord er ikke tilgængeligt lige nu.", - "sharedWithMe": "Sessioner delt med mig", - "customHoursPlaceholder": "Timer indtil adgangen udløber" - }, - "collab": { - "createRoom": "Nyt værelse", - "roomName": "Værelsesnavn", - "persistentRoom": "Permanent rum", - "persistentRoomHint": "Permanente rum forbliver på listen efter mødets afslutning og kan genbruges.", - "noRooms": "Ingen rum endnu. Opret et for at starte et møde.", - "openRoom": "Åben", - "endRoom": "Afslut mødet", - "leaveRoom": "Forlade", - "deleteMember": "Fjerne", - "removeMember": "Fjern {{name}}", - "removeMemberTitle": "Fjern dette medlem?", - "removeMemberDescription": "{{name}} mister adgangen til dette rum med det samme. Enhver aktiv SSH-seer vil blive afbrudt.", - "invite": "Invitere", - "inviteTitle": "Inviter medlemmer", - "members": "Medlemmer", - "membersPanel": "Rummedlemmer og kontrolanmodninger", - "membersWithCount": "Medlemmer ({{count}})", - "guestsWithCount": "Gæster ({{count}})", - "removeGuests": "Fjern gæster", - "anonymousGuest": "Gæst", - "online": "online", - "offline": "offline", - "you": "(du)", - "hostBadge": "Vært", - "presenterBadge": "Præsentation", - "present": "Nuværende", - "presentTitle": "Indtag scenen", - "presentHost": "Vært", - "searchHosts": "Søg efter værter", - "noHostsFound": "Ingen tilgængelige værter fundet.", - "presentProtocol": "Protokol", - "stopPresenting": "Stop med at præsentere", - "takeOver": "Overtag", - "takeOverConfirmTitle": "Erstat den nuværende præsentation?", - "takeOverDescription": "{{name}} præsenterer nu. Overtagelse vil afbryde alles forbindelse til den scene.", - "emptyStage": "Ingen præsenterer. Gå på scenen for at dele en session.", - "stageLoading": "Forbinder sig til scenen...", - "stageEnded": "Præsentationen sluttede", - "roomEnded": "Dette møde er afsluttet", - "presenterLabel": "{{name}} præsenterer", - "guestLink": "Gæstelink", - "guestLinkOn": "Gæstelinket er aktiveret. Alle med linket kan se scenen.", - "guestLinkOff": "Gæstelinket er slået fra.", - "copyLink": "Kopiér link", - "rotateLink": "Roter link", - "disableLink": "Deaktiver link", - "rotateLinkConfirmTitle": "Roter gæstelinket?", - "rotateLinkDescription": "Det nuværende link vil stoppe med at virke med det samme. Forbundne SSH-gæster vil blive afbrudt.", - "disableLinkConfirmTitle": "Deaktiver gæstelinket?", - "disableLinkDescription": "Anonyme SSH-seere vil blive afbrudt med det samme, og det nuværende link vil stoppe med at virke.", - "linkCopied": "Linket er kopieret", - "roles": "Roller", - "users": "Brugere", - "searchUsers": "Søg brugere", - "endConfirmTitle": "Afslutte dette møde?", - "endDescription": "{{name}} slutter for alle og kan ikke genåbnes.", - "endPersistentDescription": "Den aktive præsentation i {{name}} stopper. Rummet forbliver tilgængeligt til genbrug.", - "deleteRoom": "Slet rum", - "deleteConfirmTitle": "Slet dette rum?", - "deleteDescription": "{{name}} vil blive slettet permanent for alle og kan ikke fortrydes.", - "invitedTo": "Du blev inviteret til \"{{name}}\"", - "guest": { - "title": "Møde", - "waiting": "Venter på præsentatoren...", - "linkInvalid": "Dette gæstelink er ugyldigt, eller mødet er afsluttet" - }, - "reopenFromPanel": "Denne mødefane er udløbet. Genåbn rummet fra panelet Møder.", - "retry": "Prøv igen", - "requestControl": "Anmod om kontrol", - "cancelControlRequest": "Annuller anmodning", - "controlQueue": "Kontrolanmodninger", - "noControlRequests": "Ingen ventende anmodninger.", - "grantControlTo": "Giv kontrol til {{name}}", - "dismissControlRequest": "Afvis kontrolanmodningen fra {{name}}", - "releaseControl": "Slipkontrol", - "grantControl": "Giv kontrol", - "revokeControl": "Tag kontrollen tilbage", - "controlRequestedBy": "{{name}} bad om kontrol", - "grant": "Give", - "controlBadge": "I kontrol", - "youArePresenting": "Du præsenterer i en anden fane. Stop præsentationen for at overdrage scenen.", - "created": "Rum oprettet", - "invited": "Medlemmer inviteret", - "roomTab": "Møde", - "serverChanged": "Mødeserveren er ændret. Genåbn dette møde fra panelet Møder.", - "hostNotSynced": "Synkroniser denne vært med mødeserveren før præsentation.", - "linkRequiresServer": "Forbind dette skrivebord til en Termix-server, og opret mødet der for at dele et weblink." - }, - "nav": { - "collab": "Møder" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Tillad deling af sessioner", - "description": "Tillad deling af live terminal-, RDP-, VNC- og Telnet-sessioner på tværs af instanser. Tilsidesætter alle delingsindstillinger pr. vært, når de er deaktiveret." - } - }, - "host": { - "allowSessionSharing": { - "label": "Tillad deling af sessioner", - "description": "Tillad live-sessioner på denne vært at blive delt via link eller med andre brugere" - } - } - }, - "permissions": { - "use": { - "title": "Del sessioner og deltag i rum", - "description": "Del livesessioner via link eller med andre brugere, og opret og deltag i samarbejdsrum." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Aldrig", - "oneHour": "1 time", - "oneDay": "24 timer", - "sevenDays": "7 dage", - "thirtyDays": "30 dage", - "custom": "Skik" - } - } - }, - "connections": { - "sectionSharedWithMe": "Delt med mig", - "sharedBy": "Delt af {{username}}", - "join": "Deltag", - "sharedSessionLabel": "{{hostName}} (delt)" - }, - "plugin": { - "name": "Sessionsdeling" - } -} diff --git a/plugins/session-sharing/locales/translated/de_DE.json b/plugins/session-sharing/locales/translated/de_DE.json deleted file mode 100644 index 5fcbdee6b..000000000 --- a/plugins/session-sharing/locales/translated/de_DE.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Verbindung zur gemeinsamen Sitzung wird hergestellt...", - "linkInvalid": "Dieser Freigabelink ist ungültig, abgelaufen oder wurde widerrufen.", - "rateLimited": "Zu viele Versuche, bitte versuchen Sie es in Kürze erneut.", - "sessionEnded": "Diese Sitzung ist beendet.", - "readOnlyBadge": "Nur Ansicht", - "ownerLabel": "Gastgeber" - }, - "modalTitle": "Sitzung teilen", - "shareButton": "Teilen", - "notReadyToShare": "Die Sitzung ist noch nicht zur Freigabe bereit.", - "modeTab": { - "link": "Link", - "user": "Benutzer" - }, - "linkModeDescription": "Jeder mit diesem Link kann beitreten, ein Konto ist nicht erforderlich.", - "userModeDescription": "Teilen Sie die Sitzung mit einem bestimmten Benutzer, der bereits Zugriff auf diesen Host hat. Falls er noch keinen Zugriff hat, teilen Sie ihm den Host zuerst mit oder verwenden Sie stattdessen einen Link. Nach der Freigabe erscheint die Sitzung im Tab „Verbindungen“ des Benutzers.", - "permissionLevel": { - "label": "Berechtigungsstufe", - "readOnly": "Schreibgeschützt", - "readOnlyDescription": "Ich kann die Sitzung live verfolgen, aber nicht tippen oder interagieren.", - "readWrite": "Lesen und Schreiben", - "readWriteDescription": "Kann in der Sitzung genauso tippen und mit ihr interagieren wie der Besitzer." - }, - "expiryLabel": "Linkablauf", - "createLinkButton": "Verknüpfung erstellen", - "createShareButton": "Mit dem Nutzer teilen", - "searchUsersPlaceholder": "Benutzer suchen...", - "noUsersFound": "Keine Benutzer gefunden", - "linkCreated": "Link zum Teilen erstellt", - "linkNeedsServer": "Freigabelinks funktionieren nur für Sitzungen auf einem Termix-Server. Verbinden Sie diesen Host über Ihren verknüpften Server, um ihn per Link freizugeben.", - "linkCopied": "Link in die Zwischenablage kopiert", - "copyLink": "Link kopieren", - "shareCreated": "Die Sitzung wurde freigegeben. Sie wird im Tab „Verbindungen“ des Benutzers angezeigt.", - "shareFailed": "Fehler beim Erstellen der Freigabe", - "userLacksHostAccess": "Dieser Benutzer hat noch keinen Zugriff auf diesen Host. Teilen Sie ihm den Host zuerst mit oder verwenden Sie stattdessen einen Link.", - "activeShares": "Aktive Anteile", - "noActiveShares": "Keine aktiven Anteile für diese Sitzung", - "revoke": "Widerrufen", - "revokeConfirmTitle": "Diese Aktie widerrufen?", - "revokeConfirmDescription": "Wer diese Freigabe nutzt, verliert sofort den Zugriff.", - "revoked": "Anteil widerrufen", - "revokeFailed": "Aktienwiderruf fehlgeschlagen", - "joinCount": "{{count}} beitreten", - "joinCount_other": "{{count}} verbindet sich", - "expiresAt": "Läuft ab {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Benutzer: {{username}}", - "loadSharesFailed": "Fehler beim Laden der aktiven Anteile", - "remoteDisplayUnavailable": "Remote-Desktop ist momentan nicht verfügbar.", - "sharedWithMe": "Sitzungen, die mit mir geteilt wurden", - "customHoursPlaceholder": "Stunden bis zum Zugriffsablauf" - }, - "collab": { - "createRoom": "Neues Zimmer", - "roomName": "Zimmername", - "persistentRoom": "Persistenter Raum", - "persistentRoomHint": "Die gespeicherten Räume bleiben auch nach Ende des Meetings gelistet und können wiederverwendet werden.", - "noRooms": "Noch keine Räume vorhanden. Erstellen Sie einen, um ein Meeting zu starten.", - "openRoom": "Offen", - "endRoom": "Ende der Besprechung", - "leaveRoom": "Verlassen", - "deleteMember": "Entfernen", - "removeMember": "{{name}} entfernen", - "removeMemberTitle": "Dieses Mitglied entfernen?", - "removeMemberDescription": "{{name}} verliert sofort den Zugriff auf diesen Raum. Alle aktiven SSH-Viewer werden getrennt.", - "invite": "Einladen", - "inviteTitle": "Mitglieder einladen", - "members": "Mitglieder", - "membersPanel": "Raummitglieder und Kontrollanfragen", - "membersWithCount": "Mitglieder ({{count}})", - "guestsWithCount": "Gäste ({{count}})", - "removeGuests": "Gäste entfernen", - "anonymousGuest": "Gast", - "online": "online", - "offline": "offline", - "you": "(Du)", - "hostBadge": "Host", - "presenterBadge": "Präsentation", - "present": "Gegenwärtig", - "presentTitle": "Betretet die Bühne", - "presentHost": "Host", - "searchHosts": "Hosts suchen", - "noHostsFound": "Es wurden keine verfügbaren Hosts gefunden.", - "presentProtocol": "Protokoll", - "stopPresenting": "Hör auf zu präsentieren", - "takeOver": "Übernehmen", - "takeOverConfirmTitle": "Die aktuelle Präsentation ersetzen?", - "takeOverDescription": "{{name}} präsentiert sich jetzt. Die Übernahme trennt alle von dieser Bühne.", - "emptyStage": "Niemand präsentiert. Ergreife die Bühne, um eine Session zu teilen.", - "stageLoading": "Verbindung zur Bühne...", - "stageEnded": "Die Präsentation endete", - "roomEnded": "Diese Sitzung ist beendet.", - "presenterLabel": "{{name}} präsentiert", - "guestLink": "Gastlink", - "guestLinkOn": "Der Gastzugang ist aktiviert. Jeder mit dem Link kann die Bühnenshow verfolgen.", - "guestLinkOff": "Der Gastlink ist deaktiviert.", - "copyLink": "Link kopieren", - "rotateLink": "Drehverbindung", - "disableLink": "Link deaktivieren", - "rotateLinkConfirmTitle": "Den Gastlink drehen?", - "rotateLinkDescription": "Die bestehende Verbindung wird sofort unterbrochen. Verbundene SSH-Gäste werden getrennt.", - "disableLinkConfirmTitle": "Gastlink deaktivieren?", - "disableLinkDescription": "Anonyme SSH-Zugriffe werden sofort getrennt und die aktuelle Verbindung funktioniert nicht mehr.", - "linkCopied": "Link kopiert", - "roles": "Rollen", - "users": "Benutzer", - "searchUsers": "Nutzer suchen", - "endConfirmTitle": "Diese Sitzung beenden?", - "endDescription": "{{name}} wird für alle beendet und kann nicht wieder geöffnet werden.", - "endPersistentDescription": "Die aktive Präsentation in {{name}} wird beendet. Der Raum steht weiterhin zur erneuten Verwendung zur Verfügung.", - "deleteRoom": "Raum löschen", - "deleteConfirmTitle": "Diesen Raum löschen?", - "deleteDescription": "{{name}} wird für alle dauerhaft gelöscht und kann nicht rückgängig gemacht werden.", - "invitedTo": "Sie wurden eingeladen zu \"{{name}}\"", - "guest": { - "title": "Treffen", - "waiting": "Ich warte auf den Moderator...", - "linkInvalid": "Dieser Gastlink ist ungültig oder das Meeting ist beendet." - }, - "reopenFromPanel": "Dieser Besprechungsreiter ist abgelaufen. Öffnen Sie den Raum erneut über das Besprechungsmenü.", - "retry": "Erneut versuchen", - "requestControl": "Anforderungskontrolle", - "cancelControlRequest": "Stornierungsanfrage", - "controlQueue": "Kontrollanfragen", - "noControlRequests": "Keine offenen Anfragen.", - "grantControlTo": "Übergib die Kontrolle an {{name}}", - "dismissControlRequest": "Kontrollanfrage von {{name}}ablehnen", - "releaseControl": "Freigabekontrolle", - "grantControl": "Kontrolle abgeben", - "revokeControl": "Übernehmen Sie wieder die Kontrolle", - "controlRequestedBy": "{{name}} hat um Kontrolle gebeten", - "grant": "Gewähren", - "controlBadge": "Unter Kontrolle", - "youArePresenting": "Sie präsentieren in einem anderen Tab. Beenden Sie Ihre Präsentation, um die Bühne zu übergeben.", - "created": "Raum geschaffen", - "invited": "Mitglieder eingeladen", - "roomTab": "Treffen", - "serverChanged": "Der Meeting-Server wurde geändert. Öffnen Sie dieses Meeting erneut über das Meeting-Panel.", - "hostNotSynced": "Synchronisieren Sie diesen Host vor der Präsentation mit dem Meeting-Server.", - "linkRequiresServer": "Verbinden Sie diesen Desktop mit einem Termix-Server und erstellen Sie dort das Meeting, um einen Weblink freizugeben." - }, - "nav": { - "collab": "Treffen" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Sitzungsfreigabe zulassen", - "description": "Ermöglicht die instanzweite Freigabe von Live-Terminal-, RDP-, VNC- und Telnet-Sitzungen. Überschreibt alle Freigabeeinstellungen pro Host, wenn diese deaktiviert sind." - } - }, - "host": { - "allowSessionSharing": { - "label": "Sitzungsfreigabe zulassen", - "description": "Ermöglichen Sie es, Live-Sitzungen auf diesem Host per Link oder mit anderen Benutzern zu teilen." - } - } - }, - "permissions": { - "use": { - "title": "Sitzungen teilen und Räumen beitreten", - "description": "Teilen Sie Live-Sitzungen per Link oder mit anderen Nutzern und erstellen und treten Sie Kollaborationsräumen bei." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Nie", - "oneHour": "1 Stunde", - "oneDay": "24 Stunden", - "sevenDays": "7 Tage", - "thirtyDays": "30 Tage", - "custom": "Benutzerdefiniert" - } - } - }, - "connections": { - "sectionSharedWithMe": "Mit mir geteilt", - "sharedBy": "Geteilt von {{username}}", - "join": "Verbinden", - "sharedSessionLabel": "{{hostName}} (gemeinsam genutzt)" - }, - "plugin": { - "name": "Sitzungsteilung" - } -} diff --git a/plugins/session-sharing/locales/translated/el_GR.json b/plugins/session-sharing/locales/translated/el_GR.json deleted file mode 100644 index ce0055e54..000000000 --- a/plugins/session-sharing/locales/translated/el_GR.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Σύνδεση σε κοινόχρηστη συνεδρία...", - "linkInvalid": "Αυτός ο σύνδεσμος κοινής χρήσης δεν είναι έγκυρος, έχει λήξει ή έχει ανακληθεί", - "rateLimited": "Πάρα πολλές προσπάθειες, δοκιμάστε ξανά σύντομα.", - "sessionEnded": "Αυτή η συνεδρία έληξε", - "readOnlyBadge": "Μόνο προβολή", - "ownerLabel": "Κάτοχος κεντρικού υπολογιστή" - }, - "modalTitle": "Κοινή χρήση περιόδου σύνδεσης", - "shareButton": "Μερίδιο", - "notReadyToShare": "Η συνεδρία δεν είναι ακόμη έτοιμη για κοινή χρήση", - "modeTab": { - "link": "Σύνδεσμος", - "user": "Μεταχειριζόμενος" - }, - "linkModeDescription": "Οποιοσδήποτε έχει αυτόν τον σύνδεσμο μπορεί να συμμετάσχει, δεν απαιτείται λογαριασμός.", - "userModeDescription": "Κοινοποίηση με έναν συγκεκριμένο χρήστη που έχει ήδη πρόσβαση σε αυτόν τον κεντρικό υπολογιστή. Εάν δεν έχει ακόμη πρόσβαση, μοιραστείτε πρώτα τον κεντρικό υπολογιστή μαζί του ή χρησιμοποιήστε έναν σύνδεσμο. Μόλις κοινοποιηθεί, η συνεδρία εμφανίζεται στην καρτέλα Συνδέσεις.", - "permissionLevel": { - "label": "Επίπεδο δικαιωμάτων", - "readOnly": "Μόνο για ανάγνωση", - "readOnlyDescription": "Μπορώ να παρακολουθήσω την συνεδρία ζωντανά, αλλά δεν μπορώ να πληκτρολογήσω ή να αλληλεπιδράσω.", - "readWrite": "Ανάγνωση-εγγραφή", - "readWriteDescription": "Μπορεί να πληκτρολογήσει και να αλληλεπιδράσει με την περίοδο λειτουργίας όπως ο κάτοχος." - }, - "expiryLabel": "Λήξη σύνδεσης", - "createLinkButton": "Δημιουργία συνδέσμου", - "createShareButton": "Κοινοποίηση με χρήστη", - "searchUsersPlaceholder": "Αναζήτηση χρηστών...", - "noUsersFound": "Δεν βρέθηκαν χρήστες", - "linkCreated": "Ο σύνδεσμος κοινής χρήσης δημιουργήθηκε", - "linkNeedsServer": "Οι σύνδεσμοι κοινής χρήσης λειτουργούν μόνο για συνεδρίες σε διακομιστή Termix. Συνδέστε αυτόν τον κεντρικό υπολογιστή μέσω του συνδεδεμένου διακομιστή σας για να τον κοινοποιήσετε με έναν σύνδεσμο.", - "linkCopied": "Ο σύνδεσμος αντιγράφηκε στο πρόχειρο", - "copyLink": "Αντιγραφή συνδέσμου", - "shareCreated": "Η συνεδρία κοινοποιήθηκε. Θα εμφανιστεί στην καρτέλα \"Συνδέσεις\".", - "shareFailed": "Η δημιουργία κοινής χρήσης απέτυχε", - "userLacksHostAccess": "Αυτός ο χρήστης δεν έχει ακόμη πρόσβαση σε αυτόν τον κεντρικό υπολογιστή. Κοινοποιήστε πρώτα τον κεντρικό υπολογιστή μαζί του ή χρησιμοποιήστε έναν σύνδεσμο.", - "activeShares": "Ενεργές κοινοποιήσεις", - "noActiveShares": "Δεν υπάρχουν ενεργές κοινοποιήσεις για αυτήν την περίοδο σύνδεσης", - "revoke": "Ανακαλώ", - "revokeConfirmTitle": "Ανάκληση αυτής της κοινοποίησης;", - "revokeConfirmDescription": "Όποιος χρησιμοποιεί αυτήν την κοινοποίηση θα χάσει αμέσως την πρόσβαση.", - "revoked": "Η κοινοποίηση ανακλήθηκε", - "revokeFailed": "Η ανάκληση της κοινής χρήσης απέτυχε", - "joinCount": "{{count}} συμμετοχή", - "joinCount_other": "{{count}} ενώσεις", - "expiresAt": "Λήγει {{date}}", - "linkShareBadge": "Σύνδεσμος", - "userShareBadge": "Χρήστης: {{username}}", - "loadSharesFailed": "Αποτυχία φόρτωσης ενεργών κοινόχρηστων στοιχείων", - "remoteDisplayUnavailable": "Η απομακρυσμένη επιφάνεια εργασίας δεν είναι διαθέσιμη αυτήν τη στιγμή.", - "sharedWithMe": "Συνεδρίες που κοινοποιήθηκαν σε εμένα", - "customHoursPlaceholder": "Ώρες μέχρι τη λήξη της πρόσβασης" - }, - "collab": { - "createRoom": "Νέο δωμάτιο", - "roomName": "Όνομα δωματίου", - "persistentRoom": "Μόνιμο δωμάτιο", - "persistentRoomHint": "Οι μόνιμες αίθουσες παραμένουν καταχωρημένες μετά το τέλος της σύσκεψης και μπορούν να επαναχρησιμοποιηθούν.", - "noRooms": "Δεν υπάρχουν ακόμη αίθουσες. Δημιουργήστε μία για να ξεκινήσετε μια σύσκεψη.", - "openRoom": "Ανοιχτό", - "endRoom": "Λήξη συνάντησης", - "leaveRoom": "Αδεια", - "deleteMember": "Αφαιρώ", - "removeMember": "Αφαίρεση {{name}}", - "removeMemberTitle": "Κατάργηση αυτού του μέλους;", - "removeMemberDescription": "Ο χρήστης {{name}} θα χάσει αμέσως την πρόσβαση σε αυτό το δωμάτιο. Οποιοσδήποτε ενεργός χρήστης προβολής SSH θα αποσυνδεθεί.", - "invite": "Καλώ", - "inviteTitle": "Πρόσκληση μελών", - "members": "Μέλη", - "membersPanel": "Μέλη δωματίου και αιτήματα ελέγχου", - "membersWithCount": "Μέλη ({{count}})", - "guestsWithCount": "Επισκέπτες ({{count}})", - "removeGuests": "Αφαίρεση επισκεπτών", - "anonymousGuest": "Επισκέπτης", - "online": "διαδικτυακά", - "offline": "εκτός σύνδεσης", - "you": "(εσείς)", - "hostBadge": "Πλήθος", - "presenterBadge": "Παρουσίαση", - "present": "Παρόν", - "presentTitle": "Ανέβα στη σκηνή", - "presentHost": "Πλήθος", - "searchHosts": "Αναζήτηση κεντρικών υπολογιστών", - "noHostsFound": "Δεν βρέθηκαν διαθέσιμοι κεντρικοί υπολογιστές.", - "presentProtocol": "Πρωτόκολλο", - "stopPresenting": "Διακοπή παρουσίασης", - "takeOver": "Αναλαμβάνω", - "takeOverConfirmTitle": "Αντικατάσταση της τρέχουσας παρουσίασης;", - "takeOverDescription": "Το {{name}} παρουσιάζεται τώρα. Η ανάληψη του ελέγχου θα αποσυνδέσει όλους από αυτό το στάδιο.", - "emptyStage": "Κανείς δεν κάνει παρουσίαση. Ανεβείτε στη σκηνή για να μοιραστείτε μια συνεδρία.", - "stageLoading": "Σύνδεση με τη σκηνή...", - "stageEnded": "Η παρουσίαση έληξε", - "roomEnded": "Αυτή η συνάντηση έληξε", - "presenterLabel": "{{name}} παρουσιάζει", - "guestLink": "Σύνδεσμος επισκέπτη", - "guestLinkOn": "Ο σύνδεσμος επισκεπτών είναι ενεργοποιημένος. Όποιος έχει τον σύνδεσμο μπορεί να παρακολουθήσει τη σκηνή.", - "guestLinkOff": "Ο σύνδεσμος επισκέπτη είναι απενεργοποιημένος.", - "copyLink": "Αντιγραφή συνδέσμου", - "rotateLink": "Περιστροφή συνδέσμου", - "disableLink": "Απενεργοποίηση συνδέσμου", - "rotateLinkConfirmTitle": "Εναλλαγή του συνδέσμου επισκέπτη;", - "rotateLinkDescription": "Ο τρέχων σύνδεσμος θα σταματήσει να λειτουργεί αμέσως. Οι συνδεδεμένοι επισκέπτες SSH θα αποσυνδεθούν.", - "disableLinkConfirmTitle": "Απενεργοποίηση του συνδέσμου επισκέπτη;", - "disableLinkDescription": "Οι ανώνυμοι θεατές SSH θα αποσυνδεθούν αμέσως και ο τρέχων σύνδεσμος θα σταματήσει να λειτουργεί.", - "linkCopied": "Ο σύνδεσμος αντιγράφηκε", - "roles": "Ρόλοι", - "users": "Χρήστες", - "searchUsers": "Αναζήτηση χρηστών", - "endConfirmTitle": "Λήξη αυτής της συνάντησης;", - "endDescription": "Το {{name}} θα λήξει για όλους και δεν μπορεί να ανοίξει ξανά.", - "endPersistentDescription": "Η ενεργή παρουσίαση στο {{name}} θα σταματήσει. Η αίθουσα παραμένει διαθέσιμη για επαναχρησιμοποίηση.", - "deleteRoom": "Διαγραφή δωματίου", - "deleteConfirmTitle": "Διαγραφή αυτού του δωματίου;", - "deleteDescription": "Η εντολή {{name}} θα διαγραφεί οριστικά για όλους και δεν θα είναι δυνατή η αναίρεση της.", - "invitedTo": "Σας προσκάλεσαν στο \"{{name}}\"", - "guest": { - "title": "Συνάντηση", - "waiting": "Περιμένοντας τον παρουσιαστή...", - "linkInvalid": "Αυτός ο σύνδεσμος επισκέπτη δεν είναι έγκυρος ή η σύσκεψη έχει λήξει" - }, - "reopenFromPanel": "Αυτή η καρτέλα σύσκεψης έληξε. Ανοίξτε ξανά την αίθουσα από τον πίνακα \"Συνέδρια\".", - "retry": "Δοκιμάζω πάλι", - "requestControl": "Αίτημα ελέγχου", - "cancelControlRequest": "Αίτημα ακύρωσης", - "controlQueue": "Αιτήματα ελέγχου", - "noControlRequests": "Δεν υπάρχουν εκκρεμή αιτήματα.", - "grantControlTo": "Δώστε τον έλεγχο σε {{name}}", - "dismissControlRequest": "Απόρριψη αιτήματος ελέγχου του {{name}}", - "releaseControl": "Έλεγχος απελευθέρωσης", - "grantControl": "Δώστε τον έλεγχο", - "revokeControl": "Πάρε πίσω τον έλεγχο", - "controlRequestedBy": "{{name}} ζήτησε έλεγχο", - "grant": "Χορήγηση", - "controlBadge": "Υπό έλεγχο", - "youArePresenting": "Κάνετε παρουσίαση σε άλλη καρτέλα. Διακόψτε την παρουσίαση για να παραδώσετε τη σκηνή.", - "created": "Δημιουργήθηκε δωμάτιο", - "invited": "Μέλη που έχουν προσκληθεί", - "roomTab": "Συνάντηση", - "serverChanged": "Ο διακομιστής συσκέψεων άλλαξε. Ανοίξτε ξανά αυτήν τη σύσκεψη από τον πίνακα \"Συσκέψεις\".", - "hostNotSynced": "Συγχρονίστε αυτόν τον κεντρικό υπολογιστή με τον διακομιστή συσκέψεων πριν από την παρουσίαση.", - "linkRequiresServer": "Συνδέστε αυτήν την επιφάνεια εργασίας με έναν διακομιστή Termix και δημιουργήστε τη σύσκεψη εκεί για να μοιραστείτε έναν σύνδεσμο ιστού." - }, - "nav": { - "collab": "Συναντήσεις" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Επιτρέψτε την κοινή χρήση περιόδου σύνδεσης", - "description": "Επιτρέπεται η κοινή χρήση των ζωντανών συνεδριών τερματικού, RDP, VNC και Telnet σε ολόκληρη την παρουσία. Παρακάμπτει κάθε εναλλαγή κοινής χρήσης ανά κεντρικό υπολογιστή όταν είναι απενεργοποιημένη." - } - }, - "host": { - "allowSessionSharing": { - "label": "Επιτρέψτε την κοινή χρήση περιόδου σύνδεσης", - "description": "Να επιτρέπεται η κοινοποίηση ζωντανών συνεδριών σε αυτόν τον πάροχο μέσω συνδέσμου ή με άλλους χρήστες" - } - } - }, - "permissions": { - "use": { - "title": "Κοινή χρήση συνεδριών και συμμετοχή σε αίθουσες", - "description": "Μοιραστείτε ζωντανές συνεδρίες μέσω συνδέσμου ή με άλλους χρήστες και δημιουργήστε και συμμετάσχετε σε αίθουσες συνεργασίας." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Ποτέ", - "oneHour": "1 ώρα", - "oneDay": "24 ώρες", - "sevenDays": "7 ημέρες", - "thirtyDays": "30 ημέρες", - "custom": "Εθιμο" - } - } - }, - "connections": { - "sectionSharedWithMe": "Κοινοποιήθηκε σε εμένα", - "sharedBy": "Κοινοποίηση από {{username}}", - "join": "Ενώνω", - "sharedSessionLabel": "{{hostName}} (κοινόχρηστο)" - }, - "plugin": { - "name": "Κοινή χρήση περιόδου σύνδεσης" - } -} diff --git a/plugins/session-sharing/locales/translated/es_ES.json b/plugins/session-sharing/locales/translated/es_ES.json deleted file mode 100644 index 3d37077c1..000000000 --- a/plugins/session-sharing/locales/translated/es_ES.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Conectando con la sesión compartida...", - "linkInvalid": "Este enlace para compartir no es válido, ha caducado o ha sido revocado.", - "rateLimited": "Demasiados intentos, por favor inténtelo de nuevo en breve.", - "sessionEnded": "Esta sesión ha finalizado.", - "readOnlyBadge": "Solo lectura", - "ownerLabel": "Propietario del anfitrión" - }, - "modalTitle": "Compartir sesión", - "shareButton": "Compartir", - "notReadyToShare": "La sesión aún no está lista para compartirse.", - "modeTab": { - "link": "Enlace", - "user": "Usuario" - }, - "linkModeDescription": "Cualquier persona con este enlace puede unirse, no se requiere cuenta.", - "userModeDescription": "Comparte la sesión con un usuario específico que ya tenga acceso a este host. Si aún no tiene acceso, comparte primero el host con él o utiliza un enlace. Una vez compartida, la sesión aparecerá en su pestaña Conexiones.", - "permissionLevel": { - "label": "Nivel de permiso", - "readOnly": "Solo lectura", - "readOnlyDescription": "Se puede ver la sesión en directo, pero no se puede escribir ni interactuar.", - "readWrite": "Leer y escribir", - "readWriteDescription": "Puede escribir e interactuar con la sesión igual que el propietario." - }, - "expiryLabel": "Caducidad del enlace", - "createLinkButton": "Crear enlace", - "createShareButton": "Compartir con el usuario", - "searchUsersPlaceholder": "Buscar usuarios...", - "noUsersFound": "No se encontraron usuarios", - "linkCreated": "Enlace para compartir creado", - "linkNeedsServer": "Los enlaces para compartir solo funcionan para sesiones en un servidor Termix. Conecta este host a través de tu servidor vinculado para compartirlo mediante un enlace.", - "linkCopied": "Enlace copiado al portapapeles", - "copyLink": "Copiar enlace", - "shareCreated": "Sesión compartida. Aparecerá en su pestaña Conexiones.", - "shareFailed": "No se pudo crear el recurso compartido", - "userLacksHostAccess": "Ese usuario aún no tiene acceso a este servidor. Primero, comparte el servidor con él o utiliza un enlace.", - "activeShares": "Acciones activas", - "noActiveShares": "No hay acciones activas para esta sesión.", - "revoke": "Revocar", - "revokeConfirmTitle": "¿Revocar esta acción?", - "revokeConfirmDescription": "Cualquier persona que utilice este recurso compartido perderá el acceso inmediatamente.", - "revoked": "Acción revocada", - "revokeFailed": "No se pudo revocar la participación.", - "joinCount": "{{count}} unirse", - "joinCount_other": "{{count}} se une", - "expiresAt": "Caduca {{date}}", - "linkShareBadge": "Enlace", - "userShareBadge": "Usuario: {{username}}", - "loadSharesFailed": "No se pudieron cargar los recursos compartidos activos.", - "remoteDisplayUnavailable": "El acceso remoto al escritorio no está disponible en este momento.", - "sharedWithMe": "Sesiones compartidas conmigo", - "customHoursPlaceholder": "Horas hasta que caduque el acceso" - }, - "collab": { - "createRoom": "Habitación nueva", - "roomName": "Nombre de la habitación", - "persistentRoom": "Habitación persistente", - "persistentRoomHint": "Las salas reservadas permanecen en la lista una vez finalizada la reunión y pueden volver a utilizarse.", - "noRooms": "Aún no hay salas disponibles. Crea una para iniciar una reunión.", - "openRoom": "Abrir", - "endRoom": "Fin de la reunión", - "leaveRoom": "Dejar", - "deleteMember": "Quitar", - "removeMember": "Eliminar {{name}}", - "removeMemberTitle": "¿Eliminar a este miembro?", - "removeMemberDescription": "{{name}} perderá el acceso a esta sala inmediatamente. Cualquier visor SSH activo será desconectado.", - "invite": "Invitar", - "inviteTitle": "Invitar a los miembros", - "members": "Miembros", - "membersPanel": "Miembros de la sala y solicitudes de control", - "membersWithCount": "Miembros ({{count}})", - "guestsWithCount": "Invitados ({{count}})", - "removeGuests": "Eliminar invitados", - "anonymousGuest": "Invitado", - "online": "en línea", - "offline": "desconectado", - "you": "(tú)", - "hostBadge": "Host", - "presenterBadge": "Presentación", - "present": "Presente", - "presentTitle": "Sube al escenario", - "presentHost": "Host", - "searchHosts": "Buscar hosts", - "noHostsFound": "No se encontraron hosts disponibles.", - "presentProtocol": "Protocolo", - "stopPresenting": "Deja de presentar", - "takeOver": "Tomar el control", - "takeOverConfirmTitle": "¿Reemplazar la presentación actual?", - "takeOverDescription": "{{name}} se está presentando ahora. Tomar el control desconectará a todos de ese escenario.", - "emptyStage": "Nadie va a presentar. Sube al escenario para compartir una sesión.", - "stageLoading": "Conectando con el escenario...", - "stageEnded": "La presentación terminó", - "roomEnded": "Esta reunión ha terminado.", - "presenterLabel": "{{name}} está presentando", - "guestLink": "Enlace de invitado", - "guestLinkOn": "El enlace para invitados está activado. Cualquiera que tenga el enlace puede ver el espectáculo.", - "guestLinkOff": "El enlace de invitado está desactivado.", - "copyLink": "Copiar enlace", - "rotateLink": "Enlace giratorio", - "disableLink": "Deshabilitar enlace", - "rotateLinkConfirmTitle": "¿Rotar el enlace de invitado?", - "rotateLinkDescription": "El enlace actual dejará de funcionar inmediatamente. Los invitados SSH conectados serán desconectados.", - "disableLinkConfirmTitle": "¿Deshabilitar el enlace de invitado?", - "disableLinkDescription": "Los usuarios que intenten acceder de forma anónima mediante SSH serán desconectados inmediatamente y el enlace actual dejará de funcionar.", - "linkCopied": "Enlace copiado", - "roles": "Roles", - "users": "Usuarios", - "searchUsers": "Buscar usuarios", - "endConfirmTitle": "¿Dar por terminada esta reunión?", - "endDescription": "{{name}} finalizará para todos y no podrá reabrirse.", - "endPersistentDescription": "La presentación activa en {{name}} se detendrá. La sala permanece disponible para su reutilización.", - "deleteRoom": "Eliminar habitación", - "deleteConfirmTitle": "¿Eliminar esta habitación?", - "deleteDescription": "{{name}} se eliminará permanentemente para todos y no se podrá deshacer.", - "invitedTo": "Te invitaron a \"{{name}}\"", - "guest": { - "title": "Reunión", - "waiting": "Esperando al presentador...", - "linkInvalid": "Este enlace de invitado no es válido o la reunión ha finalizado." - }, - "reopenFromPanel": "Esta pestaña de reunión ha caducado. Vuelva a abrir la sala desde el panel de Reuniones.", - "retry": "Reintentar", - "requestControl": "Control de solicitudes", - "cancelControlRequest": "Cancelar solicitud", - "controlQueue": "Solicitudes de control", - "noControlRequests": "No hay solicitudes pendientes.", - "grantControlTo": "Cede el control a {{name}}", - "dismissControlRequest": "Rechazar la solicitud de control de {{name}}", - "releaseControl": "Control de liberación", - "grantControl": "Cede el control", - "revokeControl": "Recupera el control", - "controlRequestedBy": "{{name}} pidió control", - "grant": "Conceder", - "controlBadge": "En control", - "youArePresenting": "Estás presentando en otra pestaña. Deja de presentar para ceder la palabra.", - "created": "Habitación creada", - "invited": "Miembros invitados", - "roomTab": "Reunión", - "serverChanged": "El servidor de la reunión ha cambiado. Vuelva a abrir esta reunión desde el panel de Reuniones.", - "hostNotSynced": "Sincroniza este host con el servidor de la reunión antes de la presentación.", - "linkRequiresServer": "Conecta este ordenador de escritorio a un servidor Termix y crea la reunión allí para compartir un enlace web." - }, - "nav": { - "collab": "Reuniones" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Permitir compartir la sesión", - "description": "Permite compartir sesiones de terminal en vivo, RDP, VNC y Telnet en toda la instancia. Cuando está desactivado, anula cualquier configuración de uso compartido por host." - } - }, - "host": { - "allowSessionSharing": { - "label": "Permitir compartir la sesión", - "description": "Permitir que las sesiones en vivo en este servidor se compartan mediante un enlace o con otros usuarios." - } - } - }, - "permissions": { - "use": { - "title": "Comparte sesiones y únete a salas", - "description": "Comparte sesiones en directo mediante un enlace o con otros usuarios, y crea y únete a salas de colaboración." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Nunca", - "oneHour": "1 hora", - "oneDay": "24 horas", - "sevenDays": "7 días", - "thirtyDays": "30 días", - "custom": "Personalizado" - } - } - }, - "connections": { - "sectionSharedWithMe": "Compartido conmigo", - "sharedBy": "Compartido por {{username}}", - "join": "Unirse", - "sharedSessionLabel": "{{hostName}} (compartido)" - }, - "plugin": { - "name": "Compartir la sesión" - } -} diff --git a/plugins/session-sharing/locales/translated/fi_FI.json b/plugins/session-sharing/locales/translated/fi_FI.json deleted file mode 100644 index b4a671b03..000000000 --- a/plugins/session-sharing/locales/translated/fi_FI.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Yhdistetään jaettuun istuntoon...", - "linkInvalid": "Tämä jakolinkki on virheellinen, vanhentunut tai se on peruutettu.", - "rateLimited": "Liikaa yrityksiä, yritä uudelleen pian", - "sessionEnded": "Tämä istunto on päättynyt", - "readOnlyBadge": "Vain katselu", - "ownerLabel": "Isännän omistaja" - }, - "modalTitle": "Jaa istunto", - "shareButton": "Jakaa", - "notReadyToShare": "Istunto ei ole vielä valmis jaettavaksi", - "modeTab": { - "link": "Linkki", - "user": "Käyttäjä" - }, - "linkModeDescription": "Kuka tahansa, jolla on tämä linkki, voi liittyä, tiliä ei tarvita.", - "userModeDescription": "Jaa tietyn käyttäjän kanssa, jolla on jo pääsy tähän isäntään. Jos heillä ei vielä ole pääsyä, jaa isäntä ensin heidän kanssaan tai käytä linkkiä. Jakamisen jälkeen istunto näkyy heidän Yhteydet-välilehdellään.", - "permissionLevel": { - "label": "Käyttöoikeustaso", - "readOnly": "Vain lukuoikeus", - "readOnlyDescription": "Voit katsoa istuntoa suorana, mutta et voi kirjoittaa tai olla vuorovaikutuksessa.", - "readWrite": "Lue ja kirjoita", - "readWriteDescription": "Voi kirjoittaa ja olla vuorovaikutuksessa istunnon kanssa aivan kuten omistaja." - }, - "expiryLabel": "Linkin vanheneminen", - "createLinkButton": "Luo linkki", - "createShareButton": "Jaa käyttäjän kanssa", - "searchUsersPlaceholder": "Hae käyttäjiä...", - "noUsersFound": "Ei käyttäjiä löytynyt", - "linkCreated": "Jaa linkki luotu", - "linkNeedsServer": "Jakolinkit toimivat vain Termix-palvelimella olevissa istunnoissa. Yhdistä tämä isäntä linkitetyn palvelimesi kautta jakaaksesi sen linkillä.", - "linkCopied": "Linkki kopioitu leikepöydälle", - "copyLink": "Kopioi linkki", - "shareCreated": "Istunto jaettu. Se näkyy heidän Yhteydet-välilehdellä.", - "shareFailed": "Jaon luominen epäonnistui", - "userLacksHostAccess": "Käyttäjällä ei ole vielä pääsyä tähän isäntään. Jaa isäntä ensin heidän kanssaan tai käytä linkkiä.", - "activeShares": "Aktiiviset jaot", - "noActiveShares": "Ei aktiivisia jakoja tällä istunnolla", - "revoke": "Peruuttaa", - "revokeConfirmTitle": "Perutaanko tämä jako?", - "revokeConfirmDescription": "Kuka tahansa tätä jakoa käyttävä menettää käyttöoikeuden välittömästi.", - "revoked": "Jako peruttu", - "revokeFailed": "Jakamisen peruuttaminen epäonnistui", - "joinCount": "{{count}} liity", - "joinCount_other": "{{count}} liitokset", - "expiresAt": "Vanhenee {{date}}", - "linkShareBadge": "Linkki", - "userShareBadge": "Käyttäjä: {{username}}", - "loadSharesFailed": "Aktiivisten jakojen lataaminen epäonnistui", - "remoteDisplayUnavailable": "Etätyöpöytä ei ole käytettävissä juuri nyt.", - "sharedWithMe": "Kanssani jaetut sessiot", - "customHoursPlaceholder": "Tuntia käyttöoikeuden vanhenemiseen" - }, - "collab": { - "createRoom": "Uusi huone", - "roomName": "Huoneen nimi", - "persistentRoom": "Pysyvä huone", - "persistentRoomHint": "Pysyvät huoneet pysyvät luettelossa kokouksen päättymisen jälkeen ja niitä voidaan käyttää uudelleen.", - "noRooms": "Ei vielä huoneita. Luo huone aloittaaksesi kokouksen.", - "openRoom": "Avata", - "endRoom": "Kokouksen loppu", - "leaveRoom": "Lähtö", - "deleteMember": "Poistaa", - "removeMember": "Poista {{name}}", - "removeMemberTitle": "Poistetaanko tämä jäsen?", - "removeMemberDescription": "{{name}} menettää pääsyn tähän huoneeseen välittömästi. Kaikki aktiiviset SSH-katseluohjelmat katkaistaan.", - "invite": "Kutsu", - "inviteTitle": "Kutsu jäseniä", - "members": "Jäsenet", - "membersPanel": "Huoneen jäsenet ja hallintapyynnöt", - "membersWithCount": "Jäsenet ({{count}})", - "guestsWithCount": "Vieraat ({{count}})", - "removeGuests": "Poista vieraat", - "anonymousGuest": "Vieras", - "online": "verkossa", - "offline": "offline-tilassa", - "you": "(sinä)", - "hostBadge": "Isäntä", - "presenterBadge": "Esittäminen", - "present": "Esittää", - "presentTitle": "Nouse lavalle", - "presentHost": "Isäntä", - "searchHosts": "Hae isäntiä", - "noHostsFound": "Ei saatavilla olevia isäntiä.", - "presentProtocol": "Protokolla", - "stopPresenting": "Lopeta esittäminen", - "takeOver": "Ota haltuunsa", - "takeOverConfirmTitle": "Korvataanko nykyinen esitys?", - "takeOverDescription": "{{name}} esittää nyt. Hallinnan ottaminen irrottaa kaikki yhteydet kyseiseen vaiheeseen.", - "emptyStage": "Kukaan ei esitä. Nouse lavalle ja jaa sessio.", - "stageLoading": "Yhdistetään lavalle...", - "stageEnded": "Esitys päättyi", - "roomEnded": "Tämä kokous on päättynyt", - "presenterLabel": "{{name}} esittää", - "guestLink": "Vieraslinkki", - "guestLinkOn": "Vieraslinkki on käytössä. Kuka tahansa linkin omaava voi katsoa lavaa.", - "guestLinkOff": "Vierailijalinkki on pois päältä.", - "copyLink": "Kopioi linkki", - "rotateLink": "Kierrä linkkiä", - "disableLink": "Poista linkki käytöstä", - "rotateLinkConfirmTitle": "Kierretäänkö vieraslinkkiä?", - "rotateLinkDescription": "Nykyinen linkki lakkaa toimimasta välittömästi. Yhdistettyjen SSH-vieraiden yhteys katkaistaan.", - "disableLinkConfirmTitle": "Poistetaanko vieraslinkki käytöstä?", - "disableLinkDescription": "Anonyymien SSH-katsojien yhteys katkaistaan välittömästi ja nykyinen linkki lakkaa toimimasta.", - "linkCopied": "Linkki kopioitu", - "roles": "Roolit", - "users": "Käyttäjät", - "searchUsers": "Hae käyttäjiä", - "endConfirmTitle": "Lopetetaanko tämä kokous?", - "endDescription": "{{name}} päättyy kaikilta, eikä sitä voida avata uudelleen.", - "endPersistentDescription": "Aktiivinen esitys kohdassa {{name}} pysähtyy. Huone pysyy käytettävissä uudelleen.", - "deleteRoom": "Poista huone", - "deleteConfirmTitle": "Poistetaanko tämä huone?", - "deleteDescription": "{{name}} poistetaan pysyvästi kaikilta, eikä sitä voi perua.", - "invitedTo": "Sinut kutsuttiin \"{{name}} \" -palveluun", - "guest": { - "title": "Kokous", - "waiting": "Esiintyjää odotellessa...", - "linkInvalid": "Tämä vieraslinkki on virheellinen tai kokous on päättynyt" - }, - "reopenFromPanel": "Tämä kokousvälilehti vanheni. Avaa huone uudelleen Kokoukset-paneelista.", - "retry": "Yritä uudelleen", - "requestControl": "Pyydä hallintaa", - "cancelControlRequest": "Peruuta pyyntö", - "controlQueue": "Ohjauspyynnöt", - "noControlRequests": "Ei vireillä olevia pyyntöjä.", - "grantControlTo": "Anna hallinta käyttäjälle {{name}}", - "dismissControlRequest": "Hylkää käyttäjän {{name}}hallintapyyntö", - "releaseControl": "Vapautusohjaus", - "grantControl": "Anna hallinta", - "revokeControl": "Ota hallinta takaisin", - "controlRequestedBy": "{{name}} pyysi hallintaoikeutta", - "grant": "Myöntää", - "controlBadge": "Hallinnassa", - "youArePresenting": "Esität toisella välilehdellä. Lopeta esitys luovuttaaksesi esiintymispaikan.", - "created": "Huone luotu", - "invited": "Jäsenet kutsuttuina", - "roomTab": "Kokous", - "serverChanged": "Kokouspalvelin muuttui. Avaa tämä kokous uudelleen Kokoukset-paneelista.", - "hostNotSynced": "Synkronoi tämä isäntä kokouspalvelimeen ennen esityksen aloittamista.", - "linkRequiresServer": "Linkitä tämä työpöytä Termix-palvelimeen ja luo kokous siellä jakaaksesi verkkolinkin." - }, - "nav": { - "collab": "Kokoukset" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Salli istunnon jakaminen", - "description": "Salli reaaliaikaisten pääte-, RDP-, VNC- ja Telnet-istuntojen jakaminen koko instanssin kesken. Poistamalla tämän asetuksen käytöstä ohitetaan kaikki isäntäkohtaisesti jakamiseen liittyvät asetukset." - } - }, - "host": { - "allowSessionSharing": { - "label": "Salli istunnon jakaminen", - "description": "Salli live-istuntojen jakaminen tällä isännällä linkin kautta tai muiden käyttäjien kanssa" - } - } - }, - "permissions": { - "use": { - "title": "Jaa istuntoja ja liity huoneisiin", - "description": "Jaa live-istuntoja linkin kautta tai muiden käyttäjien kanssa ja luo ja liity yhteistyöhuoneisiin." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Ei koskaan", - "oneHour": "1 tunti", - "oneDay": "24 tuntia", - "sevenDays": "7 päivää", - "thirtyDays": "30 päivää", - "custom": "Mukautettu" - } - } - }, - "connections": { - "sectionSharedWithMe": "Jaettu kanssani", - "sharedBy": "Jakanut {{username}}", - "join": "Liity", - "sharedSessionLabel": "{{hostName}} (jaettu)" - }, - "plugin": { - "name": "Istunnon jakaminen" - } -} diff --git a/plugins/session-sharing/locales/translated/fr_FR.json b/plugins/session-sharing/locales/translated/fr_FR.json deleted file mode 100644 index 2541f5871..000000000 --- a/plugins/session-sharing/locales/translated/fr_FR.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Connexion à la session partagée...", - "linkInvalid": "Ce lien de partage est invalide, a expiré ou a été révoqué.", - "rateLimited": "Trop de tentatives, veuillez réessayer dans quelques instants.", - "sessionEnded": "Cette session est terminée.", - "readOnlyBadge": "Affichage uniquement", - "ownerLabel": "propriétaire de l'hôte" - }, - "modalTitle": "Session de partage", - "shareButton": "Partager", - "notReadyToShare": "La session n'est pas encore prête à être partagée.", - "modeTab": { - "link": "Lien", - "user": "Utilisateur" - }, - "linkModeDescription": "Toute personne disposant de ce lien peut s'inscrire, aucun compte n'est requis.", - "userModeDescription": "Partagez cette session avec un utilisateur spécifique qui y a déjà accès. S'il n'y a pas encore accès, partagez-lui d'abord l'hôte ou utilisez un lien. Une fois partagée, la session apparaîtra dans son onglet Connexions.", - "permissionLevel": { - "label": "Niveau de permission", - "readOnly": "Lecture seule", - "readOnlyDescription": "Je peux suivre la session en direct, mais je ne peux ni écrire ni interagir.", - "readWrite": "Lecture-écriture", - "readWriteDescription": "Peut saisir du texte et interagir avec la session comme le propriétaire." - }, - "expiryLabel": "Expiration du lien", - "createLinkButton": "Créer un lien", - "createShareButton": "Partager avec l'utilisateur", - "searchUsersPlaceholder": "Rechercher des utilisateurs...", - "noUsersFound": "Aucun utilisateur trouvé", - "linkCreated": "Lien de partage créé", - "linkNeedsServer": "Les liens de partage ne fonctionnent que pour les sessions sur un serveur Termix. Connectez-vous à cet hôte via votre serveur lié pour le partager avec un lien.", - "linkCopied": "Lien copié dans le presse-papiers", - "copyLink": "Copier le lien", - "shareCreated": "Session partagée. Elle apparaîtra dans leur onglet Connexions.", - "shareFailed": "Échec de la création du partage", - "userLacksHostAccess": "Cet utilisateur n'a pas encore accès à cet hôte. Veuillez d'abord lui partager l'hôte ou utiliser un lien.", - "activeShares": "Actions actives", - "noActiveShares": "Aucune action active pour cette session", - "revoke": "Révoquer", - "revokeConfirmTitle": "Révoquer cette action ?", - "revokeConfirmDescription": "Toute personne utilisant ce partage perdra immédiatement l'accès.", - "revoked": "Action révoquée", - "revokeFailed": "Échec de la révocation de l'action", - "joinCount": "{{count}} rejoindre", - "joinCount_other": "{{count}} rejoint", - "expiresAt": "Expire le {{date}}", - "linkShareBadge": "Lien", - "userShareBadge": "Utilisateur : {{username}}", - "loadSharesFailed": "Échec du chargement des actions actives", - "remoteDisplayUnavailable": "Le bureau à distance n'est pas disponible pour le moment.", - "sharedWithMe": "Séances partagées avec moi", - "customHoursPlaceholder": "Heures jusqu'à l'expiration de l'accès" - }, - "collab": { - "createRoom": "Nouvelle chambre", - "roomName": "Nom de la chambre", - "persistentRoom": "Chambre persistante", - "persistentRoomHint": "Les salles persistantes restent répertoriées après la fin de la réunion et peuvent être réutilisées.", - "noRooms": "Aucune salle disponible pour le moment. Créez-en une pour démarrer une réunion.", - "openRoom": "Ouvrir", - "endRoom": "Fin de la réunion", - "leaveRoom": "Partir", - "deleteMember": "Retirer", - "removeMember": "Supprimer {{name}}", - "removeMemberTitle": "Supprimer ce membre ?", - "removeMemberDescription": "{{name}} perdra immédiatement l'accès à cette salle. Tout utilisateur SSH actif sera déconnecté.", - "invite": "Inviter", - "inviteTitle": "Inviter des membres", - "members": "Membres", - "membersPanel": "Membres de la salle et demandes de contrôle", - "membersWithCount": "Membres ({{count}})", - "guestsWithCount": "Invités ({{count}})", - "removeGuests": "Retirer les invités", - "anonymousGuest": "Invité", - "online": "en ligne", - "offline": "hors ligne", - "you": "(toi)", - "hostBadge": "Hôte", - "presenterBadge": "Présentation", - "present": "Présent", - "presentTitle": "Montez sur scène", - "presentHost": "Hôte", - "searchHosts": "Rechercher des hôtes", - "noHostsFound": "Aucun hôte disponible trouvé.", - "presentProtocol": "Protocole", - "stopPresenting": "Arrêtez de présenter", - "takeOver": "Reprendre", - "takeOverConfirmTitle": "Remplacer la présentation actuelle ?", - "takeOverDescription": "{{name}} est en train de présenter. Prendre le relais déconnectera tout le monde de cette scène.", - "emptyStage": "Personne ne présente. Montez sur scène pour animer une session.", - "stageLoading": "Se connecter à la scène...", - "stageEnded": "La présentation s'est terminée.", - "roomEnded": "Cette réunion est terminée.", - "presenterLabel": "{{name}} présente", - "guestLink": "Lien invité", - "guestLinkOn": "Le lien invité est activé. Toute personne disposant du lien peut assister à la représentation.", - "guestLinkOff": "Le lien invité est désactivé.", - "copyLink": "Copier le lien", - "rotateLink": "Lien de rotation", - "disableLink": "Désactiver le lien", - "rotateLinkConfirmTitle": "Faire pivoter le lien invité ?", - "rotateLinkDescription": "La connexion actuelle sera immédiatement interrompue. Les sessions SSH virtuelles seront déconnectées.", - "disableLinkConfirmTitle": "Désactiver le lien invité ?", - "disableLinkDescription": "Les utilisateurs anonymes utilisant SSH seront immédiatement déconnectés et le lien actuel cessera de fonctionner.", - "linkCopied": "Lien copié", - "roles": "Rôles", - "users": "Utilisateurs", - "searchUsers": "Utilisateurs de recherche", - "endConfirmTitle": "Mettre fin à cette réunion ?", - "endDescription": "{{name}} prendra fin pour tout le monde et ne pourra pas être rouvert.", - "endPersistentDescription": "La présentation en cours dans {{name}} va s'arrêter. La salle reste disponible pour une réutilisation.", - "deleteRoom": "Supprimer la salle", - "deleteConfirmTitle": "Supprimer cette pièce ?", - "deleteDescription": "{{name}} sera définitivement supprimé pour tout le monde et ne pourra pas être annulé.", - "invitedTo": "Vous avez été invité à \"{{name}}\"", - "guest": { - "title": "Réunion", - "waiting": "En attente du présentateur...", - "linkInvalid": "Ce lien invité est invalide ou la réunion est terminée." - }, - "reopenFromPanel": "Cette réunion a expiré. Veuillez rouvrir la salle depuis le panneau Réunions.", - "retry": "Réessayer", - "requestControl": "Contrôle des demandes", - "cancelControlRequest": "Demande d'annulation", - "controlQueue": "Demandes de contrôle", - "noControlRequests": "Aucune requête en attente.", - "grantControlTo": "Donnez le contrôle à {{name}}", - "dismissControlRequest": "Rejeter la demande de contrôle de {{name}}", - "releaseControl": "Contrôle de la libération", - "grantControl": "Donnez le contrôle", - "revokeControl": "Reprenez le contrôle", - "controlRequestedBy": "{{name}} a demandé le contrôle", - "grant": "Accorder", - "controlBadge": "En contrôle", - "youArePresenting": "Vous présentez dans un autre onglet. Arrêtez votre présentation pour laisser la parole à quelqu'un d'autre.", - "created": "Pièce créée", - "invited": "Les membres invités", - "roomTab": "Réunion", - "serverChanged": "Le serveur de réunion a changé. Veuillez rouvrir cette réunion depuis le panneau Réunions.", - "hostNotSynced": "Synchronisez cet hôte avec le serveur de réunion avant la présentation.", - "linkRequiresServer": "Connectez cet ordinateur de bureau à un serveur Termix et créez-y la réunion pour partager un lien web." - }, - "nav": { - "collab": "Réunions" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Autoriser le partage de session", - "description": "Autorise le partage des sessions de terminal en direct, RDP, VNC et Telnet à l'échelle de l'instance. Remplace toute option de partage par hôte lorsqu'elle est désactivée." - } - }, - "host": { - "allowSessionSharing": { - "label": "Autoriser le partage de session", - "description": "Autoriser le partage des sessions en direct sur cet hébergeur via un lien ou avec d'autres utilisateurs" - } - } - }, - "permissions": { - "use": { - "title": "Partagez des sessions et rejoignez des salles", - "description": "Partagez les sessions en direct par lien ou avec d'autres utilisateurs, et créez et rejoignez des espaces de collaboration." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Jamais", - "oneHour": "1 heure", - "oneDay": "24 heures", - "sevenDays": "7 jours", - "thirtyDays": "30 jours", - "custom": "Personnalisé" - } - } - }, - "connections": { - "sectionSharedWithMe": "Partagé avec moi", - "sharedBy": "Partagé par {{username}}", - "join": "Rejoindre", - "sharedSessionLabel": "{{hostName}} (partagé)" - }, - "plugin": { - "name": "Partage de session" - } -} diff --git a/plugins/session-sharing/locales/translated/he_IL.json b/plugins/session-sharing/locales/translated/he_IL.json deleted file mode 100644 index 79e0362ed..000000000 --- a/plugins/session-sharing/locales/translated/he_IL.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "מתחבר לסשן משותף...", - "linkInvalid": "קישור השיתוף הזה אינו תקין, פג תוקף או בוטל", - "rateLimited": "יותר מדי ניסיונות, אנא נסה שוב בקרוב", - "sessionEnded": "מושב זה הסתיים", - "readOnlyBadge": "צפייה בלבד", - "ownerLabel": "בעל המארח" - }, - "modalTitle": "שיתוף סשן", - "shareButton": "לַחֲלוֹק", - "notReadyToShare": "הסשן עדיין לא מוכן לשיתוף", - "modeTab": { - "link": "לְקַשֵׁר", - "user": "מִשׁתַמֵשׁ" - }, - "linkModeDescription": "כל מי שיש לו קישור זה יכול להצטרף, אין צורך בחשבון.", - "userModeDescription": "שתף עם משתמש ספציפי שכבר יש לו גישה למארח זה. אם עדיין אין לו גישה, שתף איתו תחילה את המארח או השתמש בקישור במקום זאת. לאחר השיתוף, הסשן מופיע בכרטיסייה \"חיבורים\" שלו.", - "permissionLevel": { - "label": "רמת הרשאה", - "readOnly": "לקריאה בלבד", - "readOnlyDescription": "יכול לצפות בפגישה בשידור חי אך לא להקליד או לתקשר.", - "readWrite": "קריאה-כתיבה", - "readWriteDescription": "יכול להקליד ולקיים אינטראקציה עם הסשן בדיוק כמו הבעלים." - }, - "expiryLabel": "תוקף הקישור", - "createLinkButton": "צור קישור", - "createShareButton": "שתף עם המשתמש", - "searchUsersPlaceholder": "חיפוש משתמשים...", - "noUsersFound": "לא נמצאו משתמשים", - "linkCreated": "קישור השיתוף נוצר", - "linkNeedsServer": "קישורי שיתוף פועלים רק עבור סשנים בשרת Termix. חבר מארח זה דרך השרת המקושר שלך כדי לשתף אותו עם קישור.", - "linkCopied": "קישור שהועתק ללוח", - "copyLink": "העתקת קישור", - "shareCreated": "הסשן שותף. זה יופיע בלשונית החיבורים שלהם.", - "shareFailed": "יצירת השיתוף נכשלה", - "userLacksHostAccess": "למשתמש הזה עדיין אין גישה למארח הזה. שתף את המארח איתו תחילה, או השתמש בקישור במקום זאת.", - "activeShares": "מניות פעילות", - "noActiveShares": "אין שיתופים פעילים עבור סשן זה", - "revoke": "לְבַטֵל", - "revokeConfirmTitle": "לבטל את השיתוף הזה?", - "revokeConfirmDescription": "כל מי שישתמש בשיתוף הזה יאבד גישה באופן מיידי.", - "revoked": "השיתוף בוטל", - "revokeFailed": "ביטול השיתוף נכשל", - "joinCount": "{{count}} join", - "joinCount_other": "{{count}} מצטרף", - "expiresAt": "פג תוקף {{date}}", - "linkShareBadge": "לְקַשֵׁר", - "userShareBadge": "משתמש: {{username}}", - "loadSharesFailed": "נכשלה טעינת השיתופים הפעילים", - "remoteDisplayUnavailable": "שולחן עבודה מרוחק אינו זמין כרגע.", - "sharedWithMe": "מפגשים ששותפו איתי", - "customHoursPlaceholder": "שעות עד לפקיעת הגישה" - }, - "collab": { - "createRoom": "חדר חדש", - "roomName": "שם החדר", - "persistentRoom": "חדר קבוע", - "persistentRoomHint": "חדרים קבועים נשארים רשומים לאחר סיום הפגישה וניתן לעשות בהם שימוש חוזר.", - "noRooms": "אין עדיין חדרים. צור אחד כדי להתחיל פגישה.", - "openRoom": "לִפְתוֹחַ", - "endRoom": "סיום פגישה", - "leaveRoom": "לַעֲזוֹב", - "deleteMember": "לְהַסִיר", - "removeMember": "הסר {{name}}", - "removeMemberTitle": "להסיר את החבר הזה?", - "removeMemberDescription": "{{name}} יאבד גישה לחדר הזה באופן מיידי. כל צופה פעיל ב-SSH ינותק.", - "invite": "לְהַזמִין", - "inviteTitle": "הזמנת חברים", - "members": "חברים", - "membersPanel": "חברי חדר ובקשות בקרה", - "membersWithCount": "חברים ({{count}})", - "guestsWithCount": "אורחים ({{count}})", - "removeGuests": "הסר אורחים", - "anonymousGuest": "אוֹרֵחַ", - "online": "באינטרנט", - "offline": "לא מקוון", - "you": "(אַתָה)", - "hostBadge": "מְאָרֵחַ", - "presenterBadge": "הצגה", - "present": "לְהַצִיג", - "presentTitle": "עלו על הבמה", - "presentHost": "מְאָרֵחַ", - "searchHosts": "חיפוש מארחים", - "noHostsFound": "לא נמצאו מארחים זמינים.", - "presentProtocol": "פּרוֹטוֹקוֹל", - "stopPresenting": "הפסקת ההצגה", - "takeOver": "השתלטות", - "takeOverConfirmTitle": "להחליף את המצגת הנוכחית?", - "takeOverDescription": "{{name}} מציג כעת. השתלטות תנתק את כולם מהבמה הזו.", - "emptyStage": "אף אחד לא מציג. עלו לבמה כדי לשתף סשן.", - "stageLoading": "מתחבר לבמה...", - "stageEnded": "המצגת הסתיימה", - "roomEnded": "פגישה זו הסתיימה", - "presenterLabel": "{{name}} מציג", - "guestLink": "קישור אורח", - "guestLinkOn": "קישור האורח פעיל. כל מי שיש לו קישור יכול לצפות בבמה.", - "guestLinkOff": "קישור האורח כבוי.", - "copyLink": "העתקת קישור", - "rotateLink": "סיבוב קישור", - "disableLink": "השבת קישור", - "rotateLinkConfirmTitle": "לסובב את קישור האורח?", - "rotateLinkDescription": "הקישור הנוכחי יפסיק לפעול באופן מיידי. אורחי SSH המחוברים ינותקו.", - "disableLinkConfirmTitle": "להשבית את קישור האורח?", - "disableLinkDescription": "צופי SSH אנונימיים ינותקו באופן מיידי והקישור הנוכחי יפסיק לפעול.", - "linkCopied": "הקישור הועתק", - "roles": "תפקידים", - "users": "משתמשים", - "searchUsers": "חיפוש משתמשים", - "endConfirmTitle": "לסיים את הפגישה הזו?", - "endDescription": "{{name}} יסתיים עבור כולם ולא ניתן יהיה לפתוח אותו מחדש.", - "endPersistentDescription": "המצגת הפעילה ב- {{name}} תיעצר. החדר יישאר זמין לשימוש חוזר.", - "deleteRoom": "מחיקת חדר", - "deleteConfirmTitle": "למחוק את החדר הזה?", - "deleteDescription": "{{name}} יימחק לצמיתות עבור כולם ולא ניתן יהיה לבטל אותו.", - "invitedTo": "הוזמנת ל-\"{{name}}\"", - "guest": { - "title": "פְּגִישָׁה", - "waiting": "מחכה למנחה...", - "linkInvalid": "קישור האורח הזה אינו תקין או שהפגישה הסתיימה" - }, - "reopenFromPanel": "פג תוקף כרטיסיית הפגישה הזו. פתח מחדש את החדר מחלונית הפגישות.", - "retry": "נסה שוב", - "requestControl": "בקרת בקשות", - "cancelControlRequest": "ביטול בקשה", - "controlQueue": "בקשות בקרה", - "noControlRequests": "אין בקשות ממתינות.", - "grantControlTo": "תן שליטה ל- {{name}}", - "dismissControlRequest": "סגור את בקשת הבקרה של {{name}}", - "releaseControl": "בקרת שחרור", - "grantControl": "תן שליטה", - "revokeControl": "קח בחזרה את השליטה", - "controlRequestedBy": "{{name}} ביקש שליטה", - "grant": "מַעֲנָק", - "controlBadge": "בשליטה", - "youArePresenting": "אתה מציג בכרטיסייה אחרת. הפסק את ההצגה כדי להעביר את הבמה.", - "created": "החדר נוצר", - "invited": "חברים שהוזמנו", - "roomTab": "פְּגִישָׁה", - "serverChanged": "שרת הפגישות השתנה. פתח מחדש פגישה זו מחלונית הפגישות.", - "hostNotSynced": "סנכרן מארח זה עם שרת הפגישה לפני ההצגה.", - "linkRequiresServer": "קשר שולחן עבודה זה לשרת Termix וצור שם את הפגישה כדי לשתף קישור אינטרנט." - }, - "nav": { - "collab": "פגישות" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "אפשר שיתוף סשנים", - "description": "מאפשר שיתוף של הפעלות מסוף חי, RDP, VNC ו-Telnet בכל המופעים. מבטל כל אפשרות שיתוף לפי מארח כאשר היא מושבתת." - } - }, - "host": { - "allowSessionSharing": { - "label": "אפשר שיתוף סשנים", - "description": "אפשר שיתוף של מפגשים חיים במארח זה באמצעות קישור או עם משתמשים אחרים" - } - } - }, - "permissions": { - "use": { - "title": "שתף מפגשים והצטרף לחדרים", - "description": "שתף מפגשים חיים באמצעות קישור או עם משתמשים אחרים, וצר והצטרף לחדרי שיתוף פעולה." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "לְעוֹלָם לֹא", - "oneHour": "שעה אחת", - "oneDay": "24 שעות", - "sevenDays": "7 ימים", - "thirtyDays": "30 ימים", - "custom": "מִנְהָג" - } - } - }, - "connections": { - "sectionSharedWithMe": "שיתף איתי", - "sharedBy": "שותף על ידי {{username}}", - "join": "לְהִצְטַרֵף", - "sharedSessionLabel": "{{hostName}} (שותף)" - }, - "plugin": { - "name": "שיתוף סשנים" - } -} diff --git a/plugins/session-sharing/locales/translated/hi_IN.json b/plugins/session-sharing/locales/translated/hi_IN.json deleted file mode 100644 index 244dab72a..000000000 --- a/plugins/session-sharing/locales/translated/hi_IN.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "साझा सत्र से जुड़ रहा है...", - "linkInvalid": "यह शेयर लिंक अमान्य है, इसकी समय सीमा समाप्त हो चुकी है, या इसे रद्द कर दिया गया है।", - "rateLimited": "बहुत अधिक प्रयास किए गए, कृपया कुछ देर बाद पुनः प्रयास करें।", - "sessionEnded": "यह सत्र समाप्त हो गया है।", - "readOnlyBadge": "केवल देखें", - "ownerLabel": "मेज़बान स्वामी" - }, - "modalTitle": "सत्र साझा करें", - "shareButton": "साझा करें", - "notReadyToShare": "सेशन अभी शेयर करने के लिए तैयार नहीं है", - "modeTab": { - "link": "जोड़ना", - "user": "उपयोगकर्ता" - }, - "linkModeDescription": "इस लिंक के जरिए कोई भी जुड़ सकता है, इसके लिए किसी खाते की आवश्यकता नहीं है।", - "userModeDescription": "इसे उस विशिष्ट उपयोगकर्ता के साथ साझा करें जिसके पास पहले से ही इस होस्ट का एक्सेस है। यदि उनके पास अभी एक्सेस नहीं है, तो पहले उनके साथ होस्ट साझा करें या इसके बजाय एक लिंक का उपयोग करें। साझा करने के बाद, सेशन उनके कनेक्शन टैब में दिखाई देगा।", - "permissionLevel": { - "label": "अनुमति स्तर", - "readOnly": "केवल-पठनीय", - "readOnlyDescription": "आप सेशन को लाइव देख सकते हैं लेकिन टाइप या इंटरैक्ट नहीं कर सकते।", - "readWrite": "पढ़ना-लिखना", - "readWriteDescription": "मालिक की तरह ही सेशन में टाइप कर सकता है और इंटरैक्ट कर सकता है।" - }, - "expiryLabel": "लिंक की समय सीमा समाप्त", - "createLinkButton": "लिंक बनाएं", - "createShareButton": "उपयोगकर्ता के साथ साझा करें", - "searchUsersPlaceholder": "उपयोगकर्ताओं को खोजें...", - "noUsersFound": "कोई उपयोगकर्ता नहीं मिला", - "linkCreated": "शेयर लिंक बनाया गया", - "linkNeedsServer": "शेयर लिंक केवल टर्मिक्स सर्वर पर मौजूद सेशन के लिए काम करते हैं। लिंक के ज़रिए शेयर करने के लिए इस होस्ट को अपने लिंक्ड सर्वर के माध्यम से कनेक्ट करें।", - "linkCopied": "लिंक क्लिपबोर्ड पर कॉपी हो गया है", - "copyLink": "लिंक की प्रतिलिपि करें", - "shareCreated": "सेशन साझा कर दिया गया है। यह उनके कनेक्शन टैब में दिखाई देगा।", - "shareFailed": "शेयर बनाने में विफल", - "userLacksHostAccess": "उस उपयोगकर्ता के पास अभी इस होस्ट तक पहुंच नहीं है। कृपया पहले उनके साथ होस्ट साझा करें, या इसके बजाय एक लिंक का उपयोग करें।", - "activeShares": "सक्रिय शेयर", - "noActiveShares": "इस सत्र के लिए कोई सक्रिय शेयर नहीं हैं", - "revoke": "निरस्त करें", - "revokeConfirmTitle": "क्या इस शेयर को रद्द कर दें?", - "revokeConfirmDescription": "इस शेयर का उपयोग करने वाले किसी भी व्यक्ति की पहुंच तुरंत समाप्त हो जाएगी।", - "revoked": "शेयर रद्द कर दिया गया", - "revokeFailed": "शेयर रद्द करने में विफल", - "joinCount": "{{count}} जुड़ें", - "joinCount_other": "{{count}} जुड़ता है", - "expiresAt": "{{date}} को समाप्त होगा", - "linkShareBadge": "जोड़ना", - "userShareBadge": "उपयोगकर्ता: {{username}}", - "loadSharesFailed": "सक्रिय शेयर लोड करने में विफल", - "remoteDisplayUnavailable": "फिलहाल रिमोट डेस्कटॉप उपलब्ध नहीं है।", - "sharedWithMe": "मेरे साथ साझा किए गए सत्र", - "customHoursPlaceholder": "पहुँच समाप्त होने तक के घंटे" - }, - "collab": { - "createRoom": "नया कमरा", - "roomName": "कमरे का नाम", - "persistentRoom": "लगातार कमरा", - "persistentRoomHint": "स्थायी कमरे मीटिंग समाप्त होने के बाद भी सूची में बने रहते हैं और उनका पुनः उपयोग किया जा सकता है।", - "noRooms": "अभी कोई कमरा उपलब्ध नहीं है। मीटिंग शुरू करने के लिए एक कमरा बनाएं।", - "openRoom": "खोलें", - "endRoom": "बैठक समाप्त", - "leaveRoom": "छुट्टी", - "deleteMember": "हटाएं", - "removeMember": "{{name}} हटाएँ", - "removeMemberTitle": "क्या इस सदस्य को हटा दें?", - "removeMemberDescription": "{{name}} को इस कमरे तक पहुँच तुरंत खोनी पड़ेगी। कोई भी सक्रिय SSH व्यूअर डिस्कनेक्ट हो जाएगा।", - "invite": "आमंत्रित करना", - "inviteTitle": "सदस्यों को आमंत्रित करो", - "members": "सदस्यों", - "membersPanel": "कमरे के सदस्य और नियंत्रण अनुरोध", - "membersWithCount": "सदस्य ({{count}})", - "guestsWithCount": "अतिथि ({{count}})", - "removeGuests": "मेहमानों को हटा दें", - "anonymousGuest": "अतिथि", - "online": "ऑनलाइन", - "offline": "ऑफलाइन", - "you": "(आप)", - "hostBadge": "होस्ट", - "presenterBadge": "प्रस्तुत करते हुए", - "present": "उपस्थित", - "presentTitle": "मंच पर आइए", - "presentHost": "होस्ट", - "searchHosts": "होस्ट खोजें", - "noHostsFound": "कोई उपलब्ध होस्ट नहीं मिला।", - "presentProtocol": "प्रोटोकॉल", - "stopPresenting": "प्रस्तुति देना बंद करो", - "takeOver": "कब्जा", - "takeOverConfirmTitle": "क्या वर्तमान प्रस्तुति को बदलना है?", - "takeOverDescription": "{{name}} अभी प्रस्तुति दे रहा है। कार्यभार संभालने से सभी लोग उस मंच से अलग हो जाएंगे।", - "emptyStage": "आज कोई प्रस्तुति नहीं दे रहा है। आप मंच पर आकर अपना सत्र साझा करें।", - "stageLoading": "मंच से जुड़ना...", - "stageEnded": "प्रस्तुति समाप्त हुई", - "roomEnded": "यह बैठक समाप्त हो गई है", - "presenterLabel": "{{name}} प्रस्तुत कर रहा है", - "guestLink": "अतिथि लिंक", - "guestLinkOn": "अतिथि लिंक चालू है। लिंक के माध्यम से कोई भी व्यक्ति मंच देख सकता है।", - "guestLinkOff": "अतिथि लिंक बंद है।", - "copyLink": "लिंक की प्रतिलिपि करें", - "rotateLink": "लिंक घुमाएँ", - "disableLink": "लिंक अक्षम करें", - "rotateLinkConfirmTitle": "अतिथि लिंक को घुमाएँ?", - "rotateLinkDescription": "वर्तमान लिंक तुरंत काम करना बंद कर देगा। कनेक्टेड एसएसएच गेस्ट डिस्कनेक्ट हो जाएंगे।", - "disableLinkConfirmTitle": "अतिथि लिंक को निष्क्रिय करें?", - "disableLinkDescription": "अनाम एसएसएच व्यूअर्स तुरंत डिस्कनेक्ट हो जाएंगे और वर्तमान लिंक काम करना बंद कर देगा।", - "linkCopied": "लिंक कॉपी हो गया", - "roles": "भूमिकाएं", - "users": "उपयोगकर्ता", - "searchUsers": "उपयोगकर्ताओं को खोजें", - "endConfirmTitle": "क्या इस बैठक को यहीं समाप्त करें?", - "endDescription": "{{name}} सभी के लिए समाप्त हो जाएगा और इसे दोबारा नहीं खोला जा सकता है।", - "endPersistentDescription": "{{name}} में चल रही प्रस्तुति बंद हो जाएगी। कमरा पुनः उपयोग के लिए उपलब्ध रहेगा।", - "deleteRoom": "कमरा हटाएं", - "deleteConfirmTitle": "क्या इस कमरे को हटा दें?", - "deleteDescription": "{{name}} को सभी के लिए स्थायी रूप से हटा दिया जाएगा और इसे पूर्ववत नहीं किया जा सकता है।", - "invitedTo": "आपको \"{{name}} \" में आमंत्रित किया गया था", - "guest": { - "title": "बैठक", - "waiting": "प्रस्तुतकर्ता का इंतजार है...", - "linkInvalid": "यह अतिथि लिंक अमान्य है या मीटिंग समाप्त हो चुकी है" - }, - "reopenFromPanel": "इस मीटिंग टैब की समय सीमा समाप्त हो गई है। कृपया मीटिंग पैनल से कमरे को पुनः खोलें।", - "retry": "पुनः प्रयास करें", - "requestControl": "अनुरोध नियंत्रण", - "cancelControlRequest": "अनुरोध को रद्द करें", - "controlQueue": "नियंत्रण अनुरोध", - "noControlRequests": "कोई लंबित अनुरोध नहीं।", - "grantControlTo": "{{name}} को नियंत्रण दें", - "dismissControlRequest": "{{name}}के नियंत्रण अनुरोध को खारिज करें", - "releaseControl": "नियंत्रण छोड़ें", - "grantControl": "नियंत्रण सौंप दें", - "revokeControl": "नियंत्रण वापस अपने हाथ में लें।", - "controlRequestedBy": "{{name}} ने नियंत्रण मांगा", - "grant": "अनुदान", - "controlBadge": "नियंत्रण में", - "youArePresenting": "आप किसी दूसरे टैब में प्रेजेंटेशन दे रहे हैं। कृपया प्रेजेंटेशन देना बंद करें और स्टेज किसी और को सौंप दें।", - "created": "कमरा बनाया गया", - "invited": "सदस्यों को आमंत्रित किया गया", - "roomTab": "बैठक", - "serverChanged": "मीटिंग सर्वर बदल गया है। कृपया मीटिंग पैनल से इस मीटिंग को दोबारा खोलें।", - "hostNotSynced": "प्रस्तुति देने से पहले इस होस्ट को मीटिंग सर्वर से सिंक्रनाइज़ करें।", - "linkRequiresServer": "इस डेस्कटॉप को टर्मिक्स सर्वर से लिंक करें और वहां मीटिंग बनाएं ताकि वेब लिंक साझा किया जा सके।" - }, - "nav": { - "collab": "बैठक" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "सेशन शेयरिंग की अनुमति दें", - "description": "लाइव टर्मिनल, RDP, VNC और Telnet सेशन को पूरे इंस्टेंस में साझा करने की अनुमति दें। अक्षम होने पर यह प्रति-होस्ट साझाकरण टॉगल को ओवरराइड कर देता है।" - } - }, - "host": { - "allowSessionSharing": { - "label": "सेशन शेयरिंग की अनुमति दें", - "description": "इस होस्ट पर होने वाले लाइव सेशन को लिंक के माध्यम से या अन्य उपयोगकर्ताओं के साथ साझा किया जा सकता है।" - } - } - }, - "permissions": { - "use": { - "title": "सेशन साझा करें और रूम में शामिल हों", - "description": "लाइव सेशन को लिंक के माध्यम से या अन्य उपयोगकर्ताओं के साथ साझा करें, और सहयोग कक्ष बनाएं और उनमें शामिल हों।" - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "कभी नहीं", - "oneHour": "1 घंटा", - "oneDay": "24 घंटे", - "sevenDays": "7 दिन", - "thirtyDays": "30 दिन", - "custom": "कस्टम" - } - } - }, - "connections": { - "sectionSharedWithMe": "मेरे साथ साझा किया गया", - "sharedBy": "{{username}} द्वारा साझा किया गया", - "join": "जोड़ना", - "sharedSessionLabel": "{{hostName}} (साझा)" - }, - "plugin": { - "name": "सत्र साझाकरण" - } -} diff --git a/plugins/session-sharing/locales/translated/hu_HU.json b/plugins/session-sharing/locales/translated/hu_HU.json deleted file mode 100644 index 8ca381dda..000000000 --- a/plugins/session-sharing/locales/translated/hu_HU.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Kapcsolódás a megosztott munkamenethez...", - "linkInvalid": "Ez a megosztási link érvénytelen, lejárt vagy visszavonták.", - "rateLimited": "Túl sok próbálkozás, kérjük, próbáld újra később", - "sessionEnded": "Ez a munkamenet véget ért", - "readOnlyBadge": "Csak megtekintés", - "ownerLabel": "Házigazda tulajdonos" - }, - "modalTitle": "Megosztási munkamenet", - "shareButton": "Részesedés", - "notReadyToShare": "A munkamenet még nem megosztható", - "modeTab": { - "link": "Link", - "user": "Felhasználó" - }, - "linkModeDescription": "Bárki csatlakozhat a link birtokában, nincs szükség regisztrációra.", - "userModeDescription": "Megosztás egy adott felhasználóval, aki már rendelkezik hozzáféréssel ehhez a gazdagéphez. Ha még nincs hozzáférésük, először oszd meg vele a gazdagépet, vagy használj hivatkozást. A megosztás után a munkamenet megjelenik a Kapcsolatok lapon.", - "permissionLevel": { - "label": "Jogosultsági szint", - "readOnly": "Csak olvasható", - "readOnlyDescription": "Élőben nézhetem az előadást, de nem gépelhetek és nem léphetek interakcióba.", - "readWrite": "Olvasható-írható", - "readWriteDescription": "Pont úgy tud gépelni és kommunikálni a munkamenettel, mint a tulajdonos." - }, - "expiryLabel": "Link lejárata", - "createLinkButton": "Link létrehozása", - "createShareButton": "Megosztás a felhasználóval", - "searchUsersPlaceholder": "Felhasználók keresése...", - "noUsersFound": "Nem található felhasználó", - "linkCreated": "Megosztási link létrehozva", - "linkNeedsServer": "A megosztási linkek csak Termix szerveren lévő munkamenetek esetén működnek. A linkkel való megosztáshoz csatlakoztasd ezt a hosztot a csatolt szervereden keresztül.", - "linkCopied": "Link a vágólapra másolva", - "copyLink": "Link másolása", - "shareCreated": "Munkamenet megosztva. Megjelenik a Kapcsolatok fülön.", - "shareFailed": "Nem sikerült létrehozni a megosztást", - "userLacksHostAccess": "A felhasználónak még nincs hozzáférése ehhez a gazdagéphez. Először oszd meg vele a gazdagépet, vagy használj linket.", - "activeShares": "Aktív megosztások", - "noActiveShares": "Nincsenek aktív megosztások ebben a munkamenetben", - "revoke": "Visszavonás", - "revokeConfirmTitle": "Visszavonja ezt a megosztást?", - "revokeConfirmDescription": "Bárki, aki ezt a megosztást használja, azonnal elveszíti a hozzáférését.", - "revoked": "Megosztás visszavonva", - "revokeFailed": "Nem sikerült visszavonni a megosztást", - "joinCount": "{{count}} csatlakozás", - "joinCount_other": "{{count}} csatlakozások", - "expiresAt": "Lejár {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Felhasználó: {{username}}", - "loadSharesFailed": "Nem sikerült betölteni az aktív megosztásokat", - "remoteDisplayUnavailable": "A távoli asztal jelenleg nem érhető el.", - "sharedWithMe": "Velem megosztott munkamenetek", - "customHoursPlaceholder": "Órák a hozzáférés lejártáig" - }, - "collab": { - "createRoom": "Új szoba", - "roomName": "Szoba neve", - "persistentRoom": "Állandó szoba", - "persistentRoomHint": "Az állandó szobák a megbeszélés befejezése után is listázva maradnak, és újra felhasználhatók.", - "noRooms": "Még nincsenek szobák. Hozz létre egyet a megbeszélés megkezdéséhez.", - "openRoom": "Nyitott", - "endRoom": "Megbeszélés vége", - "leaveRoom": "Szabadság", - "deleteMember": "Eltávolítás", - "removeMember": "Eltávolítás {{name}}", - "removeMemberTitle": "Eltávolítja ezt a tagot?", - "removeMemberDescription": "{{name}} azonnal elveszíti a hozzáférést ehhez a szobához. Minden aktív SSH-megjelenítő lekapcsolódik.", - "invite": "Meghív", - "inviteTitle": "Tagok meghívása", - "members": "Tagok", - "membersPanel": "Szoba tagjai és irányítási kérelmek", - "membersWithCount": "Tagok ({{count}})", - "guestsWithCount": "Vendégek ({{count}})", - "removeGuests": "Vendégek eltávolítása", - "anonymousGuest": "Vendég", - "online": "online", - "offline": "offline", - "you": "(te)", - "hostBadge": "Házigazda", - "presenterBadge": "Bemutatja", - "present": "Jelenlegi", - "presentTitle": "Lépj színpadra", - "presentHost": "Házigazda", - "searchHosts": "Keresés hosztok", - "noHostsFound": "Nem található elérhető host.", - "presentProtocol": "Jegyzőkönyv", - "stopPresenting": "Prezentáció leállítása", - "takeOver": "Átvétel", - "takeOverConfirmTitle": "Lecseréli a jelenlegi prezentációt?", - "takeOverDescription": "{{name}} most tart előadást. Az irányítás átvétele mindenkit lekapcsolna arról a színpadról.", - "emptyStage": "Senki sem tart előadást. Lépjen színpadra, és ossza meg velünk az előadást.", - "stageLoading": "Csatlakozás a színpadhoz...", - "stageEnded": "A prezentáció véget ért", - "roomEnded": "Ez a találkozó véget ért", - "presenterLabel": "{{name}} bemutatja", - "guestLink": "Vendéglink", - "guestLinkOn": "Vendéglink be van kapcsolva. Bárki, aki rendelkezik a linkkel, nézheti a színpadot.", - "guestLinkOff": "A vendéglink ki van kapcsolva.", - "copyLink": "Link másolása", - "rotateLink": "Link elforgatása", - "disableLink": "Link letiltása", - "rotateLinkConfirmTitle": "Vendéglink elforgatása?", - "rotateLinkDescription": "A jelenlegi kapcsolat azonnal leáll. A csatlakoztatott SSH-vendégek le lesznek választva.", - "disableLinkConfirmTitle": "Letiltja a vendéglinket?", - "disableLinkDescription": "A névtelen SSH-nézők azonnal le lesznek választva, és a jelenlegi link nem fog működni.", - "linkCopied": "Link másolva", - "roles": "Szerepkörök", - "users": "Felhasználók", - "searchUsers": "Felhasználók keresése", - "endConfirmTitle": "Befejezi ezt a megbeszélést?", - "endDescription": "{{name}} mindenki számára megszűnik, és nem lehet újra megnyitni.", - "endPersistentDescription": "A {{name}} teremben futó aktív prezentáció leáll. A szoba továbbra is elérhető marad újrafelhasználásra.", - "deleteRoom": "Szoba törlése", - "deleteConfirmTitle": "Törli ezt a szobát?", - "deleteDescription": "A {{name}} véglegesen törlésre kerül mindenki számára, és a művelet nem vonható vissza.", - "invitedTo": "Meghívtak a következőre: \"{{name}}\"", - "guest": { - "title": "Találkozó", - "waiting": "Várjuk az előadót...", - "linkInvalid": "Ez a vendéglink érvénytelen, vagy a megbeszélés véget ért." - }, - "reopenFromPanel": "Ez a megbeszélésfül lejárt. Nyissa meg újra a szobát a Megbeszélések panelről.", - "retry": "Újrapróbálkozás", - "requestControl": "Kérelemvezérlés", - "cancelControlRequest": "Kérés visszavonása", - "controlQueue": "Ellenőrzési kérelmek", - "noControlRequests": "Nincsenek függőben lévő kérések.", - "grantControlTo": "Átadni az irányítást {{name}}-nak", - "dismissControlRequest": "Elutasítom {{name}}vezérlési kérelmét", - "releaseControl": "Kioldásvezérlés", - "grantControl": "Átadni az irányítást", - "revokeControl": "Vedd vissza az irányítást", - "controlRequestedBy": "{{name}} kérte az irányítást", - "grant": "Támogatás", - "controlBadge": "Irányítás alatt", - "youArePresenting": "Egy másik lapon tartasz előadást. Állítsa le az előadást, hogy átadhassa a helyet.", - "created": "Szoba létrehozva", - "invited": "Meghívott tagok", - "roomTab": "Találkozó", - "serverChanged": "A megbeszélés szervere megváltozott. Nyissa meg újra a megbeszélést a Megbeszélések panelről.", - "hostNotSynced": "Szinkronizálja ezt a gazdagépet a megbeszélés szerverével a prezentáció megkezdése előtt.", - "linkRequiresServer": "Kapcsold össze ezt az asztalt egy Termix szerverrel, és hozd létre ott a találkozót egy webhivatkozás megosztásához." - }, - "nav": { - "collab": "Találkozók" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Munkamenet-megosztás engedélyezése", - "description": "Engedélyezi az élő terminál-, RDP-, VNC- és Telnet-munkamenetek példányonkénti megosztását. Letiltás esetén felülírja az összes gazdagépenkénti megosztási kapcsolót." - } - }, - "host": { - "allowSessionSharing": { - "label": "Munkamenet-megosztás engedélyezése", - "description": "Engedélyezi az élő munkamenetek megosztását ezen a gazdagépen linken vagy más felhasználókkal" - } - } - }, - "permissions": { - "use": { - "title": "Munkamenetek megosztása és szobákhoz való csatlakozás", - "description": "Ossz meg élő munkameneteket linken keresztül vagy más felhasználókkal, és hozz létre, illetve csatlakozz együttműködési szobákhoz." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Soha", - "oneHour": "1 óra", - "oneDay": "24 óra", - "sevenDays": "7 nap", - "thirtyDays": "30 nap", - "custom": "Szokás" - } - } - }, - "connections": { - "sectionSharedWithMe": "Megosztva velem", - "sharedBy": "Megosztotta: {{username}}", - "join": "Csatlakozik", - "sharedSessionLabel": "{{hostName}} (megosztva)" - }, - "plugin": { - "name": "Munkamenet-megosztás" - } -} diff --git a/plugins/session-sharing/locales/translated/id_ID.json b/plugins/session-sharing/locales/translated/id_ID.json deleted file mode 100644 index aec6d097a..000000000 --- a/plugins/session-sharing/locales/translated/id_ID.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Menghubungkan ke sesi bersama...", - "linkInvalid": "Tautan berbagi ini tidak valid, telah kedaluwarsa, atau telah dicabut.", - "rateLimited": "Terlalu banyak percobaan, silakan coba lagi sebentar lagi.", - "sessionEnded": "Sesi ini telah berakhir.", - "readOnlyBadge": "Hanya tampilan", - "ownerLabel": "Pemilik host" - }, - "modalTitle": "Sesi berbagi", - "shareButton": "Membagikan", - "notReadyToShare": "Sesi belum siap untuk dibagikan.", - "modeTab": { - "link": "Link", - "user": "Pengguna" - }, - "linkModeDescription": "Siapa pun yang memiliki tautan ini dapat bergabung, tidak perlu membuat akun.", - "userModeDescription": "Bagikan dengan pengguna tertentu yang sudah memiliki akses ke host ini. Jika mereka belum memiliki akses, bagikan host tersebut kepada mereka terlebih dahulu atau gunakan tautan. Setelah dibagikan, sesi akan muncul di tab Koneksi mereka.", - "permissionLevel": { - "label": "Tingkat izin", - "readOnly": "Hanya baca", - "readOnlyDescription": "Bisa menonton sesi secara langsung tetapi tidak bisa mengetik atau berinteraksi.", - "readWrite": "Baca-tulis", - "readWriteDescription": "Dapat mengetik dan berinteraksi dengan sesi seperti halnya pemilik." - }, - "expiryLabel": "Kedaluwarsa tautan", - "createLinkButton": "Buat tautan", - "createShareButton": "Bagikan dengan pengguna", - "searchUsersPlaceholder": "Cari pengguna...", - "noUsersFound": "Tidak ada pengguna yang ditemukan", - "linkCreated": "Tautan berbagi telah dibuat", - "linkNeedsServer": "Tautan berbagi hanya berfungsi untuk sesi di server Termix. Hubungkan host ini melalui server tertaut Anda untuk membagikannya dengan tautan.", - "linkCopied": "Tautan disalin ke papan klip", - "copyLink": "Salin tautan", - "shareCreated": "Sesi dibagikan. Sesi tersebut akan muncul di tab Koneksi mereka.", - "shareFailed": "Gagal membuat saham", - "userLacksHostAccess": "Pengguna tersebut belum memiliki akses ke host ini. Bagikan host tersebut kepada mereka terlebih dahulu, atau gunakan tautan sebagai gantinya.", - "activeShares": "Saham aktif", - "noActiveShares": "Tidak ada saham aktif untuk sesi ini.", - "revoke": "Menarik kembali", - "revokeConfirmTitle": "Batalkan pembagian ini?", - "revokeConfirmDescription": "Siapa pun yang menggunakan share ini akan langsung kehilangan akses.", - "revoked": "Saham dicabut", - "revokeFailed": "Gagal mencabut saham", - "joinCount": "{{count}} bergabung", - "joinCount_other": "{{count}} bergabung", - "expiresAt": "Berakhir {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Pengguna: {{username}}", - "loadSharesFailed": "Gagal memuat saham aktif.", - "remoteDisplayUnavailable": "Akses desktop jarak jauh saat ini tidak tersedia.", - "sharedWithMe": "Sesi yang dibagikan kepada saya", - "customHoursPlaceholder": "Waktu hingga akses berakhir" - }, - "collab": { - "createRoom": "Kamar baru", - "roomName": "Nama ruangan", - "persistentRoom": "Ruang permanen", - "persistentRoomHint": "Ruangan yang bersifat permanen akan tetap terdaftar setelah rapat berakhir dan dapat digunakan kembali.", - "noRooms": "Belum ada ruang rapat. Buat ruang rapat untuk memulai pertemuan.", - "openRoom": "Membuka", - "endRoom": "Mengakhiri pertemuan", - "leaveRoom": "Meninggalkan", - "deleteMember": "Menghapus", - "removeMember": "Hapus {{name}}", - "removeMemberTitle": "Hapus anggota ini?", - "removeMemberDescription": "{{name}} akan kehilangan akses ke ruangan ini segera. Setiap penonton SSH yang aktif akan terputus.", - "invite": "Mengundang", - "inviteTitle": "Undang anggota", - "members": "Anggota", - "membersPanel": "Anggota ruangan dan permintaan kontrol", - "membersWithCount": "Anggota ({{count}})", - "guestsWithCount": "Tamu ({{count}})", - "removeGuests": "Singkirkan tamu", - "anonymousGuest": "Tamu", - "online": "on line", - "offline": "offline", - "you": "(Anda)", - "hostBadge": "Tuan rumah", - "presenterBadge": "Mempersembahkan", - "present": "Hadiah", - "presentTitle": "Naik ke panggung", - "presentHost": "Tuan rumah", - "searchHosts": "Cari host", - "noHostsFound": "Tidak ditemukan host yang tersedia.", - "presentProtocol": "Protokol", - "stopPresenting": "Berhenti melakukan presentasi", - "takeOver": "Mengambil alih", - "takeOverConfirmTitle": "Mengganti presentasi yang ada?", - "takeOverDescription": "{{name}} sedang melakukan presentasi sekarang. Mengambil alih akan memutuskan koneksi semua orang dari panggung tersebut.", - "emptyStage": "Tidak ada yang sedang presentasi. Silakan naik ke panggung untuk berbagi sesi.", - "stageLoading": "Terhubung ke panggung...", - "stageEnded": "Presentasi berakhir", - "roomEnded": "Pertemuan ini telah berakhir.", - "presenterLabel": "{{name}} sedang menampilkan", - "guestLink": "Tautan tamu", - "guestLinkOn": "Tautan tamu sudah aktif. Siapa pun yang memiliki tautan tersebut dapat menonton pertunjukan di panggung.", - "guestLinkOff": "Tautan tamu dinonaktifkan.", - "copyLink": "Salin tautan", - "rotateLink": "Putar tautan", - "disableLink": "Nonaktifkan tautan", - "rotateLinkConfirmTitle": "Putar tautan tamu?", - "rotateLinkDescription": "Koneksi saat ini akan segera berhenti berfungsi. Tamu SSH yang terhubung akan terputus.", - "disableLinkConfirmTitle": "Nonaktifkan tautan tamu?", - "disableLinkDescription": "Pengguna anonim yang mengakses melalui SSH akan langsung terputus dan tautan saat ini akan berhenti berfungsi.", - "linkCopied": "Tautan disalin", - "roles": "Peran", - "users": "Pengguna", - "searchUsers": "Cari pengguna", - "endConfirmTitle": "Apakah pertemuan ini bisa diakhiri?", - "endDescription": "{{name}} akan berakhir untuk semua orang dan tidak dapat dibuka kembali.", - "endPersistentDescription": "Presentasi aktif di {{name}} akan berhenti. Ruangan tetap tersedia untuk digunakan kembali.", - "deleteRoom": "Hapus ruangan", - "deleteConfirmTitle": "Hapus ruangan ini?", - "deleteDescription": "{{name}} akan dihapus secara permanen untuk semua orang dan tidak dapat dibatalkan.", - "invitedTo": "Anda diundang ke \"{{name}}\"", - "guest": { - "title": "Pertemuan", - "waiting": "Menunggu pembawa acara...", - "linkInvalid": "Tautan tamu ini tidak valid atau rapat telah berakhir." - }, - "reopenFromPanel": "Tab rapat ini telah kedaluwarsa. Buka kembali ruangan dari panel Rapat.", - "retry": "Mencoba kembali", - "requestControl": "Kontrol permintaan", - "cancelControlRequest": "Batalkan permintaan", - "controlQueue": "Permintaan kontrol", - "noControlRequests": "Tidak ada permintaan yang tertunda.", - "grantControlTo": "Berikan kendali ke {{name}}", - "dismissControlRequest": "Abaikan permintaan kontrol {{name}}", - "releaseControl": "Lepaskan kontrol", - "grantControl": "Berikan kendali", - "revokeControl": "Ambil kembali kendali", - "controlRequestedBy": "{{name}} meminta kendali", - "grant": "Menganugerahkan", - "controlBadge": "Dalam kendali", - "youArePresenting": "Anda sedang melakukan presentasi di tab lain. Hentikan presentasi untuk memberikan kesempatan kepada saya.", - "created": "Ruangan dibuat", - "invited": "Anggota diundang", - "roomTab": "Pertemuan", - "serverChanged": "Server rapat telah berubah. Buka kembali rapat ini dari panel Rapat.", - "hostNotSynced": "Sinkronkan host ini dengan server rapat sebelum presentasi.", - "linkRequiresServer": "Hubungkan desktop ini ke server Termix dan buat rapat di sana untuk berbagi tautan web." - }, - "nav": { - "collab": "Pertemuan" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Izinkan Berbagi Sesi", - "description": "Izinkan sesi terminal langsung, RDP, VNC, dan Telnet untuk dibagikan di seluruh instance. Menggantikan setiap pengaturan berbagi per host saat dinonaktifkan." - } - }, - "host": { - "allowSessionSharing": { - "label": "Izinkan Berbagi Sesi", - "description": "Izinkan sesi langsung di host ini dibagikan melalui tautan atau dengan pengguna lain." - } - } - }, - "permissions": { - "use": { - "title": "Bagikan sesi dan bergabunglah dengan ruangan.", - "description": "Bagikan sesi langsung melalui tautan atau dengan pengguna lain, serta buat dan bergabunglah dengan ruang kolaborasi." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Tidak pernah", - "oneHour": "1 jam", - "oneDay": "24 jam", - "sevenDays": "7 hari", - "thirtyDays": "30 hari", - "custom": "Kebiasaan" - } - } - }, - "connections": { - "sectionSharedWithMe": "Dibagikan kepada saya", - "sharedBy": "Dibagikan oleh {{username}}", - "join": "Bergabung", - "sharedSessionLabel": "{{hostName}} (dibagikan)" - }, - "plugin": { - "name": "Sesi Berbagi" - } -} diff --git a/plugins/session-sharing/locales/translated/it_IT.json b/plugins/session-sharing/locales/translated/it_IT.json deleted file mode 100644 index 689441fde..000000000 --- a/plugins/session-sharing/locales/translated/it_IT.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Connessione alla sessione condivisa in corso...", - "linkInvalid": "Questo link di condivisione non è valido, è scaduto o è stato revocato.", - "rateLimited": "Troppi tentativi, riprova tra poco.", - "sessionEnded": "Questa sessione è terminata", - "readOnlyBadge": "Solo visualizzazione", - "ownerLabel": "Proprietario dell'host" - }, - "modalTitle": "Sessione di condivisione", - "shareButton": "Condividi", - "notReadyToShare": "La sessione non è ancora pronta per la condivisione.", - "modeTab": { - "link": "Collegamento", - "user": "Utente" - }, - "linkModeDescription": "Chiunque abbia questo link può partecipare, non è necessario creare un account.", - "userModeDescription": "Condividi la sessione con un utente specifico che ha già accesso a questo host. Se non ha ancora accesso, condividi prima l'host con lui oppure usa un link. Una volta condivisa, la sessione apparirà nella sua scheda Connessioni.", - "permissionLevel": { - "label": "Livello di autorizzazione", - "readOnly": "Sola lettura", - "readOnlyDescription": "È possibile seguire la sessione in diretta, ma non è possibile digitare o interagire.", - "readWrite": "lettura-scrittura", - "readWriteDescription": "Può digitare e interagire con la sessione proprio come il proprietario." - }, - "expiryLabel": "Scadenza del collegamento", - "createLinkButton": "Crea collegamento", - "createShareButton": "Condividi con l'utente", - "searchUsersPlaceholder": "Cerca utenti...", - "noUsersFound": "Nessun utente trovato", - "linkCreated": "Link di condivisione creato", - "linkNeedsServer": "I link di condivisione funzionano solo per le sessioni su un server Termix. Collega questo host tramite il tuo server collegato per condividerlo con un link.", - "linkCopied": "Link copiato negli appunti", - "copyLink": "Copia il link", - "shareCreated": "Sessione condivisa. Sarà visibile nella scheda Connessioni.", - "shareFailed": "Impossibile creare la condivisione", - "userLacksHostAccess": "Quell'utente non ha ancora accesso a questo host. Condividi prima l'host con lui, oppure usa un link.", - "activeShares": "Condivisioni attive", - "noActiveShares": "Nessuna azione attiva in questa sessione.", - "revoke": "Revoca", - "revokeConfirmTitle": "Revocare questa condivisione?", - "revokeConfirmDescription": "Chiunque utilizzi questa condivisione perderà immediatamente l'accesso.", - "revoked": "Azione revocata.", - "revokeFailed": "Impossibile revocare la quota", - "joinCount": "{{count}} unisciti", - "joinCount_other": "{{count}} si unisce", - "expiresAt": "Scade il {{date}}", - "linkShareBadge": "Collegamento", - "userShareBadge": "Utente: {{username}}", - "loadSharesFailed": "Impossibile caricare le azioni attive", - "remoteDisplayUnavailable": "Al momento il desktop remoto non è disponibile.", - "sharedWithMe": "Sessioni condivise con me", - "customHoursPlaceholder": "Ore fino alla scadenza dell'accesso" - }, - "collab": { - "createRoom": "Nuova stanza", - "roomName": "Nome della stanza", - "persistentRoom": "Stanza persistente", - "persistentRoomHint": "Le stanze persistenti rimangono elencate anche dopo la fine della riunione e possono essere riutilizzate.", - "noRooms": "Non ci sono ancora stanze. Creane una per avviare una riunione.", - "openRoom": "Apri", - "endRoom": "Fine della riunione", - "leaveRoom": "Partire", - "deleteMember": "Rimuovi", - "removeMember": "Rimuovi {{name}}", - "removeMemberTitle": "Rimuovere questo membro?", - "removeMemberDescription": "{{name}} perderà immediatamente l'accesso a questa stanza. Qualsiasi visualizzatore SSH attivo verrà disconnesso.", - "invite": "Invitare", - "inviteTitle": "Invita i membri", - "members": "Membri", - "membersPanel": "Membri della stanza e richieste di controllo", - "membersWithCount": "Membri ({{count}})", - "guestsWithCount": "Ospiti ({{count}})", - "removeGuests": "Rimuovi gli ospiti", - "anonymousGuest": "Ospite", - "online": "online", - "offline": "offline", - "you": "(Voi)", - "hostBadge": "Host", - "presenterBadge": "Presentazione", - "present": "Presente", - "presentTitle": "Sali sul palco", - "presentHost": "Host", - "searchHosts": "Cerca host", - "noHostsFound": "Nessun host disponibile trovato.", - "presentProtocol": "Protocollo", - "stopPresenting": "Smetti di presentare", - "takeOver": "Rilevare", - "takeOverConfirmTitle": "Sostituire la presentazione attuale?", - "takeOverDescription": "{{name}} sta presentando ora. Prendere il controllo disconnetterà tutti da quel palco.", - "emptyStage": "Nessuno sta presentando. Salite sul palco per condividere una sessione.", - "stageLoading": "Collegamento al palco...", - "stageEnded": "La presentazione è terminata", - "roomEnded": "La riunione è terminata", - "presenterLabel": "{{name}} sta presentando", - "guestLink": "Link ospite", - "guestLinkOn": "Il link per gli ospiti è attivo. Chiunque abbia il link può assistere alla diretta.", - "guestLinkOff": "Il link per gli ospiti è disattivato.", - "copyLink": "Copia il link", - "rotateLink": "Ruota il collegamento", - "disableLink": "Disabilita il collegamento", - "rotateLinkConfirmTitle": "Ruotare il link per gli ospiti?", - "rotateLinkDescription": "Il collegamento attuale smetterà di funzionare immediatamente. Gli ospiti SSH connessi verranno disconnessi.", - "disableLinkConfirmTitle": "Disabilitare il link per gli ospiti?", - "disableLinkDescription": "Gli utenti che tentano di connettersi tramite SSH in modo anonimo verranno disconnessi immediatamente e il collegamento corrente smetterà di funzionare.", - "linkCopied": "Collegamento copiato", - "roles": "Ruoli", - "users": "Utenti", - "searchUsers": "Utenti di ricerca", - "endConfirmTitle": "Chiudere questa riunione?", - "endDescription": "{{name}} terminerà per tutti e non potrà essere riaperto.", - "endPersistentDescription": "La presentazione attiva in {{name}} verrà interrotta. La sala rimane disponibile per un altro utilizzo.", - "deleteRoom": "Eliminare la stanza", - "deleteConfirmTitle": "Eliminare questa stanza?", - "deleteDescription": "{{name}} verrà eliminato definitivamente per tutti e non sarà possibile annullare l'operazione.", - "invitedTo": "Sei stato invitato a \"{{name}}\"", - "guest": { - "title": "Incontro", - "waiting": "In attesa del presentatore...", - "linkInvalid": "Questo link per gli ospiti non è valido oppure la riunione è terminata." - }, - "reopenFromPanel": "La scheda di questa riunione è scaduta. Riapri la sala riunioni dal pannello Riunioni.", - "retry": "Riprova", - "requestControl": "Controllo della richiesta", - "cancelControlRequest": "Annulla richiesta", - "controlQueue": "Richieste di controllo", - "noControlRequests": "Nessuna richiesta in sospeso.", - "grantControlTo": "Assegna il controllo a {{name}}", - "dismissControlRequest": "Ignora la richiesta di controllo di {{name}}", - "releaseControl": "Controllo del rilascio", - "grantControl": "Dare il controllo", - "revokeControl": "Riprendi il controllo", - "controlRequestedBy": "{{name}} ha chiesto il controllo", - "grant": "Concessione", - "controlBadge": "In controllo", - "youArePresenting": "Stai presentando in un'altra scheda. Interrompi la presentazione per cedere la parola.", - "created": "Stanza creata", - "invited": "Membri invitati", - "roomTab": "Incontro", - "serverChanged": "Il server della riunione è cambiato. Riapri la riunione dal pannello Riunioni.", - "hostNotSynced": "Sincronizza l'host con il server della riunione prima di iniziare la presentazione.", - "linkRequiresServer": "Collega questo computer desktop a un server Termix e crea la riunione lì per condividere un link web." - }, - "nav": { - "collab": "Riunioni" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Consenti la condivisione della sessione", - "description": "Consente la condivisione di sessioni live di terminale, RDP, VNC e Telnet a livello di istanza. Quando disabilitata, questa opzione sovrascrive tutte le impostazioni di condivisione per singolo host." - } - }, - "host": { - "allowSessionSharing": { - "label": "Consenti la condivisione della sessione", - "description": "Consenti la condivisione delle sessioni live su questo host tramite link o con altri utenti" - } - } - }, - "permissions": { - "use": { - "title": "Condividi sessioni e unisciti alle stanze", - "description": "Condividi sessioni live tramite link o con altri utenti, e crea e partecipa a stanze di collaborazione." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Mai", - "oneHour": "1 ora", - "oneDay": "24 ore", - "sevenDays": "7 giorni", - "thirtyDays": "30 giorni", - "custom": "Personalizzato" - } - } - }, - "connections": { - "sectionSharedWithMe": "Condiviso con me", - "sharedBy": "Condiviso da {{username}}", - "join": "Giuntura", - "sharedSessionLabel": "{{hostName}} (condiviso)" - }, - "plugin": { - "name": "Condivisione della sessione" - } -} diff --git a/plugins/session-sharing/locales/translated/ja_JP.json b/plugins/session-sharing/locales/translated/ja_JP.json deleted file mode 100644 index 70c1ddbd5..000000000 --- a/plugins/session-sharing/locales/translated/ja_JP.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "共有セッションに接続しています...", - "linkInvalid": "この共有リンクは無効、期限切れ、または取り消されています。", - "rateLimited": "試行回数が多すぎます。しばらくしてからもう一度お試しください。", - "sessionEnded": "このセッションは終了しました", - "readOnlyBadge": "閲覧のみ", - "ownerLabel": "ホストオーナー" - }, - "modalTitle": "セッションを共有する", - "shareButton": "共有", - "notReadyToShare": "セッションはまだ共有できません", - "modeTab": { - "link": "リンク", - "user": "ユーザー" - }, - "linkModeDescription": "このリンクを知っている人なら誰でも参加できます。アカウントは必要ありません。", - "userModeDescription": "このホストへのアクセス権を既に持っている特定のユーザーと共有します。まだアクセス権がない場合は、まずホストを共有するか、代わりにリンクを使用してください。共有が完了すると、セッションはユーザーの「接続」タブに表示されます。", - "permissionLevel": { - "label": "権限レベル", - "readOnly": "読み取り専用", - "readOnlyDescription": "セッションをライブで視聴することはできますが、文字入力やインタラクションはできません。", - "readWrite": "読み書き", - "readWriteDescription": "所有者と同様に、セッション内で文字を入力したり操作したりできます。" - }, - "expiryLabel": "リンクの有効期限", - "createLinkButton": "リンクを作成する", - "createShareButton": "ユーザーと共有する", - "searchUsersPlaceholder": "ユーザーを検索...", - "noUsersFound": "ユーザーが見つかりませんでした", - "linkCreated": "共有リンクが作成されました", - "linkNeedsServer": "共有リンクは、Termixサーバー上のセッションでのみ機能します。リンクを使用してホストを共有するには、リンク先のサーバー経由でこのホストに接続してください。", - "linkCopied": "リンクがクリップボードにコピーされました", - "copyLink": "リンクをコピー", - "shareCreated": "セッションが共有されました。相手の「接続」タブに表示されます。", - "shareFailed": "共有の作成に失敗しました", - "userLacksHostAccess": "そのユーザーはまだこのホストへのアクセス権を持っていません。まずホストを共有するか、代わりにリンクを使用してください。", - "activeShares": "アクティブなシェア", - "noActiveShares": "今セッションで有効な株式はありません", - "revoke": "無効化", - "revokeConfirmTitle": "この共有を取り消しますか?", - "revokeConfirmDescription": "この共有ファイルを使用している人は、即座にアクセス権を失います。", - "revoked": "シェアが取り消されました", - "revokeFailed": "シェアの取り消しに失敗しました", - "joinCount": "{{count}} 参加する", - "joinCount_other": "{{count}} が参加します", - "expiresAt": "有効期限 {{date}}", - "linkShareBadge": "リンク", - "userShareBadge": "ユーザー: {{username}}", - "loadSharesFailed": "アクティブな共有の読み込みに失敗しました", - "remoteDisplayUnavailable": "現在、リモートデスクトップはご利用いただけません。", - "sharedWithMe": "私と共有されたセッション", - "customHoursPlaceholder": "アクセス期限までの時間数" - }, - "collab": { - "createRoom": "新しい部屋", - "roomName": "部屋名", - "persistentRoom": "永続的な部屋", - "persistentRoomHint": "永続的な会議室は、会議終了後もリストに表示され、再利用できます。", - "noRooms": "まだ会議室がありません。会議室を作成して会議を開始してください。", - "openRoom": "開く", - "endRoom": "会議終了", - "leaveRoom": "離れる", - "deleteMember": "削除", - "removeMember": "{{name}}を削除", - "removeMemberTitle": "このメンバーを削除しますか?", - "removeMemberDescription": "{{name}} は直ちにこのルームへのアクセス権を失います。アクティブな SSH ビューアはすべて切断されます。", - "invite": "招待する", - "inviteTitle": "メンバーを招待する", - "members": "メンバー", - "membersPanel": "ルームメンバーと制御リクエスト", - "membersWithCount": "メンバー({{count}})", - "guestsWithCount": "ゲスト ({{count}})", - "removeGuests": "ゲストを退出させる", - "anonymousGuest": "ゲスト", - "online": "オンライン", - "offline": "オフライン", - "you": "(あなた)", - "hostBadge": "ホスト", - "presenterBadge": "発表します", - "present": "現在", - "presentTitle": "ステージに立つ", - "presentHost": "ホスト", - "searchHosts": "ホストを検索", - "noHostsFound": "利用可能なホストが見つかりませんでした。", - "presentProtocol": "プロトコル", - "stopPresenting": "プレゼンテーションをやめる", - "takeOver": "引き継ぐ", - "takeOverConfirmTitle": "現在のプレゼンテーションを置き換えますか?", - "takeOverDescription": "{{name}} が現在プレゼンテーションを行っています。引き継ぐと、そのステージから全員が切断されます。", - "emptyStage": "発表者はいません。ステージに上がってセッションを共有してください。", - "stageLoading": "ステージに接続中…", - "stageEnded": "プレゼンテーションは終了しました", - "roomEnded": "この会議は終了しました。", - "presenterLabel": "{{name}} がプレゼンテーションを行っています", - "guestLink": "ゲストリンク", - "guestLinkOn": "ゲストリンクが有効になっています。リンクを知っている人なら誰でもステージを視聴できます。", - "guestLinkOff": "ゲストリンクが無効になっています。", - "copyLink": "リンクをコピー", - "rotateLink": "リンクを回転させる", - "disableLink": "リンクを無効にする", - "rotateLinkConfirmTitle": "ゲストリンクを回転させますか?", - "rotateLinkDescription": "現在の接続は直ちに停止します。接続中のSSHゲストは切断されます。", - "disableLinkConfirmTitle": "ゲストリンクを無効にしますか?", - "disableLinkDescription": "匿名SSHビューアは直ちに切断され、現在の接続は機能しなくなります。", - "linkCopied": "リンクをコピーしました", - "roles": "ロール", - "users": "ユーザー", - "searchUsers": "ユーザーを検索", - "endConfirmTitle": "この会議を終了しますか?", - "endDescription": "{{name}} は全員に対して終了し、再開することはできません。", - "endPersistentDescription": "{{name}} で現在行われているプレゼンテーションは終了します。この部屋は引き続きご利用いただけます。", - "deleteRoom": "ルームを削除", - "deleteConfirmTitle": "この部屋を削除しますか?", - "deleteDescription": "{{name}} は全員に対して完全に削除され、元に戻すことはできません。", - "invitedTo": "あなたは「{{name}}」に招待されました。", - "guest": { - "title": "ミーティング", - "waiting": "プレゼンターをお待ちしています…", - "linkInvalid": "このゲストリンクは無効であるか、ミーティングが終了しています" - }, - "reopenFromPanel": "この会議タブの有効期限が切れました。会議パネルから会議室を再度開いてください。", - "retry": "再試行", - "requestControl": "リクエスト制御", - "cancelControlRequest": "キャンセルリクエスト", - "controlQueue": "制御要求", - "noControlRequests": "保留中のリクエストはありません。", - "grantControlTo": "{{name}}に制御権を与える", - "dismissControlRequest": "{{name}}の制御要求を却下します", - "releaseControl": "リリース制御", - "grantControl": "コントロールを与える", - "revokeControl": "主導権を取り戻す", - "controlRequestedBy": "{{name}} が制御を要求しました", - "grant": "付与", - "controlBadge": "制御する", - "youArePresenting": "あなたは別のタブでプレゼンテーションを行っています。プレゼンテーションを中断して、ステージを譲ってください。", - "created": "ルームが作成されました", - "invited": "会員の皆様を招待いたします", - "roomTab": "ミーティング", - "serverChanged": "会議サーバーが変更されました。会議パネルからこの会議を再度開いてください。", - "hostNotSynced": "プレゼンテーションを行う前に、このホストを会議サーバーと同期させてください。", - "linkRequiresServer": "このデスクトップをTermixサーバーに接続し、そこで会議を作成してWebリンクを共有してください。" - }, - "nav": { - "collab": "会議" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "セッション共有を許可する", - "description": "ライブターミナル、RDP、VNC、およびTelnetセッションをインスタンス全体で共有できるようにします。無効にすると、ホストごとの共有設定がすべて上書きされます。" - } - }, - "host": { - "allowSessionSharing": { - "label": "セッション共有を許可する", - "description": "このホスト上のライブセッションをリンクまたは他のユーザーと共有できるようにします。" - } - } - }, - "permissions": { - "use": { - "title": "セッションを共有し、ルームに参加する", - "description": "ライブセッションをリンクまたは他のユーザーと共有したり、コラボレーションルームを作成・参加したりできます。" - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "無期限", - "oneHour": "1時間", - "oneDay": "24時間", - "sevenDays": "7日間", - "thirtyDays": "30日間", - "custom": "カスタム" - } - } - }, - "connections": { - "sectionSharedWithMe": "私と共有しました", - "sharedBy": "{{username}} によって共有されました", - "join": "参加する", - "sharedSessionLabel": "{{hostName}} (共有)" - }, - "plugin": { - "name": "セッション共有" - } -} diff --git a/plugins/session-sharing/locales/translated/ko_KR.json b/plugins/session-sharing/locales/translated/ko_KR.json deleted file mode 100644 index 816e2e6f3..000000000 --- a/plugins/session-sharing/locales/translated/ko_KR.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "공유 세션에 연결 중...", - "linkInvalid": "이 공유 링크는 유효하지 않거나, 만료되었거나, 취소되었습니다.", - "rateLimited": "시도 횟수가 너무 많습니다. 잠시 후 다시 시도해 주세요.", - "sessionEnded": "이번 세션이 종료되었습니다.", - "readOnlyBadge": "보기 전용", - "ownerLabel": "호스트 소유자" - }, - "modalTitle": "공유 세션", - "shareButton": "공유", - "notReadyToShare": "세션을 공유할 준비가 아직 되지 않았습니다.", - "modeTab": { - "link": "링크", - "user": "사용자" - }, - "linkModeDescription": "이 링크만 있으면 누구나 가입할 수 있으며, 계정은 필요하지 않습니다.", - "userModeDescription": "이미 해당 호스트에 접근 권한이 있는 특정 사용자와 공유하세요. 접근 권한이 없는 경우, 먼저 호스트를 공유하거나 링크를 사용하세요. 공유가 완료되면 해당 세션이 사용자의 '연결' 탭에 나타납니다.", - "permissionLevel": { - "label": "권한 수준", - "readOnly": "읽기 전용", - "readOnlyDescription": "실시간으로 세션을 시청할 수는 있지만, 입력하거나 상호 작용할 수는 없습니다.", - "readWrite": "읽기-쓰기", - "readWriteDescription": "소유자와 마찬가지로 세션에 입력하고 상호 작용할 수 있습니다." - }, - "expiryLabel": "링크 만료", - "createLinkButton": "링크 생성", - "createShareButton": "사용자와 공유", - "searchUsersPlaceholder": "사용자 검색...", - "noUsersFound": "사용자를 찾을 수 없습니다.", - "linkCreated": "공유 링크가 생성되었습니다", - "linkNeedsServer": "공유 링크는 Termix 서버의 세션에서만 작동합니다. 링크를 사용하여 공유하려면 연결된 서버를 통해 이 호스트에 연결하십시오.", - "linkCopied": "링크가 클립보드에 복사되었습니다", - "copyLink": "링크 복사", - "shareCreated": "세션이 공유되었습니다. 상대방의 연결 탭에 표시됩니다.", - "shareFailed": "공유 폴더 생성에 실패했습니다.", - "userLacksHostAccess": "해당 사용자는 아직 이 호스트에 대한 접근 권한이 없습니다. 먼저 호스트를 공유하거나 링크를 사용하세요.", - "activeShares": "활성 주식", - "noActiveShares": "이번 세션에는 활성 주식이 없습니다.", - "revoke": "취소", - "revokeConfirmTitle": "이 지분을 취소하시겠습니까?", - "revokeConfirmDescription": "이 공유 폴더를 사용하는 사람은 누구든 즉시 접근 권한을 잃게 됩니다.", - "revoked": "공유 취소됨", - "revokeFailed": "공유 취소에 실패했습니다", - "joinCount": "{{count}} 참여", - "joinCount_other": "{{count}} 참여", - "expiresAt": "만료일: {{date}}", - "linkShareBadge": "링크", - "userShareBadge": "사용자: {{username}}", - "loadSharesFailed": "활성 공유 항목을 로드하는 데 실패했습니다.", - "remoteDisplayUnavailable": "현재 원격 데스크톱을 사용할 수 없습니다.", - "sharedWithMe": "나와 공유된 세션", - "customHoursPlaceholder": "액세스 만료까지 시간" - }, - "collab": { - "createRoom": "새로운 방", - "roomName": "객실 이름", - "persistentRoom": "지속적인 방", - "persistentRoomHint": "회의가 끝난 후에도 지정된 회의실 목록에 남아 재사용할 수 있습니다.", - "noRooms": "아직 회의실이 없습니다. 회의를 시작하려면 회의실을 만드세요.", - "openRoom": "열기", - "endRoom": "회의 종료", - "leaveRoom": "떠나다", - "deleteMember": "제거", - "removeMember": "{{name}} 제거", - "removeMemberTitle": "이 회원을 삭제하시겠습니까?", - "removeMemberDescription": "{{name}} 사용자는 즉시 이 방에 대한 접근 권한을 잃게 됩니다. 활성화된 SSH 뷰어는 모두 연결이 끊어집니다.", - "invite": "초대하다", - "inviteTitle": "회원 초대", - "members": "회원들", - "membersPanel": "회의실 구성원 및 제어 요청", - "membersWithCount": "멤버({{count}})", - "guestsWithCount": "손님 ({{count}})", - "removeGuests": "손님을 제거하세요", - "anonymousGuest": "손님", - "online": "온라인", - "offline": "오프라인", - "you": "(너)", - "hostBadge": "호스트", - "presenterBadge": "소개합니다", - "present": "현재의", - "presentTitle": "무대에 오르세요", - "presentHost": "호스트", - "searchHosts": "호스트 검색", - "noHostsFound": "사용 가능한 호스트를 찾을 수 없습니다.", - "presentProtocol": "프로토콜", - "stopPresenting": "프레젠테이션을 중단하세요", - "takeOver": "인수하세요", - "takeOverConfirmTitle": "현재 프레젠테이션을 교체하시겠습니까?", - "takeOverDescription": "{{name}} 님이 현재 발표 중입니다. 발표를 이어받으면 해당 무대에서 모든 사용자의 연결이 끊어집니다.", - "emptyStage": "발표자가 없습니다. 무대에 올라 세션을 공유해 보세요.", - "stageLoading": "무대와 연결 중...", - "stageEnded": "프레젠테이션이 끝났습니다.", - "roomEnded": "이 회의는 종료되었습니다.", - "presenterLabel": "{{name}} 가 발표합니다", - "guestLink": "게스트 링크", - "guestLinkOn": "게스트 링크가 활성화되었습니다. 링크만 있으면 누구나 무대를 시청할 수 있습니다.", - "guestLinkOff": "게스트 링크가 꺼져 있습니다.", - "copyLink": "링크 복사", - "rotateLink": "링크를 회전하세요", - "disableLink": "링크 비활성화", - "rotateLinkConfirmTitle": "게스트 링크를 변경하시겠습니까?", - "rotateLinkDescription": "현재 연결은 즉시 작동을 멈춥니다. 연결된 SSH 게스트는 연결이 끊어집니다.", - "disableLinkConfirmTitle": "게스트 링크를 비활성화하시겠습니까?", - "disableLinkDescription": "익명 SSH 뷰어는 즉시 연결이 끊어지고 현재 링크가 작동을 멈춥니다.", - "linkCopied": "링크가 복사되었습니다", - "roles": "역할", - "users": "사용자", - "searchUsers": "사용자 검색", - "endConfirmTitle": "이 회의를 종료하시겠습니까?", - "endDescription": "{{name}} 는 모든 사용자에게 종료되며 다시 열 수 없습니다.", - "endPersistentDescription": "{{name}} 에서 진행 중이던 프레젠테이션이 종료됩니다. 해당 공간은 재사용 가능합니다.", - "deleteRoom": "방 삭제", - "deleteConfirmTitle": "이 방을 삭제하시겠습니까?", - "deleteDescription": "{{name}} 는 모든 사용자에게 영구적으로 삭제되며 되돌릴 수 없습니다.", - "invitedTo": "당신은 \"{{name}} \"에 초대되었습니다.", - "guest": { - "title": "회의", - "waiting": "발표자를 기다리는 중...", - "linkInvalid": "게스트 링크가 유효하지 않거나 회의가 종료되었습니다." - }, - "reopenFromPanel": "이 회의 탭이 만료되었습니다. 회의 패널에서 회의실을 다시 여세요.", - "retry": "재시도", - "requestControl": "요청 제어", - "cancelControlRequest": "취소 요청", - "controlQueue": "제어 요청", - "noControlRequests": "보류 중인 요청이 없습니다.", - "grantControlTo": "{{name}}에 제어권을 넘겨주세요", - "dismissControlRequest": "{{name}}의 제어 요청을 기각합니다.", - "releaseControl": "릴리스 컨트롤", - "grantControl": "제어권을 넘겨주세요", - "revokeControl": "주도권을 되찾으세요", - "controlRequestedBy": "{{name}} 제어권을 요청했습니다.", - "grant": "승인하다", - "controlBadge": "통제하에", - "youArePresenting": "다른 탭에서 프레젠테이션을 진행하고 계십니다. 프레젠테이션을 중지하고 다음 사람에게 넘겨주세요.", - "created": "방이 생성되었습니다", - "invited": "회원 초대", - "roomTab": "회의", - "serverChanged": "회의 서버가 변경되었습니다. 회의 패널에서 이 회의를 다시 여세요.", - "hostNotSynced": "발표하기 전에 이 호스트를 회의 서버와 동기화하세요.", - "linkRequiresServer": "이 데스크톱을 Termix 서버에 연결하고, 그곳에서 웹 링크를 공유하는 회의를 생성하세요." - }, - "nav": { - "collab": "회의" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "세션 공유 허용", - "description": "라이브 터미널, RDP, VNC 및 Telnet 세션을 인스턴스 전체에서 공유할 수 있도록 허용합니다. 이 기능을 비활성화하면 호스트별 공유 설정이 모두 무시됩니다." - } - }, - "host": { - "allowSessionSharing": { - "label": "세션 공유 허용", - "description": "이 호스트에서 진행되는 라이브 세션을 링크 또는 다른 사용자와 공유하세요." - } - } - }, - "permissions": { - "use": { - "title": "세션을 공유하고 회의실에 참여하세요", - "description": "링크를 통해 또는 다른 사용자와 실시간 세션을 공유하고, 공동 작업 공간을 만들고 참여하세요." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "안 함", - "oneHour": "1시간", - "oneDay": "24시간", - "sevenDays": "7일", - "thirtyDays": "30일", - "custom": "사용자 지정" - } - } - }, - "connections": { - "sectionSharedWithMe": "나와 공유됨", - "sharedBy": "{{username}}님이 공유", - "join": "가입하다", - "sharedSessionLabel": "{{hostName}} (공유됨)" - }, - "plugin": { - "name": "세션 공유" - } -} diff --git a/plugins/session-sharing/locales/translated/nl_NL.json b/plugins/session-sharing/locales/translated/nl_NL.json deleted file mode 100644 index c17e16e3a..000000000 --- a/plugins/session-sharing/locales/translated/nl_NL.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Verbinding maken met de gedeelde sessie...", - "linkInvalid": "Deze deelbare link is ongeldig, verlopen of ingetrokken.", - "rateLimited": "Te veel pogingen, probeer het over een tijdje opnieuw.", - "sessionEnded": "Deze sessie is afgelopen.", - "readOnlyBadge": "Alleen bekijken", - "ownerLabel": "Gastheer/gastvrouw" - }, - "modalTitle": "Deel sessie", - "shareButton": "Deel", - "notReadyToShare": "De sessie is nog niet klaar om te delen.", - "modeTab": { - "link": "Link", - "user": "Gebruiker" - }, - "linkModeDescription": "Iedereen met deze link kan deelnemen, een account is niet nodig.", - "userModeDescription": "Deel de host met een specifieke gebruiker die er al toegang toe heeft. Als die gebruiker nog geen toegang heeft, deel de host dan eerst met hem of haar, of gebruik een link. Zodra de sessie is gedeeld, verschijnt deze in het tabblad 'Verbindingen' van de gebruiker.", - "permissionLevel": { - "label": "Toegangsniveau", - "readOnly": "Alleen-lezen", - "readOnlyDescription": "Je kunt de sessie live volgen, maar je kunt niet typen of interageren.", - "readWrite": "Lezen en schrijven", - "readWriteDescription": "Kan typen en interactie hebben met de sessie, net als de eigenaar." - }, - "expiryLabel": "Link verloopt", - "createLinkButton": "Link maken", - "createShareButton": "Delen met gebruiker", - "searchUsersPlaceholder": "Gebruikers doorzoeken...", - "noUsersFound": "Geen gebruikers gevonden", - "linkCreated": "Deellink aangemaakt", - "linkNeedsServer": "Deellinks werken alleen voor sessies op een Termix-server. Verbind deze host via uw gekoppelde server om deze met een link te delen.", - "linkCopied": "Link gekopieerd naar het klembord.", - "copyLink": "Kopieer link", - "shareCreated": "De sessie is gedeeld. Deze verschijnt in hun tabblad 'Verbindingen'.", - "shareFailed": "Aanmaken van de share is mislukt", - "userLacksHostAccess": "Die gebruiker heeft nog geen toegang tot deze host. Deel de host eerst met hem of haar, of gebruik een link.", - "activeShares": "Actieve aandelen", - "noActiveShares": "Geen actieve aandelen voor deze sessie.", - "revoke": "Herroepen", - "revokeConfirmTitle": "Wilt u dit aandeel intrekken?", - "revokeConfirmDescription": "Iedereen die deze gedeelde map gebruikt, verliest onmiddellijk de toegang.", - "revoked": "Aandeel ingetrokken", - "revokeFailed": "Het intrekken van het aandeel is mislukt.", - "joinCount": "{{count}} join", - "joinCount_other": "{{count}} sluit zich aan", - "expiresAt": "Verloopt {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Gebruiker: {{username}}", - "loadSharesFailed": "Het laden van actieve aandelen is mislukt.", - "remoteDisplayUnavailable": "Externe bureaubladtoegang is momenteel niet beschikbaar.", - "sharedWithMe": "Sessies die met mij zijn gedeeld", - "customHoursPlaceholder": "Aantal uren tot de toegang verloopt" - }, - "collab": { - "createRoom": "Nieuwe kamer", - "roomName": "Kamernaam", - "persistentRoom": "Permanente kamer", - "persistentRoomHint": "Kamers die permanent gereserveerd blijven, blijven na afloop van de vergadering beschikbaar en kunnen opnieuw worden gebruikt.", - "noRooms": "Er zijn nog geen vergaderruimtes. Maak er een aan om een vergadering te starten.", - "openRoom": "Open", - "endRoom": "Einde vergadering", - "leaveRoom": "Vertrekken", - "deleteMember": "Verwijderen", - "removeMember": "Verwijder {{name}}", - "removeMemberTitle": "Dit lid verwijderen?", - "removeMemberDescription": "{{name}} verliest onmiddellijk de toegang tot deze ruimte. Elke actieve SSH-viewer wordt losgekoppeld.", - "invite": "Uitnodiging", - "inviteTitle": "Nodig leden uit", - "members": "Leden", - "membersPanel": "Kamerleden en beheerverzoeken", - "membersWithCount": "Leden ({{count}})", - "guestsWithCount": "Gasten ({{count}})", - "removeGuests": "Verwijder gasten", - "anonymousGuest": "Gast", - "online": "online", - "offline": "offline", - "you": "(Jij)", - "hostBadge": "Gastheer", - "presenterBadge": "Presentatie", - "present": "Cadeau", - "presentTitle": "Betreed het podium", - "presentHost": "Gastheer", - "searchHosts": "Zoek hosts", - "noHostsFound": "Geen beschikbare hosts gevonden.", - "presentProtocol": "Protocol", - "stopPresenting": "Stop met presenteren", - "takeOver": "Neem het over", - "takeOverConfirmTitle": "De huidige presentatie vervangen?", - "takeOverDescription": "{{name}} presenteert nu. Als je het overneemt, wordt iedereen van dat podium losgekoppeld.", - "emptyStage": "Niemand geeft een presentatie. Betreed het podium om een sessie te delen.", - "stageLoading": "Verbinding maken met het podium...", - "stageEnded": "De presentatie eindigde.", - "roomEnded": "Deze vergadering is afgelopen.", - "presenterLabel": "{{name}} presenteert", - "guestLink": "Gastlink", - "guestLinkOn": "De gastlink is ingeschakeld. Iedereen met de link kan het optreden bekijken.", - "guestLinkOff": "De gastlink is uitgeschakeld.", - "copyLink": "Kopieer link", - "rotateLink": "Draai de link", - "disableLink": "Link uitschakelen", - "rotateLinkConfirmTitle": "De gastlink roteren?", - "rotateLinkDescription": "De huidige verbinding wordt onmiddellijk verbroken. Verbonden SSH-gasten worden losgekoppeld.", - "disableLinkConfirmTitle": "De gastlink uitschakelen?", - "disableLinkDescription": "Anonieme SSH-gebruikers worden onmiddellijk losgekoppeld en de huidige verbinding zal niet meer werken.", - "linkCopied": "Link gekopieerd", - "roles": "Rollen", - "users": "Gebruikers", - "searchUsers": "Zoek gebruikers", - "endConfirmTitle": "Deze vergadering beëindigen?", - "endDescription": "{{name}} zal voor iedereen eindigen en kan niet opnieuw worden geopend.", - "endPersistentDescription": "De actieve presentatie in {{name}} wordt gestopt. De ruimte blijft beschikbaar voor hergebruik.", - "deleteRoom": "Verwijder kamer", - "deleteConfirmTitle": "Deze kamer verwijderen?", - "deleteDescription": "{{name}} wordt permanent verwijderd voor iedereen en kan niet ongedaan worden gemaakt.", - "invitedTo": "Je werd uitgenodigd voor \"{{name}}\"", - "guest": { - "title": "Ontmoeting", - "waiting": "Wachten op de presentator...", - "linkInvalid": "Deze gastlink is ongeldig of de vergadering is afgelopen." - }, - "reopenFromPanel": "Dit vergadertabblad is verlopen. Heropen de ruimte via het paneel Vergaderingen.", - "retry": "Opnieuw proberen", - "requestControl": "Verzoekbeheer", - "cancelControlRequest": "Verzoek annuleren", - "controlQueue": "Controleverzoeken", - "noControlRequests": "Geen openstaande aanvragen.", - "grantControlTo": "Geef de controle aan {{name}}", - "dismissControlRequest": "Negeer het controleverzoek van {{name}}", - "releaseControl": "Ontgrendeling", - "grantControl": "Geef de controle", - "revokeControl": "Neem de controle terug", - "controlRequestedBy": "{{name}} vroeg om controle", - "grant": "Studiebeurs", - "controlBadge": "Onder controle", - "youArePresenting": "Je presenteert momenteel in een ander tabblad. Stop met presenteren en geef het woord aan iemand anders.", - "created": "Kamer gecreëerd", - "invited": "Leden uitgenodigd", - "roomTab": "Ontmoeting", - "serverChanged": "De vergaderserver is gewijzigd. Heropen deze vergadering via het paneel Vergaderingen.", - "hostNotSynced": "Synchroniseer deze host met de vergaderserver voordat u de presentatie geeft.", - "linkRequiresServer": "Koppel dit bureaublad aan een Termix-server en maak daar de vergadering aan om een weblink te delen." - }, - "nav": { - "collab": "Vergaderingen" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Sessiedeling toestaan", - "description": "Sta toe dat live terminal-, RDP-, VNC- en Telnet-sessies instantiebreed worden gedeeld. Deze functie overschrijft elke instelling voor delen per host wanneer deze is uitgeschakeld." - } - }, - "host": { - "allowSessionSharing": { - "label": "Sessiedeling toestaan", - "description": "Sta toe dat live sessies op deze host via een link of met andere gebruikers worden gedeeld." - } - } - }, - "permissions": { - "use": { - "title": "Deel sessies en neem deel aan chatrooms.", - "description": "Deel live sessies via een link of met andere gebruikers, en creëer en neem deel aan samenwerkingsruimtes." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Nooit", - "oneHour": "1 uur", - "oneDay": "24 uur", - "sevenDays": "7 dagen", - "thirtyDays": "30 dagen", - "custom": "Aangepast" - } - } - }, - "connections": { - "sectionSharedWithMe": "Met mij gedeeld", - "sharedBy": "Gedeeld door {{username}}", - "join": "Meedoen", - "sharedSessionLabel": "{{hostName}} (gedeeld)" - }, - "plugin": { - "name": "Sessie delen" - } -} diff --git a/plugins/session-sharing/locales/translated/no_NO.json b/plugins/session-sharing/locales/translated/no_NO.json deleted file mode 100644 index 8b07cb755..000000000 --- a/plugins/session-sharing/locales/translated/no_NO.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Kobler til delt økt...", - "linkInvalid": "Denne delingslenken er ugyldig, utløpt eller har blitt tilbakekalt", - "rateLimited": "For mange forsøk, prøv igjen snart", - "sessionEnded": "Denne økten er avsluttet", - "readOnlyBadge": "Kun visning", - "ownerLabel": "Vertseier" - }, - "modalTitle": "Del økt", - "shareButton": "Dele", - "notReadyToShare": "Økten er ikke klar til deling ennå", - "modeTab": { - "link": "Lenke", - "user": "Bruker" - }, - "linkModeDescription": "Alle med denne lenken kan bli med, ingen konto kreves.", - "userModeDescription": "Del med en bestemt bruker som allerede har tilgang til denne verten. Hvis de ikke har tilgang ennå, del verten med dem først eller bruk en lenke i stedet. Når økten er delt, vises den i deres Tilkoblinger-fane.", - "permissionLevel": { - "label": "Tillatelsesnivå", - "readOnly": "Skrivebeskyttet", - "readOnlyDescription": "Kan se økten direkte, men kan ikke skrive eller samhandle.", - "readWrite": "Lese-skrive", - "readWriteDescription": "Kan skrive og samhandle med økten akkurat som eieren." - }, - "expiryLabel": "Utløpsdato for lenke", - "createLinkButton": "Opprett lenke", - "createShareButton": "Del med brukeren", - "searchUsersPlaceholder": "Søk etter brukere...", - "noUsersFound": "Ingen brukere funnet", - "linkCreated": "Delingslenke opprettet", - "linkNeedsServer": "Delingslenker fungerer bare for økter på en Termix-server. Koble til denne verten via den tilkoblede serveren din for å dele den med en lenke.", - "linkCopied": "Lenke kopiert til utklippstavlen", - "copyLink": "Kopier lenke", - "shareCreated": "Økten er delt. Den vil vises i Tilkoblinger-fanen deres.", - "shareFailed": "Kunne ikke opprette delingen", - "userLacksHostAccess": "Den brukeren har ikke tilgang til denne verten ennå. Del verten med dem først, eller bruk en lenke i stedet.", - "activeShares": "Aktive aksjer", - "noActiveShares": "Ingen aktive delinger for denne økten", - "revoke": "Tilbakekalling", - "revokeConfirmTitle": "Tilbakekalle denne delingen?", - "revokeConfirmDescription": "Alle som bruker denne delingen vil miste tilgangen umiddelbart.", - "revoked": "Deling tilbakekalt", - "revokeFailed": "Kunne ikke tilbakekalle delingen", - "joinCount": "{{count}} bli med", - "joinCount_other": "{{count}} blir med", - "expiresAt": "Utløper {{date}}", - "linkShareBadge": "Lenke", - "userShareBadge": "Bruker: {{username}}", - "loadSharesFailed": "Kunne ikke laste inn aktive delinger", - "remoteDisplayUnavailable": "Eksternt skrivebord er ikke tilgjengelig akkurat nå.", - "sharedWithMe": "Økter delt med meg", - "customHoursPlaceholder": "Timer til tilgangen utløper" - }, - "collab": { - "createRoom": "Nytt rom", - "roomName": "Romnavn", - "persistentRoom": "Permanent rom", - "persistentRoomHint": "Permanente rom forblir oppført etter at møtet er avsluttet og kan brukes om igjen.", - "noRooms": "Ingen rom ennå. Opprett et for å starte et møte.", - "openRoom": "Åpne", - "endRoom": "Avslutt møtet", - "leaveRoom": "Forlate", - "deleteMember": "Fjern", - "removeMember": "Fjern {{name}}", - "removeMemberTitle": "Fjerne dette medlemmet?", - "removeMemberDescription": "{{name}} mister tilgangen til dette rommet umiddelbart. Alle aktive SSH-lesere blir frakoblet.", - "invite": "Invitere", - "inviteTitle": "Inviter medlemmer", - "members": "Medlemmer", - "membersPanel": "Rommedlemmer og kontrollforespørsler", - "membersWithCount": "Medlemmer ({{count}})", - "guestsWithCount": "Gjester ({{count}})", - "removeGuests": "Fjern gjester", - "anonymousGuest": "Gjest", - "online": "tilkoblet", - "offline": "frakoblet", - "you": "(du)", - "hostBadge": "Vert", - "presenterBadge": "Presenterer", - "present": "Nåværende", - "presentTitle": "Innta scenen", - "presentHost": "Vert", - "searchHosts": "Søk etter verter", - "noHostsFound": "Ingen tilgjengelige verter funnet.", - "presentProtocol": "Protokoll", - "stopPresenting": "Slutt å presentere", - "takeOver": "Ta over", - "takeOverConfirmTitle": "Erstatte den nåværende presentasjonen?", - "takeOverDescription": "{{name}} presenterer nå. Å ta over vil koble alle fra det stadiet.", - "emptyStage": "Ingen presenterer. Gå på scenen for å dele en økt.", - "stageLoading": "Kobler til scenen...", - "stageEnded": "Presentasjonen ble avsluttet", - "roomEnded": "Dette møtet er avsluttet", - "presenterLabel": "{{name}} presenterer", - "guestLink": "Gjestelenke", - "guestLinkOn": "Gjestelenken er på. Alle med lenken kan se scenen.", - "guestLinkOff": "Gjestelenken er av.", - "copyLink": "Kopier lenke", - "rotateLink": "Roter lenken", - "disableLink": "Deaktiver lenken", - "rotateLinkConfirmTitle": "Roter gjestelenken?", - "rotateLinkDescription": "Den nåværende lenken vil slutte å virke umiddelbart. Tilkoblede SSH-gjester vil bli frakoblet.", - "disableLinkConfirmTitle": "Deaktivere gjestelenken?", - "disableLinkDescription": "Anonyme SSH-lesere vil bli frakoblet umiddelbart, og den nåværende koblingen vil slutte å fungere.", - "linkCopied": "Lenken er kopiert", - "roles": "Roller", - "users": "Brukere", - "searchUsers": "Søk etter brukere", - "endConfirmTitle": "Avslutte dette møtet?", - "endDescription": "{{name}} vil avsluttes for alle og kan ikke åpnes igjen.", - "endPersistentDescription": "Den aktive presentasjonen i {{name}} stopper. Rommet er fortsatt tilgjengelig for gjenbruk.", - "deleteRoom": "Slett rom", - "deleteConfirmTitle": "Slette dette rommet?", - "deleteDescription": "{{name}} vil bli permanent slettet for alle og kan ikke angres.", - "invitedTo": "Du ble invitert til «{{name}}»", - "guest": { - "title": "Møte", - "waiting": "Venter på programlederen...", - "linkInvalid": "Denne gjestelenken er ugyldig, eller møtet er avsluttet" - }, - "reopenFromPanel": "Denne møtefanen er utløpt. Åpne rommet på nytt fra Møter-panelet.", - "retry": "Prøv igjen", - "requestControl": "Be om kontroll", - "cancelControlRequest": "Avbryt forespørsel", - "controlQueue": "Kontrollforespørsler", - "noControlRequests": "Ingen ventende forespørsler.", - "grantControlTo": "Gi kontroll til {{name}}", - "dismissControlRequest": "Avvis kontrollforespørselen til {{name}}", - "releaseControl": "Slipp kontrollen", - "grantControl": "Gi kontroll", - "revokeControl": "Ta tilbake kontrollen", - "controlRequestedBy": "{{name}} ba om kontroll", - "grant": "Stipend", - "controlBadge": "I kontroll", - "youArePresenting": "Du presenterer i en annen fane. Stopp presentasjonen for å gi fra deg scenen.", - "created": "Rommet er opprettet", - "invited": "Medlemmer invitert", - "roomTab": "Møte", - "serverChanged": "Møteserveren er endret. Åpne dette møtet på nytt fra Møter-panelet.", - "hostNotSynced": "Synkroniser denne verten med møteserveren før presentasjonen.", - "linkRequiresServer": "Koble dette skrivebordet til en Termix-server og opprett møtet der for å dele en nettlenke." - }, - "nav": { - "collab": "Møter" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Tillat øktdeling", - "description": "Tillat deling av Live Terminal-, RDP-, VNC- og Telnet-økter på tvers av hele instansen. Overstyrer alle delingsalternativer per vert når de er deaktivert." - } - }, - "host": { - "allowSessionSharing": { - "label": "Tillat øktdeling", - "description": "La direktesendte økter på denne verten deles via lenke eller med andre brukere" - } - } - }, - "permissions": { - "use": { - "title": "Del økter og bli med i rom", - "description": "Del direktesendte økter via lenke eller med andre brukere, og opprett og bli med i samarbeidsrom." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Aldri", - "oneHour": "1 time", - "oneDay": "24 timer", - "sevenDays": "7 dager", - "thirtyDays": "30 dager", - "custom": "Skikk" - } - } - }, - "connections": { - "sectionSharedWithMe": "Delt med meg", - "sharedBy": "Delt av {{username}}", - "join": "Bli med", - "sharedSessionLabel": "{{hostName}} (delt)" - }, - "plugin": { - "name": "Deling av økter" - } -} diff --git a/plugins/session-sharing/locales/translated/pl_PL.json b/plugins/session-sharing/locales/translated/pl_PL.json deleted file mode 100644 index a49c76864..000000000 --- a/plugins/session-sharing/locales/translated/pl_PL.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Łączenie z sesją współdzieloną...", - "linkInvalid": "Ten link do udostępniania jest nieprawidłowy, wygasł lub został odwołany", - "rateLimited": "Zbyt wiele prób, spróbuj ponownie za chwilę", - "sessionEnded": "Ta sesja dobiegła końca", - "readOnlyBadge": "Tylko do podglądu", - "ownerLabel": "Właściciel gospodarza" - }, - "modalTitle": "Udostępnij sesję", - "shareButton": "Udostępnij", - "notReadyToShare": "Sesja nie jest jeszcze gotowa do udostępnienia", - "modeTab": { - "link": "Link", - "user": "Użytkownik" - }, - "linkModeDescription": "Każdy mający ten link może dołączyć, nie jest wymagane założenie konta.", - "userModeDescription": "Udostępnij konkretnemu użytkownikowi, który ma już dostęp do tego hosta. Jeśli jeszcze go nie ma, najpierw udostępnij mu hosta albo skorzystaj z linku. Po udostępnieniu sesja pojawi się na jego karcie Połączenia.", - "permissionLevel": { - "label": "Poziom uprawnień", - "readOnly": "Tylko do odczytu", - "readOnlyDescription": "Może oglądać sesję na żywo, ale nie może pisać ani nic w niej zmieniać.", - "readWrite": "Odczyt-zapis", - "readWriteDescription": "Może pisać i uczestniczyć w sesji tak samo jak właściciel." - }, - "expiryLabel": "Wygaśnięcie linku", - "createLinkButton": "Utwórz link", - "createShareButton": "Udostępnij użytkownikowi", - "searchUsersPlaceholder": "Wyszukaj użytkowników...", - "noUsersFound": "Nie znaleziono użytkowników", - "linkCreated": "Utworzono link do udostępniania", - "linkNeedsServer": "Linki do udostępniania działają tylko w sesjach na serwerze Termix. Połącz się z tym hostem przez powiązany serwer, aby udostępnić go za pomocą linku.", - "linkCopied": "Link skopiowany do schowka", - "copyLink": "Kopiuj link", - "shareCreated": "Udostępniono sesję. Pojawi się na jego karcie Połączenia.", - "shareFailed": "Nie udało się utworzyć udostępnienia", - "userLacksHostAccess": "Ten użytkownik nie ma jeszcze dostępu do tego hosta. Udostępnij mu najpierw hosta lub skorzystaj z linku.", - "activeShares": "Aktywne udostępnienia", - "noActiveShares": "Brak aktywnych udostępnień tej sesji", - "revoke": "Unieważnij", - "revokeConfirmTitle": "Cofnąć to udostępnienie?", - "revokeConfirmDescription": "Każdy, kto korzysta z tego udostępnienia, natychmiast utraci dostęp.", - "revoked": "Unieważniono udostępnienie", - "revokeFailed": "Nie udało się unieważnić udostępnienia", - "joinCount": "{{count}} dołączenie", - "joinCount_other": "{{count}} dołączeń", - "expiresAt": "Wygasa {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Użytkownik: {{username}}", - "loadSharesFailed": "Nie udało się załadować aktywnych udostępnień", - "remoteDisplayUnavailable": "Zdalny pulpit nie jest obecnie dostępny.", - "sharedWithMe": "Sesje udostępnione mi", - "customHoursPlaceholder": "Godziny do wygaśnięcia dostępu" - }, - "collab": { - "createRoom": "Nowy pokój", - "roomName": "Nazwa pokoju", - "persistentRoom": "Trwały pokój", - "persistentRoomHint": "Trwałe pokoje pozostają na liście po zakończeniu spotkania i można je ponownie wykorzystać.", - "noRooms": "Brak pokoi. Utwórz jeden, aby rozpocząć spotkanie.", - "openRoom": "Otwórz", - "endRoom": "Zakończ spotkanie", - "leaveRoom": "Wyjechać", - "deleteMember": "Usuń", - "removeMember": "Usuń {{name}}", - "removeMemberTitle": "Usunąć tego członka?", - "removeMemberDescription": "{{name}} natychmiast utraci dostęp do tego pokoju. Każdy aktywny użytkownik SSH zostanie rozłączony.", - "invite": "Zapraszać", - "inviteTitle": "Zaproś członków", - "members": "Członkowie", - "membersPanel": "Członkowie pokoju i prośby o kontrolę", - "membersWithCount": "Członkowie ({{count}})", - "guestsWithCount": "Goście ({{count}})", - "removeGuests": "Usuń gości", - "anonymousGuest": "Gość", - "online": "online", - "offline": "offline", - "you": "(Ty)", - "hostBadge": "Host", - "presenterBadge": "Przedstawianie", - "present": "Obecny", - "presentTitle": "Wejdź na scenę", - "presentHost": "Host", - "searchHosts": "Wyszukaj hosty", - "noHostsFound": "Nie znaleziono dostępnych hostów.", - "presentProtocol": "Protokół", - "stopPresenting": "Zatrzymaj prezentację", - "takeOver": "Przejąć", - "takeOverConfirmTitle": "Zastąpić bieżącą prezentację?", - "takeOverDescription": "{{name}} jest teraz prezentowany. Przejęcie kontroli spowoduje odłączenie wszystkich od tej sceny.", - "emptyStage": "Nikt nie prowadzi prezentacji. Wejdź na scenę i podziel się swoją sesją.", - "stageLoading": "Łączę się ze sceną...", - "stageEnded": "Prezentacja dobiegła końca", - "roomEnded": "To spotkanie dobiegło końca", - "presenterLabel": "{{name}} prezentuje", - "guestLink": "Link gościnny", - "guestLinkOn": "Link dla gości jest włączony. Każdy, kto ma link, może obejrzeć scenę.", - "guestLinkOff": "Link dla gości jest wyłączony.", - "copyLink": "Kopiuj link", - "rotateLink": "Obróć link", - "disableLink": "Wyłącz łącze", - "rotateLinkConfirmTitle": "Obrócić link gościa?", - "rotateLinkDescription": "Obecny link natychmiast przestanie działać. Połączeni goście SSH zostaną rozłączeni.", - "disableLinkConfirmTitle": "Wyłączyć link gościa?", - "disableLinkDescription": "Anonimowi użytkownicy SSH zostaną natychmiast rozłączeni, a bieżące łącze przestanie działać.", - "linkCopied": "Link skopiowany", - "roles": "Role", - "users": "Użytkownicy", - "searchUsers": "Wyszukaj użytkowników", - "endConfirmTitle": "Zakończyć to spotkanie?", - "endDescription": "{{name}} zakończy się dla wszystkich i nie będzie można go ponownie otworzyć.", - "endPersistentDescription": "Aktywna prezentacja w {{name}} zostanie zatrzymana. Pokój pozostaje dostępny do ponownego użycia.", - "deleteRoom": "Usuń pokój", - "deleteConfirmTitle": "Usunąć ten pokój?", - "deleteDescription": "{{name}} zostanie trwale usunięty u wszystkich i nie będzie można tego cofnąć.", - "invitedTo": "Zostałeś zaproszony do „{{name}}”", - "guest": { - "title": "Spotkanie", - "waiting": "Czekam na prezentera...", - "linkInvalid": "Ten link dla gościa jest nieprawidłowy lub spotkanie się zakończyło" - }, - "reopenFromPanel": "Ta karta spotkania wygasła. Otwórz ponownie pokój z panelu Spotkania.", - "retry": "Ponów", - "requestControl": "Żądanie kontroli", - "cancelControlRequest": "Anuluj prośbę", - "controlQueue": "Żądania kontrolne", - "noControlRequests": "Brak oczekujących żądań.", - "grantControlTo": "Przekaż kontrolę {{name}}", - "dismissControlRequest": "Odrzuć żądanie kontroli {{name}}", - "releaseControl": "Kontrola wydania", - "grantControl": "Oddaj kontrolę", - "revokeControl": "Odzyskaj kontrolę", - "controlRequestedBy": "{{name}} poprosił o kontrolę", - "grant": "Dotacja", - "controlBadge": "Pod kontrolą", - "youArePresenting": "Prezentujesz na innej karcie. Zatrzymaj prezentację, aby przekazać scenę.", - "created": "Pokój utworzony", - "invited": "Zaproszeni członkowie", - "roomTab": "Spotkanie", - "serverChanged": "Zmieniono serwer spotkań. Otwórz ponownie to spotkanie z panelu Spotkania.", - "hostNotSynced": "Przed rozpoczęciem prezentacji zsynchronizuj tego hosta z serwerem konferencyjnym.", - "linkRequiresServer": "Połącz ten pulpit z serwerem Termix i utwórz tam spotkanie, aby udostępnić łącze internetowe." - }, - "nav": { - "collab": "Spotkania" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Zezwól na udostępnianie sesji", - "description": "Zezwalaj na udostępnianie sesji terminala na żywo, RDP, VNC i Telnet w obrębie instancji. Po wyłączeniu zastępuje wszystkie przełączniki udostępniania dla poszczególnych hostów." - } - }, - "host": { - "allowSessionSharing": { - "label": "Zezwól na udostępnianie sesji", - "description": "Zezwól na udostępnianie sesji na żywo na tym hoście przez link lub innym użytkownikom" - } - } - }, - "permissions": { - "use": { - "title": "Udostępniaj sesje i dołącz do pokoi", - "description": "Udostępniaj sesje na żywo za pośrednictwem łącza lub innym użytkownikom, a także twórz pokoje współpracy i do nich dołączaj." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Nigdy", - "oneHour": "1 godzina", - "oneDay": "24 godziny", - "sevenDays": "7 dni", - "thirtyDays": "30 dni", - "custom": "Niestandardowy" - } - } - }, - "connections": { - "sectionSharedWithMe": "Udostępniono mi", - "sharedBy": "Udostępnione przez {{username}}", - "join": "Dołącz", - "sharedSessionLabel": "{{hostName}} (udostępnione)" - }, - "plugin": { - "name": "Udostępnianie sesji" - } -} diff --git a/plugins/session-sharing/locales/translated/pt_BR.json b/plugins/session-sharing/locales/translated/pt_BR.json deleted file mode 100644 index 66d0b50f9..000000000 --- a/plugins/session-sharing/locales/translated/pt_BR.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Conectando à sessão compartilhada...", - "linkInvalid": "Este link de compartilhamento é inválido, expirou ou foi revogado.", - "rateLimited": "Muitas tentativas foram feitas, tente novamente em breve.", - "sessionEnded": "Esta sessão terminou.", - "readOnlyBadge": "Somente visualização", - "ownerLabel": "Proprietário do anfitrião" - }, - "modalTitle": "Sessão de compartilhamento", - "shareButton": "Compartilhar", - "notReadyToShare": "A sessão ainda não está pronta para ser compartilhada.", - "modeTab": { - "link": "Link", - "user": "Usuário" - }, - "linkModeDescription": "Qualquer pessoa com este link pode participar, sem necessidade de criar uma conta.", - "userModeDescription": "Compartilhe com um usuário específico que já tenha acesso a este host. Caso ele ainda não tenha acesso, compartilhe o host com ele primeiro ou use um link. Após o compartilhamento, a sessão aparecerá na aba Conexões do usuário.", - "permissionLevel": { - "label": "Nível de permissão", - "readOnly": "Somente leitura", - "readOnlyDescription": "É possível assistir à sessão ao vivo, mas não é possível digitar ou interagir.", - "readWrite": "Ler e escrever", - "readWriteDescription": "Pode digitar e interagir com a sessão da mesma forma que o proprietário." - }, - "expiryLabel": "Expiração do link", - "createLinkButton": "Criar link", - "createShareButton": "Compartilhar com o usuário", - "searchUsersPlaceholder": "Pesquisar usuários...", - "noUsersFound": "Nenhum usuário encontrado", - "linkCreated": "Compartilhar link criado", - "linkNeedsServer": "Os links de compartilhamento funcionam apenas para sessões em um servidor Termix. Conecte este host através do seu servidor vinculado para compartilhá-lo com um link.", - "linkCopied": "Link copiado para a área de transferência", - "copyLink": "Copiar link", - "shareCreated": "Sessão compartilhada. Ela aparecerá na aba Conexões deles.", - "shareFailed": "Falha ao criar compartilhamento", - "userLacksHostAccess": "Esse usuário ainda não tem acesso a este host. Compartilhe o host com ele primeiro ou use um link.", - "activeShares": "Ações ativas", - "noActiveShares": "Nenhuma ação ativa nesta sessão.", - "revoke": "Revogar", - "revokeConfirmTitle": "Revogar essa participação?", - "revokeConfirmDescription": "Qualquer pessoa que usar este compartilhamento perderá o acesso imediatamente.", - "revoked": "Ações revogadas", - "revokeFailed": "Falha ao revogar a participação", - "joinCount": "{{count}} junte-se", - "joinCount_other": "{{count}} entra", - "expiresAt": "Expira em {{date}}", - "linkShareBadge": "Link", - "userShareBadge": "Usuário: {{username}}", - "loadSharesFailed": "Falha ao carregar compartilhamentos ativos", - "remoteDisplayUnavailable": "O acesso remoto à área de trabalho não está disponível no momento.", - "sharedWithMe": "Sessões compartilhadas comigo", - "customHoursPlaceholder": "Horas restantes até o acesso expirar" - }, - "collab": { - "createRoom": "Quarto novo", - "roomName": "Nome do quarto", - "persistentRoom": "Sala persistente", - "persistentRoomHint": "As salas permanentes permanecem listadas após o término da reunião e podem ser reutilizadas.", - "noRooms": "Ainda não há salas. Crie uma para iniciar uma reunião.", - "openRoom": "Abrir", - "endRoom": "Encerramento da reunião", - "leaveRoom": "Deixar", - "deleteMember": "Remover", - "removeMember": "Remover {{name}}", - "removeMemberTitle": "Remover este membro?", - "removeMemberDescription": "{{name}} perderá o acesso a esta sala imediatamente. Qualquer visualizador SSH ativo será desconectado.", - "invite": "Convidar", - "inviteTitle": "Convide membros", - "members": "Membros", - "membersPanel": "Membros da sala e solicitações de controle", - "membersWithCount": "Membros ({{count}})", - "guestsWithCount": "Convidados ({{count}})", - "removeGuests": "Remover convidados", - "anonymousGuest": "Convidado", - "online": "online", - "offline": "offline", - "you": "(você)", - "hostBadge": "Hospedar", - "presenterBadge": "Apresentando", - "present": "Presente", - "presentTitle": "Suba ao palco", - "presentHost": "Hospedar", - "searchHosts": "Pesquisar anfitriões", - "noHostsFound": "Nenhum host disponível encontrado.", - "presentProtocol": "Protocolo", - "stopPresenting": "Pare de apresentar", - "takeOver": "Assumir", - "takeOverConfirmTitle": "Substituir a apresentação atual?", - "takeOverDescription": "{{name}} está apresentando agora. Assumir o controle desconectará todos desse palco.", - "emptyStage": "Ninguém está apresentando. Suba ao palco para compartilhar uma sessão.", - "stageLoading": "Conectando-se ao palco...", - "stageEnded": "A apresentação terminou.", - "roomEnded": "Esta reunião terminou.", - "presenterLabel": "{{name}} está apresentando", - "guestLink": "Link para convidado", - "guestLinkOn": "O link para convidados está ativado. Qualquer pessoa com o link pode assistir à apresentação.", - "guestLinkOff": "O link para visitantes está desativado.", - "copyLink": "Copiar link", - "rotateLink": "Rotacionar link", - "disableLink": "Desativar link", - "rotateLinkConfirmTitle": "Girar o link do convidado?", - "rotateLinkDescription": "A conexão atual deixará de funcionar imediatamente. Os dispositivos convidados SSH conectados serão desconectados.", - "disableLinkConfirmTitle": "Desativar o link de convidado?", - "disableLinkDescription": "Os visualizadores SSH anônimos serão desconectados imediatamente e a conexão atual deixará de funcionar.", - "linkCopied": "Link copiado", - "roles": "Funções", - "users": "Usuários", - "searchUsers": "Usuários da pesquisa", - "endConfirmTitle": "Encerrar esta reunião?", - "endDescription": "{{name}} será encerrado para todos e não poderá ser reaberto.", - "endPersistentDescription": "A apresentação ativa em {{name}} será interrompida. A sala permanece disponível para reutilização.", - "deleteRoom": "Excluir sala", - "deleteConfirmTitle": "Apagar esta sala?", - "deleteDescription": "{{name}} será excluído permanentemente para todos e não poderá ser desfeito.", - "invitedTo": "Você foi convidado para \"{{name}}\"", - "guest": { - "title": "Reunião", - "waiting": "Aguardando o apresentador...", - "linkInvalid": "Este link de convidado é inválido ou a reunião já terminou." - }, - "reopenFromPanel": "Esta aba da reunião expirou. Reabra a sala no painel Reuniões.", - "retry": "Tentar novamente", - "requestControl": "Controle de solicitação", - "cancelControlRequest": "Cancelar solicitação", - "controlQueue": "Solicitações de controle", - "noControlRequests": "Não há solicitações pendentes.", - "grantControlTo": "Dê o controle para {{name}}", - "dismissControlRequest": "Rejeitar solicitação de controle de {{name}}", - "releaseControl": "Liberar controle", - "grantControl": "Dê o controle", - "revokeControl": "Retome o controle", - "controlRequestedBy": "{{name}} solicitou o controle", - "grant": "Conceder", - "controlBadge": "No controle", - "youArePresenting": "Você está apresentando em outra aba. Pare de apresentar para passar a palavra.", - "created": "Quarto criado", - "invited": "Membros convidados", - "roomTab": "Reunião", - "serverChanged": "O servidor da reunião foi alterado. Reabra esta reunião no painel Reuniões.", - "hostNotSynced": "Sincronize este host com o servidor de reuniões antes de apresentar.", - "linkRequiresServer": "Conecte este computador a um servidor Termix e crie a reunião lá para compartilhar um link da web." - }, - "nav": { - "collab": "Reuniões" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Permitir compartilhamento de sessão", - "description": "Permite que sessões de terminal ao vivo, RDP, VNC e Telnet sejam compartilhadas em toda a instância. Substitui todas as configurações de compartilhamento por host quando desativada." - } - }, - "host": { - "allowSessionSharing": { - "label": "Permitir compartilhamento de sessão", - "description": "Permitir que as sessões ao vivo neste servidor sejam compartilhadas por meio de link ou com outros usuários." - } - } - }, - "permissions": { - "use": { - "title": "Participe de sessões e entre em salas.", - "description": "Compartilhe sessões ao vivo por link ou com outros usuários, e crie e participe de salas de colaboração." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Nunca", - "oneHour": "1 hora", - "oneDay": "24 horas", - "sevenDays": "7 dias", - "thirtyDays": "30 dias", - "custom": "Personalizado" - } - } - }, - "connections": { - "sectionSharedWithMe": "Compartilhou comigo", - "sharedBy": "Compartilhado por {{username}}", - "join": "Juntar", - "sharedSessionLabel": "{{hostName}} (compartilhado)" - }, - "plugin": { - "name": "Compartilhamento de sessão" - } -} diff --git a/plugins/session-sharing/locales/translated/pt_PT.json b/plugins/session-sharing/locales/translated/pt_PT.json deleted file mode 100644 index 4d008b8d2..000000000 --- a/plugins/session-sharing/locales/translated/pt_PT.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Ligar à sessão partilhada...", - "linkInvalid": "Este link de partilha é inválido, expirou ou foi revogado.", - "rateLimited": "Muitas tentativas foram feitas, tente novamente em breve.", - "sessionEnded": "Esta sessão terminou.", - "readOnlyBadge": "Somente visualização", - "ownerLabel": "Proprietário do anfitrião" - }, - "modalTitle": "Sessão de partilha", - "shareButton": "Partilhar", - "notReadyToShare": "A sessão ainda não está pronta para ser partilhada.", - "modeTab": { - "link": "Ligação", - "user": "Utilizador" - }, - "linkModeDescription": "Qualquer pessoa com este link pode participar, sem necessidade de criar conta.", - "userModeDescription": "Partilhe com um utilizador específico que já tenha acesso a este host. Caso ainda não tenha acesso, partilhe primeiro o host com ele ou utilize um link. Após a partilha, a sessão aparecerá no separador Conexões dele.", - "permissionLevel": { - "label": "Nível de permissão", - "readOnly": "Apenas leitura", - "readOnlyDescription": "É possível assistir à sessão em direto, mas não é possível digitar ou interagir.", - "readWrite": "Ler e escrever", - "readWriteDescription": "Pode digitar e interagir com a sessão da mesma forma que o proprietário." - }, - "expiryLabel": "Expiração do link", - "createLinkButton": "Criar link", - "createShareButton": "Partilhar com o usuário", - "searchUsersPlaceholder": "Pesquisar utilizadores...", - "noUsersFound": "Nenhum utilizador encontrado", - "linkCreated": "Partilhar link criado", - "linkNeedsServer": "Os links de partilha funcionam apenas para sessões num servidor Termix. Ligue este host através do seu servidor ligado para o partilhar com um link.", - "linkCopied": "Ligação copiada para a área de transferência", - "copyLink": "Copiar link", - "shareCreated": "Sessão partilhada. Ela aparecerá no separador Conexões deles.", - "shareFailed": "Falha ao criar partilha", - "userLacksHostAccess": "Esse utilizador ainda não tem acesso a este host. Partilhe o host com ele primeiro ou use um link.", - "activeShares": "Ações ativas", - "noActiveShares": "Sem ação ativa nesta sessão.", - "revoke": "Revogar", - "revokeConfirmTitle": "Revogar essa participação?", - "revokeConfirmDescription": "Qualquer pessoa que utilize esta partilha perderá o acesso imediatamente.", - "revoked": "Ações revogadas", - "revokeFailed": "Falha ao revogar a participação", - "joinCount": "{{count}} junte-se", - "joinCount_other": "{{count}} entra", - "expiresAt": "Expira a {{date}}", - "linkShareBadge": "Ligação", - "userShareBadge": "Utilizador: {{username}}", - "loadSharesFailed": "Falha ao carregar partilhas ativas", - "remoteDisplayUnavailable": "O acesso remoto à área de trabalho não está disponível de momento.", - "sharedWithMe": "Sessões partilhadas comigo", - "customHoursPlaceholder": "Horas até à expiração do acesso" - }, - "collab": { - "createRoom": "Quarto novo", - "roomName": "Nome do quarto", - "persistentRoom": "Sala persistente", - "persistentRoomHint": "As salas permanentes permanecem listadas após o término da reunião e podem ser reutilizadas.", - "noRooms": "Ainda não há salas. Crie uma para iniciar uma reunião.", - "openRoom": "Abrir", - "endRoom": "Encerramento da reunião", - "leaveRoom": "Ausência", - "deleteMember": "Remover", - "removeMember": "Remover {{name}}", - "removeMemberTitle": "Remover este membro?", - "removeMemberDescription": "{{name}} perderá o acesso a esta sala imediatamente. Qualquer visualizador SSH ativo será desligado.", - "invite": "Convidar", - "inviteTitle": "Convide os membros", - "members": "Membros", - "membersPanel": "Membros da sala e pedidos de controlo", - "membersWithCount": "Membros ({{count}})", - "guestsWithCount": "Convidados ({{count}})", - "removeGuests": "Remover convidados", - "anonymousGuest": "Convidado", - "online": "online", - "offline": "offline", - "you": "(tu)", - "hostBadge": "Servidor", - "presenterBadge": "Apresentando", - "present": "Presente", - "presentTitle": "Suba ao palco", - "presentHost": "Servidor", - "searchHosts": "Pesquisar hosts", - "noHostsFound": "Nenhum host disponível encontrado.", - "presentProtocol": "Protocolo", - "stopPresenting": "Pare de apresentar", - "takeOver": "Assumir", - "takeOverConfirmTitle": "Substituir a apresentação atual?", - "takeOverDescription": "{{name}} está a apresentar agora. Assumir o controlo desconectará todos desse palco.", - "emptyStage": "Ninguém está a apresentar. Suba ao palco para partilhar uma sessão.", - "stageLoading": "Ligar-se ao palco...", - "stageEnded": "A apresentação terminou.", - "roomEnded": "Esta reunião terminou.", - "presenterLabel": "{{name}} está a apresentar", - "guestLink": "Link para convidado", - "guestLinkOn": "O link para convidados está ativado. Qualquer pessoa com o link pode assistir à apresentação.", - "guestLinkOff": "O link para os visitantes está desativado.", - "copyLink": "Copiar link", - "rotateLink": "Rotacionar ligação", - "disableLink": "Desativar ligação", - "rotateLinkConfirmTitle": "Rodar o link do convidado?", - "rotateLinkDescription": "A ligação atual deixará de funcionar imediatamente. Os dispositivos convidados SSH ligados serão desligados.", - "disableLinkConfirmTitle": "Desativar o link de convidado?", - "disableLinkDescription": "Os visualizadores SSH anónimos serão imediatamente desligados e a ligação atual deixará de funcionar.", - "linkCopied": "Ligação copiada", - "roles": "Funções", - "users": "Utilizadores", - "searchUsers": "Os utilizadores da pesquisa", - "endConfirmTitle": "Encerrar esta reunião?", - "endDescription": "{{name}} será encerrado para todos e não poderá ser reaberto.", - "endPersistentDescription": "A apresentação ativa em {{name}} será interrompida. A sala continua disponível para reutilização.", - "deleteRoom": "Apagar sala", - "deleteConfirmTitle": "Apagar esta sala?", - "deleteDescription": "{{name}} será eliminado permanentemente para todos e não poderá ser anulado.", - "invitedTo": "Foi convidado para \"{{name}}\"", - "guest": { - "title": "Reunião", - "waiting": "À espera do apresentador...", - "linkInvalid": "Este link de convidado é inválido ou a reunião já terminou." - }, - "reopenFromPanel": "Esta aba da reunião expirou. Volte a abrir a sala no painel Reuniões.", - "retry": "Repetir", - "requestControl": "Controle de solicitação", - "cancelControlRequest": "Cancelar pedido", - "controlQueue": "Solicitações de controlo", - "noControlRequests": "Não há pedidos pendentes.", - "grantControlTo": "Dê o controlo a {{name}}", - "dismissControlRequest": "Rejeitar pedido de controlo de {{name}}", - "releaseControl": "Libertar controle", - "grantControl": "Dê o controlo", - "revokeControl": "Retome o comando", - "controlRequestedBy": "{{name}} solicitou o controlo", - "grant": "Subvenção", - "controlBadge": "No controlo", - "youArePresenting": "Está a apresentar em outra aba. Pare de apresentar para passar a palavra.", - "created": "Quarto criado", - "invited": "Membros convidados", - "roomTab": "Reunião", - "serverChanged": "O servidor da reunião foi alterado. Reabra esta reunião no painel Reuniões.", - "hostNotSynced": "Sincronize este host com o servidor de reuniões antes de apresentar.", - "linkRequiresServer": "Ligue este computador a um servidor Termix e crie aí a reunião para partilhar um link web." - }, - "nav": { - "collab": "Reuniões" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Permitir partilha de sessão", - "description": "Permite que as sessões de terminal ao vivo, RDP, VNC e Telnet sejam partilhadas em toda a instância. Substitui todas as definições de partilha por host quando desativada." - } - }, - "host": { - "allowSessionSharing": { - "label": "Permitir partilha de sessão", - "description": "Permitir que as sessões em direto neste servidor sejam partilhadas através de link ou com outros utilizadores." - } - } - }, - "permissions": { - "use": { - "title": "Participe em sessões e entre em salas.", - "description": "Partilhe sessões ao vivo por link ou com outros utilizadores, e crie e participe em salas de colaboração." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Nunca", - "oneHour": "1 hora", - "oneDay": "24 horas", - "sevenDays": "7 dias", - "thirtyDays": "30 dias", - "custom": "Personalizado" - } - } - }, - "connections": { - "sectionSharedWithMe": "Partilhou comigo", - "sharedBy": "Partilhado por {{username}}", - "join": "Juntar", - "sharedSessionLabel": "{{hostName}} (partilhado)" - }, - "plugin": { - "name": "Partilha de sessão" - } -} diff --git a/plugins/session-sharing/locales/translated/ro_RO.json b/plugins/session-sharing/locales/translated/ro_RO.json deleted file mode 100644 index a584603eb..000000000 --- a/plugins/session-sharing/locales/translated/ro_RO.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Conectare la sesiunea partajată...", - "linkInvalid": "Acest link de partajare este invalid, a expirat sau a fost revocat.", - "rateLimited": "Prea multe încercări, vă rugăm să încercați din nou în scurt timp", - "sessionEnded": "Această sesiune s-a încheiat", - "readOnlyBadge": "Numai vizualizare", - "ownerLabel": "Proprietarul gazdei" - }, - "modalTitle": "Partajați sesiunea", - "shareButton": "Distribuie", - "notReadyToShare": "Sesiunea nu este încă gata de partajare", - "modeTab": { - "link": "Legătură", - "user": "Utilizator" - }, - "linkModeDescription": "Oricine are acest link se poate alătura, nu este necesar un cont.", - "userModeDescription": "Partajați cu un anumit utilizator care are deja acces la această gazdă. Dacă nu are încă acces, partajați mai întâi gazda cu acesta sau utilizați un link în schimb. După partajare, sesiunea apare în fila Conexiuni.", - "permissionLevel": { - "label": "Nivel de permisiune", - "readOnly": "Doar citire", - "readOnlyDescription": "Pot urmări sesiunea în direct, dar nu pot tasta sau interacționa.", - "readWrite": "Citire-scriere", - "readWriteDescription": "Poate tasta și interacționa cu sesiunea la fel ca proprietarul." - }, - "expiryLabel": "Expirarea linkului", - "createLinkButton": "Creați un link", - "createShareButton": "Partajează cu utilizatorul", - "searchUsersPlaceholder": "Căutați utilizatori...", - "noUsersFound": "Nu au fost găsiți utilizatori", - "linkCreated": "Link de partajare creat", - "linkNeedsServer": "Linkurile de partajare funcționează doar pentru sesiunile de pe un server Termix. Conectați această gazdă prin serverul conectat pentru a o partaja printr-un link.", - "linkCopied": "Linkul a fost copiat în clipboard", - "copyLink": "Copiază linkul", - "shareCreated": "Sesiune partajată. Va apărea în fila Conexiuni.", - "shareFailed": "Nu s-a putut crea partajarea", - "userLacksHostAccess": "Utilizatorul respectiv nu are încă acces la această gazdă. Partajați mai întâi gazda cu acesta sau folosiți un link în schimb.", - "activeShares": "Acțiuni active", - "noActiveShares": "Nicio partajare activă pentru această sesiune", - "revoke": "Revoca", - "revokeConfirmTitle": "Revocați această partajare?", - "revokeConfirmDescription": "Oricine folosește această partajare va pierde accesul imediat.", - "revoked": "Acțiune revocată", - "revokeFailed": "Nu s-a putut revoca partajarea", - "joinCount": "{{count}} alătură-te", - "joinCount_other": "{{count}} se alătură", - "expiresAt": "Expiră la {{date}}", - "linkShareBadge": "Legătură", - "userShareBadge": "Utilizator: {{username}}", - "loadSharesFailed": "Nu s-au putut încărca partajările active", - "remoteDisplayUnavailable": "Desktopul la distanță nu este disponibil momentan.", - "sharedWithMe": "Sesiuni partajate cu mine", - "customHoursPlaceholder": "Ore până la expirarea accesului" - }, - "collab": { - "createRoom": "Cameră nouă", - "roomName": "Numele camerei", - "persistentRoom": "Cameră persistentă", - "persistentRoomHint": "Sălile persistente rămân listate după încheierea întâlnirii și pot fi reutilizate.", - "noRooms": "Încă nu există săli. Creează una pentru a începe o întâlnire.", - "openRoom": "Deschide", - "endRoom": "Încheiați întâlnirea", - "leaveRoom": "Pleacă", - "deleteMember": "Elimina", - "removeMember": "Eliminați {{name}}", - "removeMemberTitle": "Eliminați acest membru?", - "removeMemberDescription": "{{name}} va pierde imediat accesul la această cameră. Orice vizualizator SSH activ va fi deconectat.", - "invite": "Invita", - "inviteTitle": "Invită membri", - "members": "Membri", - "membersPanel": "Membrii camerei și solicitările de control", - "membersWithCount": "Membri ({{count}})", - "guestsWithCount": "Oaspeți ({{count}})", - "removeGuests": "Eliminați invitații", - "anonymousGuest": "Oaspete", - "online": "online", - "offline": "offline", - "you": "(tu)", - "hostBadge": "Gazdă", - "presenterBadge": "Prezentare", - "present": "Prezent", - "presentTitle": "Urcă pe scenă", - "presentHost": "Gazdă", - "searchHosts": "Căutați gazde", - "noHostsFound": "Nu au fost găsite gazde disponibile.", - "presentProtocol": "Protocol", - "stopPresenting": "Opriți prezentarea", - "takeOver": "Prelua", - "takeOverConfirmTitle": "Înlocuiți prezentarea curentă?", - "takeOverDescription": "{{name}} prezintă acum. Preluarea controlului îi va deconecta pe toți de la acea etapă.", - "emptyStage": "Nimeni nu prezintă. Urcă pe scenă pentru a împărtăși o sesiune.", - "stageLoading": "Conectarea la scenă...", - "stageEnded": "Prezentarea s-a încheiat", - "roomEnded": "Această întâlnire s-a încheiat", - "presenterLabel": "{{name}} prezintă", - "guestLink": "Link pentru invitați", - "guestLinkOn": "Linkul pentru invitați este activat. Oricine are linkul poate viziona scena.", - "guestLinkOff": "Linkul pentru invitați este dezactivat.", - "copyLink": "Copiază linkul", - "rotateLink": "Rotiți linkul", - "disableLink": "Dezactivați linkul", - "rotateLinkConfirmTitle": "Rotește linkul pentru oaspeți?", - "rotateLinkDescription": "Legătura curentă va înceta să funcționeze imediat. Oaspeții SSH conectați vor fi deconectați.", - "disableLinkConfirmTitle": "Dezactivați linkul pentru invitați?", - "disableLinkDescription": "Vizualizatorii SSH anonimi vor fi deconectați imediat, iar legătura curentă va înceta să funcționeze.", - "linkCopied": "Linkul a fost copiat", - "roles": "Roluri", - "users": "Utilizatori", - "searchUsers": "Căutați utilizatori", - "endConfirmTitle": "Încheiați această întâlnire?", - "endDescription": "{{name}} se va încheia pentru toată lumea și nu va putea fi redeschis.", - "endPersistentDescription": "Prezentarea activă din {{name}} se va opri. Sala rămâne disponibilă pentru reutilizare.", - "deleteRoom": "Ștergeți camera", - "deleteConfirmTitle": "Ștergeți această cameră?", - "deleteDescription": "{{name}} va fi șters definitiv pentru toată lumea și nu poate fi anulat.", - "invitedTo": "Ai fost invitat la „{{name}}”", - "guest": { - "title": "Întâlnire", - "waiting": "În așteptarea prezentatorului...", - "linkInvalid": "Acest link de invitat este invalid sau întâlnirea s-a încheiat." - }, - "reopenFromPanel": "Această filă de întâlnire a expirat. Redeschideți sala din panoul Întâlniri.", - "retry": "Reîncercați", - "requestControl": "Cerere de control", - "cancelControlRequest": "Anulează cererea", - "controlQueue": "Cereri de control", - "noControlRequests": "Nicio solicitare în așteptare.", - "grantControlTo": "Acordă controlul către {{name}}", - "dismissControlRequest": "Închideți solicitarea de control a utilizatorului {{name}}", - "releaseControl": "Controlul eliberării", - "grantControl": "Oferă controlul", - "revokeControl": "Preia controlul", - "controlRequestedBy": "{{name}} a cerut controlul", - "grant": "Acorda", - "controlBadge": "În control", - "youArePresenting": "Prezentați într-o altă filă. Oprește prezentarea pentru a preda scena.", - "created": "Cameră creată", - "invited": "Membrii invitați", - "roomTab": "Întâlnire", - "serverChanged": "Serverul de întâlnire s-a modificat. Redeschideți această întâlnire din panoul Întâlniri.", - "hostNotSynced": "Sincronizează această gazdă cu serverul de întâlnire înainte de prezentare.", - "linkRequiresServer": "Conectează acest desktop la un server Termix și creează întâlnirea acolo pentru a partaja un link web." - }, - "nav": { - "collab": "Întâlniri" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Permiteți partajarea sesiunii", - "description": "Permite partajarea sesiunilor live de terminal, RDP, VNC și Telnet la nivel de instanță. Suprascrie fiecare comutare de partajare per gazdă atunci când este dezactivată." - } - }, - "host": { - "allowSessionSharing": { - "label": "Permiteți partajarea sesiunii", - "description": "Permiteți partajarea sesiunilor live de pe această gazdă prin link sau cu alți utilizatori" - } - } - }, - "permissions": { - "use": { - "title": "Partajați sesiuni și alăturați-vă sălilor", - "description": "Partajați sesiuni live prin link sau cu alți utilizatori și creați și alăturați-vă sălilor de colaborare." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Nu", - "oneHour": "1 oră", - "oneDay": "24 de ore", - "sevenDays": "7 zile", - "thirtyDays": "30 de zile", - "custom": "Personalizat" - } - } - }, - "connections": { - "sectionSharedWithMe": "Distribuit cu mine", - "sharedBy": "Distribuit de {{username}}", - "join": "Alătură-te", - "sharedSessionLabel": "{{hostName}} (partajat)" - }, - "plugin": { - "name": "Partajarea sesiunii" - } -} diff --git a/plugins/session-sharing/locales/translated/ru_RU.json b/plugins/session-sharing/locales/translated/ru_RU.json deleted file mode 100644 index a2abb03f7..000000000 --- a/plugins/session-sharing/locales/translated/ru_RU.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Подключение к общей сессии...", - "linkInvalid": "Данная ссылка для общего доступа недействительна, истек срок ее действия или она была аннулирована.", - "rateLimited": "Слишком много попыток, пожалуйста, попробуйте еще раз чуть позже.", - "sessionEnded": "Данная сессия завершена.", - "readOnlyBadge": "Только для просмотра", - "ownerLabel": "Владелец хостинга" - }, - "modalTitle": "Совместное использование сессии", - "shareButton": "Поделиться", - "notReadyToShare": "Сессия пока не готова к публикации.", - "modeTab": { - "link": "Связь", - "user": "Пользователь" - }, - "linkModeDescription": "Присоединиться может любой, у кого есть эта ссылка, регистрация не требуется.", - "userModeDescription": "Предоставьте доступ конкретному пользователю, у которого уже есть доступ к этому хосту. Если у него еще нет доступа, сначала предоставьте ему доступ к хосту или используйте ссылку. После предоставления доступа сессия отобразится на вкладке «Подключения» этого пользователя.", - "permissionLevel": { - "label": "Уровень доступа", - "readOnly": "Только чтение", - "readOnlyDescription": "Можно наблюдать за сессией в прямом эфире, но нельзя печатать или взаимодействовать с ней.", - "readWrite": "Чтение-запись", - "readWriteDescription": "Может печатать и взаимодействовать с сессией так же, как и её владелец." - }, - "expiryLabel": "Срок действия ссылки истекает", - "createLinkButton": "Создать ссылку", - "createShareButton": "Поделиться с пользователем", - "searchUsersPlaceholder": "Поиск пользователей...", - "noUsersFound": "Пользователи не найдены", - "linkCreated": "Ссылка для обмена создана", - "linkNeedsServer": "Ссылки для общего доступа работают только для сессий на сервере Termix. Подключите этот хост через свой связанный сервер, чтобы поделиться им с помощью ссылки.", - "linkCopied": "Ссылка скопирована в буфер обмена", - "copyLink": "Скопировать ссылку", - "shareCreated": "Сессия была проведена совместно. Она отобразится на вкладке «Подключения».", - "shareFailed": "Не удалось создать общий ресурс.", - "userLacksHostAccess": "У этого пользователя пока нет доступа к данному хосту. Сначала предоставьте ему доступ к хосту или используйте ссылку.", - "activeShares": "Активные акции", - "noActiveShares": "В текущей торговой сессии нет активных акций.", - "revoke": "Отозвать", - "revokeConfirmTitle": "Отозвать эту публикацию?", - "revokeConfirmDescription": "Любой, кто воспользуется этой ссылкой, немедленно потеряет к ней доступ.", - "revoked": "Акция аннулирована", - "revokeFailed": "Не удалось отозвать акцию", - "joinCount": "{{count}} присоединиться", - "joinCount_other": "{{count}} присоединяется", - "expiresAt": "Истекает {{date}}", - "linkShareBadge": "Связь", - "userShareBadge": "Пользователь: {{username}}", - "loadSharesFailed": "Не удалось загрузить активные акции.", - "remoteDisplayUnavailable": "В настоящий момент функция удалённого доступа к рабочему столу недоступна.", - "sharedWithMe": "Сессии, которыми со мной поделились", - "customHoursPlaceholder": "Часов до окончания срока доступа" - }, - "collab": { - "createRoom": "Новая комната", - "roomName": "Название комнаты", - "persistentRoom": "Постоянное помещение", - "persistentRoomHint": "Список свободных комнат сохраняется и после окончания встречи, и их можно использовать повторно.", - "noRooms": "Комнат пока нет. Создайте комнату, чтобы начать встречу.", - "openRoom": "Открыть", - "endRoom": "Завершение встречи", - "leaveRoom": "Оставлять", - "deleteMember": "Удалить", - "removeMember": "Удалить {{name}}", - "removeMemberTitle": "Удалить этого участника?", - "removeMemberDescription": "{{name}} немедленно потеряет доступ к этой комнате. Любой активный SSH-просмотрщик будет отключен.", - "invite": "Приглашать", - "inviteTitle": "Пригласить участников", - "members": "Члены", - "membersPanel": "Участники комнаты и запросы на управление", - "membersWithCount": "Участники ({{count}})", - "guestsWithCount": "Гости ({{count}})", - "removeGuests": "Удалить гостей", - "anonymousGuest": "Гость", - "online": "онлайн", - "offline": "офлайн", - "you": "(ты)", - "hostBadge": "Хост", - "presenterBadge": "Презентация", - "present": "Подарок", - "presentTitle": "Выходите на сцену", - "presentHost": "Хост", - "searchHosts": "Поиск хостов", - "noHostsFound": "Доступных хостов не найдено.", - "presentProtocol": "Протокол", - "stopPresenting": "Прекратите презентацию", - "takeOver": "Перенимать", - "takeOverConfirmTitle": "Заменить текущую презентацию?", - "takeOverDescription": "{{name}} сейчас выступает. Перехват инициативы отключит всех от этой сцены.", - "emptyStage": "Никто не выступает с презентацией. Выходите на сцену, чтобы поделиться своим опытом.", - "stageLoading": "Подключение к сцене...", - "stageEnded": "Презентация завершилась.", - "roomEnded": "Встреча завершена.", - "presenterLabel": "{{name}} представляет", - "guestLink": "Гостевая ссылка", - "guestLinkOn": "Гостевая ссылка включена. Любой, у кого есть ссылка, может посмотреть выступление.", - "guestLinkOff": "Гостевая ссылка отключена.", - "copyLink": "Скопировать ссылку", - "rotateLink": "Повернуть ссылку", - "disableLink": "Отключить ссылку", - "rotateLinkConfirmTitle": "Повернуть гостевую ссылку?", - "rotateLinkDescription": "Текущее соединение немедленно перестанет работать. Подключенные через SSH гостевые системы будут отключены.", - "disableLinkConfirmTitle": "Отключить гостевую ссылку?", - "disableLinkDescription": "Анонимные пользователи SSH-сервера будут немедленно отключены, и текущая связь перестанет работать.", - "linkCopied": "Ссылка скопирована", - "roles": "Роли", - "users": "Пользователи", - "searchUsers": "Поиск пользователей", - "endConfirmTitle": "Закончить это совещание?", - "endDescription": "{{name}} завершится для всех и не может быть возобновлено.", - "endPersistentDescription": "Активная презентация в {{name}} будет остановлена. Комната остается доступной для повторного использования.", - "deleteRoom": "Удалить комнату", - "deleteConfirmTitle": "Удалить эту комнату?", - "deleteDescription": "{{name}} будет безвозвратно удалено для всех и не может быть отменено.", - "invitedTo": "Вас пригласили на \"{{name}}\"", - "guest": { - "title": "Встреча", - "waiting": "Ожидание выступления докладчика...", - "linkInvalid": "Эта гостевая ссылка недействительна, или встреча завершилась." - }, - "reopenFromPanel": "Срок действия этой вкладки «Встречи» истек. Откройте комнату заново на панели «Встречи».", - "retry": "Повторить", - "requestControl": "Управление запросами", - "cancelControlRequest": "Отменить запрос", - "controlQueue": "Запросы на управление", - "noControlRequests": "Нет ожидающих обработки запросов.", - "grantControlTo": "Передайте управление {{name}}", - "dismissControlRequest": "Отклонить запрос на управление от {{name}}", - "releaseControl": "Управление освобождением", - "grantControl": "Передайте контроль", - "revokeControl": "Верните себе контроль.", - "controlRequestedBy": "{{name}} запросил управление", - "grant": "Грант", - "controlBadge": "Под контролем", - "youArePresenting": "Вы ведёте презентацию в другой вкладке. Прекратите презентацию, чтобы передать слово другому пользователю.", - "created": "Комната создана", - "invited": "Приглашены члены", - "roomTab": "Встреча", - "serverChanged": "Сервер совещания изменился. Откройте это совещание заново из панели «Совещания».", - "hostNotSynced": "Перед началом презентации синхронизируйте этого организатора с сервером совещания.", - "linkRequiresServer": "Подключите этот компьютер к серверу Termix и создайте там собрание, чтобы поделиться веб-ссылкой." - }, - "nav": { - "collab": "Встречи" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Разрешить совместное использование сеанса", - "description": "Разрешить общий доступ к сеансам Live Terminal, RDP, VNC и Telnet для всего экземпляра. При отключении этой функции отменяет все настройки общего доступа для отдельных хостов." - } - }, - "host": { - "allowSessionSharing": { - "label": "Разрешить совместное использование сеанса", - "description": "Разрешите делиться записями онлайн-сессий на этом хостинге по ссылке или с другими пользователями." - } - } - }, - "permissions": { - "use": { - "title": "Делитесь сессиями и присоединяйтесь к комнатам.", - "description": "Делитесь записями онлайн-сессий по ссылке или с другими пользователями, а также создавайте и присоединяйтесь к комнатам для совместной работы." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Никогда", - "oneHour": "1 час", - "oneDay": "24 часа", - "sevenDays": "7 дней", - "thirtyDays": "30 дней", - "custom": "Настроить" - } - } - }, - "connections": { - "sectionSharedWithMe": "Поделился со мной", - "sharedBy": "Поделился {{username}}", - "join": "Присоединиться", - "sharedSessionLabel": "{{hostName}} (совместно)" - }, - "plugin": { - "name": "Совместное использование сессии" - } -} diff --git a/plugins/session-sharing/locales/translated/sr_SP.json b/plugins/session-sharing/locales/translated/sr_SP.json deleted file mode 100644 index 19b530bb9..000000000 --- a/plugins/session-sharing/locales/translated/sr_SP.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Повезивање са дељеном сесијом...", - "linkInvalid": "Овај линк за дељење је неважећи, истекао је или је опозван", - "rateLimited": "Превише покушаја, покушајте поново ускоро", - "sessionEnded": "Ова сесија је завршена", - "readOnlyBadge": "Само преглед", - "ownerLabel": "Власник домаћина" - }, - "modalTitle": "Дели сесију", - "shareButton": "Дели", - "notReadyToShare": "Сесија још није спремна за дељење", - "modeTab": { - "link": "Линк", - "user": "Корисник" - }, - "linkModeDescription": "Свако ко има овај линк може да се придружи, није потребан налог.", - "userModeDescription": "Делите са одређеним корисником који већ има приступ овом хосту. Ако још немају приступ, прво поделите хост са њима или користите линк. Након дељења, сесија се појављује на њиховој картици „Везе“.", - "permissionLevel": { - "label": "Ниво дозволе", - "readOnly": "Само за читање", - "readOnlyDescription": "Може да гледа сесију уживо, али не може да куца или да комуницира.", - "readWrite": "Читање и писање", - "readWriteDescription": "Може да куца и интерагује са сесијом баш као и власник." - }, - "expiryLabel": "Истек линка", - "createLinkButton": "Направи линк", - "createShareButton": "Подели са корисником", - "searchUsersPlaceholder": "Претражи кориснике...", - "noUsersFound": "Није пронађен ниједан корисник", - "linkCreated": "Линк за дељење је направљен", - "linkNeedsServer": "Линкови за дељење раде само за сесије на Termix серверу. Повежите овај хост преко повезаног сервера да бисте га делили помоћу линка.", - "linkCopied": "Линк је копиран у међуспремник", - "copyLink": "Копирај линк", - "shareCreated": "Сесија је дељена. Појавиће се на њиховој картици „Везе“.", - "shareFailed": "Није успело креирање дељења", - "userLacksHostAccess": "Тај корисник још увек нема приступ овом хосту. Прво поделите хост са њим или користите линк.", - "activeShares": "Активне акције", - "noActiveShares": "Нема активних дељења за ову сесију", - "revoke": "Поништи", - "revokeConfirmTitle": "Да ли желите да опозовете ово дељење?", - "revokeConfirmDescription": "Свако ко користи ово дељење ће одмах изгубити приступ.", - "revoked": "Дељење опозвано", - "revokeFailed": "Опозивање дељења није успело", - "joinCount": "{{count}} придружи се", - "joinCount_other": "{{count}} придруживања", - "expiresAt": "Истиче {{date}}", - "linkShareBadge": "Линк", - "userShareBadge": "Корисник: {{username}}", - "loadSharesFailed": "Учитавање активних дељења није успело", - "remoteDisplayUnavailable": "Удаљена радна површина тренутно није доступна.", - "sharedWithMe": "Сесије дељене са мном", - "customHoursPlaceholder": "Сати до истека приступа" - }, - "collab": { - "createRoom": "Нова соба", - "roomName": "Назив собе", - "persistentRoom": "Трајна соба", - "persistentRoomHint": "Трајне собе остају наведене након завршетка састанка и могу се поново користити.", - "noRooms": "Још нема соба. Направите једну да бисте започели састанак.", - "openRoom": "Отворено", - "endRoom": "Заврши састанак", - "leaveRoom": "Одиђи", - "deleteMember": "Уклони", - "removeMember": "Уклони {{name}}", - "removeMemberTitle": "Уклонити овог члана?", - "removeMemberDescription": "{{name}} ће одмах изгубити приступ овој соби. Сваки активни SSH гледалац ће бити искључен.", - "invite": "Позови", - "inviteTitle": "Позовите чланове", - "members": "Чланови", - "membersPanel": "Чланови собе и захтеви за контролу", - "membersWithCount": "Чланови ({{count}})", - "guestsWithCount": "Гости ({{count}})", - "removeGuests": "Уклони госте", - "anonymousGuest": "Гост", - "online": "онлајн", - "offline": "офлајн", - "you": "(ти)", - "hostBadge": "Домаћин", - "presenterBadge": "Презентација", - "present": "Присутан", - "presentTitle": "Изађи на сцену", - "presentHost": "Домаћин", - "searchHosts": "Претрага хостова", - "noHostsFound": "Није пронађен ниједан доступан хост.", - "presentProtocol": "Протокол", - "stopPresenting": "Заустави презентацију", - "takeOver": "Преузми", - "takeOverConfirmTitle": "Заменити тренутну презентацију?", - "takeOverDescription": "{{name}} тренутно презентује. Преузимање ће искључити све са те сцене.", - "emptyStage": "Нико не презентује. Изађите на бину да бисте поделили сесију.", - "stageLoading": "Повезивање са сценом...", - "stageEnded": "Презентација је завршена", - "roomEnded": "Овај састанак је завршен", - "presenterLabel": "{{name}} презентује", - "guestLink": "Гостујући линк", - "guestLinkOn": "Линк за госте је укључен. Свако ко има линк може да гледа сцену.", - "guestLinkOff": "Линк за госте је искључен.", - "copyLink": "Копирај линк", - "rotateLink": "Ротирај линк", - "disableLink": "Онемогући везу", - "rotateLinkConfirmTitle": "Ротирати линк за госте?", - "rotateLinkDescription": "Тренутна веза ће одмах престати да ради. Повезани SSH гости ће бити искључени.", - "disableLinkConfirmTitle": "Онемогућити линк за госте?", - "disableLinkDescription": "Анонимни SSH гледаоци ће одмах бити прекинути везу и тренутна веза ће престати да ради.", - "linkCopied": "Линк је копиран", - "roles": "Улоге", - "users": "Корисници", - "searchUsers": "Претрага корисника", - "endConfirmTitle": "Да ли желите да завршите овај састанак?", - "endDescription": "{{name}} ће бити завршен за све и не може се поново отворити.", - "endPersistentDescription": "Активна презентација у {{name}} ће се зауставити. Просторија остаје доступна за поновну употребу.", - "deleteRoom": "Обриши собу", - "deleteConfirmTitle": "Да ли желите да избришете ову собу?", - "deleteDescription": "{{name}} ће бити трајно обрисан за све и не може се поништити.", - "invitedTo": "Позвани сте у „{{name}}“", - "guest": { - "title": "Састанак", - "waiting": "Чекање водитеља...", - "linkInvalid": "Овај линк за госте је неважећи или је састанак завршен" - }, - "reopenFromPanel": "Ова картица састанка је истекла. Поново отворите просторију из панела Састанци.", - "retry": "Покушај поново", - "requestControl": "Захтевај контролу", - "cancelControlRequest": "Откажи захтев", - "controlQueue": "Захтеви за контролу", - "noControlRequests": "Нема захтева на чекању.", - "grantControlTo": "Дајте контролу кориснику {{name}}", - "dismissControlRequest": "Одбаци захтев за контролу корисника {{name}}", - "releaseControl": "Контрола ослобађања", - "grantControl": "Дајте контролу", - "revokeControl": "Преузмите контролу назад", - "controlRequestedBy": "{{name}} је затражио контролу", - "grant": "Грант", - "controlBadge": "Под контролом", - "youArePresenting": "Презентујете у другој картици. Прекините презентацију да бисте предали сцену.", - "created": "Соба је направљена", - "invited": "Чланови позвани", - "roomTab": "Састанак", - "serverChanged": "Сервер за састанке се променио. Поново отворите овај састанак из панела Састанци.", - "hostNotSynced": "Синхронизујте овог хоста са сервером за састанке пре презентације.", - "linkRequiresServer": "Повежите ову радну површину са Termix сервером и тамо креирајте састанак да бисте делили веб везу." - }, - "nav": { - "collab": "Састанци" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Дозволи дељење сесије", - "description": "Дозвољава дељење сесија уживо терминала, RDP, VNC и Telnet сесија на нивоу целе инстанце. Замењује сваки прекидач за дељење по хосту када је онемогућен." - } - }, - "host": { - "allowSessionSharing": { - "label": "Дозволи дељење сесије", - "description": "Дозволите дељење сесија уживо на овом хосту путем линка или са другим корисницима" - } - } - }, - "permissions": { - "use": { - "title": "Делите сесије и придружите се собама", - "description": "Делите сесије уживо путем линка или са другим корисницима, креирајте собе за сарадњу и придружите им се." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Никад", - "oneHour": "1 сат", - "oneDay": "24 сата", - "sevenDays": "7 дана", - "thirtyDays": "30 дана", - "custom": "Прилагођено" - } - } - }, - "connections": { - "sectionSharedWithMe": "Дељено са мном", - "sharedBy": "Дели {{username}}", - "join": "Придружи се", - "sharedSessionLabel": "{{hostName}} (дељено)" - }, - "plugin": { - "name": "Дељење сесије" - } -} diff --git a/plugins/session-sharing/locales/translated/sv_SE.json b/plugins/session-sharing/locales/translated/sv_SE.json deleted file mode 100644 index 4c2c54508..000000000 --- a/plugins/session-sharing/locales/translated/sv_SE.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Ansluter till delad session...", - "linkInvalid": "Den här delningslänken är ogiltig, har löpt ut eller har återkallats", - "rateLimited": "För många försök, försök igen snart", - "sessionEnded": "Den här sessionen är avslutad", - "readOnlyBadge": "Endast visning", - "ownerLabel": "Värdägare" - }, - "modalTitle": "Dela session", - "shareButton": "Dela", - "notReadyToShare": "Sessionen är inte redo att delas ännu", - "modeTab": { - "link": "Länk", - "user": "Användare" - }, - "linkModeDescription": "Vem som helst med den här länken kan gå med, inget konto krävs.", - "userModeDescription": "Dela med en specifik användare som redan har åtkomst till den här värden. Om de inte har åtkomst ännu, dela värden med dem först eller använd en länk istället. När sessionen har delats visas den på deras flik Anslutningar.", - "permissionLevel": { - "label": "Behörighetsnivå", - "readOnly": "Skrivskyddad", - "readOnlyDescription": "Kan titta på sessionen live men kan inte skriva eller interagera.", - "readWrite": "Läs-skriv", - "readWriteDescription": "Kan skriva och interagera med sessionen precis som ägaren." - }, - "expiryLabel": "Länkens utgångsdatum", - "createLinkButton": "Skapa länk", - "createShareButton": "Dela med användaren", - "searchUsersPlaceholder": "Sök användare...", - "noUsersFound": "Inga användare hittades", - "linkCreated": "Delningslänk skapad", - "linkNeedsServer": "Delningslänkar fungerar bara för sessioner på en Termix-server. Anslut den här värden via din länkade server för att dela den med en länk.", - "linkCopied": "Länk kopierad till urklipp", - "copyLink": "Kopiera länk", - "shareCreated": "Sessionen delade. Den kommer att visas på fliken Anslutningar.", - "shareFailed": "Misslyckades med att skapa delningen", - "userLacksHostAccess": "Den användaren har inte åtkomst till den här värden ännu. Dela värden med dem först, eller använd en länk istället.", - "activeShares": "Aktiva aktier", - "noActiveShares": "Inga aktiva delningar för den här sessionen", - "revoke": "Återkalla", - "revokeConfirmTitle": "Återkalla den här delningen?", - "revokeConfirmDescription": "Alla som använder den här delningen kommer att förlora åtkomsten omedelbart.", - "revoked": "Delning återkallad", - "revokeFailed": "Misslyckades med att återkalla delningen", - "joinCount": "{{count}} gå med", - "joinCount_other": "{{count}} ansluter sig", - "expiresAt": "Utgångsdatum {{date}}", - "linkShareBadge": "Länk", - "userShareBadge": "Användare: {{username}}", - "loadSharesFailed": "Misslyckades med att läsa in aktiva delningar", - "remoteDisplayUnavailable": "Fjärrskrivbord är inte tillgängligt just nu.", - "sharedWithMe": "Sessioner delades med mig", - "customHoursPlaceholder": "Timmar tills åtkomsten upphör" - }, - "collab": { - "createRoom": "Nytt rum", - "roomName": "Rumsnamn", - "persistentRoom": "Permanent rum", - "persistentRoomHint": "Beständiga rum finns kvar i listan efter att mötet är slut och kan återanvändas.", - "noRooms": "Inga rum ännu. Skapa ett för att starta ett möte.", - "openRoom": "Öppna", - "endRoom": "Avsluta mötet", - "leaveRoom": "Lämna", - "deleteMember": "Ta bort", - "removeMember": "Ta bort {{name}}", - "removeMemberTitle": "Ta bort den här medlemmen?", - "removeMemberDescription": "{{name}} kommer att förlora åtkomst till det här rummet omedelbart. Alla aktiva SSH-läsare kommer att kopplas bort.", - "invite": "Bjuda", - "inviteTitle": "Bjud in medlemmar", - "members": "Medlemmar", - "membersPanel": "Rumsmedlemmar och kontrollförfrågningar", - "membersWithCount": "Medlemmar ({{count}})", - "guestsWithCount": "Gäster ({{count}})", - "removeGuests": "Ta bort gäster", - "anonymousGuest": "Gäst", - "online": "online", - "offline": "off-line", - "you": "(du)", - "hostBadge": "Värd", - "presenterBadge": "Presenterar", - "present": "Presentera", - "presentTitle": "Inta scenen", - "presentHost": "Värd", - "searchHosts": "Sök värdar", - "noHostsFound": "Inga tillgängliga värdar hittades.", - "presentProtocol": "Protokoll", - "stopPresenting": "Sluta presentera", - "takeOver": "Ta över", - "takeOverConfirmTitle": "Ersätta den aktuella presentationen?", - "takeOverDescription": "{{name}} presenterar nu. Att ta över kommer att koppla bort alla från den scenen.", - "emptyStage": "Ingen presenterar. Gå ut på scenen för att dela en session.", - "stageLoading": "Ansluter till scenen...", - "stageEnded": "Presentationen avslutades", - "roomEnded": "Det här mötet har avslutats", - "presenterLabel": "{{name}} presenterar", - "guestLink": "Gästlänk", - "guestLinkOn": "Gästlänken är aktiverad. Alla med länken kan titta på scenen.", - "guestLinkOff": "Gästlänken är avstängd.", - "copyLink": "Kopiera länk", - "rotateLink": "Rotera länken", - "disableLink": "Inaktivera länk", - "rotateLinkConfirmTitle": "Rotera gästlänken?", - "rotateLinkDescription": "Den aktuella länken kommer att sluta fungera omedelbart. Anslutna SSH-gäster kommer att kopplas bort.", - "disableLinkConfirmTitle": "Inaktivera gästlänken?", - "disableLinkDescription": "Anonyma SSH-användare kommer att kopplas bort omedelbart och den aktuella länken kommer att sluta fungera.", - "linkCopied": "Länk kopierad", - "roles": "Roller", - "users": "Användare", - "searchUsers": "Sök användare", - "endConfirmTitle": "Avsluta det här mötet?", - "endDescription": "{{name}} kommer att upphöra för alla och kan inte öppnas igen.", - "endPersistentDescription": "Den aktiva presentationen i {{name}} kommer att stoppas. Rummet är fortfarande tillgängligt för återanvändning.", - "deleteRoom": "Ta bort rum", - "deleteConfirmTitle": "Radera det här rummet?", - "deleteDescription": "{{name}} kommer att raderas permanent för alla och kan inte ångras.", - "invitedTo": "Du blev inbjuden till \"{{name}}\"", - "guest": { - "title": "Möte", - "waiting": "Väntar på programledaren...", - "linkInvalid": "Denna gästlänk är ogiltig eller så har mötet avslutats" - }, - "reopenFromPanel": "Den här mötesfliken har upphört att gälla. Öppna rummet igen från panelen Möten.", - "retry": "Försöka igen", - "requestControl": "Begäran om kontroll", - "cancelControlRequest": "Avbryt begäran", - "controlQueue": "Kontrollförfrågningar", - "noControlRequests": "Inga väntande förfrågningar.", - "grantControlTo": "Ge kontroll till {{name}}", - "dismissControlRequest": "Avvisa kontrollbegäran från {{name}}", - "releaseControl": "Släppkontroll", - "grantControl": "Ge kontroll", - "revokeControl": "Ta tillbaka kontrollen", - "controlRequestedBy": "{{name}} bad om kontroll", - "grant": "Bevilja", - "controlBadge": "I kontroll", - "youArePresenting": "Du presenterar i en annan flik. Avbryt presentationen för att lämna över scenen.", - "created": "Rum skapat", - "invited": "Medlemmar inbjudna", - "roomTab": "Möte", - "serverChanged": "Mötesservern har ändrats. Öppna mötet igen från panelen Möten.", - "hostNotSynced": "Synkronisera den här värden med mötesservern innan presentationen.", - "linkRequiresServer": "Länka den här skrivbordsmiljön till en Termix-server och skapa mötet där för att dela en webblänk." - }, - "nav": { - "collab": "Möten" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Tillåt sessionsdelning", - "description": "Tillåt att liveterminal-, RDP-, VNC- och Telnet-sessioner delas instansövergripande. Åsidosätter alla delningsknappar per värd när de är inaktiverade." - } - }, - "host": { - "allowSessionSharing": { - "label": "Tillåt sessionsdelning", - "description": "Tillåt att livesessioner på den här värden delas via länk eller med andra användare" - } - } - }, - "permissions": { - "use": { - "title": "Dela sessioner och gå med i rum", - "description": "Dela livesessioner via länk eller med andra användare, och skapa och gå med i samarbetsrum." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Aldrig", - "oneHour": "1 timme", - "oneDay": "24 timmar", - "sevenDays": "7 dagar", - "thirtyDays": "30 dagar", - "custom": "Beställnings" - } - } - }, - "connections": { - "sectionSharedWithMe": "Delade med mig", - "sharedBy": "Delat av {{username}}", - "join": "Ansluta sig till", - "sharedSessionLabel": "{{hostName}} (delad)" - }, - "plugin": { - "name": "Sessionsdelning" - } -} diff --git a/plugins/session-sharing/locales/translated/th_TH.json b/plugins/session-sharing/locales/translated/th_TH.json deleted file mode 100644 index 7f6c674be..000000000 --- a/plugins/session-sharing/locales/translated/th_TH.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "กำลังเชื่อมต่อกับเซสชันที่ใช้ร่วมกัน...", - "linkInvalid": "ลิงก์แชร์นี้ไม่ถูกต้อง หมดอายุ หรือถูกยกเลิกแล้ว", - "rateLimited": "พยายามหลายครั้งเกินไป โปรดลองใหม่อีกครั้งในภายหลัง", - "sessionEnded": "การประชุมครั้งนี้สิ้นสุดลงแล้ว", - "readOnlyBadge": "ดูได้อย่างเดียว", - "ownerLabel": "เจ้าของที่พัก" - }, - "modalTitle": "เซสชั่นการแบ่งปัน", - "shareButton": "แบ่งปัน", - "notReadyToShare": "เซสชั่นยังไม่พร้อมให้แชร์", - "modeTab": { - "link": "ลิงก์", - "user": "ผู้ใช้" - }, - "linkModeDescription": "ใครก็ตามที่มีลิงก์นี้สามารถเข้าร่วมได้ ไม่จำเป็นต้องมีบัญชีผู้ใช้", - "userModeDescription": "แชร์กับผู้ใช้เฉพาะรายที่มีสิทธิ์เข้าถึงโฮสต์นี้อยู่แล้ว หากพวกเขายังไม่มีสิทธิ์เข้าถึง ให้แชร์โฮสต์กับพวกเขาก่อน หรือใช้ลิงก์แทน เมื่อแชร์แล้ว เซสชันจะปรากฏในแท็บการเชื่อมต่อของพวกเขา", - "permissionLevel": { - "label": "ระดับการอนุญาต", - "readOnly": "อ่านอย่างเดียว", - "readOnlyDescription": "สามารถรับชมการประชุมสดได้ แต่ไม่สามารถพิมพ์หรือโต้ตอบได้", - "readWrite": "อ่าน-เขียน", - "readWriteDescription": "สามารถพิมพ์และโต้ตอบกับเซสชันได้เหมือนกับเจ้าของเซสชัน" - }, - "expiryLabel": "ลิงก์หมดอายุ", - "createLinkButton": "สร้างลิงก์", - "createShareButton": "แชร์กับผู้ใช้", - "searchUsersPlaceholder": "ค้นหาผู้ใช้...", - "noUsersFound": "ไม่พบผู้ใช้งาน", - "linkCreated": "สร้างลิงก์สำหรับแชร์แล้ว", - "linkNeedsServer": "ลิงก์สำหรับแชร์ใช้งานได้เฉพาะกับเซสชันบนเซิร์ฟเวอร์ Termix เท่านั้น เชื่อมต่อโฮสต์นี้ผ่านเซิร์ฟเวอร์ที่เชื่อมโยงของคุณเพื่อแชร์ด้วยลิงก์", - "linkCopied": "คัดลอกลิงก์ไปยังคลิปบอร์ดแล้ว", - "copyLink": "คัดลิงก์", - "shareCreated": "แชร์เซสชันแล้ว เซสชันนั้นจะปรากฏในแท็บการเชื่อมต่อของพวกเขา", - "shareFailed": "ไม่สามารถสร้างการแชร์ได้", - "userLacksHostAccess": "ผู้ใช้รายนั้นยังไม่มีสิทธิ์เข้าถึงโฮสต์นี้ โปรดแชร์โฮสต์ให้พวกเขาก่อน หรือใช้ลิงก์แทน", - "activeShares": "หุ้นที่ใช้งานอยู่", - "noActiveShares": "ไม่มีหุ้นที่เปิดซื้อขายในรอบนี้", - "revoke": "ถอน", - "revokeConfirmTitle": "ต้องการยกเลิกการแชร์นี้หรือไม่?", - "revokeConfirmDescription": "ใครก็ตามที่ใช้ลิงก์นี้จะเสียสิทธิ์การเข้าถึงทันที", - "revoked": "หุ้นถูกเพิกถอน", - "revokeFailed": "ไม่สามารถเพิกถอนหุ้นได้", - "joinCount": "{{count}} เข้าร่วม", - "joinCount_other": "{{count}} เข้าร่วม", - "expiresAt": "หมดอายุ {{date}}", - "linkShareBadge": "ลิงก์", - "userShareBadge": "ผู้ใช้: {{username}}", - "loadSharesFailed": "ไม่สามารถโหลดหุ้นที่ใช้งานอยู่ได้", - "remoteDisplayUnavailable": "ขณะนี้ยังไม่สามารถใช้งานรีโมทเดสก์ท็อปได้", - "sharedWithMe": "เซสชั่นที่แชร์กับฉัน", - "customHoursPlaceholder": "เหลือเวลาอีกกี่ชั่วโมงก่อนที่สิทธิ์การเข้าถึงจะหมดอายุ" - }, - "collab": { - "createRoom": "ห้องใหม่", - "roomName": "ชื่อห้อง", - "persistentRoom": "ห้องที่คงอยู่", - "persistentRoomHint": "ห้องประชุมที่ตั้งค่าไว้ถาวรจะยังคงอยู่ในรายการหลังจากสิ้นสุดการประชุมและสามารถนำกลับมาใช้ใหม่ได้", - "noRooms": "ยังไม่มีห้องประชุม สร้างห้องประชุมเพื่อเริ่มการประชุม", - "openRoom": "เปิด", - "endRoom": "สิ้นสุดการประชุม", - "leaveRoom": "ออกจาก", - "deleteMember": "ลบ", - "removeMember": "ลบ {{name}}", - "removeMemberTitle": "ลบสมาชิกนี้ออก?", - "removeMemberDescription": "{{name}} จะเสียสิทธิ์ในการเข้าถึงห้องนี้ทันที โปรแกรมดู SSH ที่ใช้งานอยู่จะถูกตัดการเชื่อมต่อ", - "invite": "เชิญ", - "inviteTitle": "เชิญสมาชิก", - "members": "สมาชิก", - "membersPanel": "สมาชิกห้องและคำขอควบคุม", - "membersWithCount": "สมาชิก ({{count}})", - "guestsWithCount": "แขก ({{count}})", - "removeGuests": "ไล่แขกออก", - "anonymousGuest": "แขก", - "online": "ออนไลน์", - "offline": "ออฟไลน์", - "you": "(คุณ)", - "hostBadge": "เจ้าภาพ", - "presenterBadge": "นำเสนอ", - "present": "ปัจจุบัน", - "presentTitle": "ขึ้นเวที", - "presentHost": "เจ้าภาพ", - "searchHosts": "ค้นหาโฮสต์", - "noHostsFound": "ไม่พบโฮสต์ที่ใช้งานได้", - "presentProtocol": "โปรโตคอล", - "stopPresenting": "หยุดการนำเสนอ", - "takeOver": "เข้ายึดครอง", - "takeOverConfirmTitle": "ต้องการเปลี่ยนรูปแบบการนำเสนอปัจจุบันหรือไม่?", - "takeOverDescription": "{{name}} กำลังนำเสนออยู่ การเข้าควบคุมจะตัดการเชื่อมต่อทุกคนจากเวทีนั้น", - "emptyStage": "ไม่มีใครกำลังนำเสนออยู่ เชิญขึ้นเวทีเพื่อร่วมแบ่งปันช่วงการนำเสนอได้เลย", - "stageLoading": "เชื่อมต่อกับเวที...", - "stageEnded": "การนำเสนอสิ้นสุดลงแล้ว", - "roomEnded": "การประชุมนี้ได้สิ้นสุดลงแล้ว", - "presenterLabel": "{{name}} กำลังนำเสนอ", - "guestLink": "ลิงก์สำหรับแขก", - "guestLinkOn": "ลิงก์สำหรับแขกรับเชิญเปิดใช้งานแล้ว ใครก็ตามที่มีลิงก์สามารถรับชมการแสดงสดได้", - "guestLinkOff": "ลิงก์สำหรับผู้เยี่ยมชมถูกปิดใช้งาน", - "copyLink": "คัดลิงก์", - "rotateLink": "หมุนลิงก์", - "disableLink": "ปิดใช้งานลิงก์", - "rotateLinkConfirmTitle": "หมุนลิงก์สำหรับแขกรับเชิญหรือไม่?", - "rotateLinkDescription": "ลิงก์ปัจจุบันจะหยุดทำงานทันที ผู้ใช้งาน SSH ที่เชื่อมต่ออยู่จะถูกตัดการเชื่อมต่อ", - "disableLinkConfirmTitle": "ปิดใช้งานลิงก์สำหรับแขกใช่ไหม?", - "disableLinkDescription": "ผู้ใช้งาน SSH ที่ไม่ระบุตัวตนจะถูกตัดการเชื่อมต่อทันที และลิงก์ปัจจุบันจะหยุดทำงาน", - "linkCopied": "คัดลอกลิงก์แล้ว", - "roles": "บทบาท", - "users": "ผู้ใช้", - "searchUsers": "ค้นหาผู้ใช้", - "endConfirmTitle": "จบการประชุมนี้?", - "endDescription": "{{name}} จะสิ้นสุดลงสำหรับทุกคนและไม่สามารถเปิดใช้งานใหม่ได้อีก", - "endPersistentDescription": "การนำเสนอใน {{name}} จะหยุดลง ห้องยังคงพร้อมใช้งานได้ตามปกติ", - "deleteRoom": "ลบห้อง", - "deleteConfirmTitle": "ลบห้องนี้?", - "deleteDescription": "{{name}} จะถูกลบอย่างถาวรสำหรับทุกคนและไม่สามารถกู้คืนได้", - "invitedTo": "คุณได้รับเชิญไปที่ \"{{name}}\"", - "guest": { - "title": "การประชุม", - "waiting": "กำลังรอผู้ประกาศ...", - "linkInvalid": "ลิงก์สำหรับผู้เข้าร่วมนี้ไม่ถูกต้อง หรือการประชุมได้สิ้นสุดลงแล้ว" - }, - "reopenFromPanel": "แท็บการประชุมนี้หมดอายุแล้ว โปรดเปิดห้องประชุมอีกครั้งจากแผงการประชุม", - "retry": "ลองใหม่อีกครั้ง", - "requestControl": "การควบคุมคำขอ", - "cancelControlRequest": "คำขอการยกเลิก", - "controlQueue": "คำขอควบคุม", - "noControlRequests": "ไม่มีคำขอที่รอการดำเนินการ", - "grantControlTo": "มอบการควบคุมให้กับ {{name}}", - "dismissControlRequest": "ยกเลิกคำขอควบคุมของ {{name}}", - "releaseControl": "ปล่อยการควบคุม", - "grantControl": "มอบการควบคุม", - "revokeControl": "ทวงคืนการควบคุม", - "controlRequestedBy": "{{name}} ขอควบคุม", - "grant": "ยินยอม", - "controlBadge": "ควบคุมได้", - "youArePresenting": "คุณกำลังนำเสนออยู่ในแท็บอื่น โปรดหยุดการนำเสนอเพื่อส่งต่อให้ผู้อื่น", - "created": "สร้างห้อง", - "invited": "สมาชิกที่ได้รับเชิญ", - "roomTab": "การประชุม", - "serverChanged": "เซิร์ฟเวอร์การประชุมมีการเปลี่ยนแปลง โปรดเปิดการประชุมนี้อีกครั้งจากแผงการประชุม", - "hostNotSynced": "โปรดซิงค์โฮสต์นี้กับเซิร์ฟเวอร์การประชุมก่อนเริ่มการนำเสนอ", - "linkRequiresServer": "เชื่อมต่อเดสก์ท็อปเครื่องนี้กับเซิร์ฟเวอร์ Termix และสร้างการประชุมที่นั่นเพื่อแชร์ลิงก์เว็บ" - }, - "nav": { - "collab": "การประชุม" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "อนุญาตการแชร์เซสชัน", - "description": "อนุญาตให้แชร์เซสชันเทอร์มินัลสด, RDP, VNC และ Telnet ทั่วทั้งอินสแตนซ์ การตั้งค่านี้จะแทนที่การตั้งค่าการแชร์ต่อโฮสต์ทั้งหมดเมื่อปิดใช้งานอยู่" - } - }, - "host": { - "allowSessionSharing": { - "label": "อนุญาตการแชร์เซสชัน", - "description": "อนุญาตให้แชร์เซสชันสดบนโฮสต์นี้ผ่านลิงก์หรือกับผู้ใช้รายอื่น" - } - } - }, - "permissions": { - "use": { - "title": "แชร์เซสชันและเข้าร่วมห้อง", - "description": "แชร์เซสชันสดผ่านลิงก์หรือกับผู้ใช้รายอื่น และสร้างและเข้าร่วมห้องทำงานร่วมกันได้" - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "ไม่เคย", - "oneHour": "1 ชั่วโมง", - "oneDay": "24 ชั่วโมง", - "sevenDays": "7 วัน", - "thirtyDays": "30 วัน", - "custom": "กำหนดเอง" - } - } - }, - "connections": { - "sectionSharedWithMe": "แชร์กับฉัน", - "sharedBy": "แชร์โดย {{username}}", - "join": "เข้าร่วม", - "sharedSessionLabel": "{{hostName}} (แชร์)" - }, - "plugin": { - "name": "การแบ่งปันเซสชัน" - } -} diff --git a/plugins/session-sharing/locales/translated/tr_TR.json b/plugins/session-sharing/locales/translated/tr_TR.json deleted file mode 100644 index d921a76b2..000000000 --- a/plugins/session-sharing/locales/translated/tr_TR.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Paylaşılan oturuma bağlanılıyor...", - "linkInvalid": "Bu paylaşım bağlantısı geçersiz, süresi dolmuş veya iptal edilmiş.", - "rateLimited": "Çok fazla deneme yapıldı, lütfen kısa süre sonra tekrar deneyin.", - "sessionEnded": "Bu oturum sona erdi.", - "readOnlyBadge": "Yalnızca görüntüle", - "ownerLabel": "Ev sahibi" - }, - "modalTitle": "Paylaşım oturumu", - "shareButton": "Paylaş", - "notReadyToShare": "Oturum henüz paylaşılmaya hazır değil.", - "modeTab": { - "link": "Bağlantı", - "user": "Kullanıcı" - }, - "linkModeDescription": "Bu bağlantıya sahip herkes katılabilir, hesap gerekmez.", - "userModeDescription": "Bu sunucuya zaten erişimi olan belirli bir kullanıcıyla paylaşın. Henüz erişimleri yoksa, önce sunucuyu onlarla paylaşın veya bunun yerine bir bağlantı kullanın. Paylaşıldıktan sonra, oturum Bağlantılar sekmesinde görünür.", - "permissionLevel": { - "label": "İzin düzeyi", - "readOnly": "Salt okunur", - "readOnlyDescription": "Oturumu canlı olarak izleyebilirsiniz ancak yazı yazamaz veya etkileşimde bulunamazsınız.", - "readWrite": "Okuma-yazma", - "readWriteDescription": "Oturum sahibi gibi yazabilir ve oturumla etkileşim kurabilirsiniz." - }, - "expiryLabel": "Bağlantı süresinin dolması", - "createLinkButton": "Bağlantı oluştur", - "createShareButton": "Kullanıcıyla paylaş", - "searchUsersPlaceholder": "Kullanıcıları ara...", - "noUsersFound": "Hiç kullanıcı bulunamadı.", - "linkCreated": "Paylaşım bağlantısı oluşturuldu.", - "linkNeedsServer": "Bağlantı paylaşımı yalnızca Termix sunucusundaki oturumlar için geçerlidir. Bağlantıyı kullanarak paylaşmak için bu sunucuya bağlı sunucunuz üzerinden bağlanın.", - "linkCopied": "Bağlantı panoya kopyalandı", - "copyLink": "Bağlantıyı kopyala", - "shareCreated": "Oturum paylaşıldı. Bu, onların Bağlantılar sekmesinde görünecektir.", - "shareFailed": "Paylaşım oluşturulamadı.", - "userLacksHostAccess": "Bu kullanıcının henüz bu sunucuya erişimi yok. Öncelikle sunucuyu onlarla paylaşın veya bunun yerine bir bağlantı kullanın.", - "activeShares": "Aktif hisseler", - "noActiveShares": "Bu oturum için aktif hisse senedi bulunmamaktadır.", - "revoke": "İptal Et", - "revokeConfirmTitle": "Bu hisseyi iptal etmek ister misiniz?", - "revokeConfirmDescription": "Bu paylaşımı kullanan herkes erişimini anında kaybedecektir.", - "revoked": "Hisseler geri alındı", - "revokeFailed": "Hisseleri geri alma işlemi başarısız oldu.", - "joinCount": "{{count}} katıl", - "joinCount_other": "{{count}} birleşiyor", - "expiresAt": "Son kullanma: {{date}}", - "linkShareBadge": "Bağlantı", - "userShareBadge": "Kullanıcı: {{username}}", - "loadSharesFailed": "Aktif paylaşımlar yüklenemedi.", - "remoteDisplayUnavailable": "Uzaktan masaüstü bağlantısı şu anda kullanılamıyor.", - "sharedWithMe": "Benimle paylaşılan oturumlar", - "customHoursPlaceholder": "Erişim süresi (saat)" - }, - "collab": { - "createRoom": "Yeni oda", - "roomName": "Oda adı", - "persistentRoom": "Kalıcı oda", - "persistentRoomHint": "Kalıcı olarak tahsis edilen odalar, toplantı bittikten sonra da listede kalır ve tekrar kullanılabilir.", - "noRooms": "Henüz oda yok. Toplantı başlatmak için bir oda oluşturun.", - "openRoom": "Aç", - "endRoom": "Toplantıyı sonlandırın", - "leaveRoom": "Ayrılmak", - "deleteMember": "Kaldır", - "removeMember": "{{name}} kaldır", - "removeMemberTitle": "Bu üyeyi kaldırmak mı istiyorsunuz?", - "removeMemberDescription": "{{name}} bu odaya erişimini derhal kaybedecektir. Aktif olan tüm SSH izleyicilerinin bağlantısı kesilecektir.", - "invite": "Davet etmek", - "inviteTitle": "Üyeleri davet edin", - "members": "Üyeler", - "membersPanel": "Oda üyeleri ve kontrol talepleri", - "membersWithCount": "Üyeler ({{count}})", - "guestsWithCount": "Misafirler ({{count}})", - "removeGuests": "Misafirleri çıkarın", - "anonymousGuest": "Misafir", - "online": "çevrimiçi", - "offline": "çevrimdışı", - "you": "(Sen)", - "hostBadge": "Ana Bilgisayar", - "presenterBadge": "Sunar", - "present": "Sunmak", - "presentTitle": "Sahneye çıkın", - "presentHost": "Ana Bilgisayar", - "searchHosts": "Host ara", - "noHostsFound": "Kullanılabilir sunucu bulunamadı.", - "presentProtocol": "Protokol", - "stopPresenting": "Sunum yapmayı bırakın", - "takeOver": "Devralmak", - "takeOverConfirmTitle": "Mevcut sunumu değiştirmek mi istiyorsunuz?", - "takeOverDescription": "{{name}} şu anda sunum yapıyor. Devralmak, herkesi o sahneden ayıracaktır.", - "emptyStage": "Kimse sunum yapmıyor. Sahneye çıkın ve bir sunum paylaşın.", - "stageLoading": "Sahneye bağlanmak...", - "stageEnded": "Sunum sona erdi.", - "roomEnded": "Bu toplantı sona erdi.", - "presenterLabel": "{{name}} sunuyor", - "guestLink": "Konuk bağlantısı", - "guestLinkOn": "Misafir bağlantısı aktif. Bağlantıya sahip herkes sahneyi izleyebilir.", - "guestLinkOff": "Misafir bağlantısı kapalı.", - "copyLink": "Bağlantıyı kopyala", - "rotateLink": "Bağlantıyı döndür", - "disableLink": "Bağlantıyı devre dışı bırak", - "rotateLinkConfirmTitle": "Konuk bağlantısını döndürelim mi?", - "rotateLinkDescription": "Mevcut bağlantı derhal çalışmayı durduracaktır. Bağlı SSH misafirlerinin bağlantısı kesilecektir.", - "disableLinkConfirmTitle": "Misafir bağlantısını devre dışı bırak?", - "disableLinkDescription": "Anonim SSH izleyicilerinin bağlantısı derhal kesilecek ve mevcut bağlantı çalışmayı durduracaktır.", - "linkCopied": "Bağlantı kopyalandı", - "roles": "Roller", - "users": "Kullanıcılar", - "searchUsers": "Kullanıcıları ara", - "endConfirmTitle": "Bu toplantıyı sonlandıralım mı?", - "endDescription": "{{name}} herkes için sona erecek ve yeniden açılamayacak.", - "endPersistentDescription": "{{name}} 'daki aktif sunum durdurulacaktır. Oda yeniden kullanıma açıktır.", - "deleteRoom": "Odayı sil", - "deleteConfirmTitle": "Bu odayı silmek mi?", - "deleteDescription": "{{name}} herkes için kalıcı olarak silinecek ve geri alınamaz.", - "invitedTo": "\"{{name}} \" etkinliğine davet edildiniz.", - "guest": { - "title": "Toplantı", - "waiting": "Sunucuyu bekliyoruz...", - "linkInvalid": "Bu konuk bağlantısı geçersiz veya toplantı sona erdi." - }, - "reopenFromPanel": "Bu toplantı sekmesinin süresi doldu. Toplantılar panelinden odayı yeniden açın.", - "retry": "Yeniden dene", - "requestControl": "Talep kontrolü", - "cancelControlRequest": "İptal talebi", - "controlQueue": "Kontrol talepleri", - "noControlRequests": "Bekleyen talep yok.", - "grantControlTo": "Kontrolü {{name}}'a verin.", - "dismissControlRequest": "{{name}}'ın kontrol isteğini reddet", - "releaseControl": "Kontrolü serbest bırak", - "grantControl": "Kontrolü ona verin.", - "revokeControl": "Kontrolü geri alın", - "controlRequestedBy": "{{name}} kontrolü istedi", - "grant": "Hibe etmek", - "controlBadge": "Kontrol altında", - "youArePresenting": "Başka bir sekmede sunum yapıyorsunuz. Sunumu durdurun ve sahneyi karşı tarafa devredin.", - "created": "Oda oluşturuldu", - "invited": "Üyeler davet edildi", - "roomTab": "Toplantı", - "serverChanged": "Toplantı sunucusu değişti. Bu toplantıyı Toplantılar panelinden yeniden açın.", - "hostNotSynced": "Sunuma başlamadan önce bu sunucuyu toplantı sunucusuyla senkronize edin.", - "linkRequiresServer": "Bu masaüstünü bir Termix sunucusuna bağlayın ve web bağlantısını paylaşmak için toplantıyı orada oluşturun." - }, - "nav": { - "collab": "Toplantılar" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Oturum Paylaşımına İzin Ver", - "description": "Canlı terminal, RDP, VNC ve Telnet oturumlarının tüm örnek genelinde paylaşılmasına izin verin. Devre dışı bırakıldığında, sunucu başına paylaşım ayarlarının tümünü geçersiz kılar." - } - }, - "host": { - "allowSessionSharing": { - "label": "Oturum Paylaşımına İzin Ver", - "description": "Bu sunucudaki canlı oturumların bağlantı yoluyla veya diğer kullanıcılarla paylaşılmasına izin verin." - } - } - }, - "permissions": { - "use": { - "title": "Oturumları paylaşın ve odalara katılın", - "description": "Canlı oturumları bağlantı yoluyla veya diğer kullanıcılarla paylaşın ve iş birliği odaları oluşturup bunlara katılın." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Süresiz", - "oneHour": "1 saat", - "oneDay": "24 saat", - "sevenDays": "7 gün", - "thirtyDays": "30 gün", - "custom": "Özel" - } - } - }, - "connections": { - "sectionSharedWithMe": "Benimle paylaşıldı", - "sharedBy": "{{username}} tarafından paylaşıldı", - "join": "Katılmak", - "sharedSessionLabel": "{{hostName}} (paylaşılan)" - }, - "plugin": { - "name": "Oturum Paylaşımı" - } -} diff --git a/plugins/session-sharing/locales/translated/uk_UA.json b/plugins/session-sharing/locales/translated/uk_UA.json deleted file mode 100644 index 643922202..000000000 --- a/plugins/session-sharing/locales/translated/uk_UA.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Підключення до спільного сеансу...", - "linkInvalid": "Це посилання для спільного доступу недійсне, термін його дії минув або його було відкликано", - "rateLimited": "Забагато спроб, спробуйте ще раз пізніше", - "sessionEnded": "Цей сеанс завершився", - "readOnlyBadge": "Тільки перегляд", - "ownerLabel": "Власник хоста" - }, - "modalTitle": "Поділитися сеансом", - "shareButton": "Поділитися", - "notReadyToShare": "Сеанс ще не готовий до поширення", - "modeTab": { - "link": "Посилання", - "user": "Користувач" - }, - "linkModeDescription": "Приєднатися може кожен, хто має це посилання, обліковий запис не потрібен.", - "userModeDescription": "Надайте доступ певному користувачеві, який уже має доступ до цього хоста. Якщо у нього ще немає доступу, спочатку надайте йому доступ до хоста або скористайтеся посиланням. Після надання доступу сеанс відображається на його вкладці «Підключення».", - "permissionLevel": { - "label": "Рівень дозволів", - "readOnly": "Тільки для читання", - "readOnlyDescription": "Можна дивитися сеанс у прямому ефірі, але не можна друкувати чи взаємодіяти.", - "readWrite": "Читання-запис", - "readWriteDescription": "Можна друкувати та взаємодіяти з сеансом так само, як і його власник." - }, - "expiryLabel": "Термін дії посилання", - "createLinkButton": "Створити посилання", - "createShareButton": "Поділитися з користувачем", - "searchUsersPlaceholder": "Пошук користувачів...", - "noUsersFound": "Користувачів не знайдено", - "linkCreated": "Посилання для спільного доступу створено", - "linkNeedsServer": "Посилання для спільного доступу працюють лише для сеансів на сервері Termix. Підключіть цей хост через ваш пов’язаний сервер, щоб поділитися ним за допомогою посилання.", - "linkCopied": "Посилання скопійовано в буфер обміну", - "copyLink": "Копіювати посилання", - "shareCreated": "Сеанс спільний. Він відображатиметься на вкладці «Зв’язки».", - "shareFailed": "Не вдалося створити спільний доступ", - "userLacksHostAccess": "Цей користувач ще не має доступу до цього хоста. Спочатку надайте йому доступ до хоста або скористайтеся посиланням.", - "activeShares": "Активні акції", - "noActiveShares": "Немає активних поширень для цього сеансу", - "revoke": "Скасувати", - "revokeConfirmTitle": "Скасувати цей спільний доступ?", - "revokeConfirmDescription": "Будь-хто, хто користується цим ресурсом спільного доступу, негайно втратить доступ.", - "revoked": "Акції анульовано", - "revokeFailed": "Не вдалося скасувати спільний доступ", - "joinCount": "{{count}} приєднатися", - "joinCount_other": "{{count}} приєднується", - "expiresAt": "Термін дії закінчується {{date}}", - "linkShareBadge": "Посилання", - "userShareBadge": "Користувач: {{username}}", - "loadSharesFailed": "Не вдалося завантажити активні спільні ресурси", - "remoteDisplayUnavailable": "Віддалений робочий стіл зараз недоступний.", - "sharedWithMe": "Сеанси, якими я поділився", - "customHoursPlaceholder": "Години до закінчення терміну дії доступу" - }, - "collab": { - "createRoom": "Нова кімната", - "roomName": "Назва кімнати", - "persistentRoom": "Постійна кімната", - "persistentRoomHint": "Постійні кімнати залишаються у списку після завершення зустрічі та можуть бути використані повторно.", - "noRooms": "Поки що немає кімнат. Створіть одну, щоб розпочати зустріч.", - "openRoom": "ВІДЧИНЕНО", - "endRoom": "Закінчити зустріч", - "leaveRoom": "Залишити", - "deleteMember": "Видалити", - "removeMember": "Видалити {{name}}", - "removeMemberTitle": "Видалити цього учасника?", - "removeMemberDescription": "{{name}} негайно втратить доступ до цієї кімнати. Будь-який активний переглядач SSH буде відключено.", - "invite": "Запросити", - "inviteTitle": "Запросити учасників", - "members": "Члени", - "membersPanel": "Учасники кімнати та запити на керування", - "membersWithCount": "Учасники ({{count}})", - "guestsWithCount": "Гості ({{count}})", - "removeGuests": "Видалити гостей", - "anonymousGuest": "Гість", - "online": "онлайн", - "offline": "офлайн", - "you": "(ти)", - "hostBadge": "Хост", - "presenterBadge": "Презентація", - "present": "Подарунок", - "presentTitle": "Вийди на сцену", - "presentHost": "Хост", - "searchHosts": "Пошук хостів", - "noHostsFound": "Не знайдено доступних хостів.", - "presentProtocol": "Протокол", - "stopPresenting": "Зупинити презентацію", - "takeOver": "Захопити", - "takeOverConfirmTitle": "Замінити поточну презентацію?", - "takeOverDescription": "{{name}} зараз презентує. Перехоплення сцени відключить усіх учасників.", - "emptyStage": "Ніхто не виступає. Вийдіть на сцену, щоб поділитися сесією.", - "stageLoading": "Підключення до сцени...", - "stageEnded": "Презентація закінчилася", - "roomEnded": "Ця зустріч завершилася", - "presenterLabel": "{{name}} презентує", - "guestLink": "Гостьове посилання", - "guestLinkOn": "Гостьове посилання ввімкнено. Будь-хто, хто має посилання, може дивитися сцену.", - "guestLinkOff": "Гостьове посилання вимкнено.", - "copyLink": "Копіювати посилання", - "rotateLink": "Повернути посилання", - "disableLink": "Вимкнути посилання", - "rotateLinkConfirmTitle": "Повернути гостьове посилання?", - "rotateLinkDescription": "Поточне посилання негайно перестане працювати. Підключені гості SSH будуть відключені.", - "disableLinkConfirmTitle": "Вимкнути гостьове посилання?", - "disableLinkDescription": "Анонімні користувачі SSH будуть негайно відключені, а поточне посилання перестане працювати.", - "linkCopied": "Посилання скопійовано", - "roles": "Ролі", - "users": "Користувачі", - "searchUsers": "Пошук користувачів", - "endConfirmTitle": "Завершити цю зустріч?", - "endDescription": "{{name}} завершиться для всіх і не може бути відкрито повторно.", - "endPersistentDescription": "Активна презентація в {{name}} буде зупинена. Кімната залишається доступною для повторного використання.", - "deleteRoom": "Видалити кімнату", - "deleteConfirmTitle": "Видалити цю кімнату?", - "deleteDescription": "{{name}} буде видалено назавжди для всіх і цю дію не можна буде скасувати.", - "invitedTo": "Вас запросили до «{{name}}»", - "guest": { - "title": "Зустріч", - "waiting": "Чекаємо на ведучого...", - "linkInvalid": "Це гостьове посилання недійсне або зустріч завершилася" - }, - "reopenFromPanel": "Термін дії цієї вкладки зустрічі закінчився. Відкрийте кімнату знову на панелі Зустрічі.", - "retry": "Повторити спробу", - "requestControl": "Запит на керування", - "cancelControlRequest": "Скасувати запит", - "controlQueue": "Запити на керування", - "noControlRequests": "Немає запитів, що очікують на розгляд.", - "grantControlTo": "Надайте керування {{name}}", - "dismissControlRequest": "Відхилити запит керування від {{name}}", - "releaseControl": "Контроль випуску", - "grantControl": "Надайте контроль", - "revokeControl": "Поверніть собі контроль", - "controlRequestedBy": "{{name}} запросив керування", - "grant": "Грант", - "controlBadge": "Контроль", - "youArePresenting": "Ви презентуєте в іншій вкладці. Зупиніть презентацію, щоб передати сцену.", - "created": "Кімнату створено", - "invited": "Запрошені учасники", - "roomTab": "Зустріч", - "serverChanged": "Сервер зустрічі змінився. Відкрийте цю зустріч знову з панелі «Зустрічі».", - "hostNotSynced": "Синхронізуйте цей хост із сервером зустрічей перед презентацією.", - "linkRequiresServer": "Підключіть цей робочий стіл до сервера Termix та створіть там зустріч, щоб поділитися веб-посиланням." - }, - "nav": { - "collab": "Зустрічі" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Дозволити спільний доступ до сеансу", - "description": "Дозволити спільний доступ до сеансів терміналу, RDP, VNC та Telnet для всього екземпляра. Перевизначає кожен перемикач спільного доступу для кожного хоста, якщо його вимкнено." - } - }, - "host": { - "allowSessionSharing": { - "label": "Дозволити спільний доступ до сеансу", - "description": "Дозволити ділитися прямими трансляціями на цьому хості через посилання або з іншими користувачами" - } - } - }, - "permissions": { - "use": { - "title": "Діліться сесіями та приєднуйтесь до кімнат", - "description": "Діліться сеансами в реальному часі за посиланням або з іншими користувачами, а також створюйте кімнати для співпраці та приєднуйтесь до них." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Ніколи", - "oneHour": "1 година", - "oneDay": "24 години", - "sevenDays": "7 днів", - "thirtyDays": "30 днів", - "custom": "Користувацька" - } - } - }, - "connections": { - "sectionSharedWithMe": "Поділилися зі мною", - "sharedBy": "Поділився користувачем {{username}}", - "join": "Приєднатися", - "sharedSessionLabel": "{{hostName}} (спільний доступ)" - }, - "plugin": { - "name": "Спільне використання сеансу" - } -} diff --git a/plugins/session-sharing/locales/translated/vi_VN.json b/plugins/session-sharing/locales/translated/vi_VN.json deleted file mode 100644 index 11979adc6..000000000 --- a/plugins/session-sharing/locales/translated/vi_VN.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "Đang kết nối đến phiên chia sẻ...", - "linkInvalid": "Liên kết chia sẻ này không hợp lệ, đã hết hạn hoặc đã bị thu hồi.", - "rateLimited": "Đã thử quá nhiều lần, vui lòng thử lại sau.", - "sessionEnded": "Phiên này đã kết thúc", - "readOnlyBadge": "Chỉ xem", - "ownerLabel": "Chủ nhà" - }, - "modalTitle": "Chia sẻ phiên họp", - "shareButton": "Chia sẻ", - "notReadyToShare": "Buổi học chưa sẵn sàng để chia sẻ.", - "modeTab": { - "link": "Liên kết", - "user": "Người dùng" - }, - "linkModeDescription": "Bất kỳ ai có liên kết này đều có thể tham gia, không cần tài khoản.", - "userModeDescription": "Chia sẻ với một người dùng cụ thể đã có quyền truy cập vào máy chủ này. Nếu họ chưa có quyền truy cập, hãy chia sẻ máy chủ với họ trước hoặc sử dụng liên kết thay thế. Sau khi chia sẻ, phiên làm việc sẽ xuất hiện trong tab Kết nối của họ.", - "permissionLevel": { - "label": "Cấp độ quyền", - "readOnly": "Chỉ đọc", - "readOnlyDescription": "Có thể xem phiên trực tiếp nhưng không thể gõ chữ hoặc tương tác.", - "readWrite": "Đọc-viết", - "readWriteDescription": "Có thể nhập liệu và tương tác với phiên làm việc giống như chủ sở hữu." - }, - "expiryLabel": "Liên kết hết hạn", - "createLinkButton": "Tạo liên kết", - "createShareButton": "Chia sẻ với người dùng", - "searchUsersPlaceholder": "Tìm kiếm người dùng...", - "noUsersFound": "Không tìm thấy người dùng nào", - "linkCreated": "Đã tạo liên kết chia sẻ", - "linkNeedsServer": "Liên kết chia sẻ chỉ hoạt động cho các phiên trên máy chủ Termix. Kết nối máy chủ này thông qua máy chủ được liên kết của bạn để chia sẻ nó bằng liên kết.", - "linkCopied": "Liên kết đã được sao chép vào clipboard", - "copyLink": "Sao chép liên kết", - "shareCreated": "Phiên họp đã được chia sẻ. Nó sẽ xuất hiện trong tab Kết nối của họ.", - "shareFailed": "Không thể tạo chia sẻ", - "userLacksHostAccess": "Người dùng đó chưa có quyền truy cập vào máy chủ này. Hãy chia sẻ thông tin máy chủ với họ trước, hoặc sử dụng liên kết thay thế.", - "activeShares": "Cổ phiếu tích cực", - "noActiveShares": "Hiện không có cổ phiếu nào đang được giao dịch trong phiên này.", - "revoke": "Thu hồi", - "revokeConfirmTitle": "Thu hồi cổ phần này?", - "revokeConfirmDescription": "Bất kỳ ai sử dụng thư mục chia sẻ này sẽ mất quyền truy cập ngay lập tức.", - "revoked": "Cổ phần đã bị thu hồi", - "revokeFailed": "Không thể thu hồi cổ phần", - "joinCount": "{{count}} tham gia", - "joinCount_other": "{{count}} tham gia", - "expiresAt": "Hết hạn {{date}}", - "linkShareBadge": "Liên kết", - "userShareBadge": "Người dùng: {{username}}", - "loadSharesFailed": "Không thể tải các chia sẻ đang hoạt động", - "remoteDisplayUnavailable": "Hiện tại, tính năng truy cập máy tính từ xa không khả dụng.", - "sharedWithMe": "Các buổi chia sẻ với tôi", - "customHoursPlaceholder": "Số giờ cho đến khi hết hạn truy cập" - }, - "collab": { - "createRoom": "Phòng mới", - "roomName": "Tên phòng", - "persistentRoom": "Phòng bền vững", - "persistentRoomHint": "Các phòng họp được lưu lại sẽ vẫn hiển thị sau khi cuộc họp kết thúc và có thể được sử dụng lại.", - "noRooms": "Chưa có phòng nào. Hãy tạo một phòng để bắt đầu cuộc họp.", - "openRoom": "Mở", - "endRoom": "Kết thúc cuộc họp", - "leaveRoom": "Rời khỏi", - "deleteMember": "Xóa", - "removeMember": "Xóa {{name}}", - "removeMemberTitle": "Xóa thành viên này?", - "removeMemberDescription": "{{name}} sẽ mất quyền truy cập vào phòng này ngay lập tức. Bất kỳ trình xem SSH nào đang hoạt động sẽ bị ngắt kết nối.", - "invite": "Mời", - "inviteTitle": "Mời thành viên", - "members": "Thành viên", - "membersPanel": "Thành viên phòng và yêu cầu kiểm soát", - "membersWithCount": "Thành viên ({{count}})", - "guestsWithCount": "Khách ({{count}})", - "removeGuests": "Đuổi khách", - "anonymousGuest": "Khách mời", - "online": "trực tuyến", - "offline": "ngoại tuyến", - "you": "(Bạn)", - "hostBadge": "Máy chủ", - "presenterBadge": "Giới thiệu", - "present": "Hiện tại", - "presentTitle": "Hãy lên sân khấu", - "presentHost": "Máy chủ", - "searchHosts": "Tìm kiếm máy chủ", - "noHostsFound": "Không tìm thấy máy chủ nào khả dụng.", - "presentProtocol": "Giao thức", - "stopPresenting": "Dừng thuyết trình", - "takeOver": "Tiếp quản", - "takeOverConfirmTitle": "Bạn muốn thay thế bài thuyết trình hiện tại?", - "takeOverDescription": "{{name}} đang trình bày. Việc tiếp quản sẽ ngắt kết nối mọi người khỏi sân khấu đó.", - "emptyStage": "Hiện chưa có ai thuyết trình. Hãy lên sân khấu để chia sẻ một phiên thảo luận.", - "stageLoading": "Kết nối với sân khấu...", - "stageEnded": "Buổi thuyết trình kết thúc.", - "roomEnded": "Cuộc họp này đã kết thúc.", - "presenterLabel": "{{name}} đang trình bày", - "guestLink": "Liên kết khách", - "guestLinkOn": "Link dành cho khách đã được bật. Bất cứ ai có link đều có thể xem chương trình.", - "guestLinkOff": "Liên kết khách đã bị tắt.", - "copyLink": "Sao chép liên kết", - "rotateLink": "Xoay liên kết", - "disableLink": "Vô hiệu hóa liên kết", - "rotateLinkConfirmTitle": "Xoay liên kết khách?", - "rotateLinkDescription": "Kết nối hiện tại sẽ ngừng hoạt động ngay lập tức. Các máy khách SSH đang kết nối sẽ bị ngắt kết nối.", - "disableLinkConfirmTitle": "Vô hiệu hóa liên kết khách?", - "disableLinkDescription": "Những người xem SSH ẩn danh sẽ bị ngắt kết nối ngay lập tức và liên kết hiện tại sẽ ngừng hoạt động.", - "linkCopied": "Đã sao chép liên kết", - "roles": "Vai trò", - "users": "Người dùng", - "searchUsers": "Tìm kiếm người dùng", - "endConfirmTitle": "Kết thúc cuộc họp này?", - "endDescription": "{{name}} sẽ kết thúc đối với tất cả mọi người và không thể mở lại.", - "endPersistentDescription": "Buổi thuyết trình đang diễn ra tại {{name}} sẽ dừng lại. Phòng vẫn có thể được sử dụng lại.", - "deleteRoom": "Xóa phòng", - "deleteConfirmTitle": "Xóa phòng này?", - "deleteDescription": "{{name}} sẽ bị xóa vĩnh viễn đối với tất cả mọi người và không thể hoàn tác.", - "invitedTo": "Bạn được mời đến \"{{name}}\"", - "guest": { - "title": "Cuộc họp", - "waiting": "Đang chờ người dẫn chương trình...", - "linkInvalid": "Liên kết dành cho khách này không hợp lệ hoặc cuộc họp đã kết thúc." - }, - "reopenFromPanel": "Tab cuộc họp này đã hết hạn. Vui lòng mở lại phòng từ mục Cuộc họp.", - "retry": "Thử lại", - "requestControl": "Yêu cầu kiểm soát", - "cancelControlRequest": "Yêu cầu hủy bỏ", - "controlQueue": "Yêu cầu kiểm soát", - "noControlRequests": "Không có yêu cầu nào đang chờ xử lý.", - "grantControlTo": "Trao quyền điều khiển cho {{name}}", - "dismissControlRequest": "Bỏ qua yêu cầu điều khiển của {{name}}", - "releaseControl": "Kiểm soát giải phóng", - "grantControl": "Trao quyền kiểm soát", - "revokeControl": "Giành lại quyền kiểm soát", - "controlRequestedBy": "{{name}} yêu cầu quyền điều khiển", - "grant": "Khoản trợ cấp", - "controlBadge": "Trong tầm kiểm soát", - "youArePresenting": "Bạn đang thuyết trình ở một tab khác. Hãy dừng thuyết trình để nhường sân khấu.", - "created": "Phòng được tạo", - "invited": "Các thành viên được mời", - "roomTab": "Cuộc họp", - "serverChanged": "Máy chủ cuộc họp đã thay đổi. Vui lòng mở lại cuộc họp này từ mục Cuộc họp.", - "hostNotSynced": "Hãy đồng bộ hóa máy chủ này với máy chủ cuộc họp trước khi trình bày.", - "linkRequiresServer": "Kết nối máy tính để bàn này với máy chủ Termix và tạo cuộc họp ở đó để chia sẻ liên kết web." - }, - "nav": { - "collab": "Các cuộc họp" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "Cho phép chia sẻ phiên", - "description": "Cho phép chia sẻ các phiên terminal trực tiếp, RDP, VNC và Telnet trên toàn bộ máy chủ. Khi bị vô hiệu hóa, tùy chọn này sẽ ghi đè lên mọi tùy chọn chia sẻ trên từng máy chủ riêng lẻ." - } - }, - "host": { - "allowSessionSharing": { - "label": "Cho phép chia sẻ phiên", - "description": "Cho phép chia sẻ các phiên trực tiếp trên máy chủ này qua liên kết hoặc với những người dùng khác." - } - } - }, - "permissions": { - "use": { - "title": "Chia sẻ phiên họp và tham gia phòng họp", - "description": "Chia sẻ các phiên trực tiếp qua liên kết hoặc với người dùng khác, tạo và tham gia các phòng cộng tác." - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "Không bao giờ", - "oneHour": "1 giờ", - "oneDay": "24 giờ", - "sevenDays": "7 ngày", - "thirtyDays": "30 ngày", - "custom": "Tùy chỉnh" - } - } - }, - "connections": { - "sectionSharedWithMe": "Đã chia sẻ với tôi", - "sharedBy": "Được chia sẻ bởi {{username}}", - "join": "Tham gia", - "sharedSessionLabel": "{{hostName}} (được chia sẻ)" - }, - "plugin": { - "name": "Chia sẻ phiên họp" - } -} diff --git a/plugins/session-sharing/locales/translated/zh_CN.json b/plugins/session-sharing/locales/translated/zh_CN.json deleted file mode 100644 index 777678a74..000000000 --- a/plugins/session-sharing/locales/translated/zh_CN.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "正在连接到共享会话...", - "linkInvalid": "此共享链接无效、已过期或已被撤销。", - "rateLimited": "尝试次数过多,请稍后再试。", - "sessionEnded": "此会话已结束", - "readOnlyBadge": "仅查看", - "ownerLabel": "房东" - }, - "modalTitle": "共享会话", - "shareButton": "共享", - "notReadyToShare": "会话尚未准备好共享", - "modeTab": { - "link": "链接", - "user": "用户" - }, - "linkModeDescription": "任何人只要拥有此链接即可加入,无需注册账号。", - "userModeDescription": "将主机共享给已拥有此主机访问权限的特定用户。如果他们还没有访问权限,请先将主机共享给他们,或者使用链接代替。共享后,会话将显示在他们的“连接”标签页中。", - "permissionLevel": { - "label": "权限级别", - "readOnly": "只读", - "readOnlyDescription": "可以观看直播,但无法打字或互动。", - "readWrite": "读写", - "readWriteDescription": "可以像会话所有者一样进行输入和互动。" - }, - "expiryLabel": "链接过期", - "createLinkButton": "创建链接", - "createShareButton": "与用户共享", - "searchUsersPlaceholder": "搜索用户......", - "noUsersFound": "未找到用户", - "linkCreated": "已创建共享链接", - "linkNeedsServer": "分享链接仅适用于 Termix 服务器上的会话。请通过您链接的服务器连接此主机,然后使用链接进行分享。", - "linkCopied": "链接已复制到剪贴板", - "copyLink": "复制链接", - "shareCreated": "会话已共享。它将出现在对方的“连接”标签页中。", - "shareFailed": "创建共享失败", - "userLacksHostAccess": "该用户目前没有访问此主机的权限。请先与其共享主机,或使用链接进行共享。", - "activeShares": "有效共享", - "noActiveShares": "此会话暂无活跃共享", - "revoke": "吊销", - "revokeConfirmTitle": "撤销此共享?", - "revokeConfirmDescription": "任何使用此共享的用户都将立即失去访问权限。", - "revoked": "共享已撤销", - "revokeFailed": "撤销共享失败", - "joinCount": "{{count}} 人加入", - "joinCount_other": "{{count}} 人加入", - "expiresAt": "过期于 {{date}}", - "linkShareBadge": "链接", - "userShareBadge": "用户:{{username}}", - "loadSharesFailed": "无法加载共享", - "remoteDisplayUnavailable": "目前远程桌面功能不可用。", - "sharedWithMe": "与我分享的会议", - "customHoursPlaceholder": "过期前的小时数" - }, - "collab": { - "createRoom": "新房间", - "roomName": "房间名称", - "persistentRoom": "持久房间", - "persistentRoomHint": "会议结束后,持久会议室仍会保留在列表中,可以重复使用。", - "noRooms": "暂无会议室。创建会议室即可开始会议。", - "openRoom": "打开", - "endRoom": "会议结束", - "leaveRoom": "离开", - "deleteMember": "删除", - "removeMember": "移除{{name}}", - "removeMemberTitle": "移除此成员?", - "removeMemberDescription": "{{name}} 将立即失去对该房间的访问权限。所有正在使用的 SSH 查看器都将被断开连接。", - "invite": "邀请", - "inviteTitle": "邀请成员", - "members": "成员", - "membersPanel": "房间成员和控制请求", - "membersWithCount": "成员({{count}})", - "guestsWithCount": "宾客({{count}})", - "removeGuests": "移除客人", - "anonymousGuest": "客人", - "online": "在线", - "offline": "离线", - "you": "(你)", - "hostBadge": "主机", - "presenterBadge": "呈现", - "present": "展示", - "presentTitle": "登上舞台", - "presentHost": "主机", - "searchHosts": "搜索主机", - "noHostsFound": "未找到可用主机。", - "presentProtocol": "协议", - "stopPresenting": "停止演示", - "takeOver": "接管", - "takeOverConfirmTitle": "替换当前演示文稿?", - "takeOverDescription": "{{name}} 正在直播。接管直播将会断开所有人与该直播间的连接。", - "emptyStage": "目前无人发言。请上台分享您的演讲内容。", - "stageLoading": "连接到舞台……", - "stageEnded": "演讲结束了", - "roomEnded": "会议结束", - "presenterLabel": "{{name}} 正在呈现", - "guestLink": "访客链接", - "guestLinkOn": "访客链接已开启。任何拥有该链接的人都可以观看舞台表演。", - "guestLinkOff": "访客链接已关闭。", - "copyLink": "复制链接", - "rotateLink": "旋转链接", - "disableLink": "禁用链接", - "rotateLinkConfirmTitle": "旋转访客链接?", - "rotateLinkDescription": "当前连接将立即停止工作。已连接的SSH访客将被断开连接。", - "disableLinkConfirmTitle": "禁用访客链接?", - "disableLinkDescription": "匿名 SSH 查看者将被立即断开连接,当前链接将停止工作。", - "linkCopied": "链接已复制", - "roles": "角色", - "users": "用户", - "searchUsers": "搜索用户", - "endConfirmTitle": "结束会议吗?", - "endDescription": "{{name}} 将对所有人关闭,且无法重新开启。", - "endPersistentDescription": "{{name}} 中的当前演示将停止。该房间仍可供再次使用。", - "deleteRoom": "删除房间", - "deleteConfirmTitle": "删除这个房间?", - "deleteDescription": "{{name}} 将被永久删除,且无法撤销。", - "invitedTo": "您受邀参加“{{name}}”", - "guest": { - "title": "会议", - "waiting": "等待主持人……", - "linkInvalid": "此访客链接无效或会议已结束" - }, - "reopenFromPanel": "此会议标签页已过期。请从“会议”面板重新打开会议室。", - "retry": "重试", - "requestControl": "请求控制", - "cancelControlRequest": "取消请求", - "controlQueue": "控制请求", - "noControlRequests": "没有待处理的请求。", - "grantControlTo": "将控制权交给 {{name}}", - "dismissControlRequest": "驳回 {{name}}的控制请求", - "releaseControl": "发布控制", - "grantControl": "赋予控制权", - "revokeControl": "夺回控制权", - "controlRequestedBy": "{{name}} 请求控制权", - "grant": "授予", - "controlBadge": "掌控之中", - "youArePresenting": "您正在另一个标签页中进行演示。请停止演示,将演示权交给对方。", - "created": "创建房间", - "invited": "受邀成员", - "roomTab": "会议", - "serverChanged": "会议服务器已更改。请从“会议”面板重新打开此会议。", - "hostNotSynced": "演示前,请将此主机与会议服务器同步。", - "linkRequiresServer": "将此桌面连接到 Termix 服务器,并在该服务器上创建会议以共享 Web 链接。" - }, - "nav": { - "collab": "会议" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "允许会话共享", - "description": "允许在整个实例范围内共享实时终端、RDP、VNC 和 Telnet 会话。禁用此功能将覆盖所有主机级别的共享设置。" - } - }, - "host": { - "allowSessionSharing": { - "label": "允许会话共享", - "description": "允许通过链接或其他方式共享此主机上的实时会话。" - } - } - }, - "permissions": { - "use": { - "title": "分享会话并加入房间", - "description": "通过链接或与其他用户共享实时会话,并创建和加入协作房间。" - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "永不过期", - "oneHour": "1 小时", - "oneDay": "24 小时", - "sevenDays": "7 天", - "thirtyDays": "30 天", - "custom": "自定义" - } - } - }, - "connections": { - "sectionSharedWithMe": "与我共享", - "sharedBy": "由 {{username}} 共享", - "join": "加入", - "sharedSessionLabel": "{{hostName}}(共享)" - }, - "plugin": { - "name": "会话共享" - } -} diff --git a/plugins/session-sharing/locales/translated/zh_TW.json b/plugins/session-sharing/locales/translated/zh_TW.json deleted file mode 100644 index 0501a36d1..000000000 --- a/plugins/session-sharing/locales/translated/zh_TW.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "sessionSharing": { - "guestView": { - "loading": "正在連接到共享會話...", - "linkInvalid": "此分享連結無效、已過期或已撤銷。", - "rateLimited": "嘗試次數過多,請稍後再試。", - "sessionEnded": "本次會議已結束", - "readOnlyBadge": "僅查看", - "ownerLabel": "房東" - }, - "modalTitle": "分享會話", - "shareButton": "分享", - "notReadyToShare": "會話尚未準備好分享", - "modeTab": { - "link": "關聯", - "user": "使用者" - }, - "linkModeDescription": "任何人只要擁有此連結即可加入,無需註冊帳號。", - "userModeDescription": "將主機共用給已擁有此主機存取權限的特定使用者。如果他們還沒有存取權限,請先將主機分享給他們,或使用連結代替。共享後,會話將顯示在他們的“連接”選項卡中。", - "permissionLevel": { - "label": "權限層級", - "readOnly": "唯讀", - "readOnlyDescription": "可以觀看直播,但無法打字或互動。", - "readWrite": "讀寫", - "readWriteDescription": "可以像會話擁有者一樣進行輸入和互動。" - }, - "expiryLabel": "連結過期", - "createLinkButton": "建立連結", - "createShareButton": "與用戶分享", - "searchUsersPlaceholder": "搜尋用戶...", - "noUsersFound": "未找到用戶", - "linkCreated": "已建立分享連結", - "linkNeedsServer": "分享連結僅適用於 Termix 伺服器上的會話。請透過您連結的伺服器連接此主機,然後使用連結進行分享。", - "linkCopied": "連結已複製到剪貼簿", - "copyLink": "複製連結", - "shareCreated": "會話已共用。它將出現在對方的「連線」標籤中。", - "shareFailed": "建立共享失敗", - "userLacksHostAccess": "該使用者目前沒有存取此主機的權限。請先與其共享主機,或使用連結進行共享。", - "activeShares": "活躍股", - "noActiveShares": "本會話無活躍份額", - "revoke": "撤銷", - "revokeConfirmTitle": "撤銷此份額?", - "revokeConfirmDescription": "任何使用此共用資料夾的使用者都將立即失去存取權限。", - "revoked": "股份撤銷", - "revokeFailed": "撤銷股份失敗", - "joinCount": "{{count}} 加入", - "joinCount_other": "{{count}} 加入", - "expiresAt": "到期日 {{date}}", - "linkShareBadge": "關聯", - "userShareBadge": "使用者: {{username}}", - "loadSharesFailed": "無法加載活躍份額", - "remoteDisplayUnavailable": "目前遠端桌面功能無法使用。", - "sharedWithMe": "與我分享的會議", - "customHoursPlaceholder": "存取到期前的小時數" - }, - "collab": { - "createRoom": "新房間", - "roomName": "房間名稱", - "persistentRoom": "持久房間", - "persistentRoomHint": "會議結束後,持久會議室仍會保留在清單中,可重複使用。", - "noRooms": "暫無會議室。創建會議室即可開始會議。", - "openRoom": "開啟", - "endRoom": "會議結束", - "leaveRoom": "離開", - "deleteMember": "移除", - "removeMember": "移除 {{name}}", - "removeMemberTitle": "移除此成員?", - "removeMemberDescription": "{{name}} 將立即失去該房間的存取權限。所有正在使用的 SSH 檢視器都將被斷開連接。", - "invite": "邀請", - "inviteTitle": "邀請成員", - "members": "成員", - "membersPanel": "房間成員和控制請求", - "membersWithCount": "成員({{count}})", - "guestsWithCount": "賓客({{count}})", - "removeGuests": "移除客人", - "anonymousGuest": "客人", - "online": "在線", - "offline": "離線", - "you": "(你)", - "hostBadge": "主機", - "presenterBadge": "呈現", - "present": "展示", - "presentTitle": "登上舞台", - "presentHost": "主機", - "searchHosts": "搜尋主機", - "noHostsFound": "未找到可用主機。", - "presentProtocol": "通訊協定", - "stopPresenting": "停止演示", - "takeOver": "接管", - "takeOverConfirmTitle": "替換當前簡報?", - "takeOverDescription": "{{name}} 正在直播中。接管直播將會中斷所有人與該直播間的連結。", - "emptyStage": "目前無人發言。請上台分享您的演講內容。", - "stageLoading": "連接到舞台…", - "stageEnded": "演講結束了", - "roomEnded": "會議結束", - "presenterLabel": "{{name}} 正在呈現", - "guestLink": "訪客連結", - "guestLinkOn": "訪客連結已開啟。任何擁有該連結的人都可以觀看舞台表演。", - "guestLinkOff": "訪客連結已關閉。", - "copyLink": "複製連結", - "rotateLink": "旋轉連結", - "disableLink": "停用連結", - "rotateLinkConfirmTitle": "旋轉訪客連結?", - "rotateLinkDescription": "當前連線將立即停止工作。已連線的SSH訪客將會被斷開連線。", - "disableLinkConfirmTitle": "禁用訪客連結?", - "disableLinkDescription": "匿名 SSH 檢視者將立即被斷開連接,目前連結將停止運作。", - "linkCopied": "連結已複製", - "roles": "角色", - "users": "使用者", - "searchUsers": "搜尋用戶", - "endConfirmTitle": "結束會議嗎?", - "endDescription": "{{name}} 將對所有人關閉,且無法重新開啟。", - "endPersistentDescription": "{{name}} 中的目前示範將停止。該房間仍可供再次使用。", - "deleteRoom": "刪除房間", - "deleteConfirmTitle": "刪除這個房間?", - "deleteDescription": "{{name}} 將會永久刪除,且無法撤銷。", - "invitedTo": "您受邀參加“{{name}}”", - "guest": { - "title": "會議", - "waiting": "等待主持人…", - "linkInvalid": "此訪客連結無效或會議已結束" - }, - "reopenFromPanel": "此會議標籤頁已過期。請從「會議」面板重新開啟會議室。", - "retry": "重試", - "requestControl": "請求控制", - "cancelControlRequest": "取消請求", - "controlQueue": "控制請求", - "noControlRequests": "沒有待處理的請求。", - "grantControlTo": "將控制權交給 {{name}}", - "dismissControlRequest": "駁回 {{name}}的控制請求", - "releaseControl": "發布控制", - "grantControl": "賦予控制權", - "revokeControl": "奪回控制權", - "controlRequestedBy": "{{name}} 請求控制權", - "grant": "授予", - "controlBadge": "掌控之中", - "youArePresenting": "您正在另一個標籤頁中進行示範。請停止演示,將演示權交給對方。", - "created": "創建房間", - "invited": "受邀成員", - "roomTab": "會議", - "serverChanged": "會議伺服器已更改。請從「會議」面板重新開啟此會議。", - "hostNotSynced": "演示前,請將此主機與會議伺服器同步。", - "linkRequiresServer": "將此桌面連接到 Termix 伺服器,並在該伺服器上建立會議以共用 Web 連結。" - }, - "nav": { - "collab": "會議" - }, - "settings": { - "admin": { - "globallyEnabled": { - "label": "允許會話共享", - "description": "允許在整個執行個體範圍內共用即時終端、RDP、VNC 和 Telnet 會話。停用此功能將覆蓋所有主機層級的共用設定。" - } - }, - "host": { - "allowSessionSharing": { - "label": "允許會話共享", - "description": "允許透過連結或其他方式分享此主機上的即時會話。" - } - } - }, - "permissions": { - "use": { - "title": "分享會話並加入房間", - "description": "透過連結或與其他使用者分享即時會話,並建立和加入協作房間。" - } - }, - "hosts": { - "sharing": { - "expiry": { - "never": "永不", - "oneHour": "1 小時", - "oneDay": "24 小時", - "sevenDays": "7 天", - "thirtyDays": "30 天", - "custom": "自訂" - } - } - }, - "connections": { - "sectionSharedWithMe": "與我分享", - "sharedBy": "由 {{username}} 分享", - "join": "加入", - "sharedSessionLabel": "{{hostName}} (共享)" - }, - "plugin": { - "name": "會話共享" - } -} diff --git a/plugins/session-sharing/manifest.json b/plugins/session-sharing/manifest.json deleted file mode 100644 index c80f1764e..000000000 --- a/plugins/session-sharing/manifest.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "id": "session-sharing", - "name": "Session Sharing", - "version": "1.0.0", - "description": "Share a live terminal or remote desktop session by link or with another user, and present sessions in collaboration rooms.", - "author": { - "name": "Termix" - }, - "license": "MIT", - "repository": "https://github.com/Termix-SSH/Termix", - "category": "Productivity", - "icon": "Presentation", - "engine": { - "termix": ">=2.9.0", - "api": "1" - }, - "capabilities": [ - "hosts:read", - "db:own", - "network:serve", - "ui:surface", - "users:impersonate", - "db:core-refs" - ], - "dependencies": { - "ssh-terminal": "^1.0.0" - }, - "optionalDependencies": { - "remote-desktop": "^1.0.0" - }, - "provides": [ - { - "service": "sessions.sharing", - "version": "1.0.0", - "permission": "session-sharing.use" - } - ], - "requires": [ - { - "service": "sessions.live", - "versionRange": "^1.0.0" - } - ], - "contributes": { - "guest": true, - "guestViews": ["shared", "collab-guest"], - "tabs": [ - { - "id": "collab", - "titleKey": "nav.collab", - "icon": "Presentation", - "openFrom": ["rail"] - } - ], - "actions": [ - { - "id": "session-sharing.share", - "titleKey": "sessionSharing.shareButton", - "handler": "share", - "permission": "session-sharing.use" - }, - { - "id": "sessions.sharedWithMe", - "titleKey": "sessionSharing.sharedWithMe", - "handler": "sharedWithMe", - "permission": "session-sharing.use" - } - ], - "actionSlots": [ - { - "id": "session.remoteDisplay", - "accepts": ["component"] - } - ], - "permissions": [ - { - "name": "use", - "titleKey": "permissions.use.title", - "descriptionKey": "permissions.use.description", - "defaultRoles": ["admin", "user"] - } - ], - "settings": { - "admin": [ - { - "key": "globallyEnabled", - "type": "boolean", - "labelKey": "settings.admin.globallyEnabled.label", - "descriptionKey": "settings.admin.globallyEnabled.description", - "default": true - } - ], - "host": { - "editorGroup": "ssh", - "editorOrder": 80, - "fields": [ - { - "key": "allowSessionSharing", - "type": "boolean", - "labelKey": "settings.host.allowSessionSharing.label", - "descriptionKey": "settings.host.allowSessionSharing.description", - "default": true - } - ] - } - }, - "uses": ["session.remoteDisplay.token", "terminal.open", "terminal.overlay"] - }, - "locales": "locales", - "platforms": ["linux", "win32", "darwin"] -} diff --git a/plugins/session-sharing/migrations/mysql/0001_adopt_session_sharing.sql b/plugins/session-sharing/migrations/mysql/0001_adopt_session_sharing.sql deleted file mode 100644 index 5a77df38e..000000000 --- a/plugins/session-sharing/migrations/mysql/0001_adopt_session_sharing.sql +++ /dev/null @@ -1,67 +0,0 @@ --- session-sharing 0001: adopt_session_sharing --- Generated by termix-plugin migrations. Review before committing. --- Hand-edited: keeps the legacy foreign keys the SDK cannot declare --- (nullable or set-null user and host links, and links between these tables). - -CREATE TABLE IF NOT EXISTS `session_shares` ( - `id` varchar(255) PRIMARY KEY NOT NULL, - `host_id` int NOT NULL, - `owner_user_id` varchar(255) NOT NULL, - `protocol` text NOT NULL, - `session_id` varchar(255) NOT NULL, - `tab_instance_id` text, - `share_type` text NOT NULL, - `target_user_id` varchar(255), - `link_token` varchar(255) UNIQUE, - `permission_level` text NOT NULL DEFAULT ('read-only'), - `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - `expires_at` text NOT NULL, - `revoked_at` text, - `last_joined_at` text, - `join_count` int NOT NULL DEFAULT 0, - FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE, - FOREIGN KEY (`owner_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, - FOREIGN KEY (`target_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE -); -RENAME TABLE `session_shares` TO `p_session_sharing_shares`; -CREATE TABLE IF NOT EXISTS `session_share_participants` ( - `id` int AUTO_INCREMENT PRIMARY KEY, - `share_id` varchar(255) NOT NULL, - `user_id` varchar(255), - `guest_label` text, - `joined_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - `left_at` text, - FOREIGN KEY (`share_id`) REFERENCES `p_session_sharing_shares` (`id`) ON DELETE CASCADE, - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE -); -RENAME TABLE `session_share_participants` TO `p_session_sharing_share_participants`; -CREATE TABLE IF NOT EXISTS `collab_rooms` ( - `id` varchar(255) PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `owner_user_id` varchar(255) NOT NULL, - `persistent` boolean NOT NULL DEFAULT false, - `presenter_user_id` varchar(255), - `stage_protocol` text, - `stage_host_id` int, - `stage_share_id` varchar(255), - `guest_link_token` varchar(255), - `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - `ended_at` text, - FOREIGN KEY (`owner_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, - FOREIGN KEY (`presenter_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL, - FOREIGN KEY (`stage_host_id`) REFERENCES `ssh_data` (`id`) ON DELETE SET NULL, - FOREIGN KEY (`stage_share_id`) REFERENCES `p_session_sharing_shares` (`id`) ON DELETE SET NULL -); -RENAME TABLE `collab_rooms` TO `p_session_sharing_rooms`; -CREATE TABLE IF NOT EXISTS `collab_room_members` ( - `id` int AUTO_INCREMENT PRIMARY KEY, - `room_id` varchar(255) NOT NULL, - `user_id` varchar(255) NOT NULL, - `room_role` text NOT NULL DEFAULT ('member'), - `added_by` varchar(255), - `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, - FOREIGN KEY (`room_id`) REFERENCES `p_session_sharing_rooms` (`id`) ON DELETE CASCADE, - FOREIGN KEY (`added_by`) REFERENCES `users` (`id`) ON DELETE SET NULL -); -RENAME TABLE `collab_room_members` TO `p_session_sharing_room_members`; diff --git a/plugins/session-sharing/migrations/postgres/0001_adopt_session_sharing.sql b/plugins/session-sharing/migrations/postgres/0001_adopt_session_sharing.sql deleted file mode 100644 index 038407e9e..000000000 --- a/plugins/session-sharing/migrations/postgres/0001_adopt_session_sharing.sql +++ /dev/null @@ -1,73 +0,0 @@ --- session-sharing 0001: adopt_session_sharing --- Generated by termix-plugin migrations. Review before committing. --- Hand-edited: keeps the legacy foreign keys the SDK cannot declare --- (nullable or set-null user and host links, and links between these tables). - -CREATE TABLE IF NOT EXISTS "session_shares" ( - "id" varchar(255) PRIMARY KEY NOT NULL, - "host_id" integer NOT NULL, - "owner_user_id" varchar(255) NOT NULL, - "protocol" text NOT NULL, - "session_id" varchar(255) NOT NULL, - "tab_instance_id" text, - "share_type" text NOT NULL, - "target_user_id" varchar(255), - "link_token" varchar(255) UNIQUE, - "permission_level" text NOT NULL DEFAULT 'read-only', - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "expires_at" text NOT NULL, - "revoked_at" text, - "last_joined_at" text, - "join_count" integer NOT NULL DEFAULT 0, - FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE, - FOREIGN KEY ("owner_user_id") REFERENCES "users" ("id") ON DELETE CASCADE, - FOREIGN KEY ("target_user_id") REFERENCES "users" ("id") ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS "idx_session_shares_session_id" ON "session_shares" ("session_id"); -CREATE INDEX IF NOT EXISTS "idx_session_shares_host_id" ON "session_shares" ("host_id"); -ALTER TABLE "session_shares" RENAME TO "p_session_sharing_shares"; -CREATE TABLE IF NOT EXISTS "session_share_participants" ( - "id" serial PRIMARY KEY, - "share_id" varchar(255) NOT NULL, - "user_id" varchar(255), - "guest_label" text, - "joined_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "left_at" text, - FOREIGN KEY ("share_id") REFERENCES "p_session_sharing_shares" ("id") ON DELETE CASCADE, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE -); -ALTER TABLE "session_share_participants" RENAME TO "p_session_sharing_share_participants"; -CREATE TABLE IF NOT EXISTS "collab_rooms" ( - "id" varchar(255) PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "owner_user_id" varchar(255) NOT NULL, - "persistent" boolean NOT NULL DEFAULT false, - "presenter_user_id" varchar(255), - "stage_protocol" text, - "stage_host_id" integer, - "stage_share_id" varchar(255), - "guest_link_token" varchar(255), - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "ended_at" text, - FOREIGN KEY ("owner_user_id") REFERENCES "users" ("id") ON DELETE CASCADE, - FOREIGN KEY ("presenter_user_id") REFERENCES "users" ("id") ON DELETE SET NULL, - FOREIGN KEY ("stage_host_id") REFERENCES "ssh_data" ("id") ON DELETE SET NULL, - FOREIGN KEY ("stage_share_id") REFERENCES "p_session_sharing_shares" ("id") ON DELETE SET NULL -); -CREATE UNIQUE INDEX IF NOT EXISTS "idx_collab_rooms_guest_token" ON "collab_rooms" ("guest_link_token"); -CREATE INDEX IF NOT EXISTS "idx_collab_rooms_owner" ON "collab_rooms" ("owner_user_id"); -ALTER TABLE "collab_rooms" RENAME TO "p_session_sharing_rooms"; -CREATE TABLE IF NOT EXISTS "collab_room_members" ( - "id" serial PRIMARY KEY, - "room_id" varchar(255) NOT NULL, - "user_id" varchar(255) NOT NULL, - "room_role" text NOT NULL DEFAULT 'member', - "added_by" varchar(255), - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, - FOREIGN KEY ("room_id") REFERENCES "p_session_sharing_rooms" ("id") ON DELETE CASCADE, - FOREIGN KEY ("added_by") REFERENCES "users" ("id") ON DELETE SET NULL -); -CREATE UNIQUE INDEX IF NOT EXISTS "idx_collab_room_members_room_user" ON "collab_room_members" ("room_id", "user_id"); -CREATE INDEX IF NOT EXISTS "idx_collab_room_members_user" ON "collab_room_members" ("user_id"); -ALTER TABLE "collab_room_members" RENAME TO "p_session_sharing_room_members"; diff --git a/plugins/session-sharing/migrations/snapshot.json b/plugins/session-sharing/migrations/snapshot.json deleted file mode 100644 index 4341c1fa0..000000000 --- a/plugins/session-sharing/migrations/snapshot.json +++ /dev/null @@ -1,223 +0,0 @@ -{ - "version": 1, - "tables": { - "shares": { - "columns": { - "id": { - "type": "varchar", - "length": 255, - "primaryKey": true, - "notNull": true - }, - "hostId": { - "type": "refHost", - "notNull": true - }, - "ownerUserId": { - "type": "refUser", - "length": 255, - "notNull": true - }, - "protocol": { - "type": "text", - "notNull": true - }, - "sessionId": { - "type": "varchar", - "length": 255, - "notNull": true - }, - "tabInstanceId": { - "type": "text" - }, - "shareType": { - "type": "text", - "notNull": true - }, - "targetUserId": { - "type": "varchar", - "length": 255 - }, - "linkToken": { - "type": "varchar", - "length": 255, - "unique": true - }, - "permissionLevel": { - "type": "text", - "notNull": true, - "defaultValue": "read-only" - }, - "createdAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - }, - "expiresAt": { - "type": "text", - "notNull": true - }, - "revokedAt": { - "type": "text" - }, - "lastJoinedAt": { - "type": "text" - }, - "joinCount": { - "type": "integer", - "notNull": true, - "defaultValue": 0 - } - }, - "indexes": [ - { - "name": "idx_session_shares_session_id", - "columns": ["sessionId"] - }, - { - "name": "idx_session_shares_host_id", - "columns": ["hostId"] - } - ], - "adopts": "session_shares" - }, - "share_participants": { - "columns": { - "id": { - "type": "id", - "primaryKey": true, - "notNull": true - }, - "shareId": { - "type": "varchar", - "length": 255, - "notNull": true - }, - "userId": { - "type": "varchar", - "length": 255 - }, - "guestLabel": { - "type": "text" - }, - "joinedAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - }, - "leftAt": { - "type": "text" - } - }, - "indexes": [], - "adopts": "session_share_participants" - }, - "rooms": { - "columns": { - "id": { - "type": "varchar", - "length": 255, - "primaryKey": true, - "notNull": true - }, - "name": { - "type": "text", - "notNull": true - }, - "ownerUserId": { - "type": "refUser", - "length": 255, - "notNull": true - }, - "persistent": { - "type": "boolean", - "notNull": true, - "defaultValue": false - }, - "presenterUserId": { - "type": "varchar", - "length": 255 - }, - "stageProtocol": { - "type": "text" - }, - "stageHostId": { - "type": "integer" - }, - "stageShareId": { - "type": "varchar", - "length": 255 - }, - "guestLinkToken": { - "type": "varchar", - "length": 255 - }, - "createdAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - }, - "endedAt": { - "type": "text" - } - }, - "indexes": [ - { - "name": "idx_collab_rooms_guest_token", - "columns": ["guestLinkToken"], - "unique": true - }, - { - "name": "idx_collab_rooms_owner", - "columns": ["ownerUserId"] - } - ], - "adopts": "collab_rooms" - }, - "room_members": { - "columns": { - "id": { - "type": "id", - "primaryKey": true, - "notNull": true - }, - "roomId": { - "type": "varchar", - "length": 255, - "notNull": true - }, - "userId": { - "type": "refUser", - "length": 255, - "notNull": true - }, - "roomRole": { - "type": "text", - "notNull": true, - "defaultValue": "member" - }, - "addedBy": { - "type": "varchar", - "length": 255 - }, - "createdAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - } - }, - "indexes": [ - { - "name": "idx_collab_room_members_room_user", - "columns": ["roomId", "userId"], - "unique": true - }, - { - "name": "idx_collab_room_members_user", - "columns": ["userId"] - } - ], - "adopts": "collab_room_members" - } - } -} diff --git a/plugins/session-sharing/migrations/sqlite/0001_adopt_session_sharing.sql b/plugins/session-sharing/migrations/sqlite/0001_adopt_session_sharing.sql deleted file mode 100644 index 45947f678..000000000 --- a/plugins/session-sharing/migrations/sqlite/0001_adopt_session_sharing.sql +++ /dev/null @@ -1,73 +0,0 @@ --- session-sharing 0001: adopt_session_sharing --- Generated by termix-plugin migrations. Review before committing. --- Hand-edited: keeps the legacy foreign keys the SDK cannot declare --- (nullable or set-null user and host links, and links between these tables). - -CREATE TABLE IF NOT EXISTS "session_shares" ( - "id" text PRIMARY KEY NOT NULL, - "host_id" integer NOT NULL, - "owner_user_id" text NOT NULL, - "protocol" text NOT NULL, - "session_id" text NOT NULL, - "tab_instance_id" text, - "share_type" text NOT NULL, - "target_user_id" text, - "link_token" text UNIQUE, - "permission_level" text NOT NULL DEFAULT 'read-only', - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "expires_at" text NOT NULL, - "revoked_at" text, - "last_joined_at" text, - "join_count" integer NOT NULL DEFAULT 0, - FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE, - FOREIGN KEY ("owner_user_id") REFERENCES "users" ("id") ON DELETE CASCADE, - FOREIGN KEY ("target_user_id") REFERENCES "users" ("id") ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS "idx_session_shares_session_id" ON "session_shares" ("session_id"); -CREATE INDEX IF NOT EXISTS "idx_session_shares_host_id" ON "session_shares" ("host_id"); -ALTER TABLE "session_shares" RENAME TO "p_session_sharing_shares"; -CREATE TABLE IF NOT EXISTS "session_share_participants" ( - "id" integer PRIMARY KEY AUTOINCREMENT, - "share_id" text NOT NULL, - "user_id" text, - "guest_label" text, - "joined_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "left_at" text, - FOREIGN KEY ("share_id") REFERENCES "p_session_sharing_shares" ("id") ON DELETE CASCADE, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE -); -ALTER TABLE "session_share_participants" RENAME TO "p_session_sharing_share_participants"; -CREATE TABLE IF NOT EXISTS "collab_rooms" ( - "id" text PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "owner_user_id" text NOT NULL, - "persistent" integer NOT NULL DEFAULT 0, - "presenter_user_id" text, - "stage_protocol" text, - "stage_host_id" integer, - "stage_share_id" text, - "guest_link_token" text, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "ended_at" text, - FOREIGN KEY ("owner_user_id") REFERENCES "users" ("id") ON DELETE CASCADE, - FOREIGN KEY ("presenter_user_id") REFERENCES "users" ("id") ON DELETE SET NULL, - FOREIGN KEY ("stage_host_id") REFERENCES "ssh_data" ("id") ON DELETE SET NULL, - FOREIGN KEY ("stage_share_id") REFERENCES "p_session_sharing_shares" ("id") ON DELETE SET NULL -); -CREATE UNIQUE INDEX IF NOT EXISTS "idx_collab_rooms_guest_token" ON "collab_rooms" ("guest_link_token"); -CREATE INDEX IF NOT EXISTS "idx_collab_rooms_owner" ON "collab_rooms" ("owner_user_id"); -ALTER TABLE "collab_rooms" RENAME TO "p_session_sharing_rooms"; -CREATE TABLE IF NOT EXISTS "collab_room_members" ( - "id" integer PRIMARY KEY AUTOINCREMENT, - "room_id" text NOT NULL, - "user_id" text NOT NULL, - "room_role" text NOT NULL DEFAULT 'member', - "added_by" text, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, - FOREIGN KEY ("room_id") REFERENCES "p_session_sharing_rooms" ("id") ON DELETE CASCADE, - FOREIGN KEY ("added_by") REFERENCES "users" ("id") ON DELETE SET NULL -); -CREATE UNIQUE INDEX IF NOT EXISTS "idx_collab_room_members_room_user" ON "collab_room_members" ("room_id", "user_id"); -CREATE INDEX IF NOT EXISTS "idx_collab_room_members_user" ON "collab_room_members" ("user_id"); -ALTER TABLE "collab_room_members" RENAME TO "p_session_sharing_room_members"; diff --git a/plugins/session-sharing/package.json b/plugins/session-sharing/package.json deleted file mode 100644 index 8f6971a04..000000000 --- a/plugins/session-sharing/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@termix-plugin/session-sharing", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "build": "termix-plugin build", - "test": "termix-plugin test", - "typecheck": "tsc -p .", - "validate": "termix-plugin validate" - }, - "dependencies": { - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", - "lucide-react": "^1.33.0", - "react-xtermjs": "^1.0.10", - "redis": "^6.2.1" - }, - "devDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "@testing-library/dom": "^10.4.2", - "@testing-library/react": "^16.3.3", - "@types/node": "^26.2.0", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", - "better-sqlite3": "^13.0.3", - "jsdom": "^30.0.1", - "vitest": "^4.1.11" - }, - "peerDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "drizzle-orm": "^0.45.2", - "express": "^5.2.1", - "react": "^19.2.8", - "sonner": "^2.0.8", - "ws": "^8.21.3" - }, - "termix": { - "patches": [ - "xterm-android-ime" - ] - }, - "overrides": { - "react-xtermjs": { - "@xterm/xterm": "$@xterm/xterm" - } - } -} diff --git a/plugins/session-sharing/src/backend/deps.ts b/plugins/session-sharing/src/backend/deps.ts deleted file mode 100644 index d05d347e9..000000000 --- a/plugins/session-sharing/src/backend/deps.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import type { LiveSessions } from "./live.js"; -import type { RateLimiter } from "./rate-limit.js"; -import type { - Directory, - RoomRepository, - ShareRepository, -} from "./repositories.js"; -import type { CollabRoomHub } from "./room-hub.js"; -import type { CollabRuntimeStore } from "./runtime-store.js"; - -/** Everything the routes and services share, built once in activate. */ -export interface SharingDeps { - ctx: PluginContext; - shares: ShareRepository; - rooms: RoomRepository; - directory: Directory; - live: LiveSessions; - hub: CollabRoomHub; - store: CollabRuntimeStore; - /** /resolve/:linkToken, 30 a minute per IP. */ - resolveLimiter: RateLimiter; - /** Room guest links, 60 a minute per IP. */ - guestLimiter: RateLimiter; - isSharingEnabledForHost: (hostId: number) => Promise; -} - -export const PROTOCOLS = ["ssh", "rdp", "vnc", "telnet"] as const; - -/** Where SSH shares are joined: the terminal's own socket. */ -export const TERMINAL_WS_PATH = "/plugin-ws/ssh-terminal/terminal"; -/** Where remote desktop viewers connect. */ -export const DISPLAY_WS_PATH = "/plugin-ws/remote-desktop/display"; - -export function clientIp(req: { - ip?: string; - socket?: { remoteAddress?: string }; -}): string { - return req.ip || req.socket?.remoteAddress || "unknown"; -} - -export function isNonEmptyString(value: unknown): value is string { - return typeof value === "string" && value.trim().length > 0; -} - -/** The acting user, which core set from the authenticated request. */ -export function actorOf(ctx: PluginContext): string { - const userId = ctx.currentActor(); - if (!userId) throw new Error("No acting user"); - return userId; -} diff --git a/plugins/session-sharing/src/backend/index.ts b/plugins/session-sharing/src/backend/index.ts deleted file mode 100644 index 37f012c8c..000000000 --- a/plugins/session-sharing/src/backend/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { Router } from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import type { SharingDeps } from "./deps.js"; -import { createLiveSessions, isSharingEnabledForHost } from "./live.js"; -import { createRateLimiter } from "./rate-limit.js"; -import { - createDirectory, - createRoomRepository, - createShareRepository, - type CoreRefs, - type Tables, -} from "./repositories.js"; -import { CollabRoomHub } from "./room-hub.js"; -import { createRoomActions, registerRoomRoutes } from "./room-routes.js"; -import { CollabRuntimeStore } from "./runtime-store.js"; -import { - SESSION_GUESTS_KEY, - createSessionGuests, - createSessionSharingService, -} from "./services.js"; -import { registerShareRoutes } from "./share-routes.js"; -import { roomMembers, rooms, shareParticipants, shares } from "./tables.js"; - -export async function activate(ctx: PluginContext) { - const tables: Tables = { - shares: await ctx.db.define(shares), - participants: await ctx.db.define(shareParticipants), - rooms: await ctx.db.define(rooms), - members: await ctx.db.define(roomMembers), - }; - const refs = () => ctx.db.refs(); - - const directory = createDirectory(ctx.db, refs); - const store = new CollabRuntimeStore(ctx.log); - const hub = new CollabRoomHub(store); - const resolveLimiter = createRateLimiter({ - windowMs: 60_000, - maxAttempts: 30, - }); - const guestLimiter = createRateLimiter({ windowMs: 60_000, maxAttempts: 60 }); - ctx.disposables.add(() => { - hub.dispose(); - resolveLimiter.dispose(); - guestLimiter.dispose(); - return store.close(); - }); - - const deps: SharingDeps = { - ctx, - shares: createShareRepository(ctx.db, tables), - rooms: createRoomRepository(ctx.db, tables, refs), - directory, - live: createLiveSessions(ctx), - hub, - store, - resolveLimiter, - guestLimiter, - isSharingEnabledForHost: (hostId) => - isSharingEnabledForHost(ctx, directory.hostExists, hostId), - }; - - const actions = createRoomActions(deps); - store.onEvent(actions.onRuntimeEvent); - - // Guest links carry their own token, checked in the handlers. - const router = ctx.http.router({ - public: ["/resolve/:linkToken", "/guest/:token"], - }); - registerShareRoutes(router, deps); - registerRoomRoutes(router, deps, actions); - - ctx.services.provide("sessions.sharing", createSessionSharingService(deps)); - ctx.registry.provide(SESSION_GUESTS_KEY, createSessionGuests(deps)); - - ctx.log.info("Session sharing mounted at /plugin-api/session-sharing"); -} - -export async function deactivate() { - // Everything above was registered through ctx and is disposed by core. -} diff --git a/plugins/session-sharing/src/backend/live.ts b/plugins/session-sharing/src/backend/live.ts deleted file mode 100644 index bf6eb8417..000000000 --- a/plugins/session-sharing/src/backend/live.ts +++ /dev/null @@ -1,205 +0,0 @@ -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import type { LiveProtocol } from "./repositories.js"; - -export const LIVE_SESSIONS_SERVICE = "sessions.live"; - -/** What every sessions.live provider reports about one session. */ -export interface LiveSessionInfo { - id: string; - userId: string; - hostId: number; - hostName: string; - isConnected: boolean; - createdAt: number; - tabInstanceId: string | null; -} - -/** - * sessions.live v1, keyed by session type. ssh-terminal provides "ssh"; - * remote-desktop provides "rdp", "vnc" and "telnet". Through a service handle - * every method answers with a promise. The control methods are the terminal's - * (a remote desktop viewer cannot be kicked or handed control), and - * createViewerToken is remote desktop's: a join token for a viewer. - */ -export interface LiveSessionProvider { - getSession: (sessionId: string) => LiveSessionInfo | null; - ownerEndSession?: (sessionId: string, reason: string) => void; - disconnectParticipants?: ( - sessionId: string, - shareId: string, - options: { reason: string; userId?: string | null }, - ) => number; - setRoomShareControl?: ( - sessionId: string, - shareId: string, - controllerUserId: string | null, - ) => void; - listGuests?: ( - sessionId: string, - shareId: string, - ) => { label: string | null }[]; - createViewerToken?: (sessionId: string, readOnly: boolean) => string; -} - -type Async = { - [K in keyof T]-?: T[K] extends ((...args: infer A) => infer R) | undefined - ? (...args: A) => Promise - : never; -}; - -export type LiveSessions = ReturnType; - -/** - * Reaches live sessions through sessions.live as the acting user. A caller - * with no actor (a public guest route) passes the share owner, who owns the - * session being looked at. - */ -export function createLiveSessions(ctx: PluginContext) { - async function run( - asUser: string | undefined, - fn: () => Promise, - ): Promise { - if (asUser && ctx.currentActor() !== asUser) { - return ctx.asUser(asUser, fn); - } - return fn(); - } - - function provider(protocol: string): Async | null { - const handle = ctx.services.get>( - LIVE_SESSIONS_SERVICE, - { provider: protocol }, - ); - return "getSession" in handle ? handle : null; - } - - async function getSession( - protocol: string, - sessionId: string, - asUser?: string, - ): Promise { - const live = provider(protocol); - if (!live) return null; - try { - return (await run(asUser, () => live.getSession(sessionId))) ?? null; - } catch { - return null; - } - } - - return { - getSession, - - async isLive( - protocol: string, - sessionId: string, - asUser?: string, - ): Promise { - const session = await getSession(protocol, sessionId, asUser); - return !!session?.isConnected; - }, - - async isOwnedBy( - protocol: LiveProtocol, - sessionId: string, - userId: string, - ): Promise { - const session = await getSession(protocol, sessionId); - return !!session && session.isConnected && session.userId === userId; - }, - - /** Anonymous link guests on one share; empty when the type cannot tell. */ - async listGuests( - protocol: string, - sessionId: string, - shareId: string, - asUser?: string, - ): Promise<{ label: string | null }[]> { - const live = provider(protocol); - if (!live || !("listGuests" in live)) return []; - try { - return ( - (await run(asUser, () => live.listGuests(sessionId, shareId))) ?? [] - ); - } catch { - return []; - } - }, - - /** Ends the session for every guest. Best effort, like before. */ - async ownerEndSession( - protocol: string, - sessionId: string, - reason: string, - asUser?: string, - ): Promise { - const live = provider(protocol); - if (!live || !("ownerEndSession" in live)) return; - await run(asUser, () => live.ownerEndSession(sessionId, reason)).catch( - () => {}, - ); - }, - - async disconnectParticipants( - protocol: string, - sessionId: string, - shareId: string, - options: { reason: string; userId?: string | null }, - asUser?: string, - ): Promise { - const live = provider(protocol); - if (!live || !("disconnectParticipants" in live)) return; - await run(asUser, () => - live.disconnectParticipants(sessionId, shareId, options), - ).catch(() => {}); - }, - - async setRoomShareControl( - protocol: string, - sessionId: string, - shareId: string, - controllerUserId: string | null, - asUser?: string, - ): Promise { - const live = provider(protocol); - if (!live || !("setRoomShareControl" in live)) return; - await run(asUser, () => - live.setRoomShareControl(sessionId, shareId, controllerUserId), - ).catch(() => {}); - }, - - /** A viewer token for a remote desktop session. Throws while unavailable. */ - async createViewerToken( - protocol: string, - sessionId: string, - readOnly: boolean, - asUser?: string, - ): Promise { - const live = provider(protocol); - if (!live || !("createViewerToken" in live)) { - throw new Error(`No ${protocol} session provider is running`); - } - return run(asUser, () => live.createViewerToken(sessionId, readOnly)); - }, - }; -} - -/** - * Whether sessions on a host may be shared: the admin switch first, then the - * host's own setting. Both default to on. A host that does not exist cannot - * be shared. - */ -export async function isSharingEnabledForHost( - ctx: PluginContext, - hostExists: (hostId: number) => Promise, - hostId: number, -): Promise { - if ((await ctx.settings.get("globallyEnabled")) === false) { - return false; - } - if (!(await hostExists(hostId))) return false; - return ( - (await ctx.settings.getHost(hostId, "allowSessionSharing")) !== - false - ); -} diff --git a/plugins/session-sharing/src/backend/rate-limit.ts b/plugins/session-sharing/src/backend/rate-limit.ts deleted file mode 100644 index 2994ba6d3..000000000 --- a/plugins/session-sharing/src/backend/rate-limit.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * A per-IP fixed window, for the public guest endpoints. `dispose` stops the - * sweep that forgets old windows. - */ -export function createRateLimiter(options: { - windowMs: number; - maxAttempts: number; -}) { - const attempts = new Map(); - const sweep = setInterval(() => { - const now = Date.now(); - for (const [ip, entry] of attempts) { - if (now - entry.windowStart > options.windowMs) attempts.delete(ip); - } - }, 5 * options.windowMs); - sweep.unref(); - - return { - /** Counts an attempt and says whether this IP is over the limit. */ - isLimited(ip: string): boolean { - const now = Date.now(); - const entry = attempts.get(ip); - if (!entry || now - entry.windowStart > options.windowMs) { - attempts.set(ip, { count: 1, windowStart: now }); - return false; - } - entry.count += 1; - return entry.count > options.maxAttempts; - }, - dispose(): void { - clearInterval(sweep); - attempts.clear(); - }, - }; -} - -export type RateLimiter = ReturnType; diff --git a/plugins/session-sharing/src/backend/repositories.ts b/plugins/session-sharing/src/backend/repositories.ts deleted file mode 100644 index 5987b736b..000000000 --- a/plugins/session-sharing/src/backend/repositories.ts +++ /dev/null @@ -1,591 +0,0 @@ -import { and, desc, eq, gt, inArray, isNull } from "drizzle-orm"; -import type { PluginDatabase } from "@termix/plugin-sdk/backend"; - -/* eslint-disable @typescript-eslint/no-explicit-any */ -// Tables come from ctx.db.define, which the SDK hands back untyped, and the -// drizzle handle is the server's own. Typed at this module's edge instead. -type Table = any; -type Drizzle = any; -/* eslint-enable @typescript-eslint/no-explicit-any */ - -export type ShareType = "link" | "user" | "room"; -export type PermissionLevel = "read-only" | "read-write"; -export type LiveProtocol = "ssh" | "rdp" | "vnc" | "telnet"; - -export interface ShareRecord { - id: string; - hostId: number; - ownerUserId: string; - protocol: string; - sessionId: string; - tabInstanceId: string | null; - shareType: string; - targetUserId: string | null; - linkToken: string | null; - permissionLevel: string; - createdAt: string; - expiresAt: string; - revokedAt: string | null; - lastJoinedAt: string | null; - joinCount: number; -} - -export interface ShareCreateInput { - id: string; - hostId: number; - ownerUserId: string; - protocol: string; - sessionId: string; - tabInstanceId?: string | null; - shareType: ShareType; - targetUserId?: string | null; - linkToken?: string | null; - permissionLevel: PermissionLevel; - expiresAt: string; -} - -export interface RoomRecord { - id: string; - name: string; - ownerUserId: string; - persistent: boolean; - presenterUserId: string | null; - stageProtocol: string | null; - stageHostId: number | null; - stageShareId: string | null; - guestLinkToken: string | null; - createdAt: string; - endedAt: string | null; -} - -export interface RoomMemberRecord { - id: number; - roomId: string; - userId: string; - roomRole: string; - addedBy: string | null; - createdAt: string; -} - -export interface RoomMemberWithUser { - userId: string; - username: string; - roomRole: string; - createdAt: string; -} - -export interface RoomStage { - presenterUserId: string | null; - stageProtocol: string | null; - stageHostId: number | null; - stageShareId: string | null; -} - -export interface Tables { - shares: Table; - participants: Table; - rooms: Table; - members: Table; -} - -export interface CoreRefs { - users: Table; - hosts: Table; - roles: Table; - userRoles: Table; -} - -const nowIso = () => new Date().toISOString(); - -/** Not revoked and not expired. Expiry is compared as ISO text, as before. */ -function activeShare(shares: Table, now: string) { - return and(isNull(shares.revokedAt), gt(shares.expiresAt, now)); -} - -export type ShareRepository = ReturnType; - -export function createShareRepository(db: PluginDatabase, tables: Tables) { - const client = () => db.client(); - const { shares, participants } = tables; - - async function findById(id: string): Promise { - const rows = await ( - await client() - ) - .select() - .from(shares) - .where(eq(shares.id, id)) - .limit(1); - return rows[0] ?? null; - } - - return { - findById, - - async create(input: ShareCreateInput): Promise { - await (await client()).insert(shares).values({ - id: input.id, - hostId: input.hostId, - ownerUserId: input.ownerUserId, - protocol: input.protocol, - sessionId: input.sessionId, - tabInstanceId: input.tabInstanceId ?? null, - shareType: input.shareType, - targetUserId: input.targetUserId ?? null, - linkToken: input.linkToken ?? null, - permissionLevel: input.permissionLevel, - expiresAt: input.expiresAt, - }); - await db.persist(); - return (await findById(input.id))!; - }, - - async findActiveById( - id: string, - now = nowIso(), - ): Promise { - const rows = await ( - await client() - ) - .select() - .from(shares) - .where(and(eq(shares.id, id), activeShare(shares, now))) - .limit(1); - return rows[0] ?? null; - }, - - async findByLinkToken( - linkToken: string, - now = nowIso(), - ): Promise { - const rows = await ( - await client() - ) - .select() - .from(shares) - .where(and(eq(shares.linkToken, linkToken), activeShare(shares, now))) - .limit(1); - return rows[0] ?? null; - }, - - async findActiveSharesForHost( - hostId: number, - ownerUserId: string, - now = nowIso(), - ): Promise { - return (await client()) - .select() - .from(shares) - .where( - and( - eq(shares.hostId, hostId), - eq(shares.ownerUserId, ownerUserId), - activeShare(shares, now), - ), - ); - }, - - /** Active user shares aimed at this user. */ - async findSharesTargetingUser( - userId: string, - now = nowIso(), - ): Promise { - return (await client()) - .select() - .from(shares) - .where( - and( - eq(shares.shareType, "user"), - eq(shares.targetUserId, userId), - activeShare(shares, now), - ), - ); - }, - - /** Revokes a share the given user owns. False when it is not theirs. */ - async revoke(shareId: string, ownerUserId: string): Promise { - const share = await findById(shareId); - if (!share || share.ownerUserId !== ownerUserId) return false; - await ( - await client() - ) - .update(shares) - .set({ revokedAt: nowIso() }) - .where(eq(shares.id, shareId)); - await db.persist(); - return true; - }, - - async revokeAny(shareId: string): Promise { - if (!(await findById(shareId))) return false; - await ( - await client() - ) - .update(shares) - .set({ revokedAt: nowIso() }) - .where(eq(shares.id, shareId)); - await db.persist(); - return true; - }, - - async touchUsage(shareId: string): Promise { - const current = await findById(shareId); - if (!current) return; - await ( - await client() - ) - .update(shares) - .set({ - lastJoinedAt: nowIso(), - joinCount: (current.joinCount ?? 0) + 1, - }) - .where(eq(shares.id, shareId)); - await db.persist(); - }, - - async recordParticipantJoin( - shareId: string, - userId: string | null, - guestLabel: string | null, - ): Promise { - await ( - await client() - ) - .insert(participants) - .values({ shareId, userId, guestLabel }); - await db.persist(); - }, - }; -} - -export type RoomRepository = ReturnType; - -export function createRoomRepository( - db: PluginDatabase, - tables: Tables, - refs: () => Promise, -) { - const client = () => db.client(); - const { rooms, members } = tables; - - async function findById(id: string): Promise { - const rows = await ( - await client() - ) - .select() - .from(rooms) - .where(eq(rooms.id, id)) - .limit(1); - return rows[0] ?? null; - } - - async function findMember( - roomId: string, - userId: string, - ): Promise { - const rows = await ( - await client() - ) - .select() - .from(members) - .where(and(eq(members.roomId, roomId), eq(members.userId, userId))) - .limit(1); - return rows[0] ?? null; - } - - async function updateStage(roomId: string, stage: RoomStage): Promise { - await (await client()).update(rooms).set(stage).where(eq(rooms.id, roomId)); - await db.persist(); - } - - return { - findById, - findMember, - updateStage, - - async createRoom(input: { - id: string; - name: string; - ownerUserId: string; - persistent: boolean; - }): Promise { - await (await client()).insert(rooms).values(input); - await db.persist(); - return (await findById(input.id))!; - }, - - /** The live room whose stage points at this share, if any. */ - async findByStageShareId(shareId: string): Promise { - const rows = await ( - await client() - ) - .select() - .from(rooms) - .where(and(eq(rooms.stageShareId, shareId), isNull(rooms.endedAt))) - .limit(1); - return rows[0] ?? null; - }, - - async findByGuestToken(token: string): Promise { - const rows = await ( - await client() - ) - .select() - .from(rooms) - .where(and(eq(rooms.guestLinkToken, token), isNull(rooms.endedAt))) - .limit(1); - return rows[0] ?? null; - }, - - async setGuestToken(roomId: string, token: string | null): Promise { - await ( - await client() - ) - .update(rooms) - .set({ guestLinkToken: token }) - .where(eq(rooms.id, roomId)); - await db.persist(); - }, - - async listForUser(userId: string): Promise { - const rows = await ( - await client() - ) - .select({ room: rooms }) - .from(members) - .innerJoin(rooms, eq(members.roomId, rooms.id)) - .where(and(eq(members.userId, userId), isNull(rooms.endedAt))) - .orderBy(desc(rooms.createdAt)); - return rows.map((row: { room: RoomRecord }) => row.room); - }, - - async addMember(input: { - roomId: string; - userId: string; - roomRole: "host" | "member"; - addedBy: string | null; - }): Promise { - if (await findMember(input.roomId, input.userId)) return false; - await (await client()).insert(members).values(input); - await db.persist(); - return true; - }, - - async removeMember(roomId: string, userId: string): Promise { - await ( - await client() - ) - .delete(members) - .where(and(eq(members.roomId, roomId), eq(members.userId, userId))); - await db.persist(); - }, - - async listMembers(roomId: string): Promise { - const rows: RoomMemberRecord[] = await ( - await client() - ) - .select() - .from(members) - .where(eq(members.roomId, roomId)); - const names = await usernames(rows.map((row) => row.userId)); - return rows - .filter((row) => names.has(row.userId)) - .map((row) => ({ - userId: row.userId, - username: names.get(row.userId)!, - roomRole: row.roomRole, - createdAt: row.createdAt, - })) - .sort((a, b) => a.username.localeCompare(b.username)); - }, - - /** - * Sets the stage only if it still points at `expectedShareId`, so two - * members taking the stage at once cannot both win. - */ - async replaceStage( - roomId: string, - expectedShareId: string | null, - stage: RoomStage, - ): Promise { - await ( - await client() - ) - .update(rooms) - .set(stage) - .where( - and( - eq(rooms.id, roomId), - expectedShareId - ? eq(rooms.stageShareId, expectedShareId) - : isNull(rooms.stageShareId), - ), - ); - const after = await findById(roomId); - const changed = !!after && after.stageShareId === stage.stageShareId; - if (changed) await db.persist(); - return changed; - }, - - async clearStage(roomId: string): Promise { - await updateStage(roomId, { - presenterUserId: null, - stageProtocol: null, - stageHostId: null, - stageShareId: null, - }); - }, - - async endRoom(roomId: string): Promise { - await ( - await client() - ) - .update(rooms) - .set({ - endedAt: nowIso(), - presenterUserId: null, - stageProtocol: null, - stageHostId: null, - stageShareId: null, - }) - .where(eq(rooms.id, roomId)); - await db.persist(); - }, - - async deleteRoom(roomId: string): Promise { - const drizzle = await client(); - // Not left to the foreign key: SQLite only cascades with foreign_keys on. - await drizzle.delete(members).where(eq(members.roomId, roomId)); - await drizzle.delete(rooms).where(eq(rooms.id, roomId)); - await db.persist(); - }, - }; - - async function usernames(ids: string[]): Promise> { - if (ids.length === 0) return new Map(); - const { users } = await refs(); - const rows: Array<{ id: string; username: string }> = await ( - await client() - ) - .select({ id: users.id, username: users.username }) - .from(users) - .where(inArray(users.id, ids)); - return new Map(rows.map((row) => [row.id, row.username])); - } -} - -export type Directory = ReturnType; - -/** Users and roles, read through ctx.db.refs, for names and invites. */ -export function createDirectory( - db: PluginDatabase, - refs: () => Promise, -) { - const client = () => db.client(); - - return { - async username(userId: string): Promise { - const { users } = await refs(); - const rows = await ( - await client() - ) - .select({ username: users.username }) - .from(users) - .where(eq(users.id, userId)) - .limit(1); - return rows[0]?.username ?? userId; - }, - - async userExists(userId: string): Promise { - const { users } = await refs(); - const rows = await ( - await client() - ) - .select({ id: users.id }) - .from(users) - .where(eq(users.id, userId)) - .limit(1); - return rows.length > 0; - }, - - async isAdmin(userId: string): Promise { - const { users } = await refs(); - const rows = await ( - await client() - ) - .select({ isAdmin: users.isAdmin }) - .from(users) - .where(eq(users.id, userId)) - .limit(1); - return !!rows[0]?.isAdmin; - }, - - async hostIdsBySyncId(syncId: string): Promise { - const { hosts } = await refs(); - const rows = await ( - await client() - ) - .select({ id: hosts.id }) - .from(hosts) - .where(eq(hosts.syncId, syncId)); - return rows.map((row: { id: number }) => row.id); - }, - - async hostExists(hostId: number): Promise { - const { hosts } = await refs(); - const rows = await ( - await client() - ) - .select({ id: hosts.id }) - .from(hosts) - .where(eq(hosts.id, hostId)) - .limit(1); - return rows.length > 0; - }, - - async roleExists(roleId: number): Promise { - const { roles } = await refs(); - const rows = await ( - await client() - ) - .select({ id: roles.id }) - .from(roles) - .where(eq(roles.id, roleId)) - .limit(1); - return rows.length > 0; - }, - - async roleUserIds(roleId: number): Promise { - const { userRoles } = await refs(); - const rows: Array<{ userId: string }> = await ( - await client() - ) - .select({ userId: userRoles.userId }) - .from(userRoles) - .where(eq(userRoles.roleId, roleId)); - return rows.map((row) => row.userId); - }, - - async listUsers(): Promise> { - const { users } = await refs(); - return (await client()) - .select({ id: users.id, username: users.username }) - .from(users) - .orderBy(users.username); - }, - - async listRoles(): Promise< - Array<{ id: number; name: string; displayName: string | null }> - > { - const { roles } = await refs(); - return (await client()) - .select({ - id: roles.id, - name: roles.name, - displayName: roles.displayName, - }) - .from(roles) - .orderBy(roles.name); - }, - }; -} diff --git a/plugins/session-sharing/src/backend/room-hub.ts b/plugins/session-sharing/src/backend/room-hub.ts deleted file mode 100644 index 76f339e00..000000000 --- a/plugins/session-sharing/src/backend/room-hub.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { WebSocket } from "ws"; -import type { CollabRuntimeStore } from "./runtime-store.js"; - -const PRESENCE_HEARTBEAT_MS = 15_000; - -export interface CollabRoomClient { - ws: WebSocket; - userId: string; - username: string; -} - -/** - * Local WebSocket fan-out plus the runtime store's pub/sub for room events and - * presence across instances. The terminal socket feeds subscribe and - * unsubscribe through the sessions.sharing service. - */ -export class CollabRoomHub { - private rooms = new Map>(); - private readonly heartbeat: NodeJS.Timeout; - - constructor(private readonly store: CollabRuntimeStore) { - store.onEvent((roomId, message) => { - if ( - "type" in message && - typeof message.type === "string" && - message.type.startsWith("collab_internal_") - ) { - return; - } - this.broadcastLocal(roomId, message); - }); - this.heartbeat = setInterval(() => { - for (const roomId of this.rooms.keys()) void this.refreshPresence(roomId); - }, PRESENCE_HEARTBEAT_MS); - this.heartbeat.unref(); - } - - subscribe(roomId: string, client: CollabRoomClient): void { - let clients = this.rooms.get(roomId); - if (!clients) { - clients = new Set(); - this.rooms.set(roomId, clients); - } - for (const existing of clients) { - if (existing.ws === client.ws) return; - } - clients.add(client); - void this.refreshPresence(roomId); - } - - /** Drops the socket from one room, or from every room when roomId is omitted. */ - unsubscribe(ws: WebSocket, roomId?: string): void { - for (const [id, clients] of this.rooms) { - if (roomId && id !== roomId) continue; - let removed = false; - for (const client of clients) { - if (client.ws === ws) { - clients.delete(client); - removed = true; - } - } - if (clients.size === 0) this.rooms.delete(id); - if (removed) void this.refreshPresence(id); - } - } - - broadcast(roomId: string, message: object): void { - this.broadcastLocal(roomId, message); - void this.store.publish(roomId, message); - } - - async onlineUsers( - roomId: string, - ): Promise> { - return this.store.onlineUsers(roomId, this.localOnlineUsers(roomId)); - } - - dispose(): void { - clearInterval(this.heartbeat); - this.rooms.clear(); - } - - private broadcastLocal(roomId: string, message: object): void { - const clients = this.rooms.get(roomId); - if (!clients) return; - const payload = JSON.stringify(message); - for (const client of clients) { - if (client.ws.readyState !== client.ws.OPEN) continue; - try { - client.ws.send(payload); - } catch { - /* keep broadcasting to the rest */ - } - } - } - - private localOnlineUsers( - roomId: string, - ): Array<{ userId: string; username: string }> { - const clients = this.rooms.get(roomId); - if (!clients) return []; - const seen = new Map(); - for (const client of clients) { - seen.set(client.userId, client.username); - } - return Array.from(seen, ([userId, username]) => ({ userId, username })); - } - - private async refreshPresence(roomId: string): Promise { - const localUsers = this.localOnlineUsers(roomId); - await this.store.updatePresence(roomId, localUsers); - this.broadcast(roomId, { - type: "collab_online", - roomId, - users: await this.onlineUsers(roomId), - }); - } -} diff --git a/plugins/session-sharing/src/backend/room-routes.ts b/plugins/session-sharing/src/backend/room-routes.ts deleted file mode 100644 index 8c078d8a1..000000000 --- a/plugins/session-sharing/src/backend/room-routes.ts +++ /dev/null @@ -1,1487 +0,0 @@ -import crypto from "node:crypto"; -import type { Request, Response, Router } from "express"; -import { - PROTOCOLS, - TERMINAL_WS_PATH, - actorOf, - clientIp, - isNonEmptyString, - type SharingDeps, -} from "./deps.js"; -import type { LiveProtocol, RoomRecord } from "./repositories.js"; -import type { CollabControlRequest } from "./runtime-store.js"; - -/* - * Known limits, shared with session sharing: - * - Redis synchronizes collaboration events and ephemeral state across - * instances, but live session transports still need WebSocket affinity. - * - Remote desktop stages stay read-only because guacamole-lite cannot revoke - * a writable viewer without disconnecting the whole shared session. - */ - -const STAGE_SHARE_EXPIRY_HOURS = 12; -const MAX_INVITE_TARGETS = 200; -const CONTROL_REQUEST_COOLDOWN_MS = 5000; - -function publicRoom(room: RoomRecord) { - const { guestLinkToken: secret, ...safeRoom } = room; - return { ...safeRoom, guestLinkEnabled: Boolean(secret) }; -} - -function stagePayload(room: RoomRecord) { - return { - presenterUserId: room.presenterUserId, - protocol: room.stageProtocol, - hostId: room.stageHostId, - shareId: room.stageShareId, - }; -} - -/** The pieces of room behaviour the routes and the runtime events share. */ -export function createRoomActions(deps: SharingDeps) { - const { shares, rooms, live, hub, store } = deps; - - async function requireRoomMember( - roomId: string, - userId: string, - ): Promise<{ room: RoomRecord; isHost: boolean } | null> { - const room = await rooms.findById(roomId); - if (!room || room.endedAt) return null; - const member = await rooms.findMember(roomId, userId); - if (!member) return null; - return { room, isHost: member.roomRole === "host" }; - } - - async function revokeStageShare(room: RoomRecord): Promise { - if (!room.stageShareId) return; - const share = await shares.findActiveById(room.stageShareId); - if (!share) return; - if (!(await shares.revokeAny(room.stageShareId))) { - throw new Error("Failed to revoke the active stage share"); - } - if (share.protocol === "ssh") { - await live.disconnectParticipants( - "ssh", - share.sessionId, - share.id, - { reason: "The collaboration stage ended" }, - share.ownerUserId, - ); - void store.publish(room.id, { - type: "collab_internal_stage_revoked", - sessionId: share.sessionId, - shareId: share.id, - ownerUserId: share.ownerUserId, - }); - } - } - - /** Sets the controller everywhere it lives: the store, the live gate, the hub. */ - async function applyStageControl( - room: RoomRecord, - controllerUserId: string | null, - ): Promise { - await store.setController(room.id, controllerUserId); - if (room.stageShareId && room.stageProtocol === "ssh") { - const share = await shares - .findActiveById(room.stageShareId) - .catch(() => null); - if (share) { - await live.setRoomShareControl( - "ssh", - share.sessionId, - share.id, - controllerUserId, - share.ownerUserId, - ); - } - } - hub.broadcast(room.id, { - type: "collab_control_changed", - roomId: room.id, - controllerUserId, - }); - } - - async function clearStaleStage(roomId: string): Promise { - await store.setController(roomId, null); - await rooms.clearStage(roomId); - hub.broadcast(roomId, { - type: "collab_stage_changed", - roomId, - stage: null, - }); - } - - /** The stage's share, when it is still active and its session still live. */ - async function liveStageShare(room: RoomRecord) { - if (!room.stageShareId || !room.stageProtocol) return null; - const share = await shares.findActiveById(room.stageShareId); - if (!share) return null; - const alive = await live.isLive( - room.stageProtocol, - share.sessionId, - share.ownerUserId, - ); - return alive ? share : null; - } - - /** Room events from other instances that touch live sessions here. */ - function onRuntimeEvent(roomId: string, message: object): void { - const event = message as Record; - if (event.type === "collab_control_changed") { - const controllerUserId = - typeof event.controllerUserId === "string" - ? event.controllerUserId - : null; - void rooms - .findById(roomId) - .then(async (room) => { - if (!room?.stageShareId || room.stageProtocol !== "ssh") return; - const share = await shares.findActiveById(room.stageShareId); - if (share) { - await live.setRoomShareControl( - "ssh", - share.sessionId, - share.id, - controllerUserId, - share.ownerUserId, - ); - } - }) - .catch(() => {}); - } - if ( - event.type === "collab_internal_stage_revoked" && - typeof event.sessionId === "string" && - typeof event.shareId === "string" && - typeof event.ownerUserId === "string" - ) { - void live.disconnectParticipants( - "ssh", - event.sessionId, - event.shareId, - { reason: "The collaboration stage ended" }, - event.ownerUserId, - ); - } - } - - return { - requireRoomMember, - revokeStageShare, - applyStageControl, - clearStaleStage, - liveStageShare, - onRuntimeEvent, - }; -} - -export type RoomActions = ReturnType; - -/** - * Collaboration rooms. `/guest/:token` is public (an anonymous room guest - * link); everything else needs the plugin's `use` permission and room - * membership. - */ -export function registerRoomRoutes( - router: Router, - deps: SharingDeps, - actions: RoomActions, -): void { - const { ctx, shares, rooms, directory, live, hub, store, guestLimiter } = - deps; - const { - requireRoomMember, - revokeStageShare, - applyStageControl, - clearStaleStage, - liveStageShare, - } = actions; - const requireUse = ctx.rbac.require("use") as never; - - /** - * @openapi - * /plugin-api/session-sharing/meeting-host/{syncId}: - * get: - * summary: Find this server's id for a synced host - * description: A desktop presents a host by its sync id; this returns the matching host the caller may connect to. - * tags: [Collab] - * parameters: - * - in: path - * name: syncId - * required: true - * schema: { type: string } - * responses: - * 200: { description: The host id. } - * 401: { description: Not signed in. } - * 404: { description: No reachable host with that sync id. } - */ - router.get( - "/meeting-host/:syncId", - requireUse, - async (req: Request, res: Response) => { - if (!ctx.currentActor()) - return res.status(401).json({ error: "Authentication required" }); - try { - for (const id of await directory.hostIdsBySyncId( - String(req.params.syncId), - )) { - if ((await ctx.hosts.checkAccess(id, "connect")).hasAccess) { - return res.json({ id }); - } - } - return res.status(404).json({ - error: - "The host must be synced to the meeting server before presenting", - }); - } catch { - return res - .status(500) - .json({ error: "Failed to resolve meeting host" }); - } - }, - ); - - const audit = ( - req: Request, - action: string, - room: { id: string; name: string }, - details?: Record, - ) => - ctx.audit.record({ - request: req, - action, - resourceType: "collab_room", - resourceId: room.id, - resourceName: room.name, - details: details ? JSON.stringify(details) : undefined, - success: true, - }); - - const fail = (res: Response, operation: string, message: string) => { - return (error: unknown) => { - ctx.log.error( - `${operation}: ${message}`, - error instanceof Error ? error : undefined, - ); - res.status(500).json({ error: message }); - }; - }; - - /** - * @openapi - * /plugin-api/session-sharing/rooms: - * post: - * summary: Create a collaboration room - * tags: - * - Collab - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [name] - * properties: - * name: - * type: string - * persistent: - * type: boolean - * responses: - * 200: - * description: The new room - * 400: - * description: Missing or too long name - */ - router.post("/rooms", requireUse, async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const { name, persistent } = req.body ?? {}; - if (!isNonEmptyString(name) || name.trim().length > 120) { - return res.status(400).json({ error: "Room name is required" }); - } - try { - const room = await rooms.createRoom({ - id: crypto.randomUUID(), - name: name.trim(), - ownerUserId: userId, - persistent: persistent === true, - }); - await rooms.addMember({ - roomId: room.id, - userId, - roomRole: "host", - addedBy: userId, - }); - await audit(req, "collab_room_create", room); - res.json({ room: publicRoom(room) }); - } catch (error) { - fail(res, "collab_room_create_error", "Failed to create room")(error); - } - }); - - /** - * @openapi - * /plugin-api/session-sharing/rooms: - * get: - * summary: List rooms the caller belongs to - * tags: - * - Collab - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Rooms, without their guest tokens - */ - router.get("/rooms", requireUse, async (_req: Request, res: Response) => { - try { - const list = await rooms.listForUser(actorOf(ctx)); - res.json({ rooms: list.map(publicRoom) }); - } catch (error) { - fail(res, "collab_room_list_error", "Failed to list rooms")(error); - } - }); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}: - * get: - * summary: Get a room with members, online users and stage state - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The room - * 404: - * description: Not a member, or no such room - */ - router.get("/rooms/:id", requireUse, async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - try { - let access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - - // The presenter's own client drives the stage locally and never asks - // for it, so this is where a presenter whose session died is noticed. - if (access.room.stageShareId && !(await liveStageShare(access.room))) { - await clearStaleStage(roomId); - access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - } - - const members = await rooms.listMembers(roomId); - const stageShare = await liveStageShare(access.room); - const guests = - stageShare && access.room.stageProtocol - ? await live.listGuests( - access.room.stageProtocol, - stageShare.sessionId, - stageShare.id, - stageShare.ownerUserId, - ) - : []; - const mayReview = access.isHost || access.room.presenterUserId === userId; - const controlRequests = await store.listRequests(roomId); - res.json({ - room: publicRoom(access.room), - me: userId, - isHost: access.isHost, - members, - online: await hub.onlineUsers(roomId), - stage: stagePayload(access.room), - guests, - controllerUserId: await store.getController(roomId), - controlRequests: mayReview - ? controlRequests - : controlRequests.filter((request) => request.userId === userId), - // Room events ride on the terminal socket. - eventsWsPath: TERMINAL_WS_PATH, - }); - } catch (error) { - fail(res, "collab_room_get_error", "Failed to get room")(error); - } - }); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/members: - * post: - * summary: Invite users to a room (host only) - * description: Roles expand to their current members, a snapshot. - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * userIds: - * type: array - * items: - * type: string - * roleIds: - * type: array - * items: - * type: integer - * responses: - * 200: - * description: Invited - * 403: - * description: Not the host - * 404: - * description: Room, user or role not found - */ - router.post( - "/rooms/:id/members", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - const { userIds = [], roleIds = [] } = req.body ?? {}; - - if ( - !Array.isArray(userIds) || - userIds.some((id) => !isNonEmptyString(id)) || - !Array.isArray(roleIds) || - roleIds.some((id) => !Number.isInteger(id)) || - (userIds.length === 0 && roleIds.length === 0) - ) { - return res.status(400).json({ - error: "userIds (user ids) or roleIds (integers) are required", - }); - } - if (userIds.length + roleIds.length > MAX_INVITE_TARGETS) { - return res.status(400).json({ error: "Too many invite targets" }); - } - - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - if (!access.isHost) { - return res.status(403).json({ error: "Only the host can invite" }); - } - - for (const targetId of userIds as string[]) { - if (!(await directory.userExists(targetId))) { - return res.status(404).json({ error: "User not found", targetId }); - } - } - const expanded = new Set(userIds as string[]); - for (const roleId of roleIds as number[]) { - if (!(await directory.roleExists(roleId))) { - return res.status(404).json({ error: "Role not found", roleId }); - } - for (const memberId of await directory.roleUserIds(roleId)) { - expanded.add(memberId); - } - } - for (const targetId of expanded) { - await rooms.addMember({ - roomId, - userId: targetId, - roomRole: "member", - addedBy: userId, - }); - } - - await audit(req, "collab_room_invite", access.room, { - memberCount: expanded.size, - }); - hub.broadcast(roomId, { type: "collab_members_changed", roomId }); - res.json({ success: true }); - } catch (error) { - fail( - res, - "collab_room_invite_error", - "Failed to invite members", - )(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/members/{userId}: - * delete: - * summary: Remove a member (host), or leave the room (self) - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * - in: path - * name: userId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Removed - * 400: - * description: The owner cannot be removed - * 403: - * description: Only the host can remove others - */ - router.delete( - "/rooms/:id/members/:userId", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - const targetId = String(req.params.userId); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - if (targetId !== userId && !access.isHost) { - return res - .status(403) - .json({ error: "Only the host can remove members" }); - } - if (targetId === access.room.ownerUserId) { - return res.status(400).json({ error: "The owner cannot be removed" }); - } - - await rooms.removeMember(roomId, targetId); - await store.removeRequest(roomId, targetId); - - if (access.room.stageShareId && access.room.stageProtocol === "ssh") { - const share = await shares.findActiveById(access.room.stageShareId); - if (share) { - await live.disconnectParticipants( - "ssh", - share.sessionId, - share.id, - { - userId: targetId, - reason: "You were removed from the collaboration room", - }, - share.ownerUserId, - ); - } - } - - if ((await store.getController(roomId)) === targetId) { - await applyStageControl(access.room, null); - } - - if (access.room.presenterUserId === targetId) { - await revokeStageShare(access.room); - await rooms.clearStage(roomId); - hub.broadcast(roomId, { - type: "collab_stage_changed", - roomId, - stage: null, - }); - } - - await audit( - req, - targetId === userId - ? "collab_room_leave" - : "collab_room_remove_member", - access.room, - { targetUserId: targetId }, - ); - hub.broadcast(roomId, { type: "collab_members_changed", roomId }); - res.json({ success: true }); - } catch (error) { - fail( - res, - "collab_room_remove_member_error", - "Failed to remove member", - )(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/present: - * post: - * summary: Take the stage with one of your live sessions - * description: Any member may take over the stage; the previous stage share is revoked. The caller must own the live session and sharing must be enabled for the host. - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [protocol, sessionId, hostId] - * properties: - * protocol: - * type: string - * enum: [ssh, rdp, vnc, telnet] - * sessionId: - * type: string - * hostId: - * type: integer - * responses: - * 200: - * description: The new stage - * 403: - * description: Sharing disabled, or not the session's owner - * 409: - * description: The stage changed meanwhile - */ - router.post( - "/rooms/:id/present", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - const { protocol, sessionId, hostId } = req.body ?? {}; - - if (!PROTOCOLS.includes(protocol)) { - return res.status(400).json({ error: "Invalid protocol" }); - } - if (!isNonEmptyString(sessionId) || !Number.isInteger(Number(hostId))) { - return res - .status(400) - .json({ error: "sessionId and hostId are required" }); - } - - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - - const numericHostId = Number(hostId); - if (!(await deps.isSharingEnabledForHost(numericHostId))) { - return res - .status(403) - .json({ error: "Session sharing is disabled for this host" }); - } - if ( - !(await live.isOwnedBy( - protocol as LiveProtocol, - String(sessionId), - userId, - )) - ) { - return res - .status(403) - .json({ error: "You do not own this live session" }); - } - - const share = await shares.create({ - id: crypto.randomUUID(), - hostId: numericHostId, - ownerUserId: userId, - protocol, - sessionId: String(sessionId), - shareType: "room", - permissionLevel: "read-only", - expiresAt: new Date( - Date.now() + STAGE_SHARE_EXPIRY_HOURS * 60 * 60 * 1000, - ).toISOString(), - }); - - await revokeStageShare(access.room); - const replaced = await rooms.replaceStage( - roomId, - access.room.stageShareId, - { - presenterUserId: userId, - stageProtocol: protocol, - stageHostId: numericHostId, - stageShareId: share.id, - }, - ); - if (!replaced) { - await shares.revokeAny(share.id); - return res - .status(409) - .json({ error: "The stage changed; try again" }); - } - await store.setController(roomId, null); - await store.clearRequests(roomId); - - const stage = { - presenterUserId: userId, - protocol, - hostId: numericHostId, - shareId: share.id, - }; - hub.broadcast(roomId, { type: "collab_stage_changed", roomId, stage }); - await audit(req, "collab_room_present", access.room, { - protocol, - hostId: numericHostId, - }); - res.json({ stage }); - } catch (error) { - fail( - res, - "collab_room_present_error", - "Failed to start presenting", - )(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/stop: - * post: - * summary: Stop presenting (presenter or host) - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Stage cleared - * 403: - * description: Not the presenter or host - */ - router.post( - "/rooms/:id/stop", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - if (access.room.presenterUserId !== userId && !access.isHost) { - return res - .status(403) - .json({ error: "Only the presenter or host can stop the stage" }); - } - - await revokeStageShare(access.room); - await store.setController(roomId, null); - await store.clearRequests(roomId); - await rooms.clearStage(roomId); - hub.broadcast(roomId, { - type: "collab_stage_changed", - roomId, - stage: null, - }); - await audit(req, "collab_room_stop_presenting", access.room); - res.json({ success: true }); - } catch (error) { - fail(res, "collab_room_stop_error", "Failed to stop presenting")(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/stage: - * get: - * summary: Get connect info for the current stage (members only) - * description: SSH stages are joined over the terminal socket by shareId; remote desktop stages get a freshly minted read-only viewer token. - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The stage, or null - */ - router.get( - "/rooms/:id/stage", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - const { room } = access; - if (!room.stageShareId || !room.stageProtocol) { - return res.json({ stage: null }); - } - - const share = await liveStageShare(room); - if (!share) { - // The presenter is gone (expired share or dead session). - await clearStaleStage(roomId); - return res.json({ stage: null }); - } - - const stage: Record = { - ...stagePayload(room), - sessionId: share.sessionId, - controllerUserId: await store.getController(roomId), - }; - if (room.stageProtocol !== "ssh") { - stage.connectParams = { - token: await live.createViewerToken( - room.stageProtocol, - share.sessionId, - true, - share.ownerUserId, - ), - }; - } - res.json({ stage }); - } catch (error) { - fail(res, "collab_room_stage_error", "Failed to resolve stage")(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/control: - * post: - * summary: Grant or revoke stage control (presenter or host) - * description: Grants a member write access to the current SSH stage, or revokes it with a null userId. The controller may also release control themselves. - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * userId: - * type: string - * nullable: true - * responses: - * 200: - * description: The new controller - * 400: - * description: Nothing presented, or a remote desktop stage - * 403: - * description: Not the presenter or host - */ - router.post( - "/rooms/:id/control", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - const { userId: targetId } = req.body ?? {}; - if (targetId !== null && !isNonEmptyString(targetId)) { - return res - .status(400) - .json({ error: "userId must be a user id or null" }); - } - - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - if (!access.room.stageShareId) { - return res.status(400).json({ error: "Nothing is being presented" }); - } - if (access.room.stageProtocol !== "ssh") { - return res - .status(400) - .json({ error: "Remote desktop stages are read-only" }); - } - - const releasingOwn = - targetId === null && (await store.getController(roomId)) === userId; - const mayGrant = - access.isHost || access.room.presenterUserId === userId; - if (!mayGrant && !releasingOwn) { - return res.status(403).json({ - error: "Only the presenter or host can change stage control", - }); - } - if (targetId && !(await rooms.findMember(roomId, targetId))) { - return res.status(404).json({ error: "Member not found" }); - } - - await applyStageControl(access.room, targetId); - if (targetId) { - await store.removeRequest(roomId, targetId); - hub.broadcast(roomId, { - type: "collab_control_requests_changed", - roomId, - }); - } - await audit( - req, - targetId ? "collab_control_grant" : "collab_control_revoke", - access.room, - { targetUserId: targetId }, - ); - res.json({ controllerUserId: targetId }); - } catch (error) { - fail( - res, - "collab_control_error", - "Failed to change stage control", - )(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/control/request: - * post: - * summary: Ask the presenter for stage control (hand raise) - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The request - * 429: - * description: Asked too recently - */ - router.post( - "/rooms/:id/control/request", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - if (!access.room.stageShareId) { - return res.status(400).json({ error: "Nothing is being presented" }); - } - if (access.room.stageProtocol !== "ssh") { - return res - .status(400) - .json({ error: "Remote desktop stages are read-only" }); - } - const existing = (await store.listRequests(roomId)).find( - (request) => request.userId === userId, - ); - if ( - existing && - Date.now() - Date.parse(existing.requestedAt) < - CONTROL_REQUEST_COOLDOWN_MS - ) { - return res - .status(429) - .json({ error: "Control was already requested" }); - } - const request: CollabControlRequest = { - userId, - username: await directory.username(userId), - requestedAt: new Date().toISOString(), - }; - await store.upsertRequest(roomId, request); - hub.broadcast(roomId, { - type: "collab_control_requested", - roomId, - ...request, - }); - await audit(req, "collab_control_request", access.room); - res.json({ request }); - } catch (error) { - fail( - res, - "collab_control_request_error", - "Failed to request control", - )(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/control/requests: - * get: - * summary: List pending control requests (presenter or host) - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Pending requests - * 403: - * description: Not the presenter or host - */ - router.get( - "/rooms/:id/control/requests", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - if (!access.isHost && access.room.presenterUserId !== userId) { - return res.status(403).json({ - error: "Only the presenter or host can review control requests", - }); - } - res.json({ requests: await store.listRequests(roomId) }); - } catch (error) { - fail( - res, - "collab_control_requests_list_error", - "Failed to list control requests", - )(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/control/requests/{userId}: - * delete: - * summary: Dismiss a control request (presenter or host), or cancel your own - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * - in: path - * name: userId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Dismissed - * 403: - * description: Not allowed to dismiss it - */ - router.delete( - "/rooms/:id/control/requests/:userId", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - const targetId = String(req.params.userId); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - const mayReview = - access.isHost || access.room.presenterUserId === userId; - if (!mayReview && targetId !== userId) { - return res - .status(403) - .json({ error: "Not allowed to dismiss request" }); - } - await store.removeRequest(roomId, targetId); - hub.broadcast(roomId, { - type: "collab_control_requests_changed", - roomId, - }); - await audit( - req, - targetId === userId - ? "collab_control_request_cancel" - : "collab_control_request_dismiss", - access.room, - { targetUserId: targetId }, - ); - res.json({ success: true }); - } catch (error) { - fail( - res, - "collab_control_request_dismiss_error", - "Failed to dismiss control request", - )(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/guests/remove: - * post: - * summary: Disconnect every anonymous guest watching the stage (host only) - * description: The guest link stays valid, so a guest may open it again. - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Guests disconnected - * 403: - * description: Not the host - * 404: - * description: Not a member, or no such room - */ - router.post( - "/rooms/:id/guests/remove", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - if (!access.isHost) { - return res - .status(403) - .json({ error: "Only the host can remove guests" }); - } - const share = await liveStageShare(access.room); - if (share && access.room.stageProtocol) { - await live.disconnectParticipants( - access.room.stageProtocol, - share.sessionId, - share.id, - { userId: null, reason: "The host removed the guests" }, - share.ownerUserId, - ); - } - res.json({ success: true }); - } catch (error) { - fail( - res, - "collab_guests_remove_error", - "Failed to remove guests", - )(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/guest-link: - * post: - * summary: Enable, rotate or disable the room's anonymous guest link (host only) - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [enabled] - * properties: - * enabled: - * type: boolean - * responses: - * 200: - * description: The new token, or null when disabled - * 403: - * description: Not the host - */ - router.post( - "/rooms/:id/guest-link", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - const { enabled } = req.body ?? {}; - if (typeof enabled !== "boolean") { - return res.status(400).json({ error: "enabled must be a boolean" }); - } - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - if (!access.isHost) { - return res - .status(403) - .json({ error: "Only the host can manage the guest link" }); - } - const token = enabled - ? crypto.randomBytes(24).toString("base64url") - : null; - await rooms.setGuestToken(roomId, token); - - // A rotated or disabled link drops the guests who came in on the old one. - if ( - (!enabled || access.room.guestLinkToken) && - access.room.stageShareId && - access.room.stageProtocol === "ssh" - ) { - const share = await shares.findActiveById(access.room.stageShareId); - if (share) { - await live.disconnectParticipants( - "ssh", - share.sessionId, - share.id, - { - userId: null, - reason: enabled - ? "The guest link was rotated" - : "The guest link was disabled", - }, - share.ownerUserId, - ); - } - } - - await audit( - req, - enabled ? "collab_guest_link_enable" : "collab_guest_link_disable", - access.room, - ); - res.json({ guestLinkToken: token }); - } catch (error) { - fail( - res, - "collab_guest_link_error", - "Failed to update guest link", - )(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/guest/{token}: - * get: - * summary: Resolve a room's current stage for an anonymous guest - * description: Public, rate-limited per IP. Guests poll this to follow presenter switches. Never returns host details; SSH stages are joined over the terminal socket with roomGuestToken, remote desktop stages get a read-only viewer token. - * tags: - * - Collab - * parameters: - * - in: path - * name: token - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Room name and the stage, or a null stage - * 404: - * description: Link not found - * 429: - * description: Too many requests - */ - router.get("/guest/:token", async (req: Request, res: Response) => { - if (guestLimiter.isLimited(clientIp(req))) { - return res.status(429).json({ error: "Too many requests" }); - } - const token = String(req.params.token); - try { - const room = await rooms.findByGuestToken(token); - if (!room) return res.status(404).json({ error: "Link not found" }); - - const response: Record = { - roomName: room.name, - stage: null, - }; - const share = await liveStageShare(room); - if ( - share && - room.stageProtocol && - (await deps.isSharingEnabledForHost(share.hostId)) - ) { - const protocol = room.stageProtocol as LiveProtocol; - response.stage = { - protocol, - shareId: share.id, - ...(protocol === "ssh" - ? { - wsPath: `${TERMINAL_WS_PATH}?roomGuestToken=${encodeURIComponent(token)}`, - } - : { - connectParams: { - token: await live.createViewerToken( - protocol, - share.sessionId, - true, - share.ownerUserId, - ), - }, - }), - }; - } - res.json(response); - } catch (error) { - fail( - res, - "collab_guest_resolve_error", - "Failed to resolve guest link", - )(error); - } - }); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}/end: - * post: - * summary: End the meeting (host only) - * description: Clears the stage. One-off rooms are ended for good; persistent rooms stay listed for reuse. - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Ended - * 403: - * description: Not the host - */ - router.post( - "/rooms/:id/end", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - if (!access.isHost) { - return res - .status(403) - .json({ error: "Only the host can end the room" }); - } - - await revokeStageShare(access.room); - await store.setController(roomId, null); - await store.clearRequests(roomId); - if (access.room.persistent) { - await rooms.setGuestToken(roomId, null); - await rooms.clearStage(roomId); - hub.broadcast(roomId, { - type: "collab_stage_changed", - roomId, - stage: null, - }); - } else { - await rooms.endRoom(roomId); - hub.broadcast(roomId, { type: "collab_room_ended", roomId }); - } - await audit(req, "collab_room_end", access.room); - res.json({ success: true }); - } catch (error) { - fail(res, "collab_room_end_error", "Failed to end room")(error); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/rooms/{id}: - * delete: - * summary: Permanently delete a room (host only) - * description: Revokes the active stage and removes the room and its membership. This cannot be undone. - * tags: - * - Collab - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deleted - * 403: - * description: Not the host - */ - router.delete( - "/rooms/:id", - requireUse, - async (req: Request, res: Response) => { - const userId = actorOf(ctx); - const roomId = String(req.params.id); - try { - const access = await requireRoomMember(roomId, userId); - if (!access) return res.status(404).json({ error: "Room not found" }); - if (!access.isHost) { - return res - .status(403) - .json({ error: "Only the host can delete the room" }); - } - - await revokeStageShare(access.room); - await store.setController(roomId, null); - await store.clearRequests(roomId); - hub.broadcast(roomId, { type: "collab_room_ended", roomId }); - await rooms.deleteRoom(roomId); - await audit(req, "collab_room_delete", access.room); - res.json({ success: true }); - } catch (error) { - fail(res, "collab_room_delete_error", "Failed to delete room")(error); - } - }, - ); -} diff --git a/plugins/session-sharing/src/backend/runtime-store.ts b/plugins/session-sharing/src/backend/runtime-store.ts deleted file mode 100644 index 28162110d..000000000 --- a/plugins/session-sharing/src/backend/runtime-store.ts +++ /dev/null @@ -1,289 +0,0 @@ -import crypto from "node:crypto"; -import { createClient } from "redis"; -import type { PluginLogger } from "@termix/plugin-sdk/backend"; - -export interface CollabControlRequest { - userId: string; - username: string; - requestedAt: string; -} - -interface PresenceEntry { - instanceId: string; - userId: string; - username: string; -} - -interface EventEnvelope { - source: string; - roomId: string; - message: object; -} - -const KEY_PREFIX = process.env.TERMIX_REDIS_PREFIX?.trim() || "termix:collab"; -const EVENT_CHANNEL = `${KEY_PREFIX}:events`; -const PRESENCE_TTL_MS = 45_000; -const STATE_TTL_SECONDS = 12 * 60 * 60; -const CONNECT_RETRY_MS = 15_000; - -/** - * Room state that has to be shared between Termix instances: who controls a - * stage, pending control requests, presence and room events. Redis-backed when - * REDIS_URL is set, with local state for a single instance. - */ -export class CollabRuntimeStore { - constructor(private readonly log: PluginLogger) {} - - private readonly instanceId = crypto.randomUUID(); - private publisher: ReturnType | null = null; - private subscriber: ReturnType | null = null; - private connecting: Promise | null = null; - private nextConnectAttempt = 0; - private eventListeners = new Set<(roomId: string, message: object) => void>(); - private localControllers = new Map(); - private localRequests = new Map>(); - private presenceMembers = new Map>(); - - onEvent(listener: (roomId: string, message: object) => void): void { - this.eventListeners.add(listener); - void this.ensureConnected(); - } - - async publish(roomId: string, message: object): Promise { - if (!(await this.ensureConnected()) || !this.publisher) return; - await this.publisher - .publish( - EVENT_CHANNEL, - JSON.stringify({ source: this.instanceId, roomId, message }), - ) - .catch((error) => this.logRedisFailure("publish", error)); - } - - async getController(roomId: string): Promise { - if (await this.ensureConnected()) { - const value = await this.publisher - ?.get(`${KEY_PREFIX}:controller:${roomId}`) - .catch((error) => { - this.logRedisFailure("get_controller", error); - return undefined; - }); - if (value !== undefined) return value; - } - return this.localControllers.get(roomId) ?? null; - } - - async setController(roomId: string, userId: string | null): Promise { - if (userId) this.localControllers.set(roomId, userId); - else this.localControllers.delete(roomId); - if (!(await this.ensureConnected()) || !this.publisher) return; - const key = `${KEY_PREFIX}:controller:${roomId}`; - const operation = userId - ? this.publisher.set(key, userId, { EX: STATE_TTL_SECONDS }) - : this.publisher.del(key); - await operation.catch((error) => - this.logRedisFailure("set_controller", error), - ); - } - - async listRequests(roomId: string): Promise { - if (await this.ensureConnected()) { - const values = await this.publisher - ?.hVals(`${KEY_PREFIX}:requests:${roomId}`) - .catch((error) => { - this.logRedisFailure("list_requests", error); - return null; - }); - if (values) { - return values - .flatMap((value) => this.parse(value) ?? []) - .sort((a, b) => a.requestedAt.localeCompare(b.requestedAt)); - } - } - return Array.from(this.localRequests.get(roomId)?.values() ?? []).sort( - (a, b) => a.requestedAt.localeCompare(b.requestedAt), - ); - } - - async upsertRequest( - roomId: string, - request: CollabControlRequest, - ): Promise { - let requests = this.localRequests.get(roomId); - if (!requests) { - requests = new Map(); - this.localRequests.set(roomId, requests); - } - requests.set(request.userId, request); - if (!(await this.ensureConnected()) || !this.publisher) return; - const key = `${KEY_PREFIX}:requests:${roomId}`; - await this.publisher - .multi() - .hSet(key, request.userId, JSON.stringify(request)) - .expire(key, STATE_TTL_SECONDS) - .exec() - .catch((error) => this.logRedisFailure("upsert_request", error)); - } - - async removeRequest(roomId: string, userId: string): Promise { - const requests = this.localRequests.get(roomId); - requests?.delete(userId); - if (requests?.size === 0) this.localRequests.delete(roomId); - if (!(await this.ensureConnected()) || !this.publisher) return; - await this.publisher - .hDel(`${KEY_PREFIX}:requests:${roomId}`, userId) - .catch((error) => this.logRedisFailure("remove_request", error)); - } - - async clearRequests(roomId: string): Promise { - this.localRequests.delete(roomId); - if (!(await this.ensureConnected()) || !this.publisher) return; - await this.publisher - .del(`${KEY_PREFIX}:requests:${roomId}`) - .catch((error) => this.logRedisFailure("clear_requests", error)); - } - - async updatePresence( - roomId: string, - users: Array<{ userId: string; username: string }>, - ): Promise { - if (!(await this.ensureConnected()) || !this.publisher) return; - const key = `${KEY_PREFIX}:presence:${roomId}`; - const previous = this.presenceMembers.get(roomId) ?? new Set(); - const expiresAt = Date.now() + PRESENCE_TTL_MS; - const current = new Set( - users.map((user) => - JSON.stringify({ - instanceId: this.instanceId, - ...user, - } satisfies PresenceEntry), - ), - ); - const transaction = this.publisher.multi(); - if (previous.size > 0) transaction.zRem(key, Array.from(previous)); - for (const member of current) { - transaction.zAdd(key, { score: expiresAt, value: member }); - } - transaction.expire(key, Math.ceil(PRESENCE_TTL_MS / 1000) * 2); - await transaction - .exec() - .then(() => { - if (current.size > 0) this.presenceMembers.set(roomId, current); - else this.presenceMembers.delete(roomId); - }) - .catch((error) => this.logRedisFailure("update_presence", error)); - } - - async onlineUsers( - roomId: string, - localUsers: Array<{ userId: string; username: string }>, - ): Promise> { - if (!(await this.ensureConnected()) || !this.publisher) return localUsers; - const key = `${KEY_PREFIX}:presence:${roomId}`; - const now = Date.now(); - const transaction = this.publisher - .multi() - .zRemRangeByScore(key, 0, now) - .zRangeByScore(key, now + 1, "+inf"); - const result = await transaction.exec().catch((error) => { - this.logRedisFailure("online_users", error); - return null; - }); - const members = result?.[1] as string[] | undefined; - if (!members) return localUsers; - const seen = new Map(); - for (const member of members) { - const entry = this.parse(member); - if (entry) seen.set(entry.userId, entry.username); - } - return Array.from(seen, ([userId, username]) => ({ userId, username })); - } - - async close(): Promise { - this.eventListeners.clear(); - this.nextConnectAttempt = Number.POSITIVE_INFINITY; - await Promise.allSettled([ - this.publisher?.isOpen ? this.publisher.quit() : Promise.resolve(), - this.subscriber?.isOpen ? this.subscriber.quit() : Promise.resolve(), - ]); - this.publisher = null; - this.subscriber = null; - } - - private async ensureConnected(): Promise { - const url = process.env.REDIS_URL?.trim(); - if (!url) return false; - if (this.publisher?.isReady && this.subscriber?.isReady) return true; - if (Date.now() < this.nextConnectAttempt) return false; - if (this.connecting) return this.connecting; - this.connecting = this.connect(url).finally(() => { - this.connecting = null; - }); - return this.connecting; - } - - private async connect(url: string): Promise { - try { - this.publisher = createClient({ - url, - socket: { connectTimeout: 1500, reconnectStrategy: false }, - }); - this.subscriber = this.publisher.duplicate(); - this.publisher.on("error", (error) => - this.logRedisFailure("client", error), - ); - this.subscriber.on("error", (error) => - this.logRedisFailure("subscriber", error), - ); - await Promise.all([this.publisher.connect(), this.subscriber.connect()]); - await this.subscriber.subscribe(EVENT_CHANNEL, (raw) => { - const envelope = this.parse(raw); - if ( - !envelope || - typeof envelope.source !== "string" || - typeof envelope.roomId !== "string" || - !envelope.message || - typeof envelope.message !== "object" || - envelope.source === this.instanceId - ) { - return; - } - for (const listener of this.eventListeners) { - try { - listener(envelope.roomId, envelope.message); - } catch (error) { - this.logRedisFailure("event_listener", error); - } - } - }); - this.log.info("Collaboration Redis runtime connected"); - this.nextConnectAttempt = 0; - return true; - } catch (error) { - this.nextConnectAttempt = Date.now() + CONNECT_RETRY_MS; - this.logRedisFailure("connect", error); - await Promise.allSettled([ - this.publisher?.disconnect(), - this.subscriber?.disconnect(), - ]); - this.publisher = null; - this.subscriber = null; - return false; - } - } - - private parse(value: string): T | null { - try { - return JSON.parse(value) as T; - } catch { - return null; - } - } - - private logRedisFailure(operation: string, error: unknown): void { - this.log.warn( - `Collaboration Redis runtime unavailable (${operation}), using local state: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } -} diff --git a/plugins/session-sharing/src/backend/services.ts b/plugins/session-sharing/src/backend/services.ts deleted file mode 100644 index ed3ad333b..000000000 --- a/plugins/session-sharing/src/backend/services.ts +++ /dev/null @@ -1,162 +0,0 @@ -import type { WebSocket } from "ws"; -import type { SharingDeps } from "./deps.js"; -import type { ShareRecord } from "./repositories.js"; - -/** - * What the terminal consumes. The shapes match - * plugins/ssh-terminal/src/backend/services.ts, which is the consumer's copy. - */ -export interface SharedSessionRef { - id: string; - sessionId: string; - permissionLevel: "read-write" | "read-only"; -} - -/** sessions.sharing v1: signed-in joins. Every call runs as the acting user. */ -export interface SessionSharingV1 { - authorizeJoin: ( - shareId: string, - ) => Promise<{ share: SharedSessionRef; displayName: string } | null>; - recordJoin: (shareId: string) => Promise; - subscribeRoom: (roomId: string, socket: unknown) => Promise; - unsubscribeRoom: (socket: unknown, roomId?: string) => Promise; -} - -/** - * Guest joins through a share link or a room guest link, on ctx.registry as - * SESSION_GUESTS_KEY. A guest has no user, so a service's per-call - * permission check cannot apply: the token is the authority. - */ -export interface SessionGuestsV1 { - resolve: (request: { - shareToken?: string; - roomGuestToken?: string; - clientIp: string; - }) => Promise< - { ok: true; share: SharedSessionRef } | { ok: false; reason: string } - >; - recordJoin: (shareId: string) => Promise; -} - -export const SESSION_GUESTS_KEY = "session-sharing.guests"; - -function toRef(share: ShareRecord): SharedSessionRef { - return { - id: share.id, - sessionId: share.sessionId, - permissionLevel: - share.permissionLevel === "read-write" ? "read-write" : "read-only", - }; -} - -export function createSessionSharingService( - deps: SharingDeps, -): SessionSharingV1 { - const { ctx, shares, rooms, directory, hub } = deps; - - return { - async authorizeJoin(shareId) { - const userId = ctx.currentActor(); - if (!userId) return null; - const share = await shares.findActiveById(shareId); - if (!share || share.protocol !== "ssh") return null; - - if (share.shareType === "user") { - // A user share needs its target, who can still reach the host. - if (share.targetUserId !== userId) return null; - const access = await ctx.hosts.checkAccess(share.hostId, "connect"); - if (!access.hasAccess) return null; - } else if (share.shareType === "room") { - // Room membership is the authorization for a room stage: it is - // read-only and never exposes host credentials or config. - const room = await rooms.findByStageShareId(share.id); - if (!room || !(await rooms.findMember(room.id, userId))) return null; - } else { - return null; - } - - return { - share: toRef(share), - displayName: await directory.username(userId), - }; - }, - - async recordJoin(shareId) { - const userId = ctx.currentActor() ?? null; - await shares.touchUsage(shareId); - await shares.recordParticipantJoin(shareId, userId, null); - }, - - async subscribeRoom(roomId, socket) { - const userId = ctx.currentActor(); - if (!userId) return false; - const room = await rooms.findById(roomId); - if (!room || room.endedAt) return false; - if (!(await rooms.findMember(roomId, userId))) return false; - hub.subscribe(roomId, { - ws: socket as WebSocket, - userId, - username: await directory.username(userId), - }); - return true; - }, - - async unsubscribeRoom(socket, roomId) { - hub.unsubscribe(socket as WebSocket, roomId); - }, - }; -} - -export function createSessionGuests(deps: SharingDeps): SessionGuestsV1 { - const { shares, rooms, guestLimiter } = deps; - - /** Sharing still allowed, and an SSH share: the only kind the socket joins. */ - async function joinable( - share: ShareRecord, - ): Promise< - { ok: true; share: SharedSessionRef } | { ok: false; reason: string } - > { - if (share.protocol !== "ssh") { - return { ok: false, reason: "Unsupported share protocol" }; - } - if (!(await deps.isSharingEnabledForHost(share.hostId))) { - return { ok: false, reason: "Session sharing is disabled for this host" }; - } - return { ok: true, share: toRef(share) }; - } - - return { - async resolve({ shareToken, roomGuestToken, clientIp }) { - if (shareToken) { - // Guessing link tokens is limited the same way room tokens are. - if (guestLimiter.isLimited(clientIp)) { - return { ok: false, reason: "Too many requests" }; - } - const share = await shares.findByLinkToken(shareToken); - if (!share || share.shareType !== "link") { - return { ok: false, reason: "Invalid or expired share link" }; - } - return joinable(share); - } - - if (roomGuestToken) { - if (guestLimiter.isLimited(clientIp)) { - return { ok: false, reason: "Too many requests" }; - } - const room = await rooms.findByGuestToken(roomGuestToken); - const share = room?.stageShareId - ? await shares.findActiveById(room.stageShareId) - : null; - if (!share) return { ok: false, reason: "Nothing is being presented" }; - return joinable(share); - } - - return { ok: false, reason: "Authentication required" }; - }, - - async recordJoin(shareId) { - await shares.touchUsage(shareId); - await shares.recordParticipantJoin(shareId, null, "Guest"); - }, - }; -} diff --git a/plugins/session-sharing/src/backend/share-routes.ts b/plugins/session-sharing/src/backend/share-routes.ts deleted file mode 100644 index c154ca496..000000000 --- a/plugins/session-sharing/src/backend/share-routes.ts +++ /dev/null @@ -1,523 +0,0 @@ -import crypto from "node:crypto"; -import type { Request, Response, Router } from "express"; -import { - DISPLAY_WS_PATH, - PROTOCOLS, - TERMINAL_WS_PATH, - actorOf, - clientIp, - type SharingDeps, -} from "./deps.js"; -import type { LiveProtocol, PermissionLevel } from "./repositories.js"; - -const DEFAULT_EXPIRY_HOURS = 24; -const MAX_EXPIRY_HOURS = 24 * 30; - -function computeExpiresAt(expiryHours: number | undefined): string { - const hours = Math.min( - Math.max(expiryHours ?? DEFAULT_EXPIRY_HOURS, 1), - MAX_EXPIRY_HOURS, - ); - return new Date(Date.now() + hours * 60 * 60 * 1000).toISOString(); -} - -/** - * The share routes. `/resolve/:linkToken` is public (an anonymous guest link); - * the rest run behind core auth and the plugin's `use` permission, which the - * caller applies to the router first. - */ -export function registerShareRoutes(router: Router, deps: SharingDeps): void { - const { ctx, shares, directory, live, resolveLimiter } = deps; - const requireUse = ctx.rbac.require("use") as never; - - /** - * @openapi - * /plugin-api/session-sharing/resolve/{linkToken}: - * get: - * summary: Resolve a guest share link - * description: Public, unauthenticated endpoint for anonymous share-link guests. Never returns host name, IP, username, or hostId. Rate-limited per IP. - * tags: - * - Session Sharing - * parameters: - * - in: path - * name: linkToken - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Resolved share connection info - * 404: - * description: Link not found, expired, revoked, or sharing disabled - * 429: - * description: Too many requests - * 500: - * description: Server error - */ - router.get("/resolve/:linkToken", async (req: Request, res: Response) => { - try { - if (resolveLimiter.isLimited(clientIp(req))) { - return res.status(429).json({ error: "Too many requests" }); - } - - const linkToken = String(req.params.linkToken); - const share = await shares.findByLinkToken(linkToken); - if (!share || share.shareType !== "link") { - return res.status(404).json({ error: "Link not found or expired" }); - } - if (!(await deps.isSharingEnabledForHost(share.hostId))) { - return res.status(404).json({ error: "Link not found or expired" }); - } - - const protocol = share.protocol as LiveProtocol; - if (!(await live.isLive(protocol, share.sessionId, share.ownerUserId))) { - return res.status(404).json({ error: "Session is no longer active" }); - } - - // Field by field on purpose: anonymous guests never see the host's - // name, address, username or id. - const response: { - protocol: LiveProtocol; - permissionLevel: PermissionLevel; - wsPath: string; - connectParams?: Record; - } = { - protocol, - permissionLevel: share.permissionLevel as PermissionLevel, - // Advisory only: the client builds its own socket URL per deployment. - wsPath: - protocol === "ssh" - ? `${TERMINAL_WS_PATH}?shareToken=${encodeURIComponent(linkToken)}` - : DISPLAY_WS_PATH, - }; - - if (protocol !== "ssh") { - response.connectParams = { - token: await live.createViewerToken( - protocol, - share.sessionId, - share.permissionLevel === "read-only", - share.ownerUserId, - ), - }; - } - - try { - await shares.touchUsage(share.id); - await shares.recordParticipantJoin(share.id, null, "Guest"); - } catch { - // Bookkeeping never fails the resolve. - } - - res.json(response); - } catch (error) { - ctx.log.error( - "Failed to resolve session share link", - error instanceof Error ? error : undefined, - ); - res.status(500).json({ error: "Failed to resolve share link" }); - } - }); - - /** - * @openapi - * /plugin-api/session-sharing/create: - * post: - * summary: Create a session share (link or targeted user) - * description: Mints a share grant for a live terminal/RDP/VNC/Telnet session. Caller must own the live session. - * tags: - * - Session Sharing - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - hostId - * - sessionId - * - protocol - * - shareType - * - permissionLevel - * properties: - * hostId: - * type: integer - * sessionId: - * type: string - * tabInstanceId: - * type: string - * protocol: - * type: string - * enum: [ssh, rdp, vnc, telnet] - * shareType: - * type: string - * enum: [link, user] - * targetUserId: - * type: string - * permissionLevel: - * type: string - * enum: [read-only, read-write] - * expiryHours: - * type: number - * responses: - * 200: - * description: Share created - * 400: - * description: Invalid request - * 403: - * description: Sharing disabled, or caller does not own the session - * 500: - * description: Server error - */ - router.post("/create", requireUse, async (req: Request, res: Response) => { - try { - const userId = actorOf(ctx); - const { - hostId, - sessionId, - tabInstanceId, - protocol, - shareType, - targetUserId, - permissionLevel, - expiryHours, - } = req.body ?? {}; - - if ( - !hostId || - !sessionId || - !protocol || - !shareType || - !permissionLevel - ) { - return res.status(400).json({ error: "Missing required fields" }); - } - if (!PROTOCOLS.includes(protocol)) { - return res.status(400).json({ error: "Invalid protocol" }); - } - if (!["link", "user"].includes(shareType)) { - return res.status(400).json({ error: "Invalid shareType" }); - } - if (!["read-only", "read-write"].includes(permissionLevel)) { - return res.status(400).json({ error: "Invalid permissionLevel" }); - } - if (shareType === "user" && !targetUserId) { - return res - .status(400) - .json({ error: "targetUserId is required for user shares" }); - } - - const numericHostId = Number(hostId); - if (!(await deps.isSharingEnabledForHost(numericHostId))) { - return res - .status(403) - .json({ error: "Session sharing is disabled for this host" }); - } - if (!(await live.isOwnedBy(protocol, String(sessionId), userId))) { - return res - .status(403) - .json({ error: "You do not own this live session" }); - } - - if (shareType === "user") { - const target = String(targetUserId); - const access = (await directory.userExists(target)) - ? await ctx.asUser(target, () => - ctx.hosts.checkAccess(numericHostId, "connect"), - ) - : { hasAccess: false }; - if (!access.hasAccess) { - return res.status(403).json({ - error: "Target user does not have access to this host", - }); - } - } - - const created = await shares.create({ - id: crypto.randomUUID(), - hostId: numericHostId, - ownerUserId: userId, - protocol, - sessionId: String(sessionId), - tabInstanceId: tabInstanceId ?? null, - shareType, - targetUserId: shareType === "user" ? String(targetUserId) : null, - linkToken: - shareType === "link" - ? crypto.randomBytes(24).toString("base64url") - : null, - permissionLevel, - expiresAt: computeExpiresAt(expiryHours), - }); - - res.json({ - shareId: created.id, - linkToken: created.linkToken, - expiresAt: created.expiresAt, - }); - } catch (error) { - ctx.log.error( - "Failed to create session share", - error instanceof Error ? error : undefined, - ); - res.status(500).json({ error: "Failed to create session share" }); - } - }); - - /** - * @openapi - * /plugin-api/session-sharing/host/{hostId}/active: - * get: - * summary: List active session shares for a host - * description: Returns active (non-revoked, non-expired) shares owned by the caller for the given host. - * tags: - * - Session Sharing - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: hostId - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: List of active shares - * 400: - * description: Invalid host id - * 500: - * description: Server error - */ - router.get( - "/host/:hostId/active", - requireUse, - async (req: Request, res: Response) => { - try { - const userId = actorOf(ctx); - const hostId = Number.parseInt(String(req.params.hostId), 10); - if (!hostId || Number.isNaN(hostId)) { - return res.status(400).json({ error: "Invalid host ID" }); - } - res.json({ - shares: await shares.findActiveSharesForHost(hostId, userId), - }); - } catch (error) { - ctx.log.error( - "Failed to list session shares", - error instanceof Error ? error : undefined, - ); - res.status(500).json({ error: "Failed to list session shares" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/shared-with-me: - * get: - * summary: List live SSH sessions other users shared with the caller - * description: Active user shares aimed at the caller whose session is still live, in the shape of /open-tabs/active-sessions. - * tags: - * - Session Sharing - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Shared sessions - * 500: - * description: Server error - */ - router.get( - "/shared-with-me", - requireUse, - async (_req: Request, res: Response) => { - try { - const userId = actorOf(ctx); - const result = []; - for (const share of await shares.findSharesTargetingUser(userId)) { - if (share.protocol !== "ssh") continue; - const session = await live.getSession("ssh", share.sessionId); - if (!session?.isConnected) continue; - result.push({ - sessionId: session.id, - hostId: session.hostId, - hostName: session.hostName, - tabInstanceId: session.tabInstanceId, - isConnected: session.isConnected, - createdAt: session.createdAt, - isOwnSession: false, - sharedByUsername: await directory.username(share.ownerUserId), - permissionLevel: share.permissionLevel, - shareId: share.id, - }); - } - res.json(result); - } catch (error) { - ctx.log.error( - "Failed to list shared sessions", - error instanceof Error ? error : undefined, - ); - res.status(500).json({ error: "Failed to list shared sessions" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/directory: - * get: - * summary: Users and roles to share with or invite - * tags: - * - Session Sharing - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Users (id, username) and roles (id, name, displayName) - * 500: - * description: Server error - */ - router.get("/directory", requireUse, async (_req: Request, res: Response) => { - try { - res.json({ - users: await directory.listUsers(), - roles: await directory.listRoles(), - }); - } catch (error) { - ctx.log.error( - "Failed to list users and roles", - error instanceof Error ? error : undefined, - ); - res.status(500).json({ error: "Failed to list users and roles" }); - } - }); - - /** - * @openapi - * /plugin-api/session-sharing/{shareId}: - * delete: - * summary: Revoke a session share - * description: Revokes a share. Owner or admin only. Best-effort kick of live SSH participants; remote desktop viewers are not force-disconnected. - * tags: - * - Session Sharing - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: shareId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Share revoked - * 403: - * description: Not authorized to revoke this share - * 404: - * description: Share not found - * 500: - * description: Server error - */ - router.delete( - "/:shareId", - requireUse, - async (req: Request, res: Response) => { - try { - const userId = actorOf(ctx); - const shareId = String(req.params.shareId); - const share = await shares.findById(shareId); - if (!share) { - return res.status(404).json({ error: "Share not found" }); - } - - let revoked = await shares.revoke(shareId, userId); - if (!revoked && (await directory.isAdmin(userId))) { - revoked = await shares.revokeAny(shareId); - } - if (!revoked) { - return res - .status(403) - .json({ error: "Not authorized to revoke this share" }); - } - - // guacamole-lite has no kick, so a revoked remote desktop link only - // blocks new resolves until the viewer's own socket ends. - if (share.protocol === "ssh") { - await live.ownerEndSession( - "ssh", - share.sessionId, - "Session share revoked by owner", - share.ownerUserId, - ); - } - - res.json({ success: true }); - } catch (error) { - ctx.log.error( - "Failed to revoke session share", - error instanceof Error ? error : undefined, - ); - res.status(500).json({ error: "Failed to revoke session share" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/session-sharing/{shareId}/end: - * post: - * summary: End a shared session for all participants - * description: Owner-only. Terminates the underlying session and notifies joined participants. Remote desktop viewers are not force-disconnected. - * tags: - * - Session Sharing - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: shareId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Session ended - * 403: - * description: Not the owner of this share - * 404: - * description: Share not found - * 500: - * description: Server error - */ - router.post( - "/:shareId/end", - requireUse, - async (req: Request, res: Response) => { - try { - const userId = actorOf(ctx); - const share = await shares.findById(String(req.params.shareId)); - if (!share) { - return res.status(404).json({ error: "Share not found" }); - } - if (share.ownerUserId !== userId) { - return res.status(403).json({ error: "Not the owner of this share" }); - } - if (share.protocol === "ssh") { - await live.ownerEndSession( - "ssh", - share.sessionId, - "Session ended by owner", - ); - } - res.json({ success: true }); - } catch (error) { - ctx.log.error( - "Failed to end shared session", - error instanceof Error ? error : undefined, - ); - res.status(500).json({ error: "Failed to end shared session" }); - } - }, - ); -} diff --git a/plugins/session-sharing/src/backend/tables.ts b/plugins/session-sharing/src/backend/tables.ts deleted file mode 100644 index 515bbf18c..000000000 --- a/plugins/session-sharing/src/backend/tables.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - adoptLegacyTable, - boolean, - defineTable, - id, - integer, - refHost, - refUser, - text, - timestamp, - varchar, -} from "@termix/plugin-sdk/db"; - -/* - * Adopted from core's session_shares, session_share_participants, - * collab_rooms and collab_room_members, so column and index names are the - * legacy ones and the rename carries every row across. - * - * Columns that point at a user or host but may be null or set null on delete, - * and the links between these four tables, are plain columns here: the SDK - * only references users and ssh_data, cascading. The adoption migrations keep - * the legacy FOREIGN KEY clauses by hand, as fleets does. - */ - -/** - * A grant to join one live session: a link for anonymous guests, a user, or - * a collab room's stage ("room"). - */ -export const shares = adoptLegacyTable( - "session_shares", - defineTable( - "shares", - { - id: varchar().primaryKey(), - hostId: refHost(), - ownerUserId: refUser(), - protocol: text().notNull(), - // The terminal's session id for SSH, guacd's connection id otherwise. - // Neither is a row, so there is no foreign key. - sessionId: varchar().notNull(), - tabInstanceId: text(), - shareType: text().notNull(), - targetUserId: varchar(), - linkToken: varchar().unique(), - permissionLevel: text().notNull().default("read-only"), - createdAt: timestamp().notNull().defaultNow(), - expiresAt: text().notNull(), - revokedAt: text(), - lastJoinedAt: text(), - joinCount: integer().notNull().default(0), - }, - { - indexes: [ - { name: "idx_session_shares_session_id", columns: ["sessionId"] }, - { name: "idx_session_shares_host_id", columns: ["hostId"] }, - ], - }, - ), -); - -/** Who joined through a share, for the owner's record. */ -export const shareParticipants = adoptLegacyTable( - "session_share_participants", - defineTable("share_participants", { - id: id(), - shareId: varchar().notNull(), - userId: varchar(), - guestLabel: text(), - joinedAt: timestamp().notNull().defaultNow(), - leftAt: text(), - }), -); - -/** - * A collaboration room: members watching one stage, the live session the - * current presenter shows through a "room" share. - */ -export const rooms = adoptLegacyTable( - "collab_rooms", - defineTable( - "rooms", - { - id: varchar().primaryKey(), - name: text().notNull(), - ownerUserId: refUser(), - persistent: boolean().notNull().default(false), - presenterUserId: varchar(), - stageProtocol: text(), - stageHostId: integer(), - stageShareId: varchar(), - guestLinkToken: varchar(), - createdAt: timestamp().notNull().defaultNow(), - endedAt: text(), - }, - { - indexes: [{ name: "idx_collab_rooms_owner", columns: ["ownerUserId"] }], - uniques: [ - { name: "idx_collab_rooms_guest_token", columns: ["guestLinkToken"] }, - ], - }, - ), -); - -export const roomMembers = adoptLegacyTable( - "collab_room_members", - defineTable( - "room_members", - { - id: id(), - roomId: varchar().notNull(), - userId: refUser(), - roomRole: text().notNull().default("member"), - addedBy: varchar(), - createdAt: timestamp().notNull().defaultNow(), - }, - { - uniques: [ - { - name: "idx_collab_room_members_room_user", - columns: ["roomId", "userId"], - }, - ], - indexes: [{ name: "idx_collab_room_members_user", columns: ["userId"] }], - }, - ), -); - -export const tables = [shares, shareParticipants, rooms, roomMembers]; diff --git a/plugins/session-sharing/src/frontend/CollabGuestView.tsx b/plugins/session-sharing/src/frontend/CollabGuestView.tsx deleted file mode 100644 index af8f2f642..000000000 --- a/plugins/session-sharing/src/frontend/CollabGuestView.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { AlertCircle, Eye, Presentation } from "lucide-react"; -import { RemoteDisplay } from "./shared"; -import { GuestTerminalView } from "./SharedSessionView"; -import { resolveCollabGuestStage, type CollabGuestStage } from "./api"; - -const POLL_MS = 5000; - -/** - * Anonymous guest page for a collab room (?view=collab-guest&token=...). - * Guests have no account and no event channel, so they poll the public - * resolve endpoint and remount the viewer whenever the stage share changes. - */ -export default function CollabGuestView() { - const { t } = useTranslation(); - const token = new URLSearchParams(window.location.search).get("token"); - const [roomName, setRoomName] = useState(null); - const [stage, setStage] = useState(null); - const [error, setError] = useState(null); - const stageShareIdRef = useRef(null); - - useEffect(() => { - if (!token) { - setError(t("collab.guest.linkInvalid")); - return; - } - let cancelled = false; - const poll = async () => { - try { - const result = await resolveCollabGuestStage(token); - if (cancelled) return; - setRoomName(result.roomName); - setError(null); - const nextShareId = result.stage?.shareId ?? null; - // Tokens are minted per resolve; only swap the viewer on a real change. - if (nextShareId !== stageShareIdRef.current) { - stageShareIdRef.current = nextShareId; - setStage(result.stage); - } - } catch { - if (!cancelled) setError(t("collab.guest.linkInvalid")); - } - }; - void poll(); - const timer = setInterval(() => void poll(), POLL_MS); - return () => { - cancelled = true; - clearInterval(timer); - }; - }, [token, t]); - - return ( -
-
- - - {roomName ?? t("collab.guest.title")} - -
- - - {t("sessionSharing.guestView.readOnlyBadge")} - -
-
-
- {error ? ( - } text={error} /> - ) : !stage ? ( - } - text={t("collab.guest.waiting")} - /> - ) : stage.protocol === "ssh" ? ( - - ) : stage.connectParams?.token ? ( - - ) : null} -
-
- ); -} - -function Note({ icon, text }: { icon: React.ReactNode; text: string }) { - return ( -
- {icon} -

{text}

-
- ); -} diff --git a/plugins/session-sharing/src/frontend/CollabMembersSidebar.tsx b/plugins/session-sharing/src/frontend/CollabMembersSidebar.tsx deleted file mode 100644 index 1a52e0975..000000000 --- a/plugins/session-sharing/src/frontend/CollabMembersSidebar.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { - Check, - Crown, - Eye, - Hand, - MonitorUp, - MousePointerClick, - UserMinus, - Users, - X, -} from "lucide-react"; -import { Badge } from "@termix/plugin-sdk/ui"; -import { Button } from "@termix/plugin-sdk/ui"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@termix/plugin-sdk/ui"; -import type { CollabControlRequest, CollabRoomDetail } from "./api"; - -interface CollabMembersSidebarProps { - detail: CollabRoomDetail; - onClose: () => void; - onInvite: () => void; - onControl: (userId: string | null) => Promise; - onDismissRequest: (userId: string) => Promise; - onRemoveMember: (userId: string) => Promise; - onRemoveGuests: () => Promise; -} - -export function CollabMembersSidebar({ - detail, - onClose, - onInvite, - onControl, - onDismissRequest, - onRemoveMember, - onRemoveGuests, -}: CollabMembersSidebarProps) { - const { t } = useTranslation(); - const [removing, setRemoving] = useState<{ - userId: string; - username: string; - } | null>(null); - const [busyUserId, setBusyUserId] = useState(null); - const guests = detail.guests ?? []; - const onlineIds = new Set(detail.online.map((user) => user.userId)); - const presenterUserId = detail.stage.presenterUserId; - const canManageControl = detail.isHost || presenterUserId === detail.me; - const canControl = - canManageControl && - detail.stage.protocol === "ssh" && - !!detail.stage.shareId; - - async function run(userId: string, action: () => Promise) { - setBusyUserId(userId); - try { - await action(); - } finally { - setBusyUserId(null); - } - } - - async function grant(request: CollabControlRequest) { - await run(request.userId, async () => { - await onControl(request.userId); - }); - } - - return ( - - ); -} diff --git a/plugins/session-sharing/src/frontend/CollabPanel.tsx b/plugins/session-sharing/src/frontend/CollabPanel.tsx deleted file mode 100644 index ff750a585..000000000 --- a/plugins/session-sharing/src/frontend/CollabPanel.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { toast } from "sonner"; -import { Loader2, Plus, Presentation, RefreshCw } from "lucide-react"; -import { Button } from "@termix/plugin-sdk/ui"; -import { Input } from "@termix/plugin-sdk/ui"; -import { Badge } from "@termix/plugin-sdk/ui"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@termix/plugin-sdk/ui"; -import type { CollabRoom } from "./api"; -import type { MeetingBackend } from "./meeting-backend"; -import { getErrorMessage } from "./shared"; - -export function CollabPanel({ - onOpenRoom, - backend, -}: { - onOpenRoom: (room: CollabRoom) => void; - backend: MeetingBackend; -}) { - const { t } = useTranslation(); - const { createCollabRoom, listCollabRooms } = backend.api; - const [rooms, setRooms] = useState([]); - const [loading, setLoading] = useState(true); - const [createOpen, setCreateOpen] = useState(false); - const [name, setName] = useState(""); - const [persistent, setPersistent] = useState(false); - const [creating, setCreating] = useState(false); - - const refresh = useCallback(async () => { - try { - const result = await listCollabRooms(); - setRooms(result.rooms ?? []); - } catch { - /* the list stays as-is */ - } finally { - setLoading(false); - } - }, [backend]); - - useEffect(() => { - void refresh(); - const timer = window.setInterval(() => void refresh(), 30000); - return () => window.clearInterval(timer); - }, [refresh]); - - async function handleCreate() { - if (!name.trim()) return; - setCreating(true); - try { - const { room } = await createCollabRoom(name.trim(), persistent); - toast.success(t("collab.created")); - setCreateOpen(false); - setName(""); - setPersistent(false); - await refresh(); - onOpenRoom(room); - } catch (error) { - toast.error(getErrorMessage(error)); - } finally { - setCreating(false); - } - } - - return ( -
-
- - -
- - {loading ? ( -
- -
- ) : rooms.length === 0 ? ( -

- {t("collab.noRooms")} -

- ) : ( -
- {rooms.map((room) => ( - - ))} -
- )} - - - - - {t("collab.createRoom")} - -
- - setName(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") void handleCreate(); - }} - /> - -
- - - - -
-
-
- ); -} diff --git a/plugins/session-sharing/src/frontend/CollabRoomTab.tsx b/plugins/session-sharing/src/frontend/CollabRoomTab.tsx deleted file mode 100644 index 2fc07dbb0..000000000 --- a/plugins/session-sharing/src/frontend/CollabRoomTab.tsx +++ /dev/null @@ -1,1051 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { - hostProtocols, - useHosts, - useTranslation, - type PluginHostRecord, -} from "@termix/plugin-sdk/frontend"; -import { toast } from "sonner"; -import { - AlertCircle, - Hand, - Link2, - Loader2, - MonitorUp, - Presentation, - Square, - Users, -} from "lucide-react"; -import { Button } from "@termix/plugin-sdk/ui"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@termix/plugin-sdk/ui"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@termix/plugin-sdk/ui"; -import { Input, PluginComponent } from "@termix/plugin-sdk/ui"; -import { CollabMembersSidebar } from "./CollabMembersSidebar"; -import { - RemoteDisplay, - createRemoteSessionToken, - getErrorMessage, - wsTargetForPath, -} from "./shared"; -import type { CollabRoomDetail, CollabStage, DirectoryRole } from "./api"; -import { meetingGuestUrl, type MeetingBackend } from "./meeting-backend"; - -type SharedProtocol = "ssh" | "rdp" | "vnc" | "telnet"; - -function isSharedProtocol(value: string): value is SharedProtocol { - return ( - value === "ssh" || value === "rdp" || value === "vnc" || value === "telnet" - ); -} - -/** A host from the shell's list; the connection fields ride along by name. */ -type SSHHostWithStatus = PluginHostRecord & { - enableSsh?: boolean; - connectionOrigin?: unknown; -}; - -const PING_INTERVAL_MS = 30000; -const POLL_FALLBACK_MS = 15000; - -// The terminal socket the ssh-terminal plugin serves. Authentication rides on -// the jwt cookie the way every terminal WS connection does. -async function roomEventsWsUrl(roomId: string, backend: MeetingBackend) { - const { eventsWsPath } = await backend.api.getCollabRoom(roomId); - await backend.assertCurrent(); - return wsTargetForPath(eventsWsPath, backend.origin); -} - -type PresentDraft = - | { protocol: "ssh"; host: SSHHostWithStatus } - | { - protocol: "rdp" | "vnc" | "telnet"; - host: SSHHostWithStatus; - token: string; - guacamoleConnectionId: string; - }; -type PresentChoice = { - host: SSHHostWithStatus; - protocol: "ssh" | "rdp" | "vnc" | "telnet"; -}; - -export function CollabRoomTab({ - roomId, - isVisible, - backend, -}: { - backend: MeetingBackend; - roomId?: string; - isVisible: boolean; -}) { - const { t } = useTranslation(); - const { - deleteCollabRoom, - endCollabRoom, - dismissCollabControlRequest, - getCollabRoom, - getCollabStage, - inviteCollabMembers, - presentCollabStage, - requestCollabStageControl, - removeCollabMember, - removeCollabGuests, - setCollabGuestLink, - setCollabStageControl, - stopCollabStage, - getDirectory, - } = backend.api; - const [detail, setDetail] = useState(null); - const [stage, setStage] = useState(null); - const [ended, setEnded] = useState(false); - const [loadError, setLoadError] = useState(null); - const [draft, setDraft] = useState(null); - const [presentOpen, setPresentOpen] = useState(false); - const { loaded: hostsLoaded } = useHosts(); - const presentLoading = !hostsLoaded; - const [inviteOpen, setInviteOpen] = useState(false); - const [membersOpen, setMembersOpen] = useState(true); - const [endOpen, setEndOpen] = useState(false); - const [deleteOpen, setDeleteOpen] = useState(false); - const [deleting, setDeleting] = useState(false); - const [takeoverChoice, setTakeoverChoice] = useState( - null, - ); - const [guestLinkToken, setGuestLinkToken] = useState(null); - const [guestLinkAction, setGuestLinkAction] = useState< - "disable" | "rotate" | null - >(null); - const [hostSearch, setHostSearch] = useState(""); - const [inviteSearch, setInviteSearch] = useState(""); - const { hosts: shellHosts } = useHosts(); - const hosts = shellHosts as SSHHostWithStatus[]; - const [users, setUsers] = useState>( - [], - ); - const [inviteSelection, setInviteSelection] = useState>( - new Set(), - ); - const [roles, setRoles] = useState([]); - const [roleSelection, setRoleSelection] = useState>(new Set()); - const draftRef = useRef(null); - draftRef.current = draft; - const stageKeyRef = useRef(null); - const presenterRef = useRef(null); - const refreshSequence = useRef(0); - - const refresh = useCallback(async () => { - if (!roomId) return; - const sequence = ++refreshSequence.current; - try { - const nextDetail = await getCollabRoom(roomId); - if (sequence !== refreshSequence.current) return; - // Only discard a confirmed presentation that another member took over. - if ( - presenterRef.current === nextDetail.me && - nextDetail.stage.presenterUserId !== nextDetail.me - ) { - setDraft(null); - } - presenterRef.current = nextDetail.stage.presenterUserId; - setDetail(nextDetail); - setLoadError(null); - // Presenting locally? The local session is the stage - don't join it. - if ( - nextDetail.stage.shareId && - nextDetail.stage.presenterUserId !== nextDetail.me - ) { - // A guac viewer reconnects whenever its token changes, so the stage - // is only re-resolved when the share or my control actually changed. - const stageKey = `${nextDetail.stage.shareId}:${ - nextDetail.controllerUserId === nextDetail.me - }`; - if (stageKeyRef.current !== stageKey) { - stageKeyRef.current = stageKey; - const { stage: resolved } = await getCollabStage(roomId); - setStage(resolved); - } - } else if (!nextDetail.stage.shareId) { - stageKeyRef.current = null; - setStage(null); - // The stage was cleared elsewhere; stop presenting locally too. - if (draftRef.current) setDraft(null); - } - } catch (error) { - if (sequence === refreshSequence.current) { - setLoadError(getErrorMessage(error)); - } - } - }, [roomId, backend]); - - useEffect(() => { - void refresh(); - }, [refresh]); - - // Live room events. Poll only while the socket is unavailable. - useEffect(() => { - if (!roomId) return; - let ws: WebSocket | null = null; - let pingTimer: ReturnType | null = null; - let pollTimer: ReturnType | null = null; - let reconnectTimer: ReturnType | null = null; - let reconnectAttempt = 0; - let cancelled = false; - - const startPolling = () => { - pollTimer ??= setInterval(() => void refresh(), POLL_FALLBACK_MS); - }; - const stopPolling = () => { - if (pollTimer) clearInterval(pollTimer); - pollTimer = null; - }; - const connect = async () => { - if (cancelled) return; - try { - const url = await roomEventsWsUrl(roomId, backend); - if (cancelled) return; - if (!url) throw new Error("No terminal endpoint is available"); - ws = new WebSocket(url.url, url.protocols); - } catch { - startPolling(); - reconnectTimer = setTimeout(() => void connect(), 5000); - return; - } - ws.onopen = () => { - reconnectAttempt = 0; - stopPolling(); - ws?.send( - JSON.stringify({ type: "collab_subscribe", data: { roomId } }), - ); - pingTimer = setInterval(() => { - if (ws?.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: "ping" })); - } - }, PING_INTERVAL_MS); - }; - ws.onmessage = (event) => { - if (cancelled) return; - let msg: { type?: string; roomId?: string }; - try { - msg = JSON.parse(event.data); - } catch { - return; - } - if (msg.roomId !== roomId) return; - switch (msg.type) { - case "collab_online": - case "collab_members_changed": - case "collab_stage_changed": - case "collab_control_changed": - case "collab_control_requested": - case "collab_control_requests_changed": - void refresh(); - break; - case "collab_room_ended": - setEnded(true); - break; - default: - break; - } - }; - ws.onclose = () => { - if (cancelled) return; - if (pingTimer) clearInterval(pingTimer); - pingTimer = null; - startPolling(); - const delay = Math.min(1000 * 2 ** reconnectAttempt++, 15000); - reconnectTimer = setTimeout(() => void connect(), delay); - }; - ws.onerror = () => ws?.close(); - }; - - void connect(); - return () => { - cancelled = true; - if (pingTimer) clearInterval(pingTimer); - if (pollTimer) clearInterval(pollTimer); - if (reconnectTimer) clearTimeout(reconnectTimer); - ws?.close(); - }; - }, [roomId, refresh]); - - const me = detail?.me; - const isHost = detail?.isHost ?? false; - const controllerUserId = detail?.controllerUserId ?? null; - const presenterUserId = detail?.stage.presenterUserId ?? null; - const iAmPresenter = !!me && presenterUserId === me; - const presenterName = detail?.members.find( - (member) => member.userId === presenterUserId, - )?.username; - - async function changeControl(targetId: string | null) { - if (!roomId) return; - try { - await setCollabStageControl(roomId, targetId); - await refresh(); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - async function dismissControlRequest(targetId: string) { - if (!roomId) return; - try { - await dismissCollabControlRequest(roomId, targetId); - await refresh(); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - async function removeGuests() { - if (!roomId) return; - try { - await removeCollabGuests(roomId); - await refresh(); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - async function removeMember(targetId: string) { - if (!roomId) return; - try { - await removeCollabMember(roomId, targetId); - await refresh(); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - async function toggleOwnControlRequest() { - if (!roomId || !me) return; - try { - const existing = detail?.controlRequests.some( - (request) => request.userId === me, - ); - if (existing) await dismissCollabControlRequest(roomId, me); - else await requestCollabStageControl(roomId); - await refresh(); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - function openPresentDialog() { - setPresentOpen(true); - } - - function choosePresent( - host: SSHHostWithStatus, - protocol: "ssh" | "rdp" | "vnc" | "telnet", - ) { - if (presenterUserId && !iAmPresenter) { - setPresentOpen(false); - setTakeoverChoice({ host, protocol }); - return; - } - void startPresent(host, protocol); - } - - async function startPresent( - host: SSHHostWithStatus, - protocol: "ssh" | "rdp" | "vnc" | "telnet", - ) { - if (!roomId) return; - setPresentOpen(false); - try { - await backend.assertCurrent(); - if (backend.origin === "remote") { - if (!host.syncId) throw new Error(t("collab.hostNotSynced")); - const resolved = await backend.api.resolveHost(host.syncId); - host = { ...host, id: String(resolved.id), connectionOrigin: "remote" }; - } else { - host = { ...host, connectionOrigin: "local" }; - } - if (protocol === "ssh") { - setDraft({ protocol, host }); - return; - } - const response = await createRemoteSessionToken( - Number(host.id), - backend.origin, - protocol, - ); - if (!response?.connectionId) { - toast.error(t("collab.stageLoading")); - return; - } - setDraft({ - protocol, - host, - token: response.token, - guacamoleConnectionId: response.connectionId, - }); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - async function registerStage( - protocol: string, - sessionId: string, - hostId: number, - ) { - if (!roomId) return; - try { - await presentCollabStage(roomId, { protocol, sessionId, hostId }); - void refresh(); - } catch (error) { - toast.error(getErrorMessage(error)); - setDraft(null); - } - } - - async function handleStop() { - if (!roomId) return; - try { - await stopCollabStage(roomId); - setDraft(null); - void refresh(); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - async function handleEnd() { - if (!roomId) return; - try { - await endCollabRoom(roomId); - setDraft(null); - if (!detail?.room.persistent) setEnded(true); - void refresh(); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - async function handleDelete() { - if (!roomId) return; - setDeleting(true); - try { - await deleteCollabRoom(roomId); - setDraft(null); - setEnded(true); - } catch (error) { - toast.error(getErrorMessage(error)); - } finally { - setDeleting(false); - } - } - - async function openInviteDialog() { - setInviteOpen(true); - setInviteSelection(new Set()); - setRoleSelection(new Set()); - try { - const directory = await getDirectory(); - setUsers(directory.users); - setRoles(directory.roles); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - async function handleGuestLink(enabled: boolean) { - if (!roomId) return; - try { - const result = await setCollabGuestLink(roomId, enabled); - setGuestLinkToken(result.guestLinkToken); - await refresh(); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - async function copyGuestLink(token: string) { - try { - await backend.assertCurrent(); - const url = meetingGuestUrl(backend.publicUrl, token); - if (!url) throw new Error(t("collab.linkRequiresServer")); - await navigator.clipboard.writeText(url); - toast.success(t("collab.linkCopied")); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - async function handleInvite() { - if (!roomId || (inviteSelection.size === 0 && roleSelection.size === 0)) - return; - try { - await inviteCollabMembers(roomId, { - userIds: Array.from(inviteSelection), - roleIds: Array.from(roleSelection), - }); - toast.success(t("collab.invited")); - setInviteOpen(false); - void refresh(); - } catch (error) { - toast.error(getErrorMessage(error)); - } - } - - if (!roomId) { - return ( -
-
- -

{t("collab.reopenFromPanel")}

-
-
- ); - } - - if (ended) { - return ( -
-
- -

{t("collab.roomEnded")}

-
-
- ); - } - - if (!detail && loadError) { - return ( -
-
- -

{loadError}

- -
-
- ); - } - - const memberIds = new Set(detail?.members.map((member) => member.userId)); - const normalizedInviteSearch = inviteSearch.trim().toLocaleLowerCase(); - const invitableUsers = users.filter( - (user) => - !memberIds.has(user.id) && - user.username.toLocaleLowerCase().includes(normalizedInviteSearch), - ); - const normalizedHostSearch = hostSearch.trim().toLocaleLowerCase(); - const filteredHosts = hosts.filter((host) => - host.name.toLocaleLowerCase().includes(normalizedHostSearch), - ); - - return ( -
- {loadError && ( -
- - {loadError} - -
- )} - {/* Header: room identity + primary controls */} -
- - - {detail?.room.name} - -
-
- {!!detail?.stage.shareId && - detail.stage.protocol === "ssh" && - !iAmPresenter && - !draft && ( - - )} - {(iAmPresenter || draft || (isHost && presenterUserId)) && ( - - )} - - - {isHost && ( - - )} - {isHost && detail?.room.persistent && ( - - )} -
-
- - {isHost && ( -
- - - {detail?.room.guestLinkEnabled - ? t("collab.guestLinkOn") - : t("collab.guestLinkOff")} - - {guestLinkToken && ( - - )} - {detail?.room.guestLinkEnabled && !guestLinkToken && ( - - )} - -
- )} - -
- {/* Stage */} -
- {draft ? ( - draft.protocol === "ssh" ? ( - - void registerStage("ssh", sessionId, Number(draft.host.id)) - } - /> - ) : ( - - void registerStage( - draft.protocol, - draft.guacamoleConnectionId, - Number(draft.host.id), - ) - } - onError={(err) => { - toast.error(err); - setDraft(null); - }} - /> - ) - ) : stage && stage.protocol && !iAmPresenter ? ( - <> - {presenterName && ( -
- {t("collab.presenterLabel", { name: presenterName })} -
- )} - {stage.protocol === "ssh" ? ( - - ) : stage.connectParams?.token ? ( - - ) : ( - - )} - - ) : iAmPresenter && !draft ? ( - - ) : ( - - )} -
- {membersOpen && detail && ( - setMembersOpen(false)} - onInvite={() => void openInviteDialog()} - onControl={changeControl} - onDismissRequest={dismissControlRequest} - onRemoveMember={removeMember} - onRemoveGuests={removeGuests} - /> - )} -
- - {/* Present dialog */} - - - - {t("collab.presentTitle")} - - setHostSearch(event.target.value)} - /> -
- {presentLoading && ( -
- -
- )} - {!presentLoading && filteredHosts.length === 0 && ( - - )} - {filteredHosts.map((host) => { - const protocols = hostProtocols( - host as unknown as PluginHostRecord, - ).filter(isSharedProtocol); - if (protocols.length === 0) return null; - return ( -
- {host.name} - {protocols.map((protocol) => ( - - ))} -
- ); - })} -
-
-
- - {/* Invite dialog */} - - - - {t("collab.inviteTitle")} - - setInviteSearch(event.target.value)} - /> -
- {roles.length > 0 && ( - - {t("collab.roles")} - - )} - {roles.map((role) => ( - - ))} - - {t("collab.users")} - - {invitableUsers.map((user) => ( - - ))} -
- - - - -
-
- - - - - {t("collab.endConfirmTitle")} - - {t( - detail?.room.persistent - ? "collab.endPersistentDescription" - : "collab.endDescription", - { name: detail?.room.name }, - )} - - - - {t("common.cancel")} - void handleEnd()} - > - {t("collab.endRoom")} - - - - - - - - - - {t("collab.deleteConfirmTitle")} - - - {t("collab.deleteDescription", { name: detail?.room.name })} - - - - - {t("common.cancel")} - - void handleDelete()} - > - {t("collab.deleteRoom")} - - - - - - !open && setTakeoverChoice(null)} - > - - - - {t("collab.takeOverConfirmTitle")} - - - {t("collab.takeOverDescription", { name: presenterName })} - - - - {t("common.cancel")} - { - const choice = takeoverChoice; - setTakeoverChoice(null); - if (choice) void startPresent(choice.host, choice.protocol); - }} - > - {t("collab.takeOver")} - - - - - - !open && setGuestLinkAction(null)} - > - - - - {t( - guestLinkAction === "rotate" - ? "collab.rotateLinkConfirmTitle" - : "collab.disableLinkConfirmTitle", - )} - - - {t( - guestLinkAction === "rotate" - ? "collab.rotateLinkDescription" - : "collab.disableLinkDescription", - )} - - - - {t("common.cancel")} - { - const enabled = guestLinkAction === "rotate"; - setGuestLinkAction(null); - void handleGuestLink(enabled); - }} - > - {t( - guestLinkAction === "rotate" - ? "collab.rotateLink" - : "collab.disableLink", - )} - - - - -
- ); -} - -function CenteredNote({ text }: { text: string }) { - return ( -
-

{text}

-
- ); -} diff --git a/plugins/session-sharing/src/frontend/ParticipantsOverlay.tsx b/plugins/session-sharing/src/frontend/ParticipantsOverlay.tsx deleted file mode 100644 index 751b46237..000000000 --- a/plugins/session-sharing/src/frontend/ParticipantsOverlay.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { - ParticipantsBadge, - type SessionParticipantInfo, -} from "./SharedSessionView"; - -interface ParticipantsOverlayProps { - subscribe: ( - listener: (message: { type: string; [key: string]: unknown }) => void, - ) => () => void; -} - -export function ParticipantsOverlay({ subscribe }: ParticipantsOverlayProps) { - const { t } = useTranslation(); - const [participants, setParticipants] = useState( - [], - ); - - useEffect( - () => - subscribe((message) => { - if ( - message.type === "participants" && - Array.isArray(message.participants) - ) { - setParticipants(message.participants as SessionParticipantInfo[]); - } else if (message.type === "session_closed") { - setParticipants([]); - } - }), - [subscribe], - ); - - return ( - - ); -} diff --git a/plugins/session-sharing/src/frontend/ShareSessionModal.tsx b/plugins/session-sharing/src/frontend/ShareSessionModal.tsx deleted file mode 100644 index 08d204b67..000000000 --- a/plugins/session-sharing/src/frontend/ShareSessionModal.tsx +++ /dev/null @@ -1,450 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { Check, Copy, Link2, Search, Shield, User, Users } from "lucide-react"; -import { toast } from "sonner"; -import { Button } from "@termix/plugin-sdk/ui"; -import { Input } from "@termix/plugin-sdk/ui"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@termix/plugin-sdk/ui"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@termix/plugin-sdk/ui"; -import { - createSessionShare, - getActiveSessionShares, - revokeSessionShare, - type SessionShareProtocol, - type SessionSharePermissionLevel, - type SessionShareRecord, - getDirectory, - type SessionOrigin, -} from "./api"; -import { guestViewUrl } from "./meeting-backend"; - -const EXPIRY_PRESETS = [ - { key: "oneHour", hours: 1 }, - { key: "oneDay", hours: 24 }, - { key: "sevenDays", hours: 24 * 7 }, - { key: "thirtyDays", hours: 24 * 30 }, - { key: "custom", hours: undefined }, -] as const; - -type ExpiryPresetKey = (typeof EXPIRY_PRESETS)[number]["key"]; - -export function ShareSessionModal({ - open, - onClose, - hostId, - sessionId, - protocol, - tabInstanceId, - origin, - resolvePublicUrl, -}: { - open: boolean; - onClose: () => void; - hostId: number; - sessionId: string | null; - protocol: SessionShareProtocol; - tabInstanceId?: string; - origin?: SessionOrigin; - resolvePublicUrl?: (origin?: SessionOrigin) => Promise; -}) { - const { t } = useTranslation(); - const [mode, setMode] = useState<"link" | "user">("link"); - const [permissionLevel, setPermissionLevel] = - useState("read-only"); - const [expiryPreset, setExpiryPreset] = useState("oneDay"); - const [customHours, setCustomHours] = useState(""); - const [search, setSearch] = useState(""); - const [users, setUsers] = useState<{ id: string; username: string }[]>([]); - const [selectedUserId, setSelectedUserId] = useState(null); - const [submitting, setSubmitting] = useState(false); - const [createdLink, setCreatedLink] = useState(null); - const [shares, setShares] = useState([]); - const [sharesLoaded, setSharesLoaded] = useState(false); - - useEffect(() => { - if (!open) return; - setMode("link"); - setPermissionLevel("read-only"); - setExpiryPreset("oneDay"); - setCustomHours(""); - setSearch(""); - setSelectedUserId(null); - setCreatedLink(null); - setSharesLoaded(false); - setShares([]); - }, [open, sessionId]); - - useEffect(() => { - if (!open || sharesLoaded) return; - setSharesLoaded(true); - Promise.all([ - getDirectory(origin).catch(() => ({ users: [] })), - getActiveSessionShares(hostId, origin).catch(() => ({ shares: [] })), - ]).then(([usersRes, sharesRes]) => { - setUsers( - (usersRes.users ?? []).map((u) => ({ - id: String(u.id), - username: u.username, - })), - ); - setShares(sharesRes.shares ?? []); - }); - }, [open, hostId, origin, sharesLoaded]); - - const filteredUsers = useMemo(() => { - const q = search.trim().toLowerCase(); - return q - ? users.filter((u) => u.username.toLowerCase().includes(q)) - : users; - }, [users, search]); - - const expiryHours = (() => { - if (expiryPreset === "custom") { - const hours = Number(customHours); - return Number.isFinite(hours) && hours > 0 ? hours : undefined; - } - return EXPIRY_PRESETS.find((p) => p.key === expiryPreset)?.hours; - })(); - - async function refreshShares() { - try { - const res = await getActiveSessionShares(hostId, origin); - setShares(res.shares ?? []); - } catch { - // silently ignore - } - } - - async function handleCreate() { - if (!sessionId) return; - if (mode === "user" && !selectedUserId) return; - if (expiryPreset === "custom" && !expiryHours) return; - - setSubmitting(true); - try { - const base = - mode === "link" - ? resolvePublicUrl - ? await resolvePublicUrl(origin) - : window.location.href - : null; - if (mode === "link" && !guestViewUrl(base, "shared", "check")) { - toast.error(t("sessionSharing.linkNeedsServer")); - return; - } - const result = await createSessionShare( - { - hostId, - sessionId, - tabInstanceId, - protocol, - shareType: mode, - targetUserId: - mode === "user" ? (selectedUserId ?? undefined) : undefined, - permissionLevel, - expiryHours, - }, - origin, - ); - - if (mode === "link" && result.linkToken) { - setCreatedLink(guestViewUrl(base, "shared", result.linkToken)); - toast.success(t("sessionSharing.linkCreated")); - } else { - toast.success(t("sessionSharing.shareCreated")); - setSelectedUserId(null); - } - await refreshShares(); - } catch (error) { - const status = (error as { status?: number })?.status; - if (mode === "user" && status === 403) { - toast.error(t("sessionSharing.userLacksHostAccess")); - } else { - toast.error(t("sessionSharing.shareFailed")); - } - } finally { - setSubmitting(false); - } - } - - async function handleCopyLink() { - if (!createdLink) return; - try { - await navigator.clipboard.writeText(createdLink); - toast.success(t("sessionSharing.linkCopied")); - } catch { - // clipboard API unavailable, ignore - } - } - - async function handleRevoke(shareId: string) { - try { - await revokeSessionShare(shareId, origin); - setShares((prev) => prev.filter((s) => s.id !== shareId)); - toast.success(t("sessionSharing.revoked")); - } catch { - toast.error(t("sessionSharing.revokeFailed")); - } - } - - return ( - !next && onClose()}> - - - {t("sessionSharing.modalTitle")} - - {mode === "link" - ? t("sessionSharing.linkModeDescription") - : t("sessionSharing.userModeDescription")} - - - -
-
- {(["link", "user"] as const).map((m) => ( - - ))} -
- - {mode === "user" && ( - <> -
- - setSearch(e.target.value)} - className="pl-8" - /> -
-
- {filteredUsers.length === 0 ? ( -
- {t("sessionSharing.noUsersFound")} -
- ) : ( - filteredUsers.map((user) => { - const isSelected = selectedUserId === user.id; - return ( - - ); - }) - )} -
- - )} - -
-
- - {t("sessionSharing.permissionLevel.label")} - - -
- - - - - - - {EXPIRY_PRESETS.map((preset) => ( - setExpiryPreset(preset.key)} - > - {expiryPreset === preset.key ? ( - - ) : ( - - )} - {t(`hosts.sharing.expiry.${preset.key}`)} - - ))} - - -
- - {expiryPreset === "custom" && ( - setCustomHours(e.target.value)} - className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> - )} - -

- {permissionLevel === "read-only" - ? t("sessionSharing.permissionLevel.readOnlyDescription") - : t("sessionSharing.permissionLevel.readWriteDescription")} -

- - - - {createdLink && ( -
- - -
- )} - -
-
- - {t("sessionSharing.activeShares")} - {shares.length > 0 && ( - - ({shares.length}) - - )} -
-
- {shares.length === 0 && ( -
- {t("sessionSharing.noActiveShares")} -
- )} - {shares.map((share) => { - const targetUser = users.find( - (u) => u.id === share.targetUserId, - ); - return ( -
-
- {share.shareType === "link" ? ( - - ) : ( - - )} -
- - {share.shareType === "link" - ? t("sessionSharing.linkShareBadge") - : t("sessionSharing.userShareBadge", { - username: - targetUser?.username ?? - share.targetUserId ?? - "?", - })} - - - - {share.permissionLevel === "read-write" - ? t("sessionSharing.permissionLevel.readWrite") - : t("sessionSharing.permissionLevel.readOnly")} - - - {t("sessionSharing.expiresAt", { - date: new Date(share.expiresAt).toLocaleString(), - })} - - -
-
- -
- ); - })} -
-
-
-
-
- ); -} diff --git a/plugins/session-sharing/src/frontend/SharedSessionView.tsx b/plugins/session-sharing/src/frontend/SharedSessionView.tsx deleted file mode 100644 index 329492583..000000000 --- a/plugins/session-sharing/src/frontend/SharedSessionView.tsx +++ /dev/null @@ -1,374 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { useXTerm } from "react-xtermjs"; -import { FitAddon } from "@xterm/addon-fit"; -import { AlertCircle, Eye, Users } from "lucide-react"; -import { - resolveShareLink, - type ResolvedShareLink, - type ShareLinkErrorKind, -} from "./api"; -import { Loader, RemoteDisplay, wsUrlForPath } from "./shared"; - -const PING_INTERVAL_MS = 30000; - -interface TerminalWsMessage { - type: string; - data?: string; - [key: string]: unknown; -} - -// A shared session link always resolves against the desktop app's embedded -// local backend: joining a session on someone else's remote server is not -// supported from the desktop app. -async function resolveTerminalWsUrl(wsPath: string): Promise { - const url = await wsUrlForPath(wsPath); - if (!url) throw new Error("No terminal endpoint is available"); - return url; -} - -export interface SessionParticipantInfo { - isOwner: boolean; - permissionLevel: "read-write" | "read-only"; - label: string | null; -} - -export function ParticipantsBadge({ - participants, - ownerLabel, -}: { - participants: SessionParticipantInfo[]; - ownerLabel: string; -}) { - if (participants.length < 2) return null; - const names = participants - .map((participant) => - participant.isOwner ? ownerLabel : (participant.label ?? "?"), - ) - .join(", "); - return ( -
- - {participants.length} -
- ); -} - -function ReadOnlyBadge({ label }: { label: string }) { - return ( -
- - {label} -
- ); -} - -function CenteredMessage({ - icon, - message, -}: { - icon: React.ReactNode; - message: string; -}) { - return ( -
- {icon} -

- {message} -

-
- ); -} - -export function GuestTerminalView({ - share, - wsPath, - hideBadges = false, - onParticipantsChange, -}: { - share: Pick; - /** The socket path the backend handed out, query string included. */ - wsPath: string; - /** Suppress the built-in overlay badges when the host page renders its own. */ - hideBadges?: boolean; - onParticipantsChange?: (participants: SessionParticipantInfo[]) => void; -}) { - const { t } = useTranslation(); - const { instance: terminal, ref: xtermRef } = useXTerm(); - const [ended, setEnded] = useState(null); - const [participants, setParticipants] = useState( - [], - ); - const wsRef = useRef(null); - const pingIntervalRef = useRef | null>(null); - const onParticipantsChangeRef = useRef(onParticipantsChange); - onParticipantsChangeRef.current = onParticipantsChange; - - useEffect(() => { - if (!terminal || !xtermRef.current) return; - - terminal.options.theme = { background: "#0c0d0b" }; - - const fitAddon = new FitAddon(); - terminal.loadAddon(fitAddon); - terminal.open(xtermRef.current); - fitAddon.fit(); - - let sharedSize: { cols: number; rows: number } | null = null; - const resizeObserver = new ResizeObserver(() => { - if (sharedSize) terminal.resize(sharedSize.cols, sharedSize.rows); - else fitAddon.fit(); - }); - resizeObserver.observe(xtermRef.current); - - let cancelled = false; - let ws: WebSocket | null = null; - - resolveTerminalWsUrl(wsPath).then((url) => { - if (cancelled) return; - ws = new WebSocket(url); - wsRef.current = ws; - - ws.onopen = () => { - pingIntervalRef.current = setInterval(() => { - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: "ping" })); - } - }, PING_INTERVAL_MS); - }; - - ws.onmessage = (event) => { - let msg: TerminalWsMessage; - try { - msg = JSON.parse(event.data); - } catch { - return; - } - - switch (msg.type) { - case "resized": { - const { cols, rows } = msg; - if ( - typeof cols === "number" && - typeof rows === "number" && - Number.isInteger(cols) && - Number.isInteger(rows) && - cols > 0 && - rows > 0 - ) { - sharedSize = { cols, rows }; - terminal.resize(cols, rows); - } - break; - } - case "data": - if (typeof msg.data === "string") terminal.write(msg.data); - break; - case "participants": - if (Array.isArray(msg.participants)) { - const next = msg.participants as SessionParticipantInfo[]; - setParticipants(next); - onParticipantsChangeRef.current?.(next); - } - break; - case "sessionExpired": - case "sessionTerminatedByOwner": - case "session_ended": - setEnded(t("sessionSharing.guestView.sessionEnded")); - break; - default: - break; - } - }; - - ws.onclose = () => { - setEnded((prev) => prev ?? t("sessionSharing.guestView.sessionEnded")); - }; - - if (share.permissionLevel === "read-write") { - terminal.onData((data) => { - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: "input", data })); - } - }); - } - }); - - return () => { - cancelled = true; - resizeObserver.disconnect(); - if (pingIntervalRef.current) clearInterval(pingIntervalRef.current); - ws?.close(); - wsRef.current = null; - }; - // Deliberately runs once terminal mounts - share/token/permission are stable for the view's lifetime. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [terminal, wsPath]); - - return ( -
- {!hideBadges && ( - <> - - {share.permissionLevel === "read-only" && ( - - )} - - )} - {ended && ( -
- - } - message={ended} - /> -
- )} -
-
- ); -} - -function GuestGuacamoleView({ share }: { share: ResolvedShareLink }) { - const { t } = useTranslation(); - const [connectionError, setConnectionError] = useState(null); - - if (!share.connectParams?.token) { - return ( - - } - message={t("sessionSharing.guestView.linkInvalid")} - /> - ); - } - - return ( -
- {share.permissionLevel === "read-only" && ( - - )} - {connectionError && ( -
- - } - message={connectionError} - /> -
- )} - setConnectionError(err)} - /> -
- ); -} - -export default function SharedSessionView() { - const { t } = useTranslation(); - const [share, setShare] = useState(null); - const [linkToken, setLinkToken] = useState(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const params = new URLSearchParams(window.location.search); - const token = params.get("token"); - if (!token) { - setError(t("sessionSharing.guestView.linkInvalid")); - setLoading(false); - return; - } - setLinkToken(token); - - resolveShareLink(token) - .then((resolved) => setShare(resolved)) - .catch((err) => { - const kind = (err as { kind?: ShareLinkErrorKind })?.kind; - if (kind === "rate-limited") { - setError(t("sessionSharing.guestView.rateLimited")); - } else { - setError(t("sessionSharing.guestView.linkInvalid")); - } - }) - .finally(() => setLoading(false)); - }, [t]); - - return ( -
-
- {loading && } - {!loading && error && ( - - } - message={error} - /> - )} - {!loading && - !error && - share && - linkToken && - (share.protocol === "ssh" ? ( - - ) : ( - - ))} -
-
- ); -} diff --git a/plugins/session-sharing/src/frontend/SharedWithMeSection.tsx b/plugins/session-sharing/src/frontend/SharedWithMeSection.tsx deleted file mode 100644 index d04b63d8f..000000000 --- a/plugins/session-sharing/src/frontend/SharedWithMeSection.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { SquareTerminal } from "lucide-react"; -import { - invokeAction, - useHosts, - usePermission, - useTranslation, - type PluginHostRecord, -} from "@termix/plugin-sdk/frontend"; -import { Badge, Button } from "@termix/plugin-sdk/ui"; -import { getSharedWithMe } from "./api"; - -const POLL_MS = 15_000; - -interface SharedSession { - sessionId: string; - hostId: number; - hostName: string; - isConnected: boolean; - sharedByUsername: string | null; - permissionLevel: string | null; - shareId: string | null; -} - -function isSharedSession(value: unknown): value is SharedSession { - const row = value as Partial | null; - return ( - !!row && - typeof row.sessionId === "string" && - typeof row.hostName === "string" - ); -} - -/** - * The active connections list's "Shared with me" section: live sessions other - * users shared with this one, each joinable in a terminal tab. - */ -export function SharedWithMeSection({ - search = "", - openTabData = [], -}: { - search?: string; - openTabData?: Array>; -}) { - const { t } = useTranslation(); - const { hosts } = useHosts(); - const allowed = usePermission("use"); - const [sessions, setSessions] = useState([]); - - const load = useCallback(() => { - if (!allowed) return; - getSharedWithMe() - .then((rows) => setSessions(rows.filter(isSharedSession))) - .catch(() => setSessions([])); - }, [allowed]); - - useEffect(() => { - load(); - const timer = window.setInterval(load, POLL_MS); - return () => window.clearInterval(timer); - }, [load]); - - const joined = new Set( - openTabData - .map((data) => data.joinSharedSessionId) - .filter((id): id is string => typeof id === "string"), - ); - const q = search.trim().toLowerCase(); - const visible = sessions.filter( - (session) => - !joined.has(session.sessionId) && - (!q || - session.hostName.toLowerCase().includes(q) || - (session.sharedByUsername ?? "").toLowerCase().includes(q)), - ); - if (!allowed || visible.length === 0) return null; - - const join = (session: SharedSession) => { - if (!session.shareId) return; - const host: PluginHostRecord = hosts.find( - (candidate) => String(candidate.id) === String(session.hostId), - ) ?? { - id: String(session.hostId), - name: session.hostName, - ip: "", - port: 0, - username: "", - }; - void invokeAction("terminal.open", host, { - joinSharedSessionId: session.sessionId, - joinShareId: session.shareId, - label: t("connections.sharedSessionLabel", { - hostName: session.hostName, - }), - }); - }; - - return ( -
-
- - {t("connections.sectionSharedWithMe")} - - - {visible.length} - -
- {visible.map((session) => { - const readWrite = session.permissionLevel === "read-write"; - return ( -
-
- -
-
-
- - - {session.hostName} - - - {readWrite - ? t("sessionSharing.permissionLevel.readWrite") - : t("sessionSharing.permissionLevel.readOnly")} - -
- - {t("connections.sharedBy", { - username: session.sharedByUsername ?? "?", - })} - -
- -
- ); - })} -
- ); -} diff --git a/plugins/session-sharing/src/frontend/api.ts b/plugins/session-sharing/src/frontend/api.ts deleted file mode 100644 index e2bedba95..000000000 --- a/plugins/session-sharing/src/frontend/api.ts +++ /dev/null @@ -1,286 +0,0 @@ -import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; - -/** - * The plugin's own /plugin-api/session-sharing client, bound in activate. - * Guest pages use it too: the two guest routes are public. - */ -let client: PluginApiClient | null = null; -let clientFor: ((origin?: unknown) => PluginApiClient) | null = null; - -export function bindApi( - api: PluginApiClient | null, - apiFor?: (origin?: unknown) => PluginApiClient, -): void { - client = api; - clientFor = api ? (apiFor ?? null) : null; -} - -/** The session lives on the backend its tab connected through. */ -function api(origin?: SessionOrigin): PluginApiClient { - if (!client) throw new Error("Session sharing is not running"); - return origin && clientFor ? clientFor(origin) : client; -} - -export type SessionOrigin = "local" | "remote"; - -interface HttpError { - response?: { status?: number; data?: { error?: string } }; -} - -function statusOf(error: unknown): number | undefined { - return (error as HttpError)?.response?.status; -} - -/** The server's own message, like core's handleApiError. */ -function apiError(error: unknown, fallback: string): Error { - const message = (error as HttpError)?.response?.data?.error; - return new Error(message || fallback); -} - -async function call( - request: () => Promise<{ data: T }>, - fallback: string, -): Promise { - try { - return (await request()).data; - } catch (error) { - throw apiError(error, fallback); - } -} - -// --------------------------------------------------------------------------- -// Guest share links -// --------------------------------------------------------------------------- - -export interface ResolvedShareLink { - protocol: "ssh" | "rdp" | "vnc" | "telnet"; - permissionLevel: "read-only" | "read-write"; - wsPath: string; - connectParams?: { token: string }; -} - -export type ShareLinkErrorKind = "not-found" | "rate-limited" | "unknown"; - -export class ShareLinkError extends Error { - constructor( - message: string, - public readonly kind: ShareLinkErrorKind, - ) { - super(message); - this.name = "ShareLinkError"; - } -} - -export async function resolveShareLink( - linkToken: string, -): Promise { - try { - const response = await api().get( - `/resolve/${encodeURIComponent(linkToken)}`, - ); - return response.data; - } catch (error) { - const status = statusOf(error); - if (status === 404) { - throw new ShareLinkError( - "Share link is invalid, expired, or revoked", - "not-found", - ); - } - if (status === 429) { - throw new ShareLinkError( - "Too many attempts, please try again shortly", - "rate-limited", - ); - } - throw new ShareLinkError("Failed to resolve share link", "unknown"); - } -} - -// --------------------------------------------------------------------------- -// Owner-side shares -// --------------------------------------------------------------------------- - -export type SessionShareProtocol = "ssh" | "rdp" | "vnc" | "telnet"; -export type SessionShareType = "link" | "user"; -export type SessionSharePermissionLevel = "read-only" | "read-write"; - -export interface SessionShareRecord { - id: string; - hostId: number; - ownerUserId: string; - protocol: SessionShareProtocol; - sessionId: string; - tabInstanceId: string | null; - shareType: SessionShareType; - targetUserId: string | null; - linkToken: string | null; - permissionLevel: SessionSharePermissionLevel; - createdAt: string; - expiresAt: string; - revokedAt: string | null; - lastJoinedAt: string | null; - joinCount: number; -} - -export interface CreateSessionShareRequest { - hostId: number; - sessionId: string; - tabInstanceId?: string; - protocol: SessionShareProtocol; - shareType: SessionShareType; - targetUserId?: string; - permissionLevel: SessionSharePermissionLevel; - expiryHours?: number; -} - -export interface CreateSessionShareResponse { - shareId: string; - linkToken: string | null; - expiresAt: string; -} - -export function createSessionShare( - request: CreateSessionShareRequest, - origin?: SessionOrigin, -): Promise { - return call( - () => api(origin).post("/create", request), - "Failed to create session share", - ); -} - -export function getActiveSessionShares( - hostId: number, - origin?: SessionOrigin, -): Promise<{ shares: SessionShareRecord[] }> { - return call( - () => - api(origin).get<{ shares: SessionShareRecord[] }>( - `/host/${hostId}/active`, - ), - "Failed to fetch active session shares", - ); -} - -export function revokeSessionShare( - shareId: string, - origin?: SessionOrigin, -): Promise { - return call( - () => api(origin).delete(`/${encodeURIComponent(shareId)}`), - "Failed to revoke session share", - ); -} - -export interface DirectoryUser { - id: string; - username: string; -} - -export interface DirectoryRole { - id: number; - name: string; - displayName: string | null; -} - -/** Users and roles to share with or invite. */ -export function getDirectory(origin?: SessionOrigin): Promise<{ - users: DirectoryUser[]; - roles: DirectoryRole[]; -}> { - return call( - () => - api(origin).get<{ users: DirectoryUser[]; roles: DirectoryRole[] }>( - "/directory", - ), - "Failed to list users and roles", - ); -} - -/** Live sessions other users shared with the caller, as active-session rows. */ -export function getSharedWithMe(): Promise { - return call( - () => api().get("/shared-with-me"), - "Failed to list shared sessions", - ); -} - -// --------------------------------------------------------------------------- -// Collab rooms -// --------------------------------------------------------------------------- - -export interface CollabRoom { - id: string; - name: string; - ownerUserId: string; - persistent: boolean; - presenterUserId: string | null; - stageProtocol: string | null; - stageHostId: number | null; - stageShareId: string | null; - guestLinkEnabled: boolean; - createdAt: string; - endedAt: string | null; -} - -export interface CollabRoomMember { - userId: string; - username: string; - roomRole: string; - createdAt: string; -} - -export interface CollabOnlineUser { - userId: string; - username: string; -} - -export interface CollabStage { - presenterUserId: string | null; - protocol: "ssh" | "rdp" | "vnc" | "telnet" | null; - hostId: number | null; - shareId: string | null; - sessionId?: string; - controllerUserId?: string | null; - connectParams?: { token: string }; -} - -export interface CollabControlRequest { - userId: string; - username: string; - requestedAt: string; -} - -export interface CollabRoomDetail { - room: CollabRoom; - me: string; - isHost: boolean; - members: CollabRoomMember[]; - online: CollabOnlineUser[]; - stage: CollabStage; - /** Anonymous guests watching the stage through the guest link. */ - guests?: { label: string | null }[]; - controllerUserId: string | null; - controlRequests: CollabControlRequest[]; - /** Where live room events are served, as /plugin-ws//. */ - eventsWsPath: string; -} - -export interface CollabGuestStage { - protocol: "ssh" | "rdp" | "vnc" | "telnet"; - shareId: string; - wsPath?: string; - connectParams?: { token: string }; -} - -/** Anonymous: guests poll this to follow the presenter. Throws on 404/429. */ -export async function resolveCollabGuestStage( - token: string, -): Promise<{ roomName: string; stage: CollabGuestStage | null }> { - const response = await api().get<{ - roomName: string; - stage: CollabGuestStage | null; - }>(`/guest/${encodeURIComponent(token)}`); - return response.data; -} diff --git a/plugins/session-sharing/src/frontend/index.tsx b/plugins/session-sharing/src/frontend/index.tsx deleted file mode 100644 index ec0b6488a..000000000 --- a/plugins/session-sharing/src/frontend/index.tsx +++ /dev/null @@ -1,369 +0,0 @@ -// The terminal surfaces need xterm's own stylesheet. -import "@xterm/xterm/css/xterm.css"; -import { - useEffect, - useRef, - useState, - type ReactNode, - type ComponentType, -} from "react"; -import { Presentation, Share2 } from "lucide-react"; -import { toast } from "sonner"; -import { - useCurrentUser, - usePermission, - type PanelProps, - type StandaloneViewProps, - type TabProps, - type TermixApp, -} from "@termix/plugin-sdk/frontend"; -import { bindApi } from "./api"; -import { SharedWithMeSection } from "./SharedWithMeSection"; -import CollabGuestView from "./CollabGuestView"; -import { CollabPanel } from "./CollabPanel"; -import { CollabRoomTab } from "./CollabRoomTab"; -import SharedSessionView from "./SharedSessionView"; -import { ParticipantsOverlay } from "./ParticipantsOverlay"; -import { - ShareDialogHost, - bindShareApp, - closeShareDialog, - openShareDialog, - type ShareTarget, -} from "./share-dialog"; -import { REMOTE_DISPLAY_SLOT } from "./shared"; - -import { connectMeetings, type MeetingBackend } from "./meeting-backend"; - -let meetingApp: TermixApp; - -function MeetingScope({ - children, - serverKey, - pin = false, -}: { - children: (backend: MeetingBackend) => ReactNode; - serverKey?: string; - pin?: boolean; -}) { - const [backend, setBackend] = useState(null); - const [error, setError] = useState(null); - const pinned = useRef(serverKey); - useEffect(() => { - let cancelled = false; - let sequence = 0; - const refresh = async () => { - const request = ++sequence; - try { - const next = await connectMeetings(meetingApp); - if (cancelled || request !== sequence) return; - if ( - pin && - pinned.current !== undefined && - pinned.current !== next.key - ) { - setBackend(null); - setError(meetingApp.t("collab.serverChanged")); - return; - } - if (pin) pinned.current ??= next.key; - setError(null); - setBackend((previous) => - previous?.key === next.key ? previous : next, - ); - } catch (failure) { - if (!cancelled) { - setBackend(null); - setError(String(failure)); - } - } - }; - void refresh(); - const dispose = meetingApp.desktop.onRemoteServerChange(() => { - void refresh(); - }); - return () => { - cancelled = true; - dispose(); - }; - }, [serverKey, pin]); - if (error) - return ( -

- {error} -

- ); - return backend ? ( -
- {children(backend)} -
- ) : null; -} - -const COLLAB = "collab"; -const SHARE_ACTION = "session-sharing.share"; -const SEEN_ROOMS_KEY = "termix:collab-rooms-seen"; -const INVITE_POLL_MS = 60_000; - -/** `?view=shared` and `?view=collab-guest`, the two anonymous guest links. */ -function GuestView({ view }: StandaloneViewProps) { - return view === "shared" ? : ; -} - -function CollabRoomTabView({ tab, isVisible }: TabProps) { - const roomId = tab.data?.roomId; - const serverKey = - typeof tab.data?.meetingServerKey === "string" - ? tab.data.meetingServerKey - : undefined; - return ( - - {(backend) => ( - - )} - - ); -} - -function CollabPanelView({ shell }: PanelProps) { - return ( - - {(backend) => ( - - shell.openTab(null, COLLAB, { - label: room.name, - forceNewTab: true, - data: { roomId: room.id, meetingServerKey: backend.key }, - }) - } - /> - )} - - ); -} - -/** - * Rooms are discovered by polling, so a room this browser has never shown - * gets one toast with an Open action. The first poll after login only - * records what already exists. - */ -function createInviteWatcher(app: TermixApp): ComponentType { - return function InviteWatcher() { - const user = useCurrentUser(); - const allowed = usePermission("use"); - const userId = user?.userId; - - useEffect(() => { - if (!userId || !allowed) return; - let cancelled = false; - const check = async () => { - try { - const { rooms = [] } = await ( - await connectMeetings(app) - ).api.listCollabRooms(); - if (cancelled) return; - let seen: string[] = []; - try { - seen = JSON.parse(localStorage.getItem(SEEN_ROOMS_KEY) ?? "[]"); - } catch { - seen = []; - } - const seenSet = new Set(seen); - const fresh = rooms.filter((room) => !seenSet.has(room.id)); - if (fresh.length === 0) return; - localStorage.setItem( - SEEN_ROOMS_KEY, - JSON.stringify([...seenSet, ...fresh.map((room) => room.id)]), - ); - if (seen.length === 0) return; - for (const room of fresh) { - if (room.ownerUserId === userId) continue; - toast(app.t("collab.invitedTo", { name: room.name }), { - action: { - label: app.t("collab.openRoom"), - onClick: () => app.tabs.openRailView(COLLAB), - }, - }); - } - } catch { - /* next poll */ - } - }; - void check(); - const timer = setInterval(() => void check(), INVITE_POLL_MS); - return () => { - cancelled = true; - clearInterval(timer); - }; - }, [userId, allowed]); - - return null; - }; -} - -/** A terminal toolbar action is invoked with the terminal's slot API. */ -interface TerminalSlotApi { - getShareTarget?: () => ShareTarget | null; -} - -/** A remote desktop toolbar action is invoked with the session it shows. */ -interface RemoteDesktopToolbarContext { - hostId: number; - sessionId: string | null; - protocol: "rdp" | "vnc" | "telnet"; - tabInstanceId?: string; - origin?: "local" | "remote"; -} - -function isRemoteDesktopContext( - value: unknown, -): value is RemoteDesktopToolbarContext { - return ( - !!value && - typeof value === "object" && - "protocol" in value && - "hostId" in value - ); -} - -export function activate(app: TermixApp): void { - meetingApp = app; - bindApi(app.api, app.apiFor); - bindShareApp(app); - app.onDispose(() => { - closeShareDialog(); - bindApi(null); - bindShareApp(null); - }); - - // Rooms and share links draw remote desktop streams through this slot. - app.declareActionSlot({ id: REMOTE_DISPLAY_SLOT, accepts: ["component"] }); - - // Guest pages open the tab type's standalone view by ?view= name. - const guestViews = { - standalone: GuestView, - standaloneViews: ["shared", "collab-guest"], - }; - - if (app.guest) { - app.registerTab(COLLAB, () => null, { hostless: true, ...guestViews }); - return; - } - - app.registerRailItem({ - id: COLLAB, - icon: Presentation, - titleKey: "nav.collab", - kind: "panel", - after: "connections", - separatorAfter: true, - permission: "use", - }); - app.registerPanel(COLLAB, CollabPanelView); - app.registerTab(COLLAB, CollabRoomTabView, { - icon: Presentation, - titleKey: "nav.collab", - hostless: true, - multiInstance: true, - ...guestViews, - }); - - app.registerAction( - SHARE_ACTION, - (context: unknown) => { - let target: ShareTarget | null = null; - if (isRemoteDesktopContext(context)) { - target = context.sessionId - ? { - hostId: context.hostId, - sessionId: context.sessionId, - protocol: context.protocol, - tabInstanceId: context.tabInstanceId, - origin: context.origin, - } - : null; - } else { - target = (context as TerminalSlotApi)?.getShareTarget?.() ?? null; - } - if (target) openShareDialog(target); - else toast.error(app.t("sessionSharing.notReadyToShare")); - }, - { permission: "use" }, - ); - for (const slot of ["terminal.toolbar", "remote-desktop.toolbar"]) { - app.registerSlotContribution(slot, { - actionId: SHARE_ACTION, - titleKey: "sessionSharing.shareButton", - icon: Share2, - kind: "button", - }); - } - - // The tab bar's right-click menu, for a terminal whose toolbar is hidden. - app.registerSlotContribution("tab.menu", { - actionId: SHARE_ACTION, - titleKey: "sessionSharing.shareButton", - icon: Share2, - kind: "button", - when: (context) => { - const handle = context.handle as - { getShareTarget?: () => unknown } | null | undefined; - return typeof handle?.getShareTarget === "function" - ? handle.getShareTarget() !== null - : false; - }, - }); - - // A small share button on the tab itself, as in 2.8. - app.registerSlotContribution("tab.inline", { - actionId: SHARE_ACTION, - titleKey: "sessionSharing.shareButton", - icon: Share2, - kind: "button", - when: (context) => - typeof (context.handle as { getShareTarget?: unknown } | null | undefined) - ?.getShareTarget === "function", - }); - - app.registerSlotContribution("terminal.overlay", { - actionId: "session-sharing.participants", - titleKey: "collab.members", - kind: "component", - component: ParticipantsOverlay as unknown as ComponentType< - Record - >, - }); - - app.registerSlotContribution("shell.overlay", { - actionId: "session-sharing.shareDialog", - titleKey: "sessionSharing.shareButton", - kind: "component", - component: ShareDialogHost as ComponentType>, - }); - app.registerSlotContribution("shell.overlay", { - actionId: "session-sharing.inviteWatcher", - titleKey: "nav.collab", - kind: "component", - component: createInviteWatcher(app) as ComponentType< - Record - >, - }); - - // The "Shared with me" section of the active connections list. - app.registerSlotContribution("connections.sections", { - actionId: "session-sharing.sharedWithMe", - titleKey: "connections.sectionSharedWithMe", - kind: "component", - component: SharedWithMeSection as ComponentType>, - }); -} - -export function deactivate(): void { - closeShareDialog(); -} diff --git a/plugins/session-sharing/src/frontend/meeting-api.ts b/plugins/session-sharing/src/frontend/meeting-api.ts deleted file mode 100644 index 30602b73e..000000000 --- a/plugins/session-sharing/src/frontend/meeting-api.ts +++ /dev/null @@ -1,216 +0,0 @@ -import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; -import type { - CollabRoom, - CollabRoomDetail, - CollabStage, - CollabControlRequest, - DirectoryUser, - DirectoryRole, -} from "./api"; - -export function createMeetingApi( - client: PluginApiClient, - assertCurrent: () => Promise, -) { - const api = () => client; - async function call( - request: () => Promise<{ data: T }>, - fallback: string, - ): Promise { - await assertCurrent(); - try { - return (await request()).data; - } catch (error) { - const response = error as { response?: { data?: { error?: string } } }; - throw new Error(response.response?.data?.error || fallback); - } - } - const room = (roomId: string, suffix = "") => - `/rooms/${encodeURIComponent(roomId)}${suffix}`; - - function listCollabRooms(): Promise<{ rooms: CollabRoom[] }> { - return call( - () => api().get<{ rooms: CollabRoom[] }>("/rooms"), - "Failed to list rooms", - ); - } - - function createCollabRoom( - name: string, - persistent: boolean, - ): Promise<{ room: CollabRoom }> { - return call( - () => api().post<{ room: CollabRoom }>("/rooms", { name, persistent }), - "Failed to create room", - ); - } - - function getCollabRoom(roomId: string): Promise { - return call( - () => api().get(room(roomId)), - "Failed to get room", - ); - } - - async function inviteCollabMembers( - roomId: string, - targets: { userIds?: string[]; roleIds?: number[] }, - ): Promise { - await call( - () => api().post(room(roomId, "/members"), targets), - "Failed to invite members", - ); - } - - async function removeCollabMember( - roomId: string, - userId: string, - ): Promise { - await call( - () => - api().delete(room(roomId, `/members/${encodeURIComponent(userId)}`)), - "Failed to remove member", - ); - } - - function presentCollabStage( - roomId: string, - input: { protocol: string; sessionId: string; hostId: number }, - ): Promise<{ stage: CollabStage }> { - return call( - () => api().post<{ stage: CollabStage }>(room(roomId, "/present"), input), - "Failed to start presenting", - ); - } - - async function stopCollabStage(roomId: string): Promise { - await call( - () => api().post(room(roomId, "/stop")), - "Failed to stop presenting", - ); - } - - function getCollabStage( - roomId: string, - ): Promise<{ stage: CollabStage | null }> { - return call( - () => api().get<{ stage: CollabStage | null }>(room(roomId, "/stage")), - "Failed to resolve stage", - ); - } - - async function setCollabStageControl( - roomId: string, - userId: string | null, - ): Promise { - await call( - () => api().post(room(roomId, "/control"), { userId }), - "Failed to change stage control", - ); - } - - function requestCollabStageControl( - roomId: string, - ): Promise<{ request: CollabControlRequest }> { - return call( - () => - api().post<{ request: CollabControlRequest }>( - room(roomId, "/control/request"), - ), - "Failed to request control", - ); - } - - function listCollabControlRequests( - roomId: string, - ): Promise<{ requests: CollabControlRequest[] }> { - return call( - () => - api().get<{ requests: CollabControlRequest[] }>( - room(roomId, "/control/requests"), - ), - "Failed to list control requests", - ); - } - - async function dismissCollabControlRequest( - roomId: string, - userId: string, - ): Promise { - await call( - () => - api().delete( - room(roomId, `/control/requests/${encodeURIComponent(userId)}`), - ), - "Failed to dismiss control request", - ); - } - - async function endCollabRoom(roomId: string): Promise { - await call(() => api().post(room(roomId, "/end")), "Failed to end room"); - } - - async function deleteCollabRoom(roomId: string): Promise { - await call(() => api().delete(room(roomId)), "Failed to delete room"); - } - - function setCollabGuestLink( - roomId: string, - enabled: boolean, - ): Promise<{ guestLinkToken: string | null }> { - return call( - () => - api().post<{ guestLinkToken: string | null }>( - room(roomId, "/guest-link"), - { enabled }, - ), - "Failed to update guest link", - ); - } - - async function removeCollabGuests(roomId: string): Promise { - await call( - () => api().post(room(roomId, "/guests/remove")), - "Failed to remove guests", - ); - } - - function getDirectory() { - return call( - () => - client.get<{ users: DirectoryUser[]; roles: DirectoryRole[] }>( - "/directory", - ), - "Failed to list users and roles", - ); - } - function resolveHost(syncId: string) { - return call( - () => - client.get<{ id: number }>( - `/meeting-host/${encodeURIComponent(syncId)}`, - ), - "The host must be synced to the meeting server before presenting", - ); - } - return { - listCollabRooms, - createCollabRoom, - getCollabRoom, - inviteCollabMembers, - removeCollabMember, - presentCollabStage, - stopCollabStage, - getCollabStage, - setCollabStageControl, - requestCollabStageControl, - listCollabControlRequests, - dismissCollabControlRequest, - endCollabRoom, - deleteCollabRoom, - setCollabGuestLink, - removeCollabGuests, - getDirectory, - resolveHost, - }; -} diff --git a/plugins/session-sharing/src/frontend/meeting-backend.ts b/plugins/session-sharing/src/frontend/meeting-backend.ts deleted file mode 100644 index 03c0c18de..000000000 --- a/plugins/session-sharing/src/frontend/meeting-backend.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { TermixApp } from "@termix/plugin-sdk/frontend"; -import { createMeetingApi } from "./meeting-api"; - -export type MeetingBackend = Awaited>; - -/** A room, its hosts and its sockets must all belong to the same backend. */ -export async function connectMeetings(app: TermixApp) { - const remoteUrl = app.desktop.available - ? await app.desktop.remoteServerUrl() - : null; - const key = remoteUrl?.replace(/\/+$/, "") || "local"; - const origin = remoteUrl ? ("remote" as const) : ("local" as const); - const publicUrl = - remoteUrl || (!app.desktop.available ? window.location.href : null); - const assertCurrent = async () => { - const current = app.desktop.available - ? await app.desktop.remoteServerUrl() - : null; - if ((current?.replace(/\/+$/, "") || "local") !== key) { - throw new Error(app.t("collab.serverChanged")); - } - }; - return { - key, - origin, - publicUrl, - assertCurrent, - api: createMeetingApi(app.apiFor(origin), assertCurrent), - }; -} - -export function meetingGuestUrl( - publicUrl: string | null, - token: string, -): string | null { - return guestViewUrl(publicUrl, "collab-guest", token); -} - -/** A guest link on a server others can open, never a file:// desktop page. */ -export function guestViewUrl( - publicUrl: string | null, - view: string, - token: string, -): string | null { - if (!publicUrl) return null; - const url = new URL(publicUrl); - if (url.protocol !== "http:" && url.protocol !== "https:") return null; - url.search = ""; - url.hash = ""; - url.searchParams.set("view", view); - url.searchParams.set("token", token); - return url.toString(); -} - -/** Where guests reach a session that runs on the given backend. */ -export async function sessionPublicUrl( - app: TermixApp, - origin: "local" | "remote" | undefined, -): Promise { - if (!app.desktop.available) return window.location.href; - if (origin !== "remote") return null; - return (await app.desktop.remoteServerUrl()) || null; -} diff --git a/plugins/session-sharing/src/frontend/share-dialog.tsx b/plugins/session-sharing/src/frontend/share-dialog.tsx deleted file mode 100644 index 487a8ea0b..000000000 --- a/plugins/session-sharing/src/frontend/share-dialog.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useSyncExternalStore } from "react"; -import type { TermixApp } from "@termix/plugin-sdk/frontend"; -import { ShareSessionModal } from "./ShareSessionModal"; -import type { SessionShareProtocol } from "./api"; -import { sessionPublicUrl } from "./meeting-backend"; - -let shareApp: TermixApp | null = null; - -export function bindShareApp(app: TermixApp | null): void { - shareApp = app; -} - -function resolveShareBase( - origin: "local" | "remote" | undefined, -): Promise { - if (!shareApp) return Promise.resolve(window.location.href); - return sessionPublicUrl(shareApp, origin); -} - -/** What the share dialog needs to share one live session. */ -export interface ShareTarget { - hostId: number; - sessionId: string; - protocol: SessionShareProtocol; - tabInstanceId?: string; - /** The backend the session runs on; a desktop can be on either. */ - origin?: "local" | "remote"; -} - -let current: ShareTarget | null = null; -const listeners = new Set<() => void>(); - -function emit(): void { - for (const listener of listeners) listener(); -} - -/** Opens the share dialog, which the shell overlay slot keeps mounted. */ -export function openShareDialog(target: ShareTarget): void { - current = target; - emit(); -} - -export function closeShareDialog(): void { - current = null; - emit(); -} - -function subscribe(listener: () => void): () => void { - listeners.add(listener); - return () => listeners.delete(listener); -} - -/** Rendered once in "shell.overlay"; shows the dialog when a toolbar asks. */ -export function ShareDialogHost() { - const target = useSyncExternalStore(subscribe, () => current); - if (!target) return null; - return ( - - ); -} diff --git a/plugins/session-sharing/src/frontend/shared.tsx b/plugins/session-sharing/src/frontend/shared.tsx deleted file mode 100644 index 75572a399..000000000 --- a/plugins/session-sharing/src/frontend/shared.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useMemo } from "react"; -import { - invokeAction, - useSlotContributions, - useTranslation, -} from "@termix/plugin-sdk/frontend"; -import { pluginWsUrl } from "@termix/plugin-sdk/ui"; - -/** - * A remote desktop stream drawn by whichever plugin provides one. Rooms and - * share links show RDP, VNC and Telnet without knowing how they are drawn: - * the provider contributes a component to this slot and answers the token - * action below. - */ -export const REMOTE_DISPLAY_SLOT = "session.remoteDisplay"; -export const REMOTE_SESSION_TOKEN_ACTION = "session.remoteDisplay.token"; - -export interface RemoteDisplayProps { - token: string; - connectionOrigin?: "local" | "remote"; - protocol: string; - isVisible: boolean; - onConnect?: () => void; - onError?: (error: string) => void; -} - -export function RemoteDisplay(props: RemoteDisplayProps) { - const { t } = useTranslation(); - const contributions = useSlotContributions(REMOTE_DISPLAY_SLOT); - const provider = useMemo( - () => - contributions.find( - (contribution) => - contribution.kind === "component" && contribution.component, - ), - [contributions], - ); - if (!provider?.component) { - return ( -
- {t("sessionSharing.remoteDisplayUnavailable")} -
- ); - } - const Display = provider.component; - return )} />; -} - -export interface RemoteSessionToken { - token: string; - connectionId: string; -} - -/** Mints a session token for presenting a host, through the provider. */ -export async function createRemoteSessionToken( - hostId: number, - origin: unknown, - protocol: string, -): Promise { - return ((await invokeAction( - REMOTE_SESSION_TOKEN_ACTION, - hostId, - origin, - protocol, - )) ?? null) as RemoteSessionToken | null; -} - -/** A socket URL for a /plugin-ws//?query path the server handed out. */ -export async function wsTargetForPath( - wsPath: string, - origin: "local" | "remote" = "local", -) { - const [pathname, query] = wsPath.split("?", 2); - const match = /^\/plugin-ws\/([^/]+)(\/.*)$/.exec(pathname); - if (!match) return null; - const target = await pluginWsUrl(match[1], match[2], { origin }); - if (!target) return null; - return { - ...target, - url: query - ? `${target.url}${target.url.includes("?") ? "&" : "?"}${query}` - : target.url, - }; -} - -export async function wsUrlForPath(wsPath: string): Promise { - return (await wsTargetForPath(wsPath))?.url ?? null; -} - -export function getErrorMessage(error: unknown): string { - if (error instanceof Error) return error.message; - return typeof error === "string" ? error : String(error); -} - -export function Loader({ message }: { message: string }) { - return ( -
-
-

{message}

-
- ); -} diff --git a/plugins/session-sharing/tests/backend/activate.test.ts b/plugins/session-sharing/tests/backend/activate.test.ts deleted file mode 100644 index eaa2d4155..000000000 --- a/plugins/session-sharing/tests/backend/activate.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import express from "express"; -import { afterEach, describe, expect, it } from "vitest"; -import { PluginCapabilityError } from "@termix/plugin-sdk/backend"; -import { - createMockCtx, - createTestDb, - type TestDb, -} from "@termix/plugin-sdk/testing"; -import { activate } from "../../src/backend/index.js"; -import { manifest, pluginDir, startServer, type TestServer } from "./helpers"; - -let db: TestDb | null = null; -let server: TestServer | null = null; - -afterEach(async () => { - db?.close(); - db = null; - await server?.close(); - server = null; -}); - -describe("session-sharing activate", () => { - it("provides sessions.sharing and the guest resolver", async () => { - server = await startServer(); - expect(server.mock.services.get("sessions.sharing")).toBeDefined(); - expect(typeof server.guests.resolve).toBe("function"); - expect(server.mock.httpRouters).toEqual([ - { public: ["/resolve/:linkToken", "/guest/:token"] }, - ]); - }); - - it.each(["db:own", "network:serve"])( - "fails closed without %s", - async (capability) => { - db = await createTestDb(pluginDir); - const mock = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: manifest.capabilities.filter((c) => c !== capability), - db: db.database, - router: () => express.Router(), - }); - await expect(activate(mock.ctx)).rejects.toBeInstanceOf( - PluginCapabilityError, - ); - for (const dispose of [...mock.disposals].reverse()) await dispose(); - }, - ); - - it("cannot check a share target's host access without hosts:read", async () => { - db = await createTestDb(pluginDir); - db.sqlite.exec( - "INSERT INTO users (id, username) VALUES ('alice', 'alice'), ('bob', 'bob'); INSERT INTO ssh_data (id) VALUES (1)", - ); - const mock = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: manifest.capabilities.filter((c) => c !== "hosts:read"), - db: db.database, - router: () => express.Router(), - actor: "bob", - }); - await activate(mock.ctx); - await expect( - mock.ctx.asUser("bob", () => mock.ctx.hosts.checkAccess(1, "connect")), - ).rejects.toBeInstanceOf(PluginCapabilityError); - for (const dispose of [...mock.disposals].reverse()) await dispose(); - }); -}); diff --git a/plugins/session-sharing/tests/backend/helpers.ts b/plugins/session-sharing/tests/backend/helpers.ts deleted file mode 100644 index 6c39e33cc..000000000 --- a/plugins/session-sharing/tests/backend/helpers.ts +++ /dev/null @@ -1,196 +0,0 @@ -import http from "node:http"; -import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core"; -import type { AddressInfo } from "node:net"; -import { fileURLToPath } from "node:url"; -import express, { type Router } from "express"; -import { - createMockCtx, - createTestDb, - type MockPluginContext, - type TestDb, -} from "@termix/plugin-sdk/testing"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import manifestJson from "../../manifest.json"; -import type { LiveSessionInfo } from "../../src/backend/live.js"; -import type { - SessionGuestsV1, - SessionSharingV1, -} from "../../src/backend/services.js"; - -export const pluginDir = fileURLToPath(new URL("../..", import.meta.url)); -export const manifest = manifestJson as unknown as PluginManifest; - -/** A stand-in for the terminal's "ssh" sessions.live provider. */ -export function createFakeLive() { - const sessions = new Map(); - const calls: Array<[string, ...unknown[]]> = []; - return { - sessions, - calls, - addSession(id: string, userId: string, hostId = 1) { - sessions.set(id, { - id, - userId, - hostId, - hostName: `host-${hostId}`, - isConnected: true, - createdAt: 1, - tabInstanceId: `tab-${id}`, - }); - }, - provider: { - getSession: (id: string) => sessions.get(id) ?? null, - ownerEndSession: (...args: unknown[]) => { - calls.push(["ownerEndSession", ...args]); - }, - disconnectParticipants: (...args: unknown[]) => { - calls.push(["disconnectParticipants", ...args]); - return 0; - }, - setRoomShareControl: (...args: unknown[]) => { - calls.push(["setRoomShareControl", ...args]); - }, - listGuests: () => [{ label: "Guest 1" }], - }, - }; -} - -export interface TestServer { - db: TestDb; - mock: MockPluginContext; - live: ReturnType; - sharing: SessionSharingV1; - guests: SessionGuestsV1; - request: ( - method: string, - path: string, - options?: { user?: string | null; body?: unknown }, - // Route bodies vary per test; asserting on them is the point. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ) => Promise<{ status: number; body: any }>; - close: () => Promise; -} - -/** - * The plugin activated against a real in-memory database, its router mounted - * the way core mounts it: JSON parsed and the acting user set from the - * request (none for a guest), everything else left to the plugin. - */ -export async function startServer( - options: { - permissions?: string[]; - users?: string[]; - admins?: string[]; - hostIds?: number[]; - /** Hosts ctx.hosts.checkAccess grants. Defaults to every host. */ - accessibleHostIds?: number[]; - settings?: Record; - } = {}, -): Promise { - const db = await createTestDb(pluginDir, { - before: (sqlite) => { - sqlite.exec("ALTER TABLE ssh_data ADD COLUMN sync_id TEXT"); - }, - }); - const refs = await db.database.refs(); - db.database.refs = (async () => ({ - ...(refs as object), - hosts: sqliteTable("ssh_data", { - id: integer("id").primaryKey(), - syncId: text("sync_id"), - }), - })) as typeof db.database.refs; - for (const user of options.users ?? ["alice", "bob", "carol"]) { - db.sqlite - .prepare("INSERT INTO users (id, username, is_admin) VALUES (?, ?, ?)") - .run(user, user, options.admins?.includes(user) ? 1 : 0); - } - const hostIds = options.hostIds ?? [1, 2]; - for (const id of hostIds) { - db.sqlite.prepare("INSERT INTO ssh_data (id) VALUES (?)").run(id); - } - - const live = createFakeLive(); - let router: Router | null = null; - const mock = createMockCtx({ - pluginId: manifest.id, - manifest, - capabilities: manifest.capabilities, - db: db.database, - router: () => (router = express.Router()), - permissions: options.permissions ?? ["session-sharing.use"], - settings: options.settings, - hosts: (options.accessibleHostIds ?? hostIds).map((id) => ({ - id, - userId: "alice", - name: `host-${id}`, - ip: "10.0.0.1", - port: 22, - username: "root", - tags: [], - folder: null, - authType: "password", - })) as never, - services: { "sessions.live#ssh": live.provider }, - }); - - const { activate } = await import("../../src/backend/index.js"); - await activate(mock.ctx); - - const app = express(); - app.use(express.json()); - app.use((req, _res, next) => { - mock.setActor(req.header("x-test-user") || undefined); - next(); - }); - app.use((req, res, next) => router!(req, res, next)); - - const server = http.createServer(app); - await new Promise((resolve) => server.listen(0, resolve)); - const { port } = server.address() as AddressInfo; - - return { - db, - mock, - live, - sharing: mock.services.get("sessions.sharing") as SessionSharingV1, - guests: mock.ctx.registry.consume( - "session-sharing.guests", - )!, - async request(method, path, { user = "alice", body: given } = {}) { - const body = method === "GET" ? undefined : given; - const response = await fetch(`http://127.0.0.1:${port}${path}`, { - method, - headers: { - ...(user ? { "x-test-user": user } : {}), - ...(body !== undefined ? { "content-type": "application/json" } : {}), - }, - body: body !== undefined ? JSON.stringify(body) : undefined, - }); - const text = await response.text(); - return { status: response.status, body: text ? JSON.parse(text) : null }; - }, - async close() { - await new Promise((resolve) => server.close(() => resolve())); - for (const dispose of [...mock.disposals].reverse()) { - await Promise.resolve(dispose()).catch(() => {}); - } - db.close(); - }, - }; -} - -/** Runs a service call the way a consumer's handle does: as that user. */ -export function asUser( - server: TestServer, - userId: string, - fn: () => Promise, -): Promise { - return server.mock.ctx.asUser(userId, fn); -} - -export function expireShare(server: TestServer, shareId: string): void { - server.db.sqlite - .prepare("UPDATE p_session_sharing_shares SET expires_at = ? WHERE id = ?") - .run(new Date(Date.now() - 60_000).toISOString(), shareId); -} diff --git a/plugins/session-sharing/tests/backend/migrations.test.ts b/plugins/session-sharing/tests/backend/migrations.test.ts deleted file mode 100644 index b69bc1696..000000000 --- a/plugins/session-sharing/tests/backend/migrations.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing"; -import { pluginDir } from "./helpers"; - -// The four tables as core's SQLite bootstrap created them before 2.9.0, -// including the guest token column and its index added later. -const LEGACY_DDL = ` - CREATE TABLE session_shares ( - id TEXT PRIMARY KEY, - host_id INTEGER NOT NULL, - owner_user_id TEXT NOT NULL, - protocol TEXT NOT NULL, - session_id TEXT NOT NULL, - tab_instance_id TEXT, - share_type TEXT NOT NULL, - target_user_id TEXT, - link_token TEXT UNIQUE, - permission_level TEXT NOT NULL DEFAULT 'read-only', - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TEXT NOT NULL, - revoked_at TEXT, - last_joined_at TEXT, - join_count INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE, - FOREIGN KEY (owner_user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (target_user_id) REFERENCES users (id) ON DELETE CASCADE - ); - CREATE TABLE session_share_participants ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - share_id TEXT NOT NULL, - user_id TEXT, - guest_label TEXT, - joined_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - left_at TEXT, - FOREIGN KEY (share_id) REFERENCES session_shares (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE - ); - CREATE TABLE collab_rooms ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - owner_user_id TEXT NOT NULL, - persistent INTEGER NOT NULL DEFAULT 0, - presenter_user_id TEXT, - stage_protocol TEXT, - stage_host_id INTEGER, - stage_share_id TEXT, - guest_link_token TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - ended_at TEXT, - FOREIGN KEY (owner_user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (presenter_user_id) REFERENCES users (id) ON DELETE SET NULL, - FOREIGN KEY (stage_host_id) REFERENCES ssh_data (id) ON DELETE SET NULL, - FOREIGN KEY (stage_share_id) REFERENCES session_shares (id) ON DELETE SET NULL - ); - CREATE TABLE collab_room_members ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - room_id TEXT NOT NULL, - user_id TEXT NOT NULL, - room_role TEXT NOT NULL DEFAULT 'member', - added_by TEXT, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE (room_id, user_id), - FOREIGN KEY (room_id) REFERENCES collab_rooms (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, - FOREIGN KEY (added_by) REFERENCES users (id) ON DELETE SET NULL - ); - CREATE INDEX idx_session_shares_session_id ON session_shares (session_id); - CREATE INDEX idx_session_shares_host_id ON session_shares (host_id); - CREATE UNIQUE INDEX idx_collab_rooms_guest_token ON collab_rooms (guest_link_token); -`; - -const TABLES = { - session_shares: "p_session_sharing_shares", - session_share_participants: "p_session_sharing_share_participants", - collab_rooms: "p_session_sharing_rooms", - collab_room_members: "p_session_sharing_room_members", -}; - -let db: TestDb | null = null; - -afterEach(() => { - db?.close(); - db = null; -}); - -const exists = (type: "table" | "index", name: string) => - !!db!.sqlite - .prepare("SELECT name FROM sqlite_master WHERE type = ? AND name = ?") - .get(type, name); - -function seedUsersAndHost(sqlite: TestDb["sqlite"]) { - sqlite.exec( - "INSERT INTO users (id, username) VALUES ('alice', 'alice'), ('bob', 'bob')", - ); - sqlite.exec("INSERT INTO ssh_data (id) VALUES (7)"); -} - -describe("adopting the session sharing tables", () => { - it("keeps every row and index when the legacy tables are there", async () => { - db = await createTestDb(pluginDir, { - before: (sqlite) => { - seedUsersAndHost(sqlite); - sqlite.exec(LEGACY_DDL); - sqlite.exec(` - INSERT INTO session_shares (id, host_id, owner_user_id, protocol, session_id, share_type, link_token, permission_level, expires_at, join_count) - VALUES ('share-1', 7, 'alice', 'ssh', 'sess-1', 'link', 'tok-1', 'read-write', '2999-01-01T00:00:00.000Z', 3); - INSERT INTO session_share_participants (share_id, user_id, guest_label) VALUES ('share-1', NULL, 'Guest'); - INSERT INTO collab_rooms (id, name, owner_user_id, persistent, stage_share_id, stage_host_id, guest_link_token) - VALUES ('room-1', 'Standup', 'alice', 1, 'share-1', 7, 'guest-tok'); - INSERT INTO collab_room_members (room_id, user_id, room_role) VALUES ('room-1', 'alice', 'host'), ('room-1', 'bob', 'member'); - `); - }, - }); - - expect(db.applied).toEqual(["0001_adopt_session_sharing"]); - for (const [legacy, adopted] of Object.entries(TABLES)) { - expect(exists("table", legacy)).toBe(false); - expect(exists("table", adopted)).toBe(true); - } - - expect( - db.sqlite - .prepare( - "SELECT id, link_token, permission_level, join_count FROM p_session_sharing_shares", - ) - .all(), - ).toEqual([ - { - id: "share-1", - link_token: "tok-1", - permission_level: "read-write", - join_count: 3, - }, - ]); - expect( - db.sqlite - .prepare( - "SELECT name, persistent, stage_share_id, guest_link_token FROM p_session_sharing_rooms", - ) - .get(), - ).toEqual({ - name: "Standup", - persistent: 1, - stage_share_id: "share-1", - guest_link_token: "guest-tok", - }); - expect( - db.sqlite - .prepare("SELECT COUNT(*) AS n FROM p_session_sharing_room_members") - .get(), - ).toEqual({ n: 2 }); - expect( - db.sqlite - .prepare("SELECT guest_label FROM p_session_sharing_share_participants") - .get(), - ).toEqual({ guest_label: "Guest" }); - - for (const index of [ - "idx_session_shares_session_id", - "idx_session_shares_host_id", - "idx_collab_rooms_guest_token", - "idx_collab_rooms_owner", - "idx_collab_room_members_user", - ]) { - expect(exists("index", index)).toBe(true); - } - }); - - it("follows the rename in the links between the tables", async () => { - db = await createTestDb(pluginDir, { - before: (sqlite) => { - seedUsersAndHost(sqlite); - sqlite.exec(LEGACY_DDL); - }, - }); - const ddl = (name: string) => - ( - db!.sqlite - .prepare("SELECT sql FROM sqlite_master WHERE name = ?") - .get(name) as { sql: string } - ).sql; - - expect(ddl("p_session_sharing_rooms")).toContain( - '"p_session_sharing_shares"', - ); - expect(ddl("p_session_sharing_room_members")).toContain( - '"p_session_sharing_rooms"', - ); - }); - - it("creates the tables fresh when there are no legacy ones", async () => { - db = await createTestDb(pluginDir, { - before: (sqlite) => seedUsersAndHost(sqlite), - }); - - for (const [legacy, adopted] of Object.entries(TABLES)) { - expect(exists("table", legacy)).toBe(false); - expect(exists("table", adopted)).toBe(true); - } - - db.sqlite.exec(` - INSERT INTO p_session_sharing_shares (id, host_id, owner_user_id, protocol, session_id, share_type, expires_at) - VALUES ('s', 7, 'alice', 'ssh', 'x', 'room', '2999-01-01'); - INSERT INTO p_session_sharing_rooms (id, name, owner_user_id, stage_share_id) VALUES ('r', 'R', 'alice', 's'); - INSERT INTO p_session_sharing_room_members (room_id, user_id) VALUES ('r', 'bob'); - `); - // The hand-kept set-null link: deleting the share clears the stage. - db.sqlite.exec("DELETE FROM p_session_sharing_shares WHERE id = 's'"); - expect( - db.sqlite - .prepare("SELECT stage_share_id FROM p_session_sharing_rooms") - .get(), - ).toEqual({ stage_share_id: null }); - // And deleting a user cascades to their memberships. - db.sqlite.exec("DELETE FROM users WHERE id = 'bob'"); - expect( - db.sqlite - .prepare("SELECT COUNT(*) AS n FROM p_session_sharing_room_members") - .get(), - ).toEqual({ n: 0 }); - }); -}); diff --git a/plugins/session-sharing/tests/backend/rooms.test.ts b/plugins/session-sharing/tests/backend/rooms.test.ts deleted file mode 100644 index ccb94a8f6..000000000 --- a/plugins/session-sharing/tests/backend/rooms.test.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { startServer, type TestServer } from "./helpers"; - -let server: TestServer | null = null; - -afterEach(async () => { - await server?.close(); - server = null; -}); - -async function roomWithBob(s: TestServer, persistent = false) { - const created = await s.request("POST", "/rooms", { - body: { name: "Standup", persistent }, - }); - const roomId = created.body.room.id as string; - await s.request("POST", `/rooms/${roomId}/members`, { - body: { userIds: ["bob"] }, - }); - return roomId; -} - -async function present(s: TestServer, roomId: string, user = "alice") { - s.live.addSession(`sess-${user}`, user); - return s.request("POST", `/rooms/${roomId}/present`, { - user, - body: { protocol: "ssh", sessionId: `sess-${user}`, hostId: 1 }, - }); -} - -describe("collab rooms", () => { - it("resolves a synced host to the server ID only with connect access", async () => { - server = await startServer({ hostIds: [41, 42], accessibleHostIds: [42] }); - server.db.sqlite.exec("UPDATE ssh_data SET sync_id = 'shared-host'"); - expect(await server.request("GET", "/meeting-host/shared-host")).toEqual({ - status: 200, - body: { id: 42 }, - }); - server.db.sqlite.exec( - "UPDATE ssh_data SET sync_id = 'private-host' WHERE id = 41", - ); - expect( - (await server.request("GET", "/meeting-host/private-host")).status, - ).toBe(404); - expect( - (await server.request("GET", "/meeting-host/missing-host")).status, - ).toBe(404); - expect( - (await server.request("GET", "/meeting-host/shared-host", { user: null })) - .status, - ).toBe(401); - }); - - it("creates a room with the creator as host and hides the guest token", async () => { - server = await startServer(); - const res = await server.request("POST", "/rooms", { - body: { name: " Standup " }, - }); - expect(res.status).toBe(200); - expect(res.body.room).toMatchObject({ - name: "Standup", - ownerUserId: "alice", - guestLinkEnabled: false, - }); - expect(res.body.room).not.toHaveProperty("guestLinkToken"); - - const detail = await server.request("GET", `/rooms/${res.body.room.id}`); - expect(detail.body.isHost).toBe(true); - expect(detail.body.members).toEqual([ - expect.objectContaining({ userId: "alice", roomRole: "host" }), - ]); - expect(detail.body.eventsWsPath).toBe("/plugin-ws/ssh-terminal/terminal"); - expect(server.mock.audits.map((entry) => entry.action)).toContain( - "collab_room_create", - ); - }); - - it("keeps rooms private to members and invites to the host", async () => { - server = await startServer(); - const roomId = await roomWithBob(server); - - expect( - (await server.request("GET", `/rooms/${roomId}`, { user: "carol" })) - .status, - ).toBe(404); - expect( - ( - await server.request("POST", `/rooms/${roomId}/members`, { - user: "bob", - body: { userIds: ["carol"] }, - }) - ).status, - ).toBe(403); - expect( - (await server.request("GET", "/rooms", { user: "bob" })).body.rooms, - ).toHaveLength(1); - }); - - it("expands a role invite to its current members", async () => { - server = await startServer(); - server.db.sqlite.exec( - "INSERT INTO roles (id, name) VALUES (5, 'ops'); INSERT INTO user_roles (user_id, role_id) VALUES ('carol', 5)", - ); - const created = await server.request("POST", "/rooms", { - body: { name: "Ops" }, - }); - const roomId = created.body.room.id; - await server.request("POST", `/rooms/${roomId}/members`, { - body: { roleIds: [5] }, - }); - expect( - (await server.request("GET", `/rooms/${roomId}`, { user: "carol" })) - .status, - ).toBe(200); - expect( - ( - await server.request("POST", `/rooms/${roomId}/members`, { - body: { roleIds: [99] }, - }) - ).status, - ).toBe(404); - }); - - it("puts a member's live session on stage as a read-only room share", async () => { - server = await startServer(); - const roomId = await roomWithBob(server); - const res = await present(server, roomId, "bob"); - expect(res.status).toBe(200); - - const stage = await server.request("GET", `/rooms/${roomId}/stage`); - expect(stage.body.stage).toMatchObject({ - presenterUserId: "bob", - protocol: "ssh", - sessionId: "sess-bob", - }); - const row = server.db.sqlite - .prepare( - "SELECT share_type, permission_level FROM p_session_sharing_shares", - ) - .get(); - expect(row).toEqual({ share_type: "room", permission_level: "read-only" }); - }); - - it("refuses to present someone else's session", async () => { - server = await startServer(); - const roomId = await roomWithBob(server); - server.live.addSession("sess-alice", "alice"); - const res = await server.request("POST", `/rooms/${roomId}/present`, { - user: "bob", - body: { protocol: "ssh", sessionId: "sess-alice", hostId: 1 }, - }); - expect(res.status).toBe(403); - }); - - it("clears a stage whose session died", async () => { - server = await startServer(); - const roomId = await roomWithBob(server); - await present(server, roomId); - server.live.sessions.delete("sess-alice"); - - const stage = await server.request("GET", `/rooms/${roomId}/stage`, { - user: "bob", - }); - expect(stage.body.stage).toBeNull(); - const detail = await server.request("GET", `/rooms/${roomId}`); - expect(detail.body.stage.shareId).toBeNull(); - }); - - it("hands control only from the presenter or host, and only to members", async () => { - server = await startServer(); - const roomId = await roomWithBob(server); - await present(server, roomId); - - expect( - ( - await server.request("POST", `/rooms/${roomId}/control`, { - user: "bob", - body: { userId: "bob" }, - }) - ).status, - ).toBe(403); - expect( - ( - await server.request("POST", `/rooms/${roomId}/control`, { - body: { userId: "carol" }, - }) - ).status, - ).toBe(404); - - const granted = await server.request("POST", `/rooms/${roomId}/control`, { - body: { userId: "bob" }, - }); - expect(granted.body).toEqual({ controllerUserId: "bob" }); - expect(server.live.calls).toContainEqual([ - "setRoomShareControl", - "sess-alice", - expect.any(String), - "bob", - ]); - - // The controller may give it back. - expect( - ( - await server.request("POST", `/rooms/${roomId}/control`, { - user: "bob", - body: { userId: null }, - }) - ).status, - ).toBe(200); - }); - - it("rate limits hand raises", async () => { - server = await startServer(); - const roomId = await roomWithBob(server); - await present(server, roomId); - - const first = await server.request( - "POST", - `/rooms/${roomId}/control/request`, - { user: "bob" }, - ); - expect(first.body.request).toMatchObject({ - userId: "bob", - username: "bob", - }); - expect( - ( - await server.request("POST", `/rooms/${roomId}/control/request`, { - user: "bob", - }) - ).status, - ).toBe(429); - expect( - ( - await server.request("GET", `/rooms/${roomId}/control/requests`, { - user: "bob", - }) - ).status, - ).toBe(403); - expect( - (await server.request("GET", `/rooms/${roomId}/control/requests`)).body - .requests, - ).toHaveLength(1); - }); - - it("removes a member and drops them from the stage", async () => { - server = await startServer(); - const roomId = await roomWithBob(server); - await present(server, roomId); - - expect( - (await server.request("DELETE", `/rooms/${roomId}/members/alice`)).status, - ).toBe(400); - expect( - (await server.request("DELETE", `/rooms/${roomId}/members/bob`)).status, - ).toBe(200); - expect(server.live.calls).toContainEqual([ - "disconnectParticipants", - "sess-alice", - expect.any(String), - { - userId: "bob", - reason: "You were removed from the collaboration room", - }, - ]); - }); - - it("lists stage guests for members and lets only the host remove them", async () => { - server = await startServer(); - const roomId = await roomWithBob(server); - await present(server, roomId); - - const detail = await server.request("GET", `/rooms/${roomId}`, { - user: "bob", - }); - expect(detail.body.guests).toEqual([{ label: "Guest 1" }]); - expect( - ( - await server.request("POST", `/rooms/${roomId}/guests/remove`, { - user: "bob", - }) - ).status, - ).toBe(403); - expect( - (await server.request("POST", `/rooms/${roomId}/guests/remove`)).status, - ).toBe(200); - expect(server.live.calls).toContainEqual([ - "disconnectParticipants", - "sess-alice", - expect.any(String), - { userId: null, reason: "The host removed the guests" }, - ]); - }); - - it("ends a one-off room for good and keeps a persistent one", async () => { - server = await startServer(); - const oneOff = await roomWithBob(server); - await present(server, oneOff); - expect( - (await server.request("POST", `/rooms/${oneOff}/end`, { user: "bob" })) - .status, - ).toBe(403); - await server.request("POST", `/rooms/${oneOff}/end`); - expect((await server.request("GET", `/rooms/${oneOff}`)).status).toBe(404); - - const persistent = await roomWithBob(server, true); - await server.request("POST", `/rooms/${persistent}/end`); - expect((await server.request("GET", `/rooms/${persistent}`)).status).toBe( - 200, - ); - }); - - it("deletes a room and its members", async () => { - server = await startServer(); - const roomId = await roomWithBob(server); - expect((await server.request("DELETE", `/rooms/${roomId}`)).status).toBe( - 200, - ); - expect( - server.db.sqlite - .prepare("SELECT COUNT(*) AS n FROM p_session_sharing_room_members") - .get(), - ).toEqual({ n: 0 }); - }); -}); diff --git a/plugins/session-sharing/tests/backend/security.test.ts b/plugins/session-sharing/tests/backend/security.test.ts deleted file mode 100644 index be07427b3..000000000 --- a/plugins/session-sharing/tests/backend/security.test.ts +++ /dev/null @@ -1,425 +0,0 @@ -/** - * Guest tokens, participant permissions and expiry. These are what keep a - * share link from becoming a way into someone else's session, so each rule - * the old core code enforced has a test here. - */ - -import { afterEach, describe, expect, it } from "vitest"; -import { asUser, expireShare, startServer, type TestServer } from "./helpers"; - -let server: TestServer | null = null; - -afterEach(async () => { - await server?.close(); - server = null; -}); - -async function createShare( - s: TestServer, - body: Record = {}, -): Promise<{ shareId: string; linkToken: string | null }> { - if (!s.live.sessions.has("sess-1")) s.live.addSession("sess-1", "alice"); - const res = await s.request("POST", "/create", { - body: { - hostId: 1, - sessionId: "sess-1", - protocol: "ssh", - shareType: "link", - permissionLevel: "read-only", - ...body, - }, - }); - expect(res.status).toBe(200); - return res.body; -} - -async function roomOnStage(s: TestServer) { - const created = await s.request("POST", "/rooms", { body: { name: "R" } }); - const roomId = created.body.room.id as string; - await s.request("POST", `/rooms/${roomId}/members`, { - body: { userIds: ["bob"] }, - }); - s.live.addSession("sess-stage", "alice"); - await s.request("POST", `/rooms/${roomId}/present`, { - body: { protocol: "ssh", sessionId: "sess-stage", hostId: 1 }, - }); - const link = await s.request("POST", `/rooms/${roomId}/guest-link`, { - body: { enabled: true }, - }); - return { roomId, token: link.body.guestLinkToken as string }; -} - -describe("share link guests", () => { - it("resolves a live link without any host details", async () => { - server = await startServer(); - const { linkToken } = await createShare(server, { - permissionLevel: "read-write", - }); - - const res = await server.request("GET", `/resolve/${linkToken}`, { - user: null, - }); - expect(res.status).toBe(200); - expect(res.body).toEqual({ - protocol: "ssh", - permissionLevel: "read-write", - wsPath: `/plugin-ws/ssh-terminal/terminal?shareToken=${linkToken}`, - }); - - const joined = await server.guests.resolve({ - shareToken: linkToken!, - clientIp: "1.1.1.1", - }); - expect(joined).toEqual({ - ok: true, - share: expect.objectContaining({ - sessionId: "sess-1", - permissionLevel: "read-write", - }), - }); - }); - - it("keeps a read-only link read-only", async () => { - server = await startServer(); - const { linkToken } = await createShare(server); - const joined = await server.guests.resolve({ - shareToken: linkToken!, - clientIp: "1.1.1.1", - }); - expect(joined).toMatchObject({ share: { permissionLevel: "read-only" } }); - }); - - it("refuses an unknown or guessed token", async () => { - server = await startServer(); - await createShare(server); - for (const token of ["nope", "", "%00"]) { - const res = await server.request("GET", `/resolve/${token || "x"}`, { - user: null, - }); - expect(res.status).toBe(404); - expect( - await server.guests.resolve({ shareToken: token, clientIp: "1.1.1.1" }), - ).toMatchObject({ ok: false }); - } - }); - - it("refuses a revoked link", async () => { - server = await startServer(); - const { shareId, linkToken } = await createShare(server); - await server.request("DELETE", `/${shareId}`); - - expect( - (await server.request("GET", `/resolve/${linkToken}`, { user: null })) - .status, - ).toBe(404); - expect( - await server.guests.resolve({ - shareToken: linkToken!, - clientIp: "1.1.1.1", - }), - ).toEqual({ ok: false, reason: "Invalid or expired share link" }); - }); - - it("refuses a link once sharing is switched off", async () => { - server = await startServer(); - const { linkToken } = await createShare(server); - await server.mock.ctx.settings.set("globallyEnabled", false); - - expect( - (await server.request("GET", `/resolve/${linkToken}`, { user: null })) - .status, - ).toBe(404); - expect( - await server.guests.resolve({ - shareToken: linkToken!, - clientIp: "1.1.1.1", - }), - ).toMatchObject({ ok: false }); - }); - - it("answers 404 once the session is gone", async () => { - server = await startServer(); - const { linkToken } = await createShare(server); - server.live.sessions.delete("sess-1"); - const res = await server.request("GET", `/resolve/${linkToken}`, { - user: null, - }); - expect(res.status).toBe(404); - }); - - it("rate limits resolves per IP", async () => { - server = await startServer(); - let last = 0; - for (let i = 0; i < 31; i++) { - last = (await server.request("GET", "/resolve/guess", { user: null })) - .status; - } - expect(last).toBe(429); - }); - - it("records a guest join against the share", async () => { - server = await startServer(); - const { shareId } = await createShare(server); - await server.guests.recordJoin(shareId); - expect( - server.db.sqlite - .prepare( - "SELECT join_count, (SELECT guest_label FROM p_session_sharing_share_participants) AS label FROM p_session_sharing_shares", - ) - .get(), - ).toEqual({ join_count: 1, label: "Guest" }); - }); -}); - -describe("expired shares", () => { - it("stops resolving a link once it expires", async () => { - server = await startServer(); - const { shareId, linkToken } = await createShare(server); - expireShare(server, shareId); - - expect( - (await server.request("GET", `/resolve/${linkToken}`, { user: null })) - .status, - ).toBe(404); - expect( - await server.guests.resolve({ - shareToken: linkToken!, - clientIp: "1.1.1.1", - }), - ).toMatchObject({ ok: false }); - expect((await server.request("GET", "/host/1/active")).body.shares).toEqual( - [], - ); - }); - - it("refuses a member join on an expired user share", async () => { - server = await startServer(); - const { shareId } = await createShare(server, { - shareType: "user", - targetUserId: "bob", - }); - expireShare(server, shareId); - expect( - await asUser(server, "bob", () => server!.sharing.authorizeJoin(shareId)), - ).toBeNull(); - }); - - it("clears a room stage whose share expired and stops guests", async () => { - server = await startServer(); - const { roomId, token } = await roomOnStage(server); - const shareId = (await server.request("GET", `/rooms/${roomId}`)).body.stage - .shareId; - expireShare(server, shareId); - - const guest = await server.request("GET", `/guest/${token}`, { - user: null, - }); - expect(guest.body).toEqual({ roomName: "R", stage: null }); - expect( - await server.guests.resolve({ - roomGuestToken: token, - clientIp: "1.1.1.1", - }), - ).toEqual({ ok: false, reason: "Nothing is being presented" }); - expect( - (await server.request("GET", `/rooms/${roomId}/stage`)).body.stage, - ).toBeNull(); - }); -}); - -describe("member joins", () => { - it("lets only the target of a user share join", async () => { - server = await startServer(); - const { shareId } = await createShare(server, { - shareType: "user", - targetUserId: "bob", - permissionLevel: "read-write", - }); - - expect( - await asUser(server, "bob", () => server!.sharing.authorizeJoin(shareId)), - ).toEqual({ - share: { - id: shareId, - sessionId: "sess-1", - permissionLevel: "read-write", - }, - displayName: "bob", - }); - expect( - await asUser(server, "carol", () => - server!.sharing.authorizeJoin(shareId), - ), - ).toBeNull(); - }); - - it("refuses the target once they lose access to the host", async () => { - server = await startServer({ accessibleHostIds: [2] }); - server.live.addSession("sess-1", "alice", 1); - // Inserted directly: bob could reach host 1 when it was shared with him. - server.db.sqlite - .prepare( - "INSERT INTO p_session_sharing_shares (id, host_id, owner_user_id, protocol, session_id, share_type, target_user_id, expires_at) VALUES ('s', 1, 'alice', 'ssh', 'sess-1', 'user', 'bob', ?)", - ) - .run(new Date(Date.now() + 60_000).toISOString()); - - expect( - await asUser(server, "bob", () => server!.sharing.authorizeJoin("s")), - ).toBeNull(); - }); - - it("refuses a link share joined as a member, and no user at all", async () => { - server = await startServer(); - const { shareId } = await createShare(server); - expect( - await asUser(server, "bob", () => server!.sharing.authorizeJoin(shareId)), - ).toBeNull(); - expect(await server.sharing.authorizeJoin(shareId)).toBeNull(); - }); - - it("lets room members, and only them, join a room stage read-only", async () => { - server = await startServer(); - const { roomId } = await roomOnStage(server); - const shareId = (await server.request("GET", `/rooms/${roomId}`)).body.stage - .shareId; - - expect( - await asUser(server, "bob", () => server!.sharing.authorizeJoin(shareId)), - ).toMatchObject({ share: { permissionLevel: "read-only" } }); - expect( - await asUser(server, "carol", () => - server!.sharing.authorizeJoin(shareId), - ), - ).toBeNull(); - }); - - it("subscribes only members to room events", async () => { - server = await startServer(); - const { roomId } = await roomOnStage(server); - const socket = { readyState: 1, OPEN: 1, send: () => {} }; - expect( - await asUser(server, "carol", () => - server!.sharing.subscribeRoom(roomId, socket), - ), - ).toBe(false); - expect( - await asUser(server, "bob", () => - server!.sharing.subscribeRoom(roomId, socket), - ), - ).toBe(true); - await server.sharing.unsubscribeRoom(socket); - }); -}); - -describe("room guest links", () => { - it("resolves the stage for a guest without host details", async () => { - server = await startServer(); - const { token } = await roomOnStage(server); - - const res = await server.request("GET", `/guest/${token}`, { user: null }); - expect(res.body).toEqual({ - roomName: "R", - stage: { - protocol: "ssh", - shareId: expect.any(String), - wsPath: `/plugin-ws/ssh-terminal/terminal?roomGuestToken=${token}`, - }, - }); - expect( - await server.guests.resolve({ - roomGuestToken: token, - clientIp: "1.1.1.1", - }), - ).toMatchObject({ ok: true, share: { permissionLevel: "read-only" } }); - }); - - it("refuses the old token after a rotate and any token after disable", async () => { - server = await startServer(); - const { roomId, token } = await roomOnStage(server); - - const rotated = await server.request( - "POST", - `/rooms/${roomId}/guest-link`, - { - body: { enabled: true }, - }, - ); - expect( - (await server.request("GET", `/guest/${token}`, { user: null })).status, - ).toBe(404); - expect( - await server.guests.resolve({ - roomGuestToken: token, - clientIp: "1.1.1.1", - }), - ).toMatchObject({ ok: false }); - expect(server.live.calls).toContainEqual([ - "disconnectParticipants", - "sess-stage", - expect.any(String), - { userId: null, reason: "The guest link was rotated" }, - ]); - - await server.request("POST", `/rooms/${roomId}/guest-link`, { - body: { enabled: false }, - }); - expect( - ( - await server.request("GET", `/guest/${rotated.body.guestLinkToken}`, { - user: null, - }) - ).status, - ).toBe(404); - }); - - it("refuses a guest link after the room ends", async () => { - server = await startServer(); - const { roomId, token } = await roomOnStage(server); - await server.request("POST", `/rooms/${roomId}/end`); - expect( - (await server.request("GET", `/guest/${token}`, { user: null })).status, - ).toBe(404); - }); - - it("does not accept a room guest token as a share link, or the reverse", async () => { - server = await startServer(); - const { token } = await roomOnStage(server); - const { linkToken } = await createShare(server); - - expect( - await server.guests.resolve({ shareToken: token, clientIp: "1.1.1.1" }), - ).toMatchObject({ ok: false }); - expect( - (await server.request("GET", `/guest/${linkToken}`, { user: null })) - .status, - ).toBe(404); - }); - - it("only lets the host manage the guest link", async () => { - server = await startServer(); - const { roomId } = await roomOnStage(server); - expect( - ( - await server.request("POST", `/rooms/${roomId}/guest-link`, { - user: "bob", - body: { enabled: true }, - }) - ).status, - ).toBe(403); - }); - - it("rate limits room guests per IP", async () => { - server = await startServer(); - let refused = false; - for (let i = 0; i < 61; i++) { - const result = await server.guests.resolve({ - roomGuestToken: "guess", - clientIp: "9.9.9.9", - }); - if (result.ok === false && result.reason === "Too many requests") { - refused = true; - } - } - expect(refused).toBe(true); - }); -}); diff --git a/plugins/session-sharing/tests/backend/shares.test.ts b/plugins/session-sharing/tests/backend/shares.test.ts deleted file mode 100644 index 693c68fd5..000000000 --- a/plugins/session-sharing/tests/backend/shares.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { startServer, type TestServer } from "./helpers"; - -let server: TestServer | null = null; - -afterEach(async () => { - await server?.close(); - server = null; -}); - -const linkShare = (overrides: Record = {}) => ({ - hostId: 1, - sessionId: "sess-1", - protocol: "ssh", - shareType: "link", - permissionLevel: "read-only", - ...overrides, -}); - -describe("share routes", () => { - it("creates a link share for a session the caller owns", async () => { - server = await startServer(); - server.live.addSession("sess-1", "alice"); - - const res = await server.request("POST", "/create", { body: linkShare() }); - - expect(res.status).toBe(200); - expect(res.body.linkToken).toMatch(/^[A-Za-z0-9_-]{32}$/); - const active = await server.request("GET", "/host/1/active"); - expect(active.body.shares).toHaveLength(1); - expect(active.body.shares[0]).toMatchObject({ - ownerUserId: "alice", - shareType: "link", - permissionLevel: "read-only", - }); - }); - - it("refuses a session someone else owns", async () => { - server = await startServer(); - server.live.addSession("sess-1", "bob"); - - const res = await server.request("POST", "/create", { body: linkShare() }); - expect(res.status).toBe(403); - }); - - it("refuses when sharing is off for the host or everywhere", async () => { - server = await startServer({ settings: { globallyEnabled: false } }); - server.live.addSession("sess-1", "alice"); - expect( - (await server.request("POST", "/create", { body: linkShare() })).status, - ).toBe(403); - await server.close(); - - server = await startServer(); - server.live.addSession("sess-1", "alice"); - await server.mock.ctx.settings.setHost(1, "allowSessionSharing", false); - expect( - (await server.request("POST", "/create", { body: linkShare() })).status, - ).toBe(403); - }); - - it("validates the request", async () => { - server = await startServer(); - for (const body of [ - {}, - linkShare({ protocol: "ftp" }), - linkShare({ shareType: "room" }), - linkShare({ permissionLevel: "owner" }), - linkShare({ shareType: "user" }), - ]) { - expect((await server.request("POST", "/create", { body })).status).toBe( - 400, - ); - } - }); - - it("shares with a user only when they can reach the host", async () => { - server = await startServer({ accessibleHostIds: [1] }); - server.live.addSession("sess-1", "alice", 1); - server.live.addSession("sess-2", "alice", 2); - - const ok = await server.request("POST", "/create", { - body: linkShare({ shareType: "user", targetUserId: "bob" }), - }); - expect(ok.status).toBe(200); - expect(ok.body.linkToken).toBeNull(); - - const noAccess = await server.request("POST", "/create", { - body: linkShare({ - hostId: 2, - sessionId: "sess-2", - shareType: "user", - targetUserId: "bob", - }), - }); - expect(noAccess.status).toBe(403); - - const unknownUser = await server.request("POST", "/create", { - body: linkShare({ shareType: "user", targetUserId: "nobody" }), - }); - expect(unknownUser.status).toBe(403); - }); - - it("lets the owner or an admin revoke, and kicks SSH guests", async () => { - server = await startServer({ admins: ["carol"] }); - server.live.addSession("sess-1", "alice"); - const { body } = await server.request("POST", "/create", { - body: linkShare(), - }); - - expect( - (await server.request("DELETE", `/${body.shareId}`, { user: "bob" })) - .status, - ).toBe(403); - expect( - (await server.request("DELETE", `/${body.shareId}`, { user: "carol" })) - .status, - ).toBe(200); - expect(server.live.calls[0]).toEqual([ - "ownerEndSession", - "sess-1", - "Session share revoked by owner", - ]); - expect( - ( - await server.request("GET", `/resolve/${body.linkToken}`, { - user: null, - }) - ).status, - ).toBe(404); - expect((await server.request("DELETE", "/missing")).status).toBe(404); - }); - - it("ends a shared session for its owner only", async () => { - server = await startServer(); - server.live.addSession("sess-1", "alice"); - const { body } = await server.request("POST", "/create", { - body: linkShare(), - }); - - expect( - (await server.request("POST", `/${body.shareId}/end`, { user: "bob" })) - .status, - ).toBe(403); - expect((await server.request("POST", `/${body.shareId}/end`)).status).toBe( - 200, - ); - expect(server.live.calls).toContainEqual([ - "ownerEndSession", - "sess-1", - "Session ended by owner", - ]); - }); - - it("lists live sessions shared with the caller", async () => { - server = await startServer(); - server.live.addSession("sess-1", "alice"); - const { body } = await server.request("POST", "/create", { - body: linkShare({ shareType: "user", targetUserId: "bob" }), - }); - - const res = await server.request("GET", "/shared-with-me", { user: "bob" }); - expect(res.body).toEqual([ - expect.objectContaining({ - sessionId: "sess-1", - hostName: "host-1", - isOwnSession: false, - sharedByUsername: "alice", - shareId: body.shareId, - permissionLevel: "read-only", - }), - ]); - - server.live.sessions.delete("sess-1"); - expect( - (await server.request("GET", "/shared-with-me", { user: "bob" })).body, - ).toEqual([]); - }); - - it("lists users and roles for the pickers", async () => { - server = await startServer(); - server.db.sqlite - .prepare("INSERT INTO roles (id, name, display_name) VALUES (?, ?, ?)") - .run(3, "ops", "Operations"); - const res = await server.request("GET", "/directory"); - expect(res.body.users.map((u: { id: string }) => u.id)).toEqual([ - "alice", - "bob", - "carol", - ]); - expect(res.body.roles).toEqual([ - { id: 3, name: "ops", displayName: "Operations" }, - ]); - }); - - it("denies every signed-in route without the use permission", async () => { - server = await startServer({ permissions: [] }); - for (const [method, path] of [ - ["POST", "/create"], - ["GET", "/host/1/active"], - ["GET", "/shared-with-me"], - ["GET", "/directory"], - ["DELETE", "/share-1"], - ["POST", "/share-1/end"], - ["POST", "/rooms"], - ["GET", "/rooms"], - ["GET", "/rooms/r"], - ["POST", "/rooms/r/members"], - ["DELETE", "/rooms/r/members/bob"], - ["POST", "/rooms/r/present"], - ["POST", "/rooms/r/stop"], - ["GET", "/rooms/r/stage"], - ["POST", "/rooms/r/control"], - ["POST", "/rooms/r/control/request"], - ["GET", "/rooms/r/control/requests"], - ["DELETE", "/rooms/r/control/requests/bob"], - ["POST", "/rooms/r/guest-link"], - ["POST", "/rooms/r/end"], - ["DELETE", "/rooms/r"], - ]) { - const res = await server.request(method, path, { body: {} }); - expect([method, path, res.status]).toEqual([method, path, 403]); - } - }); -}); diff --git a/plugins/session-sharing/tests/frontend/SharedSessionView.test.tsx b/plugins/session-sharing/tests/frontend/SharedSessionView.test.tsx deleted file mode 100644 index 57c469093..000000000 --- a/plugins/session-sharing/tests/frontend/SharedSessionView.test.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import type { ComponentType } from "react"; -import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; -import { - renderWithApp, - type RenderedPluginApp, -} from "@termix/plugin-sdk/testing"; -import { PluginComponent } from "@termix/plugin-sdk/ui"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import * as plugin from "../../src/frontend/index"; -import SharedSessionView from "../../src/frontend/SharedSessionView"; -import manifestJson from "../../manifest.json"; -import locales from "../../locales/en.json"; - -vi.mock("react-xtermjs", () => ({ - useXTerm: () => ({ - instance: { - options: {}, - loadAddon: vi.fn(), - open: vi.fn(), - write: vi.fn(), - onData: vi.fn(), - }, - ref: { current: document.createElement("div") }, - }), -})); - -vi.mock("@xterm/addon-fit", () => ({ - FitAddon: class FitAddon { - fit() {} - }, -})); - -const manifest = manifestJson as unknown as PluginManifest; -const guestView = locales.sessionSharing.guestView; - -let rendered: RenderedPluginApp | null = null; - -afterEach(async () => { - await rendered?.deactivate(); - rendered = null; -}); - -function httpError(status: number) { - return Object.assign(new Error(`HTTP ${status}`), { response: { status } }); -} - -/** The shared-link page, rendered as a guest inside the plugin's scope. */ -async function renderShared(search: string, get: PluginApiClient["get"]) { - window.history.pushState({}, "", `/?${search}`); - rendered = await renderWithApp(plugin, { - manifest, - locales, - guest: true, - api: { get } as unknown as PluginApiClient, - }); - rendered.app.registerComponent( - "session-sharing.testSharedView", - SharedSessionView as ComponentType>, - ); - rendered.app.registerSlotContribution("session.remoteDisplay", { - actionId: "fixture.display", - titleKey: "nav.collab", - kind: "component", - component: () =>
, - }); - render(); -} - -describe("SharedSessionView", () => { - it("says the link is invalid when there is no token", async () => { - const get = vi.fn(); - await renderShared("view=shared", get as never); - expect(await screen.findByText(guestView.linkInvalid)).toBeTruthy(); - expect(get).not.toHaveBeenCalled(); - }); - - it("says the link is invalid for a revoked or expired link", async () => { - await renderShared( - "view=shared&token=gone", - vi.fn(async () => { - throw httpError(404); - }) as never, - ); - expect(await screen.findByText(guestView.linkInvalid)).toBeTruthy(); - }); - - it("asks the guest to wait when rate limited", async () => { - await renderShared( - "view=shared&token=busy", - vi.fn(async () => { - throw httpError(429); - }) as never, - ); - expect(await screen.findByText(guestView.rateLimited)).toBeTruthy(); - }); - - it("draws a remote desktop share through the display slot", async () => { - const get = vi.fn(async () => ({ - data: { - protocol: "rdp", - permissionLevel: "read-only", - wsPath: "/plugin-ws/remote-desktop/display", - connectParams: { token: "t" }, - }, - })); - await renderShared("view=shared&token=ok", get as never); - expect(await screen.findByTestId("remote-display")).toBeTruthy(); - expect(screen.getByText(guestView.readOnlyBadge)).toBeTruthy(); - expect(get).toHaveBeenCalledWith("/resolve/ok"); - }); -}); diff --git a/plugins/session-sharing/tests/frontend/SharedWithMeSection.test.tsx b/plugins/session-sharing/tests/frontend/SharedWithMeSection.test.tsx deleted file mode 100644 index 051730e6e..000000000 --- a/plugins/session-sharing/tests/frontend/SharedWithMeSection.test.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; - -const api = vi.hoisted(() => ({ - getSharedWithMe: vi.fn(async () => [] as unknown[]), -})); -const sdk = vi.hoisted(() => ({ - invokeAction: vi.fn(async () => undefined), - allowed: true, -})); - -vi.mock("../../src/frontend/api", () => api); -vi.mock("@termix/plugin-sdk/frontend", async (importActual) => ({ - ...(await importActual()), - invokeAction: sdk.invokeAction, - usePermission: () => sdk.allowed, - useHosts: () => ({ hosts: [], loaded: true }), - useTranslation: () => ({ - t: (key: string, options?: Record) => - options ? `${key}:${JSON.stringify(options)}` : key, - language: "en", - }), -})); - -import { SharedWithMeSection } from "../../src/frontend/SharedWithMeSection"; - -function shared(overrides: Record = {}) { - return { - sessionId: "sess-1", - hostId: 5, - hostName: "prod-db", - isConnected: true, - sharedByUsername: "alice", - permissionLevel: "read-only", - shareId: "share-1", - ...overrides, - }; -} - -beforeEach(() => { - api.getSharedWithMe.mockReset(); - sdk.invokeAction.mockClear(); - sdk.allowed = true; -}); - -afterEach(() => cleanup()); - -describe("shared with me section", () => { - it("lists sessions shared with the user", async () => { - api.getSharedWithMe.mockResolvedValue([shared()]); - render(); - - expect(await screen.findByText("prod-db")).toBeTruthy(); - expect( - screen.getByText('connections.sharedBy:{"username":"alice"}'), - ).toBeTruthy(); - expect( - screen.getByText("sessionSharing.permissionLevel.readOnly"), - ).toBeTruthy(); - }); - - it("hides a session the user already joined in a tab", async () => { - api.getSharedWithMe.mockResolvedValue([shared()]); - render( - , - ); - await vi.waitFor(() => expect(api.getSharedWithMe).toHaveBeenCalled()); - expect(screen.queryByText("prod-db")).toBeNull(); - }); - - it("filters by the connections search", async () => { - api.getSharedWithMe.mockResolvedValue([ - shared(), - shared({ sessionId: "sess-2", hostName: "web", sharedByUsername: "bob" }), - ]); - render(); - expect(await screen.findByText("web")).toBeTruthy(); - expect(screen.queryByText("prod-db")).toBeNull(); - }); - - it("joins through the terminal's open action", async () => { - api.getSharedWithMe.mockResolvedValue([shared()]); - render(); - fireEvent.click(await screen.findByText("connections.join")); - - expect(sdk.invokeAction).toHaveBeenCalledWith( - "terminal.open", - expect.objectContaining({ id: "5", name: "prod-db" }), - expect.objectContaining({ - joinSharedSessionId: "sess-1", - joinShareId: "share-1", - }), - ); - }); - - it("renders nothing for a user without the use permission", async () => { - sdk.allowed = false; - api.getSharedWithMe.mockResolvedValue([shared()]); - const { container } = render(); - expect(container.textContent).toBe(""); - expect(api.getSharedWithMe).not.toHaveBeenCalled(); - }); -}); diff --git a/plugins/session-sharing/tests/frontend/activate.test.tsx b/plugins/session-sharing/tests/frontend/activate.test.tsx deleted file mode 100644 index 601f4d78d..000000000 --- a/plugins/session-sharing/tests/frontend/activate.test.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { act, fireEvent, screen, waitFor } from "@testing-library/react"; -import { - invokeAction, - type PluginApiClient, -} from "@termix/plugin-sdk/frontend"; -import { - renderWithApp, - type RenderedPluginApp, -} from "@termix/plugin-sdk/testing"; -import type { PluginManifest } from "@termix/plugin-sdk/manifest"; -import * as plugin from "../../src/frontend/index"; -import manifestJson from "../../manifest.json"; -import locales from "../../locales/en.json"; - -const manifest = manifestJson as unknown as PluginManifest; - -let rendered: RenderedPluginApp | null = null; - -afterEach(async () => { - await rendered?.deactivate(); - rendered = null; -}); - -function stubApi(overrides: Partial = {}): PluginApiClient { - return { - get: vi.fn(async (url: string) => ({ - data: - url === "/directory" - ? { users: [{ id: "bob", username: "bob" }], roles: [] } - : url.includes("/active") - ? { shares: [] } - : url === "/rooms" - ? { rooms: [] } - : {}, - })), - post: vi.fn(async () => ({ - data: { shareId: "s1", linkToken: "tok", expiresAt: "2999-01-01" }, - })), - put: vi.fn(async () => ({ data: {} })), - patch: vi.fn(async () => ({ data: {} })), - delete: vi.fn(async () => ({ data: {} })), - ...overrides, - } as PluginApiClient; -} - -describe("session-sharing activate", () => { - it("registers the rooms rail item, panel and tab, and the share buttons", async () => { - rendered = await renderWithApp(plugin, { - manifest, - locales, - api: stubApi(), - }); - - expect(rendered.registered.railItems()).toEqual([ - expect.objectContaining({ - id: "collab", - permission: "session-sharing.use", - }), - ]); - expect(rendered.registered.panels()).toEqual(["collab"]); - expect(rendered.registered.tabs()).toEqual(["collab"]); - expect(rendered.registered.slot("terminal.toolbar")).toEqual([ - "session-sharing.share", - ]); - expect(rendered.registered.slot("remote-desktop.toolbar")).toEqual([ - "session-sharing.share", - ]); - expect(rendered.registered.slot("tab.inline")).toEqual([ - "session-sharing.share", - ]); - expect(rendered.registered.slot("shell.overlay")).toHaveLength(2); - expect(rendered.registered.actions().sort()).toEqual([ - "session-sharing.share", - ]); - expect(rendered.registered.slot("connections.sections")).toEqual([ - "session-sharing.sharedWithMe", - ]); - }); - - it("shows terminal participants and clears them after the last guest leaves or the socket closes", async () => { - rendered = await renderWithApp(plugin, { - manifest, - locales, - api: stubApi(), - }); - expect(rendered.registered.slot("terminal.overlay")).toEqual([ - "session-sharing.participants", - ]); - let receive: (message: { - type: string; - [key: string]: unknown; - }) => void = () => {}; - const unsubscribe = vi.fn(); - rendered.renderSlot("terminal.overlay", { - subscribe: (listener: typeof receive) => { - receive = listener; - return unsubscribe; - }, - }); - const owner = { isOwner: true, permissionLevel: "read-write", label: null }; - const guest = { - isOwner: false, - permissionLevel: "read-only", - label: "Guest 1", - }; - await act(() => - receive({ type: "participants", participants: [owner, guest] }), - ); - expect( - screen.getByTitle( - `${locales.sessionSharing.guestView.ownerLabel}, Guest 1`, - ).textContent, - ).toBe("2"); - await act(() => receive({ type: "participants", participants: [owner] })); - expect(screen.queryByText("2")).toBeNull(); - await act(() => - receive({ type: "participants", participants: [owner, guest] }), - ); - await act(() => receive({ type: "session_closed" })); - expect(screen.queryByText("2")).toBeNull(); - await rendered.deactivate(); - expect(unsubscribe).toHaveBeenCalled(); - rendered = null; - }); - - it("registers only the guest views on a guest page", async () => { - rendered = await renderWithApp(plugin, { - manifest, - locales, - api: stubApi(), - guest: true, - }); - expect(rendered.registered.tabs()).toEqual(["collab"]); - expect(rendered.registered.railItems()).toEqual([]); - expect(rendered.registered.actions()).toEqual([]); - }); - - it("opens the share dialog for a terminal's session", async () => { - const api = stubApi(); - rendered = await renderWithApp(plugin, { manifest, locales, api }); - rendered.renderSlot("shell.overlay"); - - await act(() => - invokeAction("session-sharing.share", { - getShareTarget: () => ({ - hostId: 4, - sessionId: "sess-9", - protocol: "ssh", - }), - }), - ); - - expect( - await screen.findByText(locales.sessionSharing.modalTitle), - ).toBeTruthy(); - await waitFor(() => expect(api.get).toHaveBeenCalledWith("/host/4/active")); - }); - - it("opens the dialog for a remote desktop session once it is up", async () => { - rendered = await renderWithApp(plugin, { - manifest, - locales, - api: stubApi(), - }); - rendered.renderSlot("shell.overlay"); - - await act(() => - invokeAction("session-sharing.share", { - hostId: 2, - sessionId: null, - protocol: "rdp", - }), - ); - expect(screen.queryByText(locales.sessionSharing.modalTitle)).toBeNull(); - - await act(() => - invokeAction("session-sharing.share", { - hostId: 2, - sessionId: "guac-1", - protocol: "rdp", - }), - ); - expect( - await screen.findByText(locales.sessionSharing.modalTitle), - ).toBeTruthy(); - }); - - it("creates a link share from the dialog", async () => { - const api = stubApi(); - rendered = await renderWithApp(plugin, { manifest, locales, api }); - rendered.renderSlot("shell.overlay"); - await act(() => - invokeAction("session-sharing.share", { - getShareTarget: () => ({ - hostId: 1, - sessionId: "sess-1", - protocol: "ssh", - }), - }), - ); - - fireEvent.click( - await screen.findByRole("button", { - name: locales.sessionSharing.createLinkButton, - }), - ); - await waitFor(() => - expect(api.post).toHaveBeenCalledWith( - "/create", - expect.objectContaining({ - hostId: 1, - sessionId: "sess-1", - shareType: "link", - permissionLevel: "read-only", - }), - ), - ); - }); - - it("removes everything it registered on deactivate", async () => { - const app = await renderWithApp(plugin, { - manifest, - locales, - api: stubApi(), - }); - await app.deactivate(); - expect(app.registered.tabs()).toEqual([]); - expect(app.registered.panels()).toEqual([]); - expect(app.registered.slot("terminal.toolbar")).toEqual([]); - expect(app.registered.actions()).toEqual([]); - }); -}); diff --git a/plugins/session-sharing/tests/frontend/guest-terminal-size.test.tsx b/plugins/session-sharing/tests/frontend/guest-terminal-size.test.tsx deleted file mode 100644 index 5adfede78..000000000 --- a/plugins/session-sharing/tests/frontend/guest-terminal-size.test.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { act, cleanup, render, waitFor } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { GuestTerminalView } from "../../src/frontend/SharedSessionView"; - -const f = vi.hoisted(() => ({ - terminal: { - cols: 80, - rows: 24, - options: {}, - loadAddon: vi.fn(), - open: vi.fn(), - write: vi.fn(), - resize: vi.fn(), - }, - fit: vi.fn(), - onResize: null as (() => void) | null, -})); -vi.mock("@termix/plugin-sdk/frontend", () => ({ - useTranslation: () => ({ t: (key: string) => key }), -})); -vi.mock("react-xtermjs", () => ({ - useXTerm: () => ({ instance: f.terminal, ref: { current: null } }), -})); -vi.mock("@xterm/addon-fit", () => ({ - FitAddon: class { - fit() { - f.fit(); - } - }, -})); -vi.mock("../../src/frontend/shared", () => ({ - wsUrlForPath: async () => "ws://termix.test/terminal", - RemoteDisplay: () => null, - Loader: () => null, -})); -class Socket { - static OPEN = 1; - static latest: Socket; - readyState = 1; - onopen?: () => void; - onmessage?: (event: { data: string }) => void; - onclose?: () => void; - onerror?: () => void; - constructor() { - Socket.latest = this; - } - send() {} - close() {} - message(value: object) { - this.onmessage?.({ data: JSON.stringify(value) }); - } -} -beforeEach(() => { - vi.clearAllMocks(); - f.terminal.cols = 80; - f.terminal.rows = 24; - f.terminal.resize.mockImplementation((cols: number, rows: number) => { - f.terminal.cols = cols; - f.terminal.rows = rows; - }); - f.fit.mockImplementation(() => { - f.terminal.cols = 80; - f.terminal.rows = 24; - }); - vi.stubGlobal("WebSocket", Socket); - vi.stubGlobal( - "ResizeObserver", - class { - constructor(fn: () => void) { - f.onResize = fn; - } - observe() {} - disconnect() {} - }, - ); -}); -afterEach(() => { - cleanup(); - vi.unstubAllGlobals(); -}); - -it("keeps the presenter's grid while the guest window changes size", async () => { - render( - , - ); - await waitFor(() => expect(Socket.latest.onmessage).toBeTypeOf("function")); - act(() => Socket.latest.message({ type: "resized", cols: 132, rows: 40 })); - expect([f.terminal.cols, f.terminal.rows]).toEqual([132, 40]); - act(() => f.onResize?.()); - expect([f.terminal.cols, f.terminal.rows]).toEqual([132, 40]); - act(() => Socket.latest.message({ type: "resized", cols: 100, rows: 30 })); - expect([f.terminal.cols, f.terminal.rows]).toEqual([100, 30]); -}); - -describe("invalid shared terminal dimensions", () => { - it.each([0, -1, 1.5, "132"])("ignores cols=%s", async (cols) => { - render( - , - ); - await waitFor(() => expect(Socket.latest.onmessage).toBeTypeOf("function")); - act(() => Socket.latest.message({ type: "resized", cols, rows: 24 })); - expect(f.terminal.resize).not.toHaveBeenCalled(); - }); -}); diff --git a/plugins/session-sharing/tests/frontend/meeting-backend.test.ts b/plugins/session-sharing/tests/frontend/meeting-backend.test.ts deleted file mode 100644 index 0f1d37341..000000000 --- a/plugins/session-sharing/tests/frontend/meeting-backend.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import type { PluginApiClient, TermixApp } from "@termix/plugin-sdk/frontend"; -import { - connectMeetings, - meetingGuestUrl, -} from "../../src/frontend/meeting-backend"; - -function client(): PluginApiClient { - return Object.fromEntries( - ["get", "post", "put", "patch", "delete"].map((method) => [ - method, - vi.fn(async () => ({ data: { rooms: [] } })), - ]), - ) as unknown as PluginApiClient; -} -afterEach(() => vi.restoreAllMocks()); - -describe("meeting backend", () => { - it("routes rooms, invites and host resolution to the linked server and refuses a later switch", async () => { - const local = client(); - const remote = client(); - let url: string | null = "https://termix.example/base/"; - const app = { - desktop: { available: true, remoteServerUrl: async () => url }, - apiFor: (origin: string) => (origin === "remote" ? remote : local), - t: (key: string) => key, - } as unknown as TermixApp; - const backend = await connectMeetings(app); - expect(backend.origin).toBe("remote"); - await backend.api.listCollabRooms(); - await backend.api.createCollabRoom("Standup", true); - await backend.api.getDirectory(); - await backend.api.resolveHost("stable-host-id"); - await backend.api.presentCollabStage("room", { - hostId: 99, - sessionId: "remote-session", - protocol: "ssh", - }); - expect(local.get).not.toHaveBeenCalled(); - expect(local.post).not.toHaveBeenCalled(); - expect(remote.get).toHaveBeenCalledWith("/meeting-host/stable-host-id"); - expect(remote.post).toHaveBeenCalledWith("/rooms/room/present", { - hostId: 99, - sessionId: "remote-session", - protocol: "ssh", - }); - expect(meetingGuestUrl(backend.publicUrl, "a+b&c")).toBe( - "https://termix.example/base/?view=collab-guest&token=a%2Bb%26c", - ); - url = "https://other.example"; - await expect(backend.api.deleteCollabRoom("room")).rejects.toThrow( - "collab.serverChanged", - ); - expect(remote.delete).not.toHaveBeenCalled(); - url = null; - await expect(backend.api.getCollabRoom("room")).rejects.toThrow( - "collab.serverChanged", - ); - }); - - it("keeps standalone rooms local without generating a file URL", async () => { - const local = client(); - const backend = await connectMeetings({ - desktop: { available: true, remoteServerUrl: async () => null }, - apiFor: () => local, - t: (key: string) => key, - } as unknown as TermixApp); - expect(backend.origin).toBe("local"); - await backend.api.listCollabRooms(); - expect(local.get).toHaveBeenCalledWith("/rooms"); - expect(meetingGuestUrl(backend.publicUrl, "token")).toBeNull(); - expect(meetingGuestUrl("file:///app/index.html", "token")).toBeNull(); - }); - - it("preserves web deployment paths while replacing old query and fragment values", () => { - expect( - meetingGuestUrl("https://termix.example/app/?view=other#old", "token"), - ).toBe("https://termix.example/app/?view=collab-guest&token=token"); - }); -}); diff --git a/plugins/session-sharing/tests/frontend/meeting-transport.test.ts b/plugins/session-sharing/tests/frontend/meeting-transport.test.ts deleted file mode 100644 index 166dd355b..000000000 --- a/plugins/session-sharing/tests/frontend/meeting-transport.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -const { pluginWsUrl } = vi.hoisted(() => ({ pluginWsUrl: vi.fn() })); -vi.mock("@termix/plugin-sdk/ui", () => ({ pluginWsUrl })); -import { wsTargetForPath } from "../../src/frontend/shared"; - -describe("meeting event transport", () => { - it("preserves remote authentication protocols and event query parameters", async () => { - pluginWsUrl.mockResolvedValue({ - url: "wss://server/base/plugin-ws/ssh-terminal/terminal", - protocols: ["jwt", "session-token"], - }); - expect( - await wsTargetForPath( - "/plugin-ws/ssh-terminal/terminal?room=abc", - "remote", - ), - ).toEqual({ - url: "wss://server/base/plugin-ws/ssh-terminal/terminal?room=abc", - protocols: ["jwt", "session-token"], - }); - expect(pluginWsUrl).toHaveBeenCalledWith("ssh-terminal", "/terminal", { - origin: "remote", - }); - expect( - await wsTargetForPath("https://another-server/ws", "remote"), - ).toBeNull(); - }); -}); diff --git a/plugins/session-sharing/tests/frontend/presenter-takeover.test.ts b/plugins/session-sharing/tests/frontend/presenter-takeover.test.ts deleted file mode 100644 index 539e8a6e1..000000000 --- a/plugins/session-sharing/tests/frontend/presenter-takeover.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/// -import ts from "typescript"; -import { describe, expect, it, vi } from "vitest"; -import roomSource from "../../src/frontend/CollabRoomTab.tsx?raw"; - -// Run the room's real refresh callback across successive server snapshots. -const source = ts.createSourceFile( - "CollabRoomTab.tsx", - roomSource, - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.TSX, -); -let callback: string | undefined; -function visit(node: ts.Node) { - if ( - ts.isVariableDeclaration(node) && - node.name.getText(source) === "refresh" && - node.initializer && - ts.isCallExpression(node.initializer) - ) { - callback = node.initializer.arguments[0].getText(source); - } - ts.forEachChild(node, visit); -} -visit(source); -if (!callback) throw new Error("Missing room refresh callback"); -const refresh = new Function( - "state", - ts.transpileModule( - ` - const { roomId, refreshSequence, presenterRef, stageKeyRef, draftRef, - getCollabRoom, getCollabStage, setDetail, setLoadError, setDraft, - setStage, getErrorMessage } = state; - return (${callback})(); -`, - { compilerOptions: { target: ts.ScriptTarget.ES2022 } }, - ).outputText, -) as (state: ReturnType) => Promise; - -function snapshot(presenterUserId: string, shareId: string) { - return { - me: "alice", - stage: { presenterUserId, shareId }, - controllerUserId: null, - }; -} - -function fixture() { - const state = { - roomId: "room", - refreshSequence: { current: 0 }, - presenterRef: { current: null as string | null }, - stageKeyRef: { current: null as string | null }, - draftRef: { current: null as object | null }, - getCollabRoom: vi.fn(async () => snapshot("alice", "alice-share")), - getCollabStage: vi.fn(async () => ({ stage: { shareId: "bob-share" } })), - setDetail: vi.fn(), - setLoadError: vi.fn(), - setDraft: vi.fn((draft: object | null) => { - state.draftRef.current = draft; - }), - setStage: vi.fn(), - getErrorMessage: String, - }; - return state; -} - -describe("meeting presenter takeover", () => { - it("replaces the previous presenter's local session with the new stage", async () => { - const state = fixture(); - state.draftRef.current = { protocol: "ssh", host: { id: 1 } }; - await refresh(state); - state.getCollabRoom.mockResolvedValue(snapshot("bob", "bob-share")); - await refresh(state); - - expect(state.draftRef.current).toBeNull(); - expect(state.setStage).toHaveBeenCalledWith({ shareId: "bob-share" }); - expect(state.getCollabStage).toHaveBeenCalledWith("room"); - }); - - it("keeps a new connection being prepared to take over another presenter", async () => { - const state = fixture(); - state.getCollabRoom.mockResolvedValue(snapshot("bob", "bob-share")); - await refresh(state); - const draft = { protocol: "ssh", host: { id: 2 } }; - state.draftRef.current = draft; - await refresh(state); - - expect(state.draftRef.current).toBe(draft); - expect(state.setDraft).not.toHaveBeenCalled(); - }); - - it("keeps the local session during ordinary room refreshes", async () => { - const state = fixture(); - const draft = { protocol: "ssh", host: { id: 1 } }; - state.draftRef.current = draft; - await refresh(state); - await refresh(state); - - expect(state.draftRef.current).toBe(draft); - expect(state.getCollabStage).not.toHaveBeenCalled(); - expect(state.setDraft).not.toHaveBeenCalled(); - }); -}); diff --git a/plugins/session-sharing/tests/frontend/share-link-url.test.ts b/plugins/session-sharing/tests/frontend/share-link-url.test.ts deleted file mode 100644 index d3716ebda..000000000 --- a/plugins/session-sharing/tests/frontend/share-link-url.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, it } from "vitest"; -import type { TermixApp } from "@termix/plugin-sdk/frontend"; -import { - guestViewUrl, - sessionPublicUrl, -} from "../../src/frontend/meeting-backend"; - -function desktop(url: string | null): TermixApp { - return { - desktop: { available: true, remoteServerUrl: async () => url }, - } as unknown as TermixApp; -} - -describe("session share links", () => { - it("builds the link on the linked server for a remote session", async () => { - const base = await sessionPublicUrl( - desktop("https://termix.example/"), - "remote", - ); - expect(guestViewUrl(base, "shared", "tok")).toBe( - "https://termix.example/?view=shared&token=tok", - ); - }); - - it("gives no link for a session on the desktop's own backend", async () => { - expect( - await sessionPublicUrl(desktop("https://termix.example/"), "local"), - ).toBeNull(); - expect( - guestViewUrl("file:///C:/app/index.html", "shared", "tok"), - ).toBeNull(); - }); - - it("uses the page address in a browser", async () => { - const app = { desktop: { available: false } } as unknown as TermixApp; - expect(await sessionPublicUrl(app, undefined)).toBe(window.location.href); - }); -}); diff --git a/plugins/session-sharing/tsconfig.json b/plugins/session-sharing/tsconfig.json deleted file mode 100644 index 7b35314c3..000000000 --- a/plugins/session-sharing/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@termix/plugin-sdk/tsconfig.plugin.json", - "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] -} diff --git a/plugins/session-sharing/vitest.config.ts b/plugins/session-sharing/vitest.config.ts deleted file mode 100644 index 31ba004fc..000000000 --- a/plugins/session-sharing/vitest.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; - -export default pluginVitestConfig(import.meta.url); diff --git a/plugins/snippets/CHANGELOG.md b/plugins/snippets/CHANGELOG.md deleted file mode 100644 index 8774d7fe6..000000000 --- a/plugins/snippets/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -## 1.0.0 - -First release, bundled with Termix 2.9.0. - -Save commands and notes in folders, then run or paste them into a terminal. Snippets support variables and can be shared with other users or roles. diff --git a/plugins/snippets/README.md b/plugins/snippets/README.md deleted file mode 100644 index fe4a582b7..000000000 --- a/plugins/snippets/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Snippets - -Save commands and notes, then run or paste them into a terminal. Supports variables and sharing. - -## Features - -- Save commands and notes and sort them into folders. -- Run a snippet or paste it into a terminal. -- Use variables like `$HOST` and `$INPUT_1` that are filled in when the snippet runs. -- Share snippets and folders with other users or roles. -- Snippets sync between the desktop app and a server. - -## Settings - -### User - -- Collapse folders by default: start each folder closed when you open the Snippets panel. - -## Permissions - -- `snippets.view`: See your own and shared snippets. Admins and users have it by default. -- `snippets.create`: Create snippets, notes and folders. Admins and users have it by default. -- `snippets.edit`: Change snippets and folders. Admins and users have it by default. -- `snippets.delete`: Delete snippets and folders. Admins and users have it by default. -- `snippets.share`: Share snippets and folders. Admins and users have it by default. - -## Services - -Provides to other plugins: - -- `snippets.access`: read and run snippets. - -## Development - -```bash -npm run build # build into dist/ -npm run test # run this plugin's tests -npm run typecheck # type-check this plugin -``` diff --git a/plugins/snippets/locales/en.json b/plugins/snippets/locales/en.json deleted file mode 100644 index 2101cb3b7..000000000 --- a/plugins/snippets/locales/en.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Snippets" - }, - "onboarding": { - "feature_snippets": "Snippets", - "feature_snippets_desc": "Save commands you run often and send them to a terminal in one click." - }, - "permissions": { - "view": { - "title": "View snippets", - "description": "See your own snippets, notes and shared snippets." - }, - "create": { - "title": "Create snippets", - "description": "Create new snippets, notes and folders." - }, - "edit": { - "title": "Edit snippets", - "description": "Rename or change existing snippets and folders." - }, - "delete": { - "title": "Delete snippets", - "description": "Delete snippets and folders." - }, - "share": { - "title": "Share snippets", - "description": "Share snippets and folders with other users or roles." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Collapse folders by default", - "description": "Start each snippet folder collapsed when you open the Snippets panel." - }, - "showCommands": { - "label": "Show commands", - "description": "Show the first line of each snippet's command under its name." - }, - "confirmExecution": { - "label": "Confirm before running", - "description": "Ask before a command snippet runs in a terminal." - } - }, - "cancel": "Cancel", - "loading": "Loading...", - "searchPlaceholder": "Search snippets", - "newFolder": "New folder", - "newSnippet": "New snippet", - "noFolder": "No folder", - "noSnippetsInFolder": "No snippets in this folder", - "noTerminalTabsOpen": "No terminal tab is open", - "run": "Run", - "pasteToTerminal": "Paste to terminal", - "copySuccess": "Copied to clipboard", - "importExport": "Import / Export", - "importFailed": "Failed to import snippets", - "nameLabel": "Name", - "namePlaceholder": "Snippet name", - "descriptionLabel": "Description", - "descriptionPlaceholder": "Optional description", - "folderLabel": "Folder", - "typeLabel": "Type", - "typeCommand": "Command", - "typeNote": "Note", - "commandLabel": "Command", - "commandPlaceholder": "Command to run, e.g. systemctl status $HOST", - "noteLabel": "Note", - "notePlaceholder": "Text to paste into the terminal", - "createSnippetTitle": "Create snippet", - "createSnippetButton": "Create snippet", - "createSuccess": "Snippet created", - "createFailed": "Failed to create snippet", - "editSnippetTitle": "Edit snippet", - "saveSnippetButton": "Save snippet", - "updateSuccess": "Snippet updated", - "updateFailed": "Failed to update snippet", - "deleteFailed": "Failed to delete snippet", - "createFolderTitle": "Create folder", - "createFolderButton": "Create folder", - "folderCreateSuccess": "Folder created", - "folderCreateFailed": "Failed to create folder", - "editFolderTitle": "Edit folder", - "saveFolderButton": "Save folder", - "folderEditSuccess": "Folder updated", - "folderEditFailed": "Failed to update folder", - "folderNameLabel": "Folder name", - "folderNamePlaceholder": "Folder name", - "folderIconLabel": "Icon", - "folderDeleteSuccess": "Folder deleted", - "folderDeleteFailed": "Failed to delete folder", - "confirmRunMessage": "Run \"{{name}}\"?", - "confirmRunButton": "Run", - "runSuccess": "Ran \"{{name}}\" on {{count}} terminal(s)", - "pasteSuccess": "Pasted \"{{name}}\" into {{count}} terminal(s)", - "admin": { - "tabTitle": "Snippets", - "count": "{{count}} snippets", - "add": "Add Snippet", - "namePlaceholder": "Snippet name", - "contentPlaceholder": "Command content", - "folderPlaceholder": "Folder (optional)", - "empty": "This user has no snippets", - "deleteConfirm": "Delete snippet \"{{name}}\" belonging to {{username}}?", - "deleted": "Snippet deleted", - "deleteFailed": "Failed to delete snippet", - "requiredFields": "Snippet name and content are required", - "saved": "Snippet saved", - "saveFailed": "Failed to save snippet", - "saving": "Saving...", - "save": "Save" - }, - "backToSnippets": "Back to snippets", - "settingsTitle": "Snippet settings", - "settingsDisplayTitle": "Display", - "settingsBehaviorTitle": "Behavior", - "typeCommandHint": "Runs in the active terminal.", - "typeNoteHint": "Pastes into the active terminal without running.", - "variablesHint": "Variables: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Double click to run", - "sharedBadge": "Shared", - "copyToClipboard": "Copy to clipboard", - "copyFailed": "Failed to copy to clipboard", - "moreOptions": "More options", - "moreActions": "More actions", - "moveToFolder": "Move to folder", - "deleteSnippet": "Delete snippet", - "deleteSnippetConfirm": "Delete snippet \"{{name}}\"?", - "deleteSuccess": "Snippet deleted", - "delete": "Delete", - "folderActions": "Folder actions", - "addSnippetHere": "New snippet here", - "deleteFolder": "Delete folder", - "deleteFolderConfirm": "Delete folder \"{{name}}\"? Its snippets move out of the folder.", - "createFolderDescription": "Group related snippets together.", - "editFolderDescription": "Change this folder's name, color or icon.", - "folderColorLabel": "Color", - "loadFailed": "Failed to load snippets", - "importSkipExisting": "Import (skip existing)", - "importOverwrite": "Import (overwrite existing)", - "importInvalidFile": "That file is not a valid snippets export", - "importSuccess": "Imported {{imported}}, updated {{updated}}, skipped {{skipped}}", - "exportSnippets": "Export snippets", - "exportSuccess": "Snippets exported", - "exportFailed": "Failed to export snippets", - "expandAll": "Expand all folders", - "collapseAll": "Collapse all folders", - "docsLink": "Documentation", - "clearSearch": "Clear search", - "noSearchResults": "No snippets match your search", - "emptyTitle": "No snippets yet", - "emptyDescription": "Save commands you run often, then run them in a terminal with one click.", - "shareSnippet": "Share snippet", - "shareFolder": "Share folder", - "shareExpiry": "Expires", - "shareExpiryOptions": { - "h0": "Never", - "h1": "In 1 hour", - "h24": "In 1 day", - "h168": "In 7 days", - "h720": "In 30 days" - }, - "shareButton": "Share", - "shareSuccess": "Shared with {{count}}", - "shareFailed": "Failed to share", - "shareLoadFailed": "Failed to load sharing details", - "currentAccess": "Current access", - "notSharedYet": "Not shared with anyone yet", - "revokeAccess": "Revoke", - "revokeSuccess": "Access revoked", - "revokeFailed": "Failed to revoke access", - "close": "Close", - "targetHostsLabel": "Target hosts", - "targetHostsHint": "Pick hosts to run this command on directly, without a terminal open. Leave empty to run it in the active terminal.", - "clearTargetHosts": "Clear", - "noHostsAvailable": "No hosts available", - "searchHosts": "Search hosts", - "runOnTargets": "Run on target hosts", - "executionResultTitle": "Results for \"{{name}}\"", - "executionResultDescription": "Output from each target host.", - "executionSuccess": "Success", - "executionFailed": "Failed", - "directRunSuccess": "Ran \"{{name}}\" on {{count}} host(s)", - "directRunPartialFail": "\"{{name}}\" failed on some hosts", - "reorderFailed": "Failed to reorder snippets", - "dropToRemoveFromFolder": "Drop here to move out of the folder", - "shareSnippetTitle": "Share \"{{name}}\"", - "shareFolderTitle": "Share folder \"{{name}}\"", - "shareWith": "Share with", - "usersTab": "Users", - "rolesTab": "Roles", - "shareSearchPlaceholder": "Search users or roles", - "shareNoMatches": "No matches", - "shareWithCount": "Share with {{count}}", - "shareFolderHint": "Shares every snippet in this folder. Snippets added later are not shared automatically.", - "shareFolderSummary": "Shared {{count}} snippet(s) in this folder", - "grantedBy": "Granted by", - "expired": "Expired", - "variablesDialogTitle": "Run \"{{name}}\"", - "variablesDialogDescription": "Fill in the values this snippet needs before running it.", - "variablesPreviewLabel": "Preview", - "variablesConfirmButton": "Run", - "keybindings": { - "runSnippet": "Run existing snippet", - "snippetLabel": "Snippet", - "selectSnippet": "Select a snippet", - "snippetMissing": "snippet not found", - "snippetRequired": "Select a snippet" - }, - "paletteHintRun": "Run in the active terminal tab", - "host": { - "startupTitle": "Startup Snippet", - "quickActionsTitle": "Quick Actions", - "quickActionsDescription": "Quick actions appear as buttons in the Host Metrics toolbar for one-click command execution.", - "noQuickActions": "No quick actions yet.", - "buttonLabel": "Button label", - "selectSnippet": "Select snippet...", - "addQuickAction": "Add Action", - "tabTitle": "Snippets", - "startupDescription": "Runs in the terminal each time it connects to this host. Variables like $HOST are filled in; a snippet that asks for inputs is skipped.", - "none": "None", - "removeQuickAction": "Remove quick action" - }, - "quickActions": { - "running": "Executing {{name}}...", - "succeeded": "{{name}} completed successfully", - "failed": "{{name}} failed", - "error": "Failed to execute {{name}}" - }, - "paletteHintPaste": "Paste in the active terminal tab", - "targetTerminals": "Run in", - "targetTerminalsHint": "optional, else the active tab", - "selectAll": "All", - "selectNone": "None", - "plugin": { - "name": "Snippets" - } -} diff --git a/plugins/snippets/locales/translated/af_ZA.json b/plugins/snippets/locales/translated/af_ZA.json deleted file mode 100644 index a039b57a0..000000000 --- a/plugins/snippets/locales/translated/af_ZA.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Brokkies" - }, - "onboarding": { - "feature_snippets": "Brokkies", - "feature_snippets_desc": "Stoor opdragte wat jy gereeld uitvoer en stuur dit met een klik na 'n terminaal." - }, - "permissions": { - "view": { - "title": "Bekyk brokkies", - "description": "Sien jou eie brokkies, notas en gedeelde brokkies." - }, - "create": { - "title": "Skep brokkies", - "description": "Skep nuwe brokkies, notas en gidse." - }, - "edit": { - "title": "Wysig brokkies", - "description": "Hernoem of verander bestaande brokkies en gidse." - }, - "delete": { - "title": "Vee brokkies uit", - "description": "Vee brokkies en gidse uit." - }, - "share": { - "title": "Deel brokkies", - "description": "Deel brokkies en gidse met ander gebruikers of rolle." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Vou vouers standaard in", - "description": "Begin elke brokkie-vouer ingevou wanneer jy die Brokkie-paneel oopmaak." - }, - "showCommands": { - "label": "Wys bevele", - "description": "Wys die eerste reël van elke brokkie se opdrag onder sy naam." - }, - "confirmExecution": { - "label": "Bevestig voor uitvoering", - "description": "Vra voordat 'n opdragbrokkie in 'n terminaal loop." - } - }, - "cancel": "Kanselleer", - "loading": "Laai tans...", - "searchPlaceholder": "Soekbrokkies", - "newFolder": "Nuwe vouer", - "newSnippet": "Nuwe brokkie", - "noFolder": "Geen vouer nie", - "noSnippetsInFolder": "Geen brokkies in hierdie vouer nie", - "noTerminalTabsOpen": "Geen terminaal-oortjie is oop nie", - "run": "Hardloop", - "pasteToTerminal": "Plak na terminaal", - "copySuccess": "Na knipbord gekopieer", - "importExport": "Invoer / Uitvoer", - "importFailed": "Kon nie brokkies invoer nie", - "nameLabel": "Naam", - "namePlaceholder": "Brokkie naam", - "descriptionLabel": "Beskrywing", - "descriptionPlaceholder": "Opsionele beskrywing", - "folderLabel": "Vouer", - "typeLabel": "Tipe", - "typeCommand": "Bevel", - "typeNote": "Nota", - "commandLabel": "Bevel", - "commandPlaceholder": "Opdrag om uit te voer, bv. systemctl status $HOST", - "noteLabel": "Nota", - "notePlaceholder": "Teks om in die terminaal te plak", - "createSnippetTitle": "Skep brokkie", - "createSnippetButton": "Skep brokkie", - "createSuccess": "Brokkie geskep", - "createFailed": "Kon nie brokkie skep nie", - "editSnippetTitle": "Wysig brokkie", - "saveSnippetButton": "Stoor brokkie", - "updateSuccess": "Brokkie opgedateer", - "updateFailed": "Kon nie die brokkie opdateer nie", - "deleteFailed": "Kon nie fragment uitvee nie", - "createFolderTitle": "Skep vouer", - "createFolderButton": "Skep vouer", - "folderCreateSuccess": "Vouer geskep", - "folderCreateFailed": "Kon nie vouer skep nie", - "editFolderTitle": "Wysig vouer", - "saveFolderButton": "Stoor vouer", - "folderEditSuccess": "Vouer opgedateer", - "folderEditFailed": "Kon nie vouer opdateer nie", - "folderNameLabel": "Lêernaam", - "folderNamePlaceholder": "Lêernaam", - "folderIconLabel": "Ikoon", - "folderDeleteSuccess": "Vouer uitgevee", - "folderDeleteFailed": "Kon nie vouer uitvee nie", - "confirmRunMessage": "Begin \"{{name}}\"?", - "confirmRunButton": "Hardloop", - "runSuccess": "Het \"{{name}}\" op {{count}} terminaal(e) uitgevoer", - "pasteSuccess": "Het \"{{name}}\" in {{count}} terminaal(e) geplak", - "admin": { - "tabTitle": "Brokkies", - "count": "{{count}} brokkies", - "add": "Voeg brokkie by", - "namePlaceholder": "Brokkie naam", - "contentPlaceholder": "Opdraginhoud", - "folderPlaceholder": "Vouer (opsioneel)", - "empty": "Hierdie gebruiker het geen brokkies nie", - "deleteConfirm": "Vee die brokkie \"{{name}}\" uit wat aan {{username}} behoort?", - "deleted": "Brokkie uitgevee", - "deleteFailed": "Kon nie fragment uitvee nie", - "requiredFields": "Brokkienaam en inhoud word vereis", - "saved": "Brokkie gestoor", - "saveFailed": "Kon nie die brokkie stoor nie", - "saving": "Stoor...", - "save": "Stoor" - }, - "backToSnippets": "Terug na brokkies", - "settingsTitle": "Brokkie-instellings", - "settingsDisplayTitle": "Vertoon", - "settingsBehaviorTitle": "Gedrag", - "typeCommandHint": "Loop in die aktiewe terminaal.", - "typeNoteHint": "Plak in die aktiewe terminaal sonder om te loop.", - "variablesHint": "Veranderlikes: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Etiket}", - "doubleClickToRun": "Dubbelklik om te hardloop", - "sharedBadge": "Gedeel", - "copyToClipboard": "Kopieer na knipbord", - "copyFailed": "Kon nie na knipbord kopieer nie", - "moreOptions": "Meer opsies", - "moreActions": "Meer aksies", - "moveToFolder": "Skuif na vouer", - "deleteSnippet": "Vee uittreksel uit", - "deleteSnippetConfirm": "Vee die brokkie \"{{name}} \" uit?", - "deleteSuccess": "Brokkie uitgevee", - "delete": "Vee uit", - "folderActions": "Voueraksies", - "addSnippetHere": "Nuwe stukkie hier", - "deleteFolder": "Vee vouer uit", - "deleteFolderConfirm": "Vee vouer \"{{name}}\" uit? Die brokkies skuif uit die vouer.", - "createFolderDescription": "Groepeer verwante brokkies saam.", - "editFolderDescription": "Verander hierdie vouer se naam, kleur of ikoon.", - "folderColorLabel": "Kleur", - "loadFailed": "Kon nie brokkies laai nie", - "importSkipExisting": "Invoer (slaan bestaande oor)", - "importOverwrite": "Invoer (oorskryf bestaande)", - "importInvalidFile": "Daardie lêer is nie 'n geldige uitvoer van brokkies nie", - "importSuccess": "Ingevoer {{imported}}, opgedateer {{updated}}, oorgeslaan {{skipped}}", - "exportSnippets": "Uitvoer brokkies", - "exportSuccess": "Uitgesnyde stukkies uitgevoer", - "exportFailed": "Kon nie brokkies uitvoer nie", - "expandAll": "Vou alle vouers uit", - "collapseAll": "Vou alle vouers in", - "docsLink": "Dokumentasie", - "clearSearch": "Vee soektog uit", - "noSearchResults": "Geen brokkies stem ooreen met jou soektog nie", - "emptyTitle": "Geen brokkies nog nie", - "emptyDescription": "Stoor opdragte wat jy gereeld uitvoer, en voer hulle dan met een klik in 'n terminaal uit.", - "shareSnippet": "Deel brokkie", - "shareFolder": "Deel vouer", - "shareExpiry": "Vervaldatum", - "shareExpiryOptions": { - "h0": "Nooit", - "h1": "Oor 1 uur", - "h24": "Oor 1 dag", - "h168": "Oor 7 dae", - "h720": "Oor 30 dae" - }, - "shareButton": "Deel", - "shareSuccess": "Gedeel met {{count}}", - "shareFailed": "Kon nie deel nie", - "shareLoadFailed": "Kon nie deelbesonderhede laai nie", - "currentAccess": "Huidige toegang", - "notSharedYet": "Nog nie met enigiemand gedeel nie", - "revokeAccess": "Herroep", - "revokeSuccess": "Toegang herroep", - "revokeFailed": "Kon nie toegang herroep nie", - "close": "Maak toe", - "targetHostsLabel": "Teikengashere", - "targetHostsHint": "Kies gashere om hierdie opdrag direk op uit te voer, sonder 'n terminaal oop. Los leeg om dit in die aktiewe terminaal uit te voer.", - "clearTargetHosts": "Duidelik", - "noHostsAvailable": "Geen gashere beskikbaar nie", - "searchHosts": "Soek gashere", - "runOnTargets": "Begin op teikengashere", - "executionResultTitle": "Resultate vir \"{{name}}\"", - "executionResultDescription": "Uitvoer van elke teikengasheer.", - "executionSuccess": "Sukses", - "executionFailed": "Misluk", - "directRunSuccess": "Het \"{{name}}\" op {{count}} gasheer(s) uitgevoer", - "directRunPartialFail": "\"{{name}}\" het op sommige gashere misluk", - "reorderFailed": "Kon nie brokkies herrangskik nie", - "dropToRemoveFromFolder": "Laat val hier om uit die vouer te skuif", - "shareSnippetTitle": "Deel \"{{name}}\"", - "shareFolderTitle": "Deel vouer \"{{name}}\"", - "shareWith": "Deel met", - "usersTab": "Gebruikers", - "rolesTab": "Rolle", - "shareSearchPlaceholder": "Soek gebruikers of rolle", - "shareNoMatches": "Geen ooreenkomste nie", - "shareWithCount": "Deel met {{count}}", - "shareFolderHint": "Deel elke brokkie in hierdie vouer. Brokkies wat later bygevoeg word, word nie outomaties gedeel nie.", - "shareFolderSummary": "Gedeelde {{count}} brokkie(s) in hierdie vouer", - "grantedBy": "Toegeken deur", - "expired": "Verstryk", - "variablesDialogTitle": "Begin \"{{name}}\"", - "variablesDialogDescription": "Vul die waardes in wat hierdie brokkie benodig voordat dit uitgevoer word.", - "variablesPreviewLabel": "Voorskou", - "variablesConfirmButton": "Hardloop", - "keybindings": { - "runSnippet": "Voer bestaande brokkie uit", - "snippetLabel": "Brokkie", - "selectSnippet": "Kies 'n uittreksel", - "snippetMissing": "brokkie nie gevind nie", - "snippetRequired": "Kies 'n uittreksel" - }, - "paletteHintRun": "Begin in die aktiewe terminaal-oortjie", - "host": { - "startupTitle": "Opstart-brokkie", - "quickActionsTitle": "Vinnige Aksies", - "quickActionsDescription": "Vinnige aksies verskyn as knoppies in die Host Metrics-nutsbalk vir die uitvoering van opdragte met een klik.", - "noQuickActions": "Geen vinnige aksies nog nie.", - "buttonLabel": "Knoppie-etiket", - "selectSnippet": "Kies uittreksel...", - "addQuickAction": "Voeg Aksie by", - "tabTitle": "Brokkies", - "startupDescription": "Loop in die terminaal elke keer as dit aan hierdie gasheer koppel. Veranderlikes soos $HOST word ingevul; 'n brokkie wat om insette vra, word oorgeslaan.", - "none": "Geen", - "removeQuickAction": "Verwyder vinnige aksie" - }, - "quickActions": { - "running": "Voer tans {{name}} uit ...", - "succeeded": "{{name}} suksesvol voltooi", - "failed": "{{name}} het misluk", - "error": "Kon nie {{name}} uitvoer nie" - }, - "paletteHintPaste": "Plak in die aktiewe terminaal-oortjie", - "targetTerminals": "Hardloop in", - "targetTerminalsHint": "opsioneel, anders die aktiewe oortjie", - "selectAll": "Alles", - "selectNone": "Geen", - "plugin": { - "name": "Brokkies" - } -} diff --git a/plugins/snippets/locales/translated/ar_SA.json b/plugins/snippets/locales/translated/ar_SA.json deleted file mode 100644 index 64010b74c..000000000 --- a/plugins/snippets/locales/translated/ar_SA.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "المقتطفات" - }, - "onboarding": { - "feature_snippets": "المقتطفات", - "feature_snippets_desc": "احفظ الأوامر التي تقوم بتشغيلها بشكل متكرر وأرسلها إلى الطرفية بنقرة واحدة." - }, - "permissions": { - "view": { - "title": "عرض مقتطفات", - "description": "اطلع على مقتطفاتك وملاحظاتك ومقتطفاتك المشتركة." - }, - "create": { - "title": "إنشاء مقتطفات", - "description": "أنشئ مقتطفات وملاحظات ومجلدات جديدة." - }, - "edit": { - "title": "تعديل المقتطفات", - "description": "أعد تسمية أو تغيير المقتطفات والمجلدات الموجودة." - }, - "delete": { - "title": "حذف المقتطفات", - "description": "احذف المقاطع والمجلدات." - }, - "share": { - "title": "مشاركة مقتطفات", - "description": "مشاركة المقتطفات والمجلدات مع مستخدمين أو أدوار أخرى." - } - }, - "settings": { - "foldersCollapsed": { - "label": "طي المجلدات افتراضيًا", - "description": "ابدأ تشغيل كل مجلد من مجلدات المقتطفات وهو مطوي عند فتح لوحة المقتطفات." - }, - "showCommands": { - "label": "عرض الأوامر", - "description": "اعرض السطر الأول من أمر كل جزء من التعليمات البرمجية أسفل اسمه." - }, - "confirmExecution": { - "label": "تأكد قبل التشغيل", - "description": "اسأل قبل تشغيل أي جزء من الأوامر في الطرفية." - } - }, - "cancel": "إلغاء", - "loading": "جارٍ التحميل...", - "searchPlaceholder": "ابحث في المقتطفات", - "newFolder": "مجلد جديد", - "newSnippet": "مقتطف جديد", - "noFolder": "بدون مجلد", - "noSnippetsInFolder": "لا توجد مقتطفات في هذا المجلد", - "noTerminalTabsOpen": "لا توجد علامة تبويب طرفية مفتوحة", - "run": "تشغيل", - "pasteToTerminal": "الصقه في الطرفية", - "copySuccess": "تم النسخ إلى الحافظة", - "importExport": "استيراد / تصدير", - "importFailed": "فشل استيراد الأجزاء البرمجية", - "nameLabel": "الاسم", - "namePlaceholder": "اسم المقتطف", - "descriptionLabel": "الوصف", - "descriptionPlaceholder": "وصف اختياري", - "folderLabel": "مجلد", - "typeLabel": "النوع", - "typeCommand": "الأمر", - "typeNote": "ملحوظة", - "commandLabel": "الأمر", - "commandPlaceholder": "الأمر المراد تنفيذه، على سبيل المثال: systemctl status $HOST", - "noteLabel": "ملحوظة", - "notePlaceholder": "نص للصقه في الطرفية", - "createSnippetTitle": "إنشاء مقتطف", - "createSnippetButton": "إنشاء مقتطف", - "createSuccess": "تم إنشاء المقتطف", - "createFailed": "فشل إنشاء المقتطف", - "editSnippetTitle": "تعديل المقتطف", - "saveSnippetButton": "حفظ المقتطف", - "updateSuccess": "تم تحديث المقتطف", - "updateFailed": "فشل تحديث المقتطف", - "deleteFailed": "فشل حذف المقتطف", - "createFolderTitle": "إنشاء مجلد", - "createFolderButton": "إنشاء مجلد", - "folderCreateSuccess": "تم إنشاء المجلد", - "folderCreateFailed": "فشل إنشاء المجلد", - "editFolderTitle": "تحرير المجلد", - "saveFolderButton": "حفظ المجلد", - "folderEditSuccess": "تم تحديث المجلد", - "folderEditFailed": "فشل تحديث المجلد", - "folderNameLabel": "اسم المجلد", - "folderNamePlaceholder": "اسم المجلد", - "folderIconLabel": "أيقونة", - "folderDeleteSuccess": "تم حذف المجلد", - "folderDeleteFailed": "فشل حذف المجلد", - "confirmRunMessage": "تشغيل \"{{name}}\"؟", - "confirmRunButton": "تشغيل", - "runSuccess": "تم تشغيل الأمر \"{{name}}\" على الجهاز الطرفي {{count}}", - "pasteSuccess": "تم لصق \"{{name}}\" في {{count}} طرفية (أطراف)", - "admin": { - "tabTitle": "المقتطفات", - "count": "{{count}} مقتطفات", - "add": "إضافة مقتطف", - "namePlaceholder": "اسم المقتطف", - "contentPlaceholder": "محتوى الأمر", - "folderPlaceholder": "المجلد (اختياري)", - "empty": "هذا المستخدم ليس لديه مقتطفات", - "deleteConfirm": "حذف المقتطف \"{{name}}\" الخاص بـ {{username}}؟", - "deleted": "تم حذف المقتطف", - "deleteFailed": "فشل حذف المقتطف", - "requiredFields": "اسم المقتطف والمحتوى مطلوبان", - "saved": "تم حفظ المقتطف", - "saveFailed": "فشل حفظ المقتطف", - "saving": "جارٍ الحفظ...", - "save": "حفظ" - }, - "backToSnippets": "العودة إلى المقتطفات", - "settingsTitle": "إعدادات المقتطف", - "settingsDisplayTitle": "عرض", - "settingsBehaviorTitle": "السلوك", - "typeCommandHint": "يعمل في الطرفية النشطة.", - "typeNoteHint": "يلصق في الطرفية النشطة دون تشغيلها.", - "variablesHint": "المتغيرات: $HOST، $USER، $PORT، $NAME، ${INPUT_1:Label}", - "doubleClickToRun": "انقر نقراً مزدوجاً للتشغيل", - "sharedBadge": "مشترك", - "copyToClipboard": "انسخ إلى الحافظة", - "copyFailed": "فشل النسخ إلى الحافظة", - "moreOptions": "خيارات إضافية", - "moreActions": "إجراءات أخرى", - "moveToFolder": "نقل إلى المجلد", - "deleteSnippet": "حذف المقتطف", - "deleteSnippetConfirm": "حذف المقتطف \"{{name}}\"؟", - "deleteSuccess": "تم حذف المقتطف", - "delete": "حذف", - "folderActions": "إجراءات المجلد", - "addSnippetHere": "مقتطف جديد هنا", - "deleteFolder": "حذف المجلد", - "deleteFolderConfirm": "حذف المجلد \"{{name}}\"؟ ستنتقل مقتطفاته خارج المجلد.", - "createFolderDescription": "قم بتجميع المقتطفات ذات الصلة معًا.", - "editFolderDescription": "قم بتغيير اسم هذا المجلد أو لونه أو أيقونته.", - "folderColorLabel": "اللون", - "loadFailed": "فشل تحميل المقتطفات", - "importSkipExisting": "استيراد (تخطّي الموجود)", - "importOverwrite": "استيراد (استبدال الموجود)", - "importInvalidFile": "هذا الملف ليس ملف تصدير مقتطفات صالحًا", - "importSuccess": "تم الاستيراد {{imported}}، تم التحديث {{updated}}، تم التخطي {{skipped}}", - "exportSnippets": "مقتطفات التصدير", - "exportSuccess": "تم تصدير مقتطفات", - "exportFailed": "فشل تصدير الأجزاء البرمجية", - "expandAll": "توسيع جميع المجلدات", - "collapseAll": "طي جميع المجلدات", - "docsLink": "الوثائق", - "clearSearch": "مسح البحث", - "noSearchResults": "لم يتم العثور على أي نتائج مطابقة لبحثك", - "emptyTitle": "لا توجد مقتطفات حتى الآن", - "emptyDescription": "احفظ الأوامر التي تقوم بتشغيلها بشكل متكرر، ثم قم بتشغيلها في الطرفية بنقرة واحدة.", - "shareSnippet": "مشاركة المقتطف", - "shareFolder": "مجلد مشترك", - "shareExpiry": "ينتهي", - "shareExpiryOptions": { - "h0": "أبدًا", - "h1": "في غضون ساعة واحدة", - "h24": "في يوم واحد", - "h168": "في غضون 7 أيام", - "h720": "خلال 30 يومًا" - }, - "shareButton": "مشاركة", - "shareSuccess": "تمت المشاركة مع {{count}}", - "shareFailed": "فشل في المشاركة", - "shareLoadFailed": "فشل تحميل تفاصيل المشاركة", - "currentAccess": "الوصول الحالي", - "notSharedYet": "لم تتم مشاركتها مع أي شخص حتى الآن", - "revokeAccess": "إبطال", - "revokeSuccess": "تم إبطال الوصول", - "revokeFailed": "فشل إبطال الوصول", - "close": "إغلاق", - "targetHostsLabel": "المضيفون المستهدفون", - "targetHostsHint": "اختر الأجهزة التي تريد تشغيل هذا الأمر عليها مباشرةً، دون فتح نافذة طرفية. اترك الحقل فارغًا لتشغيله في نافذة الطرفية النشطة.", - "clearTargetHosts": "مسح", - "noHostsAvailable": "لا يوجد مضيفون متاحون", - "searchHosts": "ابحث عن مضيفين", - "runOnTargets": "تشغيل على المضيفين المستهدفين", - "executionResultTitle": "نتائج البحث عن \"{{name}}\"", - "executionResultDescription": "مخرجات كل مضيف مستهدف.", - "executionSuccess": "نجاح", - "executionFailed": "فشل", - "directRunSuccess": "تم تشغيل \"{{name}}\" على {{count}} مضيف", - "directRunPartialFail": "فشلت عبارة \"{{name}}\" على بعض المضيفين", - "reorderFailed": "فشل في إعادة ترتيب المقاطع", - "dropToRemoveFromFolder": "أسقط هنا لنقل الملف خارج المجلد", - "shareSnippetTitle": "شارك \"{{name}}\"", - "shareFolderTitle": "مجلد المشاركة \"{{name}}\"", - "shareWith": "مشاركة مع", - "usersTab": "المستخدمون", - "rolesTab": "الأدوار", - "shareSearchPlaceholder": "البحث عن مستخدمين أو أدوار", - "shareNoMatches": "لا توجد نتائج", - "shareWithCount": "شارك مع {{count}}", - "shareFolderHint": "يشارك هذا المجلد جميع المقاطع البرمجية الموجودة فيه. أما المقاطع البرمجية التي تُضاف لاحقاً فلا تتم مشاركتها تلقائياً.", - "shareFolderSummary": "مقتطفات مشتركة {{count}} في هذا المجلد", - "grantedBy": "ممنوح بواسطة", - "expired": "منتهية", - "variablesDialogTitle": "تشغيل \"{{name}}\"", - "variablesDialogDescription": "أدخل القيم التي يحتاجها هذا المقتطف البرمجي قبل تشغيله.", - "variablesPreviewLabel": "معاينة", - "variablesConfirmButton": "تشغيل", - "keybindings": { - "runSnippet": "تشغيل المقتطف البرمجي الحالي", - "snippetLabel": "مقتطف", - "selectSnippet": "حدد جزءًا من التعليمات البرمجية", - "snippetMissing": "لم يتم العثور على المقتطف", - "snippetRequired": "حدد جزءًا من التعليمات البرمجية" - }, - "paletteHintRun": "قم بتشغيله في علامة تبويب الطرفية النشطة", - "host": { - "startupTitle": "مقتطف بدء التشغيل", - "quickActionsTitle": "إجراءات سريعة", - "quickActionsDescription": "تظهر الإجراءات السريعة كأزرار في شريط أدوات مقاييس المضيف لتنفيذ الأوامر بنقرة واحدة.", - "noQuickActions": "لا توجد إجراءات سريعة بعد.", - "buttonLabel": "تسمية الزر", - "selectSnippet": "اختر مقتطفًا...", - "addQuickAction": "إضافة إجراء", - "tabTitle": "المقتطفات", - "startupDescription": "يتم تشغيله في الطرفية في كل مرة يتصل فيها بهذا المضيف. يتم ملء متغيرات مثل $HOST ؛ ويتم تخطي جزء يطلب إدخالات.", - "none": "لا شيء", - "removeQuickAction": "إزالة الإجراء السريع" - }, - "quickActions": { - "running": "جارٍ تنفيذ {{name}}...", - "succeeded": "تم إكمال {{name}} بنجاح", - "failed": "فشل {{name}}", - "error": "فشل تنفيذ {{name}}" - }, - "paletteHintPaste": "الصقي في علامة تبويب الطرفية النشطة", - "targetTerminals": "اركض", - "targetTerminalsHint": "اختياري، وإلا ستكون علامة التبويب النشطة", - "selectAll": "الكل", - "selectNone": "لا شيء", - "plugin": { - "name": "المقتطفات" - } -} diff --git a/plugins/snippets/locales/translated/bg_BG.json b/plugins/snippets/locales/translated/bg_BG.json deleted file mode 100644 index 9eb3ed7aa..000000000 --- a/plugins/snippets/locales/translated/bg_BG.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Откъси" - }, - "onboarding": { - "feature_snippets": "Откъси", - "feature_snippets_desc": "Запазвайте команди, които изпълнявате често, и ги изпращайте към терминал с едно щракване." - }, - "permissions": { - "view": { - "title": "Преглед на фрагменти", - "description": "Вижте вашите собствени фрагменти, бележки и споделени фрагменти." - }, - "create": { - "title": "Създаване на фрагменти", - "description": "Създавайте нови фрагменти, бележки и папки." - }, - "edit": { - "title": "Редактиране на фрагменти", - "description": "Преименувайте или променете съществуващи фрагменти и папки." - }, - "delete": { - "title": "Изтриване на фрагменти", - "description": "Изтрийте фрагменти и папки." - }, - "share": { - "title": "Споделяне на фрагменти", - "description": "Споделяйте фрагменти и папки с други потребители или роли." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Свиване на папките по подразбиране", - "description": "Започнете всяка папка с фрагменти свита, когато отворите панела „Фрагменти“." - }, - "showCommands": { - "label": "Показване на команди", - "description": "Покажете първия ред от командата на всеки фрагмент под неговото име." - }, - "confirmExecution": { - "label": "Потвърдете преди стартиране", - "description": "Попитай преди изпълнението на фрагмент от команда в терминал." - } - }, - "cancel": "Отказ", - "loading": "Зареждане...", - "searchPlaceholder": "Търсене на фрагменти", - "newFolder": "Нова папка", - "newSnippet": "Нов фрагмент", - "noFolder": "Няма папка", - "noSnippetsInFolder": "Няма фрагменти в тази папка", - "noTerminalTabsOpen": "Няма отворен раздел на терминала", - "run": "Бягане", - "pasteToTerminal": "Поставяне в терминала", - "copySuccess": "Копирано в буферната памет", - "importExport": "Внос / Износ", - "importFailed": "Импортирането на фрагменти не бе успешно", - "nameLabel": "Име", - "namePlaceholder": "Име на фрагмента", - "descriptionLabel": "Описание", - "descriptionPlaceholder": "Незадължително описание", - "folderLabel": "Папка", - "typeLabel": "Тип", - "typeCommand": "Команда", - "typeNote": "Забележка", - "commandLabel": "Команда", - "commandPlaceholder": "Команда за изпълнение, например systemctl status $HOST", - "noteLabel": "Забележка", - "notePlaceholder": "Текст за поставяне в терминала", - "createSnippetTitle": "Създаване на фрагмент", - "createSnippetButton": "Създаване на фрагмент", - "createSuccess": "Фрагментът е създаден", - "createFailed": "Създаването на фрагмент не бе успешно", - "editSnippetTitle": "Редактиране на фрагмент", - "saveSnippetButton": "Запазване на фрагмент", - "updateSuccess": "Фрагментът е актуализиран", - "updateFailed": "Актуализирането на фрагмента не бе успешно", - "deleteFailed": "Изтриването на фрагмента не бе успешно", - "createFolderTitle": "Създаване на папка", - "createFolderButton": "Създаване на папка", - "folderCreateSuccess": "Папката е създадена", - "folderCreateFailed": "Създаването на папка не бе успешно", - "editFolderTitle": "Редактиране на папка", - "saveFolderButton": "Запазване на папката", - "folderEditSuccess": "Папката е актуализирана", - "folderEditFailed": "Актуализирането на папката не бе успешно", - "folderNameLabel": "Име на папката", - "folderNamePlaceholder": "Име на папката", - "folderIconLabel": "Икона", - "folderDeleteSuccess": "Папката е изтрита", - "folderDeleteFailed": "Изтриването на папката не бе успешно", - "confirmRunMessage": "Изпълни „{{name}}“?", - "confirmRunButton": "Бягане", - "runSuccess": "Изпълни „{{name}}“ на {{count}} терминал(и)", - "pasteSuccess": "Поставено е „{{name}}“ в {{count}} терминал(и)", - "admin": { - "tabTitle": "Откъси", - "count": "{{count}} фрагменти", - "add": "Добавяне на фрагмент", - "namePlaceholder": "Име на фрагмента", - "contentPlaceholder": "Съдържание на командата", - "folderPlaceholder": "Папка (по избор)", - "empty": "Този потребител няма фрагменти", - "deleteConfirm": "Да се изтрие ли фрагментът „{{name}}“, принадлежащ на {{username}}?", - "deleted": "Фрагментът е изтрит", - "deleteFailed": "Изтриването на фрагмента не бе успешно", - "requiredFields": "Името и съдържанието на фрагмента са задължителни", - "saved": "Фрагментът е запазен", - "saveFailed": "Запазването на фрагмента не бе успешно", - "saving": "Запазване...", - "save": "Запазване" - }, - "backToSnippets": "Обратно към откъси", - "settingsTitle": "Настройки на фрагмента", - "settingsDisplayTitle": "Дисплей", - "settingsBehaviorTitle": "Поведение", - "typeCommandHint": "Изпълнява се в активния терминал.", - "typeNoteHint": "Поставя в активния терминал без да се изпълнява.", - "variablesHint": "Променливи: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Етикет}", - "doubleClickToRun": "Щракнете двукратно, за да стартирате", - "sharedBadge": "Споделено", - "copyToClipboard": "Копиране в клипборда", - "copyFailed": "Копирането в буферната памет не бе успешно", - "moreOptions": "Още опции", - "moreActions": "Още действия", - "moveToFolder": "Преместване в папка", - "deleteSnippet": "Изтриване на фрагмент", - "deleteSnippetConfirm": "Да се изтрие ли фрагментът „{{name}}“?", - "deleteSuccess": "Фрагментът е изтрит", - "delete": "Изтриване", - "folderActions": "Действия с папки", - "addSnippetHere": "Нов фрагмент тук", - "deleteFolder": "Изтриване на папка", - "deleteFolderConfirm": "Да се изтрие ли папка „{{name}}“? Нейните фрагменти ще бъдат преместени извън папката.", - "createFolderDescription": "Групирайте свързани фрагменти заедно.", - "editFolderDescription": "Променете името, цвета или иконата на тази папка.", - "folderColorLabel": "Цвят", - "loadFailed": "Зареждането на фрагменти не бе успешно", - "importSkipExisting": "Импортиране (пропускане на съществуващото)", - "importOverwrite": "Импортиране (презаписване на съществуващо)", - "importInvalidFile": "Този файл не е валиден експорт на фрагменти", - "importSuccess": "Импортирано {{imported}}, актуализирано {{updated}}, пропуснато {{skipped}}", - "exportSnippets": "Експортиране на фрагменти", - "exportSuccess": "Експортирани фрагменти", - "exportFailed": "Експортирането на фрагменти не бе успешно", - "expandAll": "Разгъване на всички папки", - "collapseAll": "Свиване на всички папки", - "docsLink": "Документация", - "clearSearch": "Изчистване на търсенето", - "noSearchResults": "Няма фрагменти, съответстващи на търсенето ви", - "emptyTitle": "Все още няма фрагменти", - "emptyDescription": "Запазете командите, които изпълнявате често, след което ги стартирайте в терминал с едно щракване.", - "shareSnippet": "Споделяне на фрагмент", - "shareFolder": "Споделяне на папка", - "shareExpiry": "Изтича", - "shareExpiryOptions": { - "h0": "Никога", - "h1": "След 1 час", - "h24": "След 1 ден", - "h168": "След 7 дни", - "h720": "След 30 дни" - }, - "shareButton": "Споделяне", - "shareSuccess": "Споделено с {{count}}", - "shareFailed": "Споделянето не бе успешно", - "shareLoadFailed": "Зареждането на подробностите за споделянето не бе успешно", - "currentAccess": "Текущ достъп", - "notSharedYet": "Все още не е споделено с никого", - "revokeAccess": "Отмяна", - "revokeSuccess": "Достъпът е отменен", - "revokeFailed": "Неуспешно отменяне на достъпа", - "close": "Затвори", - "targetHostsLabel": "Целеви хостове", - "targetHostsHint": "Изберете хостове, на които да се изпълни тази команда директно, без да е отворен терминал. Оставете празно, за да се изпълни в активния терминал.", - "clearTargetHosts": "Изчисти", - "noHostsAvailable": "Няма налични хостове", - "searchHosts": "Търсене на хостове", - "runOnTargets": "Изпълнява се на целеви хостове", - "executionResultTitle": "Резултати за „{{name}}“", - "executionResultDescription": "Изход от всеки целеви хост.", - "executionSuccess": "Успех", - "executionFailed": "Неуспешно", - "directRunSuccess": "Изпълни „{{name}}“ на {{count}} хост(ове)", - "directRunPartialFail": "„{{name}}“ не успя на някои хостове", - "reorderFailed": "Пренареждането на фрагменти не бе успешно", - "dropToRemoveFromFolder": "Пуснете тук, за да излезете от папката", - "shareSnippetTitle": "Сподели „{{name}}“", - "shareFolderTitle": "Споделяне на папка „{{name}}“", - "shareWith": "Сподели с", - "usersTab": "Потребители", - "rolesTab": "Роли", - "shareSearchPlaceholder": "Търсене на потребители или роли", - "shareNoMatches": "Няма съвпадения", - "shareWithCount": "Сподели с {{count}}", - "shareFolderHint": "Споделя всеки фрагмент в тази папка. Фрагментите, добавени по-късно, не се споделят автоматично.", - "shareFolderSummary": "Споделени {{count}} фрагмент(а) в тази папка", - "grantedBy": "Предоставено от", - "expired": "Изтекъл", - "variablesDialogTitle": "Изпълни „{{name}}“", - "variablesDialogDescription": "Попълнете стойностите, необходими на този фрагмент, преди да го изпълните.", - "variablesPreviewLabel": "Преглед", - "variablesConfirmButton": "Бягане", - "keybindings": { - "runSnippet": "Изпълнете съществуващия фрагмент", - "snippetLabel": "Фрагмент", - "selectSnippet": "Изберете фрагмент", - "snippetMissing": "фрагмент не е намерен", - "snippetRequired": "Изберете фрагмент" - }, - "paletteHintRun": "Изпълняване в активния раздел на терминала", - "host": { - "startupTitle": "Стартов фрагмент", - "quickActionsTitle": "Бързи действия", - "quickActionsDescription": "Бързите действия се показват като бутони в лентата с инструменти Host Metrics за изпълнение на команди с едно щракване.", - "noQuickActions": "Все още няма бързи действия.", - "buttonLabel": "Етикет на бутона", - "selectSnippet": "Изберете фрагмент...", - "addQuickAction": "Добавяне на действие", - "tabTitle": "Откъси", - "startupDescription": "Изпълнява се в терминала всеки път, когато се свързва с този хост. Променливи като $HOST се попълват; фрагмент, който изисква входни данни, се пропуска.", - "none": "Няма", - "removeQuickAction": "Премахване на бързо действие" - }, - "quickActions": { - "running": "Изпълнява се {{name}}...", - "succeeded": "{{name}} завършено успешно", - "failed": "{{name}} неуспешно", - "error": "Неуспешно изпълнение на {{name}}" - }, - "paletteHintPaste": "Поставяне в активния раздел на терминала", - "targetTerminals": "Влизане", - "targetTerminalsHint": "по избор, в противен случай активният раздел", - "selectAll": "Всички", - "selectNone": "Няма", - "plugin": { - "name": "Откъси" - } -} diff --git a/plugins/snippets/locales/translated/bn_BD.json b/plugins/snippets/locales/translated/bn_BD.json deleted file mode 100644 index 0847bfe44..000000000 --- a/plugins/snippets/locales/translated/bn_BD.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "খণ্ডাংশ" - }, - "onboarding": { - "feature_snippets": "খণ্ডাংশ", - "feature_snippets_desc": "আপনার প্রায়শই ব্যবহৃত কমান্ডগুলো সংরক্ষণ করুন এবং এক ক্লিকে টার্মিনালে পাঠিয়ে দিন।" - }, - "permissions": { - "view": { - "title": "স্নিপেটগুলি দেখুন", - "description": "আপনার নিজের স্নিপেট, নোট এবং শেয়ার করা স্নিপেটগুলো দেখুন।" - }, - "create": { - "title": "স্নিপেট তৈরি করুন", - "description": "নতুন স্নিপেট, নোট এবং ফোল্ডার তৈরি করুন।" - }, - "edit": { - "title": "সম্পাদনা স্নিপেট", - "description": "বিদ্যমান স্নিপেট এবং ফোল্ডারগুলির নাম পরিবর্তন করুন।" - }, - "delete": { - "title": "স্নিপেটগুলি মুছুন", - "description": "স্নিপেট এবং ফোল্ডারগুলো মুছে ফেলুন।" - }, - "share": { - "title": "ছোট ছোট অংশ শেয়ার করুন", - "description": "অন্যান্য ব্যবহারকারী বা রোলের সাথে স্নিপেট এবং ফোল্ডার শেয়ার করুন।" - } - }, - "settings": { - "foldersCollapsed": { - "label": "ডিফল্টরূপে ফোল্ডারগুলি সংকুচিত করুন", - "description": "স্নিপেট প্যানেল খোলার সময় প্রতিটি স্নিপেট ফোল্ডার সংকুচিত অবস্থায় শুরু হয়।" - }, - "showCommands": { - "label": "কমান্ড দেখান", - "description": "প্রতিটি স্নিপেটের কমান্ডের প্রথম লাইনটি তার নামের নিচে দেখান।" - }, - "confirmExecution": { - "label": "চালানোর আগে নিশ্চিত করুন।", - "description": "টার্মিনালে কোনো কমান্ড স্নিপেট চালানোর আগে জিজ্ঞাসা করুন।" - } - }, - "cancel": "বাতিল করুন", - "loading": "লোড হচ্ছে...", - "searchPlaceholder": "অনুসন্ধান স্নিপেট", - "newFolder": "নতুন ফোল্ডার", - "newSnippet": "নতুন স্নিপেট", - "noFolder": "কোন ফোল্ডার নেই", - "noSnippetsInFolder": "এই ফোল্ডারে কোনো স্নিপেট নেই", - "noTerminalTabsOpen": "কোন টার্মিনাল ট্যাব খোলা নেই", - "run": "দৌড়", - "pasteToTerminal": "টার্মিনালে পেস্ট করুন", - "copySuccess": "ক্লিপবোর্ডে কপি করা হয়েছে", - "importExport": "আমদানি / রপ্তানি", - "importFailed": "স্নিপেট আমদানি করতে ব্যর্থ হয়েছে", - "nameLabel": "নাম", - "namePlaceholder": "স্নিপেট নাম", - "descriptionLabel": "বর্ণনা", - "descriptionPlaceholder": "ঐচ্ছিক বিবরণ", - "folderLabel": "ফোল্ডার", - "typeLabel": "প্রকার", - "typeCommand": "আদেশ", - "typeNote": "দ্রষ্টব্য", - "commandLabel": "আদেশ", - "commandPlaceholder": "চালানোর জন্য কমান্ড, যেমন systemctl status $HOST", - "noteLabel": "দ্রষ্টব্য", - "notePlaceholder": "টার্মিনালে পেস্ট করার জন্য টেক্সট", - "createSnippetTitle": "স্নিপেট তৈরি করুন", - "createSnippetButton": "স্নিপেট তৈরি করুন", - "createSuccess": "স্নিপেট তৈরি করা হয়েছে", - "createFailed": "স্নিপেট তৈরি করতে ব্যর্থ হয়েছে", - "editSnippetTitle": "সম্পাদনা স্নিপেট", - "saveSnippetButton": "স্নিপেট সংরক্ষণ করুন", - "updateSuccess": "স্নিপেট আপডেট করা হয়েছে", - "updateFailed": "স্নিপেট আপডেট করতে ব্যর্থ হয়েছে", - "deleteFailed": "স্নিপেট মুছে ফেলা ব্যর্থ হয়েছে", - "createFolderTitle": "ফোল্ডার তৈরি করুন", - "createFolderButton": "ফোল্ডার তৈরি করুন", - "folderCreateSuccess": "ফোল্ডার তৈরি করা হয়েছে", - "folderCreateFailed": "ফোল্ডার তৈরি করতে ব্যর্থ হয়েছে", - "editFolderTitle": "ফোল্ডার সম্পাদনা করুন", - "saveFolderButton": "ফোল্ডার সংরক্ষণ করুন", - "folderEditSuccess": "ফোল্ডার আপডেট করা হয়েছে", - "folderEditFailed": "ফোল্ডার আপডেট করতে ব্যর্থ হয়েছে", - "folderNameLabel": "ফোল্ডারের নাম", - "folderNamePlaceholder": "ফোল্ডারের নাম", - "folderIconLabel": "আইকন", - "folderDeleteSuccess": "ফোল্ডার মুছে ফেলা হয়েছে", - "folderDeleteFailed": "ফোল্ডারটি মুছে ফেলা সম্ভব হয়নি", - "confirmRunMessage": "\"{{name}} \" চালান?", - "confirmRunButton": "দৌড়", - "runSuccess": "{{count}} টার্মিনালে \"{{name}}\" চালানো হয়েছে", - "pasteSuccess": "{{name}}\" {{count}} টার্মিনালে পেস্ট করা হয়েছে", - "admin": { - "tabTitle": "খণ্ডাংশ", - "count": "{{count}} খণ্ডাংশ", - "add": "স্নিপেট যোগ করুন", - "namePlaceholder": "স্নিপেট নাম", - "contentPlaceholder": "কমান্ডের বিষয়বস্তু", - "folderPlaceholder": "ফোল্ডার (ঐচ্ছিক)", - "empty": "এই ব্যবহারকারীর কোনো স্নিপেট নেই", - "deleteConfirm": "{{username}} এর অন্তর্গত স্নিপেট \"{{name}}\" মুছে ফেলবেন?", - "deleted": "স্নিপেট মুছে ফেলা হয়েছে", - "deleteFailed": "স্নিপেট মুছে ফেলা ব্যর্থ হয়েছে", - "requiredFields": "স্নিপেটের নাম এবং বিষয়বস্তু আবশ্যক।", - "saved": "স্নিপেট সংরক্ষিত", - "saveFailed": "স্নিপেট সংরক্ষণ করতে ব্যর্থ হয়েছে", - "saving": "সঞ্চয়...", - "save": "সংরক্ষণ করুন" - }, - "backToSnippets": "স্নিপেটগুলিতে ফিরে যান", - "settingsTitle": "স্নিপেট সেটিংস", - "settingsDisplayTitle": "প্রদর্শন", - "settingsBehaviorTitle": "আচরণ", - "typeCommandHint": "সক্রিয় টার্মিনালে চলে।", - "typeNoteHint": "রান না করেই সক্রিয় টার্মিনালে পেস্ট করে।", - "variablesHint": "ভেরিয়েবল: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "চালানোর জন্য ডাবল ক্লিক করুন", - "sharedBadge": "ভাগ করা হয়েছে", - "copyToClipboard": "ক্লিপবোর্ডে কপি করুন", - "copyFailed": "ক্লিপবোর্ডে কপি করতে ব্যর্থ হয়েছে", - "moreOptions": "আরও বিকল্প", - "moreActions": "আরও পদক্ষেপ", - "moveToFolder": "ফোল্ডারে সরান", - "deleteSnippet": "স্নিপেট মুছুন", - "deleteSnippetConfirm": "\"{{name}} \" স্নিপেটটি মুছে ফেলবেন?", - "deleteSuccess": "স্নিপেট মুছে ফেলা হয়েছে", - "delete": "মুছে ফেলুন", - "folderActions": "ফোল্ডারের কার্যকলাপ", - "addSnippetHere": "এখানে নতুন অংশ", - "deleteFolder": "ফোল্ডারটি মুছে ফেলুন", - "deleteFolderConfirm": "\"{{name}}\" ফোল্ডারটি মুছে ফেলবেন? এর স্নিপেটগুলো ফোল্ডারের বাইরে চলে যায়।", - "createFolderDescription": "সম্পর্কিত অংশগুলো একসাথে গ্রুপ করুন।", - "editFolderDescription": "এই ফোল্ডারটির নাম, রঙ বা আইকন পরিবর্তন করুন।", - "folderColorLabel": "রঙ", - "loadFailed": "স্নিপেট লোড করতে ব্যর্থ হয়েছে", - "importSkipExisting": "আমদানি করুন (বিদ্যমানটি বাদ দিন)", - "importOverwrite": "আমদানি করুন (বিদ্যমানটি মুছে ফেলুন)", - "importInvalidFile": "ওই ফাইলটি একটি বৈধ স্নিপেট এক্সপোর্ট নয়।", - "importSuccess": "{{imported}}আমদানি করা হয়েছে, {{updated}}আপডেট করা হয়েছে, {{skipped}} বাদ দেওয়া হয়েছে", - "exportSnippets": "স্নিপেট রপ্তানি করুন", - "exportSuccess": "স্নিপেট রপ্তানি করা হয়েছে", - "exportFailed": "স্নিপেট রপ্তানি করতে ব্যর্থ হয়েছে", - "expandAll": "সমস্ত ফোল্ডার প্রসারিত করুন", - "collapseAll": "সমস্ত ফোল্ডার সংকুচিত করুন", - "docsLink": "ডকুমেন্টেশন", - "clearSearch": "পরিষ্কার অনুসন্ধান", - "noSearchResults": "আপনার অনুসন্ধানের সাথে কোনো স্নিপেট মেলেনি।", - "emptyTitle": "এখনও কোনো স্নিপেট নেই", - "emptyDescription": "যে কমান্ডগুলো আপনি প্রায়ই ব্যবহার করেন, সেগুলো সেভ করে রাখুন, তারপর টার্মিনালে এক ক্লিকেই চালান।", - "shareSnippet": "স্নিপেট শেয়ার করুন", - "shareFolder": "ফোল্ডার শেয়ার করুন", - "shareExpiry": "মেয়াদ শেষ", - "shareExpiryOptions": { - "h0": "কখনো না", - "h1": "১ ঘন্টায়", - "h24": "১ দিনে", - "h168": "৭ দিনে", - "h720": "৩০ দিনে" - }, - "shareButton": "শেয়ার", - "shareSuccess": "{{count}} এর সাথে শেয়ার করা হয়েছে", - "shareFailed": "শেয়ার করতে ব্যর্থ হয়েছে", - "shareLoadFailed": "শেয়ারিং বিবরণ লোড করতে ব্যর্থ হয়েছে", - "currentAccess": "বর্তমান অ্যাক্সেস", - "notSharedYet": "এখনো কারো সাথে শেয়ার করা হয়নি", - "revokeAccess": "বাতিল করুন", - "revokeSuccess": "প্রবেশাধিকার বাতিল করা হয়েছে", - "revokeFailed": "অ্যাক্সেস প্রত্যাহার করা যায়নিfdfg", - "close": "বন্ধ করুন", - "targetHostsLabel": "লক্ষ্য হোস্ট", - "targetHostsHint": "টার্মিনাল খোলা ছাড়াই সরাসরি এই কমান্ডটি চালানোর জন্য হোস্ট নির্বাচন করুন। সক্রিয় টার্মিনালে এটি চালাতে খালি রাখুন।", - "clearTargetHosts": "পরিষ্কার", - "noHostsAvailable": "কোন হোস্ট উপলব্ধ নেই", - "searchHosts": "হোস্ট অনুসন্ধান করুন", - "runOnTargets": "টার্গেট হোস্টগুলিতে চালান", - "executionResultTitle": "\"{{name}} \" এর ফলাফল", - "executionResultDescription": "প্রতিটি টার্গেট হোস্ট থেকে আউটপুট।", - "executionSuccess": "সাফল্য", - "executionFailed": "ব্যর্থ", - "directRunSuccess": "{{count}} হোস্টে \"{{name}}\" চালানো হয়েছে", - "directRunPartialFail": "কিছু হোস্টে \"{{name}}\" ব্যর্থ হয়েছে", - "reorderFailed": "স্নিপেটগুলির পুনর্বিন্যাস করতে ব্যর্থ হয়েছে", - "dropToRemoveFromFolder": "ফোল্ডার থেকে বের হতে এখানে ড্রপ করুন।", - "shareSnippetTitle": "শেয়ার করুন \"{{name}}\"", - "shareFolderTitle": "ফোল্ডার শেয়ার করুন \"{{name}}\"", - "shareWith": "শেয়ার করুন", - "usersTab": "ব্যবহারকারীরা", - "rolesTab": "ভূমিকা", - "shareSearchPlaceholder": "ব্যবহারকারী বা ভূমিকা অনুসন্ধান করুন", - "shareNoMatches": "কোনো মিল নেই", - "shareWithCount": "{{count}} এর সাথে শেয়ার করুন", - "shareFolderHint": "এই ফোল্ডারের প্রতিটি স্নিপেট শেয়ার করা হয়। পরে যোগ করা স্নিপেটগুলো স্বয়ংক্রিয়ভাবে শেয়ার করা হয় না।", - "shareFolderSummary": "এই ফোল্ডারে শেয়ার করা {{count}} স্নিপেট(গুলি)", - "grantedBy": "মঞ্জুর করেছেন", - "expired": "মেয়াদোত্তীর্ণ", - "variablesDialogTitle": "দৌড়াও \"{{name}}\"", - "variablesDialogDescription": "এই কোড স্নিপেটটি চালানোর আগে প্রয়োজনীয় মানগুলি পূরণ করুন।", - "variablesPreviewLabel": "প্রিভিউ", - "variablesConfirmButton": "দৌড়", - "keybindings": { - "runSnippet": "বিদ্যমান কোড চালান", - "snippetLabel": "স্নিপেট", - "selectSnippet": "একটি স্নিপেট নির্বাচন করুন", - "snippetMissing": "স্নিপেটটি খুঁজে পাওয়া যায়নি", - "snippetRequired": "একটি স্নিপেট নির্বাচন করুন" - }, - "paletteHintRun": "সক্রিয় টার্মিনাল ট্যাবে চালান", - "host": { - "startupTitle": "স্টার্টআপ স্নিপেট", - "quickActionsTitle": "দ্রুত পদক্ষেপ", - "quickActionsDescription": "এক ক্লিকে কমান্ড কার্যকর করার জন্য কুইক অ্যাকশনগুলো হোস্ট মেট্রিক্স টুলবারে বাটন হিসেবে প্রদর্শিত হয়।", - "noQuickActions": "এখনো কোনো দ্রুত পদক্ষেপ নেওয়া হয়নি।", - "buttonLabel": "বোতাম লেবেল", - "selectSnippet": "স্নিপেট নির্বাচন করুন...", - "addQuickAction": "অ্যাকশন যোগ করুন", - "tabTitle": "খণ্ডাংশ", - "startupDescription": "প্রতিবার এই হোস্টে সংযোগ করার সময় এটি টার্মিনালে চলে। $HOST -এর মতো ভেরিয়েবলগুলো পূরণ করা হয়; ইনপুট চাওয়ার একটি অংশ এড়িয়ে যাওয়া হয়।", - "none": "কোনোটিই না", - "removeQuickAction": "দ্রুত পদক্ষেপ অপসারণ করুন" - }, - "quickActions": { - "running": "{{name}} কার্যকর করা হচ্ছে ...", - "succeeded": "{{name}} সফলভাবে সম্পন্ন হয়েছে", - "failed": "{{name}} ব্যর্থ হয়েছে", - "error": "{{name}} কার্যকর করতে ব্যর্থ হয়েছে" - }, - "paletteHintPaste": "সক্রিয় টার্মিনাল ট্যাবে পেস্ট করুন", - "targetTerminals": "দৌড়ে যান", - "targetTerminalsHint": "ঐচ্ছিক, অন্যথায় সক্রিয় ট্যাব", - "selectAll": "সব", - "selectNone": "কোনোটিই না", - "plugin": { - "name": "খণ্ডাংশ" - } -} diff --git a/plugins/snippets/locales/translated/ca_ES.json b/plugins/snippets/locales/translated/ca_ES.json deleted file mode 100644 index 9fe9370c0..000000000 --- a/plugins/snippets/locales/translated/ca_ES.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Fragments" - }, - "onboarding": { - "feature_snippets": "Fragments", - "feature_snippets_desc": "Desa les ordres que executes sovint i envia-les a un terminal amb un sol clic." - }, - "permissions": { - "view": { - "title": "Veure fragments", - "description": "Veure els vostres propis fragments, notes i fragments compartits." - }, - "create": { - "title": "Crea fragments", - "description": "Crea nous fragments, notes i carpetes." - }, - "edit": { - "title": "Edita fragments", - "description": "Canvieu el nom o modifiqueu els fragments i les carpetes existents." - }, - "delete": { - "title": "Suprimeix fragments", - "description": "Suprimeix fragments i carpetes." - }, - "share": { - "title": "Comparteix fragments", - "description": "Comparteix fragments i carpetes amb altres usuaris o rols." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Replega les carpetes per defecte", - "description": "Inicia cada carpeta de fragments reduïda quan obris el panell Fragments." - }, - "showCommands": { - "label": "Mostra les ordres", - "description": "Mostra la primera línia de l'ordre de cada fragment sota el seu nom." - }, - "confirmExecution": { - "label": "Confirma abans d'executar", - "description": "Pregunta abans que s'executi un fragment d'ordre en un terminal." - } - }, - "cancel": "Cancel·la", - "loading": "S'està carregant...", - "searchPlaceholder": "Fragments de cerca", - "newFolder": "Nova carpeta", - "newSnippet": "Nou fragment", - "noFolder": "Sense carpeta", - "noSnippetsInFolder": "No hi ha fragments en aquesta carpeta", - "noTerminalTabsOpen": "No hi ha cap pestanya de terminal oberta", - "run": "Corre", - "pasteToTerminal": "Enganxa al terminal", - "copySuccess": "Copiat al porta-retalls", - "importExport": "Importació / Exportació", - "importFailed": "No s'han pogut importar els fragments", - "nameLabel": "Nom", - "namePlaceholder": "Nom del fragment", - "descriptionLabel": "Descripció", - "descriptionPlaceholder": "Descripció opcional", - "folderLabel": "Carpeta", - "typeLabel": "Tipus", - "typeCommand": "Comandament", - "typeNote": "Nota", - "commandLabel": "Comandament", - "commandPlaceholder": "Ordre per executar, p. ex. systemctl status $HOST", - "noteLabel": "Nota", - "notePlaceholder": "Text per enganxar al terminal", - "createSnippetTitle": "Crea un fragment", - "createSnippetButton": "Crea un fragment", - "createSuccess": "Fragment creat", - "createFailed": "No s'ha pogut crear el fragment", - "editSnippetTitle": "Edita el fragment", - "saveSnippetButton": "Desa el fragment", - "updateSuccess": "Fragment actualitzat", - "updateFailed": "No s'ha pogut actualitzar el fragment", - "deleteFailed": "No s'ha pogut suprimir el fragment", - "createFolderTitle": "Crea una carpeta", - "createFolderButton": "Crea una carpeta", - "folderCreateSuccess": "Carpeta creada", - "folderCreateFailed": "No s'ha pogut crear la carpeta", - "editFolderTitle": "Edita la carpeta", - "saveFolderButton": "Desa la carpeta", - "folderEditSuccess": "Carpeta actualitzada", - "folderEditFailed": "No s'ha pogut actualitzar la carpeta", - "folderNameLabel": "Nom de la carpeta", - "folderNamePlaceholder": "Nom de la carpeta", - "folderIconLabel": "Icona", - "folderDeleteSuccess": "Carpeta suprimida", - "folderDeleteFailed": "No s'ha pogut suprimir la carpeta", - "confirmRunMessage": "Executar \"{{name}}\"?", - "confirmRunButton": "Corre", - "runSuccess": "Ha executat \"{{name}}\" al(s) terminal(s) {{count}}", - "pasteSuccess": "S'ha enganxat \"{{name}}\" al(s) terminal(s) {{count}}", - "admin": { - "tabTitle": "Fragments", - "count": "{{count}} fragments", - "add": "Afegeix un fragment", - "namePlaceholder": "Nom del fragment", - "contentPlaceholder": "Contingut de l'ordre", - "folderPlaceholder": "Carpeta (opcional)", - "empty": "Aquest usuari no té fragments", - "deleteConfirm": "Voleu suprimir el fragment \"{{name}}\" pertanyent a {{username}}?", - "deleted": "Fragment suprimit", - "deleteFailed": "No s'ha pogut suprimir el fragment", - "requiredFields": "El nom i el contingut del fragment són obligatoris", - "saved": "Fragment desat", - "saveFailed": "No s'ha pogut desar el fragment", - "saving": "Desant...", - "save": "Desa" - }, - "backToSnippets": "Torna a fragments", - "settingsTitle": "Configuració del fragment", - "settingsDisplayTitle": "Pantalla", - "settingsBehaviorTitle": "Comportament", - "typeCommandHint": "S'executa al terminal actiu.", - "typeNoteHint": "Enganxa al terminal actiu sense executar-se.", - "variablesHint": "Variables: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Etiqueta}", - "doubleClickToRun": "Feu doble clic per executar", - "sharedBadge": "Compartit", - "copyToClipboard": "Copia al porta-retalls", - "copyFailed": "No s'ha pogut copiar al porta-retalls", - "moreOptions": "Més opcions", - "moreActions": "Més accions", - "moveToFolder": "Mou a la carpeta", - "deleteSnippet": "Suprimeix el fragment", - "deleteSnippetConfirm": "Voleu suprimir el fragment \"{{name}}\"?", - "deleteSuccess": "Fragment suprimit", - "delete": "Suprimeix", - "folderActions": "Accions de carpeta", - "addSnippetHere": "Nou fragment aquí", - "deleteFolder": "Suprimeix la carpeta", - "deleteFolderConfirm": "Suprimeix la carpeta \"{{name}}\"? Els seus fragments es mouen fora de la carpeta.", - "createFolderDescription": "Agrupa fragments relacionats.", - "editFolderDescription": "Canvia el nom, el color o la icona d'aquesta carpeta.", - "folderColorLabel": "Color", - "loadFailed": "No s'han pogut carregar els fragments", - "importSkipExisting": "Importa (omet els existents)", - "importOverwrite": "Importa (sobreescriu l'existent)", - "importInvalidFile": "Aquest fitxer no és una exportació de fragments vàlida.", - "importSuccess": "Importat {{imported}}, actualitzat {{updated}}, omès {{skipped}}", - "exportSnippets": "Exporta fragments", - "exportSuccess": "Fragments exportats", - "exportFailed": "No s'han pogut exportar els fragments", - "expandAll": "Expandir totes les carpetes", - "collapseAll": "Replega totes les carpetes", - "docsLink": "Documentació", - "clearSearch": "Esborra la cerca", - "noSearchResults": "No hi ha fragments que coincideixin amb la teva cerca", - "emptyTitle": "Encara no hi ha fragments", - "emptyDescription": "Desa les ordres que executes sovint i després executa-les en un terminal amb un sol clic.", - "shareSnippet": "Comparteix fragment", - "shareFolder": "Compartir carpeta", - "shareExpiry": "Caduca", - "shareExpiryOptions": { - "h0": "Mai", - "h1": "En 1 hora", - "h24": "En 1 dia", - "h168": "En 7 dies", - "h720": "En 30 dies" - }, - "shareButton": "Comparteix", - "shareSuccess": "Compartit amb {{count}}", - "shareFailed": "No s'ha pogut compartir", - "shareLoadFailed": "No s'han pogut carregar els detalls de compartició", - "currentAccess": "Accés actual", - "notSharedYet": "Encara no s'ha compartit amb ningú", - "revokeAccess": "Revocar", - "revokeSuccess": "Accés revocat", - "revokeFailed": "No s'ha pogut revocar l'accés", - "close": "Tanca", - "targetHostsLabel": "Amfitrions de destinació", - "targetHostsHint": "Trieu els hosts on executar aquesta ordre directament, sense un terminal obert. Deixeu-ho en blanc per executar-la al terminal actiu.", - "clearTargetHosts": "Clar", - "noHostsAvailable": "No hi ha amfitrions disponibles", - "searchHosts": "Cerca amfitrions", - "runOnTargets": "Executar en els amfitrions de destinació", - "executionResultTitle": "Resultats per a \"{{name}}\"", - "executionResultDescription": "Sortida de cada amfitrió de destinació.", - "executionSuccess": "Èxit", - "executionFailed": "Fallat", - "directRunSuccess": "Ha executat \"{{name}}\" en {{count}} host(s)", - "directRunPartialFail": "\"{{name}}\" ha fallat en alguns hosts", - "reorderFailed": "No s'han pogut reordenar els fragments", - "dropToRemoveFromFolder": "Deixa anar aquí per sortir de la carpeta", - "shareSnippetTitle": "Comparteix \"{{name}}\"", - "shareFolderTitle": "Comparteix la carpeta \"{{name}}\"", - "shareWith": "Comparteix amb", - "usersTab": "Usuaris", - "rolesTab": "Rols", - "shareSearchPlaceholder": "Cerca usuaris o rols", - "shareNoMatches": "Sense coincidències", - "shareWithCount": "Comparteix amb {{count}}", - "shareFolderHint": "Comparteix tots els fragments d'aquesta carpeta. Els fragments afegits més tard no es comparteixen automàticament.", - "shareFolderSummary": "Fragments compartits {{count}} en aquesta carpeta", - "grantedBy": "Atorgat per", - "expired": "Caducat", - "variablesDialogTitle": "Executa \"{{name}}\"", - "variablesDialogDescription": "Empleneu els valors que necessita aquest fragment abans d'executar-lo.", - "variablesPreviewLabel": "Vista prèvia", - "variablesConfirmButton": "Corre", - "keybindings": { - "runSnippet": "Executa el fragment existent", - "snippetLabel": "Fragment", - "selectSnippet": "Selecciona un fragment", - "snippetMissing": "fragment no trobat", - "snippetRequired": "Selecciona un fragment" - }, - "paletteHintRun": "Executar a la pestanya activa del terminal", - "host": { - "startupTitle": "Fragment d'inici", - "quickActionsTitle": "Accions ràpides", - "quickActionsDescription": "Les accions ràpides apareixen com a botons a la barra d'eines de mètriques de l'amfitrió per executar ordres amb un sol clic.", - "noQuickActions": "Encara no hi ha accions ràpides.", - "buttonLabel": "Etiqueta del botó", - "selectSnippet": "Selecciona un fragment...", - "addQuickAction": "Afegeix una acció", - "tabTitle": "Fragments", - "startupDescription": "S'executa al terminal cada vegada que es connecta a aquest host. Les variables com ara $HOST s'omplen; un fragment que demana entrades s'omet.", - "none": "Cap", - "removeQuickAction": "Elimina l'acció ràpida" - }, - "quickActions": { - "running": "Executant {{name}}...", - "succeeded": "{{name}} s'ha completat correctament", - "failed": "{{name}} ha fallat", - "error": "No s'ha pogut executar {{name}}" - }, - "paletteHintPaste": "Enganxa a la pestanya activa del terminal", - "targetTerminals": "Entrada", - "targetTerminalsHint": "opcional, si no, la pestanya activa", - "selectAll": "Tot", - "selectNone": "Cap", - "plugin": { - "name": "Fragments" - } -} diff --git a/plugins/snippets/locales/translated/cs_CZ.json b/plugins/snippets/locales/translated/cs_CZ.json deleted file mode 100644 index 0b9c82304..000000000 --- a/plugins/snippets/locales/translated/cs_CZ.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Úryvky" - }, - "onboarding": { - "feature_snippets": "Úryvky", - "feature_snippets_desc": "Uložte si často spouštíné příkazy a odešlete je do terminálu jedním kliknutím." - }, - "permissions": { - "view": { - "title": "Zobrazit úryvky", - "description": "Prohlédněte si své vlastní úryvky, poznámky a sdílené úryvky." - }, - "create": { - "title": "Vytvořit úryvky", - "description": "Vytvořte nové úryvky, poznámky a složky." - }, - "edit": { - "title": "Upravit úryvky", - "description": "Přejmenujte nebo změňte existující úryvky a složky." - }, - "delete": { - "title": "Smazat úryvky", - "description": "Smazat úryvky a složky." - }, - "share": { - "title": "Sdílet úryvky", - "description": "Sdílejte úryvky a složky s ostatními uživateli nebo rolemi." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Sbalit složky ve výchozím nastavení", - "description": "Spuštění každé složky úryvků kódu sbalené při otevření panelu Úryvky kódu." - }, - "showCommands": { - "label": "Zobrazit příkazy", - "description": "Zobrazit první řádek příkazu každého úryvku pod jeho názvem." - }, - "confirmExecution": { - "label": "Potvrďte před spuštěním", - "description": "Zeptat se před spuštěním úryvku příkazu v terminálu." - } - }, - "cancel": "Zrušit", - "loading": "Načítání...", - "searchPlaceholder": "Hledat úryvky", - "newFolder": "Nová složka", - "newSnippet": "Nový úryvek", - "noFolder": "Žádná složka", - "noSnippetsInFolder": "V této složce nejsou žádné úryvky", - "noTerminalTabsOpen": "Není otevřená žádná záložka terminálu", - "run": "Běh", - "pasteToTerminal": "Vložit do terminálu", - "copySuccess": "Zkopírováno do schránky", - "importExport": "Import / Export", - "importFailed": "Import úryvků se nezdařilo", - "nameLabel": "Jméno", - "namePlaceholder": "Název úryvku", - "descriptionLabel": "Popis", - "descriptionPlaceholder": "Volitelný popis", - "folderLabel": "Složka", - "typeLabel": "Typ", - "typeCommand": "Příkaz", - "typeNote": "Poznámka", - "commandLabel": "Příkaz", - "commandPlaceholder": "Příkaz ke spuštění, např. systemctl status $HOST", - "noteLabel": "Poznámka", - "notePlaceholder": "Text k vložení do terminálu", - "createSnippetTitle": "Vytvořit úryvek", - "createSnippetButton": "Vytvořit úryvek", - "createSuccess": "Úryvek vytvořen", - "createFailed": "Nepodařilo se vytvořit úryvek", - "editSnippetTitle": "Upravit úryvek", - "saveSnippetButton": "Uložit úryvek", - "updateSuccess": "Úryvek aktualizován", - "updateFailed": "Nepodařilo se aktualizovat úryvek", - "deleteFailed": "Nepodařilo se odstranit úryvek", - "createFolderTitle": "Vytvořit složku", - "createFolderButton": "Vytvořit složku", - "folderCreateSuccess": "Složka vytvořena", - "folderCreateFailed": "Nepodařilo se vytvořit složku", - "editFolderTitle": "Upravit složku", - "saveFolderButton": "Uložit složku", - "folderEditSuccess": "Složka aktualizována", - "folderEditFailed": "Nepodařilo se aktualizovat složku", - "folderNameLabel": "Název složky", - "folderNamePlaceholder": "Název složky", - "folderIconLabel": "Ikona", - "folderDeleteSuccess": "Složka smazána", - "folderDeleteFailed": "Složku se nepodařilo smazat", - "confirmRunMessage": "Spustit „{{name}}“?", - "confirmRunButton": "Běh", - "runSuccess": "Spuštěno „{{name}}“ na terminálu(ech) {{count}}", - "pasteSuccess": "Vloženo „{{name}}“ do terminálu(ů) {{count}}", - "admin": { - "tabTitle": "Úryvky", - "count": "úryvky {{count}}", - "add": "Přidat úryvek", - "namePlaceholder": "Název úryvku", - "contentPlaceholder": "Obsah příkazu", - "folderPlaceholder": "Složka (volitelné)", - "empty": "Tento uživatel nemá žádné úryvky", - "deleteConfirm": "Smazat úryvek „{{name}}“ patřící do {{username}}?", - "deleted": "Úryvek smazán", - "deleteFailed": "Nepodařilo se odstranit úryvek", - "requiredFields": "Název a obsah úryvku jsou povinné", - "saved": "Úryvek uložen", - "saveFailed": "Uložení úryvku se nezdařilo", - "saving": "Ukládání…", - "save": "Uložit" - }, - "backToSnippets": "Zpět k úryvkům", - "settingsTitle": "Nastavení úryvků", - "settingsDisplayTitle": "Zobrazit", - "settingsBehaviorTitle": "Chování", - "typeCommandHint": "Běží v aktivním terminálu.", - "typeNoteHint": "Vloží do aktivního terminálu bez spuštění.", - "variablesHint": "Proměnné: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Popis}", - "doubleClickToRun": "Dvojitým kliknutím spustíte", - "sharedBadge": "Sdílené", - "copyToClipboard": "Kopírovat do schránky", - "copyFailed": "Kopírování do schránky se nezdařilo", - "moreOptions": "Více možností", - "moreActions": "Další akce", - "moveToFolder": "Přesunout do složky", - "deleteSnippet": "Smazat úryvek", - "deleteSnippetConfirm": "Smazat úryvek „{{name}}“?", - "deleteSuccess": "Úryvek smazán", - "delete": "Vymazat", - "folderActions": "Akce se složkou", - "addSnippetHere": "Nový úryvek zde", - "deleteFolder": "Smazat složku", - "deleteFolderConfirm": "Smazat složku „{{name}}“? Její úryvky se přesunou mimo složku.", - "createFolderDescription": "Seskupte související úryvky.", - "editFolderDescription": "Změňte název, barvu nebo ikonu této složky.", - "folderColorLabel": "Barva", - "loadFailed": "Načtení úryvků se nezdařilo", - "importSkipExisting": "Importovat (přeskočit existující)", - "importOverwrite": "Importovat (přepsat existující)", - "importInvalidFile": "Tento soubor není platným exportem úryvků.", - "importSuccess": "Importováno {{imported}}, aktualizováno {{updated}}, přeskočeno {{skipped}}", - "exportSnippets": "Exportovat úryvky", - "exportSuccess": "Exportované úryvky", - "exportFailed": "Export úryvků se nezdařilo", - "expandAll": "Rozbalit všechny složky", - "collapseAll": "Sbalit všechny složky", - "docsLink": "Dokumentace", - "clearSearch": "Vymazat vyhledávání", - "noSearchResults": "Žádné úryvky neodpovídají vašemu vyhledávání", - "emptyTitle": "Zatím žádné úryvky", - "emptyDescription": "Uložte si často spouštíné příkazy a poté je spusťte v terminálu jedním kliknutím.", - "shareSnippet": "Sdílet úryvek", - "shareFolder": "Sdílet složku", - "shareExpiry": "Platnost vyprší", - "shareExpiryOptions": { - "h0": "Nikdy", - "h1": "Za 1 hodinu", - "h24": "Za 1 den", - "h168": "Za 7 dní", - "h720": "Za 30 dní" - }, - "shareButton": "Podíl", - "shareSuccess": "Sdíleno s {{count}}", - "shareFailed": "Sdílení se nezdařilo", - "shareLoadFailed": "Nepodařilo se načíst podrobnosti o sdílení", - "currentAccess": "Aktuální přístup", - "notSharedYet": "Zatím s nikým nesdíleno", - "revokeAccess": "Zrušit", - "revokeSuccess": "Přístup zrušen", - "revokeFailed": "Zrušení přístupu se nezdařilo", - "close": "Blízko", - "targetHostsLabel": "Cílové hostitele", - "targetHostsHint": "Vyberte hostitele, na kterých se má tento příkaz spustit přímo, bez otevřeného terminálu. Pro spuštění v aktivním terminálu ponechte pole prázdné.", - "clearTargetHosts": "Jasný", - "noHostsAvailable": "Nejsou dostupní žádní hostitelé", - "searchHosts": "Vyhledávání hostitelů", - "runOnTargets": "Spustit na cílových hostitelích", - "executionResultTitle": "Výsledky pro „{{name}}“", - "executionResultDescription": "Výstup z každého cílového hostitele.", - "executionSuccess": "Úspěch", - "executionFailed": "Selhalo", - "directRunSuccess": "Spuštěno „{{name}}“ na hostiteli(ích) {{count}}", - "directRunPartialFail": "„{{name}}“ selhalo na některých hostitelích", - "reorderFailed": "Nepodařilo se změnit pořadí úryvků", - "dropToRemoveFromFolder": "Přetáhněte sem pro přesun ze složky", - "shareSnippetTitle": "Sdílet „{{name}}“", - "shareFolderTitle": "Sdílet složku „{{name}}“", - "shareWith": "Sdílet s", - "usersTab": "Uživatelé", - "rolesTab": "Role", - "shareSearchPlaceholder": "Vyhledávání uživatelů nebo rolí", - "shareNoMatches": "Žádné shody", - "shareWithCount": "Sdílet s {{count}}", - "shareFolderHint": "Sdílí všechny úryvky v této složce. Úryvky přidané později se nesdílejí automaticky.", - "shareFolderSummary": "Sdílené úryvky kódu {{count}} v této složce", - "grantedBy": "Uděleno", - "expired": "Platnost vypršela", - "variablesDialogTitle": "Spustit „{{name}}“", - "variablesDialogDescription": "Před spuštěním úryvku kódu vyplňte jeho potřebné hodnoty.", - "variablesPreviewLabel": "Náhled", - "variablesConfirmButton": "Běh", - "keybindings": { - "runSnippet": "Spustit existující úryvek kódu", - "snippetLabel": "Úryvek", - "selectSnippet": "Vyberte úryvek", - "snippetMissing": "úryvek nenalezen", - "snippetRequired": "Vyberte úryvek" - }, - "paletteHintRun": "Spustit v aktivní kartě terminálu", - "host": { - "startupTitle": "Spustit úryvek", - "quickActionsTitle": "Rychlé akce", - "quickActionsDescription": "Rychlé akce se zobrazují jako tlačítka v panelu nástrojů Metriky hostitele pro spuštění příkazů jedním kliknutím.", - "noQuickActions": "Zatím žádné rychlé akce.", - "buttonLabel": "Štítek tlačítka", - "selectSnippet": "Vybrat úryvek...", - "addQuickAction": "Přidat akci", - "tabTitle": "Úryvky", - "startupDescription": "Spustí se v terminálu při každém připojení k tomuto hostiteli. Proměnné jako $HOST jsou vyplněny; úryvek kódu, který požaduje vstupy, je přeskočen.", - "none": "Žádný", - "removeQuickAction": "Odebrat rychlou akci" - }, - "quickActions": { - "running": "Spouštění {{name}}...", - "succeeded": "{{name}} úspěšně dokončeno", - "failed": "{{name}} selhalo", - "error": "Nepodařilo se spustit {{name}}" - }, - "paletteHintPaste": "Vložit do aktivní záložky terminálu", - "targetTerminals": "Zaběhnout", - "targetTerminalsHint": "volitelné, jinak aktivní záložka", - "selectAll": "Vše", - "selectNone": "Žádný", - "plugin": { - "name": "Úryvky" - } -} diff --git a/plugins/snippets/locales/translated/da_DK.json b/plugins/snippets/locales/translated/da_DK.json deleted file mode 100644 index f8cb4ef03..000000000 --- a/plugins/snippets/locales/translated/da_DK.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Uddrag" - }, - "onboarding": { - "feature_snippets": "Uddrag", - "feature_snippets_desc": "Gem kommandoer, du kører ofte, og send dem til en terminal med et enkelt klik." - }, - "permissions": { - "view": { - "title": "Se uddrag", - "description": "Se dine egne uddrag, noter og delte uddrag." - }, - "create": { - "title": "Opret uddrag", - "description": "Opret nye uddrag, noter og mapper." - }, - "edit": { - "title": "Rediger uddrag", - "description": "Omdøb eller rediger eksisterende uddrag og mapper." - }, - "delete": { - "title": "Slet uddrag", - "description": "Slet uddrag og mapper." - }, - "share": { - "title": "Del uddrag", - "description": "Del uddrag og mapper med andre brugere eller roller." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Skjul mapper som standard", - "description": "Start hver uddragsmappe skjult, når du åbner panelet Uddrag." - }, - "showCommands": { - "label": "Vis kommandoer", - "description": "Vis den første linje af hver kodestykkets kommando under dens navn." - }, - "confirmExecution": { - "label": "Bekræft før kørsel", - "description": "Spørg før et kommandosnipp kører i en terminal." - } - }, - "cancel": "Ophæve", - "loading": "Indlæser...", - "searchPlaceholder": "Søgeuddrag", - "newFolder": "Ny mappe", - "newSnippet": "Nyt uddrag", - "noFolder": "Ingen mappe", - "noSnippetsInFolder": "Ingen uddrag i denne mappe", - "noTerminalTabsOpen": "Ingen terminalfane er åben", - "run": "Løbe", - "pasteToTerminal": "Indsæt i terminalen", - "copySuccess": "Kopieret til udklipsholderen", - "importExport": "Import/eksport", - "importFailed": "Kunne ikke importere uddrag", - "nameLabel": "Navn", - "namePlaceholder": "Snippetnavn", - "descriptionLabel": "Beskrivelse", - "descriptionPlaceholder": "Valgfri beskrivelse", - "folderLabel": "Folder", - "typeLabel": "Type", - "typeCommand": "Kommando", - "typeNote": "Note", - "commandLabel": "Kommando", - "commandPlaceholder": "Kommando til at køre, f.eks. systemctl status $HOST", - "noteLabel": "Note", - "notePlaceholder": "Tekst, der skal indsættes i terminalen", - "createSnippetTitle": "Opret uddrag", - "createSnippetButton": "Opret uddrag", - "createSuccess": "Uddrag oprettet", - "createFailed": "Kunne ikke oprette kodestykket", - "editSnippetTitle": "Rediger uddrag", - "saveSnippetButton": "Gem uddrag", - "updateSuccess": "Uddrag opdateret", - "updateFailed": "Kunne ikke opdatere uddraget", - "deleteFailed": "Kunne ikke slette uddraget", - "createFolderTitle": "Opret mappe", - "createFolderButton": "Opret mappe", - "folderCreateSuccess": "Mappe oprettet", - "folderCreateFailed": "Kunne ikke oprette en mappe", - "editFolderTitle": "Rediger mappe", - "saveFolderButton": "Gem mappe", - "folderEditSuccess": "Mappen er opdateret", - "folderEditFailed": "Mappen kunne ikke opdateres", - "folderNameLabel": "Mappenavn", - "folderNamePlaceholder": "Mappenavn", - "folderIconLabel": "Ikon", - "folderDeleteSuccess": "Mappe slettet", - "folderDeleteFailed": "Kunne ikke slette mappen", - "confirmRunMessage": "Kør \"{{name}}\"?", - "confirmRunButton": "Løbe", - "runSuccess": "Kørte \"{{name}}\" på {{count}} terminal(er)", - "pasteSuccess": "Indsatte \"{{name}}\" i {{count}} terminal(er)", - "admin": { - "tabTitle": "Uddrag", - "count": "{{count}} uddrag", - "add": "Tilføj uddrag", - "namePlaceholder": "Snippetnavn", - "contentPlaceholder": "Kommandoindhold", - "folderPlaceholder": "Mappe (valgfrit)", - "empty": "Denne bruger har ingen uddrag", - "deleteConfirm": "Slet uddraget \"{{name}}\" der tilhører {{username}}?", - "deleted": "Uddrag slettet", - "deleteFailed": "Kunne ikke slette uddraget", - "requiredFields": "Navn og indhold på uddrag er påkrævet", - "saved": "Uddrag gemt", - "saveFailed": "Kunne ikke gemme uddraget", - "saving": "Gemmer...", - "save": "Spare" - }, - "backToSnippets": "Tilbage til uddrag", - "settingsTitle": "Indstillinger for uddrag", - "settingsDisplayTitle": "Vise", - "settingsBehaviorTitle": "Opførsel", - "typeCommandHint": "Kører i den aktive terminal.", - "typeNoteHint": "Indsætter i den aktive terminal uden at køre.", - "variablesHint": "Variabler: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Dobbeltklik for at køre", - "sharedBadge": "Delt", - "copyToClipboard": "Kopiér til udklipsholder", - "copyFailed": "Kunne ikke kopiere til udklipsholder", - "moreOptions": "Flere muligheder", - "moreActions": "Flere handlinger", - "moveToFolder": "Flyt til mappe", - "deleteSnippet": "Slet uddrag", - "deleteSnippetConfirm": "Slet uddraget \"{{name}}\"?", - "deleteSuccess": "Uddrag slettet", - "delete": "Slet", - "folderActions": "Mappehandlinger", - "addSnippetHere": "Nyt uddrag her", - "deleteFolder": "Slet mappe", - "deleteFolderConfirm": "Slet mappen \"{{name}}\"? Dens uddrag flyttes ud af mappen.", - "createFolderDescription": "Gruppér relaterede uddrag sammen.", - "editFolderDescription": "Skift denne mappes navn, farve eller ikon.", - "folderColorLabel": "Farve", - "loadFailed": "Kunne ikke indlæse uddrag", - "importSkipExisting": "Importér (spring eksisterende over)", - "importOverwrite": "Importér (overskriv eksisterende)", - "importInvalidFile": "Den fil er ikke en gyldig eksport af kodestykker", - "importSuccess": "Importeret {{imported}}, opdateret {{updated}}, sprunget over {{skipped}}", - "exportSnippets": "Eksportér uddrag", - "exportSuccess": "Eksporterede uddrag", - "exportFailed": "Kunne ikke eksportere uddrag", - "expandAll": "Udvid alle mapper", - "collapseAll": "Skjul alle mapper", - "docsLink": "Dokumentation", - "clearSearch": "Ryd søgning", - "noSearchResults": "Ingen uddrag matcher din søgning", - "emptyTitle": "Ingen uddrag endnu", - "emptyDescription": "Gem kommandoer, du kører ofte, og kør dem derefter i en terminal med et enkelt klik.", - "shareSnippet": "Del uddrag", - "shareFolder": "Del mappe", - "shareExpiry": "Udløber", - "shareExpiryOptions": { - "h0": "Aldrig", - "h1": "Om 1 time", - "h24": "Om 1 dag", - "h168": "Om 7 dage", - "h720": "Om 30 dage" - }, - "shareButton": "Dele", - "shareSuccess": "Delt med {{count}}", - "shareFailed": "Kunne ikke dele", - "shareLoadFailed": "Delingsoplysningerne kunne ikke indlæses", - "currentAccess": "Nuværende adgang", - "notSharedYet": "Ikke delt med nogen endnu", - "revokeAccess": "Tilbagekald", - "revokeSuccess": "Adgang tilbagekaldt", - "revokeFailed": "Adgang kunne ikke tilbagekaldes", - "close": "Tæt", - "targetHostsLabel": "Mål værter", - "targetHostsHint": "Vælg værter, som denne kommando skal køres direkte på, uden en terminal åben. Lad feltet stå tomt for at køre den i den aktive terminal.", - "clearTargetHosts": "Klar", - "noHostsAvailable": "Ingen værter tilgængelige", - "searchHosts": "Søg efter værter", - "runOnTargets": "Kør på målværter", - "executionResultTitle": "Resultater for \"{{name}}\"", - "executionResultDescription": "Output fra hver målvært.", - "executionSuccess": "Succes", - "executionFailed": "Mislykkedes", - "directRunSuccess": "Kørte \"{{name}}\" på {{count}} vært(er)", - "directRunPartialFail": "\"{{name}}\" mislykkedes på nogle værter", - "reorderFailed": "Kunne ikke omarrangere uddrag", - "dropToRemoveFromFolder": "Slip her for at flytte ud af mappen", - "shareSnippetTitle": "Del \"{{name}}\"", - "shareFolderTitle": "Del mappe \"{{name}}\"", - "shareWith": "Del med", - "usersTab": "Brugere", - "rolesTab": "Roller", - "shareSearchPlaceholder": "Søg efter brugere eller roller", - "shareNoMatches": "Ingen matches", - "shareWithCount": "Del med {{count}}", - "shareFolderHint": "Deler alle uddrag i denne mappe. Uddrag, der tilføjes senere, deles ikke automatisk.", - "shareFolderSummary": "Delte {{count}} kodestykker i denne mappe", - "grantedBy": "Tildelt af", - "expired": "Udløbet", - "variablesDialogTitle": "Kør \"{{name}}\"", - "variablesDialogDescription": "Udfyld de værdier, som dette kodestykke skal bruge, før det køres.", - "variablesPreviewLabel": "Forhåndsvisning", - "variablesConfirmButton": "Løbe", - "keybindings": { - "runSnippet": "Kør eksisterende kodestykke", - "snippetLabel": "Uddrag", - "selectSnippet": "Vælg et uddrag", - "snippetMissing": "uddrag ikke fundet", - "snippetRequired": "Vælg et uddrag" - }, - "paletteHintRun": "Kør i den aktive terminalfane", - "host": { - "startupTitle": "Opstartskodestykke", - "quickActionsTitle": "Hurtige handlinger", - "quickActionsDescription": "Hurtige handlinger vises som knapper i værktøjslinjen Host Metrics til udførelse af kommandoer med et enkelt klik.", - "noQuickActions": "Ingen hurtige handlinger endnu.", - "buttonLabel": "Knapmærke", - "selectSnippet": "Vælg uddrag...", - "addQuickAction": "Tilføj handling", - "tabTitle": "Uddrag", - "startupDescription": "Kører i terminalen hver gang den opretter forbindelse til denne vært. Variabler som $HOST udfyldes; et kodestykke, der beder om input, springes over.", - "none": "Ingen", - "removeQuickAction": "Fjern hurtig handling" - }, - "quickActions": { - "running": "Udfører {{name}}...", - "succeeded": "{{name}} gennemført", - "failed": "{{name}} mislykkedes", - "error": "Kunne ikke udføre {{name}}" - }, - "paletteHintPaste": "Indsæt i den aktive terminalfane", - "targetTerminals": "Kør ind", - "targetTerminalsHint": "valgfrit, ellers den aktive fane", - "selectAll": "Alle", - "selectNone": "Ingen", - "plugin": { - "name": "Snippets" - } -} diff --git a/plugins/snippets/locales/translated/de_DE.json b/plugins/snippets/locales/translated/de_DE.json deleted file mode 100644 index cdac19569..000000000 --- a/plugins/snippets/locales/translated/de_DE.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Snippets" - }, - "onboarding": { - "feature_snippets": "Snippets", - "feature_snippets_desc": "Speichern Sie häufig ausgeführte Befehle und senden Sie sie mit einem Klick an ein Terminal." - }, - "permissions": { - "view": { - "title": "Ausschnitte ansehen", - "description": "Sehen Sie Ihre eigenen Ausschnitte, Notizen und geteilten Ausschnitte." - }, - "create": { - "title": "Code-Snippets erstellen", - "description": "Neue Snippets, Notizen und Ordner erstellen." - }, - "edit": { - "title": "Bearbeitungs-Snippets", - "description": "Vorhandene Snippets und Ordner umbenennen oder ändern." - }, - "delete": { - "title": "Code-Snippets löschen", - "description": "Snippets und Ordner löschen." - }, - "share": { - "title": "Ausschnitte teilen", - "description": "Teilen Sie Codeausschnitte und Ordner mit anderen Benutzern oder Rollen." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Ordner standardmäßig einklappen", - "description": "Beim Öffnen des Snippets-Panels sind alle Snippet-Ordner standardmäßig ausgeblendet." - }, - "showCommands": { - "label": "Befehle anzeigen", - "description": "Zeigen Sie die erste Zeile des Befehls jedes Code-Snippets unter dessen Namen an." - }, - "confirmExecution": { - "label": "Vor dem Ausführen bestätigen.", - "description": "Fragen Sie vor der Ausführung eines Befehlsabschnitts im Terminal nach." - } - }, - "cancel": "Abbrechen", - "loading": "Laden...", - "searchPlaceholder": "Snippets durchsuchen", - "newFolder": "Neuer Ordner", - "newSnippet": "Neuer Ausschnitt", - "noFolder": "Kein Ordner", - "noSnippetsInFolder": "Keine Snippets in diesem Ordner", - "noTerminalTabsOpen": "Es ist kein Terminal-Tab geöffnet.", - "run": "Ausführen", - "pasteToTerminal": "In Terminal einfügen", - "copySuccess": "In die Zwischenablage kopiert", - "importExport": "Import / Export", - "importFailed": "Snippets konnten nicht importiert werden", - "nameLabel": "Name", - "namePlaceholder": "Snippet-Name", - "descriptionLabel": "Beschreibung", - "descriptionPlaceholder": "Optionale Beschreibung", - "folderLabel": "Ordner", - "typeLabel": "Typ", - "typeCommand": "Befehl", - "typeNote": "Notiz", - "commandLabel": "Befehl", - "commandPlaceholder": "Auszuführender Befehl, z. B. systemctl status $HOST", - "noteLabel": "Notiz", - "notePlaceholder": "Text, der in das Terminal eingefügt werden soll", - "createSnippetTitle": "Snippet erstellen", - "createSnippetButton": "Snippet erstellen", - "createSuccess": "Snippet erstellt", - "createFailed": "Erstellen des Snippets fehlgeschlagen", - "editSnippetTitle": "Ausschnitt bearbeiten", - "saveSnippetButton": "Ausschnitt speichern", - "updateSuccess": "Ausschnitt aktualisiert", - "updateFailed": "Aktualisieren des Snippets fehlgeschlagen", - "deleteFailed": "Löschen des Snippets fehlgeschlagen", - "createFolderTitle": "Ordner erstellen", - "createFolderButton": "Ordner erstellen", - "folderCreateSuccess": "Ordner erstellt", - "folderCreateFailed": "Erstellen des Ordners fehlgeschlagen", - "editFolderTitle": "Ordner bearbeiten", - "saveFolderButton": "Ordner speichern", - "folderEditSuccess": "Ordner aktualisiert", - "folderEditFailed": "Aktualisieren des Ordners fehlgeschlagen", - "folderNameLabel": "Ordnername", - "folderNamePlaceholder": "Ordnername", - "folderIconLabel": "Symbol", - "folderDeleteSuccess": "Ordner gelöscht", - "folderDeleteFailed": "Löschen des Ordners fehlgeschlagen", - "confirmRunMessage": "„{{name}}“ ausführen?", - "confirmRunButton": "Ausführen", - "runSuccess": "Ran \"{{name}}\" on {{count}} terminal(s)", - "pasteSuccess": "\"{{name}}\" wurde in das/die Terminal(s) {{count}} eingefügt", - "admin": { - "tabTitle": "Snippets", - "count": "{{count}} Snippets", - "add": "Snippet hinzufügen", - "namePlaceholder": "Snippet-Name", - "contentPlaceholder": "Befehlsinhalt", - "folderPlaceholder": "Ordner (optional)", - "empty": "Dieser Benutzer hat keine Snippets", - "deleteConfirm": "Snippet \"{{name}}\" von {{username}} löschen?", - "deleted": "Snippet gelöscht", - "deleteFailed": "Löschen des Snippets fehlgeschlagen", - "requiredFields": "Name und Inhalt des Snippets sind erforderlich", - "saved": "Snippet gespeichert", - "saveFailed": "Fehler beim Speichern des Snippets", - "saving": "Speichern...", - "save": "Speichern" - }, - "backToSnippets": "Zurück zu den Ausschnitten", - "settingsTitle": "Snippet-Einstellungen", - "settingsDisplayTitle": "Anzeige", - "settingsBehaviorTitle": "Verhalten", - "typeCommandHint": "Wird im aktiven Terminal ausgeführt.", - "typeNoteHint": "Fügt den Inhalt in das aktive Terminal ein, ohne es auszuführen.", - "variablesHint": "Variablen: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Doppelklicken zum Ausführen", - "sharedBadge": "Freigegeben", - "copyToClipboard": "In die Zwischenablage kopieren", - "copyFailed": "Kopieren in die Zwischenablage fehlgeschlagen", - "moreOptions": "Mehr Optionen", - "moreActions": "Weitere Aktionen", - "moveToFolder": "In Ordner verschieben", - "deleteSnippet": "Ausschnitt löschen", - "deleteSnippetConfirm": "Ausschnitt löschen \"{{name}}\"?", - "deleteSuccess": "Snippet gelöscht", - "delete": "Löschen", - "folderActions": "Ordneraktionen", - "addSnippetHere": "Neuer Ausschnitt hier", - "deleteFolder": "Ordner löschen", - "deleteFolderConfirm": "Ordner \"{{name}}\" löschen? Seine Snippets werden aus dem Ordner verschoben.", - "createFolderDescription": "Zusammengehörige Textbausteine gruppieren.", - "editFolderDescription": "Ändern Sie den Namen, die Farbe oder das Symbol dieses Ordners.", - "folderColorLabel": "Farbe", - "loadFailed": "Snippets konnten nicht geladen werden", - "importSkipExisting": "Importieren (Bestehende überspringen)", - "importOverwrite": "Importieren (vorhandene Daten überschreiben)", - "importInvalidFile": "Diese Datei ist kein gültiger Snippet-Export.", - "importSuccess": "Importiert {{imported}}, aktualisiert {{updated}}, übersprungen {{skipped}}", - "exportSnippets": "Export-Snippets", - "exportSuccess": "Ausschnitte exportiert", - "exportFailed": "Snippets konnten nicht exportiert werden", - "expandAll": "Alle Ordner ausklappen", - "collapseAll": "Alle Ordner einklappen", - "docsLink": "Dokumentation", - "clearSearch": "Suche löschen", - "noSearchResults": "Keine Suchergebnisse entsprechen Ihrer Suche", - "emptyTitle": "Noch keine Ausschnitte.", - "emptyDescription": "Speichern Sie häufig verwendete Befehle und führen Sie diese dann mit einem Klick im Terminal aus.", - "shareSnippet": "Ausschnitt teilen", - "shareFolder": "Ordner freigeben", - "shareExpiry": "Läuft ab", - "shareExpiryOptions": { - "h0": "Nie", - "h1": "In 1 Stunde", - "h24": "In 1 Tag", - "h168": "In 7 Tagen", - "h720": "In 30 Tagen" - }, - "shareButton": "Teilen", - "shareSuccess": "Geteilt mit {{count}}", - "shareFailed": "Übermittlung fehlgeschlagen", - "shareLoadFailed": "Die Freigabedetails konnten nicht geladen werden.", - "currentAccess": "Aktueller Zugriff", - "notSharedYet": "Noch mit niemandem geteilt", - "revokeAccess": "Widerrufen", - "revokeSuccess": "Zugriff widerrufen", - "revokeFailed": "Zugriff konnte nicht widerrufen werden", - "close": "Schließen", - "targetHostsLabel": "Zielhosts", - "targetHostsHint": "Wählen Sie die Hosts aus, auf denen dieser Befehl direkt, ohne geöffnetes Terminal, ausgeführt werden soll. Lassen Sie das Feld leer, um ihn im aktiven Terminal auszuführen.", - "clearTargetHosts": "Löschen", - "noHostsAvailable": "Keine Hosts verfügbar", - "searchHosts": "Hosts suchen", - "runOnTargets": "Auf den Zielhosts ausführen", - "executionResultTitle": "Ergebnisse für \"{{name}}\"", - "executionResultDescription": "Ausgabe von jedem Zielhost.", - "executionSuccess": "Erfolgreich", - "executionFailed": "Fehlgeschlagen", - "directRunSuccess": "\"{{name}}\" auf {{count}} Host(s) ausgeführt", - "directRunPartialFail": "\"{{name}}\" schlug auf einigen Hosts fehl.", - "reorderFailed": "Fehler beim Neuanordnen der Codeausschnitte", - "dropToRemoveFromFolder": "Hierhin ziehen, um den Ordner zu verlassen", - "shareSnippetTitle": "Teilen \"{{name}}\"", - "shareFolderTitle": "Ordner freigeben \"{{name}}\"", - "shareWith": "Teilen mit", - "usersTab": "Benutzer", - "rolesTab": "Rollen", - "shareSearchPlaceholder": "Benutzer oder Rollen suchen", - "shareNoMatches": "Keine Übereinstimmungen", - "shareWithCount": "Teilen mit {{count}}", - "shareFolderHint": "Teilt alle Code-Snippets in diesem Ordner. Später hinzugefügte Snippets werden nicht automatisch geteilt.", - "shareFolderSummary": "Gemeinsame {{count}} Codeausschnitte in diesem Ordner", - "grantedBy": "Freigegeben von", - "expired": "Abgelaufen", - "variablesDialogTitle": "Run \"{{name}}\"", - "variablesDialogDescription": "Füllen Sie die für diesen Codeabschnitt erforderlichen Werte aus, bevor Sie ihn ausführen.", - "variablesPreviewLabel": "Vorschau", - "variablesConfirmButton": "Ausführen", - "keybindings": { - "runSnippet": "Vorhandenen Codeausschnitt ausführen", - "snippetLabel": "Ausschnitt", - "selectSnippet": "Wählen Sie einen Ausschnitt aus", - "snippetMissing": "Ausschnitt nicht gefunden", - "snippetRequired": "Wählen Sie einen Ausschnitt aus" - }, - "paletteHintRun": "Im aktiven Terminal-Tab ausführen", - "host": { - "startupTitle": "Startup-Snippet", - "quickActionsTitle": "Schnellaktionen", - "quickActionsDescription": "Schnellaktionen erscheinen als Schaltflächen in der Host-Metriken-Symbolleiste zur Ausführung von Befehlen mit einem Klick.", - "noQuickActions": "Noch keine Schnellaktionen.", - "buttonLabel": "Schaltflächenbeschriftung", - "selectSnippet": "Snippet auswählen...", - "addQuickAction": "Aktion hinzufügen", - "tabTitle": "Snippets", - "startupDescription": "Wird bei jeder Verbindung zu diesem Host im Terminal ausgeführt. Variablen wie $HOST werden ausgefüllt; ein Codeabschnitt, der nach Eingaben fragt, wird übersprungen.", - "none": "Keine", - "removeQuickAction": "Schnellaktion entfernen" - }, - "quickActions": { - "running": "{{name}} wird ausgeführt...", - "succeeded": "{{name}} erfolgreich abgeschlossen", - "failed": "{{name}} fehlgeschlagen", - "error": "Fehler beim Ausführen von {{name}}" - }, - "paletteHintPaste": "Fügen Sie es im aktiven Terminal-Tab ein.", - "targetTerminals": "Laufen", - "targetTerminalsHint": "optional, ansonsten der aktive Tab", - "selectAll": "Alle", - "selectNone": "Keine", - "plugin": { - "name": "Snippets" - } -} diff --git a/plugins/snippets/locales/translated/el_GR.json b/plugins/snippets/locales/translated/el_GR.json deleted file mode 100644 index fc7cc35fc..000000000 --- a/plugins/snippets/locales/translated/el_GR.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Αποσπάσματα" - }, - "onboarding": { - "feature_snippets": "Αποσπάσματα", - "feature_snippets_desc": "Αποθηκεύστε τις εντολές που εκτελείτε συχνά και στείλτε τις σε ένα τερματικό με ένα κλικ." - }, - "permissions": { - "view": { - "title": "Προβολή αποσπασμάτων", - "description": "Δείτε τα δικά σας αποσπάσματα, σημειώσεις και κοινόχρηστα αποσπάσματα." - }, - "create": { - "title": "Δημιουργία αποσπασμάτων", - "description": "Δημιουργήστε νέα αποσπάσματα, σημειώσεις και φακέλους." - }, - "edit": { - "title": "Επεξεργασία αποσπασμάτων", - "description": "Μετονομάστε ή αλλάξτε υπάρχοντα αποσπάσματα και φακέλους." - }, - "delete": { - "title": "Διαγραφή αποσπασμάτων", - "description": "Διαγραφή αποσπασμάτων και φακέλων." - }, - "share": { - "title": "Κοινή χρήση αποσπασμάτων", - "description": "Κοινή χρήση αποσπασμάτων και φακέλων με άλλους χρήστες ή ρόλους." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Σύμπτυξη φακέλων από προεπιλογή", - "description": "Ξεκινήστε κάθε φάκελο αποσπάσματος συμπτυγμένο όταν ανοίγετε τον πίνακα Αποσπάσματα." - }, - "showCommands": { - "label": "Εμφάνιση εντολών", - "description": "Εμφανίστε την πρώτη γραμμή της εντολής κάθε τμήματος κώδικα κάτω από το όνομά της." - }, - "confirmExecution": { - "label": "Επιβεβαίωση πριν από την εκτέλεση", - "description": "Ερώτηση πριν από την εκτέλεση ενός αποσπάσματος εντολής σε ένα τερματικό." - } - }, - "cancel": "Ματαίωση", - "loading": "Φόρτωση...", - "searchPlaceholder": "Αναζήτηση αποσπασμάτων", - "newFolder": "Νέος φάκελος", - "newSnippet": "Νέο απόσπασμα", - "noFolder": "Δεν υπάρχει φάκελος", - "noSnippetsInFolder": "Δεν υπάρχουν αποσπάσματα σε αυτόν τον φάκελο", - "noTerminalTabsOpen": "Δεν υπάρχει ανοιχτή καρτέλα τερματικού", - "run": "Τρέξιμο", - "pasteToTerminal": "Επικόλληση στο τερματικό", - "copySuccess": "Αντιγράφηκε στο πρόχειρο", - "importExport": "Εισαγωγή / Εξαγωγή", - "importFailed": "Η εισαγωγή αποσπασμάτων απέτυχε", - "nameLabel": "Ονομα", - "namePlaceholder": "Όνομα αποσπάσματος", - "descriptionLabel": "Περιγραφή", - "descriptionPlaceholder": "Προαιρετική περιγραφή", - "folderLabel": "Ντοσιέ", - "typeLabel": "Τύπος", - "typeCommand": "Εντολή", - "typeNote": "Σημείωμα", - "commandLabel": "Εντολή", - "commandPlaceholder": "Εντολή για εκτέλεση, π.χ. systemctl status $HOST", - "noteLabel": "Σημείωμα", - "notePlaceholder": "Κείμενο για επικόλληση στο τερματικό", - "createSnippetTitle": "Δημιουργία αποσπάσματος", - "createSnippetButton": "Δημιουργία αποσπάσματος", - "createSuccess": "Δημιουργήθηκε απόσπασμα", - "createFailed": "Η δημιουργία αποσπάσματος απέτυχε", - "editSnippetTitle": "Επεξεργασία αποσπάσματος", - "saveSnippetButton": "Αποθήκευση αποσπάσματος", - "updateSuccess": "Το απόσπασμα ενημερώθηκε", - "updateFailed": "Η ενημέρωση του αποσπάσματος απέτυχε", - "deleteFailed": "Η διαγραφή του αποσπάσματος απέτυχε", - "createFolderTitle": "Δημιουργία φακέλου", - "createFolderButton": "Δημιουργία φακέλου", - "folderCreateSuccess": "Ο φάκελος δημιουργήθηκε", - "folderCreateFailed": "Η δημιουργία φακέλου απέτυχε", - "editFolderTitle": "Επεξεργασία φακέλου", - "saveFolderButton": "Αποθήκευση φακέλου", - "folderEditSuccess": "Ο φάκελος ενημερώθηκε", - "folderEditFailed": "Η ενημέρωση του φακέλου απέτυχε", - "folderNameLabel": "Όνομα φακέλου", - "folderNamePlaceholder": "Όνομα φακέλου", - "folderIconLabel": "Εικόνισμα", - "folderDeleteSuccess": "Ο φάκελος διαγράφηκε", - "folderDeleteFailed": "Η διαγραφή του φακέλου απέτυχε", - "confirmRunMessage": "Εκτέλεση \"{{name}}\";", - "confirmRunButton": "Τρέξιμο", - "runSuccess": "Εκτέλεση \"{{name}}\" σε {{count}} τερματικό(α)", - "pasteSuccess": "Επικολλήθηκε το \"{{name}}\" σε {{count}} τερματικό(α)", - "admin": { - "tabTitle": "Αποσπάσματα", - "count": "{{count}} αποσπάσματα", - "add": "Προσθήκη αποσπάσματος", - "namePlaceholder": "Όνομα αποσπάσματος", - "contentPlaceholder": "Περιεχόμενο εντολών", - "folderPlaceholder": "Φάκελος (προαιρετικό)", - "empty": "Αυτός ο χρήστης δεν έχει αποσπάσματα", - "deleteConfirm": "Διαγραφή αποσπάσματος \"{{name}}\" που ανήκει στο {{username}};", - "deleted": "Το απόσπασμα διαγράφηκε", - "deleteFailed": "Η διαγραφή του αποσπάσματος απέτυχε", - "requiredFields": "Απαιτούνται το όνομα και το περιεχόμενο του αποσπάσματος", - "saved": "Το απόσπασμα αποθηκεύτηκε", - "saveFailed": "Η αποθήκευση του αποσπάσματος απέτυχε", - "saving": "Οικονομία...", - "save": "Εκτός" - }, - "backToSnippets": "Επιστροφή στα αποσπάσματα", - "settingsTitle": "Ρυθμίσεις αποσπάσματος", - "settingsDisplayTitle": "Επίδειξη", - "settingsBehaviorTitle": "Συμπεριφορά", - "typeCommandHint": "Εκτελείται στο ενεργό τερματικό.", - "typeNoteHint": "Επικολλά στο ενεργό τερματικό χωρίς να εκτελείται.", - "variablesHint": "Μεταβλητές: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Κάντε διπλό κλικ για εκτέλεση", - "sharedBadge": "Κοινόχρηστο", - "copyToClipboard": "Αντιγραφή στο πρόχειρο", - "copyFailed": "Η αντιγραφή στο πρόχειρο απέτυχε", - "moreOptions": "Περισσότερες επιλογές", - "moreActions": "Περισσότερες ενέργειες", - "moveToFolder": "Μετακίνηση σε φάκελο", - "deleteSnippet": "Διαγραφή αποσπάσματος", - "deleteSnippetConfirm": "Διαγραφή αποσπάσματος \"{{name}}\";", - "deleteSuccess": "Το απόσπασμα διαγράφηκε", - "delete": "Διαγράφω", - "folderActions": "Ενέργειες φακέλου", - "addSnippetHere": "Νέο απόσπασμα εδώ", - "deleteFolder": "Διαγραφή φακέλου", - "deleteFolderConfirm": "Διαγραφή φακέλου \"{{name}}\"; Τα αποσπάσματά του μετακινούνται εκτός του φακέλου.", - "createFolderDescription": "Ομαδοποιήστε σχετικά αποσπάσματα.", - "editFolderDescription": "Αλλάξτε το όνομα, το χρώμα ή το εικονίδιο αυτού του φακέλου.", - "folderColorLabel": "Χρώμα", - "loadFailed": "Η φόρτωση αποσπασμάτων απέτυχε", - "importSkipExisting": "Εισαγωγή (παράλειψη υπαρχόντων)", - "importOverwrite": "Εισαγωγή (αντικατάσταση υπαρχόντων)", - "importInvalidFile": "Αυτό το αρχείο δεν αποτελεί έγκυρη εξαγωγή αποσπασμάτων (snippets)", - "importSuccess": "Εισήχθη {{imported}}, ενημερώθηκε {{updated}}, παραλείφθηκε {{skipped}}", - "exportSnippets": "Εξαγωγή αποσπασμάτων", - "exportSuccess": "Αποσπάσματα που εξήχθησαν", - "exportFailed": "Η εξαγωγή αποσπασμάτων απέτυχε", - "expandAll": "Ανάπτυξη όλων των φακέλων", - "collapseAll": "Σύμπτυξη όλων των φακέλων", - "docsLink": "Απόδειξη με έγγραφα", - "clearSearch": "Εκκαθάριση αναζήτησης", - "noSearchResults": "Δεν υπάρχουν αποσπάσματα που να αντιστοιχούν στην αναζήτησή σας", - "emptyTitle": "Δεν υπάρχουν ακόμη αποσπάσματα", - "emptyDescription": "Αποθηκεύστε τις εντολές που εκτελείτε συχνά και, στη συνέχεια, εκτελέστε τες σε ένα τερματικό με ένα κλικ.", - "shareSnippet": "Κοινή χρήση αποσπάσματος", - "shareFolder": "Κοινή χρήση φακέλου", - "shareExpiry": "Λήγει", - "shareExpiryOptions": { - "h0": "Ποτέ", - "h1": "Σε 1 ώρα", - "h24": "Σε 1 ημέρα", - "h168": "Σε 7 ημέρες", - "h720": "Σε 30 ημέρες" - }, - "shareButton": "Μερίδιο", - "shareSuccess": "Κοινοποιημένο με {{count}}", - "shareFailed": "Η κοινοποίηση απέτυχε", - "shareLoadFailed": "Αποτυχία φόρτωσης λεπτομερειών κοινής χρήσης", - "currentAccess": "Τρέχουσα πρόσβαση", - "notSharedYet": "Δεν έχει κοινοποιηθεί ακόμη σε κανέναν", - "revokeAccess": "Ανακαλώ", - "revokeSuccess": "Η πρόσβαση ανακλήθηκε", - "revokeFailed": "Η ανάκληση της πρόσβασης απέτυχε", - "close": "Κοντά", - "targetHostsLabel": "Στοχευμένοι οικοδεσπότες", - "targetHostsHint": "Επιλέξτε κεντρικούς υπολογιστές στους οποίους θα εκτελεστεί αυτή η εντολή απευθείας, χωρίς να είναι ανοιχτό κάποιο τερματικό. Αφήστε το κενό για να την εκτελέσετε στο ενεργό τερματικό.", - "clearTargetHosts": "Σαφής", - "noHostsAvailable": "Δεν υπάρχουν διαθέσιμοι κεντρικοί υπολογιστές", - "searchHosts": "Αναζήτηση κεντρικών υπολογιστών", - "runOnTargets": "Εκτέλεση σε κεντρικούς υπολογιστές-στόχους", - "executionResultTitle": "Αποτελέσματα για \"{{name}}\"", - "executionResultDescription": "Έξοδος από κάθε κεντρικό υπολογιστή-στόχο.", - "executionSuccess": "Επιτυχία", - "executionFailed": "Αποτυχημένος", - "directRunSuccess": "Εκτέλεση \"{{name}}\" σε {{count}} κεντρικό υπολογιστή (ή κεντρικούς υπολογιστές)", - "directRunPartialFail": "Το \"{{name}}\" απέτυχε σε ορισμένους κεντρικούς υπολογιστές", - "reorderFailed": "Η αναδιάταξη των αποσπασμάτων απέτυχε", - "dropToRemoveFromFolder": "Αποθέστε εδώ για να μετακινηθείτε εκτός του φακέλου", - "shareSnippetTitle": "Κοινή χρήση \"{{name}}\"", - "shareFolderTitle": "Κοινόχρηστος φάκελος \"{{name}}\"", - "shareWith": "Κοινοποίηση με", - "usersTab": "Χρήστες", - "rolesTab": "Ρόλοι", - "shareSearchPlaceholder": "Αναζήτηση χρηστών ή ρόλων", - "shareNoMatches": "Δεν υπάρχουν αντιστοιχίσεις", - "shareWithCount": "Κοινοποίηση με {{count}}", - "shareFolderHint": "Κοινοποιεί κάθε απόσπασμα σε αυτόν τον φάκελο. Τα αποσπάσματα που προστίθενται αργότερα δεν κοινοποιούνται αυτόματα.", - "shareFolderSummary": "Κοινόχρηστο {{count}} απόσπασμα(-τα) σε αυτόν τον φάκελο", - "grantedBy": "Χορηγήθηκε από", - "expired": "Λήξη", - "variablesDialogTitle": "Εκτέλεση \"{{name}}\"", - "variablesDialogDescription": "Συμπληρώστε τις τιμές που χρειάζεται αυτό το τμήμα κώδικα πριν το εκτελέσετε.", - "variablesPreviewLabel": "Πρεμιέρα", - "variablesConfirmButton": "Τρέξιμο", - "keybindings": { - "runSnippet": "Εκτέλεση υπάρχοντος αποσπάσματος", - "snippetLabel": "Απόσπασμα", - "selectSnippet": "Επιλέξτε ένα απόσπασμα", - "snippetMissing": "το απόσπασμα δεν βρέθηκε", - "snippetRequired": "Επιλέξτε ένα απόσπασμα" - }, - "paletteHintRun": "Εκτέλεση στην ενεργή καρτέλα τερματικού", - "host": { - "startupTitle": "Απόσπασμα εκκίνησης", - "quickActionsTitle": "Γρήγορες ενέργειες", - "quickActionsDescription": "Οι γρήγορες ενέργειες εμφανίζονται ως κουμπιά στη γραμμή εργαλείων Μετρήσεων κεντρικού υπολογιστή για εκτέλεση εντολών με ένα κλικ.", - "noQuickActions": "Δεν υπάρχουν ακόμη γρήγορες ενέργειες.", - "buttonLabel": "Ετικέτα κουμπιού", - "selectSnippet": "Επιλογή αποσπάσματος...", - "addQuickAction": "Προσθήκη ενέργειας", - "tabTitle": "Αποσπάσματα", - "startupDescription": "Εκτελείται στο τερματικό κάθε φορά που συνδέεται με αυτόν τον κεντρικό υπολογιστή. Μεταβλητές όπως $HOST συμπληρώνονται. Ένα τμήμα κώδικα που ζητά δεδομένα εισόδου παραλείπεται.", - "none": "Κανένας", - "removeQuickAction": "Κατάργηση γρήγορης ενέργειας" - }, - "quickActions": { - "running": "Εκτέλεση {{name}}...", - "succeeded": "{{name}} ολοκληρώθηκε με επιτυχία", - "failed": "{{name}} απέτυχε", - "error": "Αποτυχία εκτέλεσης {{name}}" - }, - "paletteHintPaste": "Επικόλληση στην ενεργή καρτέλα τερματικού", - "targetTerminals": "Τρέξιμο μέσα", - "targetTerminalsHint": "προαιρετικά, διαφορετικά η ενεργή καρτέλα", - "selectAll": "Ολοι", - "selectNone": "Κανένας", - "plugin": { - "name": "Αποσπάσματα" - } -} diff --git a/plugins/snippets/locales/translated/es_ES.json b/plugins/snippets/locales/translated/es_ES.json deleted file mode 100644 index 87676f891..000000000 --- a/plugins/snippets/locales/translated/es_ES.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Fragmentos" - }, - "onboarding": { - "feature_snippets": "Snippets", - "feature_snippets_desc": "Guarda los comandos que ejecutas con frecuencia y envíalos a una terminal con un solo clic." - }, - "permissions": { - "view": { - "title": "Ver fragmentos", - "description": "Consulta tus propios fragmentos, notas y fragmentos compartidos." - }, - "create": { - "title": "Crear fragmentos", - "description": "Crea nuevos fragmentos, notas y carpetas." - }, - "edit": { - "title": "Editar fragmentos", - "description": "Cambiar el nombre o modificar los fragmentos y carpetas existentes." - }, - "delete": { - "title": "Eliminar fragmentos", - "description": "Eliminar fragmentos y carpetas." - }, - "share": { - "title": "Compartir fragmentos", - "description": "Comparta fragmentos y carpetas con otros usuarios o roles." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Contraer carpetas por defecto", - "description": "Al abrir el panel de fragmentos, cada carpeta de fragmentos aparecerá contraída." - }, - "showCommands": { - "label": "Mostrar comandos", - "description": "Muestra la primera línea del comando de cada fragmento debajo de su nombre." - }, - "confirmExecution": { - "label": "Confirmar antes de ejecutar", - "description": "Pregunte antes de ejecutar un fragmento de comando en una terminal." - } - }, - "cancel": "Cancelar", - "loading": "Cargando...", - "searchPlaceholder": "Buscar fragmentos", - "newFolder": "Nueva carpeta", - "newSnippet": "Nuevo fragmento", - "noFolder": "Sin carpeta", - "noSnippetsInFolder": "No hay fragmentos en esta carpeta", - "noTerminalTabsOpen": "No hay ninguna pestaña de terminal abierta.", - "run": "Ejecutar", - "pasteToTerminal": "Pegar en la terminal", - "copySuccess": "Copiado al portapapeles", - "importExport": "Importar / Exportar", - "importFailed": "Error al importar los fragmentos", - "nameLabel": "Nombre", - "namePlaceholder": "Nombre del fragmento", - "descriptionLabel": "Descripción", - "descriptionPlaceholder": "Descripción opcional", - "folderLabel": "Carpeta", - "typeLabel": "Tipo", - "typeCommand": "Comando", - "typeNote": "Nota", - "commandLabel": "Comando", - "commandPlaceholder": "Comando a ejecutar, por ejemplo systemctl status $HOST", - "noteLabel": "Nota", - "notePlaceholder": "Texto para pegar en la terminal", - "createSnippetTitle": "Crear fragmento", - "createSnippetButton": "Crear fragmento", - "createSuccess": "Fragmento creado", - "createFailed": "No se pudo crear el fragmento", - "editSnippetTitle": "Editar fragmento", - "saveSnippetButton": "Guardar fragmento", - "updateSuccess": "Fragmento actualizado", - "updateFailed": "No se pudo actualizar el fragmento", - "deleteFailed": "No se pudo eliminar el fragmento", - "createFolderTitle": "Crear carpeta", - "createFolderButton": "Crear carpeta", - "folderCreateSuccess": "Carpeta creada", - "folderCreateFailed": "No se pudo crear la carpeta", - "editFolderTitle": "Editar carpeta", - "saveFolderButton": "Guardar carpeta", - "folderEditSuccess": "Carpeta actualizada", - "folderEditFailed": "No se pudo actualizar la carpeta", - "folderNameLabel": "Nombre de la carpeta", - "folderNamePlaceholder": "Nombre de la carpeta", - "folderIconLabel": "Icono", - "folderDeleteSuccess": "Carpeta eliminada", - "folderDeleteFailed": "No se pudo eliminar la carpeta", - "confirmRunMessage": "¿Ejecutar \"{{name}}\"?", - "confirmRunButton": "Ejecutar", - "runSuccess": "Se ejecutó \"{{name}}\" en {{count}} terminal(es)", - "pasteSuccess": "Se pegó \"{{name}}\" en {{count}} terminal(es)", - "admin": { - "tabTitle": "Snippets", - "count": "{{count}} fragmentos", - "add": "Añadir fragmento", - "namePlaceholder": "Nombre del fragmento", - "contentPlaceholder": "Contenido del comando", - "folderPlaceholder": "Carpeta (opcional)", - "empty": "Este usuario no tiene fragmentos", - "deleteConfirm": "¿Eliminar fragmento \"{{name}}\" de {{username}}?", - "deleted": "Fragmento eliminado", - "deleteFailed": "No se pudo eliminar el fragmento", - "requiredFields": "El nombre y el contenido del fragmento son obligatorios", - "saved": "Fragmento guardado", - "saveFailed": "Error al guardar fragmento", - "saving": "Guardando...", - "save": "Guardar" - }, - "backToSnippets": "Volver a los fragmentos", - "settingsTitle": "Configuración del fragmento", - "settingsDisplayTitle": "Mostrar", - "settingsBehaviorTitle": "Comportamiento", - "typeCommandHint": "Se ejecuta en la terminal activa.", - "typeNoteHint": "Se pega en la terminal activa sin ejecutarse.", - "variablesHint": "Variables: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Haz doble clic para ejecutar", - "sharedBadge": "Compartido", - "copyToClipboard": "Copiar al portapapeles", - "copyFailed": "Error al copiar al portapapeles", - "moreOptions": "Más opciones", - "moreActions": "Más acciones", - "moveToFolder": "Mover a carpeta", - "deleteSnippet": "Eliminar fragmento", - "deleteSnippetConfirm": "¿Eliminar fragmento \"{{name}}\"?", - "deleteSuccess": "Fragmento eliminado", - "delete": "Eliminar", - "folderActions": "Acciones de carpeta", - "addSnippetHere": "Nuevo fragmento aquí", - "deleteFolder": "Eliminar carpeta", - "deleteFolderConfirm": "¿Eliminar la carpeta \"{{name}}\"? Sus fragmentos se moverán fuera de la carpeta.", - "createFolderDescription": "Agrupa los fragmentos relacionados.", - "editFolderDescription": "Cambia el nombre, el color o el icono de esta carpeta.", - "folderColorLabel": "Color", - "loadFailed": "No se pudieron cargar los fragmentos.", - "importSkipExisting": "Importar (omitir existentes)", - "importOverwrite": "Importar (sobrescribir existente)", - "importInvalidFile": "Ese archivo no es una exportación de fragmentos válida.", - "importSuccess": "Importado {{imported}}, actualizado {{updated}}, omitido {{skipped}}", - "exportSnippets": "Exportar fragmentos", - "exportSuccess": "Fragmentos exportados", - "exportFailed": "Error al exportar los fragmentos", - "expandAll": "Expandir todas las carpetas", - "collapseAll": "Contraer todas las carpetas", - "docsLink": "Documentación", - "clearSearch": "Borrar búsqueda", - "noSearchResults": "No se encontraron fragmentos que coincidan con su búsqueda.", - "emptyTitle": "Aún no hay fragmentos", - "emptyDescription": "Guarda los comandos que uses con frecuencia y ejecútalos en la terminal con un solo clic.", - "shareSnippet": "Compartir fragmento", - "shareFolder": "Carpeta compartida", - "shareExpiry": "Expira", - "shareExpiryOptions": { - "h0": "Nunca", - "h1": "En 1 hora", - "h24": "En 1 día", - "h168": "En 7 días", - "h720": "En 30 días" - }, - "shareButton": "Compartir", - "shareSuccess": "Compartido con {{count}}", - "shareFailed": "No se pudo compartir", - "shareLoadFailed": "No se pudieron cargar los detalles de uso compartido.", - "currentAccess": "Acceso actual", - "notSharedYet": "Aún no lo he compartido con nadie.", - "revokeAccess": "Revocar", - "revokeSuccess": "Acceso revocado", - "revokeFailed": "Error al revocar el acceso", - "close": "Cerrar", - "targetHostsLabel": "Anfitriones objetivo", - "targetHostsHint": "Seleccione los hosts en los que desea ejecutar este comando directamente, sin tener una terminal abierta. Deje el campo vacío para ejecutarlo en la terminal activa.", - "clearTargetHosts": "Limpiar", - "noHostsAvailable": "No hay anfitriones disponibles", - "searchHosts": "Buscar hosts", - "runOnTargets": "Ejecutar en los hosts de destino", - "executionResultTitle": "Resultados para \"{{name}}\"", - "executionResultDescription": "Salida de cada host de destino.", - "executionSuccess": "Éxito", - "executionFailed": "Fallido", - "directRunSuccess": "Se ejecutó \"{{name}}\" en {{count}} host(s)", - "directRunPartialFail": "\"{{name}}\" falló en algunos hosts", - "reorderFailed": "No se pudieron reordenar los fragmentos.", - "dropToRemoveFromFolder": "Arrastra aquí para salir de la carpeta.", - "shareSnippetTitle": "Compartir \"{{name}}\"", - "shareFolderTitle": "Compartir carpeta \"{{name}}\"", - "shareWith": "Compartir con", - "usersTab": "Usuarios", - "rolesTab": "Roles", - "shareSearchPlaceholder": "Buscar usuarios o roles", - "shareNoMatches": "No hay coincidencias", - "shareWithCount": "Compartir con {{count}}", - "shareFolderHint": "Comparte todos los fragmentos de esta carpeta. Los fragmentos añadidos posteriormente no se comparten automáticamente.", - "shareFolderSummary": "Fragmento(s) compartido(s) {{count}} en esta carpeta", - "grantedBy": "Concedido por", - "expired": "Caducado", - "variablesDialogTitle": "Ejecutar \"{{name}}\"", - "variablesDialogDescription": "Rellena los valores que necesita este fragmento de código antes de ejecutarlo.", - "variablesPreviewLabel": "Vista previa", - "variablesConfirmButton": "Ejecutar", - "keybindings": { - "runSnippet": "Ejecutar el fragmento existente", - "snippetLabel": "Retazo", - "selectSnippet": "Seleccione un fragmento", - "snippetMissing": "Fragmento no encontrado", - "snippetRequired": "Seleccione un fragmento" - }, - "paletteHintRun": "Ejecutar en la pestaña de terminal activa", - "host": { - "startupTitle": "Fragmento de inicio", - "quickActionsTitle": "Acciones rápidas", - "quickActionsDescription": "Las acciones rápidas aparecen como botones en la barra de herramientas de Métricas del host para la ejecución de comandos con un solo clic.", - "noQuickActions": "Aún no hay acciones rápidas.", - "buttonLabel": "Etiqueta del botón", - "selectSnippet": "Seleccionar fragmento...", - "addQuickAction": "Añadir acción", - "tabTitle": "Snippets", - "startupDescription": "Se ejecuta en la terminal cada vez que se conecta a este host. Se rellenan variables como $HOST ; se omite un fragmento que solicita entradas.", - "none": "Ninguna", - "removeQuickAction": "Eliminar acción rápida" - }, - "quickActions": { - "running": "Ejecutando {{name}}...", - "succeeded": "{{name}} completado correctamente", - "failed": "{{name}} falló", - "error": "Error al ejecutar {{name}}" - }, - "paletteHintPaste": "Pegar en la pestaña del terminal activo", - "targetTerminals": "Rodar", - "targetTerminalsHint": "opcional, de lo contrario la pestaña activa", - "selectAll": "Revocar todo", - "selectNone": "Ninguna", - "plugin": { - "name": "Snippets" - } -} diff --git a/plugins/snippets/locales/translated/fi_FI.json b/plugins/snippets/locales/translated/fi_FI.json deleted file mode 100644 index e6f0b2450..000000000 --- a/plugins/snippets/locales/translated/fi_FI.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Katkelmat" - }, - "onboarding": { - "feature_snippets": "Katkelmat", - "feature_snippets_desc": "Tallenna usein käyttämäsi komennot ja lähetä ne terminaaliin yhdellä napsautuksella." - }, - "permissions": { - "view": { - "title": "Näytä katkelmat", - "description": "Katso omia katkelmiasi, muistiinpanojasi ja jaettuja katkelmiasi." - }, - "create": { - "title": "Luo katkelmia", - "description": "Luo uusia katkelmia, muistiinpanoja ja kansioita." - }, - "edit": { - "title": "Muokkaa katkelmia", - "description": "Nimeä uudelleen tai muuta olemassa olevia katkelmia ja kansioita." - }, - "delete": { - "title": "Poista katkelmia", - "description": "Poista katkelmia ja kansioita." - }, - "share": { - "title": "Jaa katkelmia", - "description": "Jaa katkelmia ja kansioita muiden käyttäjien tai roolien kanssa." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Pienennä kansiot oletusarvoisesti", - "description": "Aloita jokainen katkelmakansio supistettuna, kun avaat Katkelmat-paneelin." - }, - "showCommands": { - "label": "Näytä komennot", - "description": "Näytä kunkin koodinpätkän komennon ensimmäinen rivi sen nimen alla." - }, - "confirmExecution": { - "label": "Vahvista ennen suorittamista", - "description": "Kysy ennen komennonpätkän suorittamista terminaalissa." - } - }, - "cancel": "Peruuttaa", - "loading": "Ladataan...", - "searchPlaceholder": "Hakukatkelmia", - "newFolder": "Uusi kansio", - "newSnippet": "Uusi katkelma", - "noFolder": "Ei kansiota", - "noSnippetsInFolder": "Tässä kansiossa ei ole katkelmia", - "noTerminalTabsOpen": "Yhtäkään päätevälilehteä ei ole auki", - "run": "Juokse", - "pasteToTerminal": "Liitä terminaaliin", - "copySuccess": "Kopioitu leikepöydälle", - "importExport": "Tuonti / vienti", - "importFailed": "Katkelmien tuonti epäonnistui", - "nameLabel": "Nimi", - "namePlaceholder": "Katkelman nimi", - "descriptionLabel": "Kuvaus", - "descriptionPlaceholder": "Valinnainen kuvaus", - "folderLabel": "Kansio", - "typeLabel": "Tyyppi", - "typeCommand": "Komento", - "typeNote": "Huomautus", - "commandLabel": "Komento", - "commandPlaceholder": "Suoritettava komento, esim. systemctl status $HOST", - "noteLabel": "Huomautus", - "notePlaceholder": "Päätelaitteeseen liitettävä teksti", - "createSnippetTitle": "Luo koodinpätkä", - "createSnippetButton": "Luo koodinpätkä", - "createSuccess": "Katkelma luotu", - "createFailed": "Katkelman luominen epäonnistui", - "editSnippetTitle": "Muokkaa katkelmaa", - "saveSnippetButton": "Tallenna koodinpätkä", - "updateSuccess": "Katkelma päivitetty", - "updateFailed": "Katkelman päivittäminen epäonnistui", - "deleteFailed": "Katkelman poistaminen epäonnistui", - "createFolderTitle": "Luo kansio", - "createFolderButton": "Luo kansio", - "folderCreateSuccess": "Kansio luotu", - "folderCreateFailed": "Kansion luominen epäonnistui", - "editFolderTitle": "Muokkaa kansiota", - "saveFolderButton": "Tallenna kansio", - "folderEditSuccess": "Kansio päivitetty", - "folderEditFailed": "Kansion päivittäminen epäonnistui", - "folderNameLabel": "Kansion nimi", - "folderNamePlaceholder": "Kansion nimi", - "folderIconLabel": "Kuvake", - "folderDeleteSuccess": "Kansio poistettu", - "folderDeleteFailed": "Kansion poistaminen epäonnistui", - "confirmRunMessage": "Suorittaa \"{{name}}\"?", - "confirmRunButton": "Juokse", - "runSuccess": "Suoritti komennon \"{{name}}\" {{count}} päätteessä/päätteissä", - "pasteSuccess": "Liitti \"{{name}}\" {{count}} terminaaliin/terminaaleihin", - "admin": { - "tabTitle": "Katkelmat", - "count": "{{count}} katkelmia", - "add": "Lisää katkelma", - "namePlaceholder": "Katkelman nimi", - "contentPlaceholder": "Komennon sisältö", - "folderPlaceholder": "Kansio (valinnainen)", - "empty": "Tällä käyttäjällä ei ole katkelmia", - "deleteConfirm": "Poistetaanko koodinpätkä \"{{name}}\", joka kuuluu käyttäjälle {{username}}?", - "deleted": "Katkelma poistettu", - "deleteFailed": "Katkelman poistaminen epäonnistui", - "requiredFields": "Katkelman nimi ja sisältö vaaditaan", - "saved": "Katkelma tallennettu", - "saveFailed": "Katkelman tallentaminen epäonnistui", - "saving": "Tallennetaan...", - "save": "Tallentaa" - }, - "backToSnippets": "Takaisin katkelmiin", - "settingsTitle": "Katkelman asetukset", - "settingsDisplayTitle": "Näyttö", - "settingsBehaviorTitle": "Käyttäytyminen", - "typeCommandHint": "Suoritetaan aktiivisessa terminaalissa.", - "typeNoteHint": "Liittää aktiiviseen terminaaliin suorittamatta sitä.", - "variablesHint": "Muuttujat: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Kaksoisnapsauta suorittaaksesi", - "sharedBadge": "Jaettu", - "copyToClipboard": "Kopioi leikepöydälle", - "copyFailed": "Leikepöydälle kopiointi epäonnistui", - "moreOptions": "Lisää vaihtoehtoja", - "moreActions": "Lisää toimintoja", - "moveToFolder": "Siirrä kansioon", - "deleteSnippet": "Poista katkelma", - "deleteSnippetConfirm": "Poistetaanko koodinpätkä \"{{name}}\"?", - "deleteSuccess": "Katkelma poistettu", - "delete": "Poistaa", - "folderActions": "Kansiotoiminnot", - "addSnippetHere": "Uusi pätkä tässä", - "deleteFolder": "Poista kansio", - "deleteFolderConfirm": "Poistetaanko kansio \"{{name}}\"? Sen koodinpätkät siirretään pois kansiosta.", - "createFolderDescription": "Ryhmittele toisiinsa liittyvät katkelmat yhteen.", - "editFolderDescription": "Muuta tämän kansion nimeä, väriä tai kuvaketta.", - "folderColorLabel": "Väri", - "loadFailed": "Katkelmien lataaminen epäonnistui", - "importSkipExisting": "Tuo (ohita olemassa oleva)", - "importOverwrite": "Tuo (korvaa olemassa oleva)", - "importInvalidFile": "Tuo tiedosto ei ole kelvollinen katkelmien vienti.", - "importSuccess": "Tuotu {{imported}}, päivitetty {{updated}}, ohitettu {{skipped}}", - "exportSnippets": "Vie koodinpätkiä", - "exportSuccess": "Katkelmat viety", - "exportFailed": "Katkelmien vienti epäonnistui", - "expandAll": "Laajenna kaikki kansiot", - "collapseAll": "Pienennä kaikki kansiot", - "docsLink": "Dokumentaatio", - "clearSearch": "Tyhjennä haku", - "noSearchResults": "Hakuasi vastaavia katkelmia ei löytynyt.", - "emptyTitle": "Ei vielä katkelmia", - "emptyDescription": "Tallenna usein käyttämäsi komennot ja suorita ne sitten terminaalissa yhdellä napsautuksella.", - "shareSnippet": "Jaa katkelma", - "shareFolder": "Jaa kansio", - "shareExpiry": "Vanhenee", - "shareExpiryOptions": { - "h0": "Ei koskaan", - "h1": "Tunnin kuluttua", - "h24": "1 päivän kuluttua", - "h168": "7 päivän kuluttua", - "h720": "30 päivän kuluttua" - }, - "shareButton": "Jakaa", - "shareSuccess": "Jaettu käyttäjän {{count}} kanssa", - "shareFailed": "Jakaminen epäonnistui", - "shareLoadFailed": "Jakamistietojen lataaminen epäonnistui", - "currentAccess": "Nykyinen käyttöoikeus", - "notSharedYet": "Ei vielä jaettu kenenkään kanssa", - "revokeAccess": "Peruuttaa", - "revokeSuccess": "Pääsy peruutettu", - "revokeFailed": "Käyttöoikeuden peruuttaminen epäonnistui", - "close": "Lähellä", - "targetHostsLabel": "Kohdeisännät", - "targetHostsHint": "Valitse isännät, joille tämä komento suoritetaan suoraan ilman avointa päätettä. Jätä tyhjäksi, jos haluat sen suoritettavan aktiivisessa päätelaitteessa.", - "clearTargetHosts": "Tyhjennä", - "noHostsAvailable": "Ei isäntiä saatavilla", - "searchHosts": "Hae isäntiä", - "runOnTargets": "Suorita kohdekoneissa", - "executionResultTitle": "Tulokset haulle \"{{name}}\"", - "executionResultDescription": "Kunkin kohdeisännän tuloste.", - "executionSuccess": "Menestys", - "executionFailed": "Epäonnistui", - "directRunSuccess": "Suoritti komennon \"{{name}}\" {{count}} isäntäkoneella/isännillä", - "directRunPartialFail": "\"{{name}}\" epäonnistui joillakin isännillä", - "reorderFailed": "Katkelmien uudelleenjärjestely epäonnistui", - "dropToRemoveFromFolder": "Pudota tähän siirtääksesi pois kansiosta", - "shareSnippetTitle": "Jaa \"{{name}}\"", - "shareFolderTitle": "Jaa kansio \"{{name}}\"", - "shareWith": "Jaa kanssa", - "usersTab": "Käyttäjät", - "rolesTab": "Roolit", - "shareSearchPlaceholder": "Hae käyttäjiä tai rooleja", - "shareNoMatches": "Ei osumia", - "shareWithCount": "Jaa käyttäjän {{count}} kanssa", - "shareFolderHint": "Jakaa kaikki tässä kansiossa olevat katkelmat. Myöhemmin lisättyjä katkelmia ei jaeta automaattisesti.", - "shareFolderSummary": "Jaettu(a) {{count}} katkelma(a) tässä kansiossa", - "grantedBy": "Myöntänyt", - "expired": "Vanhentunut", - "variablesDialogTitle": "Suorita \"{{name}}\"", - "variablesDialogDescription": "Täytä tämän koodinpätkän tarvitsemat arvot ennen sen suorittamista.", - "variablesPreviewLabel": "Esikatselu", - "variablesConfirmButton": "Juokse", - "keybindings": { - "runSnippet": "Suorita olemassa oleva koodinpätkä", - "snippetLabel": "Katkelma", - "selectSnippet": "Valitse katkelma", - "snippetMissing": "katkelmaa ei löytynyt", - "snippetRequired": "Valitse katkelma" - }, - "paletteHintRun": "Suorita aktiivisessa päätevälilehdessä", - "host": { - "startupTitle": "Käynnistyskoodinpätkä", - "quickActionsTitle": "Pikatoiminnot", - "quickActionsDescription": "Pikatoiminnot näkyvät painikkeina isäntämetriikkatyökalurivillä komentojen suorittamista varten yhdellä napsautuksella.", - "noQuickActions": "Ei vielä nopeita toimia.", - "buttonLabel": "Painikkeen nimi", - "selectSnippet": "Valitse katkelma...", - "addQuickAction": "Lisää toiminto", - "tabTitle": "Katkelmat", - "startupDescription": "Suoritetaan terminaalissa joka kerta, kun se muodostaa yhteyden tähän isäntään. Muuttujat, kuten $HOST , täytetään; syötteitä pyytävä koodinpätkä ohitetaan.", - "none": "Ei mitään", - "removeQuickAction": "Poista pikatoiminto" - }, - "quickActions": { - "running": "Suoritetaan {{name}}...", - "succeeded": "{{name}} suoritettu onnistuneesti", - "failed": "{{name}} epäonnistui", - "error": "Suoritus epäonnistui {{name}}" - }, - "paletteHintPaste": "Liitä aktiiviseen päätevälilehteen", - "targetTerminals": "Aja sisään", - "targetTerminalsHint": "valinnainen, muuten aktiivinen välilehti", - "selectAll": "Kaikki", - "selectNone": "Ei mitään", - "plugin": { - "name": "Katkelmat" - } -} diff --git a/plugins/snippets/locales/translated/fr_FR.json b/plugins/snippets/locales/translated/fr_FR.json deleted file mode 100644 index b8d339c50..000000000 --- a/plugins/snippets/locales/translated/fr_FR.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Extraits" - }, - "onboarding": { - "feature_snippets": "Snippets", - "feature_snippets_desc": "Enregistrez les commandes que vous exécutez fréquemment et envoyez-les à un terminal en un seul clic." - }, - "permissions": { - "view": { - "title": "Voir des extraits", - "description": "Consultez vos propres extraits, notes et extraits partagés." - }, - "create": { - "title": "Créer des extraits", - "description": "Créez de nouveaux extraits, notes et dossiers." - }, - "edit": { - "title": "Modifier des extraits", - "description": "Renommer ou modifier les extraits et dossiers existants." - }, - "delete": { - "title": "Supprimer des extraits", - "description": "Supprimer les extraits et les dossiers." - }, - "share": { - "title": "Extraits de partage", - "description": "Partagez des extraits de code et des dossiers avec d'autres utilisateurs ou rôles." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Réduire les dossiers par défaut", - "description": "Chaque dossier d'extraits de code doit être réduit au démarrage lorsque vous ouvrez le panneau Extraits de code." - }, - "showCommands": { - "label": "Afficher les commandes", - "description": "Afficher la première ligne de la commande de chaque extrait sous son nom." - }, - "confirmExecution": { - "label": "Veuillez confirmer avant d'exécuter", - "description": "Demandez la permission avant d'exécuter un extrait de commande dans un terminal." - } - }, - "cancel": "Annuler", - "loading": "Chargement...", - "searchPlaceholder": "Rechercher des snippets", - "newFolder": "Nouveau dossier", - "newSnippet": "Nouvel extrait", - "noFolder": "Aucun dossier", - "noSnippetsInFolder": "Aucun snippet dans ce dossier", - "noTerminalTabsOpen": "Aucun onglet de terminal n'est ouvert.", - "run": "Exécuter", - "pasteToTerminal": "Coller dans le terminal", - "copySuccess": "Copié dans le presse-papiers.", - "importExport": "Importer / Exporter", - "importFailed": "Échec de l’importation des snippets", - "nameLabel": "Nom", - "namePlaceholder": "Nom du snippet", - "descriptionLabel": "Description", - "descriptionPlaceholder": "Description facultative", - "folderLabel": "Dossier", - "typeLabel": "Type", - "typeCommand": "Commande", - "typeNote": "Note", - "commandLabel": "Commande", - "commandPlaceholder": "Commande à exécuter, par exemple systemctl status $HOST", - "noteLabel": "Note", - "notePlaceholder": "Texte à coller dans le terminal", - "createSnippetTitle": "Créer un extrait", - "createSnippetButton": "Créer un extrait", - "createSuccess": "Extrait créé", - "createFailed": "Échec de la création du snippet", - "editSnippetTitle": "Modifier l'extrait", - "saveSnippetButton": "Enregistrer l'extrait", - "updateSuccess": "Extrait mis à jour", - "updateFailed": "Échec de la mise à jour du snippet", - "deleteFailed": "Échec de la suppression du snippet", - "createFolderTitle": "Créer un dossier", - "createFolderButton": "Créer un dossier", - "folderCreateSuccess": "Dossier créé", - "folderCreateFailed": "Échec de la création du dossier", - "editFolderTitle": "Modifier le dossier", - "saveFolderButton": "Enregistrer le dossier", - "folderEditSuccess": "Dossier mis à jour", - "folderEditFailed": "Échec de la mise à jour du dossier", - "folderNameLabel": "Nom du dossier", - "folderNamePlaceholder": "Nom du dossier", - "folderIconLabel": "Icône", - "folderDeleteSuccess": "Dossier supprimé", - "folderDeleteFailed": "Échec de la suppression du dossier", - "confirmRunMessage": "Exécuter « {{name}} » ?", - "confirmRunButton": "Exécuter", - "runSuccess": "Exécution de «{{name}}» sur le(s) terminal(s) {{count}}", - "pasteSuccess": "Collé \"{{name}}\" dans le(s) terminal(s) {{count}}", - "admin": { - "tabTitle": "Snippets", - "count": "{{count}} snippets", - "add": "Ajouter un snippet", - "namePlaceholder": "Nom du snippet", - "contentPlaceholder": "Contenu de la commande", - "folderPlaceholder": "Dossier (optionnel)", - "empty": "Cet utilisateur n'a aucun snippet", - "deleteConfirm": "Supprimer le snippet \"{{name}}\" appartenant à {{username}} ?", - "deleted": "Snippet supprimé", - "deleteFailed": "Échec de la suppression du snippet", - "requiredFields": "Le nom et le contenu du snippet sont requis", - "saved": "Snippet enregistré", - "saveFailed": "Échec de l'enregistrement du snippet", - "saving": "Enregistrement…", - "save": "Enregistrer" - }, - "backToSnippets": "Retour aux extraits", - "settingsTitle": "Paramètres de l'extrait", - "settingsDisplayTitle": "Afficher", - "settingsBehaviorTitle": "Comportement", - "typeCommandHint": "S'exécute dans le terminal actif.", - "typeNoteHint": "Coller dans le terminal actif sans exécuter de commande.", - "variablesHint": "Variables : $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Double-cliquez pour exécuter", - "sharedBadge": "Partagé", - "copyToClipboard": "Copier dans le presse-papiers", - "copyFailed": "Échec de la copie dans le presse-papiers.", - "moreOptions": "Plus d'options", - "moreActions": "Plus d'actions", - "moveToFolder": "Déplacer vers le dossier", - "deleteSnippet": "Supprimer l'extrait", - "deleteSnippetConfirm": "Supprimer l'extrait \"{{name}}\"?", - "deleteSuccess": "Snippet supprimé", - "delete": "Supprimer", - "folderActions": "Actions sur le dossier", - "addSnippetHere": "Nouvel extrait ici", - "deleteFolder": "Supprimer le dossier", - "deleteFolderConfirm": "Supprimer le dossier «{{name}}» ? Ses extraits seront déplacés hors du dossier.", - "createFolderDescription": "Regroupez les extraits similaires.", - "editFolderDescription": "Modifiez le nom, la couleur ou l'icône de ce dossier.", - "folderColorLabel": "Couleur", - "loadFailed": "Échec du chargement des extraits", - "importSkipExisting": "Importer (ignorer existants)", - "importOverwrite": "Importer (écraser l'existant)", - "importInvalidFile": "Ce fichier n'est pas une exportation de fragments valide", - "importSuccess": "Importé {{imported}}, mis à jour {{updated}}, ignoré {{skipped}}", - "exportSnippets": "Extraits d'exportation", - "exportSuccess": "Extraits exportés", - "exportFailed": "Échec de l’exportation des snippets", - "expandAll": "Développer tous les dossiers", - "collapseAll": "Réduire tous les dossiers", - "docsLink": "Documentation", - "clearSearch": "Recherche propre", - "noSearchResults": "Aucun extrait ne correspond à votre recherche", - "emptyTitle": "Aucun extrait pour l'instant", - "emptyDescription": "Enregistrez les commandes que vous exécutez fréquemment, puis exécutez-les dans un terminal en un seul clic.", - "shareSnippet": "Extrait de partage", - "shareFolder": "Dossier partagé", - "shareExpiry": "Expiration", - "shareExpiryOptions": { - "h0": "Jamais", - "h1": "Dans 1 heure", - "h24": "En 1 jour", - "h168": "Dans 7 jours", - "h720": "En 30 jours" - }, - "shareButton": "Partager", - "shareSuccess": "Partagé avec {{count}}", - "shareFailed": "Échec du partage", - "shareLoadFailed": "Impossible de charger les détails de partage", - "currentAccess": "Accès actuel", - "notSharedYet": "Pas encore partagé avec qui que ce soit", - "revokeAccess": "Révoquer", - "revokeSuccess": "Accès révoqué", - "revokeFailed": "Échec de la révocation de l’accès", - "close": "Fermer", - "targetHostsLabel": "hôtes cibles", - "targetHostsHint": "Sélectionnez les hôtes sur lesquels exécuter directement cette commande, sans terminal ouvert. Laissez ce champ vide pour l'exécuter dans le terminal actif.", - "clearTargetHosts": "Effacer", - "noHostsAvailable": "Aucun hôte disponible", - "searchHosts": "Rechercher des hôtes", - "runOnTargets": "Exécuter sur les hôtes cibles", - "executionResultTitle": "Résultats pour \"{{name}}\"", - "executionResultDescription": "Sortie de chaque hôte cible.", - "executionSuccess": "Succès", - "executionFailed": "Échec", - "directRunSuccess": "« {{name}} » exécuté sur {{count}} hôte(s)", - "directRunPartialFail": "\"{{name}}\" a échoué sur certains hôtes", - "reorderFailed": "Impossible de réorganiser les extraits", - "dropToRemoveFromFolder": "Déposez ici pour sortir du dossier", - "shareSnippetTitle": "Partager \"{{name}}\"", - "shareFolderTitle": "Dossier partagé \"{{name}}\"", - "shareWith": "Partager avec", - "usersTab": "Utilisateurs", - "rolesTab": "Rôles", - "shareSearchPlaceholder": "Rechercher des utilisateurs ou des rôles", - "shareNoMatches": "Aucun résultat", - "shareWithCount": "Partager avec {{count}}", - "shareFolderHint": "Partage tous les extraits de code contenus dans ce dossier. Les extraits ajoutés ultérieurement ne sont pas partagés automatiquement.", - "shareFolderSummary": "Extrait(s) partagé(s) {{count}} dans ce dossier", - "grantedBy": "Accordé par", - "expired": "Expiré", - "variablesDialogTitle": "Exécutez \"{{name}}\"", - "variablesDialogDescription": "Renseignez les valeurs nécessaires à cet extrait de code avant de l'exécuter.", - "variablesPreviewLabel": "Aperçu", - "variablesConfirmButton": "Exécuter", - "keybindings": { - "runSnippet": "Exécuter l'extrait de code existant", - "snippetLabel": "Fragment", - "selectSnippet": "Sélectionnez un extrait", - "snippetMissing": "extrait introuvable", - "snippetRequired": "Sélectionnez un extrait" - }, - "paletteHintRun": "Exécuter dans l'onglet du terminal actif", - "host": { - "startupTitle": "Extrait de démarrage", - "quickActionsTitle": "Actions rapides", - "quickActionsDescription": "Les actions rapides apparaissent sous forme de boutons dans la barre d'outils Métriques de l'hôte pour une exécution en un clic.", - "noQuickActions": "Aucune action rapide pour le moment.", - "buttonLabel": "Étiquette du bouton", - "selectSnippet": "Sélectionner un snippet...", - "addQuickAction": "Ajouter une action", - "tabTitle": "Snippets", - "startupDescription": "S'exécute dans le terminal à chaque connexion à cet hôte. Les variables telles que $HOST sont renseignées ; la partie du code demandant des entrées est ignorée.", - "none": "Aucune", - "removeQuickAction": "Supprimer l'action rapide" - }, - "quickActions": { - "running": "Exécution de {{name}}...", - "succeeded": "{{name}} terminé avec succès", - "failed": "Échec de {{name}}", - "error": "Échec de l'exécution de {{name}}" - }, - "paletteHintPaste": "Collez dans l'onglet du terminal actif", - "targetTerminals": "Courir", - "targetTerminalsHint": "facultatif, sinon l'onglet actif", - "selectAll": "Tout", - "selectNone": "Aucune", - "plugin": { - "name": "Snippets" - } -} diff --git a/plugins/snippets/locales/translated/he_IL.json b/plugins/snippets/locales/translated/he_IL.json deleted file mode 100644 index 42ee34135..000000000 --- a/plugins/snippets/locales/translated/he_IL.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "קטעי טקסט" - }, - "onboarding": { - "feature_snippets": "קטעי טקסט", - "feature_snippets_desc": "שמור פקודות שאתה מפעיל לעתים קרובות ושלח אותן למסוף בלחיצה אחת." - }, - "permissions": { - "view": { - "title": "הצג קטעי וידאו", - "description": "ראה קטעי וידאו, הערות וקטעי וידאו משותפים שלך." - }, - "create": { - "title": "צור קטעי טקסט", - "description": "צור קטעי קוד, הערות ותיקיות חדשים." - }, - "edit": { - "title": "עריכת קטעי טקסט", - "description": "שינוי שם או שינוי של קטעי טקסט ותיקיות קיימים." - }, - "delete": { - "title": "מחיקת קטעי טקסט", - "description": "מחיקת קטעי קוד ותיקיות." - }, - "share": { - "title": "שתף קטעי טקסט", - "description": "שתף קטעי קוד ותיקיות עם משתמשים או תפקידים אחרים." - } - }, - "settings": { - "foldersCollapsed": { - "label": "כיווץ תיקיות כברירת מחדל", - "description": "התחל כל תיקיית קטעי טקסט מקומטת בעת פתיחת החלונית 'קטעי טקסט'." - }, - "showCommands": { - "label": "הצג פקודות", - "description": "הצג את השורה הראשונה של כל פקודה של קטע תחת שמו." - }, - "confirmExecution": { - "label": "אשר לפני הפעלה", - "description": "שאל לפני שקטע פקודה מופעל בטרמינל." - } - }, - "cancel": "לְבַטֵל", - "loading": "טְעִינָה...", - "searchPlaceholder": "קטעי חיפוש", - "newFolder": "תיקייה חדשה", - "newSnippet": "קטע חדש", - "noFolder": "אין תיקייה", - "noSnippetsInFolder": "אין קטעי טקסט בתיקייה זו", - "noTerminalTabsOpen": "אין לשונית מסוף פתוחה", - "run": "לָרוּץ", - "pasteToTerminal": "הדבקה לטרמינל", - "copySuccess": "הועתק ללוח", - "importExport": "ייבוא / ייצוא", - "importFailed": "ייבוא קטעי הקוד נכשל", - "nameLabel": "שֵׁם", - "namePlaceholder": "שם הקטע", - "descriptionLabel": "תֵאוּר", - "descriptionPlaceholder": "תיאור אופציונלי", - "folderLabel": "תיקייה", - "typeLabel": "סוּג", - "typeCommand": "פְּקוּדָה", - "typeNote": "פֶּתֶק", - "commandLabel": "פְּקוּדָה", - "commandPlaceholder": "פקודה להפעלה, לדוגמה systemctl status $HOST", - "noteLabel": "פֶּתֶק", - "notePlaceholder": "טקסט להדבקה בטרמינל", - "createSnippetTitle": "צור קטע", - "createSnippetButton": "צור קטע", - "createSuccess": "קטע נוצר", - "createFailed": "נכשלה יצירת קטע הקוד", - "editSnippetTitle": "עריכת קטע", - "saveSnippetButton": "שמור קטע", - "updateSuccess": "קטע הטקסט עודכן", - "updateFailed": "נכשל עדכון הקטע", - "deleteFailed": "מחיקת קטע הקוד נכשלה", - "createFolderTitle": "צור תיקייה", - "createFolderButton": "צור תיקייה", - "folderCreateSuccess": "תיקייה נוצרה", - "folderCreateFailed": "יצירת התיקייה נכשלה", - "editFolderTitle": "עריכת תיקייה", - "saveFolderButton": "שמור תיקייה", - "folderEditSuccess": "התיקייה עודכנה", - "folderEditFailed": "עדכון התיקייה נכשל", - "folderNameLabel": "שם התיקייה", - "folderNamePlaceholder": "שם התיקייה", - "folderIconLabel": "אייקון", - "folderDeleteSuccess": "תיקייה נמחקה", - "folderDeleteFailed": "מחיקת התיקייה נכשלה", - "confirmRunMessage": "להפעיל את \"{{name}}\"?", - "confirmRunButton": "לָרוּץ", - "runSuccess": "הריץ את \"{{name}}\" על הטרמינל/ים {{count}}", - "pasteSuccess": "הודבק \"{{name}}\" לתוך הטרמינל/ים {{count}}", - "admin": { - "tabTitle": "קטעי טקסט", - "count": "קטעי טקסט {{count}}", - "add": "הוסף קטע", - "namePlaceholder": "שם קטע", - "contentPlaceholder": "תוכן הפקודה", - "folderPlaceholder": "תיקייה (אופציונלי)", - "empty": "למשתמש זה אין קטעי טקסט", - "deleteConfirm": "למחוק את קטע הקוד \"{{name}}\" השייך ל- {{username}}?", - "deleted": "קטע הטקסט נמחק", - "deleteFailed": "מחיקת קטע הקוד נכשלה", - "requiredFields": "שם ותוכן הקטע נדרשים", - "saved": "קטע הטקסט נשמר", - "saveFailed": "שמירת קטע הקוד נכשלה", - "saving": "חִסָכוֹן...", - "save": "לְהַצִיל" - }, - "backToSnippets": "חזרה לקטעי הטקסט", - "settingsTitle": "הגדרות קטע", - "settingsDisplayTitle": "לְהַצִיג", - "settingsBehaviorTitle": "הִתְנַהֲגוּת", - "typeCommandHint": "פועל בטרמינל הפעיל.", - "typeNoteHint": "מדביק לתוך הטרמינל הפעיל מבלי להריץ.", - "variablesHint": "משתנים: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "לחץ פעמיים כדי להפעיל", - "sharedBadge": "מְשׁוּתָף", - "copyToClipboard": "העתק ללוח", - "copyFailed": "נכשל בהעתקה ללוח", - "moreOptions": "אפשרויות נוספות", - "moreActions": "פעולות נוספות", - "moveToFolder": "העבר לתיקייה", - "deleteSnippet": "מחיקת קטע", - "deleteSnippetConfirm": "למחוק את קטע הקוד \"{{name}}\"?", - "deleteSuccess": "קטע הטקסט נמחק", - "delete": "לִמְחוֹק", - "folderActions": "פעולות תיקייה", - "addSnippetHere": "קטע חדש כאן", - "deleteFolder": "מחיקת תיקייה", - "deleteFolderConfirm": "למחוק את התיקייה \"{{name}}\"? קטעי הטקסט שלה יוזבו מהתיקייה.", - "createFolderDescription": "קבץ קטעי טקסט קשורים יחד.", - "editFolderDescription": "שנה את שם, צבע או סמל תיקייה זו.", - "folderColorLabel": "צֶבַע", - "loadFailed": "טעינת קטעי הקוד נכשלה", - "importSkipExisting": "ייבוא (דלג על קיים)", - "importOverwrite": "ייבוא (החלפת קבצים קיימים)", - "importInvalidFile": "קובץ זה אינו קובץ ייצוא חוקי של קטעי טקסט", - "importSuccess": "יובא {{imported}}, עודכן {{updated}}, דילג על {{skipped}}", - "exportSnippets": "ייצוא קטעי טקסט", - "exportSuccess": "קטעי הטקסט יוצאו", - "exportFailed": "ייצוא קטעי הקוד נכשל", - "expandAll": "הרחב את כל התיקיות", - "collapseAll": "כווץ את כל התיקיות", - "docsLink": "תיעוד", - "clearSearch": "נקה חיפוש", - "noSearchResults": "אין קטעי טקסט התואמים את החיפוש שלך", - "emptyTitle": "עדיין אין קטעי טקסט", - "emptyDescription": "שמור פקודות שאתה מפעיל לעתים קרובות, ולאחר מכן הפעל אותן בטרמינל בלחיצה אחת.", - "shareSnippet": "שתף קטע", - "shareFolder": "שיתוף תיקייה", - "shareExpiry": "פג תוקף", - "shareExpiryOptions": { - "h0": "לְעוֹלָם לֹא", - "h1": "בעוד שעה", - "h24": "בעוד יום אחד", - "h168": "בעוד 7 ימים", - "h720": "בעוד 30 יום" - }, - "shareButton": "לַחֲלוֹק", - "shareSuccess": "שותף עם {{count}}", - "shareFailed": "השיתוף נכשל", - "shareLoadFailed": "טעינת פרטי השיתוף נכשלה", - "currentAccess": "גישה נוכחית", - "notSharedYet": "עדיין לא שותף עם אף אחד", - "revokeAccess": "לְבַטֵל", - "revokeSuccess": "הגישה בוטלה", - "revokeFailed": "ביטול הגישה נכשל", - "close": "לִסְגוֹר", - "targetHostsLabel": "מארחי יעד", - "targetHostsHint": "בחר מארחים להפעלת פקודה זו ישירות, ללא טרמינל פתוח. השאר ריק כדי להפעיל אותה בטרמינל הפעיל.", - "clearTargetHosts": "בָּרוּר", - "noHostsAvailable": "אין מארחים זמינים", - "searchHosts": "חיפוש מארחים", - "runOnTargets": "הפעלה על מארחי היעד", - "executionResultTitle": "תוצאות עבור \"{{name}}\"", - "executionResultDescription": "פלט מכל מארח יעד.", - "executionSuccess": "הַצלָחָה", - "executionFailed": "נִכשָׁל", - "directRunSuccess": "הריץ את \"{{name}}\" על מארחים {{count}}", - "directRunPartialFail": "\"{{name}}\" נכשל בחלק מהמארחים", - "reorderFailed": "נכשל הסידור מחדש של קטעי הקוד", - "dropToRemoveFromFolder": "שחרר כאן כדי להוציא את התיקייה", - "shareSnippetTitle": "שתף \"{{name}}\"", - "shareFolderTitle": "שיתוף תיקייה \"{{name}}\"", - "shareWith": "שתף עם", - "usersTab": "משתמשים", - "rolesTab": "תפקידים", - "shareSearchPlaceholder": "חיפוש משתמשים או תפקידים", - "shareNoMatches": "אין התאמות", - "shareWithCount": "שתף עם {{count}}", - "shareFolderHint": "משתף כל קטע קוד בתיקייה זו. קטעי קוד שנוספו מאוחר יותר אינם משותפים אוטומטית.", - "shareFolderSummary": "קטעי {{count}} משותפים בתיקייה זו", - "grantedBy": "הוענק על ידי", - "expired": "פג תוקף", - "variablesDialogTitle": "הפעל \"{{name}}\"", - "variablesDialogDescription": "מלא את הערכים הדרושים לקטע זה לפני הפעלתו.", - "variablesPreviewLabel": "תצוגה מקדימה", - "variablesConfirmButton": "לָרוּץ", - "keybindings": { - "runSnippet": "הפעלת קטע קוד קיים", - "snippetLabel": "קטע", - "selectSnippet": "בחר קטע", - "snippetMissing": "קטע לא נמצא", - "snippetRequired": "בחר קטע" - }, - "paletteHintRun": "הפעל בלשונית הטרמינל הפעילה", - "host": { - "startupTitle": "קטע הפעלה", - "quickActionsTitle": "פעולות מהירות", - "quickActionsDescription": "פעולות מהירות מופיעות ככפתורים בסרגל הכלים של Host Metrics לביצוע פקודה בלחיצה אחת.", - "noQuickActions": "אין עדיין פעולות מהירות.", - "buttonLabel": "תווית כפתור", - "selectSnippet": "בחר קטע...", - "addQuickAction": "הוסף פעולה", - "tabTitle": "קטעי טקסט", - "startupDescription": "פועל בטרמינל בכל פעם שהוא מתחבר למארח זה. משתנים כמו $HOST מתמלאים; קטע שמבקש קלט מדלג.", - "none": "אַף לֹא אֶחָד", - "removeQuickAction": "הסר פעולה מהירה" - }, - "quickActions": { - "running": "מבצע {{name}}...", - "succeeded": "{{name}} הושלם בהצלחה", - "failed": "{{name}} נכשל", - "error": "נכשל בביצוע {{name}}" - }, - "paletteHintPaste": "הדבק בכרטיסייה הפעילה של הטרמינל", - "targetTerminals": "לְהַרִיץ", - "targetTerminalsHint": "אופציונלי, אחרת הכרטיסייה הפעילה", - "selectAll": "כֹּל", - "selectNone": "אַף לֹא אֶחָד", - "plugin": { - "name": "קטעי טקסט" - } -} diff --git a/plugins/snippets/locales/translated/hi_IN.json b/plugins/snippets/locales/translated/hi_IN.json deleted file mode 100644 index df0dcb65b..000000000 --- a/plugins/snippets/locales/translated/hi_IN.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "स्निपेट्स" - }, - "onboarding": { - "feature_snippets": "स्निपेट्स", - "feature_snippets_desc": "उन कमांडों को सेव करें जिन्हें आप अक्सर चलाते हैं और उन्हें एक क्लिक में टर्मिनल पर भेजें।" - }, - "permissions": { - "view": { - "title": "अंश देखें", - "description": "अपने खुद के अंश, नोट्स और साझा किए गए अंश देखें।" - }, - "create": { - "title": "स्निपेट बनाएं", - "description": "नए स्निपेट, नोट्स और फोल्डर बनाएं।" - }, - "edit": { - "title": "स्निपेट संपादित करें", - "description": "मौजूदा स्निपेट और फ़ोल्डरों का नाम बदलें या उनमें बदलाव करें।" - }, - "delete": { - "title": "स्निपेट्स हटाएं", - "description": "स्निपेट और फ़ोल्डर हटाएं।" - }, - "share": { - "title": "अंश साझा करें", - "description": "अन्य उपयोगकर्ताओं या भूमिकाओं के साथ स्निपेट और फ़ोल्डर साझा करें।" - } - }, - "settings": { - "foldersCollapsed": { - "label": "डिफ़ॉल्ट रूप से फ़ोल्डर्स संकुचित करें", - "description": "स्निपेट्स पैनल खोलते समय प्रत्येक स्निपेट फ़ोल्डर को बंद अवस्था में प्रारंभ करें।" - }, - "showCommands": { - "label": "कमांड दिखाएँ", - "description": "प्रत्येक स्निपेट के कमांड की पहली पंक्ति को उसके नाम के नीचे दिखाएँ।" - }, - "confirmExecution": { - "label": "चलाने से पहले पुष्टि करें", - "description": "टर्मिनल में कमांड चलाने से पहले पूछ लें।" - } - }, - "cancel": "रद्द करें", - "loading": "लोड हो रहा है...", - "searchPlaceholder": "खोज अंश", - "newFolder": "नया फ़ोल्डर", - "newSnippet": "नया अंश", - "noFolder": "कोई फ़ोल्डर नहीं", - "noSnippetsInFolder": "इस फ़ोल्डर में कोई स्निपेट नहीं है", - "noTerminalTabsOpen": "कोई टर्मिनल टैब खुला नहीं है", - "run": "चलाएं", - "pasteToTerminal": "टर्मिनल में पेस्ट करें", - "copySuccess": "क्लिपबोर्ड पर कॉपी किया गया", - "importExport": "आयात / निर्यात", - "importFailed": "स्निपेट आयात करने में विफल", - "nameLabel": "नाम", - "namePlaceholder": "स्निपेट का नाम", - "descriptionLabel": "विवरण", - "descriptionPlaceholder": "वैकल्पिक विवरण", - "folderLabel": "फ़ोल्डर", - "typeLabel": "प्रकार", - "typeCommand": "कमांड", - "typeNote": "टिप्पणी", - "commandLabel": "कमांड", - "commandPlaceholder": "चलाने के लिए कमांड, उदाहरण के लिए systemctl status $HOST", - "noteLabel": "टिप्पणी", - "notePlaceholder": "टर्मिनल में पेस्ट करने के लिए टेक्स्ट", - "createSnippetTitle": "स्निपेट बनाएं", - "createSnippetButton": "स्निपेट बनाएं", - "createSuccess": "स्निपेट बनाया गया", - "createFailed": "स्निपेट बनाने में विफल", - "editSnippetTitle": "स्निपेट संपादित करें", - "saveSnippetButton": "अंश सहेजें", - "updateSuccess": "अंश अपडेट किया गया", - "updateFailed": "स्निपेट अपडेट करने में विफल", - "deleteFailed": "स्निपेट हटाने में विफल", - "createFolderTitle": "फ़ोल्डर बनाएँ", - "createFolderButton": "फ़ोल्डर बनाएँ", - "folderCreateSuccess": "फ़ोल्डर बनाया गया", - "folderCreateFailed": "फ़ोल्डर बनाने में विफल", - "editFolderTitle": "फ़ोल्डर संपादित करें", - "saveFolderButton": "फ़ोल्डर सहेजें", - "folderEditSuccess": "फ़ोल्डर अपडेट हो गया", - "folderEditFailed": "फ़ोल्डर अपडेट करने में विफल", - "folderNameLabel": "फ़ोल्डर का नाम", - "folderNamePlaceholder": "फ़ोल्डर का नाम", - "folderIconLabel": "आइकन", - "folderDeleteSuccess": "फ़ोल्डर हटा दिया गया", - "folderDeleteFailed": "फ़ोल्डर हटाने में विफल", - "confirmRunMessage": "\"{{name}}\" चलाएँ?", - "confirmRunButton": "चलाएं", - "runSuccess": "Ran \"{{name}}\" on {{count}} terminal(s)", - "pasteSuccess": "\"{{name}}\" को {{count}} टर्मिनल(ओं) में पेस्ट किया गया", - "admin": { - "tabTitle": "स्निपेट्स", - "count": "{{count}} स्निपेट", - "add": "स्निपेट जोड़ें", - "namePlaceholder": "स्निपेट का नाम", - "contentPlaceholder": "कमांड सामग्री", - "folderPlaceholder": "फ़ोल्डर (वैकल्पिक)", - "empty": "इस उपयोगकर्ता के पास कोई स्निपेट नहीं है", - "deleteConfirm": "स्निपेट \"{{name}}\" हटाएं जो {{username}} का है?", - "deleted": "स्निपेट हटा दिया गया", - "deleteFailed": "स्निपेट हटाने में विफल", - "requiredFields": "स्निपेट का नाम और सामग्री आवश्यक हैं", - "saved": "स्निपेट सहेजा गया", - "saveFailed": "स्निपेट सहेजने में विफल", - "saving": "सहेजा जा रहा है...", - "save": "सहेजें" - }, - "backToSnippets": "स्निपेट्स पर वापस जाएँ", - "settingsTitle": "स्निपेट सेटिंग्स", - "settingsDisplayTitle": "प्रदर्शन", - "settingsBehaviorTitle": "व्यवहार", - "typeCommandHint": "यह सक्रिय टर्मिनल में चलता है।", - "typeNoteHint": "यह सक्रिय टर्मिनल में बिना चलाए पेस्ट करता है।", - "variablesHint": "चर: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "चलाने के लिए डबल क्लिक करें", - "sharedBadge": "साझा", - "copyToClipboard": "क्लिपबोर्ड पर कॉपी करें", - "copyFailed": "क्लिपबोर्ड पर कॉपी करने में विफल", - "moreOptions": "अधिक विकल्प", - "moreActions": "अधिक कार्रवाइयां", - "moveToFolder": "फ़ोल्डर में ले जाएँ", - "deleteSnippet": "स्निपेट हटाएं", - "deleteSnippetConfirm": "स्निपेट \"{{name}} \" हटाएं?", - "deleteSuccess": "स्निपेट हटा दिया गया", - "delete": "हटाएँ", - "folderActions": "फ़ोल्डर क्रियाएँ", - "addSnippetHere": "यहां नया अंश है", - "deleteFolder": "फ़ोल्डर हटाएं", - "deleteFolderConfirm": "फ़ोल्डर \"{{name}}\" हटाएं? इसके स्निपेट फ़ोल्डर से बाहर चले जाते हैं।", - "createFolderDescription": "संबंधित अंशों को एक साथ समूहित करें।", - "editFolderDescription": "इस फ़ोल्डर का नाम, रंग या आइकन बदलें।", - "folderColorLabel": "रंग", - "loadFailed": "स्निपेट लोड करने में विफल", - "importSkipExisting": "इम्पोर्ट करें (मौजूदा को छोड़ें)", - "importOverwrite": "आयात करें (मौजूदा को ओवरराइट करें)", - "importInvalidFile": "वह फ़ाइल वैध स्निपेट निर्यात नहीं है", - "importSuccess": "आयातित {{imported}}, अद्यतनित {{updated}}, छोड़ा गया {{skipped}}", - "exportSnippets": "निर्यात स्निपेट", - "exportSuccess": "निर्यात किए गए अंश", - "exportFailed": "स्निपेट निर्यात करने में विफल", - "expandAll": "सभी फ़ोल्डर विस्तृत करें", - "collapseAll": "सभी फ़ोल्डर संकुचित करें", - "docsLink": "प्रलेखन", - "clearSearch": "खोज साफ़ करें", - "noSearchResults": "आपकी खोज से मेल खाने वाले कोई अंश नहीं हैं", - "emptyTitle": "अभी तक कोई अंश उपलब्ध नहीं हैं", - "emptyDescription": "जिन कमांड्स को आप अक्सर चलाते हैं, उन्हें सेव कर लें और फिर उन्हें एक क्लिक में टर्मिनल में चलाएं।", - "shareSnippet": "अंश साझा करें", - "shareFolder": "फ़ोल्डर साझा करें", - "shareExpiry": "समाप्ति", - "shareExpiryOptions": { - "h0": "कभी नहीं", - "h1": "1 घंटे में", - "h24": "1 दिन में", - "h168": "7 दिनों में", - "h720": "30 दिनों में" - }, - "shareButton": "साझा करें", - "shareSuccess": "{{count}} के साथ साझा किया गया", - "shareFailed": "साझा करने में विफल", - "shareLoadFailed": "शेयरिंग विवरण लोड करने में विफल", - "currentAccess": "वर्तमान पहुँच", - "notSharedYet": "अभी तक किसी के साथ साझा नहीं किया गया है", - "revokeAccess": "निरस्त करें", - "revokeSuccess": "पहुँच रद्द की गई", - "revokeFailed": "पहुँच रद्द करने में विफल", - "close": "बंद करें", - "targetHostsLabel": "लक्षित मेजबान", - "targetHostsHint": "इस कमांड को सीधे चलाने के लिए होस्ट चुनें, बिना टर्मिनल खोले। सक्रिय टर्मिनल में चलाने के लिए इसे खाली छोड़ दें।", - "clearTargetHosts": "साफ़ करें", - "noHostsAvailable": "कोई होस्ट उपलब्ध नहीं है", - "searchHosts": "होस्ट खोजें", - "runOnTargets": "लक्ष्य होस्ट पर चलाएँ", - "executionResultTitle": "\"{{name}} \" के लिए परिणाम", - "executionResultDescription": "प्रत्येक लक्ष्य होस्ट से आउटपुट।", - "executionSuccess": "सफल", - "executionFailed": "असफल", - "directRunSuccess": "\"{{name}}\" को {{count}} होस्ट पर चलाया गया", - "directRunPartialFail": "कुछ होस्ट पर \"{{name}}\" विफल रहा", - "reorderFailed": "स्निपेट्स को पुनर्व्यवस्थित करने में विफल", - "dropToRemoveFromFolder": "फोल्डर से बाहर निकलने के लिए यहां ड्रॉप करें", - "shareSnippetTitle": "शेयर करें \"{{name}}\"", - "shareFolderTitle": "फ़ोल्डर साझा करें \"{{name}}\"", - "shareWith": "के साथ साझा करें", - "usersTab": "उपयोगकर्ता", - "rolesTab": "भूमिकाएं", - "shareSearchPlaceholder": "उपयोगकर्ताओं या भूमिकाओं को खोजें", - "shareNoMatches": "कोई मेल नहीं", - "shareWithCount": "{{count}} के साथ साझा करें", - "shareFolderHint": "इस फ़ोल्डर में मौजूद सभी स्निपेट साझा करता है। बाद में जोड़े गए स्निपेट स्वचालित रूप से साझा नहीं होते हैं।", - "shareFolderSummary": "इस फ़ोल्डर में साझा किए गए {{count}} अंश", - "grantedBy": "प्रदानकर्ता", - "expired": "समाप्त", - "variablesDialogTitle": "रन \"{{name}}\"", - "variablesDialogDescription": "इस कोड स्निपेट को चलाने से पहले इसमें आवश्यक मान भरें।", - "variablesPreviewLabel": "पूर्वावलोकन", - "variablesConfirmButton": "चलाएं", - "keybindings": { - "runSnippet": "मौजूदा स्निपेट चलाएँ", - "snippetLabel": "टुकड़ा", - "selectSnippet": "एक अंश का चयन करें", - "snippetMissing": "अंश नहीं मिला", - "snippetRequired": "एक अंश का चयन करें" - }, - "paletteHintRun": "सक्रिय टर्मिनल टैब में चलाएँ", - "host": { - "startupTitle": "स्टार्टअप स्निपेट", - "quickActionsTitle": "त्वरित कार्रवाइयाँ", - "quickActionsDescription": "त्वरित कार्रवाइयाँ एक-क्लिक कमांड निष्पादन के लिए होस्ट मेट्रिक्स टूलबार में बटन के रूप में दिखाई देती हैं।", - "noQuickActions": "अभी तक कोई त्वरित कार्रवाई नहीं।", - "buttonLabel": "बटन लेबल", - "selectSnippet": "स्निपेट चुनें...", - "addQuickAction": "कार्रवाई जोड़ें", - "tabTitle": "स्निपेट्स", - "startupDescription": "यह टर्मिनल में हर बार इस होस्ट से कनेक्ट होने पर चलता है। $HOST जैसे वैरिएबल भरे जाते हैं; इनपुट मांगने वाला एक अंश छोड़ दिया जाता है।", - "none": "कोई नहीं", - "removeQuickAction": "त्वरित कार्रवाई हटाएँ" - }, - "quickActions": { - "running": "{{name}} निष्पादित हो रहा है...", - "succeeded": "{{name}} सफलतापूर्वक पूर्ण हुआ", - "failed": "{{name}} विफल रहा", - "error": "{{name}} निष्पादित करने में विफल" - }, - "paletteHintPaste": "सक्रिय टर्मिनल टैब में पेस्ट करें", - "targetTerminals": "झगड़ा", - "targetTerminalsHint": "वैकल्पिक, अन्यथा सक्रिय टैब", - "selectAll": "सभी", - "selectNone": "कोई नहीं", - "plugin": { - "name": "स्निपेट्स" - } -} diff --git a/plugins/snippets/locales/translated/hu_HU.json b/plugins/snippets/locales/translated/hu_HU.json deleted file mode 100644 index 0bc811c7d..000000000 --- a/plugins/snippets/locales/translated/hu_HU.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Kódrészletek" - }, - "onboarding": { - "feature_snippets": "Kódrészletek", - "feature_snippets_desc": "Mentsd el a gyakran futtatott parancsokat, és küldd el őket egy terminálba egyetlen kattintással." - }, - "permissions": { - "view": { - "title": "Kódrészletek megtekintése", - "description": "Tekintse meg saját részleteit, jegyzeteit és megosztott részleteit." - }, - "create": { - "title": "Kódrészletek létrehozása", - "description": "Új kódrészletek, jegyzetek és mappák létrehozása." - }, - "edit": { - "title": "Kódrészletek szerkesztése", - "description": "Nevezze át vagy módosítsa a meglévő kódrészleteket és mappákat." - }, - "delete": { - "title": "Kódrészletek törlése", - "description": "Töröljön kódrészleteket és mappákat." - }, - "share": { - "title": "Részletek megosztása", - "description": "Kódrészletek és mappák megosztása más felhasználókkal vagy szerepkörökkel." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Mappák összecsukása alapértelmezés szerint", - "description": "Minden kódrészletmappa összecsukott állapotban indul el a Kódrészletek panel megnyitásakor." - }, - "showCommands": { - "label": "Parancsok megjelenítése", - "description": "Mutasd meg az egyes kódrészletek parancsának első sorát a neve alatt." - }, - "confirmExecution": { - "label": "Futtatás előtti megerősítés", - "description": "Kérdezzen rá, mielőtt egy parancsrészlet fut a terminálban." - } - }, - "cancel": "Mégsem", - "loading": "Terhelés...", - "searchPlaceholder": "Keresési kódrészletek", - "newFolder": "Új mappa", - "newSnippet": "Új részlet", - "noFolder": "Nincs mappa", - "noSnippetsInFolder": "Nincsenek kódrészletek ebben a mappában", - "noTerminalTabsOpen": "Nincs megnyitva terminálfül", - "run": "Fut", - "pasteToTerminal": "Beillesztés terminálba", - "copySuccess": "Vágólapra másolva", - "importExport": "Importálás / Exportálás", - "importFailed": "Nem sikerült importálni a kódrészleteket", - "nameLabel": "Név", - "namePlaceholder": "Kódrészlet neve", - "descriptionLabel": "Leírás", - "descriptionPlaceholder": "Opcionális leírás", - "folderLabel": "Mappa", - "typeLabel": "Típus", - "typeCommand": "Parancs", - "typeNote": "Jegyzet", - "commandLabel": "Parancs", - "commandPlaceholder": "Futtatandó parancs, pl. systemctl status $HOST", - "noteLabel": "Jegyzet", - "notePlaceholder": "A terminálba beillesztendő szöveg", - "createSnippetTitle": "Kódrészlet létrehozása", - "createSnippetButton": "Kódrészlet létrehozása", - "createSuccess": "Kódrészlet létrehozva", - "createFailed": "Nem sikerült létrehozni a kódrészletet", - "editSnippetTitle": "Kódrészlet szerkesztése", - "saveSnippetButton": "Kódrészlet mentése", - "updateSuccess": "Kódrészlet frissítve", - "updateFailed": "Nem sikerült frissíteni a kódrészletet", - "deleteFailed": "Nem sikerült törölni a részletet", - "createFolderTitle": "Mappa létrehozása", - "createFolderButton": "Mappa létrehozása", - "folderCreateSuccess": "Mappa létrehozva", - "folderCreateFailed": "Nem sikerült létrehozni a mappát", - "editFolderTitle": "Mappa szerkesztése", - "saveFolderButton": "Mappa mentése", - "folderEditSuccess": "Mappa frissítve", - "folderEditFailed": "Nem sikerült frissíteni a mappát", - "folderNameLabel": "Mappa neve", - "folderNamePlaceholder": "Mappa neve", - "folderIconLabel": "Ikon", - "folderDeleteSuccess": "Mappa törölve", - "folderDeleteFailed": "Nem sikerült törölni a mappát", - "confirmRunMessage": "Futtassa a \"{{name}} \" programot?", - "confirmRunButton": "Fut", - "runSuccess": "\"{{name}}\" futtatva {{count}} terminálon", - "pasteSuccess": "Beillesztve a(z) \"{{name}}\" elemet a(z) {{count}} terminál(ok)ba", - "admin": { - "tabTitle": "Kódrészletek", - "count": "{{count}} kódrészletek", - "add": "Kódrészlet hozzáadása", - "namePlaceholder": "Kódrészlet neve", - "contentPlaceholder": "Parancs tartalma", - "folderPlaceholder": "Mappa (opcionális)", - "empty": "Ennek a felhasználónak nincsenek kódrészletei", - "deleteConfirm": "Törölni kell a(z) {{username}}-hoz tartozó \"{{name}}\" kódrészletet?", - "deleted": "Kódrészlet törölve", - "deleteFailed": "Nem sikerült törölni a részletet", - "requiredFields": "A részlet nevének és tartalmának megadása kötelező", - "saved": "Kódrészlet mentve", - "saveFailed": "Nem sikerült menteni a részletet", - "saving": "Megtakarítás...", - "save": "Megtakarítás" - }, - "backToSnippets": "Vissza a részletekhez", - "settingsTitle": "Kódrészlet beállításai", - "settingsDisplayTitle": "Kijelző", - "settingsBehaviorTitle": "Viselkedés", - "typeCommandHint": "Az aktív terminálban fut.", - "typeNoteHint": "Beilleszti az aktív terminálba futtatás nélkül.", - "variablesHint": "Változók: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Címke}", - "doubleClickToRun": "Dupla kattintás a futtatáshoz", - "sharedBadge": "Megosztott", - "copyToClipboard": "Másolás a vágólapra", - "copyFailed": "Nem sikerült a vágólapra másolni", - "moreOptions": "További lehetőségek", - "moreActions": "További műveletek", - "moveToFolder": "Áthelyezés mappába", - "deleteSnippet": "Kódrészlet törlése", - "deleteSnippetConfirm": "Törölni a(z) \"{{name}} \" kódrészletet?", - "deleteSuccess": "Kódrészlet törölve", - "delete": "Töröl", - "folderActions": "Mappaműveletek", - "addSnippetHere": "Új részlet itt", - "deleteFolder": "Mappa törlése", - "deleteFolderConfirm": "Törli a(z) „{{name}}” mappát? A benne lévő kódrészletek átkerülnek a mappából.", - "createFolderDescription": "Csoportosítsd a kapcsolódó kódrészleteket.", - "editFolderDescription": "Módosítsa a mappa nevét, színét vagy ikonját.", - "folderColorLabel": "Szín", - "loadFailed": "Nem sikerült betölteni a kódrészleteket", - "importSkipExisting": "Importálás (meglévő kihagyása)", - "importOverwrite": "Importálás (meglévő felülírása)", - "importInvalidFile": "Ez a fájl nem érvényes kódrészlet export.", - "importSuccess": "Importálva {{imported}}, frissítve {{updated}}, kihagyva {{skipped}}", - "exportSnippets": "Kódrészletek exportálása", - "exportSuccess": "Exportált kódrészletek", - "exportFailed": "Nem sikerült exportálni a kódrészleteket", - "expandAll": "Az összes mappa kibontása", - "collapseAll": "Az összes mappa összecsukása", - "docsLink": "Dokumentáció", - "clearSearch": "Keresés törlése", - "noSearchResults": "Egyik kódrészlet sem felel meg a keresésnek", - "emptyTitle": "Még nincsenek részletek", - "emptyDescription": "Mentsd el a gyakran futtatott parancsokat, majd futtasd őket egy terminálban egyetlen kattintással.", - "shareSnippet": "Kódrészlet megosztása", - "shareFolder": "Mappa megosztása", - "shareExpiry": "Lejár", - "shareExpiryOptions": { - "h0": "Soha", - "h1": "1 óra múlva", - "h24": "1 nap múlva", - "h168": "7 nap múlva", - "h720": "30 nap múlva" - }, - "shareButton": "Részesedés", - "shareSuccess": "Megosztva {{count}} felhasználóval", - "shareFailed": "Nem sikerült megosztani", - "shareLoadFailed": "Nem sikerült betölteni a megosztási adatokat", - "currentAccess": "Jelenlegi hozzáférés", - "notSharedYet": "Még senkivel sincs megosztva", - "revokeAccess": "Visszavonás", - "revokeSuccess": "Hozzáférés visszavonva", - "revokeFailed": "Nem sikerült visszavonni a hozzáférést", - "close": "Közeli", - "targetHostsLabel": "Célzott hosztok", - "targetHostsHint": "Válassza ki azokat a gépeket, amelyeken közvetlenül, terminál megnyitása nélkül futtatni szeretné ezt a parancsot. Hagyja üresen, ha az aktív terminálon szeretné futtatni.", - "clearTargetHosts": "Világos", - "noHostsAvailable": "Nincsenek elérhető házigazdák", - "searchHosts": "Keresés hosztok", - "runOnTargets": "Futtatás a célállomásokon", - "executionResultTitle": "Eredmények a következőre: \"{{name}}\"", - "executionResultDescription": "Kimenet az egyes célállomásokról.", - "executionSuccess": "Siker", - "executionFailed": "Sikertelen", - "directRunSuccess": "\"{{name}}\" futtatva {{count}} gazdagépen", - "directRunPartialFail": "„{{name}}” hiba történt néhány gépen", - "reorderFailed": "Nem sikerült átrendezni a kódrészleteket", - "dropToRemoveFromFolder": "Húzd ide, ha ki szeretnél mozdulni a mappából", - "shareSnippetTitle": "Megosztás \"{{name}}\"", - "shareFolderTitle": "Mappa megosztása: \"{{name}}\"", - "shareWith": "Megosztás", - "usersTab": "Felhasználók", - "rolesTab": "Szerepkörök", - "shareSearchPlaceholder": "Felhasználók vagy szerepkörök keresése", - "shareNoMatches": "Nincs találat", - "shareWithCount": "Megosztás {{count}} felhasználóval", - "shareFolderHint": "Megosztja a mappában található összes kódrészletet. A később hozzáadott kódrészletek nem kerülnek automatikusan megosztásra.", - "shareFolderSummary": "Megosztott {{count}} kódrészlet(ek) ebben a mappában", - "grantedBy": "Megadta", - "expired": "Lejárt", - "variablesDialogTitle": "Futtassa a \"{{name}}\"", - "variablesDialogDescription": "Töltse ki a kódrészlet futtatása előtt szükséges értékeket.", - "variablesPreviewLabel": "Előnézet", - "variablesConfirmButton": "Fut", - "keybindings": { - "runSnippet": "Meglévő kódrészlet futtatása", - "snippetLabel": "Töredék", - "selectSnippet": "Válasszon ki egy részletet", - "snippetMissing": "részlet nem található", - "snippetRequired": "Válasszon ki egy részletet" - }, - "paletteHintRun": "Futtatás az aktív terminálfülön", - "host": { - "startupTitle": "Indítási kódrészlet", - "quickActionsTitle": "Gyors műveletek", - "quickActionsDescription": "A gyors műveletek gombokként jelennek meg a Host Metrics eszköztáron, így egyetlen kattintással végrehajthatók a parancsok.", - "noQuickActions": "Még nincsenek gyors intézkedések.", - "buttonLabel": "Gombcímke", - "selectSnippet": "Válasszon részletet...", - "addQuickAction": "Művelet hozzáadása", - "tabTitle": "Kódrészletek", - "startupDescription": "Minden alkalommal lefut a terminálban, amikor csatlakozik ehhez a géphez. A $HOST -hoz hasonló változók kitöltődnek; a bemenetet kérő kódrészlet kimarad.", - "none": "Egyik sem", - "removeQuickAction": "Gyorsművelet eltávolítása" - }, - "quickActions": { - "running": "{{name}} végrehajtása ...", - "succeeded": "{{name}} sikeresen befejeződött", - "failed": "{{name}} sikertelen", - "error": "Nem sikerült végrehajtani a {{name}} parancsot." - }, - "paletteHintPaste": "Beillesztés az aktív terminálfülbe", - "targetTerminals": "Befutás", - "targetTerminalsHint": "opcionális, különben az aktív lap", - "selectAll": "Minden", - "selectNone": "Egyik sem", - "plugin": { - "name": "Kódrészletek" - } -} diff --git a/plugins/snippets/locales/translated/id_ID.json b/plugins/snippets/locales/translated/id_ID.json deleted file mode 100644 index 2385f9070..000000000 --- a/plugins/snippets/locales/translated/id_ID.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Cuplikan" - }, - "onboarding": { - "feature_snippets": "Cuplikan", - "feature_snippets_desc": "Simpan perintah yang sering Anda jalankan dan kirimkan ke terminal hanya dengan satu klik." - }, - "permissions": { - "view": { - "title": "Lihat cuplikan", - "description": "Lihat cuplikan, catatan, dan cuplikan yang dibagikan milik Anda sendiri." - }, - "create": { - "title": "Buat cuplikan", - "description": "Buat cuplikan, catatan, dan folder baru." - }, - "edit": { - "title": "Edit cuplikan", - "description": "Ubah nama atau mengganti cuplikan dan folder yang sudah ada." - }, - "delete": { - "title": "Hapus cuplikan", - "description": "Hapus cuplikan dan folder." - }, - "share": { - "title": "Bagikan cuplikan", - "description": "Bagikan cuplikan dan folder dengan pengguna atau peran lain." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Secara default, folder akan diciutkan.", - "description": "Saat membuka panel Cuplikan, setiap folder cuplikan akan tertutup." - }, - "showCommands": { - "label": "Tampilkan perintah", - "description": "Tampilkan baris pertama perintah setiap cuplikan di bawah namanya." - }, - "confirmExecution": { - "label": "Konfirmasi sebelum menjalankan", - "description": "Tanyakan terlebih dahulu sebelum menjalankan cuplikan perintah di terminal." - } - }, - "cancel": "Membatalkan", - "loading": "Memuat...", - "searchPlaceholder": "Cuplikan pencarian", - "newFolder": "Folder baru", - "newSnippet": "Cuplikan baru", - "noFolder": "Tidak ada folder", - "noSnippetsInFolder": "Tidak ada cuplikan di folder ini", - "noTerminalTabsOpen": "Tidak ada tab terminal yang terbuka.", - "run": "Berlari", - "pasteToTerminal": "Tempel ke terminal", - "copySuccess": "Disalin ke papan klip", - "importExport": "Impor / Ekspor", - "importFailed": "Gagal mengimpor cuplikan kode.", - "nameLabel": "Nama", - "namePlaceholder": "Nama cuplikan", - "descriptionLabel": "Keterangan", - "descriptionPlaceholder": "Deskripsi opsional", - "folderLabel": "Map", - "typeLabel": "Jenis", - "typeCommand": "Memerintah", - "typeNote": "Catatan", - "commandLabel": "Memerintah", - "commandPlaceholder": "Perintah yang akan dijalankan, misalnya systemctl status $HOST", - "noteLabel": "Catatan", - "notePlaceholder": "Teks yang akan ditempelkan ke terminal", - "createSnippetTitle": "Buat cuplikan", - "createSnippetButton": "Buat cuplikan", - "createSuccess": "Cuplikan dibuat", - "createFailed": "Gagal membuat cuplikan", - "editSnippetTitle": "Sunting cuplikan", - "saveSnippetButton": "Simpan cuplikan", - "updateSuccess": "Cuplikan diperbarui", - "updateFailed": "Gagal memperbarui cuplikan", - "deleteFailed": "Gagal menghapus cuplikan", - "createFolderTitle": "Buat folder", - "createFolderButton": "Buat folder", - "folderCreateSuccess": "Folder telah dibuat.", - "folderCreateFailed": "Gagal membuat folder", - "editFolderTitle": "Edit folder", - "saveFolderButton": "Simpan folder", - "folderEditSuccess": "Folder telah diperbarui.", - "folderEditFailed": "Gagal memperbarui folder", - "folderNameLabel": "Nama folder", - "folderNamePlaceholder": "Nama folder", - "folderIconLabel": "Ikon", - "folderDeleteSuccess": "Folder dihapus", - "folderDeleteFailed": "Gagal menghapus folder", - "confirmRunMessage": "Jalankan \"{{name}}\"?", - "confirmRunButton": "Berlari", - "runSuccess": "Menjalankan \"{{name}}\" pada {{count}} terminal", - "pasteSuccess": "\"{{name}}\" ditempelkan ke terminal {{count}}", - "admin": { - "tabTitle": "Cuplikan", - "count": "{{count}} cuplikan", - "add": "Tambahkan Cuplikan", - "namePlaceholder": "Nama cuplikan", - "contentPlaceholder": "Isi perintah", - "folderPlaceholder": "Folder (opsional)", - "empty": "Pengguna ini tidak memiliki cuplikan.", - "deleteConfirm": "Hapus cuplikan \"{{name}}\" milik {{username}}?", - "deleted": "Cuplikan dihapus", - "deleteFailed": "Gagal menghapus cuplikan", - "requiredFields": "Nama cuplikan dan konten wajib diisi.", - "saved": "Cuplikan tersimpan", - "saveFailed": "Gagal menyimpan cuplikan", - "saving": "Penghematan...", - "save": "Menyimpan" - }, - "backToSnippets": "Kembali ke cuplikan", - "settingsTitle": "Pengaturan cuplikan", - "settingsDisplayTitle": "Menampilkan", - "settingsBehaviorTitle": "Perilaku", - "typeCommandHint": "Berjalan di terminal aktif.", - "typeNoteHint": "Menempelkan teks ke terminal aktif tanpa menjalankannya.", - "variablesHint": "Variabel: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Klik dua kali untuk menjalankan", - "sharedBadge": "Dibagikan", - "copyToClipboard": "Salin ke papan klip", - "copyFailed": "Gagal menyalin ke papan klip", - "moreOptions": "Opsi lainnya", - "moreActions": "Tindakan lainnya", - "moveToFolder": "Pindahkan ke folder", - "deleteSnippet": "Hapus cuplikan", - "deleteSnippetConfirm": "Hapus cuplikan \"{{name}}\"?", - "deleteSuccess": "Cuplikan dihapus", - "delete": "Menghapus", - "folderActions": "Tindakan folder", - "addSnippetHere": "Cuplikan baru di sini", - "deleteFolder": "Hapus folder", - "deleteFolderConfirm": "Hapus folder \"{{name}}\"? Cuplikan-cuplikannya akan dipindahkan keluar dari folder.", - "createFolderDescription": "Kelompokkan cuplikan yang terkait bersama-sama.", - "editFolderDescription": "Ubah nama, warna, atau ikon folder ini.", - "folderColorLabel": "Warna", - "loadFailed": "Gagal memuat cuplikan", - "importSkipExisting": "Impor (lewati yang sudah ada)", - "importOverwrite": "Impor (timpa yang sudah ada)", - "importInvalidFile": "File tersebut bukan ekspor cuplikan yang valid.", - "importSuccess": "Diimpor {{imported}}, diperbarui {{updated}}, dilewati {{skipped}}", - "exportSnippets": "Ekspor cuplikan", - "exportSuccess": "Cuplikan yang diekspor", - "exportFailed": "Gagal mengekspor cuplikan", - "expandAll": "Buka semua folder", - "collapseAll": "Tutup semua folder", - "docsLink": "Dokumentasi", - "clearSearch": "Hapus pencarian", - "noSearchResults": "Tidak ada cuplikan yang sesuai dengan pencarian Anda.", - "emptyTitle": "Belum ada cuplikan", - "emptyDescription": "Simpan perintah yang sering Anda jalankan, lalu jalankan di terminal dengan sekali klik.", - "shareSnippet": "Bagikan cuplikan", - "shareFolder": "Folder berbagi", - "shareExpiry": "Berakhir", - "shareExpiryOptions": { - "h0": "Tidak pernah", - "h1": "Dalam 1 jam", - "h24": "Dalam 1 hari", - "h168": "Dalam 7 hari", - "h720": "Dalam 30 hari" - }, - "shareButton": "Membagikan", - "shareSuccess": "Dibagikan dengan {{count}}", - "shareFailed": "Gagal berbagi", - "shareLoadFailed": "Gagal memuat detail berbagi.", - "currentAccess": "Akses saat ini", - "notSharedYet": "Belum dibagikan kepada siapa pun", - "revokeAccess": "Menarik kembali", - "revokeSuccess": "Akses dicabut", - "revokeFailed": "Gagal mencabut akses", - "close": "Tutup", - "targetHostsLabel": "Host target", - "targetHostsHint": "Pilih host tempat perintah ini akan dijalankan secara langsung, tanpa terminal terbuka. Biarkan kosong untuk menjalankannya di terminal aktif.", - "clearTargetHosts": "Jernih", - "noHostsAvailable": "Tidak ada tuan rumah yang tersedia.", - "searchHosts": "Cari host", - "runOnTargets": "Jalankan pada host target", - "executionResultTitle": "Hasil untuk \"{{name}}\"", - "executionResultDescription": "Output dari setiap host target.", - "executionSuccess": "Kesuksesan", - "executionFailed": "Gagal", - "directRunSuccess": "Menjalankan \"{{name}}\" pada {{count}} host", - "directRunPartialFail": "\"{{name}}\" gagal pada beberapa host", - "reorderFailed": "Gagal menyusun ulang cuplikan", - "dropToRemoveFromFolder": "Seret di sini untuk keluar dari folder", - "shareSnippetTitle": "Bagikan \"{{name}}\"", - "shareFolderTitle": "Folder berbagi \"{{name}}\"", - "shareWith": "Bagikan dengan", - "usersTab": "Pengguna", - "rolesTab": "Peran", - "shareSearchPlaceholder": "Cari pengguna atau peran", - "shareNoMatches": "Tidak ada kecocokan", - "shareWithCount": "Bagikan dengan {{count}}", - "shareFolderHint": "Membagikan semua cuplikan kode dalam folder ini. Cuplikan kode yang ditambahkan kemudian tidak dibagikan secara otomatis.", - "shareFolderSummary": "Cuplikan {{count}} yang dibagikan di folder ini", - "grantedBy": "Diberikan oleh", - "expired": "Kedaluwarsa", - "variablesDialogTitle": "Jalankan \"{{name}}\"", - "variablesDialogDescription": "Isi nilai-nilai yang dibutuhkan cuplikan kode ini sebelum menjalankannya.", - "variablesPreviewLabel": "Pratinjau", - "variablesConfirmButton": "Berlari", - "keybindings": { - "runSnippet": "Jalankan cuplikan kode yang sudah ada.", - "snippetLabel": "Potongan", - "selectSnippet": "Pilih cuplikan", - "snippetMissing": "cuplikan tidak ditemukan", - "snippetRequired": "Pilih cuplikan" - }, - "paletteHintRun": "Jalankan di tab terminal aktif", - "host": { - "startupTitle": "Cuplikan Startup", - "quickActionsTitle": "Tindakan Cepat", - "quickActionsDescription": "Tindakan cepat muncul sebagai tombol di bilah alat Metrik Host untuk eksekusi perintah sekali klik.", - "noQuickActions": "Belum ada tindakan cepat.", - "buttonLabel": "Label tombol", - "selectSnippet": "Pilih cuplikan...", - "addQuickAction": "Tambahkan Tindakan", - "tabTitle": "Cuplikan", - "startupDescription": "Berjalan di terminal setiap kali terhubung ke host ini. Variabel seperti $HOST diisi; cuplikan yang meminta input dilewati.", - "none": "Tidak ada", - "removeQuickAction": "Hapus tindakan cepat" - }, - "quickActions": { - "running": "Menjalankan {{name}}...", - "succeeded": "{{name}} berhasil diselesaikan", - "failed": "{{name}} gagal", - "error": "Gagal mengeksekusi {{name}}" - }, - "paletteHintPaste": "Tempelkan di tab terminal aktif", - "targetTerminals": "Berlari masuk", - "targetTerminalsHint": "opsional, jika tidak, tab aktif", - "selectAll": "Semua", - "selectNone": "Tidak ada", - "plugin": { - "name": "Cuplikan" - } -} diff --git a/plugins/snippets/locales/translated/it_IT.json b/plugins/snippets/locales/translated/it_IT.json deleted file mode 100644 index 06b810ce2..000000000 --- a/plugins/snippets/locales/translated/it_IT.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Frammenti" - }, - "onboarding": { - "feature_snippets": "Snippet", - "feature_snippets_desc": "Salva i comandi che esegui più spesso e inviali a un terminale con un solo clic." - }, - "permissions": { - "view": { - "title": "Visualizza frammenti", - "description": "Visualizza i tuoi frammenti, le tue note e i frammenti condivisi." - }, - "create": { - "title": "Crea frammenti", - "description": "Crea nuovi frammenti, note e cartelle." - }, - "edit": { - "title": "Modifica frammenti", - "description": "Rinominare o modificare frammenti e cartelle esistenti." - }, - "delete": { - "title": "Elimina frammenti", - "description": "Elimina frammenti e cartelle." - }, - "share": { - "title": "Condividi frammenti", - "description": "Condividi frammenti e cartelle con altri utenti o ruoli." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Chiudi le cartelle per impostazione predefinita", - "description": "Quando apri il pannello Snippet, ogni cartella di snippet viene visualizzata compressa." - }, - "showCommands": { - "label": "Mostra i tasti", - "description": "Mostra la prima riga di ogni comando dello snippet sotto il suo nome." - }, - "confirmExecution": { - "label": "Conferma prima dell'esecuzione", - "description": "Chiedi conferma prima di eseguire un comando nel terminale." - } - }, - "cancel": "Annulla", - "loading": "Caricamento in corso...", - "searchPlaceholder": "Cerca snippet", - "newFolder": "Nuova cartella", - "newSnippet": "Nuovo frammento", - "noFolder": "Nessuna cartella", - "noSnippetsInFolder": "Nessuno snippet in questa cartella", - "noTerminalTabsOpen": "Non è aperta alcuna scheda del terminale.", - "run": "Esegui", - "pasteToTerminal": "Incolla nel terminale", - "copySuccess": "Copiato negli appunti", - "importExport": "Importa / Esporta", - "importFailed": "Importazione snippet non riuscita", - "nameLabel": "Nome", - "namePlaceholder": "Nome snippet", - "descriptionLabel": "Descrizione", - "descriptionPlaceholder": "Descrizione opzionale", - "folderLabel": "Cartella", - "typeLabel": "Tipo", - "typeCommand": "Comando", - "typeNote": "Nota", - "commandLabel": "Comando", - "commandPlaceholder": "Comando da eseguire, ad esempio systemctl status $HOST", - "noteLabel": "Nota", - "notePlaceholder": "Testo da incollare nel terminale", - "createSnippetTitle": "Crea snippet", - "createSnippetButton": "Crea snippet", - "createSuccess": "Snippet creato", - "createFailed": "Impossibile creare lo snippet", - "editSnippetTitle": "Modifica frammento", - "saveSnippetButton": "Salva frammento", - "updateSuccess": "Snippet aggiornato", - "updateFailed": "Impossibile aggiornare lo snippet", - "deleteFailed": "Impossibile eliminare lo snippet", - "createFolderTitle": "Crea cartella", - "createFolderButton": "Crea cartella", - "folderCreateSuccess": "Cartella creata", - "folderCreateFailed": "Impossibile creare la cartella", - "editFolderTitle": "Modifica cartella", - "saveFolderButton": "Salva cartella", - "folderEditSuccess": "Cartella aggiornata", - "folderEditFailed": "Impossibile aggiornare la cartella", - "folderNameLabel": "Nome della cartella", - "folderNamePlaceholder": "Nome della cartella", - "folderIconLabel": "Icona", - "folderDeleteSuccess": "Cartella eliminata", - "folderDeleteFailed": "Impossibile eliminare la cartella", - "confirmRunMessage": "Eseguire \"{{name}}\"?", - "confirmRunButton": "Esegui", - "runSuccess": "Eseguito \"{{name}}\" sul/sui terminale/i {{count}}", - "pasteSuccess": "Incollato \"{{name}}\" nel/nei terminale/i {{count}}", - "admin": { - "tabTitle": "Snippet", - "count": "{{count}} snippet", - "add": "Aggiungi snippet", - "namePlaceholder": "Nome snippet", - "contentPlaceholder": "Contenuto comando", - "folderPlaceholder": "Cartella (opzionale)", - "empty": "Questo utente non ha snippet", - "deleteConfirm": "Eliminare lo snippet \"{{name}}\" appartenente a {{username}}?", - "deleted": "Snippet eliminato", - "deleteFailed": "Impossibile eliminare lo snippet", - "requiredFields": "Nome e contenuto dello snippet sono obbligatori", - "saved": "Snippet salvato", - "saveFailed": "Impossibile salvare lo snippet", - "saving": "Salvataggio in corso...", - "save": "Salva" - }, - "backToSnippets": "Torna agli snippet", - "settingsTitle": "Impostazioni dello snippet", - "settingsDisplayTitle": "Display", - "settingsBehaviorTitle": "Comportamento", - "typeCommandHint": "Esegue nel terminale attivo.", - "typeNoteHint": "Incolla nel terminale attivo senza eseguirlo.", - "variablesHint": "Variabili: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Etichetta}", - "doubleClickToRun": "Doppio clic per eseguire", - "sharedBadge": "Condiviso", - "copyToClipboard": "Copia negli appunti", - "copyFailed": "Copia negli appunti non riuscita", - "moreOptions": "Altre opzioni", - "moreActions": "Altre azioni", - "moveToFolder": "Sposta nella cartella", - "deleteSnippet": "Elimina frammento", - "deleteSnippetConfirm": "Eliminare il frammento \"{{name}}\"?", - "deleteSuccess": "Snippet eliminato", - "delete": "Elimina", - "folderActions": "Azioni della cartella", - "addSnippetHere": "Nuovo frammento qui", - "deleteFolder": "Elimina cartella", - "deleteFolderConfirm": "Eliminare la cartella \"{{name}}\"? I suoi frammenti vengono spostati fuori dalla cartella.", - "createFolderDescription": "Raggruppa i frammenti di testo correlati.", - "editFolderDescription": "Modifica il nome, il colore o l'icona di questa cartella.", - "folderColorLabel": "Colore", - "loadFailed": "Impossibile caricare gli snippet", - "importSkipExisting": "Importa (salta esistenti)", - "importOverwrite": "Importa (sovrascrivi i dati esistenti)", - "importInvalidFile": "Quel file non è un'esportazione di snippet valida.", - "importSuccess": "Importato {{imported}}, aggiornato {{updated}}, saltato {{skipped}}", - "exportSnippets": "Esporta frammenti", - "exportSuccess": "Frammenti esportati", - "exportFailed": "Esportazione snippet non riuscita", - "expandAll": "Espandi tutte le cartelle", - "collapseAll": "Comprimi tutte le cartelle", - "docsLink": "Documentazione", - "clearSearch": "Cancella ricerca", - "noSearchResults": "Nessun frammento corrisponde alla tua ricerca", - "emptyTitle": "Ancora nessun frammento", - "emptyDescription": "Salva i comandi che usi spesso, poi eseguili in un terminale con un solo clic.", - "shareSnippet": "Condividi frammento", - "shareFolder": "Cartella condivisa", - "shareExpiry": "Scadenza", - "shareExpiryOptions": { - "h0": "Mai", - "h1": "Tra 1 ora", - "h24": "In 1 giorno", - "h168": "In 7 giorni", - "h720": "In 30 giorni" - }, - "shareButton": "Condividi", - "shareSuccess": "Condiviso con {{count}}", - "shareFailed": "Impossibile condividere", - "shareLoadFailed": "Impossibile caricare i dettagli di condivisione", - "currentAccess": "Accesso corrente", - "notSharedYet": "Non ancora condiviso con nessuno.", - "revokeAccess": "Revoca", - "revokeSuccess": "Accesso revocato", - "revokeFailed": "Revoca accesso non riuscita", - "close": "Chiudi", - "targetHostsLabel": "Host di destinazione", - "targetHostsHint": "Seleziona gli host su cui eseguire questo comando direttamente, senza un terminale aperto. Lascia vuoto per eseguirlo nel terminale attivo.", - "clearTargetHosts": "Pulisci", - "noHostsAvailable": "Nessun host disponibile", - "searchHosts": "Cerca host", - "runOnTargets": "Eseguire sugli host di destinazione", - "executionResultTitle": "Risultati per \"{{name}}\"", - "executionResultDescription": "Output da ciascun host di destinazione.", - "executionSuccess": "Successo", - "executionFailed": "Fallito", - "directRunSuccess": "Eseguito \"{{name}}\" su {{count}} host", - "directRunPartialFail": "\"{{name}}\" non riuscito su alcuni host", - "reorderFailed": "Impossibile riordinare gli snippet", - "dropToRemoveFromFolder": "Trascina qui per spostare fuori dalla cartella", - "shareSnippetTitle": "Condividi \"{{name}}\"", - "shareFolderTitle": "Condividi la cartella \"{{name}}\"", - "shareWith": "Condividi con", - "usersTab": "Utenti", - "rolesTab": "Ruoli", - "shareSearchPlaceholder": "Cerca utenti o ruoli", - "shareNoMatches": "Nessuna partita", - "shareWithCount": "Condividi con {{count}}", - "shareFolderHint": "Condivide tutti gli snippet presenti in questa cartella. Gli snippet aggiunti successivamente non vengono condivisi automaticamente.", - "shareFolderSummary": "Frammenti condivisi {{count}} in questa cartella", - "grantedBy": "Concesso da", - "expired": "Scaduto", - "variablesDialogTitle": "Eseguire \"{{name}}\"", - "variablesDialogDescription": "Inserisci i valori richiesti da questo frammento di codice prima di eseguirlo.", - "variablesPreviewLabel": "Anteprima", - "variablesConfirmButton": "Esegui", - "keybindings": { - "runSnippet": "Esegui il frammento di codice esistente", - "snippetLabel": "Frammento", - "selectSnippet": "Seleziona un frammento", - "snippetMissing": "frammento non trovato", - "snippetRequired": "Seleziona un frammento" - }, - "paletteHintRun": "Esegui nella scheda del terminale attiva", - "host": { - "startupTitle": "Frammento di avvio", - "quickActionsTitle": "Azioni rapide", - "quickActionsDescription": "Le azioni rapide appaiono come pulsanti nella barra delle metriche host per l'esecuzione di comandi con un clic.", - "noQuickActions": "Nessuna azione rapida ancora.", - "buttonLabel": "Etichetta pulsante", - "selectSnippet": "Seleziona snippet...", - "addQuickAction": "Aggiungi Azione", - "tabTitle": "Snippet", - "startupDescription": "Viene eseguito nel terminale ogni volta che si connette a questo host. Le variabili come $HOST vengono compilate; un frammento di codice che richiede input viene saltato.", - "none": "Nessuno", - "removeQuickAction": "Rimuovi azione rapida" - }, - "quickActions": { - "running": "Esecuzione di {{name}}...", - "succeeded": "{{name}} completato con successo", - "failed": "{{name}} non riuscito", - "error": "Impossibile eseguire {{name}}" - }, - "paletteHintPaste": "Incolla nella scheda del terminale attiva", - "targetTerminals": "Correre in", - "targetTerminalsHint": "facoltativo, altrimenti la scheda attiva", - "selectAll": "Tutte", - "selectNone": "Nessuno", - "plugin": { - "name": "Snippet" - } -} diff --git a/plugins/snippets/locales/translated/ja_JP.json b/plugins/snippets/locales/translated/ja_JP.json deleted file mode 100644 index 24e8aae11..000000000 --- a/plugins/snippets/locales/translated/ja_JP.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "スニペット" - }, - "onboarding": { - "feature_snippets": "スニペット", - "feature_snippets_desc": "よく使うコマンドを保存して、ワンクリックでターミナルに送信できます。" - }, - "permissions": { - "view": { - "title": "抜粋を見る", - "description": "自分のスニペット、メモ、共有されたスニペットを確認できます。" - }, - "create": { - "title": "スニペットを作成する", - "description": "新しいスニペット、メモ、フォルダーを作成します。" - }, - "edit": { - "title": "スニペットを編集する", - "description": "既存のスニペットやフォルダの名前を変更または修正します。" - }, - "delete": { - "title": "スニペットを削除する", - "description": "スニペットとフォルダを削除します。" - }, - "share": { - "title": "抜粋を共有する", - "description": "スニペットやフォルダを他のユーザーやロールと共有します。" - } - }, - "settings": { - "foldersCollapsed": { - "label": "デフォルトでフォルダを折りたたむ", - "description": "スニペットパネルを開いたとき、各スニペットフォルダは折りたたまれた状態で起動します。" - }, - "showCommands": { - "label": "コマンドを表示する", - "description": "各スニペットのコマンドの最初の行を、その名前の下に表示します。" - }, - "confirmExecution": { - "label": "実行前に確認してください", - "description": "ターミナルでコマンドを実行する前に、必ず許可を求めてください。" - } - }, - "cancel": "キャンセル", - "loading": "読み込み中...", - "searchPlaceholder": "スニペットを検索", - "newFolder": "新規フォルダ", - "newSnippet": "新しいスニペット", - "noFolder": "フォルダーなし", - "noSnippetsInFolder": "このフォルダにスニペットはありません", - "noTerminalTabsOpen": "ターミナルタブが開いていません", - "run": "実行", - "pasteToTerminal": "ターミナルに貼り付けてください", - "copySuccess": "クリップボードにコピーしました", - "importExport": "インポート / エクスポート", - "importFailed": "スニペットのインポートに失敗しました", - "nameLabel": "名前", - "namePlaceholder": "スニペット名", - "descriptionLabel": "説明", - "descriptionPlaceholder": "任意の説明", - "folderLabel": "フォルダ", - "typeLabel": "タイプ", - "typeCommand": "コマンド", - "typeNote": "注記", - "commandLabel": "コマンド", - "commandPlaceholder": "実行するコマンド、例:systemctl status $HOST", - "noteLabel": "注記", - "notePlaceholder": "ターミナルに貼り付けるテキスト", - "createSnippetTitle": "スニペットを作成する", - "createSnippetButton": "スニペットを作成する", - "createSuccess": "スニペットが作成されました", - "createFailed": "スニペットの作成に失敗しました", - "editSnippetTitle": "スニペットを編集", - "saveSnippetButton": "スニペットを保存", - "updateSuccess": "スニペットが更新されました", - "updateFailed": "スニペットの更新に失敗しました", - "deleteFailed": "スニペットの削除に失敗しました", - "createFolderTitle": "フォルダの作成", - "createFolderButton": "フォルダの作成", - "folderCreateSuccess": "フォルダが作成されました", - "folderCreateFailed": "フォルダの作成に失敗しました", - "editFolderTitle": "フォルダを編集", - "saveFolderButton": "フォルダーを保存", - "folderEditSuccess": "フォルダが更新されました", - "folderEditFailed": "フォルダの更新に失敗しました", - "folderNameLabel": "フォルダ名", - "folderNamePlaceholder": "フォルダ名", - "folderIconLabel": "アイコン", - "folderDeleteSuccess": "フォルダが削除されました", - "folderDeleteFailed": "フォルダの削除に失敗しました", - "confirmRunMessage": "「{{name}}」を実行しますか?", - "confirmRunButton": "実行", - "runSuccess": "{{name}}を {{count}} 端末で実行しました", - "pasteSuccess": "「{{name}}」を {{count}} 端末に貼り付けました", - "admin": { - "tabTitle": "スニペット", - "count": "{{count}} 個のスニペット", - "add": "スニペットを追加", - "namePlaceholder": "スニペット名", - "contentPlaceholder": "コマンドの内容", - "folderPlaceholder": "フォルダー (任意)", - "empty": "このユーザーはスニペットを持っていません", - "deleteConfirm": "{{username}} に属するスニペット \"{{name}}\" を削除しますか?", - "deleted": "スニペットを削除しました", - "deleteFailed": "スニペットの削除に失敗しました", - "requiredFields": "スニペット名と内容は必須です", - "saved": "スニペットを保存しました", - "saveFailed": "スニペットの保存に失敗しました", - "saving": "保存中...", - "save": "保存" - }, - "backToSnippets": "抜粋に戻る", - "settingsTitle": "スニペット設定", - "settingsDisplayTitle": "画面", - "settingsBehaviorTitle": "動作", - "typeCommandHint": "アクティブな端末で実行されます。", - "typeNoteHint": "実行せずにアクティブなターミナルに貼り付けます。", - "variablesHint": "変数: $HOST、 $USER、 $PORT、 $NAME、 ${INPUT_1:Label}", - "doubleClickToRun": "ダブルクリックして実行", - "sharedBadge": "共有済み", - "copyToClipboard": "クリップボードにコピー", - "copyFailed": "クリップボードへのコピーに失敗しました", - "moreOptions": "その他のオプション", - "moreActions": "その他の操作", - "moveToFolder": "フォルダに移動", - "deleteSnippet": "スニペットを削除", - "deleteSnippetConfirm": "スニペット「{{name}}」を削除しますか?", - "deleteSuccess": "スニペットを削除しました", - "delete": "削除", - "folderActions": "フォルダー操作", - "addSnippetHere": "新しいスニペットはこちら", - "deleteFolder": "フォルダを削除", - "deleteFolderConfirm": "フォルダ \"{{name}}\" を削除しますか?そのスニペットはフォルダから移動します。", - "createFolderDescription": "関連する断片をまとめてグループ化する。", - "editFolderDescription": "このフォルダの名前、色、またはアイコンを変更します。", - "folderColorLabel": "色", - "loadFailed": "スニペットの読み込みに失敗しました", - "importSkipExisting": "インポート (既存をスキップ)", - "importOverwrite": "インポート(既存のものを上書き)", - "importInvalidFile": "そのファイルは有効なスニペットエクスポートではありません", - "importSuccess": "インポート済み {{imported}}、更新済み {{updated}}、スキップ済み {{skipped}}", - "exportSnippets": "スニペットをエクスポートする", - "exportSuccess": "スニペットをエクスポートしました", - "exportFailed": "スニペットのエクスポートに失敗しました", - "expandAll": "すべてのフォルダーを展開", - "collapseAll": "すべてのフォルダーを折りたたむ", - "docsLink": "文書", - "clearSearch": "検索をクリアする", - "noSearchResults": "検索条件に一致するスニペットはありません", - "emptyTitle": "まだスニペットはありません", - "emptyDescription": "よく使うコマンドを保存しておけば、ワンクリックでターミナルから実行できます。", - "shareSnippet": "スニペットを共有", - "shareFolder": "共有フォルダ", - "shareExpiry": "有効期限", - "shareExpiryOptions": { - "h0": "なし", - "h1": "1時間後", - "h24": "1日で", - "h168": "7日後", - "h720": "30日後" - }, - "shareButton": "共有", - "shareSuccess": "{{count}}と共有されています", - "shareFailed": "共有に失敗しました", - "shareLoadFailed": "共有情報の読み込みに失敗しました", - "currentAccess": "現在のアクセス", - "notSharedYet": "まだ誰にも共有されていません", - "revokeAccess": "無効化", - "revokeSuccess": "アクセスが取り消されました", - "revokeFailed": "アクセスの取り消しに失敗しました", - "close": "閉じる", - "targetHostsLabel": "対象ホスト", - "targetHostsHint": "このコマンドをターミナルを開かずに直接実行するホストを選択してください。空欄のままにすると、アクティブなターミナルで実行されます。", - "clearTargetHosts": "クリア", - "noHostsAvailable": "ホストがいません", - "searchHosts": "ホストを検索", - "runOnTargets": "ターゲットホスト上で実行", - "executionResultTitle": "「{{name}}」の検索結果", - "executionResultDescription": "各ターゲットホストからの出力。", - "executionSuccess": "成功", - "executionFailed": "失敗", - "directRunSuccess": "\"{{name}}\" を {{count}} 台のホストで実行しました", - "directRunPartialFail": "「{{name}}」が一部のホストで失敗しました", - "reorderFailed": "スニペットの並べ替えに失敗しました", - "dropToRemoveFromFolder": "フォルダから移動するには、ここにドロップしてください。", - "shareSnippetTitle": "「{{name}} を共有する」", - "shareFolderTitle": "共有フォルダ \"{{name}}\"", - "shareWith": "共有相手", - "usersTab": "ユーザー", - "rolesTab": "ロール", - "shareSearchPlaceholder": "ユーザーまたはロールを検索", - "shareNoMatches": "一致する項目はありません", - "shareWithCount": "{{count}}と共有する", - "shareFolderHint": "このフォルダ内のすべてのスニペットを共有します。後から追加されたスニペットは自動的に共有されません。", - "shareFolderSummary": "このフォルダには共有されたスニペットが {{count}} 個あります。", - "grantedBy": "付与者", - "expired": "期限切れ", - "variablesDialogTitle": "「{{name}} を実行」", - "variablesDialogDescription": "このコードスニペットを実行する前に、必要な値を入力してください。", - "variablesPreviewLabel": "プレビュー", - "variablesConfirmButton": "実行", - "keybindings": { - "runSnippet": "既存のスニペットを実行する", - "snippetLabel": "抜粋", - "selectSnippet": "抜粋を選択してください", - "snippetMissing": "スニペットが見つかりません", - "snippetRequired": "抜粋を選択してください" - }, - "paletteHintRun": "アクティブなターミナルタブで実行します", - "host": { - "startupTitle": "起動時スニペット", - "quickActionsTitle": "クイックアクション", - "quickActionsDescription": "クイックアクションはホストメトリクスツールバーにボタンとして表示され、ワンクリックでコマンドを実行できます。", - "noQuickActions": "クイックアクションはまだありません", - "buttonLabel": "ボタンラベル", - "selectSnippet": "スニペットを選択...", - "addQuickAction": "アクションを追加", - "tabTitle": "スニペット", - "startupDescription": "このホストに接続するたびにターミナルで実行されます。 $HOST のような変数には値が入力され、入力を求めるスニペットはスキップされます。", - "none": "すべて解除", - "removeQuickAction": "クイックアクションを削除する" - }, - "quickActions": { - "running": "{{name}} を実行中...", - "succeeded": "{{name}} が正常に完了しました", - "failed": "{{name}} が失敗しました", - "error": "{{name}} の実行に失敗しました" - }, - "paletteHintPaste": "アクティブなターミナルタブに貼り付けてください", - "targetTerminals": "走る", - "targetTerminalsHint": "オプション、それ以外の場合はアクティブなタブ", - "selectAll": "すべて選択", - "selectNone": "すべて解除", - "plugin": { - "name": "スニペット" - } -} diff --git a/plugins/snippets/locales/translated/ko_KR.json b/plugins/snippets/locales/translated/ko_KR.json deleted file mode 100644 index e5f1f4251..000000000 --- a/plugins/snippets/locales/translated/ko_KR.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "스니펫" - }, - "onboarding": { - "feature_snippets": "스니펫", - "feature_snippets_desc": "자주 실행하는 명령어를 저장하고 한 번의 클릭으로 터미널에 전송하세요." - }, - "permissions": { - "view": { - "title": "미리보기", - "description": "내가 작성한 코드 조각, 메모, 공유한 코드 조각을 확인하세요." - }, - "create": { - "title": "코드 조각 만들기", - "description": "새로운 스니펫, 메모 및 폴더를 만드세요." - }, - "edit": { - "title": "코드 조각 편집", - "description": "기존 코드 조각 및 폴더의 이름을 바꾸거나 변경하세요." - }, - "delete": { - "title": "코드 조각을 삭제하세요", - "description": "스니펫과 폴더를 삭제하세요." - }, - "share": { - "title": "짧은 글 공유하기", - "description": "다른 사용자 또는 역할과 코드 조각 및 폴더를 공유하세요." - } - }, - "settings": { - "foldersCollapsed": { - "label": "기본적으로 폴더 접기", - "description": "스니펫 패널을 열 때 각 스니펫 폴더가 접힌 상태로 시작됩니다." - }, - "showCommands": { - "label": "명령 표시", - "description": "각 코드 조각의 명령 첫 번째 줄을 이름 아래에 표시합니다." - }, - "confirmExecution": { - "label": "실행하기 전에 확인하세요", - "description": "터미널에서 명령어를 실행하기 전에 먼저 허락을 구하세요." - } - }, - "cancel": "취소", - "loading": "로딩 중...", - "searchPlaceholder": "스니펫 검색", - "newFolder": "새 폴더", - "newSnippet": "새로운 스니펫", - "noFolder": "폴더 없음", - "noSnippetsInFolder": "이 폴더에 스니펫이 없습니다", - "noTerminalTabsOpen": "터미널 탭이 열려 있지 않습니다.", - "run": "실행", - "pasteToTerminal": "터미널에 붙여넣기", - "copySuccess": "클립보드에 복사됨", - "importExport": "가져오기 / 내보내기", - "importFailed": "스니펫 가져오기 실패", - "nameLabel": "이름", - "namePlaceholder": "스니펫 이름", - "descriptionLabel": "설명", - "descriptionPlaceholder": "선택 사항 설명", - "folderLabel": "폴더", - "typeLabel": "유형", - "typeCommand": "명령", - "typeNote": "메모", - "commandLabel": "명령", - "commandPlaceholder": "실행할 명령어 (예: systemctl status $HOST)", - "noteLabel": "메모", - "notePlaceholder": "터미널에 붙여넣을 텍스트", - "createSnippetTitle": "코드 조각 생성", - "createSnippetButton": "코드 조각 생성", - "createSuccess": "코드 조각이 생성되었습니다", - "createFailed": "스니펫 생성에 실패했습니다", - "editSnippetTitle": "코드 조각 편집", - "saveSnippetButton": "코드 조각 저장", - "updateSuccess": "스니펫이 업데이트되었습니다.", - "updateFailed": "스니펫 업데이트에 실패했습니다", - "deleteFailed": "스니펫 삭제에 실패했습니다", - "createFolderTitle": "폴더 생성", - "createFolderButton": "폴더 생성", - "folderCreateSuccess": "폴더가 생성되었습니다", - "folderCreateFailed": "폴더 생성에 실패했습니다", - "editFolderTitle": "폴더 편집", - "saveFolderButton": "폴더 저장", - "folderEditSuccess": "폴더가 업데이트되었습니다", - "folderEditFailed": "폴더 업데이트에 실패했습니다", - "folderNameLabel": "폴더 이름", - "folderNamePlaceholder": "폴더 이름", - "folderIconLabel": "아이콘", - "folderDeleteSuccess": "폴더가 삭제되었습니다", - "folderDeleteFailed": "폴더 삭제 실패", - "confirmRunMessage": "\"{{name}}\"을(를) 실행하시겠습니까?", - "confirmRunButton": "실행", - "runSuccess": "{{name}}\"을 {{count}} 터미널에서 실행했습니다.", - "pasteSuccess": "\"{{name}}\"을 {{count}} 터미널에 붙여넣었습니다.", - "admin": { - "tabTitle": "스니펫", - "count": "{{count}}개의 스니펫", - "add": "스니펫 추가", - "namePlaceholder": "스니펫 이름", - "contentPlaceholder": "명령 내용", - "folderPlaceholder": "폴더 (선택 사항)", - "empty": "이 사용자는 스니펫이 없습니다", - "deleteConfirm": "{{username}}님의 스니펫 \"{{name}}\"을 삭제하시겠습니까?", - "deleted": "스니펫이 삭제되었습니다", - "deleteFailed": "스니펫 삭제에 실패했습니다", - "requiredFields": "스니펫 이름과 내용이 필요합니다", - "saved": "스니펫이 저장되었습니다", - "saveFailed": "스니펫 저장에 실패했습니다", - "saving": "저장 중...", - "save": "저장" - }, - "backToSnippets": "스니펫으로 돌아가기", - "settingsTitle": "스니펫 설정", - "settingsDisplayTitle": "표시하다", - "settingsBehaviorTitle": "동작", - "typeCommandHint": "현재 활성화된 터미널에서 실행됩니다.", - "typeNoteHint": "실행하지 않고 활성화된 터미널에 붙여넣습니다.", - "variablesHint": "변수: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "두 번 클릭하여 실행하세요", - "sharedBadge": "공유됨", - "copyToClipboard": "클립보드에 복사", - "copyFailed": "클립보드로 복사하지 못했습니다.", - "moreOptions": "더 많은 옵션", - "moreActions": "더 보기", - "moveToFolder": "폴더로 이동", - "deleteSnippet": "코드 조각 삭제", - "deleteSnippetConfirm": "\"{{name}} \" 부분을 삭제하시겠습니까?", - "deleteSuccess": "스니펫이 삭제되었습니다", - "delete": "삭제", - "folderActions": "폴더 작업", - "addSnippetHere": "여기에 새로운 코드 조각이 있습니다.", - "deleteFolder": "폴더 삭제", - "deleteFolderConfirm": "\"{{name}}\" 폴더를 삭제하시겠습니까? 해당 폴더의 조각들이 폴더 밖으로 이동합니다.", - "createFolderDescription": "관련된 코드 조각들을 함께 그룹화하세요.", - "editFolderDescription": "이 폴더의 이름, 색상 또는 아이콘을 변경하세요.", - "folderColorLabel": "색상", - "loadFailed": "스니펫을 로드하는 데 실패했습니다.", - "importSkipExisting": "가져오기 (기존 건너뛰기)", - "importOverwrite": "가져오기(기존 파일 덮어쓰기)", - "importInvalidFile": "해당 파일은 유효한 스니펫 내보내기 파일이 아닙니다.", - "importSuccess": "{{imported}}가져오기 완료, {{updated}}업데이트 완료, {{skipped}} 건너뛰기 완료", - "exportSnippets": "내보내기 스니펫", - "exportSuccess": "내보낸 코드 조각", - "exportFailed": "스니펫 내보내기 실패", - "expandAll": "모든 폴더 펼치기", - "collapseAll": "모든 폴더 접기", - "docsLink": "문서", - "clearSearch": "검색 결과 지우기", - "noSearchResults": "검색 결과와 일치하는 스니펫이 없습니다.", - "emptyTitle": "아직 설정된 스니펫이 없습니다", - "emptyDescription": "자주 사용하는 명령어를 저장해 두었다가 터미널에서 한 번의 클릭으로 실행하세요.", - "shareSnippet": "스니펫 공유", - "shareFolder": "공유 폴더", - "shareExpiry": "만료", - "shareExpiryOptions": { - "h0": "없음", - "h1": "1시간 안에", - "h24": "하루 만에", - "h168": "7일 안에", - "h720": "30일 안에" - }, - "shareButton": "공유", - "shareSuccess": "{{count}}와 공유됨", - "shareFailed": "공유에 실패했습니다", - "shareLoadFailed": "공유 세부 정보를 불러오는 데 실패했습니다.", - "currentAccess": "현재 접근 권한", - "notSharedYet": "아직 아무에게도 공유하지 않았습니다.", - "revokeAccess": "취소", - "revokeSuccess": "접근 권한이 취소되었습니다", - "revokeFailed": "접근 권한 취소 실패", - "close": "닫기", - "targetHostsLabel": "대상 호스트", - "targetHostsHint": "터미널 창을 열지 않고 이 명령을 직접 실행할 호스트를 선택하세요. 활성 터미널에서 실행하려면 비워 두세요.", - "clearTargetHosts": "비우기", - "noHostsAvailable": "사용 가능한 호스트가 없습니다.", - "searchHosts": "호스트 검색", - "runOnTargets": "대상 호스트에서 실행", - "executionResultTitle": "\"{{name}} \"에 대한 결과", - "executionResultDescription": "각 대상 호스트의 출력입니다.", - "executionSuccess": "성공", - "executionFailed": "실패", - "directRunSuccess": "\"{{name}}\"을(를) {{count}}개 호스트에서 실행했습니다", - "directRunPartialFail": "\"{{name}}\"가 일부 호스트에서 실패했습니다.", - "reorderFailed": "스니펫 순서 변경에 실패했습니다.", - "dropToRemoveFromFolder": "폴더 밖으로 이동하려면 여기에 드롭하세요.", - "shareSnippetTitle": "\"{{name}} \" 공유", - "shareFolderTitle": "공유 폴더 \"{{name}}\"", - "shareWith": "공유 대상", - "usersTab": "사용자", - "rolesTab": "역할", - "shareSearchPlaceholder": "사용자 또는 역할 검색", - "shareNoMatches": "일치하는 항목이 없습니다", - "shareWithCount": "{{count}}와 공유하세요", - "shareFolderHint": "이 폴더에 있는 모든 코드 조각을 공유합니다. 나중에 추가된 코드 조각은 자동으로 공유되지 않습니다.", - "shareFolderSummary": "이 폴더에 공유된 {{count}} 코드 조각", - "grantedBy": "공유자", - "expired": "만료됨", - "variablesDialogTitle": "\"{{name}} \" 실행", - "variablesDialogDescription": "이 코드 조각을 실행하기 전에 필요한 값을 입력하세요.", - "variablesPreviewLabel": "미리보기", - "variablesConfirmButton": "실행", - "keybindings": { - "runSnippet": "기존 코드 조각을 실행하세요", - "snippetLabel": "단편", - "selectSnippet": "코드 조각을 선택하세요", - "snippetMissing": "스니펫을 찾을 수 없습니다.", - "snippetRequired": "코드 조각을 선택하세요" - }, - "paletteHintRun": "현재 활성화된 터미널 탭에서 실행하세요", - "host": { - "startupTitle": "시작 스니펫", - "quickActionsTitle": "빠른 작업", - "quickActionsDescription": "빠른 작업은 한 번의 클릭으로 명령을 실행할 수 있도록 호스트 메트릭 도구 모음에 버튼으로 표시됩니다.", - "noQuickActions": "아직 빠른 작업이 없습니다.", - "buttonLabel": "버튼 레이블", - "selectSnippet": "스니펫 선택...", - "addQuickAction": "작업 추가", - "tabTitle": "스니펫", - "startupDescription": "이 호스트에 연결될 때마다 터미널에서 실행됩니다. $HOST 와 같은 변수가 채워지고, 입력을 요청하는 부분은 건너뜁니다.", - "none": "전체 해제", - "removeQuickAction": "빠른 실행 제거" - }, - "quickActions": { - "running": "{{name}} 실행 중...", - "succeeded": "{{name}}이(가) 성공적으로 완료되었습니다.", - "failed": "{{name}} 실패", - "error": "{{name}} 실행에 실패했습니다." - }, - "paletteHintPaste": "활성화된 터미널 탭에 붙여넣으세요", - "targetTerminals": "달리기", - "targetTerminalsHint": "선택 사항이며, 그렇지 않으면 활성 탭이 표시됩니다.", - "selectAll": "모두", - "selectNone": "전체 해제", - "plugin": { - "name": "스니펫" - } -} diff --git a/plugins/snippets/locales/translated/nl_NL.json b/plugins/snippets/locales/translated/nl_NL.json deleted file mode 100644 index 90a70c0c9..000000000 --- a/plugins/snippets/locales/translated/nl_NL.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Fragmenten" - }, - "onboarding": { - "feature_snippets": "Fragmenten", - "feature_snippets_desc": "Sla de commando's die je vaak gebruikt op en stuur ze met één klik naar een terminal." - }, - "permissions": { - "view": { - "title": "Bekijk fragmenten", - "description": "Bekijk je eigen fragmenten, notities en gedeelde fragmenten." - }, - "create": { - "title": "Maak codefragmenten aan.", - "description": "Maak nieuwe fragmenten, notities en mappen aan." - }, - "edit": { - "title": "Bewerk fragmenten", - "description": "Hernoem of wijzig bestaande codefragmenten en mappen." - }, - "delete": { - "title": "Verwijder fragmenten", - "description": "Verwijder fragmenten en mappen." - }, - "share": { - "title": "Deel fragmenten", - "description": "Deel fragmenten en mappen met andere gebruikers of rollen." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Mappen standaard inklappen", - "description": "Wanneer u het paneel 'Snippets' opent, wordt elke map met codefragmenten standaard ingeklapt weergegeven." - }, - "showCommands": { - "label": "Toon commando's", - "description": "Toon de eerste regel van de opdracht van elk codefragment onder de naam ervan." - }, - "confirmExecution": { - "label": "Controleer voordat u begint.", - "description": "Vraag toestemming voordat een commandofragment in een terminal wordt uitgevoerd." - } - }, - "cancel": "Annuleren", - "loading": "Laden...", - "searchPlaceholder": "Zoekfragmenten", - "newFolder": "Nieuwe map", - "newSnippet": "Nieuw fragment", - "noFolder": "Geen map", - "noSnippetsInFolder": "Geen codefragmenten in deze map", - "noTerminalTabsOpen": "Er is geen terminaltabblad geopend", - "run": "Loop", - "pasteToTerminal": "Plak in de terminal", - "copySuccess": "Gekopieerd naar het klembord", - "importExport": "Import/Export", - "importFailed": "Het importeren van codefragmenten is mislukt.", - "nameLabel": "Naam", - "namePlaceholder": "Fragmentnaam", - "descriptionLabel": "Beschrijving", - "descriptionPlaceholder": "Optionele beschrijving", - "folderLabel": "Map", - "typeLabel": "Type", - "typeCommand": "Commando", - "typeNote": "Opmerking", - "commandLabel": "Commando", - "commandPlaceholder": "Commando om uit te voeren, bijvoorbeeld systemctl status $HOST", - "noteLabel": "Opmerking", - "notePlaceholder": "Tekst om in de terminal te plakken", - "createSnippetTitle": "Maak een codefragment aan", - "createSnippetButton": "Maak een codefragment aan", - "createSuccess": "Fragment gemaakt", - "createFailed": "Het is niet gelukt om een codefragment te maken.", - "editSnippetTitle": "Bewerk fragment", - "saveSnippetButton": "Bewaar fragment", - "updateSuccess": "Fragment bijgewerkt", - "updateFailed": "Het bijwerken van het codefragment is mislukt.", - "deleteFailed": "Het verwijderen van het fragment is mislukt.", - "createFolderTitle": "Map aanmaken", - "createFolderButton": "Map aanmaken", - "folderCreateSuccess": "Map aangemaakt", - "folderCreateFailed": "Map aanmaken mislukt", - "editFolderTitle": "Map bewerken", - "saveFolderButton": "Map opslaan", - "folderEditSuccess": "Map bijgewerkt", - "folderEditFailed": "Het bijwerken van de map is mislukt.", - "folderNameLabel": "Mapnaam", - "folderNamePlaceholder": "Mapnaam", - "folderIconLabel": "Icon", - "folderDeleteSuccess": "Map verwijderd", - "folderDeleteFailed": "Map verwijderen mislukt", - "confirmRunMessage": "Run \"{{name}}\"?", - "confirmRunButton": "Loop", - "runSuccess": "Voerde \"{{name}}\" uit op {{count}} terminal(s)", - "pasteSuccess": "\"{{name}}\" is geplakt in de terminal(s) {{count}}", - "admin": { - "tabTitle": "Fragmenten", - "count": "{{count}} fragmenten", - "add": "Voeg codefragment toe", - "namePlaceholder": "Fragmentnaam", - "contentPlaceholder": "Opdrachtinhoud", - "folderPlaceholder": "Map (optioneel)", - "empty": "Deze gebruiker heeft geen codefragmenten.", - "deleteConfirm": "Verwijder het fragment \"{{name}}\" dat bij {{username}} hoort.", - "deleted": "Fragment verwijderd", - "deleteFailed": "Het verwijderen van het fragment is mislukt.", - "requiredFields": "De naam en inhoud van het codefragment zijn verplicht.", - "saved": "Fragment opgeslagen", - "saveFailed": "Het opslaan van het codefragment is mislukt.", - "saving": "Besparing...", - "save": "Redden" - }, - "backToSnippets": "Terug naar fragmenten", - "settingsTitle": "Snippet-instellingen", - "settingsDisplayTitle": "Weergave", - "settingsBehaviorTitle": "Gedrag", - "typeCommandHint": "Wordt uitgevoerd in de actieve terminal.", - "typeNoteHint": "Plakt in de actieve terminal zonder deze uit te voeren.", - "variablesHint": "Variabelen: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Dubbelklik om te starten", - "sharedBadge": "Gedeeld", - "copyToClipboard": "Kopiëren naar het klembord", - "copyFailed": "Kopiëren naar klembord mislukt", - "moreOptions": "Meer opties", - "moreActions": "Meer acties", - "moveToFolder": "Verplaatsen naar map", - "deleteSnippet": "Verwijder fragment", - "deleteSnippetConfirm": "Verwijder fragment \"{{name}}\"?", - "deleteSuccess": "Fragment verwijderd", - "delete": "Verwijderen", - "folderActions": "Mapacties", - "addSnippetHere": "Nieuw fragment hier", - "deleteFolder": "Map verwijderen", - "deleteFolderConfirm": "Map \"{{name}}\" verwijderen? De fragmenten ervan worden uit de map verplaatst.", - "createFolderDescription": "Groepeer verwante fragmenten bij elkaar.", - "editFolderDescription": "Wijzig de naam, kleur of het pictogram van deze map.", - "folderColorLabel": "Kleur", - "loadFailed": "Het laden van codefragmenten is mislukt.", - "importSkipExisting": "Importeren (bestaande overslaan)", - "importOverwrite": "Importeren (bestaande overschrijven)", - "importInvalidFile": "Dat bestand is geen geldige codefragmentexport.", - "importSuccess": "Geïmporteerd {{imported}}, bijgewerkt {{updated}}, overgeslagen {{skipped}}", - "exportSnippets": "Exporteer fragmenten", - "exportSuccess": "Uittreksels geëxporteerd", - "exportFailed": "Het exporteren van codefragmenten is mislukt.", - "expandAll": "Vouw alle mappen uit", - "collapseAll": "Alle mappen inklappen", - "docsLink": "Documentatie", - "clearSearch": "Zoekopdracht wissen", - "noSearchResults": "Er zijn geen resultaten gevonden die overeenkomen met uw zoekopdracht.", - "emptyTitle": "Nog geen fragmenten beschikbaar", - "emptyDescription": "Sla de commando's die je vaak gebruikt op, zodat je ze vervolgens met één klik in een terminal kunt uitvoeren.", - "shareSnippet": "Deel fragment", - "shareFolder": "Map delen", - "shareExpiry": "Verloopt", - "shareExpiryOptions": { - "h0": "Nooit", - "h1": "Over 1 uur", - "h24": "Binnen 1 dag", - "h168": "Over 7 dagen", - "h720": "Over 30 dagen" - }, - "shareButton": "Deel", - "shareSuccess": "Gedeeld met {{count}}", - "shareFailed": "Delen mislukt", - "shareLoadFailed": "Het laden van de deelgegevens is mislukt.", - "currentAccess": "Huidige toegang", - "notSharedYet": "Nog met niemand gedeeld", - "revokeAccess": "Herroepen", - "revokeSuccess": "Toegang ingetrokken", - "revokeFailed": "Toegang intrekken is mislukt", - "close": "Dichtbij", - "targetHostsLabel": "Doelwithosts", - "targetHostsHint": "Selecteer de hosts waarop u deze opdracht direct wilt uitvoeren, zonder dat er een terminal open is. Laat dit veld leeg om de opdracht in de actieve terminal uit te voeren.", - "clearTargetHosts": "Duidelijk", - "noHostsAvailable": "Geen hosts beschikbaar", - "searchHosts": "Zoek hosts", - "runOnTargets": "Uitvoeren op doelhosts", - "executionResultTitle": "Resultaten voor \"{{name}}\"", - "executionResultDescription": "Uitvoer van elke doelhost.", - "executionSuccess": "Succes", - "executionFailed": "Mislukt", - "directRunSuccess": "Voerde \"{{name}}\" uit op {{count}} host(s)", - "directRunPartialFail": "\"{{name}}\" is op sommige hosts mislukt.", - "reorderFailed": "Het is niet gelukt om de fragmenten opnieuw te ordenen.", - "dropToRemoveFromFolder": "Sleep hierheen om de map te verlaten.", - "shareSnippetTitle": "Delen \"{{name}}\"", - "shareFolderTitle": "Map delen \"{{name}}\"", - "shareWith": "Delen met", - "usersTab": "Gebruikers", - "rolesTab": "Rollen", - "shareSearchPlaceholder": "Zoek gebruikers of rollen", - "shareNoMatches": "Geen overeenkomsten", - "shareWithCount": "Delen met {{count}}", - "shareFolderHint": "Deelt alle codefragmenten in deze map. Later toegevoegde fragmenten worden niet automatisch gedeeld.", - "shareFolderSummary": "Gedeelde {{count}} fragment(en) in deze map", - "grantedBy": "Toegekend door", - "expired": "Verlopen", - "variablesDialogTitle": "Uitvoeren \"{{name}}\"", - "variablesDialogDescription": "Vul de waarden in die dit codefragment nodig heeft voordat je het uitvoert.", - "variablesPreviewLabel": "Voorbeeld", - "variablesConfirmButton": "Loop", - "keybindings": { - "runSnippet": "Voer het bestaande codefragment uit.", - "snippetLabel": "Fragment", - "selectSnippet": "Selecteer een fragment", - "snippetMissing": "fragment niet gevonden", - "snippetRequired": "Selecteer een fragment" - }, - "paletteHintRun": "Voer dit uit in het actieve terminaltabblad.", - "host": { - "startupTitle": "Opstartfragment", - "quickActionsTitle": "Snelle acties", - "quickActionsDescription": "Snelle acties verschijnen als knoppen in de Host Metrics-werkbalk, zodat u opdrachten met één klik kunt uitvoeren.", - "noQuickActions": "Nog geen snelle acties ondernomen.", - "buttonLabel": "Knoplabel", - "selectSnippet": "Selecteer fragment...", - "addQuickAction": "Actie toevoegen", - "tabTitle": "Fragmenten", - "startupDescription": "Wordt elke keer dat er verbinding wordt gemaakt met deze host in de terminal uitgevoerd. Variabelen zoals $HOST worden ingevuld; een codefragment dat om invoer vraagt, wordt overgeslagen.", - "none": "Geen", - "removeQuickAction": "Verwijder de snelle actie" - }, - "quickActions": { - "running": "Het uitvoeren van {{name}}...", - "succeeded": "{{name}} succesvol voltooid", - "failed": "{{name}} mislukt", - "error": "Het uitvoeren van {{name}} is mislukt." - }, - "paletteHintPaste": "Plak dit in het actieve terminaltabblad.", - "targetTerminals": "Ren binnen", - "targetTerminalsHint": "optioneel, anders het actieve tabblad", - "selectAll": "Alle", - "selectNone": "Geen", - "plugin": { - "name": "Fragmenten" - } -} diff --git a/plugins/snippets/locales/translated/no_NO.json b/plugins/snippets/locales/translated/no_NO.json deleted file mode 100644 index c28caed19..000000000 --- a/plugins/snippets/locales/translated/no_NO.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Utdrag" - }, - "onboarding": { - "feature_snippets": "Utdrag", - "feature_snippets_desc": "Lagre kommandoer du kjører ofte og send dem til en terminal med ett klikk." - }, - "permissions": { - "view": { - "title": "Se utdrag", - "description": "Se dine egne utdrag, notater og delte utdrag." - }, - "create": { - "title": "Lag utdrag", - "description": "Opprett nye utdrag, notater og mapper." - }, - "edit": { - "title": "Rediger utdrag", - "description": "Gi nytt navn til eller endre eksisterende utdrag og mapper." - }, - "delete": { - "title": "Slett utdrag", - "description": "Slett utdrag og mapper." - }, - "share": { - "title": "Del utdrag", - "description": "Del utdrag og mapper med andre brukere eller roller." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Skjul mapper som standard", - "description": "Start hver utdragsmappe skjult når du åpner Utdragspanelet." - }, - "showCommands": { - "label": "Vis kommandoer", - "description": "Vis den første linjen i hver kodebits kommando under navnet." - }, - "confirmExecution": { - "label": "Bekreft før kjøring", - "description": "Spør før en kommandosnutt kjøres i en terminal." - } - }, - "cancel": "Avbryt", - "loading": "Laster...", - "searchPlaceholder": "Søk i snippets", - "newFolder": "Ny mappe", - "newSnippet": "Nytt utdrag", - "noFolder": "Ingen mappe", - "noSnippetsInFolder": "Ingen utdrag i denne mappen", - "noTerminalTabsOpen": "Ingen terminalfane er åpen", - "run": "Kjør", - "pasteToTerminal": "Lim inn i terminalen", - "copySuccess": "Kopiert til utklippstavlen", - "importExport": "Import/eksport", - "importFailed": "Kunne ikke importere kodebiter", - "nameLabel": "Navn", - "namePlaceholder": "Navn på utdrag", - "descriptionLabel": "Beskrivelse", - "descriptionPlaceholder": "Valgfri beskrivelse", - "folderLabel": "Mappe", - "typeLabel": "Type", - "typeCommand": "Kommando", - "typeNote": "Note", - "commandLabel": "Kommando", - "commandPlaceholder": "Kommando som skal kjøres, f.eks. systemctl status $HOST", - "noteLabel": "Note", - "notePlaceholder": "Tekst som skal limes inn i terminalen", - "createSnippetTitle": "Opprett kodebit", - "createSnippetButton": "Opprett kodebit", - "createSuccess": "Kodeutdrag opprettet", - "createFailed": "Kunne ikke opprette snippet", - "editSnippetTitle": "Rediger utdrag", - "saveSnippetButton": "Lagre utdrag", - "updateSuccess": "Utdrag oppdatert", - "updateFailed": "Kunne ikke oppdatere snippet", - "deleteFailed": "Kunne ikke slette snippet", - "createFolderTitle": "Opprett mappe", - "createFolderButton": "Opprett mappe", - "folderCreateSuccess": "Mappe opprettet", - "folderCreateFailed": "Kunne ikke opprette mappe", - "editFolderTitle": "Rediger mappe", - "saveFolderButton": "Lagre mappe", - "folderEditSuccess": "Mappen er oppdatert", - "folderEditFailed": "Kunne ikke oppdatere mappen", - "folderNameLabel": "Mappenavn", - "folderNamePlaceholder": "Mappenavn", - "folderIconLabel": "Ikon", - "folderDeleteSuccess": "Mappen er slettet", - "folderDeleteFailed": "Kunne ikke slette mappen", - "confirmRunMessage": "Kjør \"{{name}}\"?", - "confirmRunButton": "Kjør", - "runSuccess": "Kjørte \"{{name}}\" på {{count}} terminal(er)", - "pasteSuccess": "Limte inn «{{name}}» i {{count}} terminal(er)", - "admin": { - "tabTitle": "Utdrag", - "count": "{{count}} utdrag", - "add": "Legg til utdrag", - "namePlaceholder": "Navn på utdrag", - "contentPlaceholder": "Kommandoinnhold", - "folderPlaceholder": "Mappe (valgfritt)", - "empty": "Denne brukeren har ingen utdrag", - "deleteConfirm": "Slett utdraget «{{name}}» som tilhører {{username}}?", - "deleted": "Utdrag slettet", - "deleteFailed": "Kunne ikke slette snippet", - "requiredFields": "Navn og innhold på kodebiten er obligatorisk", - "saved": "Kodestykket er lagret", - "saveFailed": "Kunne ikke lagre utdraget", - "saving": "Lagrer...", - "save": "Lagre" - }, - "backToSnippets": "Tilbake til utdrag", - "settingsTitle": "Innstillinger for utdrag", - "settingsDisplayTitle": "Utstilling", - "settingsBehaviorTitle": "Oppførsel", - "typeCommandHint": "Kjører i den aktive terminalen.", - "typeNoteHint": "Limer inn i den aktive terminalen uten å kjøre.", - "variablesHint": "Variabler: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Etikett}", - "doubleClickToRun": "Dobbeltklikk for å kjøre", - "sharedBadge": "Delt", - "copyToClipboard": "Kopier til utklippstavlen", - "copyFailed": "Kunne ikke kopiere til utklippstavlen", - "moreOptions": "Flere alternativer", - "moreActions": "Flere handlinger", - "moveToFolder": "Flytt til mappe", - "deleteSnippet": "Slett utdrag", - "deleteSnippetConfirm": "Slette utdraget «{{name}}»?", - "deleteSuccess": "Utdrag slettet", - "delete": "Slett", - "folderActions": "Mappehandlinger", - "addSnippetHere": "Nytt utdrag her", - "deleteFolder": "Slett mappe", - "deleteFolderConfirm": "Slett mappen «{{name}}»? Skal utdragene flyttes ut av mappen.", - "createFolderDescription": "Grupper relaterte utdrag sammen.", - "editFolderDescription": "Endre navnet, fargen eller ikonet til denne mappen.", - "folderColorLabel": "Farge", - "loadFailed": "Kunne ikke laste inn utdrag", - "importSkipExisting": "Importer (hopp over eksisterende)", - "importOverwrite": "Importer (overskriv eksisterende)", - "importInvalidFile": "Den filen er ikke en gyldig eksport av snippets", - "importSuccess": "Importert {{imported}}, oppdatert {{updated}}, hoppet over {{skipped}}", - "exportSnippets": "Eksporter utdrag", - "exportSuccess": "Utdrag eksportert", - "exportFailed": "Kunne ikke eksportere utdrag", - "expandAll": "Utvid alle mapper", - "collapseAll": "Skjul alle mapper", - "docsLink": "Dokumentasjon", - "clearSearch": "Tøm søk", - "noSearchResults": "Ingen utdrag samsvarer med søket ditt", - "emptyTitle": "Ingen snippets ennå", - "emptyDescription": "Lagre kommandoer du kjører ofte, og kjør dem deretter i en terminal med ett klikk.", - "shareSnippet": "Del utdrag", - "shareFolder": "Del mappe", - "shareExpiry": "Utløper", - "shareExpiryOptions": { - "h0": "Aldri", - "h1": "Om 1 time", - "h24": "Om 1 dag", - "h168": "Om 7 dager", - "h720": "Om 30 dager" - }, - "shareButton": "Dele", - "shareSuccess": "Delt med {{count}}", - "shareFailed": "Kunne ikke dele", - "shareLoadFailed": "Kunne ikke laste inn delingsdetaljene", - "currentAccess": "Nåværende tilgang", - "notSharedYet": "Ikke delt med noen ennå", - "revokeAccess": "Tilbakekalling", - "revokeSuccess": "Tilgang tilbakekalt", - "revokeFailed": "Kunne ikke tilbakekalle tilgang", - "close": "Lukk", - "targetHostsLabel": "Målverter", - "targetHostsHint": "Velg verter å kjøre denne kommandoen direkte på, uten en åpen terminal. La feltet stå tomt for å kjøre den i den aktive terminalen.", - "clearTargetHosts": "Tøm", - "noHostsAvailable": "Ingen verter tilgjengelig", - "searchHosts": "Søk etter verter", - "runOnTargets": "Kjør på målverter", - "executionResultTitle": "Resultater for «{{name}}»", - "executionResultDescription": "Utdata fra hver målvert.", - "executionSuccess": "Suksess", - "executionFailed": "Mislyktes", - "directRunSuccess": "Kjørte \"{{name}}\" på {{count}} vert(er)", - "directRunPartialFail": "«{{name}}» mislyktes på noen verter", - "reorderFailed": "Kunne ikke endre rekkefølgen på utdragene", - "dropToRemoveFromFolder": "Slipp her for å flytte ut av mappen", - "shareSnippetTitle": "Del «{{name}}»", - "shareFolderTitle": "Del mappen «{{name}}»", - "shareWith": "Del med", - "usersTab": "Brukere", - "rolesTab": "Roller", - "shareSearchPlaceholder": "Søk etter brukere eller roller", - "shareNoMatches": "Ingen treff", - "shareWithCount": "Del med {{count}}", - "shareFolderHint": "Deler alle utdrag i denne mappen. Utdrag som legges til senere deles ikke automatisk.", - "shareFolderSummary": "Delte {{count}} kodebiter i denne mappen", - "grantedBy": "Tildelt av", - "expired": "Utløpt", - "variablesDialogTitle": "Kjør «{{name}}»", - "variablesDialogDescription": "Fyll inn verdiene dette kodestykket trenger før du kjører det.", - "variablesPreviewLabel": "Forhåndsvis", - "variablesConfirmButton": "Kjør", - "keybindings": { - "runSnippet": "Kjør eksisterende kodebit", - "snippetLabel": "Utdrag", - "selectSnippet": "Velg et utdrag", - "snippetMissing": "utdraget ikke funnet", - "snippetRequired": "Velg et utdrag" - }, - "paletteHintRun": "Kjør i den aktive terminalfanen", - "host": { - "startupTitle": "Oppstarts-snippet", - "quickActionsTitle": "Hurtighandlinger", - "quickActionsDescription": "Hurtighandlinger vises som knapper i verktøylinjen for vertsmålinger for utførelse av kommandoer med ett klikk.", - "noQuickActions": "Ingen hurtighandlinger ennå.", - "buttonLabel": "Knappetikett", - "selectSnippet": "Velg snippet...", - "addQuickAction": "Legg til handling", - "tabTitle": "Utdrag", - "startupDescription": "Kjører i terminalen hver gang den kobler seg til denne verten. Variabler som $HOST fylles ut; et kodeutdrag som ber om inndata hoppes over.", - "none": "Ingen", - "removeQuickAction": "Fjern hurtighandling" - }, - "quickActions": { - "running": "Utfører {{name}}...", - "succeeded": "{{name}} fullført", - "failed": "{{name}} mislyktes", - "error": "Kunne ikke utføre {{name}}" - }, - "paletteHintPaste": "Lim inn i den aktive terminalfanen", - "targetTerminals": "Løp inn", - "targetTerminalsHint": "valgfritt, ellers den aktive fanen", - "selectAll": "Alle", - "selectNone": "Ingen", - "plugin": { - "name": "Utdrag" - } -} diff --git a/plugins/snippets/locales/translated/pl_PL.json b/plugins/snippets/locales/translated/pl_PL.json deleted file mode 100644 index 1b40d898e..000000000 --- a/plugins/snippets/locales/translated/pl_PL.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Snippety" - }, - "onboarding": { - "feature_snippets": "Snippety", - "feature_snippets_desc": "Zapisuj często uruchamiane polecenia i wysyłaj je do terminala jednym kliknięciem." - }, - "permissions": { - "view": { - "title": "Wyświetl fragmenty", - "description": "Zobacz własne fragmenty, notatki i udostępnione fragmenty." - }, - "create": { - "title": "Utwórz fragmenty kodu", - "description": "Utwórz nowe fragmenty kodu, notatki i foldery." - }, - "edit": { - "title": "Edytuj fragmenty", - "description": "Zmień nazwę lub zmodyfikuj istniejące fragmenty kodu i foldery." - }, - "delete": { - "title": "Usuń fragmenty", - "description": "Usuń fragmenty kodu i foldery." - }, - "share": { - "title": "Udostępnij fragmenty", - "description": "Udostępniaj fragmenty kodu i foldery innym użytkownikom lub rolom." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Domyślnie zwiń foldery", - "description": "Każdy folder fragmentów kodu powinien być zwinięty po otwarciu panelu Fragmenty kodu." - }, - "showCommands": { - "label": "Pokaż polecenia", - "description": "Wyświetl pierwszy wiersz polecenia każdego fragmentu kodu pod jego nazwą." - }, - "confirmExecution": { - "label": "Potwierdź przed uruchomieniem", - "description": "Zapytaj przed uruchomieniem fragmentu polecenia w terminalu." - } - }, - "cancel": "Anuluj", - "loading": "Ładowanie...", - "searchPlaceholder": "Wyszukaj snippety", - "newFolder": "Nowy folder", - "newSnippet": "Nowy fragment kodu", - "noFolder": "Brak folderu", - "noSnippetsInFolder": "Brak snippetów w tym folderze", - "noTerminalTabsOpen": "Żadna zakładka terminala nie jest otwarta", - "run": "Uruchom", - "pasteToTerminal": "Wklej do terminala", - "copySuccess": "Skopiowano do schowka", - "importExport": "Import/Eksport", - "importFailed": "Nie udało się zaimportować snippetów", - "nameLabel": "Nazwa", - "namePlaceholder": "Nazwa snippetu", - "descriptionLabel": "Opis", - "descriptionPlaceholder": "Opis opcjonalny", - "folderLabel": "Folder", - "typeLabel": "Typ", - "typeCommand": "Polecenie", - "typeNote": "Notatka", - "commandLabel": "Polecenie", - "commandPlaceholder": "Polecenie do uruchomienia, np. systemctl status $HOST", - "noteLabel": "Notatka", - "notePlaceholder": "Tekst do wklejenia do terminala", - "createSnippetTitle": "Utwórz fragment kodu", - "createSnippetButton": "Utwórz fragment kodu", - "createSuccess": "Utworzono fragment kodu", - "createFailed": "Nie udało się utworzyć snippetu", - "editSnippetTitle": "Edytuj fragment", - "saveSnippetButton": "Zapisz fragment kodu", - "updateSuccess": "Zaktualizowano fragment kodu", - "updateFailed": "Nie udało się zaktualizować snippetu", - "deleteFailed": "Nie udało się usunąć snippetu", - "createFolderTitle": "Utwórz folder", - "createFolderButton": "Utwórz folder", - "folderCreateSuccess": "Utworzono folder", - "folderCreateFailed": "Nie udało się utworzyć folderu", - "editFolderTitle": "Edytuj folder", - "saveFolderButton": "Zapisz folder", - "folderEditSuccess": "Folder zaktualizowany", - "folderEditFailed": "Nie udało się zaktualizować folderu", - "folderNameLabel": "Nazwa folderu", - "folderNamePlaceholder": "Nazwa folderu", - "folderIconLabel": "Ikona", - "folderDeleteSuccess": "Folder usunięty", - "folderDeleteFailed": "Nie udało się usunąć folderu", - "confirmRunMessage": "Uruchomić „{{name}}”?", - "confirmRunButton": "Uruchom", - "runSuccess": "Uruchomiono „{{name}}” na terminalach {{count}}", - "pasteSuccess": "Wklejono „{{name}}” do {{count}} terminali", - "admin": { - "tabTitle": "Snippety", - "count": "{{count}} snippetów", - "add": "Dodaj snippet", - "namePlaceholder": "Nazwa snippetu", - "contentPlaceholder": "Treść polecenia", - "folderPlaceholder": "Folder (opcjonalnie)", - "empty": "Ten użytkownik nie ma żadnych snippetów", - "deleteConfirm": "Usunąć snippet „{{name}}” należący do {{username}}?", - "deleted": "Usunięto snippet", - "deleteFailed": "Nie udało się usunąć snippetu", - "requiredFields": "Nazwa i treść snippetu są wymagane", - "saved": "Zapisano snippet", - "saveFailed": "Nie udało się zapisać snippetu", - "saving": "Zapisywanie...", - "save": "Zapisz" - }, - "backToSnippets": "Powrót do fragmentów", - "settingsTitle": "Ustawienia snippetów", - "settingsDisplayTitle": "Wyświetlanie", - "settingsBehaviorTitle": "Zachowanie", - "typeCommandHint": "Działa w aktywnym terminalu.", - "typeNoteHint": "Wkleja do aktywnego terminala bez uruchamiania.", - "variablesHint": "Zmienne: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Kliknij dwukrotnie, aby uruchomić", - "sharedBadge": "Udostępniony", - "copyToClipboard": "Kopiuj do schowka", - "copyFailed": "Nie udało się skopiować do schowka", - "moreOptions": "Więcej opcji", - "moreActions": "Więcej akcji", - "moveToFolder": "Przenieś do folderu", - "deleteSnippet": "Usuń fragment kodu", - "deleteSnippetConfirm": "Usunąć fragment „{{name}}”?", - "deleteSuccess": "Usunięto snippet", - "delete": "Usuń", - "folderActions": "Akcje folderów", - "addSnippetHere": "Nowy fragment tutaj", - "deleteFolder": "Usuń folder", - "deleteFolderConfirm": "Usunąć folder „{{name}}”? Jego fragmenty zostaną przeniesione poza folder.", - "createFolderDescription": "Połącz powiązane ze sobą fragmenty.", - "editFolderDescription": "Zmień nazwę, kolor lub ikonę tego folderu.", - "folderColorLabel": "Kolor", - "loadFailed": "Nie udało się załadować fragmentów kodu", - "importSkipExisting": "Importuj (pomiń istniejące)", - "importOverwrite": "Importuj (nadpisz istniejące)", - "importInvalidFile": "Ten plik nie jest prawidłowym eksportem fragmentów kodu", - "importSuccess": "Zaimportowano {{imported}}, zaktualizowano {{updated}}, pominięto {{skipped}}", - "exportSnippets": "Eksportuj fragmenty kodu", - "exportSuccess": "Wyeksportowano fragmenty kodu", - "exportFailed": "Nie udało się wyeksportować snippetów", - "expandAll": "Rozwiń wszystkie foldery", - "collapseAll": "Zwiń wszystkie foldery", - "docsLink": "Dokumentacja", - "clearSearch": "Wyczyść wyszukiwanie", - "noSearchResults": "Brak fragmentów odpowiadających Twojemu wyszukiwaniu", - "emptyTitle": "Brak fragmentów", - "emptyDescription": "Zapisz często uruchamiane polecenia, a następnie uruchom je w terminalu jednym kliknięciem.", - "shareSnippet": "Udostępnij fragment", - "shareFolder": "Udostępnij folder", - "shareExpiry": "Wygasa", - "shareExpiryOptions": { - "h0": "Nigdy", - "h1": "Za 1 godzinę", - "h24": "W 1 dzień", - "h168": "Za 7 dni", - "h720": "Za 30 dni" - }, - "shareButton": "Udostępnij", - "shareSuccess": "Udostępniono {{count}}", - "shareFailed": "Nie udało się udostępnić", - "shareLoadFailed": "Nie udało się załadować szczegółów udostępniania", - "currentAccess": "Aktualny dostęp", - "notSharedYet": "Jeszcze nikomu nie udostępniono", - "revokeAccess": "Unieważnij", - "revokeSuccess": "Unieważniono dostęp", - "revokeFailed": "Nie udało się unieważnić dostępu", - "close": "Zamknij", - "targetHostsLabel": "Docelowi gospodarze", - "targetHostsHint": "Wybierz hosty, na których chcesz uruchomić to polecenie bezpośrednio, bez otwierania terminala. Pozostaw puste, aby uruchomić je w aktywnym terminalu.", - "clearTargetHosts": "Wyczyść", - "noHostsAvailable": "Brak dostępnych gospodarzy", - "searchHosts": "Wyszukaj hosty", - "runOnTargets": "Uruchom na hostach docelowych", - "executionResultTitle": "Wyniki dla „{{name}}”", - "executionResultDescription": "Dane wyjściowe z każdego hosta docelowego.", - "executionSuccess": "Powodzenie", - "executionFailed": "Niepowodzenie", - "directRunSuccess": "Uruchomiono „{{name}}” na {{count}} hostach", - "directRunPartialFail": "„{{name}}” nie powiodło się na niektórych hostach", - "reorderFailed": "Nie udało się zmienić kolejności fragmentów kodu", - "dropToRemoveFromFolder": "Upuść tutaj, aby wyjść z folderu", - "shareSnippetTitle": "Udostępnij „{{name}}”", - "shareFolderTitle": "Udostępnij folder „{{name}}”", - "shareWith": "Udostępnij", - "usersTab": "Użytkownicy", - "rolesTab": "Role", - "shareSearchPlaceholder": "Wyszukaj użytkowników lub role", - "shareNoMatches": "Brak dopasowań", - "shareWithCount": "Udostępnij za pomocą {{count}}", - "shareFolderHint": "Udostępnia każdy fragment kodu w tym folderze. Fragmenty dodane później nie są udostępniane automatycznie.", - "shareFolderSummary": "Udostępniono {{count}} fragmentów w tym folderze", - "grantedBy": "Przyznane przez", - "expired": "Wygasły", - "variablesDialogTitle": "Uruchom „{{name}}”", - "variablesDialogDescription": "Przed uruchomieniem snippetu wprowadź wymagane wartości.", - "variablesPreviewLabel": "Podgląd", - "variablesConfirmButton": "Uruchom", - "keybindings": { - "runSnippet": "Uruchom istniejący snippet", - "snippetLabel": "Snippet", - "selectSnippet": "Wybierz snippet", - "snippetMissing": "nie znaleziono snippetu", - "snippetRequired": "Wybierz snippet" - }, - "paletteHintRun": "Uruchom w aktywnej karcie terminala", - "host": { - "startupTitle": "Snippet startowy", - "quickActionsTitle": "Szybkie akcje", - "quickActionsDescription": "Szybkie akcje pojawiają się jako przyciski na pasku narzędzi Metryki hosta, umożliwiając wykonanie polecenia jednym kliknięciem.", - "noQuickActions": "Brak szybkich akcji.", - "buttonLabel": "Etykieta przycisku", - "selectSnippet": "Wybierz snippet...", - "addQuickAction": "Dodaj akcję", - "tabTitle": "Snippety", - "startupDescription": "Działa w terminalu za każdym razem, gdy łączy się z tym hostem. Zmienne takie jak $HOST są wypełniane; fragment kodu, który prosi o dane wejściowe, jest pomijany.", - "none": "Żaden", - "removeQuickAction": "Usuń szybką akcję" - }, - "quickActions": { - "running": "Wykonywanie {{name}}...", - "succeeded": "{{name}} ukończono pomyślnie", - "failed": "{{name}} nie powiodło się", - "error": "Nie udało się wykonać {{name}}" - }, - "paletteHintPaste": "Wklej w aktywnej karcie terminala", - "targetTerminals": "Wbiec", - "targetTerminalsHint": "opcjonalnie, w przeciwnym razie aktywna karta", - "selectAll": "Wszystkie", - "selectNone": "Żaden", - "plugin": { - "name": "Snippety" - } -} diff --git a/plugins/snippets/locales/translated/pt_BR.json b/plugins/snippets/locales/translated/pt_BR.json deleted file mode 100644 index 08d2d09ff..000000000 --- a/plugins/snippets/locales/translated/pt_BR.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Snippets" - }, - "onboarding": { - "feature_snippets": "Snippets", - "feature_snippets_desc": "Salve os comandos que você executa com frequência e envie-os para um terminal com um único clique." - }, - "permissions": { - "view": { - "title": "Ver trechos", - "description": "Veja seus próprios trechos, anotações e trechos compartilhados." - }, - "create": { - "title": "Criar trechos", - "description": "Crie novos trechos de código, notas e pastas." - }, - "edit": { - "title": "Editar trechos", - "description": "Renomeie ou altere trechos de código e pastas existentes." - }, - "delete": { - "title": "Excluir trechos", - "description": "Exclua trechos de código e pastas." - }, - "share": { - "title": "Compartilhar trechos", - "description": "Compartilhe trechos de código e pastas com outros usuários ou funções." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Recolher pastas por padrão", - "description": "Ao abrir o painel de Snippets, cada pasta de trechos deve estar recolhida." - }, - "showCommands": { - "label": "Mostrar comandos", - "description": "Exiba a primeira linha do comando de cada trecho de código abaixo do seu nome." - }, - "confirmExecution": { - "label": "Confirme antes de executar.", - "description": "Pergunte antes de executar um trecho de comando no terminal." - } - }, - "cancel": "Cancelar", - "loading": "Carregando...", - "searchPlaceholder": "Trechos de pesquisa", - "newFolder": "Nova pasta", - "newSnippet": "Novo trecho", - "noFolder": "Sem pasta", - "noSnippetsInFolder": "Não há trechos de código nesta pasta.", - "noTerminalTabsOpen": "Nenhuma aba de terminal está aberta.", - "run": "Correr", - "pasteToTerminal": "Cole no terminal", - "copySuccess": "Copiado para a área de transferência", - "importExport": "Importação/Exportação", - "importFailed": "Falha ao importar trechos de código.", - "nameLabel": "Nome", - "namePlaceholder": "Nome do trecho", - "descriptionLabel": "Descrição", - "descriptionPlaceholder": "Descrição opcional", - "folderLabel": "Pasta", - "typeLabel": "Tipo", - "typeCommand": "Comando", - "typeNote": "Observação", - "commandLabel": "Comando", - "commandPlaceholder": "Comando a ser executado, por exemplo, systemctl status $HOST", - "noteLabel": "Observação", - "notePlaceholder": "Texto para colar no terminal", - "createSnippetTitle": "Criar trecho", - "createSnippetButton": "Criar trecho", - "createSuccess": "Trecho criado", - "createFailed": "Falha ao criar o trecho de código", - "editSnippetTitle": "Editar trecho", - "saveSnippetButton": "Salvar trecho", - "updateSuccess": "Trecho atualizado", - "updateFailed": "Falha ao atualizar o trecho de código", - "deleteFailed": "Falha ao excluir o trecho", - "createFolderTitle": "Criar pasta", - "createFolderButton": "Criar pasta", - "folderCreateSuccess": "Pasta criada", - "folderCreateFailed": "Falha ao criar a pasta", - "editFolderTitle": "Pasta de edição", - "saveFolderButton": "Pasta de salvamento", - "folderEditSuccess": "Pasta atualizada", - "folderEditFailed": "Falha ao atualizar a pasta", - "folderNameLabel": "Nome da pasta", - "folderNamePlaceholder": "Nome da pasta", - "folderIconLabel": "Ícone", - "folderDeleteSuccess": "Pasta excluída", - "folderDeleteFailed": "Falha ao excluir a pasta", - "confirmRunMessage": "Executar \"{{name}}\"?", - "confirmRunButton": "Correr", - "runSuccess": "Executado \"{{name}}\" em {{count}} terminal(is)", - "pasteSuccess": "Colado \"{{name}}\" em {{count}} terminal(is)", - "admin": { - "tabTitle": "Snippets", - "count": "{{count}} trechos", - "add": "Adicionar trecho de código", - "namePlaceholder": "Nome do trecho", - "contentPlaceholder": "Conteúdo do comando", - "folderPlaceholder": "Pasta (opcional)", - "empty": "Este usuário não possui trechos de código.", - "deleteConfirm": "Excluir trecho \"{{name}}\" pertencente a {{username}}?", - "deleted": "Trecho excluído", - "deleteFailed": "Falha ao excluir o trecho", - "requiredFields": "O nome e o conteúdo do snippet são obrigatórios.", - "saved": "Trecho salvo", - "saveFailed": "Falha ao salvar o trecho de código", - "saving": "Salvando...", - "save": "Salvar" - }, - "backToSnippets": "Voltar aos trechos", - "settingsTitle": "Configurações de trechos de código", - "settingsDisplayTitle": "Mostrar", - "settingsBehaviorTitle": "Comportamento", - "typeCommandHint": "Executa no terminal ativo.", - "typeNoteHint": "Cola o texto no terminal ativo sem executá-lo.", - "variablesHint": "Variáveis: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Clique duas vezes para executar", - "sharedBadge": "Compartilhado", - "copyToClipboard": "Copiar para a área de transferência", - "copyFailed": "Falha ao copiar para área de transferência", - "moreOptions": "Mais opções", - "moreActions": "Mais ações", - "moveToFolder": "Mover para a pasta", - "deleteSnippet": "Excluir trecho", - "deleteSnippetConfirm": "Excluir trecho \"{{name}}\"?", - "deleteSuccess": "Trecho excluído", - "delete": "Apagar", - "folderActions": "Ações da pasta", - "addSnippetHere": "Novo trecho aqui", - "deleteFolder": "Excluir pasta", - "deleteFolderConfirm": "Excluir pasta \"{{name}}\"? Seus trechos de código serão movidos para fora da pasta.", - "createFolderDescription": "Agrupe trechos relacionados.", - "editFolderDescription": "Alterar o nome, a cor ou o ícone desta pasta.", - "folderColorLabel": "Cor", - "loadFailed": "Falha ao carregar os trechos de código.", - "importSkipExisting": "Importar (ignorar existente)", - "importOverwrite": "Importar (sobrescrever existente)", - "importInvalidFile": "Esse arquivo não é uma exportação de trechos válida.", - "importSuccess": "Importado {{imported}}, atualizado {{updated}}, ignorado {{skipped}}", - "exportSnippets": "Exportar trechos", - "exportSuccess": "Trechos exportados", - "exportFailed": "Falha ao exportar trechos de código.", - "expandAll": "Expandir todas as pastas", - "collapseAll": "Recolher todas as pastas", - "docsLink": "Documentação", - "clearSearch": "Limpar pesquisa", - "noSearchResults": "Nenhum trecho corresponde à sua pesquisa.", - "emptyTitle": "Ainda não há trechos de código.", - "emptyDescription": "Salve os comandos que você executa com frequência e, em seguida, execute-os em um terminal com um único clique.", - "shareSnippet": "Compartilhar trecho", - "shareFolder": "Pasta compartilhada", - "shareExpiry": "Expira", - "shareExpiryOptions": { - "h0": "Nunca", - "h1": "Em 1 hora", - "h24": "Em 1 dia", - "h168": "Em 7 dias", - "h720": "Em 30 dias" - }, - "shareButton": "Compartilhar", - "shareSuccess": "Compartilhado com {{count}}", - "shareFailed": "Não foi possível compartilhar", - "shareLoadFailed": "Não foi possível carregar os detalhes de compartilhamento.", - "currentAccess": "Acesso atual", - "notSharedYet": "Ainda não compartilhei com ninguém.", - "revokeAccess": "Revogar", - "revokeSuccess": "Acesso revogado", - "revokeFailed": "Falha ao revogar o acesso", - "close": "Fechar", - "targetHostsLabel": "A Target hospeda", - "targetHostsHint": "Selecione os hosts nos quais deseja executar este comando diretamente, sem abrir um terminal. Deixe em branco para executá-lo no terminal ativo.", - "clearTargetHosts": "Limpar", - "noHostsAvailable": "Nenhum anfitrião disponível", - "searchHosts": "Pesquisar anfitriões", - "runOnTargets": "Executar em hosts de destino", - "executionResultTitle": "Resultados para \"{{name}}\"", - "executionResultDescription": "Saída de cada host de destino.", - "executionSuccess": "Sucesso", - "executionFailed": "Fracassado", - "directRunSuccess": "Executado \"{{name}}\" em {{count}} host(s)", - "directRunPartialFail": "\"{{name}}\" falhou em alguns hosts", - "reorderFailed": "Falha ao reordenar os trechos de código.", - "dropToRemoveFromFolder": "Solte aqui para sair da pasta.", - "shareSnippetTitle": "Compartilhar \"{{name}}\"", - "shareFolderTitle": "Pasta compartilhada \"{{name}}\"", - "shareWith": "Compartilhe com", - "usersTab": "Usuários", - "rolesTab": "Funções", - "shareSearchPlaceholder": "Pesquisar usuários ou funções", - "shareNoMatches": "Nenhuma correspondência", - "shareWithCount": "Compartilhe com {{count}}", - "shareFolderHint": "Compartilha todos os trechos de código nesta pasta. Trechos adicionados posteriormente não são compartilhados automaticamente.", - "shareFolderSummary": "Trechos compartilhados {{count}} nesta pasta", - "grantedBy": "Concedido por", - "expired": "Expirado", - "variablesDialogTitle": "Executar \"{{name}}\"", - "variablesDialogDescription": "Preencha os valores necessários para este trecho de código antes de executá-lo.", - "variablesPreviewLabel": "Pré-visualização", - "variablesConfirmButton": "Correr", - "keybindings": { - "runSnippet": "Executar trecho de código existente", - "snippetLabel": "Trecho", - "selectSnippet": "Selecione um trecho", - "snippetMissing": "trecho não encontrado", - "snippetRequired": "Selecione um trecho" - }, - "paletteHintRun": "Execute na aba do terminal ativo", - "host": { - "startupTitle": "Snippet Inicial", - "quickActionsTitle": "Ações Rápidas", - "quickActionsDescription": "As ações rápidas aparecem como botões na barra de ferramentas do Host Metrics para execução de comandos com um único clique.", - "noQuickActions": "Nenhuma ação imediata ainda.", - "buttonLabel": "Rótulo do botão", - "selectSnippet": "Selecionar snippet...", - "addQuickAction": "Adicionar ação", - "tabTitle": "Snippets", - "startupDescription": "Executa no terminal sempre que se conecta a este host. Variáveis como $HOST são preenchidas; um trecho que solicita entradas é ignorado.", - "none": "Nenhum", - "removeQuickAction": "Remover ação rápida" - }, - "quickActions": { - "running": "Executando {{name}}...", - "succeeded": "{{name}} concluído com sucesso", - "failed": "{{name}} falhou", - "error": "Falha ao executar {{name}}" - }, - "paletteHintPaste": "Cole na aba do terminal ativo", - "targetTerminals": "Correr em", - "targetTerminalsHint": "Opcional, caso contrário, a aba ativa.", - "selectAll": "Todos", - "selectNone": "Nenhum", - "plugin": { - "name": "Snippets" - } -} diff --git a/plugins/snippets/locales/translated/pt_PT.json b/plugins/snippets/locales/translated/pt_PT.json deleted file mode 100644 index 50e323565..000000000 --- a/plugins/snippets/locales/translated/pt_PT.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Fragmentos" - }, - "onboarding": { - "feature_snippets": "Snippets", - "feature_snippets_desc": "Guarde os comandos que executa com frequência e envie-os para um terminal com um único clique." - }, - "permissions": { - "view": { - "title": "Ver excertos", - "description": "Veja os seus próprios excertos, anotações e excertos partilhados." - }, - "create": { - "title": "Criar excertos", - "description": "Crie novos excertos de código, notas e pastas." - }, - "edit": { - "title": "Editar excertos", - "description": "Renomeie ou altere os excertos de código e pastas existentes." - }, - "delete": { - "title": "Excluir trechos", - "description": "Apague trechos de código e pastas." - }, - "share": { - "title": "Partilhar trechos", - "description": "Partilhe excertos de código e pastas com outros utilizadores ou funções." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Recolher pastas por predefinição", - "description": "Ao abrir o painel Snippets, cada pasta de snippets de código deve estar recolhida." - }, - "showCommands": { - "label": "Mostrar comandos", - "description": "Apresente a primeira linha do comando de cada trecho de código abaixo do seu nome." - }, - "confirmExecution": { - "label": "Confirme antes de executar.", - "description": "Pergunte antes de executar um trecho de comando no terminal." - } - }, - "cancel": "Cancelar", - "loading": "A carregar...", - "searchPlaceholder": "Pesquisar snippets", - "newFolder": "Nova pasta", - "newSnippet": "Novo trecho", - "noFolder": "Sem pasta", - "noSnippetsInFolder": "Nenhum snippet nesta pasta", - "noTerminalTabsOpen": "Nenhuma patilha de terminal está aberta.", - "run": "Executar", - "pasteToTerminal": "Cole no terminal", - "copySuccess": "Copiado para a área de transferência.", - "importExport": "Importar / Exportar", - "importFailed": "Falha ao importar fragmentos", - "nameLabel": "Nome", - "namePlaceholder": "Nome do snippet", - "descriptionLabel": "Descrição", - "descriptionPlaceholder": "Descrição opcional", - "folderLabel": "Pasta", - "typeLabel": "Tipo", - "typeCommand": "Comando", - "typeNote": "Nota", - "commandLabel": "Comando", - "commandPlaceholder": "Comando a executar, por exemplo, systemctl status $HOST", - "noteLabel": "Nota", - "notePlaceholder": "Texto para colar no terminal", - "createSnippetTitle": "Criar excerto", - "createSnippetButton": "Criar excerto", - "createSuccess": "Trecho criado", - "createFailed": "Falha ao criar snippet", - "editSnippetTitle": "Editar trecho", - "saveSnippetButton": "Guardar trecho", - "updateSuccess": "Trecho atualizado", - "updateFailed": "Falha ao atualizar snippet", - "deleteFailed": "Falha ao eliminar snippet", - "createFolderTitle": "Criar pasta", - "createFolderButton": "Criar pasta", - "folderCreateSuccess": "Pasta criada", - "folderCreateFailed": "Falha ao criar pasta", - "editFolderTitle": "Editar pasta", - "saveFolderButton": "Guardar pasta", - "folderEditSuccess": "Pasta atualizada", - "folderEditFailed": "Falha ao atualizar pasta", - "folderNameLabel": "Nome da pasta", - "folderNamePlaceholder": "Nome da pasta", - "folderIconLabel": "Ícone", - "folderDeleteSuccess": "Pasta excluída", - "folderDeleteFailed": "Falha ao eliminar pasta", - "confirmRunMessage": "Executar \"{{name}}\"?", - "confirmRunButton": "Executar", - "runSuccess": "Executado \"{{name}}\" em {{count}} terminal(is)", - "pasteSuccess": "Colado \"{{name}}\" em {{count}} terminal(is)", - "admin": { - "tabTitle": "Snippets", - "count": "{{count}} snippets", - "add": "Adicionar snippet", - "namePlaceholder": "Nome do snippet", - "contentPlaceholder": "Conteúdo do comando", - "folderPlaceholder": "Pasta (opcional)", - "empty": "Este utilizador não tem snippets", - "deleteConfirm": "Eliminar snippet \"{{name}}\" de {{username}}?", - "deleted": "Snippet eliminado", - "deleteFailed": "Falha ao eliminar snippet", - "requiredFields": "O nome e o conteúdo do snippet são obrigatórios", - "saved": "Snippet guardado", - "saveFailed": "Falha ao guardar snippet", - "saving": "A guardar...", - "save": "Guardar" - }, - "backToSnippets": "Voltar aos excertos", - "settingsTitle": "Configurações de excertos de código", - "settingsDisplayTitle": "Ecrã", - "settingsBehaviorTitle": "Comportamento", - "typeCommandHint": "Executa no terminal ativo.", - "typeNoteHint": "Cola o texto no terminal ativo sem o executar.", - "variablesHint": "Variáveis: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Clique duas vezes para executar", - "sharedBadge": "Partilhado", - "copyToClipboard": "Copiar para a área de transferência", - "copyFailed": "Falha ao copiar para a área de transferência", - "moreOptions": "Mais opções", - "moreActions": "Mais ações", - "moveToFolder": "Mover para a pasta", - "deleteSnippet": "Excluir trecho", - "deleteSnippetConfirm": "Apagar excerto \"{{name}}\"?", - "deleteSuccess": "Snippet eliminado", - "delete": "Eliminar", - "folderActions": "Ações da pasta", - "addSnippetHere": "Novo excerto aqui", - "deleteFolder": "Eliminar pasta", - "deleteFolderConfirm": "Apagar pasta \"{{name}}\"? Os seus trechos de código serão movidos para fora da pasta.", - "createFolderDescription": "Agrupe excertos relacionados.", - "editFolderDescription": "Alterar o nome, a cor ou o ícone desta pasta.", - "folderColorLabel": "Cor", - "loadFailed": "Falha ao carregar os excertos de código.", - "importSkipExisting": "Importar (ignorar existentes)", - "importOverwrite": "Importar (sobrescrever existente)", - "importInvalidFile": "Este ficheiro não é uma exportação de trechos válida.", - "importSuccess": "Importado {{imported}}, atualizado {{updated}}, ignorado {{skipped}}", - "exportSnippets": "Exportar trechos", - "exportSuccess": "Trechos exportados", - "exportFailed": "Falha ao exportar fragmentos", - "expandAll": "Expandir todas as pastas", - "collapseAll": "Recolher todas as pastas", - "docsLink": "Documentação", - "clearSearch": "Limpar pesquisa", - "noSearchResults": "Nenhum excerto corresponde à sua pesquisa.", - "emptyTitle": "Ainda não há excertos de código.", - "emptyDescription": "Guarde os comandos que executa com frequência e, em seguida, execute-os num terminal com um único clique.", - "shareSnippet": "Partilhar trecho", - "shareFolder": "Pasta partilhada", - "shareExpiry": "Expira", - "shareExpiryOptions": { - "h0": "Nunca", - "h1": "Em 1 hora", - "h24": "Em 1 dia", - "h168": "Em 7 dias", - "h720": "Em 30 dias" - }, - "shareButton": "Partilhar", - "shareSuccess": "Partilhado com {{count}}", - "shareFailed": "Não foi possível partilhar", - "shareLoadFailed": "Não foi possível carregar os detalhes de partilha.", - "currentAccess": "Acesso atual", - "notSharedYet": "Ainda não partilhei com ninguém.", - "revokeAccess": "Revogar", - "revokeSuccess": "Acesso revogado", - "revokeFailed": "Falha ao revogar acesso", - "close": "Fechar", - "targetHostsLabel": "A Target hospeda", - "targetHostsHint": "Selecione os hosts nos quais pretende executar este comando diretamente, sem abrir um terminal. Deixe em branco para o executar no terminal ativo.", - "clearTargetHosts": "Limpar", - "noHostsAvailable": "Nenhum anfitrião disponível", - "searchHosts": "Pesquisar hosts", - "runOnTargets": "Executar em hosts de destino", - "executionResultTitle": "Resultados para \"{{name}}\"", - "executionResultDescription": "Saída de cada host de destino.", - "executionSuccess": "Sucesso", - "executionFailed": "Falhou", - "directRunSuccess": "Executou \"{{name}}\" em {{count}} máquina(s)", - "directRunPartialFail": "\"{{name}}\" falhou em alguns hosts", - "reorderFailed": "Falha ao reordenar os excertos de código.", - "dropToRemoveFromFolder": "Solte aqui para sair da pasta.", - "shareSnippetTitle": "Partilhar \"{{name}}\"", - "shareFolderTitle": "Pasta partilhada \"{{name}}\"", - "shareWith": "Partilhar com", - "usersTab": "Utilizadores", - "rolesTab": "Funções", - "shareSearchPlaceholder": "Procurar utilizadores ou funções", - "shareNoMatches": "Nenhuma correspondência", - "shareWithCount": "Partilhar com {{count}}", - "shareFolderHint": "Partilha todos os excertos de código nesta pasta. Os trechos adicionados posteriormente não são partilhados automaticamente.", - "shareFolderSummary": "Excertos partilhados {{count}} nesta pasta", - "grantedBy": "Concedido por", - "expired": "Expirado", - "variablesDialogTitle": "Executar \"{{name}}\"", - "variablesDialogDescription": "Preencha os valores necessários para este excerto de código antes de o executar.", - "variablesPreviewLabel": "Pré-visualizar", - "variablesConfirmButton": "Executar", - "keybindings": { - "runSnippet": "Executar excerto de código existente", - "snippetLabel": "Trecho", - "selectSnippet": "Selecione um excerto", - "snippetMissing": "trecho não encontrado", - "snippetRequired": "Selecione um excerto" - }, - "paletteHintRun": "Execute no separador do terminal ativo", - "host": { - "startupTitle": "Fragmento de inicialização", - "quickActionsTitle": "Ações Rápidas", - "quickActionsDescription": "As ações rápidas aparecem como botões na barra de ferramentas de Métricas do Host para execução de comandos com um clique.", - "noQuickActions": "Ainda não existem ações rápidas.", - "buttonLabel": "Texto do botão", - "selectSnippet": "Selecionar snippet...", - "addQuickAction": "Adicionar Ação", - "tabTitle": "Snippets", - "startupDescription": "Executa no terminal sempre que se liga a este host. As variáveis como $HOST são preenchidas; um troço que solicita entradas é ignorado.", - "none": "Nenhuma", - "removeQuickAction": "Remover ação rápida" - }, - "quickActions": { - "running": "A executar {{name}}...", - "succeeded": "{{name}} concluído com sucesso", - "failed": "{{name}} falhou", - "error": "Falha ao executar {{name}}" - }, - "paletteHintPaste": "Cole no separador do terminal ativo", - "targetTerminals": "Correr em", - "targetTerminalsHint": "Opcional, caso contrário, o separador ativo.", - "selectAll": "Todas", - "selectNone": "Nenhuma", - "plugin": { - "name": "Snippets" - } -} diff --git a/plugins/snippets/locales/translated/ro_RO.json b/plugins/snippets/locales/translated/ro_RO.json deleted file mode 100644 index 5ea3d4ab8..000000000 --- a/plugins/snippets/locales/translated/ro_RO.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Fragmente" - }, - "onboarding": { - "feature_snippets": "Fragmente", - "feature_snippets_desc": "Salvează comenzile pe care le execuți des și trimite-le către un terminal cu un singur clic." - }, - "permissions": { - "view": { - "title": "Vizualizați fragmente", - "description": "Vedeți propriile fragmente, notițe și fragmente partajate." - }, - "create": { - "title": "Creați fragmente", - "description": "Creați fragmente, note și foldere noi." - }, - "edit": { - "title": "Editați fragmentele", - "description": "Redenumiți sau modificați fragmentele și folderele existente." - }, - "delete": { - "title": "Ștergeți fragmentele", - "description": "Ștergeți fragmente și foldere." - }, - "share": { - "title": "Partajați fragmente", - "description": "Partajați fragmente și foldere cu alți utilizatori sau roluri." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Restrângeți folderele în mod implicit", - "description": "Pornește fiecare folder de fragmente restrâns când deschizi panoul Fragmente." - }, - "showCommands": { - "label": "Afișați comenzile", - "description": "Afișează prima linie a comenzii fiecărui fragment de cod sub numele său." - }, - "confirmExecution": { - "label": "Confirmați înainte de rulare", - "description": "Întreabă înainte ca un fragment de comandă să ruleze într-un terminal." - } - }, - "cancel": "Anula", - "loading": "Încărcare...", - "searchPlaceholder": "Fragmente de căutare", - "newFolder": "Dosar nou", - "newSnippet": "Fragment nou", - "noFolder": "Niciun dosar", - "noSnippetsInFolder": "Niciun fragment în acest dosar", - "noTerminalTabsOpen": "Nicio filă de terminal nu este deschisă", - "run": "Aleargă", - "pasteToTerminal": "Lipire în terminal", - "copySuccess": "Copiat în clipboard", - "importExport": "Import / Export", - "importFailed": "Importarea fragmentelor a eșuat", - "nameLabel": "Nume", - "namePlaceholder": "Nume fragment", - "descriptionLabel": "Descriere", - "descriptionPlaceholder": "Descriere opțională", - "folderLabel": "Pliant", - "typeLabel": "Tip", - "typeCommand": "Comanda", - "typeNote": "Nota", - "commandLabel": "Comanda", - "commandPlaceholder": "Comandă de rulat, de exemplu, systemctl status $HOST", - "noteLabel": "Nota", - "notePlaceholder": "Text de lipit în terminal", - "createSnippetTitle": "Creați fragment", - "createSnippetButton": "Creați fragment", - "createSuccess": "Fragment creat", - "createFailed": "Nu s-a putut crea fragmentul", - "editSnippetTitle": "Editați fragmentul", - "saveSnippetButton": "Salvați fragmentul", - "updateSuccess": "Fragment actualizat", - "updateFailed": "Fragmentul nu a putut fi actualizat", - "deleteFailed": "Nu s-a putut șterge fragmentul", - "createFolderTitle": "Creați un folder", - "createFolderButton": "Creați un folder", - "folderCreateSuccess": "Dosar creat", - "folderCreateFailed": "Nu s-a putut crea folderul", - "editFolderTitle": "Editați folderul", - "saveFolderButton": "Salvați folderul", - "folderEditSuccess": "Dosar actualizat", - "folderEditFailed": "Nu s-a putut actualiza folderul", - "folderNameLabel": "Numele folderului", - "folderNamePlaceholder": "Numele folderului", - "folderIconLabel": "Pictogramă", - "folderDeleteSuccess": "Dosar șters", - "folderDeleteFailed": "Ștergerea folderului a eșuat", - "confirmRunMessage": "Executați „{{name}}”?", - "confirmRunButton": "Aleargă", - "runSuccess": "A rulat „{{name}}” pe terminalul(ele) {{count}}", - "pasteSuccess": "„{{name}}” a fost lipit în terminalul(ele) {{count}}", - "admin": { - "tabTitle": "Fragmente", - "count": "fragmente {{count}}", - "add": "Adăugați fragment", - "namePlaceholder": "Nume fragment", - "contentPlaceholder": "Conținutul comenzii", - "folderPlaceholder": "Dosar (opțional)", - "empty": "Acest utilizator nu are fragmente", - "deleteConfirm": "Ștergeți fragmentul „{{name}}” aparținând lui {{username}}?", - "deleted": "Fragment șters", - "deleteFailed": "Nu s-a putut șterge fragmentul", - "requiredFields": "Numele și conținutul fragmentului sunt obligatorii", - "saved": "Fragment salvat", - "saveFailed": "Nu s-a putut salva fragmentul", - "saving": "Economisire...", - "save": "Salva" - }, - "backToSnippets": "Înapoi la fragmente", - "settingsTitle": "Setări fragment", - "settingsDisplayTitle": "Afişa", - "settingsBehaviorTitle": "Comportament", - "typeCommandHint": "Rulează în terminalul activ.", - "typeNoteHint": "Lipește în terminalul activ fără a rula.", - "variablesHint": "Variabile: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Etichetă}", - "doubleClickToRun": "Dublu clic pentru a rula", - "sharedBadge": "Partajat", - "copyToClipboard": "Copiați în clipboard", - "copyFailed": "Copierea în clipboard nu a reușit", - "moreOptions": "Mai multe opțiuni", - "moreActions": "Mai multe acțiuni", - "moveToFolder": "Mută în folder", - "deleteSnippet": "Ștergeți fragmentul", - "deleteSnippetConfirm": "Ștergeți fragmentul „{{name}}”?", - "deleteSuccess": "Fragment șters", - "delete": "Şterge", - "folderActions": "Acțiuni în folder", - "addSnippetHere": "Fragment nou aici", - "deleteFolder": "Ștergeți folderul", - "deleteFolderConfirm": "Ștergeți folderul „{{name}}”? Fragmentele sale sunt mutate în afara folderului.", - "createFolderDescription": "Grupează fragmentele corelate.", - "editFolderDescription": "Schimbă numele, culoarea sau pictograma acestui folder.", - "folderColorLabel": "Culoare", - "loadFailed": "Nu s-au putut încărca fragmentele", - "importSkipExisting": "Import (săriți peste elementele existente)", - "importOverwrite": "Import (suprascrierea existentă)", - "importInvalidFile": "Fișierul respectiv nu este un export valid de fragmente.", - "importSuccess": "Importat {{imported}}, actualizat {{updated}}, omis {{skipped}}", - "exportSnippets": "Exportați fragmente", - "exportSuccess": "Fragmente exportate", - "exportFailed": "Exportul fragmentelor a eșuat", - "expandAll": "Extindeți toate folderele", - "collapseAll": "Restrângeți toate folderele", - "docsLink": "Documentare", - "clearSearch": "Șterge căutarea", - "noSearchResults": "Niciun fragment nu corespunde căutării dvs.", - "emptyTitle": "Niciun fragment încă", - "emptyDescription": "Salvează comenzile pe care le execuți des, apoi rulează-le într-un terminal cu un singur clic.", - "shareSnippet": "Partajați fragmentul", - "shareFolder": "Partajați folderul", - "shareExpiry": "Expiră", - "shareExpiryOptions": { - "h0": "Nu", - "h1": "Într-o oră", - "h24": "Într-o zi", - "h168": "În 7 zile", - "h720": "În 30 de zile" - }, - "shareButton": "Distribuie", - "shareSuccess": "Partajat cu {{count}}", - "shareFailed": "Partajarea nu a reușit", - "shareLoadFailed": "Nu s-au putut încărca detaliile de partajare", - "currentAccess": "Acces curent", - "notSharedYet": "Nu a fost încă distribuit nimănui", - "revokeAccess": "Revoca", - "revokeSuccess": "Acces revocat", - "revokeFailed": "Nu s-a putut revoca accesul", - "close": "Aproape", - "targetHostsLabel": "Gazde țintă", - "targetHostsHint": "Alegeți gazdele pe care să rulați această comandă direct, fără un terminal deschis. Lăsați câmpul gol pentru a o rula în terminalul activ.", - "clearTargetHosts": "Clar", - "noHostsAvailable": "Nicio gazdă disponibilă", - "searchHosts": "Căutați gazde", - "runOnTargets": "Executare pe gazdele țintă", - "executionResultTitle": "Rezultate pentru „{{name}}”", - "executionResultDescription": "Ieșire de la fiecare gazdă țintă.", - "executionSuccess": "Succes", - "executionFailed": "Eșuat", - "directRunSuccess": "A rulat „{{name}}” pe gazdă(e) {{count}}", - "directRunPartialFail": "„{{name}}” a eșuat pe unele gazde", - "reorderFailed": "Nu s-au putut reordona fragmentele", - "dropToRemoveFromFolder": "Plasați aici pentru a ieși din folder", - "shareSnippetTitle": "Distribuie „{{name}}”", - "shareFolderTitle": "Partajați folderul „{{name}}”", - "shareWith": "Distribuiți cu", - "usersTab": "Utilizatori", - "rolesTab": "Roluri", - "shareSearchPlaceholder": "Căutați utilizatori sau roluri", - "shareNoMatches": "Nicio potrivire", - "shareWithCount": "Distribuie cu {{count}}", - "shareFolderHint": "Partajează fiecare fragment din acest folder. Fragmentele adăugate ulterior nu sunt partajate automat.", - "shareFolderSummary": "Fragmente {{count}} partajate în acest folder", - "grantedBy": "Acordat de", - "expired": "Expirat", - "variablesDialogTitle": "Executați „{{name}}”", - "variablesDialogDescription": "Completați valorile necesare acestui fragment de cod înainte de a-l rula.", - "variablesPreviewLabel": "Previzualizare", - "variablesConfirmButton": "Aleargă", - "keybindings": { - "runSnippet": "Execută fragmentul existent", - "snippetLabel": "Fragment", - "selectSnippet": "Selectați un fragment", - "snippetMissing": "fragmentul nu a fost găsit", - "snippetRequired": "Selectați un fragment" - }, - "paletteHintRun": "Execută în fila activă a terminalului", - "host": { - "startupTitle": "Fragment de pornire", - "quickActionsTitle": "Acțiuni rapide", - "quickActionsDescription": "Acțiunile rapide apar sub formă de butoane în bara de instrumente Host Metrics pentru executarea comenzilor cu un singur clic.", - "noQuickActions": "Nicio acțiune rapidă încă.", - "buttonLabel": "Etichetă buton", - "selectSnippet": "Selectați fragmentul...", - "addQuickAction": "Adăugați o acțiune", - "tabTitle": "Fragmente", - "startupDescription": "Rulează în terminal de fiecare dată când se conectează la această gazdă. Variabilele precum $HOST sunt completate; un fragment care solicită intrări este omis.", - "none": "Nici unul", - "removeQuickAction": "Eliminați acțiunea rapidă" - }, - "quickActions": { - "running": "Se execută {{name}}...", - "succeeded": "{{name}} finalizat cu succes", - "failed": "{{name}} a eșuat", - "error": "Executarea {{name}} a eșuat" - }, - "paletteHintPaste": "Lipiți în fila activă a terminalului", - "targetTerminals": "Rulare în", - "targetTerminalsHint": "opțional, altfel fila activă", - "selectAll": "Toate", - "selectNone": "Nici unul", - "plugin": { - "name": "Fragmente" - } -} diff --git a/plugins/snippets/locales/translated/ru_RU.json b/plugins/snippets/locales/translated/ru_RU.json deleted file mode 100644 index da9066fe2..000000000 --- a/plugins/snippets/locales/translated/ru_RU.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Сниппеты" - }, - "onboarding": { - "feature_snippets": "Сниппеты", - "feature_snippets_desc": "Сохраняйте часто выполняемые команды и отправляйте их на терминал одним щелчком мыши." - }, - "permissions": { - "view": { - "title": "Просмотреть фрагменты", - "description": "Просматривайте свои собственные фрагменты текста, заметки и фрагменты текста, которыми поделились другие пользователи." - }, - "create": { - "title": "Создавайте фрагменты текста", - "description": "Создавайте новые фрагменты текста, заметки и папки." - }, - "edit": { - "title": "Редактировать фрагменты", - "description": "Переименуйте или измените существующие фрагменты кода и папки." - }, - "delete": { - "title": "Удалить фрагменты", - "description": "Удалите фрагменты текста и папки." - }, - "share": { - "title": "Делитесь фрагментами", - "description": "Делитесь фрагментами кода и папками с другими пользователями или ролями." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Сворачивать папки по умолчанию", - "description": "При открытии панели «Фрагменты» каждая папка с фрагментами кода должна быть свернута." - }, - "showCommands": { - "label": "Показать команды", - "description": "Отобразите первую строку команды каждого фрагмента кода под его именем." - }, - "confirmExecution": { - "label": "Перед запуском подтвердите.", - "description": "Перед запуском фрагмента команды в терминале необходимо запросить подтверждение." - } - }, - "cancel": "Отмена", - "loading": "Загрузка...", - "searchPlaceholder": "Поиск сниппетов", - "newFolder": "Новая папка", - "newSnippet": "Новый фрагмент", - "noFolder": "Без папки", - "noSnippetsInFolder": "В этой папке нет сниппетов", - "noTerminalTabsOpen": "Вкладка терминала не открыта", - "run": "Запустить", - "pasteToTerminal": "Вставить в терминал", - "copySuccess": "Скопировано в буфер обмена", - "importExport": "Импорт / Экспорт", - "importFailed": "Не удалось импортировать сниппеты", - "nameLabel": "Название", - "namePlaceholder": "Имя сниппета", - "descriptionLabel": "Описание", - "descriptionPlaceholder": "Необязательное описание", - "folderLabel": "Папка", - "typeLabel": "Тип", - "typeCommand": "Команда", - "typeNote": "Примечание", - "commandLabel": "Команда", - "commandPlaceholder": "Команда для выполнения, например, systemctl status $HOST", - "noteLabel": "Примечание", - "notePlaceholder": "Текст для вставки в терминал", - "createSnippetTitle": "Создать фрагмент", - "createSnippetButton": "Создать фрагмент", - "createSuccess": "Создан фрагмент кода.", - "createFailed": "Не удалось создать сниппет", - "editSnippetTitle": "Редактировать фрагмент", - "saveSnippetButton": "Сохранить фрагмент", - "updateSuccess": "Фрагмент обновлен", - "updateFailed": "Не удалось обновить сниппет", - "deleteFailed": "Не удалось удалить сниппет", - "createFolderTitle": "Создать папку", - "createFolderButton": "Создать папку", - "folderCreateSuccess": "Создана папка", - "folderCreateFailed": "Не удалось создать папку", - "editFolderTitle": "Редактировать папку", - "saveFolderButton": "Сохранить папку", - "folderEditSuccess": "Папка обновлена", - "folderEditFailed": "Не удалось обновить папку", - "folderNameLabel": "Имя папки", - "folderNamePlaceholder": "Имя папки", - "folderIconLabel": "Значок", - "folderDeleteSuccess": "Папка удалена", - "folderDeleteFailed": "Не удалось удалить папку", - "confirmRunMessage": "Запустить \"{{name}}\"?", - "confirmRunButton": "Запустить", - "runSuccess": "Выполнено \"{{name}}\" на {{count}} терминале(ах)", - "pasteSuccess": "Вставлено \"{{name}}\" в {{count}} терминал(ы)", - "admin": { - "tabTitle": "Сниппеты", - "count": "{{count}} сниппетов", - "add": "Добавить сниппет", - "namePlaceholder": "Имя сниппета", - "contentPlaceholder": "Содержимое команды", - "folderPlaceholder": "Папка (необязательно)", - "empty": "У этого пользователя нет сниппетов", - "deleteConfirm": "Удалить сниппет \"{{name}}\", принадлежащий {{username}}?", - "deleted": "Сниппет удалён", - "deleteFailed": "Не удалось удалить сниппет", - "requiredFields": "Имя и содержимое сниппета обязательны", - "saved": "Сниппет сохранён", - "saveFailed": "Не удалось сохранить сниппет", - "saving": "Сохранение...", - "save": "Сохранить" - }, - "backToSnippets": "Вернуться к фрагментам", - "settingsTitle": "Настройки фрагмента", - "settingsDisplayTitle": "Отображать", - "settingsBehaviorTitle": "Поведение", - "typeCommandHint": "Запускается в активном терминале.", - "typeNoteHint": "Вставляется в активный терминал без запуска.", - "variablesHint": "Переменные: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Двойной щелчок для запуска", - "sharedBadge": "Общий", - "copyToClipboard": "Скопировать в буфер обмена", - "copyFailed": "Не удалось скопировать в буфер обмена", - "moreOptions": "Больше вариантов", - "moreActions": "Дополнительные действия", - "moveToFolder": "Переместить в папку", - "deleteSnippet": "Удалить фрагмент", - "deleteSnippetConfirm": "Удалить фрагмент \"{{name}}\"?", - "deleteSuccess": "Сниппет удалён", - "delete": "Удалить", - "folderActions": "Действия с папками", - "addSnippetHere": "Новый фрагмент здесь", - "deleteFolder": "Удалить папку", - "deleteFolderConfirm": "Удалить папку \"{{name}}\"? Ее фрагменты перемещаются за пределы папки.", - "createFolderDescription": "Сгруппируйте связанные фрагменты текста вместе.", - "editFolderDescription": "Измените название, цвет или значок этой папки.", - "folderColorLabel": "Цвет", - "loadFailed": "Не удалось загрузить фрагменты.", - "importSkipExisting": "Импорт (пропустить существующие)", - "importOverwrite": "Импорт (замена существующего)", - "importInvalidFile": "Этот файл не является допустимым экспортом фрагментов кода.", - "importSuccess": "Импортировано {{imported}}, обновлено {{updated}}, пропущено {{skipped}}", - "exportSnippets": "Экспорт фрагментов", - "exportSuccess": "Экспорт фрагментов", - "exportFailed": "Не удалось экспортировать сниппеты", - "expandAll": "Развернуть все папки", - "collapseAll": "Свернуть все папки", - "docsLink": "Документация", - "clearSearch": "Очистить поиск", - "noSearchResults": "Поисковые запросы не дали результатов.", - "emptyTitle": "Пока нет фрагментов.", - "emptyDescription": "Сохраняйте часто используемые команды, а затем запускайте их в терминале одним щелчком мыши.", - "shareSnippet": "Поделиться фрагментом", - "shareFolder": "Общая папка", - "shareExpiry": "Истекает", - "shareExpiryOptions": { - "h0": "Никогда", - "h1": "Через 1 час", - "h24": "За 1 день", - "h168": "Через 7 дней", - "h720": "Через 30 дней" - }, - "shareButton": "Поделиться", - "shareSuccess": "Поделились с {{count}}", - "shareFailed": "Не удалось поделиться", - "shareLoadFailed": "Не удалось загрузить данные о распределении нагрузки.", - "currentAccess": "Текущий доступ", - "notSharedYet": "Пока никому не сообщалось", - "revokeAccess": "Отозвать", - "revokeSuccess": "Доступ отозван", - "revokeFailed": "Не удалось отозвать доступ", - "close": "Закрыть", - "targetHostsLabel": "Целевые хосты", - "targetHostsHint": "Выберите хосты, на которых будет выполняться эта команда напрямую, без открытого терминала. Оставьте поле пустым, чтобы выполнить команду в активном терминале.", - "clearTargetHosts": "Очистить", - "noHostsAvailable": "Нет доступных хостов", - "searchHosts": "Поиск хостов", - "runOnTargets": "Запуск на целевых хостах", - "executionResultTitle": "Результаты поиска по запросу \"{{name}}\"", - "executionResultDescription": "Вывод с каждого целевого хоста.", - "executionSuccess": "Успех", - "executionFailed": "Ошибка", - "directRunSuccess": "Запущен \"{{name}}\" на {{count}} хосте(ах)", - "directRunPartialFail": "\"{{name}}\" не удалось на некоторых хостах", - "reorderFailed": "Не удалось изменить порядок фрагментов.", - "dropToRemoveFromFolder": "Перетащите курсор сюда, чтобы выйти из папки.", - "shareSnippetTitle": "Поделиться \"{{name}}\"", - "shareFolderTitle": "Общая папка \"{{name}}\"", - "shareWith": "Поделиться с", - "usersTab": "Пользователи", - "rolesTab": "Роли", - "shareSearchPlaceholder": "Поиск пользователей или ролей", - "shareNoMatches": "Совпадений нет", - "shareWithCount": "Поделитесь с {{count}}", - "shareFolderHint": "Предоставляет доступ ко всем фрагментам кода в этой папке. Фрагменты, добавленные позже, не передаются автоматически.", - "shareFolderSummary": "В этой папке находятся общие фрагменты кода {{count}}.", - "grantedBy": "Кем предоставлен", - "expired": "Истек", - "variablesDialogTitle": "Запустить \"{{name}}\"", - "variablesDialogDescription": "Перед запуском заполните необходимые для этого фрагмента значения.", - "variablesPreviewLabel": "Предпросмотр", - "variablesConfirmButton": "Запустить", - "keybindings": { - "runSnippet": "Запустить существующий фрагмент", - "snippetLabel": "Фрагмент", - "selectSnippet": "Выберите фрагмент", - "snippetMissing": "фрагмент не найден", - "snippetRequired": "Выберите фрагмент" - }, - "paletteHintRun": "Запустите в активной вкладке терминала.", - "host": { - "startupTitle": "Фрагмент запуска", - "quickActionsTitle": "Быстрые действия", - "quickActionsDescription": "Быстрые действия отображаются в виде кнопок на панели инструментов метрик хоста для выполнения команд в один клик.", - "noQuickActions": "Пока нет быстрых действий.", - "buttonLabel": "Надпись кнопки", - "selectSnippet": "Выбрать сниппет...", - "addQuickAction": "Добавить действие", - "tabTitle": "Сниппеты", - "startupDescription": "Программа запускается в терминале каждый раз при подключении к этому хосту. Переменные типа $HOST заполняются; фрагмент кода, запрашивающий ввод, пропускается.", - "none": "Ни одного", - "removeQuickAction": "Удалить быстрое действие" - }, - "quickActions": { - "running": "Выполняется {{name}}...", - "succeeded": "{{name}} успешно завершено", - "failed": "{{name}} не удалось", - "error": "Не удалось выполнить {{name}}" - }, - "paletteHintPaste": "Вставьте текст во вкладку активного терминала.", - "targetTerminals": "Забег", - "targetTerminalsHint": "необязателен, в противном случае активная вкладка.", - "selectAll": "Все", - "selectNone": "Ни одного", - "plugin": { - "name": "Сниппеты" - } -} diff --git a/plugins/snippets/locales/translated/sr_SP.json b/plugins/snippets/locales/translated/sr_SP.json deleted file mode 100644 index 5d104577f..000000000 --- a/plugins/snippets/locales/translated/sr_SP.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Исечци" - }, - "onboarding": { - "feature_snippets": "Исечци", - "feature_snippets_desc": "Сачувајте команде које често покрећете и пошаљите их у терминал једним кликом." - }, - "permissions": { - "view": { - "title": "Погледајте фрагменте", - "description": "Погледајте своје сопствене фрагменте, белешке и дељене фрагменте." - }, - "create": { - "title": "Направите фрагменте", - "description": "Креирајте нове исечке, белешке и фасцикле." - }, - "edit": { - "title": "Уреди фрагменте", - "description": "Преименујте или промените постојеће фрагменте и фасцикле." - }, - "delete": { - "title": "Обриши фрагменте", - "description": "Обришите фрагменте и фасцикле." - }, - "share": { - "title": "Делите фрагменте", - "description": "Делите фрагменте и фасцикле са другим корисницима или улогама." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Сажми фасцикле подразумевано", - "description": "Покрените сваку фасциклу исечака сакупљену када отворите панел Исечци." - }, - "showCommands": { - "label": "Прикажи команде", - "description": "Прикажи први ред команде сваког фрагмента испод његовог имена." - }, - "confirmExecution": { - "label": "Потврдите пре покретања", - "description": "Питај пре него што се фрагмент команде покрене у терминалу." - } - }, - "cancel": "Откажи", - "loading": "Учитавање...", - "searchPlaceholder": "Претрага исечака", - "newFolder": "Нова фасцикла", - "newSnippet": "Нови исечак", - "noFolder": "Нема фасцикле", - "noSnippetsInFolder": "Нема фрагмената у овој фасцикли", - "noTerminalTabsOpen": "Ниједна картица терминала није отворена", - "run": "Трчи", - "pasteToTerminal": "Налепи у терминал", - "copySuccess": "Копирано у међуспремник", - "importExport": "Увоз / Извоз", - "importFailed": "Увоз фрагмената није успео", - "nameLabel": "Име", - "namePlaceholder": "Назив исечка", - "descriptionLabel": "Опис", - "descriptionPlaceholder": "Опциони опис", - "folderLabel": "Фасцикла", - "typeLabel": "Тип", - "typeCommand": "Команда", - "typeNote": "Напомена", - "commandLabel": "Команда", - "commandPlaceholder": "Команда за покретање, нпр. systemctl status $HOST", - "noteLabel": "Напомена", - "notePlaceholder": "Текст који треба налепити у терминал", - "createSnippetTitle": "Направи исечак", - "createSnippetButton": "Направи исечак", - "createSuccess": "Исечак је направљен", - "createFailed": "Није успело креирање фрагмента", - "editSnippetTitle": "Измени исечак", - "saveSnippetButton": "Сачувај исечак", - "updateSuccess": "Исечак је ажуриран", - "updateFailed": "Ажурирање исечка није успело", - "deleteFailed": "Брисање исечка није успело", - "createFolderTitle": "Направи фасциклу", - "createFolderButton": "Направи фасциклу", - "folderCreateSuccess": "Фасцикла је направљена", - "folderCreateFailed": "Није успело креирање фасцикле", - "editFolderTitle": "Уреди фасциклу", - "saveFolderButton": "Сачувај фасциклу", - "folderEditSuccess": "Фолдер је ажуриран", - "folderEditFailed": "Ажурирање фасцикле није успело", - "folderNameLabel": "Назив фасцикле", - "folderNamePlaceholder": "Назив фасцикле", - "folderIconLabel": "Икона", - "folderDeleteSuccess": "Фолдер је обрисан", - "folderDeleteFailed": "Брисање фасцикле није успело", - "confirmRunMessage": "Покрени „{{name}}“?", - "confirmRunButton": "Трчи", - "runSuccess": "Покренуто је „{{name}}“ на {{count}} терминалу(има)", - "pasteSuccess": "Налепљено је „{{name}}“ у {{count}} терминал(е)", - "admin": { - "tabTitle": "Исечци", - "count": "{{count}} исечци", - "add": "Додај исечак", - "namePlaceholder": "Назив исечка", - "contentPlaceholder": "Садржај команде", - "folderPlaceholder": "Фасцикла (опционо)", - "empty": "Овај корисник нема исечке кода", - "deleteConfirm": "Да ли желите да обришете исечак „{{name}}“ који припада {{username}}?", - "deleted": "Исечак је обрисан", - "deleteFailed": "Брисање исечка није успело", - "requiredFields": "Назив и садржај фрагмента су обавезни", - "saved": "Исечак је сачуван", - "saveFailed": "Чување фрагмента није успело", - "saving": "Чување...", - "save": "Сачувај" - }, - "backToSnippets": "Назад на фрагменте", - "settingsTitle": "Подешавања исечка", - "settingsDisplayTitle": "Приказ", - "settingsBehaviorTitle": "Понашање", - "typeCommandHint": "Ради у активном терминалу.", - "typeNoteHint": "Лепи у активни терминал без покретања.", - "variablesHint": "Променљиве: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Ознака}", - "doubleClickToRun": "Двапут кликните да бисте покренули", - "sharedBadge": "Дељено", - "copyToClipboard": "Копирај у међуспремник", - "copyFailed": "Копирање у међуспремник није успело", - "moreOptions": "Више опција", - "moreActions": "Више акција", - "moveToFolder": "Премести у фасциклу", - "deleteSnippet": "Обриши исечак", - "deleteSnippetConfirm": "Обришите исечак „{{name}}“?", - "deleteSuccess": "Исечак је обрисан", - "delete": "Обриши", - "folderActions": "Радње са фасциклама", - "addSnippetHere": "Нови исечак овде", - "deleteFolder": "Обриши фасциклу", - "deleteFolderConfirm": "Обришите фасциклу „{{name}}“? Њени фрагменти се премештају ван фасцикле.", - "createFolderDescription": "Групишите повезане фрагменте заједно.", - "editFolderDescription": "Промените име, боју или икону ове фасцикле.", - "folderColorLabel": "Боја", - "loadFailed": "Учитавање фрагмената није успело", - "importSkipExisting": "Увези (прескочи постојеће)", - "importOverwrite": "Увези (пребриши постојеће)", - "importInvalidFile": "Та датотека није важећи извоз фрагмената", - "importSuccess": "Увезено {{imported}}, ажурирано {{updated}}, прескочено {{skipped}}", - "exportSnippets": "Извоз исечака", - "exportSuccess": "Извезени фрагменти", - "exportFailed": "Извоз фрагмената није успео", - "expandAll": "Прошири све фасцикле", - "collapseAll": "Скупи све фасцикле", - "docsLink": "Документација", - "clearSearch": "Обриши претрагу", - "noSearchResults": "Ниједан фрагмент не одговара вашој претрази", - "emptyTitle": "Још нема фрагмената", - "emptyDescription": "Сачувајте команде које често покрећете, а затим их покрените у терминалу једним кликом.", - "shareSnippet": "Дели исечак", - "shareFolder": "Дели фасциклу", - "shareExpiry": "Истиче", - "shareExpiryOptions": { - "h0": "Никад", - "h1": "За 1 сат", - "h24": "За 1 дан", - "h168": "За 7 дана", - "h720": "За 30 дана" - }, - "shareButton": "Дели", - "shareSuccess": "Дељено са {{count}}", - "shareFailed": "Дељење није успело", - "shareLoadFailed": "Учитавање детаља о дељењу није успело", - "currentAccess": "Тренутни приступ", - "notSharedYet": "Још није дељено ни са ким", - "revokeAccess": "Поништи", - "revokeSuccess": "Приступ је опозван", - "revokeFailed": "Опозивање приступа није успело", - "close": "Затвори", - "targetHostsLabel": "Циљни хостови", - "targetHostsHint": "Изаберите хостове на којима ћете директно покренути ову команду, без отвореног терминала. Оставите празно да бисте је покренули у активном терминалу.", - "clearTargetHosts": "Јасно", - "noHostsAvailable": "Нема доступних хостова", - "searchHosts": "Претрага хостова", - "runOnTargets": "Покрени на циљним хостовима", - "executionResultTitle": "Резултати за „{{name}}“", - "executionResultDescription": "Излаз са сваког циљног хоста.", - "executionSuccess": "Успех", - "executionFailed": "Неуспешно", - "directRunSuccess": "Покренуто „{{name}}“ на {{count}} хосту(има)", - "directRunPartialFail": "„{{name}}“ није успело на неким хостовима", - "reorderFailed": "Промена редоследа фрагмената није успела", - "dropToRemoveFromFolder": "Отпустите овде да бисте изашли из фасцикле", - "shareSnippetTitle": "Подели „{{name}}“", - "shareFolderTitle": "Дели фасциклу „{{name}}“", - "shareWith": "Подели са", - "usersTab": "Корисници", - "rolesTab": "Улоге", - "shareSearchPlaceholder": "Претражите кориснике или улоге", - "shareNoMatches": "Нема подударања", - "shareWithCount": "Подели са {{count}}", - "shareFolderHint": "Дели сваки исечак у овој фасцикли. Исечци додати касније се не деле аутоматски.", - "shareFolderSummary": "Дељено {{count}} фрагмената у овој фасцикли", - "grantedBy": "Додељено од стране", - "expired": "Истекло", - "variablesDialogTitle": "Покрени „{{name}}“", - "variablesDialogDescription": "Унесите вредности које су потребне овом фрагменту пре него што га покренете.", - "variablesPreviewLabel": "Преглед", - "variablesConfirmButton": "Трчи", - "keybindings": { - "runSnippet": "Покрени постојећи исечак кода", - "snippetLabel": "Исечак", - "selectSnippet": "Изаберите исечак", - "snippetMissing": "фрагмент није пронађен", - "snippetRequired": "Изаберите исечак" - }, - "paletteHintRun": "Покрени у активној картици терминала", - "host": { - "startupTitle": "Исечак за покретање", - "quickActionsTitle": "Брзе акције", - "quickActionsDescription": "Брзе акције се појављују као дугмад у траци са алаткама Host Metrics за извршавање команди једним кликом.", - "noQuickActions": "Још нема брзих акција.", - "buttonLabel": "Ознака дугмета", - "selectSnippet": "Изаберите исечак...", - "addQuickAction": "Додај акцију", - "tabTitle": "Исечци", - "startupDescription": "Покреће се у терминалу сваки пут када се повеже са овим хостом. Променљиве попут $HOST су попуњене; исечак који тражи уносе је прескочен.", - "none": "Ниједан", - "removeQuickAction": "Уклони брзу акцију" - }, - "quickActions": { - "running": "Извршава се {{name}}...", - "succeeded": "{{name}} успешно завршено", - "failed": "{{name}} није успело", - "error": "Није успело извршавање {{name}}" - }, - "paletteHintPaste": "Налепи у активну картицу терминала", - "targetTerminals": "Утрчавање", - "targetTerminalsHint": "опционо, иначе активна картица", - "selectAll": "Сви", - "selectNone": "Ниједан", - "plugin": { - "name": "Исечци" - } -} diff --git a/plugins/snippets/locales/translated/sv_SE.json b/plugins/snippets/locales/translated/sv_SE.json deleted file mode 100644 index 5d14015ce..000000000 --- a/plugins/snippets/locales/translated/sv_SE.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Snuttar" - }, - "onboarding": { - "feature_snippets": "Snuttar", - "feature_snippets_desc": "Spara kommandon du kör ofta och skicka dem till en terminal med ett enda klick." - }, - "permissions": { - "view": { - "title": "Visa utdrag", - "description": "Se dina egna utdrag, anteckningar och delade utdrag." - }, - "create": { - "title": "Skapa utdrag", - "description": "Skapa nya utdrag, anteckningar och mappar." - }, - "edit": { - "title": "Redigera utdrag", - "description": "Byt namn på eller ändra befintliga utdrag och mappar." - }, - "delete": { - "title": "Ta bort utdrag", - "description": "Ta bort utdrag och mappar." - }, - "share": { - "title": "Dela utdrag", - "description": "Dela utdrag och mappar med andra användare eller roller." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Komprimera mappar som standard", - "description": "Börja varje kodavsnittsmapp hopfälld när du öppnar kodavsnittspanelen." - }, - "showCommands": { - "label": "Visa kommandon", - "description": "Visa den första raden i varje kodavsnitts kommando under dess namn." - }, - "confirmExecution": { - "label": "Bekräfta innan du kör", - "description": "Fråga innan ett kommandosnutt körs i en terminal." - } - }, - "cancel": "Avboka", - "loading": "Belastning...", - "searchPlaceholder": "Sök utdrag", - "newFolder": "Ny mapp", - "newSnippet": "Nytt utdrag", - "noFolder": "Ingen mapp", - "noSnippetsInFolder": "Inga utdrag i den här mappen", - "noTerminalTabsOpen": "Ingen terminalflik är öppen", - "run": "Sikt", - "pasteToTerminal": "Klistra in i terminalen", - "copySuccess": "Kopierad till urklipp", - "importExport": "Importera / Exportera", - "importFailed": "Misslyckades med att importera utdrag", - "nameLabel": "Namn", - "namePlaceholder": "Utdragsnamn", - "descriptionLabel": "Beskrivning", - "descriptionPlaceholder": "Valfri beskrivning", - "folderLabel": "Mapp", - "typeLabel": "Typ", - "typeCommand": "Kommando", - "typeNote": "Notera", - "commandLabel": "Kommando", - "commandPlaceholder": "Kommando att köra, t.ex. systemctl status $HOST", - "noteLabel": "Notera", - "notePlaceholder": "Text att klistra in i terminalen", - "createSnippetTitle": "Skapa utdrag", - "createSnippetButton": "Skapa utdrag", - "createSuccess": "Utdrag skapat", - "createFailed": "Misslyckades med att skapa kodavsnittet", - "editSnippetTitle": "Redigera utdrag", - "saveSnippetButton": "Spara utdrag", - "updateSuccess": "Utdrag uppdaterat", - "updateFailed": "Misslyckades med att uppdatera utdraget", - "deleteFailed": "Misslyckades med att ta bort utdraget", - "createFolderTitle": "Skapa mapp", - "createFolderButton": "Skapa mapp", - "folderCreateSuccess": "Mapp skapad", - "folderCreateFailed": "Misslyckades med att skapa mappen", - "editFolderTitle": "Redigera mapp", - "saveFolderButton": "Spara mapp", - "folderEditSuccess": "Mappen uppdaterad", - "folderEditFailed": "Misslyckades med att uppdatera mappen", - "folderNameLabel": "Mappnamn", - "folderNamePlaceholder": "Mappnamn", - "folderIconLabel": "Ikon", - "folderDeleteSuccess": "Mappen raderad", - "folderDeleteFailed": "Misslyckades med att ta bort mappen", - "confirmRunMessage": "Kör \"{{name}}\"?", - "confirmRunButton": "Sikt", - "runSuccess": "Körde \"{{name}}\" på {{count}} terminal(er)", - "pasteSuccess": "Klistrade in \"{{name}}\" i {{count}} terminal(er)", - "admin": { - "tabTitle": "Snuttar", - "count": "{{count}} utdrag", - "add": "Lägg till utdrag", - "namePlaceholder": "Utdragsnamn", - "contentPlaceholder": "Kommandoinnehåll", - "folderPlaceholder": "Mapp (valfritt)", - "empty": "Den här användaren har inga utdrag", - "deleteConfirm": "Ta bort utdraget \"{{name}}\" som tillhör {{username}}?", - "deleted": "Utdraget raderat", - "deleteFailed": "Misslyckades med att ta bort utdraget", - "requiredFields": "Namn och innehåll på utdrag krävs", - "saved": "Utdrag sparat", - "saveFailed": "Kunde inte spara utdraget", - "saving": "Sparande...", - "save": "Spara" - }, - "backToSnippets": "Tillbaka till utdrag", - "settingsTitle": "Inställningar för utdrag", - "settingsDisplayTitle": "Visa", - "settingsBehaviorTitle": "Beteende", - "typeCommandHint": "Körs i den aktiva terminalen.", - "typeNoteHint": "Klistrar in i den aktiva terminalen utan att köra.", - "variablesHint": "Variabler: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Etikett}", - "doubleClickToRun": "Dubbelklicka för att köra", - "sharedBadge": "Delad", - "copyToClipboard": "Kopiera till urklipp", - "copyFailed": "Misslyckades med att kopiera till urklipp", - "moreOptions": "Fler alternativ", - "moreActions": "Fler åtgärder", - "moveToFolder": "Flytta till mapp", - "deleteSnippet": "Ta bort utdrag", - "deleteSnippetConfirm": "Ta bort utdraget \"{{name}}\"?", - "deleteSuccess": "Utdraget raderat", - "delete": "Radera", - "folderActions": "Mappåtgärder", - "addSnippetHere": "Nytt utdrag här", - "deleteFolder": "Ta bort mapp", - "deleteFolderConfirm": "Ta bort mappen \"{{name}}\"? Dess utdrag flyttas ut ur mappen.", - "createFolderDescription": "Gruppera relaterade utdrag tillsammans.", - "editFolderDescription": "Ändra den här mappens namn, färg eller ikon.", - "folderColorLabel": "Färg", - "loadFailed": "Misslyckades med att ladda utdrag", - "importSkipExisting": "Importera (hoppa över befintliga)", - "importOverwrite": "Importera (skriv över befintlig)", - "importInvalidFile": "Den filen är inte en giltig exportkod för snippets", - "importSuccess": "Importerad {{imported}}, uppdaterad {{updated}}, hoppad över {{skipped}}", - "exportSnippets": "Exportera utdrag", - "exportSuccess": "Exporterade utdrag", - "exportFailed": "Misslyckades med att exportera utdrag", - "expandAll": "Expandera alla mappar", - "collapseAll": "Komprimera alla mappar", - "docsLink": "Dokumentation", - "clearSearch": "Rensa sökningen", - "noSearchResults": "Inga utdrag matchar din sökning", - "emptyTitle": "Inga utdrag ännu", - "emptyDescription": "Spara kommandon du kör ofta och kör dem sedan i en terminal med ett klick.", - "shareSnippet": "Dela utdrag", - "shareFolder": "Dela mapp", - "shareExpiry": "Utgår", - "shareExpiryOptions": { - "h0": "Aldrig", - "h1": "Om 1 timme", - "h24": "Om 1 dag", - "h168": "Om 7 dagar", - "h720": "Om 30 dagar" - }, - "shareButton": "Dela", - "shareSuccess": "Delad med {{count}}", - "shareFailed": "Misslyckades med att dela", - "shareLoadFailed": "Det gick inte att läsa in delningsinformationen", - "currentAccess": "Nuvarande åtkomst", - "notSharedYet": "Inte delat med någon ännu", - "revokeAccess": "Återkalla", - "revokeSuccess": "Åtkomst återkallad", - "revokeFailed": "Misslyckades med att återkalla åtkomst", - "close": "Nära", - "targetHostsLabel": "Målvärdar", - "targetHostsHint": "Välj värdar att köra det här kommandot direkt på, utan att en terminal är öppen. Lämna tomt för att köra det i den aktiva terminalen.", - "clearTargetHosts": "Rensa", - "noHostsAvailable": "Inga värdar tillgängliga", - "searchHosts": "Sök värdar", - "runOnTargets": "Kör på målvärdar", - "executionResultTitle": "Resultat för \"{{name}}\"", - "executionResultDescription": "Utdata från varje målvärd.", - "executionSuccess": "Framgång", - "executionFailed": "Misslyckades", - "directRunSuccess": "Körde \"{{name}}\" på {{count}} värd(ar)", - "directRunPartialFail": "\"{{name}}\" misslyckades på vissa värdar", - "reorderFailed": "Misslyckades med att ändra ordningen på utdragen", - "dropToRemoveFromFolder": "Släpp här för att flytta ut ur mappen", - "shareSnippetTitle": "Dela \"{{name}}\"", - "shareFolderTitle": "Dela mappen \"{{name}}\"", - "shareWith": "Dela med", - "usersTab": "Användare", - "rolesTab": "Roller", - "shareSearchPlaceholder": "Sök användare eller roller", - "shareNoMatches": "Inga träffar", - "shareWithCount": "Dela med {{count}}", - "shareFolderHint": "Delar alla utdrag i den här mappen. Utdrag som läggs till senare delas inte automatiskt.", - "shareFolderSummary": "Delade {{count}} kodavsnitt i den här mappen", - "grantedBy": "Beviljad av", - "expired": "Utgången", - "variablesDialogTitle": "Kör \"{{name}}\"", - "variablesDialogDescription": "Fyll i de värden som detta kodavsnitt behöver innan du kör det.", - "variablesPreviewLabel": "Förhandsvisning", - "variablesConfirmButton": "Sikt", - "keybindings": { - "runSnippet": "Kör befintligt kodavsnitt", - "snippetLabel": "Utdrag", - "selectSnippet": "Välj ett utdrag", - "snippetMissing": "utdraget hittades inte", - "snippetRequired": "Välj ett utdrag" - }, - "paletteHintRun": "Kör i den aktiva terminalfliken", - "host": { - "startupTitle": "Startkodavsnitt", - "quickActionsTitle": "Snabbåtgärder", - "quickActionsDescription": "Snabbåtgärder visas som knappar i verktygsfältet Host Metrics för att köra kommandon med ett klick.", - "noQuickActions": "Inga snabba åtgärder ännu.", - "buttonLabel": "Knappetikett", - "selectSnippet": "Välj utdrag...", - "addQuickAction": "Lägg till åtgärd", - "tabTitle": "Snuttar", - "startupDescription": "Körs i terminalen varje gång den ansluter till den här värden. Variabler som $HOST fylls i; ett kodavsnitt som ber om indata hoppas över.", - "none": "Ingen", - "removeQuickAction": "Ta bort snabbåtgärden" - }, - "quickActions": { - "running": "Kör {{name}}...", - "succeeded": "{{name}} slutfört", - "failed": "{{name}} misslyckades", - "error": "Misslyckades med att köra {{name}}" - }, - "paletteHintPaste": "Klistra in i den aktiva terminalfliken", - "targetTerminals": "Spring in", - "targetTerminalsHint": "valfritt, annars den aktiva fliken", - "selectAll": "Alla", - "selectNone": "Ingen", - "plugin": { - "name": "Snuttar" - } -} diff --git a/plugins/snippets/locales/translated/th_TH.json b/plugins/snippets/locales/translated/th_TH.json deleted file mode 100644 index fadc37647..000000000 --- a/plugins/snippets/locales/translated/th_TH.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "เศษเสี้ยว" - }, - "onboarding": { - "feature_snippets": "เศษเสี้ยว", - "feature_snippets_desc": "บันทึกคำสั่งที่คุณใช้งานบ่อยและส่งไปยังเทอร์มินัลได้ด้วยการคลิกเพียงครั้งเดียว" - }, - "permissions": { - "view": { - "title": "ดูตัวอย่าง", - "description": "ดูข้อความย่อ บันทึก และข้อความย่อที่แชร์ของคุณเอง" - }, - "create": { - "title": "สร้างโค้ดตัวอย่าง", - "description": "สร้างข้อความย่อ บันทึก และโฟลเดอร์ใหม่" - }, - "edit": { - "title": "แก้ไขข้อความย่อ", - "description": "เปลี่ยนชื่อหรือแก้ไขโค้ดตัวอย่างและโฟลเดอร์ที่มีอยู่" - }, - "delete": { - "title": "ลบส่วนย่อย", - "description": "ลบโค้ดตัวอย่างและโฟลเดอร์" - }, - "share": { - "title": "แบ่งปันข้อความสั้นๆ", - "description": "แชร์โค้ดตัวอย่างและโฟลเดอร์กับผู้ใช้หรือบทบาทอื่นๆ" - } - }, - "settings": { - "foldersCollapsed": { - "label": "ยุบโฟลเดอร์โดยค่าเริ่มต้น", - "description": "เมื่อเปิดแผง Snippets ให้เริ่มโดยที่โฟลเดอร์โค้ดตัวอย่างแต่ละโฟลเดอร์ยังอยู่ในสถานะพับอยู่" - }, - "showCommands": { - "label": "แสดงคำสั่ง", - "description": "แสดงบรรทัดแรกของคำสั่งแต่ละส่วนย่อยไว้ใต้ชื่อของคำสั่งนั้น" - }, - "confirmExecution": { - "label": "ยืนยันก่อนดำเนินการ", - "description": "ถามก่อนเรียกใช้คำสั่งในเทอร์มินัล" - } - }, - "cancel": "ยกเลิก", - "loading": "กำลังโหลด...", - "searchPlaceholder": "ตัวอย่างการค้นหา", - "newFolder": "โฟลเดอร์ใหม่", - "newSnippet": "โค้ดตัวอย่างใหม่", - "noFolder": "ไม่มีโฟลเดอร์", - "noSnippetsInFolder": "ไม่มีไฟล์ตัวอย่างในโฟลเดอร์นี้", - "noTerminalTabsOpen": "ไม่มีแท็บเทอร์มินัลเปิดอยู่", - "run": "วิ่ง", - "pasteToTerminal": "วางลงในเทอร์มินัล", - "copySuccess": "คัดลอกไปยังคลิปบอร์ดแล้ว", - "importExport": "นำเข้า/ส่งออก", - "importFailed": "ไม่สามารถนำเข้าส่วนย่อยได้", - "nameLabel": "ชื่อ", - "namePlaceholder": "ชื่อส่วนย่อย", - "descriptionLabel": "คำอธิบาย", - "descriptionPlaceholder": "คำอธิบายเพิ่มเติม (ไม่บังคับ)", - "folderLabel": "โฟลเดอร์", - "typeLabel": "พิมพ์", - "typeCommand": "สั่งการ", - "typeNote": "บันทึก", - "commandLabel": "สั่งการ", - "commandPlaceholder": "คำสั่งที่จะเรียกใช้ เช่น systemctl status $HOST", - "noteLabel": "บันทึก", - "notePlaceholder": "ข้อความที่จะวางลงในเทอร์มินัล", - "createSnippetTitle": "สร้างโค้ดตัวอย่าง", - "createSnippetButton": "สร้างโค้ดตัวอย่าง", - "createSuccess": "สร้างตัวอย่างโค้ดแล้ว", - "createFailed": "ไม่สามารถสร้างโค้ดตัวอย่างได้", - "editSnippetTitle": "แก้ไขส่วนย่อย", - "saveSnippetButton": "บันทึกส่วนย่อ", - "updateSuccess": "อัปเดตส่วนย่อยแล้ว", - "updateFailed": "ไม่สามารถอัปเดตโค้ดตัวอย่างได้", - "deleteFailed": "ไม่สามารถลบส่วนย่อยได้", - "createFolderTitle": "สร้างโฟลเดอร์", - "createFolderButton": "สร้างโฟลเดอร์", - "folderCreateSuccess": "สร้างโฟลเดอร์แล้ว", - "folderCreateFailed": "ไม่สามารถสร้างโฟลเดอร์ได้", - "editFolderTitle": "แก้ไขโฟลเดอร์", - "saveFolderButton": "บันทึกโฟลเดอร์", - "folderEditSuccess": "โฟลเดอร์ได้รับการอัปเดตแล้ว", - "folderEditFailed": "ไม่สามารถอัปเดตโฟลเดอร์ได้", - "folderNameLabel": "ชื่อโฟลเดอร์", - "folderNamePlaceholder": "ชื่อโฟลเดอร์", - "folderIconLabel": "ไอคอน", - "folderDeleteSuccess": "โฟลเดอร์ถูกลบแล้ว", - "folderDeleteFailed": "ไม่สามารถลบโฟลเดอร์ได้", - "confirmRunMessage": "เรียกใช้ \"{{name}}\"?", - "confirmRunButton": "วิ่ง", - "runSuccess": "รัน \"{{name}}\" บนเทอร์มินัล {{count}} เครื่อง", - "pasteSuccess": "วาง \"{{name}}\" ลงในเทอร์มินัล {{count}} แล้ว", - "admin": { - "tabTitle": "เศษเสี้ยว", - "count": "{{count}} ตัวอย่างข้อความ", - "add": "เพิ่มโค้ดตัวอย่าง", - "namePlaceholder": "ชื่อส่วนย่อย", - "contentPlaceholder": "เนื้อหาคำสั่ง", - "folderPlaceholder": "โฟลเดอร์ (ไม่จำเป็น)", - "empty": "ผู้ใช้รายนี้ไม่มีตัวอย่างโค้ด", - "deleteConfirm": "ลบส่วนย่อย \"{{name}}\" ที่เป็นของ {{username}} หรือไม่?", - "deleted": "ข้อความบางส่วนถูกลบแล้ว", - "deleteFailed": "ไม่สามารถลบส่วนย่อยได้", - "requiredFields": "จำเป็นต้องระบุชื่อและเนื้อหาของโค้ดตัวอย่าง", - "saved": "บันทึกส่วนย่อยแล้ว", - "saveFailed": "ไม่สามารถบันทึกส่วนย่อยได้", - "saving": "ประหยัด...", - "save": "บันทึก" - }, - "backToSnippets": "กลับไปที่ส่วนย่อ", - "settingsTitle": "การตั้งค่าสนิปเป็ต", - "settingsDisplayTitle": "แสดง", - "settingsBehaviorTitle": "พฤติกรรม", - "typeCommandHint": "ทำงานในเทอร์มินัลที่ใช้งานอยู่", - "typeNoteHint": "วางข้อความลงในเทอร์มินัลที่ใช้งานอยู่โดยไม่ต้องเรียกใช้โปรแกรม", - "variablesHint": "ตัวแปร: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "ดับเบิ้ลคลิกเพื่อเรียกใช้งาน", - "sharedBadge": "แชร์", - "copyToClipboard": "คัดลอกไปยังคลิปบอร์ด", - "copyFailed": "ไม่สามารถคัดลอกไปยังคลิปบอร์ดได้", - "moreOptions": "ตัวเลือกเพิ่มเติม", - "moreActions": "การดำเนินการเพิ่มเติม", - "moveToFolder": "ย้ายไปยังโฟลเดอร์", - "deleteSnippet": "ลบส่วนย่อย", - "deleteSnippetConfirm": "ลบโค้ดส่วนย่อย \"{{name}}\"?", - "deleteSuccess": "ข้อความบางส่วนถูกลบแล้ว", - "delete": "ลบ", - "folderActions": "การดำเนินการโฟลเดอร์", - "addSnippetHere": "ข้อความใหม่ตรงนี้", - "deleteFolder": "ลบโฟลเดอร์", - "deleteFolderConfirm": "ลบโฟลเดอร์ \"{{name}}\"? โค้ดตัวอย่างจะถูกย้ายออกจากโฟลเดอร์", - "createFolderDescription": "จัดกลุ่มข้อความที่เกี่ยวข้องเข้าด้วยกัน", - "editFolderDescription": "เปลี่ยนชื่อ สี หรือไอคอนของโฟลเดอร์นี้", - "folderColorLabel": "สี", - "loadFailed": "ไม่สามารถโหลดส่วนย่อยได้", - "importSkipExisting": "นำเข้า (ข้ามรายการที่มีอยู่)", - "importOverwrite": "นำเข้า (เขียนทับข้อมูลที่มีอยู่)", - "importInvalidFile": "ไฟล์นั้นไม่ใช่ไฟล์ส่งออกโค้ดตัวอย่างที่ถูกต้อง", - "importSuccess": "นำเข้า {{imported}}, อัปเดต {{updated}}, ข้าม {{skipped}}", - "exportSnippets": "ส่งออกส่วนย่อย", - "exportSuccess": "ส่งออกข้อมูลตัวอย่าง", - "exportFailed": "ไม่สามารถส่งออกส่วนย่อยได้", - "expandAll": "ขยายโฟลเดอร์ทั้งหมด", - "collapseAll": "ยุบโฟลเดอร์ทั้งหมด", - "docsLink": "เอกสารประกอบ", - "clearSearch": "ล้างการค้นหา", - "noSearchResults": "ไม่มีตัวอย่างผลลัพธ์ใดตรงกับการค้นหาของคุณ", - "emptyTitle": "ยังไม่มีตัวอย่างโค้ด", - "emptyDescription": "บันทึกคำสั่งที่คุณใช้งานบ่อย แล้วเรียกใช้คำสั่งเหล่านั้นในเทอร์มินัลได้ด้วยการคลิกเพียงครั้งเดียว", - "shareSnippet": "แชร์ตัวอย่าง", - "shareFolder": "แชร์โฟลเดอร์", - "shareExpiry": "หมดอายุ", - "shareExpiryOptions": { - "h0": "ไม่เคย", - "h1": "ภายใน 1 ชั่วโมง", - "h24": "ภายใน 1 วัน", - "h168": "ภายใน 7 วัน", - "h720": "ภายใน 30 วัน" - }, - "shareButton": "แบ่งปัน", - "shareSuccess": "แชร์กับ {{count}}", - "shareFailed": "ไม่สามารถแชร์ได้", - "shareLoadFailed": "ไม่สามารถโหลดรายละเอียดการแชร์ได้", - "currentAccess": "การเข้าถึงปัจจุบัน", - "notSharedYet": "ยังไม่ได้แชร์ให้ใครเลย", - "revokeAccess": "ถอน", - "revokeSuccess": "สิทธิ์การเข้าถึงถูกเพิกถอน", - "revokeFailed": "ไม่สามารถเพิกถอนสิทธิ์การเข้าถึงได้", - "close": "ปิด", - "targetHostsLabel": "เป้าหมายโฮสต์", - "targetHostsHint": "เลือกโฮสต์ที่จะรันคำสั่งนี้โดยตรง โดยไม่ต้องเปิดเทอร์มินัล หากต้องการรันในเทอร์มินัลที่ใช้งานอยู่ ให้เว้นว่างไว้", - "clearTargetHosts": "ชัดเจน", - "noHostsAvailable": "ไม่มีโฮสต์ให้บริการ", - "searchHosts": "ค้นหาโฮสต์", - "runOnTargets": "เรียกใช้งานบนโฮสต์เป้าหมาย", - "executionResultTitle": "ผลลัพธ์สำหรับ \"{{name}}\"", - "executionResultDescription": "ผลลัพธ์จากโฮสต์เป้าหมายแต่ละตัว", - "executionSuccess": "ความสำเร็จ", - "executionFailed": "ล้มเหลว", - "directRunSuccess": "Ran \"{{name}}\" บนโฮสต์ {{count}} เครื่อง", - "directRunPartialFail": "\"{{name}}\" ล้มเหลวบนโฮสต์บางตัว", - "reorderFailed": "ไม่สามารถเรียงลำดับส่วนย่อยใหม่ได้", - "dropToRemoveFromFolder": "ลากตรงนี้เพื่อย้ายออกจากโฟลเดอร์", - "shareSnippetTitle": "แชร์ \"{{name}}\"", - "shareFolderTitle": "แชร์โฟลเดอร์ \"{{name}}\"", - "shareWith": "แบ่งปันกับ", - "usersTab": "ผู้ใช้", - "rolesTab": "บทบาท", - "shareSearchPlaceholder": "ค้นหาผู้ใช้หรือบทบาท", - "shareNoMatches": "ไม่พบผลลัพธ์ใดๆ", - "shareWithCount": "แชร์กับ {{count}}", - "shareFolderHint": "แชร์โค้ดตัวอย่างทั้งหมดในโฟลเดอร์นี้ โค้ดตัวอย่างที่เพิ่มเข้ามาภายหลังจะไม่ถูกแชร์โดยอัตโนมัติ", - "shareFolderSummary": "โค้ดตัวอย่างที่แชร์ {{count}} ในโฟลเดอร์นี้", - "grantedBy": "ได้รับอนุญาตจาก", - "expired": "หมดอายุ", - "variablesDialogTitle": "วิ่ง \"{{name}}\"", - "variablesDialogDescription": "กรอกค่าที่จำเป็นสำหรับโค้ดส่วนนี้ก่อนที่จะเรียกใช้งาน", - "variablesPreviewLabel": "ตัวอย่าง", - "variablesConfirmButton": "วิ่ง", - "keybindings": { - "runSnippet": "เรียกใช้โค้ดตัวอย่างที่มีอยู่", - "snippetLabel": "ส่วนหนึ่ง", - "selectSnippet": "เลือกส่วนย่อย", - "snippetMissing": "ไม่พบโค้ดตัวอย่าง", - "snippetRequired": "เลือกส่วนย่อย" - }, - "paletteHintRun": "เรียกใช้ในแท็บเทอร์มินัลที่ใช้งานอยู่", - "host": { - "startupTitle": "ตัวอย่างโค้ดสำหรับเริ่มต้น", - "quickActionsTitle": "การดำเนินการด่วน", - "quickActionsDescription": "การดำเนินการด่วนจะปรากฏเป็นปุ่มในแถบเครื่องมือ Host Metrics เพื่อให้สามารถเรียกใช้คำสั่งได้ด้วยการคลิกเพียงครั้งเดียว", - "noQuickActions": "ยังไม่มีการดำเนินการใดๆ อย่างเร่งด่วนในขณะนี้", - "buttonLabel": "ป้ายกำกับปุ่ม", - "selectSnippet": "เลือกส่วนย่อย...", - "addQuickAction": "เพิ่มการดำเนินการ", - "tabTitle": "เศษเสี้ยว", - "startupDescription": "โปรแกรมนี้จะทำงานในเทอร์มินัลทุกครั้งที่เชื่อมต่อกับโฮสต์นี้ ตัวแปรต่างๆ เช่น $HOST จะถูกเติมค่าลงไป และส่วนที่ขอข้อมูลป้อนเข้าจะถูกข้ามไป", - "none": "ไม่มี", - "removeQuickAction": "ลบการดำเนินการอย่างรวดเร็ว" - }, - "quickActions": { - "running": "กำลังดำเนินการ {{name}}...", - "succeeded": "{{name}} เสร็จสมบูรณ์เรียบร้อยแล้ว", - "failed": "{{name}} ล้มเหลว", - "error": "ไม่สามารถดำเนินการ {{name}} ได้" - }, - "paletteHintPaste": "วางลงในแท็บเทอร์มินัลที่ใช้งานอยู่", - "targetTerminals": "วิ่งใน", - "targetTerminalsHint": "ตัวเลือกเสริม มิฉะนั้นจะเป็นแท็บที่ใช้งานอยู่", - "selectAll": "ทั้งหมด", - "selectNone": "ไม่มี", - "plugin": { - "name": "เศษเสี้ยว" - } -} diff --git a/plugins/snippets/locales/translated/tr_TR.json b/plugins/snippets/locales/translated/tr_TR.json deleted file mode 100644 index c7dd3ca8e..000000000 --- a/plugins/snippets/locales/translated/tr_TR.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Kod Parçacıkları" - }, - "onboarding": { - "feature_snippets": "Parçacıklar", - "feature_snippets_desc": "Sık kullandığınız komutları kaydedin ve tek tıklamayla terminale gönderin." - }, - "permissions": { - "view": { - "title": "Örnekleri görüntüle", - "description": "Kendi yazdığınız kısa metinleri, notları ve paylaşılan kısa metinleri görün." - }, - "create": { - "title": "Kod parçacıkları oluşturun", - "description": "Yeni kod parçacıkları, notlar ve klasörler oluşturun." - }, - "edit": { - "title": "Kod parçacıklarını düzenle", - "description": "Mevcut kod parçacıklarını ve klasörleri yeniden adlandırın veya değiştirin." - }, - "delete": { - "title": "Parçacıkları sil", - "description": "Kod parçacıklarını ve klasörleri silin." - }, - "share": { - "title": "Kısa özetleri paylaşın", - "description": "Kod parçacıklarını ve klasörleri diğer kullanıcılar veya rollerle paylaşın." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Klasörler varsayılan olarak kapalı olsun", - "description": "Kod parçacıkları panelini açtığınızda her bir kod parçacığı klasörü kapalı olarak başlar." - }, - "showCommands": { - "label": "Komutları göster", - "description": "Her bir kod parçacığının komutunun ilk satırını adının altında gösterin." - }, - "confirmExecution": { - "label": "Çalıştırmadan önce onaylayın.", - "description": "Terminalde bir komut parçacığı çalıştırılmadan önce izin isteyin." - } - }, - "cancel": "İptal", - "loading": "Yükleniyor...", - "searchPlaceholder": "Parçacıkları ara", - "newFolder": "Yeni klasör", - "newSnippet": "Yeni kısa özet", - "noFolder": "Klasör yok", - "noSnippetsInFolder": "Bu klasörde parçacık yok", - "noTerminalTabsOpen": "Hiçbir terminal sekmesi açık değil.", - "run": "Çalıştır", - "pasteToTerminal": "Terminale yapıştır", - "copySuccess": "Panoya kopyalandı", - "importExport": "İçe / Dışa Aktar", - "importFailed": "Kod parçacıkları içe aktarılamadı", - "nameLabel": "Ad", - "namePlaceholder": "Kod parçası adı", - "descriptionLabel": "Açıklama", - "descriptionPlaceholder": "İsteğe bağlı açıklama", - "folderLabel": "Klasör", - "typeLabel": "Tür", - "typeCommand": "Komut", - "typeNote": "Not", - "commandLabel": "Komut", - "commandPlaceholder": "Çalıştırılacak komut, örneğin systemctl status $HOST", - "noteLabel": "Not", - "notePlaceholder": "Terminale yapıştırılacak metin", - "createSnippetTitle": "Kod parçacığı oluştur", - "createSnippetButton": "Kod parçacığı oluştur", - "createSuccess": "Oluşturulan kod parçası", - "createFailed": "Parçacık oluşturulamadı", - "editSnippetTitle": "Kod parçasını düzenle", - "saveSnippetButton": "Kod parçasını kaydet", - "updateSuccess": "Kod parçası güncellendi.", - "updateFailed": "Parçacık güncellenemedi", - "deleteFailed": "Parçacık silinemedi", - "createFolderTitle": "Klasör oluştur", - "createFolderButton": "Klasör oluştur", - "folderCreateSuccess": "Klasör oluşturuldu", - "folderCreateFailed": "Klasör oluşturulamadı", - "editFolderTitle": "Klasörü düzenle", - "saveFolderButton": "Klasörü kaydet", - "folderEditSuccess": "Klasör güncellendi", - "folderEditFailed": "Klasör güncellenemedi", - "folderNameLabel": "Klasör adı", - "folderNamePlaceholder": "Klasör adı", - "folderIconLabel": "Simge", - "folderDeleteSuccess": "Klasör silindi", - "folderDeleteFailed": "Klasör silinemedi", - "confirmRunMessage": "\"{{name}}\" çalıştırılsın mı?", - "confirmRunButton": "Çalıştır", - "runSuccess": "\"{{name}}\" komutunu {{count}} terminal(ler)inde çalıştırdı", - "pasteSuccess": "\"{{name}}\" ifadesi {{count}} terminal(ler)ine yapıştırıldı", - "admin": { - "tabTitle": "Parçacıklar", - "count": "{{count}} kod parçası", - "add": "Kod Parçası Ekle", - "namePlaceholder": "Kod parçası adı", - "contentPlaceholder": "Komut içeriği", - "folderPlaceholder": "Klasör (isteğe bağlı)", - "empty": "Bu kullanıcıya ait kod parçası yok", - "deleteConfirm": "{{username}} kullanıcısına ait \"{{name}}\" kod parçası silinsin mi?", - "deleted": "Kod parçası silindi", - "deleteFailed": "Parçacık silinemedi", - "requiredFields": "Kod parçası adı ve içeriği zorunludur", - "saved": "Kod parçası kaydedildi", - "saveFailed": "Kod parçası kaydedilemedi", - "saving": "Kaydediliyor...", - "save": "Kaydet" - }, - "backToSnippets": "Özetlere geri dön", - "settingsTitle": "Kod parçacığı ayarları", - "settingsDisplayTitle": "Görüntülemek", - "settingsBehaviorTitle": "Davranış", - "typeCommandHint": "Etkin terminalde çalışır.", - "typeNoteHint": "Çalıştırmadan etkin terminale yapıştırır.", - "variablesHint": "Değişkenler: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Çalıştırmak için çift tıklayın.", - "sharedBadge": "Paylaşıldı", - "copyToClipboard": "Panoya kopyala", - "copyFailed": "Panoya kopyalanamadı", - "moreOptions": "Daha fazla seçenek", - "moreActions": "Diğer işlemler", - "moveToFolder": "Klasöre taşı", - "deleteSnippet": "Kod parçasını sil", - "deleteSnippetConfirm": "\"{{name}} \" kod parçasını silmek mi istiyorsunuz?", - "deleteSuccess": "Kod parçası silindi", - "delete": "Sil", - "folderActions": "Klasör işlemleri", - "addSnippetHere": "Yeni bir kesit burada.", - "deleteFolder": "Klasörü sil", - "deleteFolderConfirm": "\"{{name}}\" klasörünü silmek mi istiyorsunuz? İçindeki kod parçacıkları klasörün dışına taşınıyor.", - "createFolderDescription": "Birbiriyle ilgili kısa metin parçalarını gruplandırın.", - "editFolderDescription": "Bu klasörün adını, rengini veya simgesini değiştirin.", - "folderColorLabel": "Renk", - "loadFailed": "Kod parçacıkları yüklenemedi.", - "importSkipExisting": "İçe Aktar (mevcut olanları atla)", - "importOverwrite": "İçe aktar (mevcut olanın üzerine yaz)", - "importInvalidFile": "Bu dosya geçerli bir kod parçacığı dışa aktarımı değil.", - "importSuccess": "{{imported}}içe aktarıldı, {{updated}}güncellendi, {{skipped}} atlandı", - "exportSnippets": "Kod parçacıklarını dışa aktar", - "exportSuccess": "Dışa aktarılan kod parçacıkları", - "exportFailed": "Kod parçacıkları dışa aktarılamadı", - "expandAll": "Tüm klasörleri genişlet", - "collapseAll": "Tüm klasörleri daralt", - "docsLink": "Dokümantasyon", - "clearSearch": "Arama sonuçlarını temizle", - "noSearchResults": "Arama kriterlerinize uyan sonuç bulunamadı.", - "emptyTitle": "Henüz hiçbir kısa video yok.", - "emptyDescription": "Sık kullandığınız komutları kaydedin, ardından terminalde tek tıklamayla çalıştırın.", - "shareSnippet": "Özeti paylaş", - "shareFolder": "Paylaşılan klasör", - "shareExpiry": "Bitiş", - "shareExpiryOptions": { - "h0": "Süresiz", - "h1": "1 saat içinde", - "h24": "1 gün içinde", - "h168": "7 gün içinde", - "h720": "30 gün içinde" - }, - "shareButton": "Paylaş", - "shareSuccess": "{{count}} ile paylaşıldı", - "shareFailed": "Paylaşılamadı", - "shareLoadFailed": "Paylaşım ayrıntıları yüklenemedi.", - "currentAccess": "Mevcut erişim", - "notSharedYet": "Henüz kimseyle paylaşılmadı.", - "revokeAccess": "İptal Et", - "revokeSuccess": "Erişim iptal edildi", - "revokeFailed": "Erişim iptal edilemedi", - "close": "Kapat", - "targetHostsLabel": "Hedef sunucular", - "targetHostsHint": "Bu komutu doğrudan, açık bir terminal olmadan çalıştırmak istediğiniz sunucuları seçin. Aktif terminalde çalıştırmak için boş bırakın.", - "clearTargetHosts": "Temizle", - "noHostsAvailable": "Hiç ev sahibi mevcut değil.", - "searchHosts": "Host ara", - "runOnTargets": "Hedef sunucularda çalıştırın", - "executionResultTitle": "\"{{name}} \" için sonuçlar", - "executionResultDescription": "Her hedef sunucudan gelen çıktı.", - "executionSuccess": "Başarılı", - "executionFailed": "Başarısız", - "directRunSuccess": "{{count}} sunucuda \"{{name}}\" çalıştırıldı", - "directRunPartialFail": "\"{{name}}\" bazı sunucularda başarısız oldu", - "reorderFailed": "Kod parçacıklarının yeniden sıralanması başarısız oldu.", - "dropToRemoveFromFolder": "Klasörden çıkmak için buraya sürükleyin.", - "shareSnippetTitle": "Paylaş \"{{name}}\"", - "shareFolderTitle": "Paylaşılan klasör \"{{name}}\"", - "shareWith": "Kimlerle paylaş", - "usersTab": "Kullanıcılar", - "rolesTab": "Roller", - "shareSearchPlaceholder": "Kullanıcı veya rol ara", - "shareNoMatches": "Eşleşme bulunamadı.", - "shareWithCount": "{{count}} ile paylaş", - "shareFolderHint": "Bu klasördeki tüm kod parçacıklarını paylaşır. Daha sonra eklenen kod parçacıkları otomatik olarak paylaşılmaz.", - "shareFolderSummary": "Bu klasörde paylaşılan {{count}} kod parçacığı(ları)", - "grantedBy": "Veren", - "expired": "Süresi dolmuş", - "variablesDialogTitle": "Çalıştır \"{{name}}\"", - "variablesDialogDescription": "Bu kod parçasını çalıştırmadan önce gerekli değerleri girin.", - "variablesPreviewLabel": "Önizle", - "variablesConfirmButton": "Çalıştır", - "keybindings": { - "runSnippet": "Mevcut kod parçasını çalıştır", - "snippetLabel": "Alıntı", - "selectSnippet": "Bir parça seçin", - "snippetMissing": "Kod parçası bulunamadı", - "snippetRequired": "Bir parça seçin" - }, - "paletteHintRun": "Etkin terminal sekmesinde çalıştırın.", - "host": { - "startupTitle": "Başlangıç Betiği", - "quickActionsTitle": "Hızlı Eylemler", - "quickActionsDescription": "Hızlı eylemler, tek tıklamayla komut çalıştırmak için Ana Bilgisayar Metrikleri araç çubuğunda düğme olarak görünür.", - "noQuickActions": "Henüz hızlı eylem yok.", - "buttonLabel": "Düğme etiketi", - "selectSnippet": "Parçacık seç...", - "addQuickAction": "Eylem Ekle", - "tabTitle": "Parçacıklar", - "startupDescription": "Bu sunucuya her bağlandığında terminalde çalışır. $HOST gibi değişkenler doldurulur; girdi isteyen bir bölüm atlanır.", - "none": "Yok", - "removeQuickAction": "Hızlı işlemi kaldır" - }, - "quickActions": { - "running": "{{name}} çalıştırılıyor...", - "succeeded": "{{name}} başarıyla tamamlandı", - "failed": "{{name}} başarısız oldu", - "error": "{{name}} çalıştırılamadı" - }, - "paletteHintPaste": "Etkin terminal sekmesine yapıştırın.", - "targetTerminals": "Koşun", - "targetTerminalsHint": "isteğe bağlı, aksi takdirde aktif sekme", - "selectAll": "Tümü", - "selectNone": "Yok", - "plugin": { - "name": "Parçacıklar" - } -} diff --git a/plugins/snippets/locales/translated/uk_UA.json b/plugins/snippets/locales/translated/uk_UA.json deleted file mode 100644 index 12521f201..000000000 --- a/plugins/snippets/locales/translated/uk_UA.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Фрагменти" - }, - "onboarding": { - "feature_snippets": "Фрагменти", - "feature_snippets_desc": "Зберігайте команди, які ви часто виконуєте, та надсилайте їх у термінал одним клацанням миші." - }, - "permissions": { - "view": { - "title": "Переглянути фрагменти", - "description": "Переглядайте власні фрагменти, нотатки та спільні фрагменти." - }, - "create": { - "title": "Створити фрагменти", - "description": "Створюйте нові фрагменти, нотатки та папки." - }, - "edit": { - "title": "Редагувати фрагменти", - "description": "Перейменуйте або змініть існуючі фрагменти та папки." - }, - "delete": { - "title": "Видалити фрагменти", - "description": "Видалити фрагменти та папки." - }, - "share": { - "title": "Поділитися фрагментами", - "description": "Надавати доступ до фрагментів коду та папок іншим користувачам або ролям." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Згортати папки за замовчуванням", - "description": "Згортати кожну папку фрагментів під час відкриття панелі «Фрагменти»." - }, - "showCommands": { - "label": "Показати команди", - "description": "Показати перший рядок команди кожного фрагмента під його назвою." - }, - "confirmExecution": { - "label": "Підтвердьте перед запуском", - "description": "Запитувати перед виконанням фрагмента команди в терміналі." - } - }, - "cancel": "Скасувати", - "loading": "Завантаження...", - "searchPlaceholder": "Пошук фрагментів", - "newFolder": "Нова папка", - "newSnippet": "Новий фрагмент", - "noFolder": "Немає папки", - "noSnippetsInFolder": "У цій папці немає фрагментів", - "noTerminalTabsOpen": "Жодна вкладка термінала не відкрита", - "run": "Бігти", - "pasteToTerminal": "Вставити в термінал", - "copySuccess": "Скопійовано в буфер обміну", - "importExport": "Імпорт / Експорт", - "importFailed": "Не вдалося імпортувати фрагменти", - "nameLabel": "Ім'я", - "namePlaceholder": "Назва фрагмента", - "descriptionLabel": "Опис", - "descriptionPlaceholder": "Додатковий опис", - "folderLabel": "Папка", - "typeLabel": "Тип", - "typeCommand": "Команда", - "typeNote": "Примітка", - "commandLabel": "Команда", - "commandPlaceholder": "Команда для запуску, наприклад, systemctl status $HOST", - "noteLabel": "Примітка", - "notePlaceholder": "Текст для вставки в термінал", - "createSnippetTitle": "Створити фрагмент", - "createSnippetButton": "Створити фрагмент", - "createSuccess": "Фрагмент створено", - "createFailed": "Не вдалося створити фрагмент", - "editSnippetTitle": "Редагувати фрагмент", - "saveSnippetButton": "Зберегти фрагмент", - "updateSuccess": "Фрагмент оновлено", - "updateFailed": "Не вдалося оновити фрагмент", - "deleteFailed": "Не вдалося видалити фрагмент", - "createFolderTitle": "Створити папку", - "createFolderButton": "Створити папку", - "folderCreateSuccess": "Папку створено", - "folderCreateFailed": "Не вдалося створити папку", - "editFolderTitle": "Редагувати папку", - "saveFolderButton": "Зберегти папку", - "folderEditSuccess": "Папку оновлено", - "folderEditFailed": "Не вдалося оновити папку", - "folderNameLabel": "Назва папки", - "folderNamePlaceholder": "Назва папки", - "folderIconLabel": "Значок", - "folderDeleteSuccess": "Папку видалено", - "folderDeleteFailed": "Не вдалося видалити папку", - "confirmRunMessage": "Запустити \"{{name}}\"?", - "confirmRunButton": "Бігти", - "runSuccess": "Виконано \"{{name}}\" на терміналі(ах) {{count}}", - "pasteSuccess": "Вставлено \"{{name}}\" у термінал(и) {{count}}", - "admin": { - "tabTitle": "Фрагменти", - "count": "{{count}} фрагменти", - "add": "Додати фрагмент", - "namePlaceholder": "Назва фрагмента", - "contentPlaceholder": "Зміст команди", - "folderPlaceholder": "Папка (необов'язково)", - "empty": "Цей користувач не має фрагментів", - "deleteConfirm": "Видалити фрагмент \"{{name}}\", що належить {{username}}?", - "deleted": "Фрагмент видалено", - "deleteFailed": "Не вдалося видалити фрагмент", - "requiredFields": "Назва та вміст фрагмента обов'язкові", - "saved": "Фрагмент збережено", - "saveFailed": "Не вдалося зберегти фрагмент", - "saving": "Збереження...", - "save": "Зберегти" - }, - "backToSnippets": "Повернутися до фрагментів", - "settingsTitle": "Налаштування фрагмента", - "settingsDisplayTitle": "Дисплей", - "settingsBehaviorTitle": "Поведінка", - "typeCommandHint": "Працює в активному терміналі.", - "typeNoteHint": "Вставляє в активний термінал без запуску.", - "variablesHint": "Змінні: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Мітка}", - "doubleClickToRun": "Двічі клацніть, щоб запустити", - "sharedBadge": "Спільний доступ", - "copyToClipboard": "Копіювати в буфер обміну", - "copyFailed": "Не вдалося скопіювати в буфер обміну", - "moreOptions": "Більше опцій", - "moreActions": "Більше дій", - "moveToFolder": "Перемістити до папки", - "deleteSnippet": "Видалити фрагмент", - "deleteSnippetConfirm": "Видалити фрагмент \"{{name}}\"?", - "deleteSuccess": "Фрагмент видалено", - "delete": "Видалити", - "folderActions": "Дії з папкою", - "addSnippetHere": "Новий фрагмент тут", - "deleteFolder": "Видалити папку", - "deleteFolderConfirm": "Видалити папку \"{{name}}\"? Її фрагменти перемістяться за межі папки.", - "createFolderDescription": "Згрупуйте пов'язані фрагменти разом.", - "editFolderDescription": "Змініть назву, колір або значок цієї папки.", - "folderColorLabel": "Колір", - "loadFailed": "Не вдалося завантажити фрагменти", - "importSkipExisting": "Імпорт (пропустити існуючі)", - "importOverwrite": "Імпорт (перезаписати існуюче)", - "importInvalidFile": "Цей файл не є дійсним експортом фрагментів", - "importSuccess": "Імпортовано {{imported}}, оновлено {{updated}}, пропущено {{skipped}}", - "exportSnippets": "Експортувати фрагменти", - "exportSuccess": "Експортовано фрагментів", - "exportFailed": "Не вдалося експортувати фрагменти", - "expandAll": "Розгорнути всі папки", - "collapseAll": "Згорнути всі папки", - "docsLink": "Документація", - "clearSearch": "Очистити пошук", - "noSearchResults": "Немає фрагментів, що відповідають вашому пошуковому запиту", - "emptyTitle": "Поки що немає фрагментів", - "emptyDescription": "Зберігайте команди, які ви часто виконуєте, а потім запускайте їх у терміналі одним клацанням миші.", - "shareSnippet": "Поділитися фрагментом", - "shareFolder": "Спільний доступ до папки", - "shareExpiry": "Термін дії закінчується", - "shareExpiryOptions": { - "h0": "Ніколи", - "h1": "Через 1 годину", - "h24": "Через 1 день", - "h168": "Через 7 днів", - "h720": "Через 30 днів" - }, - "shareButton": "Поділитися", - "shareSuccess": "Спільно з {{count}}", - "shareFailed": "Не вдалося поділитися", - "shareLoadFailed": "Не вдалося завантажити деталі спільного доступу", - "currentAccess": "Поточний доступ", - "notSharedYet": "Ще ні з ким не поділилися", - "revokeAccess": "Скасувати", - "revokeSuccess": "Доступ скасовано", - "revokeFailed": "Не вдалося скасувати доступ", - "close": "Закрити", - "targetHostsLabel": "Цільові хости", - "targetHostsHint": "Виберіть хости, на яких потрібно виконати цю команду безпосередньо, без відкритого терміналу. Залиште поле порожнім, щоб запустити її в активному терміналі.", - "clearTargetHosts": "Очистити", - "noHostsAvailable": "Немає доступних хостів", - "searchHosts": "Пошук хостів", - "runOnTargets": "Запускати на цільових хостах", - "executionResultTitle": "Результати для запиту «{{name}}»", - "executionResultDescription": "Вихідні дані з кожного цільового хоста.", - "executionSuccess": "Успіх", - "executionFailed": "Не вдалося", - "directRunSuccess": "Виконано \"{{name}}\" на хості(ах) {{count}}", - "directRunPartialFail": "«{{name}}» не вдалося виконати на деяких хостах", - "reorderFailed": "Не вдалося змінити порядок фрагментів", - "dropToRemoveFromFolder": "Перетягніть сюди, щоб вийти за межі папки", - "shareSnippetTitle": "Поділитися «{{name}}»", - "shareFolderTitle": "Спільний доступ до папки «{{name}}»", - "shareWith": "Поділитися з", - "usersTab": "Користувачі", - "rolesTab": "Ролі", - "shareSearchPlaceholder": "Пошук користувачів або ролей", - "shareNoMatches": "Немає збігів", - "shareWithCount": "Поділитися з {{count}}", - "shareFolderHint": "Надає доступ до кожного фрагмента в цій папці. Фрагменти, додані пізніше, не надсилаються автоматично.", - "shareFolderSummary": "Спільні фрагменти {{count}} у цій папці", - "grantedBy": "Надано", - "expired": "Термін дії минув", - "variablesDialogTitle": "Виконати \"{{name}}\"", - "variablesDialogDescription": "Заповніть значення, необхідні цьому фрагменту, перш ніж запускати його.", - "variablesPreviewLabel": "Попередній перегляд", - "variablesConfirmButton": "Бігти", - "keybindings": { - "runSnippet": "Запустити існуючий фрагмент коду", - "snippetLabel": "Фрагмент", - "selectSnippet": "Виберіть фрагмент", - "snippetMissing": "фрагмент не знайдено", - "snippetRequired": "Виберіть фрагмент" - }, - "paletteHintRun": "Запустити в активній вкладці терміналу", - "host": { - "startupTitle": "Фрагмент запуску", - "quickActionsTitle": "Швидкі дії", - "quickActionsDescription": "Швидкі дії відображаються як кнопки на панелі інструментів «Метрики хоста» для виконання команд одним клацанням миші.", - "noQuickActions": "Поки що жодних швидких дій.", - "buttonLabel": "Мітка кнопки", - "selectSnippet": "Вибрати фрагмент...", - "addQuickAction": "Додати дію", - "tabTitle": "Фрагменти", - "startupDescription": "Виконується в терміналі кожного разу, коли підключається до цього хоста. Змінні типу $HOST заповнюються; фрагмент коду, який запитує введення, пропускається.", - "none": "Жоден", - "removeQuickAction": "Видалити швидку дію" - }, - "quickActions": { - "running": "Виконання {{name}}...", - "succeeded": "{{name}} успішно завершено", - "failed": "{{name}} не вдалося", - "error": "Не вдалося виконати {{name}}" - }, - "paletteHintPaste": "Вставити в активну вкладку терміналу", - "targetTerminals": "Оббіг", - "targetTerminalsHint": "необов'язково, інакше активна вкладка", - "selectAll": "Усі", - "selectNone": "Жоден", - "plugin": { - "name": "Фрагменти" - } -} diff --git a/plugins/snippets/locales/translated/vi_VN.json b/plugins/snippets/locales/translated/vi_VN.json deleted file mode 100644 index 34548afa2..000000000 --- a/plugins/snippets/locales/translated/vi_VN.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "Đoạn mã" - }, - "onboarding": { - "feature_snippets": "Đoạn mã", - "feature_snippets_desc": "Lưu lại các lệnh bạn thường xuyên sử dụng và gửi chúng đến cửa sổ dòng lệnh chỉ với một cú nhấp chuột." - }, - "permissions": { - "view": { - "title": "Xem đoạn trích", - "description": "Xem các đoạn trích, ghi chú và đoạn trích được chia sẻ của riêng bạn." - }, - "create": { - "title": "Tạo đoạn mã", - "description": "Tạo đoạn văn bản, ghi chú và thư mục mới." - }, - "edit": { - "title": "Chỉnh sửa đoạn mã", - "description": "Đổi tên hoặc thay đổi nội dung các đoạn mã và thư mục hiện có." - }, - "delete": { - "title": "Xóa đoạn mã", - "description": "Xóa các đoạn mã và thư mục." - }, - "share": { - "title": "Chia sẻ đoạn trích", - "description": "Chia sẻ các đoạn mã và thư mục với người dùng hoặc vai trò khác." - } - }, - "settings": { - "foldersCollapsed": { - "label": "Mặc định thu gọn thư mục", - "description": "Khi mở bảng Điều khiển Đoạn mã, mỗi thư mục đoạn mã sẽ được thu gọn lại." - }, - "showCommands": { - "label": "Hiển thị lệnh", - "description": "Hiển thị dòng đầu tiên của lệnh trong mỗi đoạn mã, ngay bên dưới tên của nó." - }, - "confirmExecution": { - "label": "Xác nhận trước khi chạy.", - "description": "Hãy hỏi trước khi chạy một đoạn mã lệnh trong cửa sổ dòng lệnh." - } - }, - "cancel": "Hủy", - "loading": "Đang tải...", - "searchPlaceholder": "Tìm kiếm snippet", - "newFolder": "Thư mục mới", - "newSnippet": "Đoạn mã mới", - "noFolder": "Không có thư mục", - "noSnippetsInFolder": "Không có snippet nào trong thư mục này", - "noTerminalTabsOpen": "Không có tab terminal nào đang mở.", - "run": "Chạy", - "pasteToTerminal": "Dán vào cửa sổ terminal", - "copySuccess": "Đã sao chép vào bộ nhớ tạm", - "importExport": "Nhập / Xuất", - "importFailed": "Nhập đoạn mã thất bại", - "nameLabel": "Tên", - "namePlaceholder": "Tên đoạn mã", - "descriptionLabel": "Mô tả", - "descriptionPlaceholder": "Mô tả tùy chọn", - "folderLabel": "Thư mục", - "typeLabel": "Loại", - "typeCommand": "Lệnh", - "typeNote": "Ghi chú", - "commandLabel": "Lệnh", - "commandPlaceholder": "Lệnh cần chạy, ví dụ: systemctl status $HOST", - "noteLabel": "Ghi chú", - "notePlaceholder": "Văn bản cần dán vào cửa sổ terminal", - "createSnippetTitle": "Tạo đoạn mã", - "createSnippetButton": "Tạo đoạn mã", - "createSuccess": "Đoạn mã được tạo", - "createFailed": "Tạo snippet thất bại", - "editSnippetTitle": "Chỉnh sửa đoạn mã", - "saveSnippetButton": "Lưu đoạn mã", - "updateSuccess": "Đoạn mã đã được cập nhật", - "updateFailed": "Cập nhật snippet thất bại", - "deleteFailed": "Xóa snippet thất bại", - "createFolderTitle": "Tạo thư mục", - "createFolderButton": "Tạo thư mục", - "folderCreateSuccess": "Thư mục đã được tạo", - "folderCreateFailed": "Tạo thư mục thất bại", - "editFolderTitle": "Sửa thư mục", - "saveFolderButton": "Lưu thư mục", - "folderEditSuccess": "Thư mục đã được cập nhật", - "folderEditFailed": "Cập nhật thư mục thất bại", - "folderNameLabel": "Tên thư mục", - "folderNamePlaceholder": "Tên thư mục", - "folderIconLabel": "Biểu tượng", - "folderDeleteSuccess": "Thư mục đã bị xóa", - "folderDeleteFailed": "Xóa thư mục thất bại", - "confirmRunMessage": "Chạy \"{{name}}\"?", - "confirmRunButton": "Chạy", - "runSuccess": "Ran \"{{name}}\" on {{count}} terminal(s)", - "pasteSuccess": "Đã dán \"{{name}}\" vào {{count}} thiết bị đầu cuối", - "admin": { - "tabTitle": "Đoạn mã", - "count": "{{count}} đoạn mã", - "add": "Thêm đoạn mã", - "namePlaceholder": "Tên đoạn mã", - "contentPlaceholder": "Nội dung lệnh", - "folderPlaceholder": "Thư mục (tùy chọn)", - "empty": "Người dùng này không có đoạn mã nào", - "deleteConfirm": "Xóa đoạn mã \"{{name}}\" của {{username}}?", - "deleted": "Đã xóa đoạn mã", - "deleteFailed": "Xóa snippet thất bại", - "requiredFields": "Cần tên và nội dung đoạn mã", - "saved": "Đã lưu đoạn mã", - "saveFailed": "Lưu đoạn mã thất bại", - "saving": "Đang lưu...", - "save": "Lưu" - }, - "backToSnippets": "Quay lại các đoạn trích", - "settingsTitle": "Cài đặt đoạn mã", - "settingsDisplayTitle": "Trưng bày", - "settingsBehaviorTitle": "Hành vi", - "typeCommandHint": "Chạy trong cửa sổ dòng lệnh đang hoạt động.", - "typeNoteHint": "Sao chép vào cửa sổ terminal đang hoạt động mà không chạy.", - "variablesHint": "Biến: $HOST, $USER, $PORT, $NAME, ${INPUT_1:Label}", - "doubleClickToRun": "Nhấp đúp để chạy", - "sharedBadge": "Đã chia sẻ", - "copyToClipboard": "Sao chép vào clipboard", - "copyFailed": "Không thể sao chép vào bảng tạm", - "moreOptions": "Nhiều tùy chọn hơn", - "moreActions": "Thao tác khác", - "moveToFolder": "Di chuyển vào thư mục", - "deleteSnippet": "Xóa đoạn mã", - "deleteSnippetConfirm": "Xóa đoạn mã \"{{name}}\"?", - "deleteSuccess": "Đã xóa đoạn mã", - "delete": "Xóa", - "folderActions": "Thao tác thư mục", - "addSnippetHere": "Đoạn mã mới ở đây", - "deleteFolder": "Xóa thư mục", - "deleteFolderConfirm": "Xóa thư mục \"{{name}}\"? Các đoạn mã của nó sẽ được chuyển ra khỏi thư mục.", - "createFolderDescription": "Gom các đoạn văn bản có liên quan lại với nhau.", - "editFolderDescription": "Thay đổi tên, màu sắc hoặc biểu tượng của thư mục này.", - "folderColorLabel": "Màu sắc", - "loadFailed": "Không thể tải đoạn mã.", - "importSkipExisting": "Nhập (bỏ qua mục đã có)", - "importOverwrite": "Nhập khẩu (ghi đè lên nội dung hiện có)", - "importInvalidFile": "Tệp đó không phải là tệp xuất đoạn mã hợp lệ.", - "importSuccess": "Đã nhập {{imported}}, đã cập nhật {{updated}}, đã bỏ qua {{skipped}}", - "exportSnippets": "Xuất đoạn mã", - "exportSuccess": "Các đoạn mã đã xuất", - "exportFailed": "Xuất đoạn mã thất bại", - "expandAll": "Mở rộng tất cả thư mục", - "collapseAll": "Thu gọn tất cả thư mục", - "docsLink": "Tài liệu", - "clearSearch": "Xóa tìm kiếm", - "noSearchResults": "Không có kết quả nào phù hợp với tìm kiếm của bạn", - "emptyTitle": "Chưa có đoạn trích nào.", - "emptyDescription": "Lưu lại các lệnh bạn thường xuyên sử dụng, sau đó chạy chúng trong cửa sổ dòng lệnh chỉ với một cú nhấp chuột.", - "shareSnippet": "Chia sẻ đoạn trích", - "shareFolder": "Chia sẻ thư mục", - "shareExpiry": "Hết hạn", - "shareExpiryOptions": { - "h0": "Chưa bao giờ", - "h1": "Trong 1 giờ", - "h24": "Trong 1 ngày", - "h168": "Trong 7 ngày", - "h720": "Trong 30 ngày" - }, - "shareButton": "Chia sẻ", - "shareSuccess": "Đã chia sẻ với {{count}}", - "shareFailed": "Không thể chia sẻ", - "shareLoadFailed": "Không thể tải thông tin chia sẻ.", - "currentAccess": "Truy cập hiện tại", - "notSharedYet": "Chưa chia sẻ với ai cả.", - "revokeAccess": "Thu hồi", - "revokeSuccess": "Đã thu hồi quyền truy cập", - "revokeFailed": "Thu hồi quyền truy cập thất bại", - "close": "Đóng", - "targetHostsLabel": "Mục tiêu chủ nhà", - "targetHostsHint": "Chọn các máy chủ để chạy lệnh này trực tiếp, không cần mở cửa sổ terminal. Để trống nếu muốn chạy lệnh trong cửa sổ terminal đang hoạt động.", - "clearTargetHosts": "Xóa", - "noHostsAvailable": "Hiện không có máy chủ nào khả dụng", - "searchHosts": "Tìm kiếm máy chủ", - "runOnTargets": "Chạy trên máy chủ mục tiêu", - "executionResultTitle": "Kết quả cho \"{{name}}\"", - "executionResultDescription": "Kết quả đầu ra từ mỗi máy chủ mục tiêu.", - "executionSuccess": "Thành công", - "executionFailed": "Thất bại", - "directRunSuccess": "Đã chạy \"{{name}}\" trên {{count}} máy chủ", - "directRunPartialFail": "\"{{name}}\" bị lỗi trên một số máy chủ", - "reorderFailed": "Không thể sắp xếp lại các đoạn mã.", - "dropToRemoveFromFolder": "Kéo thả vào đây để di chuyển ra khỏi thư mục.", - "shareSnippetTitle": "Chia sẻ \"{{name}}\"", - "shareFolderTitle": "Chia sẻ thư mục \"{{name}}\"", - "shareWith": "Chia sẻ với", - "usersTab": "Người dùng", - "rolesTab": "Vai trò", - "shareSearchPlaceholder": "Tìm kiếm người dùng hoặc vai trò", - "shareNoMatches": "Không có kết quả phù hợp", - "shareWithCount": "Chia sẻ với {{count}}", - "shareFolderHint": "Chia sẻ tất cả các đoạn mã trong thư mục này. Các đoạn mã được thêm vào sau sẽ không được chia sẻ tự động.", - "shareFolderSummary": "Đã chia sẻ {{count}} đoạn mã trong thư mục này", - "grantedBy": "Được cấp bởi", - "expired": "Đã hết hạn", - "variablesDialogTitle": "Chạy \"{{name}}\"", - "variablesDialogDescription": "Hãy điền đầy đủ các giá trị cần thiết cho đoạn mã này trước khi chạy.", - "variablesPreviewLabel": "Xem trước", - "variablesConfirmButton": "Chạy", - "keybindings": { - "runSnippet": "Chạy đoạn mã hiện có", - "snippetLabel": "Đoạn trích", - "selectSnippet": "Chọn một đoạn trích", - "snippetMissing": "Không tìm thấy đoạn mã.", - "snippetRequired": "Chọn một đoạn trích" - }, - "paletteHintRun": "Chạy trong tab cửa sổ terminal đang hoạt động", - "host": { - "startupTitle": "Đoạn mã khởi động", - "quickActionsTitle": "Hành động nhanh", - "quickActionsDescription": "Hành động nhanh xuất hiện dưới dạng nút trên thanh công cụ Chỉ số máy để thực thi lệnh một lần nhấp.", - "noQuickActions": "Chưa có hành động nhanh nào.", - "buttonLabel": "Nhãn nút", - "selectSnippet": "Chọn đoạn mã...", - "addQuickAction": "Thêm hành động", - "tabTitle": "Đoạn mã", - "startupDescription": "Đoạn mã này chạy trong terminal mỗi khi kết nối với máy chủ này. Các biến như $HOST được điền vào; đoạn mã yêu cầu nhập liệu sẽ bị bỏ qua.", - "none": "Không chọn", - "removeQuickAction": "Loại bỏ thao tác nhanh" - }, - "quickActions": { - "running": "Đang thực thi {{name}}...", - "succeeded": "{{name}} đã hoàn tất thành công", - "failed": "{{name}} thất bại", - "error": "Thực thi {{name}} thất bại" - }, - "paletteHintPaste": "Dán vào tab cửa sổ terminal đang hoạt động.", - "targetTerminals": "Chạy vào", - "targetTerminalsHint": "tùy chọn, nếu không thì tab đang hoạt động", - "selectAll": "Tất cả", - "selectNone": "Không chọn", - "plugin": { - "name": "Đoạn mã" - } -} diff --git a/plugins/snippets/locales/translated/zh_CN.json b/plugins/snippets/locales/translated/zh_CN.json deleted file mode 100644 index a6dbeab23..000000000 --- a/plugins/snippets/locales/translated/zh_CN.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "代码片段" - }, - "onboarding": { - "feature_snippets": "代码片段", - "feature_snippets_desc": "保存常用命令,一键发送至终端执行。" - }, - "permissions": { - "view": { - "title": "查看摘要", - "description": "查看您自己的代码片段、笔记和共享的代码片段。" - }, - "create": { - "title": "创建代码片段", - "description": "创建新的代码片段、笔记和文件夹。" - }, - "edit": { - "title": "编辑片段", - "description": "重命名或更改现有代码片段和文件夹。" - }, - "delete": { - "title": "删除片段", - "description": "删除代码片段和文件夹。" - }, - "share": { - "title": "分享片段", - "description": "与其他用户或角色共享代码片段和文件夹。" - } - }, - "settings": { - "foldersCollapsed": { - "label": "默认折叠文件夹", - "description": "打开代码片段面板时,每个代码片段文件夹都默认折叠。" - }, - "showCommands": { - "label": "显示命令", - "description": "在代码片段名称下方显示每个代码片段命令的第一行。" - }, - "confirmExecution": { - "label": "运行前请确认", - "description": "在终端运行命令片段之前,请先询问。" - } - }, - "cancel": "取消", - "loading": "加载中...", - "searchPlaceholder": "搜索代码片段", - "newFolder": "新建文件夹", - "newSnippet": "新片段", - "noFolder": "无文件夹", - "noSnippetsInFolder": "此文件夹中没有代码片段", - "noTerminalTabsOpen": "未打开任何终端标签页", - "run": "运行", - "pasteToTerminal": "粘贴到终端", - "copySuccess": "已复制到剪贴板", - "importExport": "导入 / 导出", - "importFailed": "导入代码片段失败", - "nameLabel": "名称", - "namePlaceholder": "代码片段名称", - "descriptionLabel": "描述", - "descriptionPlaceholder": "可选描述", - "folderLabel": "文件夹", - "typeLabel": "类型", - "typeCommand": "命令", - "typeNote": "笔记", - "commandLabel": "命令", - "commandPlaceholder": "要运行的命令,例如 systemctl status $HOST", - "noteLabel": "笔记", - "notePlaceholder": "要粘贴到终端中的文本", - "createSnippetTitle": "创建代码片段", - "createSnippetButton": "创建代码片段", - "createSuccess": "已创建代码片段", - "createFailed": "创建代码片段失败", - "editSnippetTitle": "编辑片段", - "saveSnippetButton": "保存代码片段", - "updateSuccess": "代码片段已更新", - "updateFailed": "更新代码片段失败", - "deleteFailed": "删除代码片段失败", - "createFolderTitle": "创建文件夹", - "createFolderButton": "创建文件夹", - "folderCreateSuccess": "已创建文件夹", - "folderCreateFailed": "创建文件夹失败", - "editFolderTitle": "编辑文件夹", - "saveFolderButton": "保存文件夹", - "folderEditSuccess": "文件夹已更新", - "folderEditFailed": "更新文件夹失败", - "folderNameLabel": "文件夹名称", - "folderNamePlaceholder": "文件夹名称", - "folderIconLabel": "图标", - "folderDeleteSuccess": "文件夹已删除", - "folderDeleteFailed": "删除文件夹失败", - "confirmRunMessage": "运行“{{name}}”?", - "confirmRunButton": "运行", - "runSuccess": "在 {{count}} 终端上运行“{{name}}”", - "pasteSuccess": "已将“{{name}}”粘贴到 {{count}} 终端中", - "admin": { - "tabTitle": "代码片段", - "count": "{{count}} 个代码片段", - "add": "添加代码片段", - "namePlaceholder": "代码片段名称", - "contentPlaceholder": "命令内容", - "folderPlaceholder": "文件夹(可选)", - "empty": "该用户没有代码片段", - "deleteConfirm": "删除属于 {{username}} 的代码片段 \"{{name}}\"?", - "deleted": "代码片段已删除", - "deleteFailed": "删除代码片段失败", - "requiredFields": "代码片段名称和内容为必填项", - "saved": "代码片段已保存", - "saveFailed": "保存代码片段失败", - "saving": "保存中...", - "save": "保存" - }, - "backToSnippets": "返回片段", - "settingsTitle": "代码片段设置", - "settingsDisplayTitle": "展示", - "settingsBehaviorTitle": "交互", - "typeCommandHint": "在当前终端中运行。", - "typeNoteHint": "无需运行即可粘贴到活动终端中。", - "variablesHint": "变量:$HOST、$USER、$PORT、$NAME、${INPUT_1:Label}", - "doubleClickToRun": "双击运行", - "sharedBadge": "已共享", - "copyToClipboard": "复制到剪贴板", - "copyFailed": "复制到剪贴板失败", - "moreOptions": "更多选项", - "moreActions": "更多操作", - "moveToFolder": "移至文件夹", - "deleteSnippet": "删除片段", - "deleteSnippetConfirm": "删除代码片段“{{name}}”?", - "deleteSuccess": "代码片段已删除", - "delete": "删除", - "folderActions": "文件夹操作", - "addSnippetHere": "这里新增了一段代码片段。", - "deleteFolder": "删除文件夹", - "deleteFolderConfirm": "删除文件夹“{{name}}”?它的代码片段会移出该文件夹。", - "createFolderDescription": "将相关的片段归类在一起。", - "editFolderDescription": "更改此文件夹的名称、颜色或图标。", - "folderColorLabel": "颜色", - "loadFailed": "代码片段加载失败", - "importSkipExisting": "导入(跳过现有)", - "importOverwrite": "导入(覆盖现有内容)", - "importInvalidFile": "该文件不是有效的代码片段导出文件。", - "importSuccess": "已导入 {{imported}},已更新 {{updated}},已跳过 {{skipped}}", - "exportSnippets": "导出代码片段", - "exportSuccess": "导出的代码片段", - "exportFailed": "导出代码片段失败", - "expandAll": "展开所有文件夹", - "collapseAll": "收起所有文件夹", - "docsLink": "文档", - "clearSearch": "清除搜索", - "noSearchResults": "没有找到与您的搜索匹配的摘要", - "emptyTitle": "目前还没有代码片段", - "emptyDescription": "保存您经常运行的命令,然后一键在终端中运行它们。", - "shareSnippet": "分享片段", - "shareFolder": "共享文件夹", - "shareExpiry": "过期时间", - "shareExpiryOptions": { - "h0": "从未", - "h1": "1小时内", - "h24": "1天", - "h168": "7天后", - "h720": "30天后" - }, - "shareButton": "共享", - "shareSuccess": "与 {{count}} 共享", - "shareFailed": "共享失败", - "shareLoadFailed": "共享详情加载失败", - "currentAccess": "当前访问权限", - "notSharedYet": "尚未与任何人分享", - "revokeAccess": "吊销", - "revokeSuccess": "访问权限已撤销", - "revokeFailed": "撤销访问权限失败", - "close": "关闭", - "targetHostsLabel": "目标主机", - "targetHostsHint": "选择要直接运行此命令的主机,无需打开终端。留空则在活动终端中运行。", - "clearTargetHosts": "清除", - "noHostsAvailable": "暂无可用主机", - "searchHosts": "搜索主机", - "runOnTargets": "在目标主机上运行", - "executionResultTitle": "搜索“{{name}}”的结果", - "executionResultDescription": "来自每个目标主机的输出。", - "executionSuccess": "成功", - "executionFailed": "失败", - "directRunSuccess": "已在 {{count}} 台主机上运行 \"{{name}}\"", - "directRunPartialFail": "在某些主机上,“{{name}}” 失败。", - "reorderFailed": "重新排序代码片段失败", - "dropToRemoveFromFolder": "从这里拖出文件夹", - "shareSnippetTitle": "分享“{{name}}", - "shareFolderTitle": "共享文件夹“{{name}}", - "shareWith": "共享给", - "usersTab": "用户", - "rolesTab": "角色", - "shareSearchPlaceholder": "搜索用户或角色", - "shareNoMatches": "没有匹配项", - "shareWithCount": "分享给 {{count}}", - "shareFolderHint": "共享此文件夹中的所有代码片段。之后添加的代码片段不会自动共享。", - "shareFolderSummary": "此文件夹中共享的 {{count}} 代码片段", - "grantedBy": "授权者", - "expired": "已过期", - "variablesDialogTitle": "运行“{{name}}”", - "variablesDialogDescription": "运行此代码片段之前,请填写所需的值。", - "variablesPreviewLabel": "预览", - "variablesConfirmButton": "运行", - "keybindings": { - "runSnippet": "运行现有代码片段", - "snippetLabel": "片段", - "selectSnippet": "选择代码片段", - "snippetMissing": "未找到代码片段", - "snippetRequired": "选择代码片段" - }, - "paletteHintRun": "在活动终端标签页中运行", - "host": { - "startupTitle": "启动代码片段", - "quickActionsTitle": "快速操作", - "quickActionsDescription": "快速操作以按钮形式显示在主机指标工具栏中,以便一键执行命令。", - "noQuickActions": "尚无快速操作。", - "buttonLabel": "按钮标签", - "selectSnippet": "选择代码片段……", - "addQuickAction": "添加操作", - "tabTitle": "代码片段", - "startupDescription": "每次连接到此主机时,都会在终端中运行。诸如 $HOST 之类的变量会被填充;需要输入信息的代码片段会被跳过。", - "none": "全不选", - "removeQuickAction": "快速行动" - }, - "quickActions": { - "running": "正在执行 {{name}}...", - "succeeded": "{{name}} 执行成功", - "failed": "{{name}} 执行失败", - "error": "执行 {{name}} 失败" - }, - "paletteHintPaste": "粘贴到当前终端选项卡中", - "targetTerminals": "跑进", - "targetTerminalsHint": "可选,否则为活动选项卡", - "selectAll": "全部", - "selectNone": "全不选", - "plugin": { - "name": "代码片段" - } -} diff --git a/plugins/snippets/locales/translated/zh_TW.json b/plugins/snippets/locales/translated/zh_TW.json deleted file mode 100644 index 6fb46eb17..000000000 --- a/plugins/snippets/locales/translated/zh_TW.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "nav": { - "snippets": "程式碼片段" - }, - "onboarding": { - "feature_snippets": "指令片段", - "feature_snippets_desc": "儲存您經常執行的命令,並一鍵將其發送到終端。" - }, - "permissions": { - "view": { - "title": "查看摘要", - "description": "查看您自己的程式碼片段、筆記和分享的程式碼片段。" - }, - "create": { - "title": "建立程式碼片段", - "description": "建立新的程式碼片段、筆記和資料夾。" - }, - "edit": { - "title": "編輯片段", - "description": "重新命名或更改現有程式碼片段和資料夾。" - }, - "delete": { - "title": "刪除片段", - "description": "刪除程式碼片段和資料夾。" - }, - "share": { - "title": "分享片段", - "description": "與其他使用者或角色共用程式碼片段和資料夾。" - } - }, - "settings": { - "foldersCollapsed": { - "label": "預設收合資料夾", - "description": "打開程式碼片段面板時,每個程式碼片段資料夾預設為折疊狀態。" - }, - "showCommands": { - "label": "顯示命令", - "description": "在程式碼片段名稱下方顯示每個程式碼片段指令的第一行。" - }, - "confirmExecution": { - "label": "運行前請確認", - "description": "在終端機運行命令片段之前,請先詢問。" - } - }, - "cancel": "取消", - "loading": "載入中...", - "searchPlaceholder": "搜尋指令片段", - "newFolder": "新資料夾", - "newSnippet": "新片段", - "noFolder": "無資料夾", - "noSnippetsInFolder": "此資料夾中無指令片段", - "noTerminalTabsOpen": "未開啟任何終端標籤頁", - "run": "執行", - "pasteToTerminal": "貼上到終端", - "copySuccess": "已複製到剪貼簿", - "importExport": "匯入 / 匯出", - "importFailed": "匯入片段失敗", - "nameLabel": "名稱", - "namePlaceholder": "片段名稱", - "descriptionLabel": "說明", - "descriptionPlaceholder": "選用說明", - "folderLabel": "資料夾", - "typeLabel": "類型", - "typeCommand": "指令", - "typeNote": "筆記", - "commandLabel": "指令", - "commandPlaceholder": "要執行的命令,例如 systemctl status $HOST", - "noteLabel": "筆記", - "notePlaceholder": "要貼上到終端中的文本", - "createSnippetTitle": "建立程式碼片段", - "createSnippetButton": "建立程式碼片段", - "createSuccess": "已建立程式碼片段", - "createFailed": "建立指令片段失敗", - "editSnippetTitle": "編輯片段", - "saveSnippetButton": "儲存程式碼片段", - "updateSuccess": "程式碼片段已更新", - "updateFailed": "更新指令片段失敗", - "deleteFailed": "刪除指令片段失敗", - "createFolderTitle": "建立資料夾", - "createFolderButton": "建立資料夾", - "folderCreateSuccess": "已建立資料夾", - "folderCreateFailed": "建立資料夾失敗", - "editFolderTitle": "編輯資料夾", - "saveFolderButton": "儲存資料夾", - "folderEditSuccess": "資料夾已更新", - "folderEditFailed": "更新資料夾失敗", - "folderNameLabel": "資料夾名稱", - "folderNamePlaceholder": "資料夾名稱", - "folderIconLabel": "圖示", - "folderDeleteSuccess": "資料夾已刪除", - "folderDeleteFailed": "刪除資料夾失敗", - "confirmRunMessage": "執行 \"{{name}}\"?", - "confirmRunButton": "執行", - "runSuccess": "在 {{count}} 終端機上執行“{{name}}”", - "pasteSuccess": "已將「{{name}}」貼到 {{count}} 終端機中", - "admin": { - "tabTitle": "指令片段", - "count": "{{count}} 個片段", - "add": "新增片段", - "namePlaceholder": "片段名稱", - "contentPlaceholder": "指令內容", - "folderPlaceholder": "資料夾(可選)", - "empty": "此使用者沒有任何片段", - "deleteConfirm": "刪除屬於 {{username}} 的片段「{{name}}」?", - "deleted": "片段已刪除", - "deleteFailed": "刪除指令片段失敗", - "requiredFields": "片段名稱與內容為必填", - "saved": "片段已儲存", - "saveFailed": "無法儲存片段", - "saving": "儲存中...", - "save": "儲存" - }, - "backToSnippets": "返回片段", - "settingsTitle": "程式碼片段設定", - "settingsDisplayTitle": "展示", - "settingsBehaviorTitle": "行為", - "typeCommandHint": "在當前終端中運行。", - "typeNoteHint": "無需運行即可貼上到活動終端中。", - "variablesHint": "變數: $HOST、 $USER、 $PORT、 $NAME、${INPUT_1:Label}", - "doubleClickToRun": "按兩下運行", - "sharedBadge": "已分享", - "copyToClipboard": "複製到剪貼簿", - "copyFailed": "複製到剪貼簿失敗", - "moreOptions": "更多選項", - "moreActions": "更多動作", - "moveToFolder": "移至資料夾", - "deleteSnippet": "刪除片段", - "deleteSnippetConfirm": "刪除程式碼片段“{{name}}”?", - "deleteSuccess": "片段已刪除", - "delete": "刪除", - "folderActions": "資料夾操作", - "addSnippetHere": "這裡新增了一段程式碼片段。", - "deleteFolder": "刪除資料夾", - "deleteFolderConfirm": "刪除資料夾“{{name}}”?它的程式碼片段會移出該資料夾。", - "createFolderDescription": "將相關的片段歸類在一起。", - "editFolderDescription": "變更此資料夾的名稱、顏色或圖示。", - "folderColorLabel": "顏色", - "loadFailed": "程式碼片段載入失敗", - "importSkipExisting": "匯入(跳過已存在項目)", - "importOverwrite": "導入(覆蓋現有內容)", - "importInvalidFile": "該檔案不是有效的程式碼片段匯出檔案。", - "importSuccess": "已匯入 {{imported}},已更新 {{updated}},已跳過 {{skipped}}", - "exportSnippets": "匯出程式碼片段", - "exportSuccess": "導出的程式碼片段", - "exportFailed": "匯出片段失敗", - "expandAll": "展開所有資料夾", - "collapseAll": "收合所有資料夾", - "docsLink": "說明文件", - "clearSearch": "清除搜尋", - "noSearchResults": "沒有找到與您的搜尋相符的摘要", - "emptyTitle": "目前還沒有片段", - "emptyDescription": "儲存您經常執行的命令,然後一鍵在終端機中執行它們。", - "shareSnippet": "分享片段", - "shareFolder": "共享資料夾", - "shareExpiry": "到期", - "shareExpiryOptions": { - "h0": "從未", - "h1": "1小時內", - "h24": "1天", - "h168": "7天后", - "h720": "30天后" - }, - "shareButton": "分享", - "shareSuccess": "與 {{count}} 分享", - "shareFailed": "分享失敗", - "shareLoadFailed": "共享詳情加載失敗", - "currentAccess": "目前存取權限", - "notSharedYet": "尚未與任何人分享", - "revokeAccess": "撤銷", - "revokeSuccess": "已撤銷存取權限", - "revokeFailed": "撤銷存取失敗", - "close": "關閉", - "targetHostsLabel": "目標主機", - "targetHostsHint": "選擇要直接執行此命令的主機,無需開啟終端機。留空則在活動終端機中運作。", - "clearTargetHosts": "清除", - "noHostsAvailable": "暫無可用主機", - "searchHosts": "搜尋主機", - "runOnTargets": "在目標主機上執行", - "executionResultTitle": "搜尋「{{name}}」的結果", - "executionResultDescription": "來自每個目標主機的輸出。", - "executionSuccess": "成功", - "executionFailed": "失敗", - "directRunSuccess": "已在 {{count}} 台主機上執行「{{name}}」", - "directRunPartialFail": "在某些主機上,“{{name}}” 失敗。", - "reorderFailed": "重新排序程式碼片段失敗", - "dropToRemoveFromFolder": "從這裡拖出資料夾", - "shareSnippetTitle": "分享「{{name}}", - "shareFolderTitle": "共用資料夾「{{name}}", - "shareWith": "分享對象", - "usersTab": "使用者", - "rolesTab": "角色", - "shareSearchPlaceholder": "搜尋使用者或角色", - "shareNoMatches": "沒有匹配項", - "shareWithCount": "分享給 {{count}}", - "shareFolderHint": "共用此資料夾中的所有程式碼片段。之後新增的程式碼片段不會自動共用。", - "shareFolderSummary": "此資料夾中共用的 {{count}} 程式碼片段", - "grantedBy": "授予者", - "expired": "已過期", - "variablesDialogTitle": "運行“{{name}}", - "variablesDialogDescription": "在運行此程式碼片段之前,請填寫所需的值。", - "variablesPreviewLabel": "預覽", - "variablesConfirmButton": "執行", - "keybindings": { - "runSnippet": "運行現有程式碼片段", - "snippetLabel": "片段", - "selectSnippet": "選擇一段文字", - "snippetMissing": "未找到程式碼片段", - "snippetRequired": "選擇一段文字" - }, - "paletteHintRun": "在活動終端選項卡中運行", - "host": { - "startupTitle": "啟動程式碼片段", - "quickActionsTitle": "快速操作", - "quickActionsDescription": "快速動作會以按鈕形式顯示在主機計量工具列上,可一鍵執行指令。", - "noQuickActions": "尚無快速動作。", - "buttonLabel": "按鈕標籤", - "selectSnippet": "選取程式碼片段...", - "addQuickAction": "新增動作", - "tabTitle": "指令片段", - "startupDescription": "每次連接到此主機時,都會在終端機中執行。諸如 $HOST 之類的變數會被填入;需要輸入資訊的程式碼片段會被跳過。", - "none": "取消全選", - "removeQuickAction": "快速行動" - }, - "quickActions": { - "running": "正在執行 {{name}}...", - "succeeded": "{{name}} 已成功完成", - "failed": "{{name}} 失敗", - "error": "無法執行 {{name}}" - }, - "paletteHintPaste": "貼上到目前終端選項卡中", - "targetTerminals": "跑進", - "targetTerminalsHint": "可選,否則為活動選項卡", - "selectAll": "全選", - "selectNone": "取消全選", - "plugin": { - "name": "指令片段" - } -} diff --git a/plugins/snippets/manifest.json b/plugins/snippets/manifest.json deleted file mode 100644 index 7f6a2ad76..000000000 --- a/plugins/snippets/manifest.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "id": "snippets", - "name": "Snippets", - "version": "1.0.0", - "description": "Save commands and notes, then run or paste them into a terminal. Supports variables and sharing.", - "author": { - "name": "Termix" - }, - "license": "MIT", - "repository": "https://github.com/Termix-SSH/Termix", - "category": "Productivity", - "icon": "Play", - "engine": { - "termix": ">=2.9.0", - "api": "1" - }, - "capabilities": [ - "db:own", - "network:serve", - "ssh:connect", - "credentials:use", - "hosts:write", - "ui:surface", - "events:core", - "db:core-refs" - ], - "provides": [ - { - "service": "snippets.access", - "version": "1.0.0", - "permission": "snippets.view" - } - ], - "locales": "locales", - "platforms": ["linux", "win32", "darwin"], - "contributes": { - "panels": [ - { - "id": "snippets", - "titleKey": "nav.snippets", - "icon": "Play" - } - ], - "permissions": [ - { - "name": "view", - "titleKey": "permissions.view.title", - "descriptionKey": "permissions.view.description", - "defaultRoles": ["user"] - }, - { - "name": "create", - "titleKey": "permissions.create.title", - "descriptionKey": "permissions.create.description", - "defaultRoles": ["user"] - }, - { - "name": "edit", - "titleKey": "permissions.edit.title", - "descriptionKey": "permissions.edit.description", - "defaultRoles": ["user"] - }, - { - "name": "delete", - "titleKey": "permissions.delete.title", - "descriptionKey": "permissions.delete.description", - "defaultRoles": ["user"] - }, - { - "name": "share", - "titleKey": "permissions.share.title", - "descriptionKey": "permissions.share.description", - "defaultRoles": ["user"] - } - ], - "settings": { - "user": [ - { - "key": "foldersCollapsed", - "type": "boolean", - "labelKey": "settings.foldersCollapsed.label", - "descriptionKey": "settings.foldersCollapsed.description", - "default": true - }, - { - "key": "showCommands", - "type": "boolean", - "labelKey": "settings.showCommands.label", - "descriptionKey": "settings.showCommands.description", - "default": true - }, - { - "key": "confirmExecution", - "type": "boolean", - "labelKey": "settings.confirmExecution.label", - "descriptionKey": "settings.confirmExecution.description", - "default": false - } - ], - "host": { - "editorGroup": "ssh", - "editorOrder": 15, - "fields": [ - { - "key": "startupSnippetId", - "type": "number", - "labelKey": "host.startupTitle", - "descriptionKey": "host.startupDescription", - "hidden": true, - "min": 1, - "shareRead": "view", - "defaultLevels": ["user", "folder"] - }, - { - "key": "quickActions", - "type": "json", - "labelKey": "host.quickActionsTitle", - "descriptionKey": "host.quickActionsDescription", - "hidden": true, - "default": [], - "shareRead": "view", - "defaultLevels": ["user", "folder"] - } - ] - } - }, - "syncEntities": ["snippetFolders", "snippets"], - "keybindingActions": [ - { - "id": "runSnippet", - "params": { - "snippetId": { - "type": "string", - "pattern": "^[0-9]+$", - "maxLength": 20, - "syncEntity": "snippets" - }, - "appendEnter": { - "type": "boolean" - } - } - } - ], - "http": { - "adminImpersonation": true - }, - "uses": [ - "host-metrics.toolbar", - "terminal.sendToActive", - "terminal.sendToSession", - "terminal.listSessions" - ] - } -} diff --git a/plugins/snippets/migrations/mysql/0001_adopt_snippets_tables.sql b/plugins/snippets/migrations/mysql/0001_adopt_snippets_tables.sql deleted file mode 100644 index fa81ab2a9..000000000 --- a/plugins/snippets/migrations/mysql/0001_adopt_snippets_tables.sql +++ /dev/null @@ -1,43 +0,0 @@ --- snippets 0001: adopt_snippets_tables --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS `snippets` ( - `id` int AUTO_INCREMENT PRIMARY KEY, - `user_id` varchar(255) NOT NULL, - `name` text NOT NULL, - `content` text NOT NULL, - `description` text, - `folder` text, - `order` int NOT NULL DEFAULT 0, - `sync_id` varchar(255) UNIQUE, - `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - `host_filter` text, - `is_note` boolean NOT NULL DEFAULT false, - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE -); -RENAME TABLE `snippets` TO `p_snippets_snippets`; -CREATE TABLE IF NOT EXISTS `snippet_folders` ( - `id` int AUTO_INCREMENT PRIMARY KEY, - `user_id` varchar(255) NOT NULL, - `name` text NOT NULL, - `color` text, - `icon` text, - `sync_id` varchar(255) UNIQUE, - `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE -); -RENAME TABLE `snippet_folders` TO `p_snippets_snippet_folders`; -CREATE TABLE IF NOT EXISTS `snippet_access` ( - `id` int AUTO_INCREMENT PRIMARY KEY, - `snippet_id` int NOT NULL, - `user_id` varchar(255), - `role_id` int, - `granted_by` varchar(255) NOT NULL, - `permission_level` text NOT NULL DEFAULT ('view'), - `expires_at` text, - `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), - FOREIGN KEY (`granted_by`) REFERENCES `users` (`id`) ON DELETE CASCADE -); -RENAME TABLE `snippet_access` TO `p_snippets_snippet_access`; diff --git a/plugins/snippets/migrations/postgres/0001_adopt_snippets_tables.sql b/plugins/snippets/migrations/postgres/0001_adopt_snippets_tables.sql deleted file mode 100644 index 7c7a0106a..000000000 --- a/plugins/snippets/migrations/postgres/0001_adopt_snippets_tables.sql +++ /dev/null @@ -1,47 +0,0 @@ --- snippets 0001: adopt_snippets_tables --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS "snippets" ( - "id" serial PRIMARY KEY, - "user_id" varchar(255) NOT NULL, - "name" text NOT NULL, - "content" text NOT NULL, - "description" text, - "folder" text, - "order" integer NOT NULL DEFAULT 0, - "sync_id" varchar(255) UNIQUE, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "host_filter" text, - "is_note" boolean NOT NULL DEFAULT false, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS "idx_snippets_user_id" ON "snippets" ("user_id"); -ALTER TABLE "snippets" RENAME TO "p_snippets_snippets"; -CREATE TABLE IF NOT EXISTS "snippet_folders" ( - "id" serial PRIMARY KEY, - "user_id" varchar(255) NOT NULL, - "name" text NOT NULL, - "color" text, - "icon" text, - "sync_id" varchar(255) UNIQUE, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE -); -ALTER TABLE "snippet_folders" RENAME TO "p_snippets_snippet_folders"; -CREATE TABLE IF NOT EXISTS "snippet_access" ( - "id" serial PRIMARY KEY, - "snippet_id" integer NOT NULL, - "user_id" varchar(255), - "role_id" integer, - "granted_by" varchar(255) NOT NULL, - "permission_level" text NOT NULL DEFAULT 'view', - "expires_at" text, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("granted_by") REFERENCES "users" ("id") ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS "idx_snippet_access_user_id" ON "snippet_access" ("user_id"); -CREATE INDEX IF NOT EXISTS "idx_snippet_access_snippet_id" ON "snippet_access" ("snippet_id"); -CREATE INDEX IF NOT EXISTS "idx_snippet_access_role_id" ON "snippet_access" ("role_id"); -ALTER TABLE "snippet_access" RENAME TO "p_snippets_snippet_access"; diff --git a/plugins/snippets/migrations/snapshot.json b/plugins/snippets/migrations/snapshot.json deleted file mode 100644 index 3f8f00f75..000000000 --- a/plugins/snippets/migrations/snapshot.json +++ /dev/null @@ -1,162 +0,0 @@ -{ - "version": 1, - "tables": { - "snippets": { - "columns": { - "id": { - "type": "id", - "primaryKey": true, - "notNull": true - }, - "userId": { - "type": "refUser", - "length": 255, - "notNull": true - }, - "name": { - "type": "text", - "notNull": true - }, - "content": { - "type": "text", - "notNull": true - }, - "description": { - "type": "text" - }, - "folder": { - "type": "text" - }, - "order": { - "type": "integer", - "notNull": true, - "defaultValue": 0 - }, - "syncId": { - "type": "varchar", - "length": 255, - "unique": true - }, - "createdAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - }, - "updatedAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - }, - "hostFilter": { - "type": "text" - }, - "isNote": { - "type": "boolean", - "notNull": true, - "defaultValue": false - } - }, - "indexes": [ - { - "name": "idx_snippets_user_id", - "columns": ["userId"] - } - ], - "adopts": "snippets" - }, - "snippet_folders": { - "columns": { - "id": { - "type": "id", - "primaryKey": true, - "notNull": true - }, - "userId": { - "type": "refUser", - "length": 255, - "notNull": true - }, - "name": { - "type": "text", - "notNull": true - }, - "color": { - "type": "text" - }, - "icon": { - "type": "text" - }, - "syncId": { - "type": "varchar", - "length": 255, - "unique": true - }, - "createdAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - }, - "updatedAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - } - }, - "indexes": [], - "adopts": "snippet_folders" - }, - "snippet_access": { - "columns": { - "id": { - "type": "id", - "primaryKey": true, - "notNull": true - }, - "snippetId": { - "type": "integer", - "notNull": true - }, - "userId": { - "type": "varchar", - "length": 255 - }, - "roleId": { - "type": "integer" - }, - "grantedBy": { - "type": "refUser", - "length": 255, - "notNull": true - }, - "permissionLevel": { - "type": "text", - "notNull": true, - "defaultValue": "view" - }, - "expiresAt": { - "type": "timestamp" - }, - "createdAt": { - "type": "timestamp", - "notNull": true, - "defaultNow": true - } - }, - "indexes": [ - { - "name": "idx_snippet_access_user_id", - "columns": ["userId"] - }, - { - "name": "idx_snippet_access_snippet_id", - "columns": ["snippetId"] - }, - { - "name": "idx_snippet_access_role_id", - "columns": ["roleId"] - } - ], - "adopts": "snippet_access" - } - } -} diff --git a/plugins/snippets/migrations/sqlite/0001_adopt_snippets_tables.sql b/plugins/snippets/migrations/sqlite/0001_adopt_snippets_tables.sql deleted file mode 100644 index 545da73e4..000000000 --- a/plugins/snippets/migrations/sqlite/0001_adopt_snippets_tables.sql +++ /dev/null @@ -1,47 +0,0 @@ --- snippets 0001: adopt_snippets_tables --- Generated by termix-plugin migrations. Review before committing. - -CREATE TABLE IF NOT EXISTS "snippets" ( - "id" integer PRIMARY KEY AUTOINCREMENT, - "user_id" text NOT NULL, - "name" text NOT NULL, - "content" text NOT NULL, - "description" text, - "folder" text, - "order" integer NOT NULL DEFAULT 0, - "sync_id" text UNIQUE, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "host_filter" text, - "is_note" integer NOT NULL DEFAULT 0, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS "idx_snippets_user_id" ON "snippets" ("user_id"); -ALTER TABLE "snippets" RENAME TO "p_snippets_snippets"; -CREATE TABLE IF NOT EXISTS "snippet_folders" ( - "id" integer PRIMARY KEY AUTOINCREMENT, - "user_id" text NOT NULL, - "name" text NOT NULL, - "color" text, - "icon" text, - "sync_id" text UNIQUE, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE -); -ALTER TABLE "snippet_folders" RENAME TO "p_snippets_snippet_folders"; -CREATE TABLE IF NOT EXISTS "snippet_access" ( - "id" integer PRIMARY KEY AUTOINCREMENT, - "snippet_id" integer NOT NULL, - "user_id" text, - "role_id" integer, - "granted_by" text NOT NULL, - "permission_level" text NOT NULL DEFAULT 'view', - "expires_at" text, - "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("granted_by") REFERENCES "users" ("id") ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS "idx_snippet_access_user_id" ON "snippet_access" ("user_id"); -CREATE INDEX IF NOT EXISTS "idx_snippet_access_snippet_id" ON "snippet_access" ("snippet_id"); -CREATE INDEX IF NOT EXISTS "idx_snippet_access_role_id" ON "snippet_access" ("role_id"); -ALTER TABLE "snippet_access" RENAME TO "p_snippets_snippet_access"; diff --git a/plugins/snippets/package.json b/plugins/snippets/package.json deleted file mode 100644 index 57452fa3d..000000000 --- a/plugins/snippets/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@termix-plugin/snippets", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "build": "termix-plugin build", - "test": "termix-plugin test", - "typecheck": "tsc -p .", - "validate": "termix-plugin validate" - }, - "dependencies": { - "lucide-react": "^1.33.0" - }, - "devDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "@testing-library/dom": "^10.4.2", - "@testing-library/react": "^16.3.3", - "@types/node": "^26.2.0", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", - "better-sqlite3": "^13.0.3", - "jsdom": "^30.0.1", - "vitest": "^4.1.11" - }, - "peerDependencies": { - "@termix/plugin-sdk": "^1.0.0", - "drizzle-orm": "^0.45.2", - "express": "^5.2.1", - "react": "^19.2.8", - "sonner": "^2.0.8", - "ssh2": "^1.17.0" - } -} diff --git a/plugins/snippets/src/backend/execution.ts b/plugins/snippets/src/backend/execution.ts deleted file mode 100644 index 3fba5054a..000000000 --- a/plugins/snippets/src/backend/execution.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface SnippetExecutionResult { - success: boolean; - output: string; - error?: string; -} - -export { resolveSnippetContent as resolveSnippetCommand } from "../shared/variables.js"; -export type { SnippetHostContext as SnippetHostVars } from "../shared/variables.js"; - -export function getSnippetExecutionTimeoutMs( - value = process.env.SNIPPET_EXECUTION_TIMEOUT_SECONDS, -): number | undefined { - if (value === undefined || value.trim() === "") return undefined; - - const seconds = Number(value); - if (!Number.isFinite(seconds) || seconds <= 0) return undefined; - - return seconds * 1000; -} - -export function createSnippetExecutionResult( - exitCode: number | null, - output: string, - errorOutput: string, -): SnippetExecutionResult { - const success = exitCode === 0 || (exitCode === null && !errorOutput); - return { - success, - output, - ...(errorOutput ? { error: errorOutput } : {}), - }; -} diff --git a/plugins/snippets/src/backend/host-import.ts b/plugins/snippets/src/backend/host-import.ts deleted file mode 100644 index 9af3cd611..000000000 --- a/plugins/snippets/src/backend/host-import.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { - PLUGIN_ID, - readQuickActions, - readStartupSnippetId, -} from "../shared/host-settings.js"; - -/** - * Registered as "snippets.hostImportNormalizer": a 2.8 or early 2.9 export - * carried quick actions as a host field and the startup snippet inside - * terminalConfig. An export that already carries this plugin's - * pluginSettings has had them written by core. - */ -export function hostImportNormalizer( - raw: Record, -): Record | null { - const carried = (raw.pluginSettings as Record | undefined)?.[ - PLUGIN_ID - ]; - if ( - carried && - typeof carried === "object" && - Object.keys(carried).length > 0 - ) { - return null; - } - - const values: Record = {}; - const quickActions = readQuickActions(raw.quickActions); - if (quickActions.length > 0) values.quickActions = quickActions; - - let terminalConfig = raw.terminalConfig; - if (typeof terminalConfig === "string") { - try { - terminalConfig = JSON.parse(terminalConfig); - } catch { - terminalConfig = null; - } - } - const startup = readStartupSnippetId( - (terminalConfig as { startupSnippetId?: unknown } | null)?.startupSnippetId, - ); - if (startup !== null) values.startupSnippetId = startup; - - return Object.keys(values).length > 0 ? values : null; -} - -/** - * Registered as "snippets.hostPayloadLegacy": quickActions and - * terminalConfig.startupSnippetId in their 2.8 shape, for clients that - * still read them (Termix-Mobile). Remove once the mobile app reads - * pluginSettings.snippets. - */ -export function hostPayloadLegacy( - values: Record, -): Record { - const out: Record = { - quickActions: readQuickActions(values.quickActions), - }; - const startup = readStartupSnippetId(values.startupSnippetId); - if (startup !== null) out.terminalConfig = { startupSnippetId: startup }; - return out; -} - -/** What hostSettingsSync needs from the repository. */ -export interface SnippetSyncLookup { - findSyncIdById: (id: number) => Promise; - findIdBySyncId: (syncId: string) => Promise; -} - -/** - * Registered as "snippets.hostSettingsSync": both host settings hold local - * snippet ids, so remote sync carries the snippets' sync ids instead, which - * are the same on both sides. - */ -export function createHostSettingsSync(lookup: SnippetSyncLookup) { - const toSyncId = async (id: number) => lookup.findSyncIdById(id); - const toId = async (value: unknown) => - typeof value === "string" && value ? lookup.findIdBySyncId(value) : null; - - return { - exportValue: async (key: string, value: unknown) => { - if (key === "startupSnippetId") { - const id = readStartupSnippetId(value); - return id === null ? null : await toSyncId(id); - } - if (key === "quickActions") { - const actions = readQuickActions(value); - const out = []; - for (const action of actions) { - const syncId = await toSyncId(action.snippetId); - if (syncId) out.push({ name: action.name, snippetId: syncId }); - } - return out; - } - return value; - }, - importValue: async (key: string, value: unknown) => { - if (key === "startupSnippetId") return await toId(value); - if (key === "quickActions") { - if (!Array.isArray(value)) return []; - const out = []; - for (const entry of value) { - const row = (entry ?? {}) as { name?: unknown; snippetId?: unknown }; - const id = await toId(row.snippetId); - if (id !== null) { - out.push({ - name: typeof row.name === "string" ? row.name : "", - snippetId: id, - }); - } - } - return out; - } - return value; - }, - }; -} diff --git a/plugins/snippets/src/backend/index.ts b/plugins/snippets/src/backend/index.ts deleted file mode 100644 index ec66438ea..000000000 --- a/plugins/snippets/src/backend/index.ts +++ /dev/null @@ -1,191 +0,0 @@ -import type { Router } from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { snippetAccess, snippetFolders, snippets } from "./tables.js"; -import { createSnippetRepository } from "./repository.js"; -import { registerSnippetRoutes } from "./routes.js"; -import { - extractSnippetInputs, - resolveSnippetContent, -} from "../shared/variables.js"; -import { - createHostSettingsSync, - hostImportNormalizer, - hostPayloadLegacy, -} from "./host-import.js"; - -/** What other plugins get from ctx.services.get("snippets.access", {userId}). */ -export interface SnippetsService { - /** The calling user's own snippets (not shared ones), full content included. */ - list: () => Promise< - Array<{ - id: number; - name: string; - content: string; - description: string | null; - isNote: boolean; - folder: string | null; - }> - >; - /** A snippet the calling user owns or was shared with them. */ - get: (id: number) => Promise<{ - id: number; - name: string; - content: string; - isNote: boolean; - } | null>; - resolveCommand: ( - id: number, - vars: { ip?: string; username?: string; port?: number; name?: string }, - inputValues?: Record, - ) => Promise; - /** Creates a snippet owned by the calling user. */ - create: (input: { - name: string; - content: string; - description?: string | null; - folder?: string | null; - }) => Promise<{ id: number; name: string }>; - /** Updates a snippet the calling user owns. Throws if not found. */ - update: ( - id: number, - changes: { - name?: string; - content?: string; - description?: string | null; - folder?: string | null; - }, - ) => Promise; - /** Deletes a snippet the calling user owns. Returns whether one was deleted. */ - remove: (id: number) => Promise; -} - -export async function activate(ctx: PluginContext) { - const snippetsTable = await ctx.db.define(snippets); - const foldersTable = await ctx.db.define(snippetFolders); - const accessTable = await ctx.db.define(snippetAccess); - - const repo = createSnippetRepository( - ctx.db, - snippetsTable, - foldersTable, - accessTable, - ); - - registerSnippetRoutes(ctx.http.router(), repo, ctx); - - ctx.sync.registerEntity({ - type: "snippetFolders", - table: foldersTable, - order: 40, - }); - ctx.sync.registerEntity({ - type: "snippets", - table: snippetsTable, - order: 60, - }); - - // Host settings that name a snippet by its local id, and the 2.8 host - // fields they came from. Keybindings name one through the runSnippet - // declaration in the manifest, which core translates over sync. - ctx.registry.provide("snippets.hostImportNormalizer", hostImportNormalizer); - ctx.registry.provide("snippets.hostPayloadLegacy", hostPayloadLegacy); - ctx.registry.provide( - "snippets.hostSettingsSync", - createHostSettingsSync(repo), - ); - - // $HOST and $INPUT_n handling for commands other plugins run (fleets). A - // pure function, so it goes through the registry rather than a service - // gated on snippets.view. - ctx.registry.provide("snippets.variables", { - resolve: resolveSnippetContent, - extractInputs: extractSnippetInputs, - }); - - // The user row survives a password-reset data wipe, so it never triggers - // our refUser() cascade; this topic is core's way of telling us to drop - // our copy of that user's data too. - ctx.events.on("user.data_wiped", (payload) => { - const userId = (payload as { userId?: string } | null)?.userId; - if (userId) void repo.deleteByUserId(userId); - }); - - async function findAccessible(userId: string, id: number) { - const owned = await repo.findOwnedById(userId, id); - if (owned) return owned; - const roleIds = await repo.listUserRoleIds(userId); - return repo.findAccessibleSharedSnippet(id, userId, roleIds); - } - - // The service is gated on snippets.view; a write also needs the same - // permission its route requires, so a caller cannot write through here - // what the user could not write in the panel. - async function requirePermission(permission: string): Promise { - if (!(await ctx.rbac.has(permission))) { - throw new Error(`Missing permission snippets.${permission}`); - } - } - - const service: SnippetsService = { - list: async () => { - const userId = ctx.currentActor(); - if (!userId) return []; - const owned = await repo.listOwnedSnippets(userId); - return owned.map((s) => ({ - id: s.id, - name: s.name, - content: s.content, - description: s.description, - isNote: s.isNote, - folder: s.folder, - })); - }, - get: async (id) => { - const userId = ctx.currentActor(); - if (!userId) return null; - const snippet = await findAccessible(userId, id); - if (!snippet) return null; - return { - id: snippet.id, - name: snippet.name, - content: snippet.content, - isNote: snippet.isNote, - }; - }, - resolveCommand: async (id, vars, inputValues = {}) => { - const userId = ctx.currentActor(); - if (!userId) return null; - const snippet = await findAccessible(userId, id); - if (!snippet) return null; - return resolveSnippetContent(snippet.content, vars, inputValues); - }, - create: async (input) => { - const userId = ctx.currentActor(); - if (!userId) throw new Error("snippets.access.create needs an actor"); - await requirePermission("create"); - const created = await repo.createSnippet(userId, input); - return { id: created.id, name: created.name }; - }, - update: async (id, changes) => { - const userId = ctx.currentActor(); - if (!userId) throw new Error("snippets.access.update needs an actor"); - await requirePermission("edit"); - const result = await repo.updateSnippet(userId, id, changes); - if (!result) throw new Error("Snippet not found"); - }, - remove: async (id) => { - const userId = ctx.currentActor(); - if (!userId) throw new Error("snippets.access.remove needs an actor"); - await requirePermission("delete"); - const existing = await repo.deleteSnippet(userId, id); - return !!existing; - }, - }; - ctx.services.provide("snippets.access", service); - - ctx.log.info("Snippets routes mounted at /plugin-api/snippets"); -} - -export async function deactivate() { - // Everything above was registered through ctx and is disposed by core. -} diff --git a/plugins/snippets/src/backend/reorder.ts b/plugins/snippets/src/backend/reorder.ts deleted file mode 100644 index 3bb74ee43..000000000 --- a/plugins/snippets/src/backend/reorder.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { SnippetReorderUpdate } from "./repository.js"; - -type SnippetReorderRequestBody = { - snippets?: unknown; - updates?: unknown; -}; - -export function extractSnippetReorderUpdates( - body: unknown, -): SnippetReorderUpdate[] | null { - if (!body || typeof body !== "object") { - return null; - } - - const payload = body as SnippetReorderRequestBody; - // Keep accepting the legacy `updates` key so older clients do not break - // while the web and desktop helpers converge on `snippets`. - const snippetsUpdates = Array.isArray(payload.snippets) - ? payload.snippets - : Array.isArray(payload.updates) - ? payload.updates - : null; - - if (!snippetsUpdates) { - return null; - } - - return snippetsUpdates as SnippetReorderUpdate[]; -} diff --git a/plugins/snippets/src/backend/repository.ts b/plugins/snippets/src/backend/repository.ts deleted file mode 100644 index 290bf5e1b..000000000 --- a/plugins/snippets/src/backend/repository.ts +++ /dev/null @@ -1,917 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { - and, - asc, - desc, - eq, - gte, - inArray, - isNull, - like, - or, - sql, -} from "drizzle-orm"; -import type { PluginDatabase } from "@termix/plugin-sdk/backend"; - -export interface SnippetRecord { - id: number; - userId: string; - name: string; - content: string; - description: string | null; - folder: string | null; - order: number; - syncId: string | null; - createdAt: string; - updatedAt: string; - hostFilter: string | null; - isNote: boolean; -} - -export interface SnippetFolderRecord { - id: number; - userId: string; - name: string; - color: string | null; - icon: string | null; - syncId: string | null; - createdAt: string; - updatedAt: string; -} - -export interface RenameSnippetFolderResult { - status: "renamed" | "missing" | "conflict"; -} - -export interface SnippetReorderUpdate { - id: number; - order: number; - folder?: string; -} - -export interface NewSnippetInput { - name: string; - content: string; - description?: string | null; - folder?: string | null; - order?: number | null; - hostFilter?: unknown; - isNote?: boolean; -} - -export interface SnippetUpdateInput { - name?: string; - content?: string; - description?: string | null; - folder?: string | null; - order?: number; - hostFilter?: unknown; - isNote?: boolean; -} - -export interface UpdateSnippetResult { - existing: SnippetRecord; - updated: SnippetRecord; -} - -export interface SnippetBulkImportResult { - snippetsImported: number; - snippetsSkipped: number; - snippetsUpdated: number; - foldersImported: number; - foldersSkipped: number; - failed: number; - errors: string[]; -} - -interface BulkImportSnippetInput { - name?: unknown; - content?: unknown; - description?: string | null; - folder?: string | null; - order?: unknown; - hostFilter?: string | null; -} - -interface BulkImportFolderInput { - name?: unknown; - color?: string | null; - icon?: string | null; -} - -export interface RbacAccessListItem { - id: number; - targetType: "user" | "role"; - userId: string | null; - roleId: number | null; - username: string | null; - roleName: string | null; - roleDisplayName: string | null; - grantedBy: string; - grantedByUsername: string | null; - permissionLevel: string; - expiresAt: string | null; - createdAt: string; -} - -export type SnippetAccessTarget = - | { targetType: "user"; targetUserId: string } - | { targetType: "role"; targetRoleId: number }; - -export type UpsertSnippetAccessInput = SnippetAccessTarget & { - snippetId: number; - grantedBy: string; - expiresAt: string | null; -}; - -export interface RbacSharedSnippet { - id: number; - name: string; - content: string; - description: string | null; - folder: string | null; - ownerUsername: string; - permissionLevel: string; - expiresAt: string | null; -} - -export interface RbacVisibleSharedSnippet extends RbacSharedSnippet { - userId: string; - order: number; - createdAt: string; - updatedAt: string; - isNote: boolean; -} - -export interface RbacAccessibleSnippet extends RbacVisibleSharedSnippet { - hostFilter: string | null; -} - -/* eslint-disable @typescript-eslint/no-explicit-any */ -// The tables come from ctx.db.define/ctx.db.refs, which the SDK hands back -// untyped, and the drizzle handle is the server's own. Typed at this module's -// edge instead. -type Table = any; -type Drizzle = any; -/* eslint-enable @typescript-eslint/no-explicit-any */ - -function isNonEmptyString(value: unknown): value is string { - return typeof value === "string" && value.trim().length > 0; -} - -function toAccessListItem( - row: Omit, -): RbacAccessListItem { - return { ...row, targetType: row.userId ? "user" : "role" }; -} - -export type SnippetRepository = ReturnType; - -/** - * Snippet, folder and sharing storage. Written without RETURNING so it runs - * the same on all three engines: an insert or update is read back by id/sync - * id rather than relying on `.returning()`. - */ -export function createSnippetRepository( - db: PluginDatabase, - snippets: Table, - folders: Table, - access: Table, -) { - const client = () => db.client(); - const refs = () => - db.refs<{ users: Table; roles: Table; userRoles: Table }>(); - - async function findOwnedById( - userId: string, - snippetId: number, - ): Promise { - const drizzle = await client(); - const rows = await drizzle - .select() - .from(snippets) - .where(and(eq(snippets.id, snippetId), eq(snippets.userId, userId))) - .limit(1); - return rows[0] ?? null; - } - - async function findFolderByName( - userId: string, - name: string, - ): Promise { - const drizzle = await client(); - const rows = await drizzle - .select() - .from(folders) - .where(and(eq(folders.userId, userId), eq(folders.name, name))) - .limit(1); - return rows[0] ?? null; - } - - async function maxOrderForFolder( - userId: string, - folder: string | null, - ): Promise { - const drizzle = await client(); - const result = await drizzle - .select({ maxOrder: sql`MAX(${snippets.order})` }) - .from(snippets) - .where( - and( - eq(snippets.userId, userId), - folder - ? eq(snippets.folder, folder) - : sql`(${snippets.folder} IS NULL OR ${snippets.folder} = '')`, - ), - ); - return result[0]?.maxOrder ?? -1; - } - - async function nextOrderForFolder( - userId: string, - folder: string, - ): Promise { - return (await maxOrderForFolder(userId, folder || null)) + 1; - } - - async function findByNameAndFolder( - userId: string, - name: string, - folder: string | null, - ): Promise { - const drizzle = await client(); - const rows = await drizzle - .select() - .from(snippets) - .where( - and( - eq(snippets.userId, userId), - eq(snippets.name, name), - folder - ? eq(snippets.folder, folder) - : sql`(${snippets.folder} IS NULL OR ${snippets.folder} = '')`, - ), - ) - .limit(1); - return rows[0] ?? null; - } - - function userOrRoleAccessFilter(userId: string, roleIds: number[]) { - if (roleIds.length === 0) return eq(access.userId, userId); - return or(eq(access.userId, userId), inArray(access.roleId, roleIds)); - } - - async function findSnippetAccess( - snippetId: number, - target: SnippetAccessTarget, - ) { - const drizzle = await client(); - const rows = await drizzle - .select() - .from(access) - .where( - and( - eq(access.snippetId, snippetId), - target.targetType === "user" - ? eq(access.userId, target.targetUserId) - : eq(access.roleId, target.targetRoleId), - ), - ) - .limit(1); - return rows[0] ?? null; - } - - return { - findOwnedById, - - async listFolders(userId: string): Promise { - const drizzle = await client(); - return drizzle - .select() - .from(folders) - .where(eq(folders.userId, userId)) - .orderBy(asc(folders.name)); - }, - - async listSnippetsForExport(userId: string): Promise { - const drizzle = await client(); - return drizzle - .select() - .from(snippets) - .where(eq(snippets.userId, userId)) - .orderBy(sql`coalesce(${snippets.folder}, '')`, asc(snippets.order)); - }, - - async listFoldersForExport(userId: string): Promise { - const drizzle = await client(); - return drizzle - .select() - .from(folders) - .where(eq(folders.userId, userId)) - .orderBy(asc(folders.name)); - }, - - async listOwnedSnippetsInFolder( - userId: string, - folder: string, - ): Promise { - const drizzle = await client(); - return drizzle - .select() - .from(snippets) - .where( - and( - eq(snippets.userId, userId), - or( - eq(snippets.folder, folder), - like(snippets.folder, `${folder} / %`), - ), - ), - ); - }, - - async listOwnedSnippets(userId: string): Promise { - const drizzle = await client(); - return drizzle - .select() - .from(snippets) - .where(eq(snippets.userId, userId)) - .orderBy( - sql`CASE WHEN ${snippets.folder} IS NULL OR ${snippets.folder} = '' THEN 0 ELSE 1 END`, - asc(snippets.folder), - asc(snippets.order), - sql`${snippets.updatedAt} DESC`, - ); - }, - - async reorderSnippets( - userId: string, - updates: SnippetReorderUpdate[], - ): Promise { - const drizzle = await client(); - for (const update of updates) { - const { id, order, folder } = update; - if (!id || order === undefined) continue; - - const updateFields: Partial<{ order: number; folder: string | null }> = - { - order, - }; - if (folder !== undefined) updateFields.folder = folder?.trim() || null; - - await drizzle - .update(snippets) - .set(updateFields) - .where(and(eq(snippets.id, id), eq(snippets.userId, userId))); - } - await db.persist(); - }, - - async createSnippet( - userId: string, - input: NewSnippetInput, - ): Promise { - const folderValue = input.folder?.trim() || ""; - const order = - input.order === undefined || input.order === null - ? await nextOrderForFolder(userId, folderValue) - : input.order; - - const syncId = randomUUID(); - const drizzle = await client(); - await drizzle.insert(snippets).values({ - syncId, - userId, - name: input.name.trim(), - content: input.content.trim(), - description: input.description?.trim() || null, - folder: input.folder?.trim() || null, - order, - hostFilter: input.hostFilter ? JSON.stringify(input.hostFilter) : null, - isNote: input.isNote ?? false, - }); - await db.persist(); - - const rows = await drizzle - .select() - .from(snippets) - .where(eq(snippets.syncId, syncId)) - .limit(1); - return rows[0]; - }, - - async updateSnippet( - userId: string, - snippetId: number, - input: SnippetUpdateInput, - ): Promise { - const existing = await findOwnedById(userId, snippetId); - if (!existing) return null; - - const updateFields: Record = { - updatedAt: sql`CURRENT_TIMESTAMP`, - }; - if (input.name !== undefined) updateFields.name = input.name.trim(); - if (input.content !== undefined) - updateFields.content = input.content.trim(); - if (input.description !== undefined) - updateFields.description = input.description?.trim() || null; - if (input.folder !== undefined) - updateFields.folder = input.folder?.trim() || null; - if (input.order !== undefined) updateFields.order = input.order; - if (input.hostFilter !== undefined) - updateFields.hostFilter = input.hostFilter - ? JSON.stringify(input.hostFilter) - : null; - if (input.isNote !== undefined) updateFields.isNote = input.isNote; - - const drizzle = await client(); - await drizzle - .update(snippets) - .set(updateFields) - .where(and(eq(snippets.id, snippetId), eq(snippets.userId, userId))); - await db.persist(); - - const updated = await findOwnedById(userId, snippetId); - return { existing, updated: updated as SnippetRecord }; - }, - - async deleteSnippet( - userId: string, - snippetId: number, - ): Promise { - const existing = await findOwnedById(userId, snippetId); - if (!existing) return null; - - const drizzle = await client(); - await drizzle - .delete(snippets) - .where(and(eq(snippets.id, snippetId), eq(snippets.userId, userId))); - await db.persist(); - return existing; - }, - - async bulkImport( - userId: string, - snippetsToImport: unknown[] | undefined, - foldersToImport: unknown[] | undefined, - overwrite: boolean, - ): Promise { - const results: SnippetBulkImportResult = { - snippetsImported: 0, - snippetsSkipped: 0, - snippetsUpdated: 0, - foldersImported: 0, - foldersSkipped: 0, - failed: 0, - errors: [], - }; - - const drizzle = await client(); - let changed = false; - - if (Array.isArray(foldersToImport)) { - for (const rawFolder of foldersToImport) { - const folder = rawFolder as BulkImportFolderInput; - if (!isNonEmptyString(folder.name)) { - results.failed++; - results.errors.push(`Folder missing name`); - continue; - } - - const existing = await findFolderByName(userId, folder.name); - if (existing) { - results.foldersSkipped++; - continue; - } - - await drizzle.insert(folders).values({ - syncId: randomUUID(), - userId, - name: folder.name.trim(), - color: folder.color?.trim() || null, - icon: folder.icon?.trim() || null, - }); - changed = true; - results.foldersImported++; - } - } - - if (Array.isArray(snippetsToImport)) { - for (let i = 0; i < snippetsToImport.length; i++) { - const snippet = snippetsToImport[i] as BulkImportSnippetInput; - - if ( - !isNonEmptyString(snippet.name) || - !isNonEmptyString(snippet.content) - ) { - results.failed++; - results.errors.push( - `Snippet ${i + 1}: name and content are required`, - ); - continue; - } - - const folderVal = snippet.folder?.trim() || null; - const existing = await findByNameAndFolder( - userId, - snippet.name.trim(), - folderVal, - ); - - if (existing) { - if (!overwrite) { - results.snippetsSkipped++; - continue; - } - - await drizzle - .update(snippets) - .set({ - content: snippet.content.trim(), - description: snippet.description?.trim() || null, - folder: folderVal, - order: - typeof snippet.order === "number" - ? snippet.order - : existing.order, - hostFilter: snippet.hostFilter || null, - updatedAt: sql`CURRENT_TIMESTAMP`, - }) - .where( - and(eq(snippets.id, existing.id), eq(snippets.userId, userId)), - ); - changed = true; - results.snippetsUpdated++; - continue; - } - - const maxOrder = await maxOrderForFolder(userId, folderVal); - await drizzle.insert(snippets).values({ - syncId: randomUUID(), - userId, - name: snippet.name.trim(), - content: snippet.content.trim(), - description: snippet.description?.trim() || null, - folder: folderVal, - order: - typeof snippet.order === "number" ? snippet.order : maxOrder + 1, - hostFilter: snippet.hostFilter || null, - }); - changed = true; - results.snippetsImported++; - } - } - - if (changed) await db.persist(); - return results; - }, - - async createFolder( - userId: string, - name: string, - color: string | null | undefined, - icon: string | null | undefined, - ): Promise { - const existing = await findFolderByName(userId, name); - if (existing) return null; - - const syncId = randomUUID(); - const drizzle = await client(); - await drizzle.insert(folders).values({ - syncId, - userId, - name: name.trim(), - color: color?.trim() || null, - icon: icon?.trim() || null, - }); - await db.persist(); - - const rows = await drizzle - .select() - .from(folders) - .where(eq(folders.syncId, syncId)) - .limit(1); - return rows[0] ?? null; - }, - - async updateFolderMetadata( - userId: string, - name: string, - color: string | null | undefined, - icon: string | null | undefined, - ): Promise { - const existing = await findFolderByName(userId, name); - if (!existing) return null; - - const updateFields: Record = { - updatedAt: sql`CURRENT_TIMESTAMP`, - }; - if (color !== undefined) updateFields.color = color?.trim() || null; - if (icon !== undefined) updateFields.icon = icon?.trim() || null; - - const drizzle = await client(); - await drizzle - .update(folders) - .set(updateFields) - .where(and(eq(folders.userId, userId), eq(folders.name, name))); - await db.persist(); - - return findFolderByName(userId, name); - }, - - async renameFolder( - userId: string, - oldName: string, - newName: string, - ): Promise { - const existing = await findFolderByName(userId, oldName); - if (!existing) return { status: "missing" }; - - const nameExists = await findFolderByName(userId, newName); - if (nameExists) return { status: "conflict" }; - - const drizzle = await client(); - await drizzle - .update(folders) - .set({ name: newName, updatedAt: sql`CURRENT_TIMESTAMP` }) - .where(and(eq(folders.userId, userId), eq(folders.name, oldName))); - - await drizzle - .update(snippets) - .set({ folder: newName }) - .where(and(eq(snippets.userId, userId), eq(snippets.folder, oldName))); - - await db.persist(); - return { status: "renamed" }; - }, - - async deleteFolder( - userId: string, - name: string, - ): Promise<{ syncId: string | null } | null> { - const drizzle = await client(); - await drizzle - .update(snippets) - .set({ folder: null }) - .where(and(eq(snippets.userId, userId), eq(snippets.folder, name))); - - const existing = await findFolderByName(userId, name); - if (!existing) { - await db.persist(); - return null; - } - - await drizzle - .delete(folders) - .where(and(eq(folders.userId, userId), eq(folders.name, name))); - await db.persist(); - return { syncId: existing.syncId }; - }, - - /** Wipes a user's snippets and folders, for the password-reset data-wipe flow. */ - async deleteByUserId(userId: string): Promise { - const drizzle = await client(); - await drizzle.delete(snippets).where(eq(snippets.userId, userId)); - await drizzle.delete(folders).where(eq(folders.userId, userId)); - await db.persist(); - }, - - // Sharing (moved from core's RbacAccessRepository) - - async listSnippetAccess(snippetId: number): Promise { - const drizzle = await client(); - const { users, roles } = await refs(); - const rows = await drizzle - .select({ - id: access.id, - userId: access.userId, - roleId: access.roleId, - username: users.username, - roleName: roles.name, - roleDisplayName: roles.displayName, - grantedBy: access.grantedBy, - grantedByUsername: sql< - string | null - >`(SELECT username FROM users WHERE id = ${access.grantedBy})`, - permissionLevel: access.permissionLevel, - expiresAt: access.expiresAt, - createdAt: access.createdAt, - }) - .from(access) - .leftJoin(users, eq(access.userId, users.id)) - .leftJoin(roles, eq(access.roleId, roles.id)) - .where(eq(access.snippetId, snippetId)) - .orderBy(desc(access.createdAt)); - - return rows.map(toAccessListItem); - }, - - async upsertSnippetAccess( - input: UpsertSnippetAccessInput, - ): Promise<{ id: number; created: boolean }> { - const existing = await findSnippetAccess(input.snippetId, input); - const drizzle = await client(); - - if (existing) { - await drizzle - .update(access) - .set({ expiresAt: input.expiresAt }) - .where(eq(access.id, existing.id)); - await db.persist(); - return { id: existing.id, created: false }; - } - - const now = new Date().toISOString(); - await drizzle.insert(access).values({ - snippetId: input.snippetId, - userId: input.targetType === "user" ? input.targetUserId : null, - roleId: input.targetType === "role" ? input.targetRoleId : null, - grantedBy: input.grantedBy, - permissionLevel: "view", - expiresAt: input.expiresAt, - createdAt: now, - }); - await db.persist(); - - const created = await findSnippetAccess(input.snippetId, input); - return { id: created!.id, created: true }; - }, - - async revokeSnippetAccess( - accessId: number, - snippetId: number, - ): Promise { - const drizzle = await client(); - await drizzle - .delete(access) - .where(and(eq(access.id, accessId), eq(access.snippetId, snippetId))); - await db.persist(); - }, - - async listSharedSnippets( - userId: string, - roleIds: number[], - now = new Date().toISOString(), - ): Promise { - const drizzle = await client(); - const { users } = await refs(); - - const directShared = await drizzle - .select({ - id: snippets.id, - name: snippets.name, - content: snippets.content, - description: snippets.description, - folder: snippets.folder, - ownerUsername: users.username, - permissionLevel: access.permissionLevel, - expiresAt: access.expiresAt, - }) - .from(access) - .innerJoin(snippets, eq(access.snippetId, snippets.id)) - .innerJoin(users, eq(snippets.userId, users.id)) - .where( - and( - eq(access.userId, userId), - or(isNull(access.expiresAt), gte(access.expiresAt, now)), - ), - ); - - if (roleIds.length === 0) return directShared; - - const directIds = new Set(directShared.map((s: { id: number }) => s.id)); - const roleShared = await drizzle - .select({ - id: snippets.id, - name: snippets.name, - content: snippets.content, - description: snippets.description, - folder: snippets.folder, - ownerUsername: users.username, - permissionLevel: access.permissionLevel, - expiresAt: access.expiresAt, - }) - .from(access) - .innerJoin(snippets, eq(access.snippetId, snippets.id)) - .innerJoin(users, eq(snippets.userId, users.id)) - .where( - and( - or(isNull(access.expiresAt), gte(access.expiresAt, now)), - inArray(access.roleId, roleIds), - ), - ); - - return [ - ...directShared, - ...roleShared.filter((s: { id: number }) => !directIds.has(s.id)), - ]; - }, - - async listVisibleSharedSnippets( - userId: string, - roleIds: number[], - now = new Date().toISOString(), - ): Promise { - const drizzle = await client(); - const { users } = await refs(); - return drizzle - .select({ - id: snippets.id, - userId: snippets.userId, - name: snippets.name, - content: snippets.content, - description: snippets.description, - folder: snippets.folder, - order: snippets.order, - createdAt: snippets.createdAt, - updatedAt: snippets.updatedAt, - isNote: snippets.isNote, - ownerUsername: users.username, - permissionLevel: access.permissionLevel, - expiresAt: access.expiresAt, - }) - .from(access) - .innerJoin(snippets, eq(access.snippetId, snippets.id)) - .innerJoin(users, eq(snippets.userId, users.id)) - .where( - and( - userOrRoleAccessFilter(userId, roleIds), - or(isNull(access.expiresAt), gte(access.expiresAt, now)), - ), - ); - }, - - async findAccessibleSharedSnippet( - snippetId: number, - userId: string, - roleIds: number[], - now = new Date().toISOString(), - ): Promise { - const drizzle = await client(); - const { users } = await refs(); - const rows = await drizzle - .select({ - id: snippets.id, - userId: snippets.userId, - name: snippets.name, - content: snippets.content, - description: snippets.description, - folder: snippets.folder, - order: snippets.order, - createdAt: snippets.createdAt, - updatedAt: snippets.updatedAt, - hostFilter: snippets.hostFilter, - isNote: snippets.isNote, - ownerUsername: users.username, - permissionLevel: access.permissionLevel, - expiresAt: access.expiresAt, - }) - .from(access) - .innerJoin(snippets, eq(access.snippetId, snippets.id)) - .innerJoin(users, eq(snippets.userId, users.id)) - .where( - and( - eq(access.snippetId, snippetId), - userOrRoleAccessFilter(userId, roleIds), - or(isNull(access.expiresAt), gte(access.expiresAt, now)), - ), - ) - .limit(1); - - return rows[0] ?? null; - }, - - /** A snippet's sync id, for a host setting that names it over sync. */ - async findSyncIdById(snippetId: number): Promise { - const drizzle = await client(); - const rows = await drizzle - .select({ syncId: snippets.syncId }) - .from(snippets) - .where(eq(snippets.id, snippetId)) - .limit(1); - return (rows[0]?.syncId as string | null | undefined) ?? null; - }, - - async findIdBySyncId(syncId: string): Promise { - const drizzle = await client(); - const rows = await drizzle - .select({ id: snippets.id }) - .from(snippets) - .where(eq(snippets.syncId, syncId)) - .limit(1); - return (rows[0]?.id as number | undefined) ?? null; - }, - - /** A user's role ids, for the sharing filters above. */ - async listUserRoleIds(userId: string): Promise { - const drizzle = await client(); - const { userRoles } = await refs(); - const rows = await drizzle - .select({ roleId: userRoles.roleId }) - .from(userRoles) - .where(eq(userRoles.userId, userId)); - return rows.map((r: { roleId: number }) => r.roleId); - }, - }; -} diff --git a/plugins/snippets/src/backend/routes.ts b/plugins/snippets/src/backend/routes.ts deleted file mode 100644 index 84ef3e72e..000000000 --- a/plugins/snippets/src/backend/routes.ts +++ /dev/null @@ -1,1260 +0,0 @@ -import type { Request, Response, Router } from "express"; -import type { PluginContext } from "@termix/plugin-sdk/backend"; -import type { SnippetRepository } from "./repository.js"; -import { extractSnippetReorderUpdates } from "./reorder.js"; -import { - createSnippetExecutionResult, - getSnippetExecutionTimeoutMs, - resolveSnippetCommand, -} from "./execution.js"; - -function isNonEmptyString(value: unknown): value is string { - return typeof value === "string" && value.trim().length > 0; -} - -function paramId(raw: string | string[] | undefined): number { - const value = Array.isArray(raw) ? raw[0] : raw; - return value === undefined ? NaN : parseInt(value, 10); -} - -function sortSnippets< - T extends { folder: string | null; order: number; updatedAt: string }, ->(a: T, b: T): number { - const aFolder = a.folder || ""; - const bFolder = b.folder || ""; - - if (!aFolder && bFolder) return -1; - if (aFolder && !bFolder) return 1; - if (aFolder !== bFolder) return aFolder.localeCompare(bFolder); - if (a.order !== b.order) return a.order - b.order; - - return b.updatedAt.localeCompare(a.updatedAt); -} - -function actor(ctx: PluginContext): string { - // Core's plugin router authenticates every request and runs it as that user. - return ctx.currentActor() as string; -} - -function logError( - ctx: PluginContext, - message: string, - error: unknown, - operation: string, -): void { - ctx.log.error( - `${message} (${operation})`, - error instanceof Error ? error : new Error(String(error)), - ); -} - -/** - * Mounts every snippets route on the plugin's router, served at - * /plugin-api/snippets. Most routes gate on their own permission; /execute - * only needs auth plus an accessible snippet and host, matching how running a - * snippet has never required a snippets.* grant. - */ -export function registerSnippetRoutes( - router: Router, - repo: SnippetRepository, - ctx: PluginContext, -): void { - async function getAccessibleSnippet(snippetId: number, userId: string) { - const owned = await repo.findOwnedById(userId, snippetId); - if (owned) return owned; - - const roleIds = await repo.listUserRoleIds(userId); - return repo.findAccessibleSharedSnippet(snippetId, userId, roleIds); - } - - /** - * @openapi - * /plugin-api/snippets/folders: - * get: - * summary: List the current user's snippet folders - * tags: - * - Snippets - * responses: - * 200: - * description: A list of snippet folders. - */ - router.get( - "/folders", - ctx.rbac.require("view") as never, - async (_req: Request, res: Response) => { - const userId = actor(ctx); - try { - res.json(await repo.listFolders(userId)); - } catch (err) { - logError( - ctx, - "Failed to fetch snippet folders", - err, - "snippet_folder_list_failed", - ); - res.status(500).json({ error: "Failed to fetch snippet folders" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/folders: - * post: - * summary: Create a snippet folder - * tags: - * - Snippets - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * color: - * type: string - * icon: - * type: string - * responses: - * 201: - * description: Snippet folder created. - * 400: - * description: Folder name is required. - * 409: - * description: A folder with this name already exists. - */ - router.post( - "/folders", - ctx.rbac.require("create") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const { name, color, icon } = req.body ?? {}; - - if (!isNonEmptyString(name)) { - return res.status(400).json({ error: "Folder name is required" }); - } - - try { - const created = await repo.createFolder(userId, name, color, icon); - if (!created) { - return res - .status(409) - .json({ error: "Folder with this name already exists" }); - } - res.status(201).json(created); - } catch (err) { - logError( - ctx, - "Failed to create snippet folder", - err, - "snippet_folder_create_failed", - ); - res.status(500).json({ error: "Failed to create snippet folder" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/folders/{name}/metadata: - * put: - * summary: Update a snippet folder's color or icon - * tags: - * - Snippets - * parameters: - * - in: path - * name: name - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * color: - * type: string - * icon: - * type: string - * responses: - * 200: - * description: Snippet folder metadata updated. - * 400: - * description: Invalid request. - * 404: - * description: Folder not found. - */ - router.put( - "/folders/:name/metadata", - ctx.rbac.require("edit") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const name = Array.isArray(req.params.name) - ? req.params.name[0] - : req.params.name; - const { color, icon } = req.body ?? {}; - - if (!name) return res.status(400).json({ error: "Invalid request" }); - - try { - const updated = await repo.updateFolderMetadata( - userId, - decodeURIComponent(name), - color, - icon, - ); - if (!updated) - return res.status(404).json({ error: "Folder not found" }); - res.json(updated); - } catch (err) { - logError( - ctx, - "Failed to update snippet folder metadata", - err, - "snippet_folder_metadata_update_failed", - ); - res - .status(500) - .json({ error: "Failed to update snippet folder metadata" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/folders/rename: - * put: - * summary: Rename a snippet folder - * tags: - * - Snippets - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * oldName: - * type: string - * newName: - * type: string - * responses: - * 200: - * description: Folder renamed. - * 400: - * description: Invalid request. - * 404: - * description: Folder not found. - * 409: - * description: A folder with the new name already exists. - */ - router.put( - "/folders/rename", - ctx.rbac.require("edit") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const { oldName, newName } = req.body ?? {}; - - if (!isNonEmptyString(oldName) || !isNonEmptyString(newName)) { - return res.status(400).json({ error: "Invalid request" }); - } - - try { - const result = await repo.renameFolder(userId, oldName, newName); - if (result.status === "missing") { - return res.status(404).json({ error: "Folder not found" }); - } - if (result.status === "conflict") { - return res - .status(409) - .json({ error: "Folder with new name already exists" }); - } - res.json({ success: true, oldName, newName }); - } catch (err) { - logError( - ctx, - "Failed to rename snippet folder", - err, - "snippet_folder_rename_failed", - ); - res.status(500).json({ error: "Failed to rename snippet folder" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/folders/{name}: - * delete: - * summary: Delete a snippet folder - * description: Moves the folder's snippets to the root instead of deleting them. - * tags: - * - Snippets - * parameters: - * - in: path - * name: name - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Snippet folder deleted. - * 400: - * description: Invalid request. - */ - router.delete( - "/folders/:name", - ctx.rbac.require("delete") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const name = Array.isArray(req.params.name) - ? req.params.name[0] - : req.params.name; - - if (!name) return res.status(400).json({ error: "Invalid request" }); - - try { - const folderName = decodeURIComponent(name); - await repo.deleteFolder(userId, folderName); - res.json({ success: true }); - } catch (err) { - logError( - ctx, - "Failed to delete snippet folder", - err, - "snippet_folder_delete_failed", - ); - res.status(500).json({ error: "Failed to delete snippet folder" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/reorder: - * put: - * summary: Bulk update the order and folder of snippets - * description: Accepts a "snippets" array, and the legacy "updates" key. - * tags: - * - Snippets - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * snippets: - * type: array - * items: - * type: object - * properties: - * id: - * type: integer - * order: - * type: integer - * folder: - * type: string - * responses: - * 200: - * description: Snippets reordered. - * 400: - * description: Invalid request. - */ - router.put( - "/reorder", - ctx.rbac.require("edit") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const updates = extractSnippetReorderUpdates(req.body); - - if (!updates || updates.length === 0) { - return res - .status(400) - .json({ error: "snippets array is required and must not be empty" }); - } - - try { - await repo.reorderSnippets(userId, updates); - res.json({ success: true, updated: updates.length }); - } catch (err) { - logError( - ctx, - "Failed to reorder snippets", - err, - "snippet_reorder_failed", - ); - res.status(500).json({ error: "Failed to reorder snippets" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/execute: - * post: - * summary: Run a snippet on a host over SSH - * description: Needs no snippets.* permission beyond an accessible snippet and host - running one has never required a separate grant. - * tags: - * - Snippets - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * snippetId: - * type: integer - * hostId: - * type: integer - * inputValues: - * type: object - * description: > - * Resolved values for $INPUT_n placeholders, keyed by - * "INPUT_n". $HOST/$USER/$PORT/$NAME are resolved - * server-side from the target host. - * additionalProperties: - * type: string - * responses: - * 200: - * description: Snippet executed. - * 400: - * description: Snippet ID and host ID are required, or the snippet is a note. - * 404: - * description: Snippet or host not found. - */ - router.post("/execute", async (req: Request, res: Response) => { - const userId = actor(ctx); - const { snippetId, hostId, inputValues } = req.body ?? {}; - - if (!snippetId || !hostId) { - return res - .status(400) - .json({ error: "Snippet ID and Host ID are required" }); - } - - let connection: Awaited> | undefined; - try { - const snippet = await getAccessibleSnippet( - parseInt(snippetId, 10), - userId, - ); - if (!snippet) return res.status(404).json({ error: "Snippet not found" }); - - if (snippet.isNote) { - return res - .status(400) - .json({ error: "Notes cannot be executed on a host" }); - } - - connection = await ctx.ssh.connect(parseInt(hostId, 10), { - purpose: "plugin", - timeoutMs: 30000, - }); - - const resolvedCommand = resolveSnippetCommand( - snippet.content, - { - ip: connection.host.ip, - username: connection.host.username, - port: connection.host.port, - name: connection.host.name as string | undefined, - }, - inputValues && typeof inputValues === "object" ? inputValues : {}, - ); - - const client = connection.client as import("ssh2").Client; - let output = ""; - let errorOutput = ""; - - const result = await new Promise< - ReturnType - >((resolve, reject) => { - const timeoutMs = getSnippetExecutionTimeoutMs(); - let timeout: NodeJS.Timeout | undefined; - - client.on("error", (err) => { - clearTimeout(timeout); - reject(err); - }); - - client.exec(resolvedCommand, (err, stream) => { - if (err) return reject(err); - - if (timeoutMs) { - timeout = setTimeout(() => { - stream.close(); - reject( - new Error(`Command execution timeout (${timeoutMs / 1000}s)`), - ); - }, timeoutMs); - } - - stream.on("close", (exitCode: number | null) => { - clearTimeout(timeout); - resolve( - createSnippetExecutionResult(exitCode, output, errorOutput), - ); - }); - stream.on("data", (data: Buffer) => { - output += data.toString(); - }); - stream.stderr.on("data", (data: Buffer) => { - errorOutput += data.toString(); - }); - }); - }); - - res.json(result); - } catch (err) { - logError(ctx, "Failed to execute snippet", err, "snippet_execute_failed"); - res.status(500).json({ error: "Failed to execute snippet" }); - } finally { - connection?.dispose(); - } - }); - - /** - * @openapi - * /plugin-api/snippets/export: - * get: - * summary: Export every snippet and folder as JSON - * tags: - * - Snippets - * responses: - * 200: - * description: Export object with snippets and folders arrays. - */ - router.get( - "/export", - ctx.rbac.require("view") as never, - async (_req: Request, res: Response) => { - const userId = actor(ctx); - try { - const allSnippets = await repo.listSnippetsForExport(userId); - const allFolders = await repo.listFoldersForExport(userId); - - res.json({ - snippets: allSnippets.map((s) => ({ - name: s.name, - content: s.content, - description: s.description, - folder: s.folder, - order: s.order, - hostFilter: s.hostFilter, - })), - folders: allFolders.map((f) => ({ - name: f.name, - color: f.color, - icon: f.icon, - })), - }); - } catch (err) { - logError( - ctx, - "Failed to export snippets", - err, - "snippet_export_failed", - ); - res.status(500).json({ error: "Failed to export snippets" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/bulk-import: - * post: - * summary: Import snippets and folders from JSON - * description: Existing folders are skipped. Existing snippets, matched by name and folder, are skipped or overwritten. - * tags: - * - Snippets - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * snippets: - * type: array - * folders: - * type: array - * overwrite: - * type: boolean - * responses: - * 200: - * description: Import results with counts. - * 400: - * description: Invalid request body. - */ - router.post( - "/bulk-import", - ctx.rbac.require("create") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const { - snippets: snippetsToImport, - folders: foldersToImport, - overwrite, - } = req.body ?? {}; - - if (!Array.isArray(snippetsToImport) && !Array.isArray(foldersToImport)) { - return res - .status(400) - .json({ error: "snippets or folders array is required" }); - } - - try { - const results = await repo.bulkImport( - userId, - snippetsToImport, - foldersToImport, - !!overwrite, - ); - res.json({ success: true, ...results }); - } catch (err) { - logError( - ctx, - "Failed to bulk import snippets", - err, - "snippet_bulk_import_failed", - ); - res.status(500).json({ error: "Failed to import snippets" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets: - * get: - * summary: List the current user's visible snippets - * description: Owned snippets plus snippets shared with the caller directly or through a role, tagged with isShared. - * tags: - * - Snippets - * responses: - * 200: - * description: A list of snippets. - */ - router.get( - "/", - ctx.rbac.require("view") as never, - async (_req: Request, res: Response) => { - const userId = actor(ctx); - try { - const owned = await repo.listOwnedSnippets(userId); - const roleIds = await repo.listUserRoleIds(userId); - const shared = await repo.listVisibleSharedSnippets(userId, roleIds); - - const visible = new Map>(); - for (const snippet of owned) { - visible.set(snippet.id, { ...snippet, isShared: false }); - } - for (const snippet of shared) { - if (visible.has(snippet.id)) continue; - visible.set(snippet.id, { ...snippet, isShared: true }); - } - - res.json( - Array.from(visible.values()).sort((a, b) => - sortSnippets( - a as { folder: string | null; order: number; updatedAt: string }, - b as { folder: string | null; order: number; updatedAt: string }, - ), - ), - ); - } catch (err) { - logError(ctx, "Failed to fetch snippets", err, "snippet_list_failed"); - res.status(500).json({ error: "Failed to fetch snippets" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/shared: - * get: - * summary: List snippets shared with the current user - * description: Registered before /:id so the literal path "shared" is not swallowed by the id parameter. - * tags: - * - Snippets - * responses: - * 200: - * description: Snippets shared with the caller. - */ - router.get("/shared", async (_req: Request, res: Response) => { - const userId = actor(ctx); - try { - const roleIds = await repo.listUserRoleIds(userId); - const sharedSnippets = await repo.listSharedSnippets(userId, roleIds); - res.json({ sharedSnippets }); - } catch (err) { - logError( - ctx, - "Failed to get shared snippets", - err, - "shared_snippets_list_failed", - ); - res.status(500).json({ error: "Failed to get shared snippets" }); - } - }); - - /** - * @openapi - * /plugin-api/snippets/{id}: - * get: - * summary: Get one snippet - * tags: - * - Snippets - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: The requested snippet. - * 400: - * description: Invalid request parameters. - * 404: - * description: Snippet not found. - */ - router.get( - "/:id", - ctx.rbac.require("view") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const id = Array.isArray(req.params.id) - ? req.params.id[0] - : req.params.id; - const snippetId = parseInt(id, 10); - - if (isNaN(snippetId)) { - return res.status(400).json({ error: "Invalid request parameters" }); - } - - try { - const result = await getAccessibleSnippet(snippetId, userId); - if (!result) - return res.status(404).json({ error: "Snippet not found" }); - res.json(result); - } catch (err) { - logError(ctx, "Failed to fetch snippet", err, "snippet_get_failed"); - res.status(500).json({ error: "Failed to fetch snippet" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets: - * post: - * summary: Create a snippet - * tags: - * - Snippets - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * content: - * type: string - * description: - * type: string - * folder: - * type: string - * order: - * type: integer - * isNote: - * type: boolean - * description: When true, the snippet is a note (copy/paste only, not directly executable on a host). - * responses: - * 201: - * description: Snippet created. - * 400: - * description: Name and content are required. - */ - router.post( - "/", - ctx.rbac.require("create") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const { name, content, description, folder, order, hostFilter, isNote } = - req.body ?? {}; - - if (!isNonEmptyString(name) || !isNonEmptyString(content)) { - return res.status(400).json({ error: "Name and content are required" }); - } - - try { - const result = await repo.createSnippet(userId, { - name, - content, - description, - folder, - order, - hostFilter, - isNote, - }); - res.status(201).json(result); - } catch (err) { - logError(ctx, "Failed to create snippet", err, "snippet_create_failed"); - res.status(500).json({ error: "Failed to create snippet" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/{id}: - * put: - * summary: Update a snippet - * tags: - * - Snippets - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * content: - * type: string - * description: - * type: string - * folder: - * type: string - * order: - * type: integer - * isNote: - * type: boolean - * responses: - * 200: - * description: The updated snippet. - * 400: - * description: Invalid request. - * 404: - * description: Snippet not found. - */ - router.put( - "/:id", - ctx.rbac.require("edit") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const id = Array.isArray(req.params.id) - ? req.params.id[0] - : req.params.id; - if (!id) return res.status(400).json({ error: "Invalid request" }); - - try { - const result = await repo.updateSnippet( - userId, - parseInt(id, 10), - req.body ?? {}, - ); - if (!result) - return res.status(404).json({ error: "Snippet not found" }); - res.json(result.updated); - } catch (err) { - logError(ctx, "Failed to update snippet", err, "snippet_update_failed"); - res.status(500).json({ error: "Failed to update snippet" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/{id}: - * delete: - * summary: Delete a snippet - * tags: - * - Snippets - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Snippet deleted. - * 400: - * description: Invalid request. - * 404: - * description: Snippet not found. - */ - router.delete( - "/:id", - ctx.rbac.require("delete") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const id = Array.isArray(req.params.id) - ? req.params.id[0] - : req.params.id; - if (!id) return res.status(400).json({ error: "Invalid request" }); - - try { - const existing = await repo.deleteSnippet(userId, parseInt(id, 10)); - if (!existing) - return res.status(404).json({ error: "Snippet not found" }); - res.json({ success: true }); - } catch (err) { - logError(ctx, "Failed to delete snippet", err, "snippet_delete_failed"); - res.status(500).json({ error: "Failed to delete snippet" }); - } - }, - ); - - // Sharing (moved from core's /rbac/snippet* routes) - - /** - * @openapi - * /plugin-api/snippets/share-targets/users: - * get: - * summary: Users the caller may share snippets with - * tags: - * - Snippets - * responses: - * 200: - * description: Users list. - * 500: - * description: Failed to list users. - */ - router.get( - "/share-targets/users", - ctx.rbac.require("share") as never, - async (_req: Request, res: Response) => { - try { - res.json({ users: await ctx.hosts.listUsers() }); - } catch (err) { - logError( - ctx, - "Failed to list share target users", - err, - "snippet_share_targets_users_failed", - ); - res.status(500).json({ error: "Failed to list users" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/share-targets/roles: - * get: - * summary: Non-system roles the caller may share snippets with - * tags: - * - Snippets - * responses: - * 200: - * description: Roles list. - * 500: - * description: Failed to list roles. - */ - router.get( - "/share-targets/roles", - ctx.rbac.require("share") as never, - async (_req: Request, res: Response) => { - try { - res.json({ roles: await ctx.hosts.listRoles() }); - } catch (err) { - logError( - ctx, - "Failed to list share target roles", - err, - "snippet_share_targets_roles_failed", - ); - res.status(500).json({ error: "Failed to list roles" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/{id}/share: - * post: - * summary: Share a snippet with a user or role - * tags: - * - Snippets - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * targetType: - * type: string - * enum: [user, role] - * targetUserId: - * type: string - * targetRoleId: - * type: integer - * expiresAt: - * type: string - * responses: - * 200: - * description: Snippet access updated. - * 400: - * description: Invalid request. - * 403: - * description: Not the snippet's owner. - */ - router.post( - "/:id/share", - ctx.rbac.require("share") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const id = Array.isArray(req.params.id) - ? req.params.id[0] - : req.params.id; - const snippetId = parseInt(id, 10); - if (isNaN(snippetId)) - return res.status(400).json({ error: "Invalid snippet ID" }); - - const { targetType, targetUserId, targetRoleId, expiresAt } = - req.body ?? {}; - if (targetType !== "user" && targetType !== "role") { - return res - .status(400) - .json({ error: "targetType must be user or role" }); - } - if (targetType === "user" && !isNonEmptyString(targetUserId)) { - return res.status(400).json({ error: "targetUserId is required" }); - } - if (targetType === "role" && typeof targetRoleId !== "number") { - return res.status(400).json({ error: "targetRoleId is required" }); - } - - try { - const snippet = await repo.findOwnedById(userId, snippetId); - if (!snippet) - return res.status(403).json({ error: "Not snippet owner" }); - - await repo.upsertSnippetAccess({ - snippetId, - grantedBy: userId, - expiresAt: expiresAt ?? null, - ...(targetType === "user" - ? { targetType: "user" as const, targetUserId } - : { targetType: "role" as const, targetRoleId }), - }); - - res.json({ - success: true, - message: `Snippet shared successfully with ${targetType}`, - }); - } catch (err) { - logError(ctx, "Failed to share snippet", err, "snippet_share_failed"); - res.status(500).json({ error: "Failed to share snippet" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/folder/share: - * put: - * summary: Share every owned snippet in a folder - * description: Also shares snippets in subfolders ("Parent / Child"). - * tags: - * - Snippets - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * folder: - * type: string - * targetType: - * type: string - * enum: [user, role] - * targetUserId: - * type: string - * targetRoleId: - * type: integer - * expiresAt: - * type: string - * responses: - * 200: - * description: Snippet folder shared. - * 400: - * description: Invalid request. - */ - router.put( - "/folder/share", - ctx.rbac.require("share") as never, - async (req: Request, res: Response) => { - const userId = actor(ctx); - const { folder, targetType, targetUserId, targetRoleId, expiresAt } = - req.body ?? {}; - - if (!isNonEmptyString(folder)) { - return res.status(400).json({ error: "folder is required" }); - } - if (targetType !== "user" && targetType !== "role") { - return res - .status(400) - .json({ error: "targetType must be user or role" }); - } - if (targetType === "user" && !isNonEmptyString(targetUserId)) { - return res.status(400).json({ error: "targetUserId is required" }); - } - if (targetType === "role" && typeof targetRoleId !== "number") { - return res.status(400).json({ error: "targetRoleId is required" }); - } - - try { - const snippetsInFolder = await repo.listOwnedSnippetsInFolder( - userId, - folder, - ); - for (const snippet of snippetsInFolder) { - await repo.upsertSnippetAccess({ - snippetId: snippet.id, - grantedBy: userId, - expiresAt: expiresAt ?? null, - ...(targetType === "user" - ? { targetType: "user" as const, targetUserId } - : { targetType: "role" as const, targetRoleId }), - }); - } - - res.json({ success: true, snippetsShared: snippetsInFolder.length }); - } catch (err) { - logError( - ctx, - "Failed to share snippet folder", - err, - "snippet_folder_share_failed", - ); - res.status(500).json({ error: "Failed to share snippet folder" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/{id}/access/{accessId}: - * delete: - * summary: Revoke a snippet share - * tags: - * - Snippets - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * - in: path - * name: accessId - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Snippet access revoked. - * 400: - * description: Invalid request. - * 403: - * description: Not the snippet's owner. - */ - router.delete( - "/:id/access/:accessId", - async (req: Request, res: Response) => { - const userId = actor(ctx); - const snippetId = paramId(req.params.id); - const accessId = paramId(req.params.accessId); - if (isNaN(snippetId) || isNaN(accessId)) { - return res.status(400).json({ error: "Invalid request" }); - } - - try { - const snippet = await repo.findOwnedById(userId, snippetId); - if (!snippet) - return res.status(403).json({ error: "Not snippet owner" }); - - await repo.revokeSnippetAccess(accessId, snippetId); - res.json({ success: true, message: "Snippet access revoked" }); - } catch (err) { - logError( - ctx, - "Failed to revoke snippet access", - err, - "snippet_access_revoke_failed", - ); - res.status(500).json({ error: "Failed to revoke snippet access" }); - } - }, - ); - - /** - * @openapi - * /plugin-api/snippets/{id}/access: - * get: - * summary: List who a snippet is shared with - * tags: - * - Snippets - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: List of access grants. - * 400: - * description: Invalid snippet ID. - * 403: - * description: Not the snippet's owner. - */ - router.get("/:id/access", async (req: Request, res: Response) => { - const userId = actor(ctx); - const snippetId = paramId(req.params.id); - if (isNaN(snippetId)) - return res.status(400).json({ error: "Invalid snippet ID" }); - - try { - const snippet = await repo.findOwnedById(userId, snippetId); - if (!snippet) return res.status(403).json({ error: "Not snippet owner" }); - - res.json(await repo.listSnippetAccess(snippetId)); - } catch (err) { - logError( - ctx, - "Failed to get snippet access list", - err, - "snippet_access_list_failed", - ); - res.status(500).json({ error: "Failed to get snippet access list" }); - } - }); -} diff --git a/plugins/snippets/src/backend/tables.ts b/plugins/snippets/src/backend/tables.ts deleted file mode 100644 index 629960447..000000000 --- a/plugins/snippets/src/backend/tables.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - adoptLegacyTable, - boolean, - defineTable, - id, - integer, - refUser, - text, - timestamp, - varchar, -} from "@termix/plugin-sdk/db"; - -export const snippets = adoptLegacyTable( - "snippets", - defineTable( - "snippets", - { - id: id(), - userId: refUser(), - name: text().notNull(), - content: text().notNull(), - description: text(), - folder: text(), - order: integer().notNull().default(0), - syncId: varchar().unique(), - createdAt: timestamp().notNull().defaultNow(), - updatedAt: timestamp().notNull().defaultNow(), - hostFilter: text(), - isNote: boolean().notNull().default(false), - }, - { indexes: [{ name: "idx_snippets_user_id", columns: ["userId"] }] }, - ), -); - -export const snippetFolders = adoptLegacyTable( - "snippet_folders", - defineTable("snippet_folders", { - id: id(), - userId: refUser(), - name: text().notNull(), - color: text(), - icon: text(), - syncId: varchar().unique(), - createdAt: timestamp().notNull().defaultNow(), - updatedAt: timestamp().notNull().defaultNow(), - }), -); - -export const snippetAccess = adoptLegacyTable( - "snippet_access", - defineTable( - "snippet_access", - { - id: id(), - snippetId: integer().notNull(), - userId: varchar(), - roleId: integer(), - grantedBy: refUser(), - permissionLevel: text().notNull().default("view"), - expiresAt: timestamp(), - createdAt: timestamp().notNull().defaultNow(), - }, - { - indexes: [ - { name: "idx_snippet_access_user_id", columns: ["userId"] }, - { name: "idx_snippet_access_snippet_id", columns: ["snippetId"] }, - { name: "idx_snippet_access_role_id", columns: ["roleId"] }, - ], - }, - ), -); - -export const tables = [snippets, snippetFolders, snippetAccess]; diff --git a/plugins/snippets/src/frontend/AdminUserSnippets.tsx b/plugins/snippets/src/frontend/AdminUserSnippets.tsx deleted file mode 100644 index 76418bbd7..000000000 --- a/plugins/snippets/src/frontend/AdminUserSnippets.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { Pencil, Plus, RefreshCw, Trash2 } from "lucide-react"; -import { toast } from "sonner"; -import { - usePluginApi, - useTranslation, - type PluginApiClient, -} from "@termix/plugin-sdk/frontend"; -import { Button, Input, useConfirmation } from "@termix/plugin-sdk/ui"; - -// Core's admin routes act on another user's data when this header names them. -export const ADMIN_TARGET_USER_HEADER = "X-Admin-Target-User"; - -export function adminOptions(targetUserId: string) { - return { headers: { [ADMIN_TARGET_USER_HEADER]: targetUserId } }; -} - -interface ManagedSnippet { - id: number; - name: string; - content: string; - folder?: string | null; -} - -export function mapSnippets(res: unknown): ManagedSnippet[] { - const list = Array.isArray(res) - ? res - : ((res as { snippets?: unknown[] })?.snippets ?? []); - return (list as Record[]).map((s) => ({ - id: Number(s.id), - name: String(s.name ?? ""), - content: String(s.content ?? ""), - folder: (s.folder as string | null) ?? null, - })); -} - -function errorMessage(error: unknown, fallback: string): string { - return ( - (error as { response?: { data?: { error?: string } } })?.response?.data - ?.error || fallback - ); -} - -async function load(api: PluginApiClient, userId: string) { - return mapSnippets((await api.get("/", adminOptions(userId))).data); -} - -/** The Snippets tab of the admin "manage user" panel. */ -export function AdminUserSnippets({ - user, -}: { - user: { id: string; username: string }; -}) { - const { t } = useTranslation(); - const api = usePluginApi(); - const { confirmWithToast } = useConfirmation(); - const [snippets, setSnippets] = useState([]); - const [loading, setLoading] = useState(true); - const [editing, setEditing] = useState(null); - const [form, setForm] = useState({ name: "", content: "", folder: "" }); - const [saving, setSaving] = useState(false); - - const reload = useCallback(() => { - setLoading(true); - load(api, user.id) - .then(setSnippets) - .catch(() => {}) - .finally(() => setLoading(false)); - }, [api, user.id]); - - useEffect(() => { - reload(); - }, [reload]); - - async function save() { - if (!form.name.trim() || !form.content.trim()) { - toast.error(t("admin.requiredFields")); - return; - } - setSaving(true); - try { - const payload = { - name: form.name.trim(), - content: form.content, - folder: form.folder.trim() || null, - }; - if (editing && editing !== "new") { - await api.put(`/${editing.id}`, payload, adminOptions(user.id)); - } else { - await api.post("/", payload, adminOptions(user.id)); - } - setEditing(null); - reload(); - toast.success(t("admin.saved")); - } catch (error) { - toast.error(errorMessage(error, t("admin.saveFailed"))); - } finally { - setSaving(false); - } - } - - function remove(snippet: ManagedSnippet) { - confirmWithToast( - t("admin.deleteConfirm", { name: snippet.name, username: user.username }), - async () => { - try { - await api.delete(`/${snippet.id}`, adminOptions(user.id)); - setSnippets((prev) => prev.filter((s) => s.id !== snippet.id)); - toast.success(t("admin.deleted")); - } catch (error) { - toast.error(errorMessage(error, t("admin.deleteFailed"))); - } - }, - ); - } - - return ( -
-
- - {t("admin.count", { count: snippets.length })} - -
- - -
-
- {editing && ( -
- setForm((p) => ({ ...p, name: e.target.value }))} - /> -