Compare commits

..

29 Commits

Author SHA1 Message Date
Luke Gustafson c67d914e61 New Crowdin updates (#713)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Hindi)
2026-04-22 17:10:21 -05:00
Luke Gustafson e3cb1f82af v2.1.0 (#711)
* feat: enhance terminal theme preview and persistence (#637)

- Refactor Terminal.tsx to optimize theme update logic and eliminate flashes
- Implement localStorage persistence for terminal themes per host
- Fix hover preview to redraw buffer content instantly
- Ensure theme preferences survive cookie clears

Co-authored-by: Gemini CLI <gemini@cli.local>

* fix: update darwin platform identifier to osx (#626)

* feat: implement SSH algorithms mapping and refactor cipher usage across SSH modules (#627)

* feat: enhance WebSocket connection handling for embedded mode (#628)

* fix(auth): pass JWT token via URL param for Electron/mobile OIDC callback (#630)

The OIDC callback redirect did not include the JWT token as a URL
parameter for desktop/mobile device types, causing Electron and
React Native webviews to have jwt = undefined after login.

Closes Termix-SSH/Support#562

* fix: remove hardcoded version number from dashboard (#632)

The version text was initialized to "v1.8.0" which displayed incorrect
version on the dashboard before the API response. Changed to empty
string so it shows nothing until the real version is fetched.

Closes Termix-SSH/Support#550

* fix: admin role toggle showing incorrect state after update (#633)

After successfully toggling admin status, onSuccess() closes the dialog
and clears the user reference, but onOpenChange(true) then reopens the
dialog with null user, causing isAdmin state to not sync properly.

Removed the redundant dialog reopen after success - let onSuccess
handle the cleanup normally.

Closes Termix-SSH/Support#549

* fix: allow disabling password login when OIDC is configured via env vars (#634)

The admin OIDC config endpoint only checked the database for OIDC
configuration, ignoring environment variables. This caused the frontend
to incorrectly block disabling password login when OIDC was configured
via OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, etc.

Now falls back to getOIDCConfigFromEnv() when no database config exists,
matching the behavior of the public /oidc-config endpoint.

Closes Termix-SSH/Support#561

* fix: sync snippet selected terminals count when tabs are closed (#635)

selectedSnippetTabIds was not cleaned up when terminal tabs were closed,
causing the snippet dialog to show stale terminal count. Added useEffect
to filter out IDs of closed tabs.

Closes Termix-SSH/Support#534

* feature: toggle history globally (#636)

* Fix RDP audio output and dynamic session resize (#625)

Co-authored-by: AllX <contact@alexmaftei.com>

* fix: check connection state before fallback exec in file manager (#644)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: check connection state before fallback exec in file manager

When SFTP operations fail and tryFallbackMethod is called, the SSH
client may already be disconnected. Calling client.exec() on a
disconnected client throws an unhandled exception that crashes the
backend process.

Added connection state check at the start of all three
tryFallbackMethod closures (listFiles, writeFile, uploadFile).

Closes Termix-SSH/Support#451

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: restrict postMessage targetOrigin to prevent JWT leakage (#645)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: restrict postMessage targetOrigin to prevent JWT leakage

Multiple postMessage calls used wildcard "*" as targetOrigin, allowing
any parent window to intercept JWT tokens if Termix is embedded in an
iframe.

Changes:
- main-axios.ts: Only send postMessage in Electron iframe context
  (added isElectron() check), use window.location.origin as target
- Auth.tsx: Replace "*" with window.location.origin for all three
  AUTH_SUCCESS postMessage calls (already gated by isInElectronWebView)
- ElectronLoginForm.tsx: Use server URL origin for iframe postMessage,
  fall back to "*" only if origin parsing fails

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: stats monitoring resolves SSH key from credential privateKey field (#643)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: stats monitoring resolves SSH key from credential privateKey field

When loading credentials for status monitoring, only the `key` field
was checked but not `privateKey`. The ssh_credentials table has both
fields and some credentials store the key in `privateKey`. This caused
stats polling to fail with auth errors for key-based credentials.

Closes Termix-SSH/Support#429

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* feat: add collapse/expand all button for host manager folders (#642)

* Update sha256 value for v2.0.0 universal dmg (#629)

* feat: add collapse/expand all button for host manager folders

All folders were always auto-expanded with no way to collapse them all
at once. Added a toggle button in the toolbar that collapses or expands
all folder accordions. Icon switches between ChevronsDownUp (collapse)
and ChevronsUpDown (expand) to indicate current action.

Closes Termix-SSH/Support#488

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: prevent invalid SSH key from crashing stats polling loop (#640)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: prevent invalid SSH key from crashing stats polling loop

Two fixes:
1. Add .catch() to pollHostMetrics() call inside setInterval to prevent
   unhandled promise rejections from crashing the process
2. Add "Invalid SSH key format" to the auth failure error patterns in
   collectMetrics so it's properly tracked instead of re-thrown

Closes Termix-SSH/Support#478

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: revoke all sessions when password is changed or reset (#647)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: revoke all sessions when password is changed or reset

logoutUser() without sessionId only cleared in-memory crypto state
but did not delete session records from the database. This meant
old JWT tokens remained valid after password change/reset.

Now deletes all session records for the user when no specific
sessionId is provided, which is the code path used by password
reset and password change handlers.

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: isolate RDP keyboard input to active tab (#663)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: disable RDP keyboard input when tab is not visible

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* Fix clipboard paste browser popup (#667)

* feat: switch to adjacent tab when closing current tab (#661)

* Update sha256 value for v2.0.0 universal dmg (#629)

* feat: switch to adjacent tab when closing current tab

Previously closing the current tab always switched to the first
remaining tab (often Dashboard). Now switches to the adjacent tab —
the next one in order, or the previous if the closed tab was last.

This matches the tab-close behavior of browsers and IDEs.

Closes Termix-SSH/Support#606

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: add auth token to database export/import in Electron app (#664)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: add Bearer token to database export/import requests in Electron

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* Fix WebSocket reconnection and add connection lost overlay (#668)

* fix: skip metrics collection for hosts with authType none or opkssh (#639)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: skip metrics collection for hosts with authType none or opkssh

supportsMetrics() only checked connectionType but ignored authType.
Hosts configured with Authentication: None (e.g. Tailscale SSH) or
opkssh would trigger SSH metrics polling, causing repeated auth
failures since no credentials are available.

Closes Termix-SSH/Support#515

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: align cookie maxAge with JWT expiration to prevent early logout (#658)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: align cookie maxAge with JWT expiration to prevent early logout

The JWT cookie maxAge for regular (non-rememberMe) logins was set to
2 hours, while the JWT token itself was valid for 24 hours. After 2
hours the cookie expired and the user was logged out, even during
active SSH sessions.

Changed cookie maxAge from 2h to 24h for regular logins to match
the JWT expiration. Affects both password login and OIDC login paths.

Closes Termix-SSH/Support#595
Closes Termix-SSH/Support#583

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: remove sensitive data from log output (#649)

- Password reset: stop logging the 6-digit reset code in plaintext.
  The code is still stored in the settings table for retrieval.
- Password reset: return identical response for non-existent users
  and OIDC users to prevent username enumeration.
- OPKSSH callback: remove URL, query params, and forwarded headers
  from log output to prevent token/code leakage.

* feat: display file owner and group in file manager list view (#654)

* Update sha256 value for v2.0.0 universal dmg (#629)

* feat: display file owner and group in file manager list view

Added an Owner column to the file manager list view showing owner:group
for each file. The data was already returned by the backend (SFTP
returns uid/gid, ls fallback returns usernames) but not displayed.

Column is hidden on small screens (md:block) to avoid crowding.

Closes Termix-SSH/Support#603

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: prevent file manager from showing stale directory contents (#655)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: prevent file manager from showing stale directory contents

Two issues caused the file browser to get stuck showing outdated
directory contents after folder operations:

1. handleRefreshDirectory could be blocked by a lingering isLoading
   state from a previous request. Now force-resets loading state
   before initiating the refresh.

2. debouncedLoadDirectory skipped requests when the path hadn't
   changed (path === lastPathChangeRef), but after create/move/delete
   operations the path stays the same while contents change. Added
   force parameter to bypass the path equality check.

Closes Termix-SSH/Support#599

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: fallback to default layout when dashboard preferences lack cards (#652)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: fallback to default layout when dashboard preferences lack cards

getDashboardPreferences may return null, empty object, or an object
without a cards array (e.g. when behind a reverse proxy that alters
the response, or on first load for a new user). This caused
layout.cards.filter() to throw, leaving the dashboard as a black
screen after login.

Now validates that the response has a cards array before using it,
falling back to DEFAULT_LAYOUT otherwise.

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: bind SSH sessions to userId and verify ownership on access (#650)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: bind SSH sessions to userId and verify ownership on access

SSHSession objects in file-manager and docker did not store userId,
allowing any authenticated user to operate on another user's session
if they knew the sessionId.

Changes:
- Added userId field to SSHSession interface in both modules
- Store userId when creating sessions (connect, TOTP, Warpgate paths)
- Added verifySessionOwnership() helper in file-manager
- Applied ownership checks to sudo-password, status, keepalive,
  listFiles endpoints in file-manager
- Applied ownership check to keepalive endpoint in docker
- Session creation in docker now stores userId in all 3 paths

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: use cookie-based auth for WebSocket instead of URL token (#646)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: use cookie-based auth for WebSocket instead of URL token

JWT tokens in WebSocket URL query strings are exposed in nginx access
logs, browser history, and proxy logs.

Backend: terminal and docker-console WebSocket servers now read JWT
from the cookie header as fallback when no URL token is provided.

Frontend: desktop terminal and docker console no longer append token
to WebSocket URL, relying on cookies sent automatically by the browser.

Mobile and Guacamole WebSocket connections are unchanged as they may
not have cookie access.

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: prevent long Docker container names from overflowing card bounds (#653)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: prevent long Docker container names from overflowing card bounds

Container names were not constrained to the card width, causing long
names to overlay adjacent container cards. Added overflow-hidden and
min-w-0 to the Card root element so the existing truncate class on
CardTitle takes effect within the grid layout.

Closes Termix-SSH/Support#601

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: preserve original timestamps in SSH login statistics (#657)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: preserve original timestamps in SSH login statistics

Failed login attempts showed the current time instead of the actual
attempt time. Two issues:

1. auth.log dates (e.g. "Mar 15 10:23:45") were parsed with a format
   that could fail on some platforms, falling back to new Date() which
   gives the current time. Changed to a more reliable format
   ("Mar 15, 2026 10:23:45") and fall back to the raw string instead
   of the current time.

2. Dates from previous years (e.g. December logs viewed in January)
   were assigned the current year, producing future dates. Now checks
   if the parsed date is in the future and subtracts a year.

Also fixed the same fallback issue for successful login timestamps.

Closes Termix-SSH/Support#570

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: remove plaintext credentials from internal host API responses (#651)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: remove plaintext credentials from internal host API responses

/db/host/internal and /db/host/internal/all returned password, key,
keyPassword, and autostart credentials in plaintext, protected only
by a static INTERNAL_AUTH_TOKEN. If the token leaked, all SSH
credentials would be exposed.

Changes:
- Stripped password, key, keyPassword, autostartPassword, autostartKey,
  autostartKeyPassword from both internal API responses
- Only return hostId, userId, and non-sensitive connection metadata
- Updated tunnel.ts endpoint resolution to use resolveHostById() for
  credentials instead of reading from HTTP response
- Autostart tunnel initialization no longer receives credentials from
  internal API, relying on server-side DB resolution at connect time

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: default keyType to auto instead of blocking host update (#641)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: default keyType to auto instead of blocking host update

When editing a host with key authentication, missing keyType value
caused form validation to fail silently, preventing the Update Host
button from saving changes. Now defaults keyType to "auto" instead
of raising a validation error.

Closes Termix-SSH/Support#510

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: downgrade credential migration errors to warnings (#659)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: downgrade credential migration errors to warnings

Credential migration failures during login (e.g. corrupted encrypted
data from older versions) were logged at ERROR level, causing alarm
in Docker logs. The migration is non-blocking — login succeeds
regardless — so these should be warnings.

Changed individual credential decryption failures and overall migration
failures from error to warn level. Also improved log messages to be
more descriptive.

Closes Termix-SSH/Support#541

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* feat: add Select All / Deselect All buttons for snippet terminal selection (#660)

* Update sha256 value for v2.0.0 universal dmg (#629)

* feat: add Select All / Deselect All buttons for snippet terminal selection

When running snippets on many terminals, users had to click each
terminal individually. Added Select All and Deselect All buttons
above the terminal list for batch selection.

Closes Termix-SSH/Support#535

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: admin user list not reading OIDC and admin status correctly (#665)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: align admin user list field names with API response (camelCase)

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: enable clipboard paste from host to RDP session (#666)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: sync host clipboard to RDP session on tab focus and mouse enter

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* fix: validate containerId and timestamp params to prevent command injection (#648)

* Update sha256 value for v2.0.0 universal dmg (#629)

* fix: validate containerId and timestamp params to prevent command injection

Docker API endpoints passed containerId, since, and until parameters
directly into shell commands via SSH exec without validation. An
authenticated user with Docker access could inject arbitrary shell
commands on the remote host.

Added Express param middleware to validate containerId against
^[a-zA-Z0-9][a-zA-Z0-9_.-]*$ for all 9 endpoints. Also validate
since/until timestamps in the logs endpoint against a strict regex.

---------

Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>

* Merge commit from fork

* Merge commit from fork

Replace double-quoted shell string interpolation with single-quoted
escaping in extractArchive and compressFiles endpoints. Double quotes
allow $(command) substitution, enabling arbitrary command execution
on the remote SSH host via crafted archive paths or file names.

Now uses the same single-quote escaping pattern used by all other
file manager operations in this file.

* Merge commit from fork

CORS: Replace permissive origin checks (any http/https) across all 6
microservices with a shared cors-config module that only allows:
- Same-origin requests (derived from Host header)
- Configured origins via CORS_ALLOWED_ORIGINS env var
- Dev origins (localhost:5173)

Docker console: Validate containerId against ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$
and restrict shell to allowlist [bash, sh, ash, zsh] to prevent command
injection via WebSocket messages.

* Merge commit from fork

* refactor: add shared host-resolver for server-side credential resolution

Creates resolveHostById() utility that loads a host from DB and resolves
its credentials entirely server-side. This will be used by connection
modules to avoid receiving credentials from the frontend.

Also adds checkHostAccess() for permission validation.

* fix: strip sensitive credentials from host API responses

Remove password, key, keyPassword, sudoPassword, and other credential
fields from GET /db/host and GET /db/host/:id responses. Add boolean
indicators (hasPassword, hasKey, hasSudoPassword) so the frontend
knows capabilities without seeing actual values.

Add GET /db/host/:id/password endpoint for the copy-password feature
to fetch a specific password on demand.

* refactor: docker-console resolves credentials server-side by hostId

Instead of receiving the full hostConfig with credentials from the
frontend WebSocket message, docker-console now extracts hostId and
uses resolveHostById() to load credentials from the database.

Also validates containerId format and restricts shell to allowlist.

* feat: add getHostPassword API and update copy-password to use it

Add getHostPassword() frontend function that calls the new server-side
password endpoint instead of reading from the host object.

Update Tab component to use boolean indicators (hasPassword, hasKey,
hasSudoPassword) from the sanitized API response, with backward
compatibility for the old response format.

Add boolean indicator fields to Host type definition.

* refactor: file-manager resolves credentials server-side via host-resolver

When frontend doesn't provide password/sshKey (due to API stripping),
file-manager now uses resolveHostById() to load credentials from DB.
Falls back to provided credentials for backward compatibility.

* refactor: terminal resolves credentials server-side via host-resolver

When frontend doesn't provide password/key (due to API stripping),
terminal now uses resolveHostById() to load credentials from DB.
Preserves backward compatibility with reconnect_with_credentials
where user provides credentials interactively.

* refactor: tunnel resolves source credentials server-side via host-resolver

When frontend doesn't provide sourcePassword/sourceSSHKey (due to API
stripping), tunnel now uses resolveHostById() to load credentials from
DB for both the connect and cleanup paths.

* fix: terminal sudo auto-fill fetches password from server on demand

After credentials are stripped from API responses, hostConfig.password
is no longer available. Sudo auto-fill now checks boolean indicators
to show the prompt, then fetches the actual password via getHostPassword
API only when the user confirms the auto-fill action.

* fix: host editor fetches full credentials via export API for editing

After credentials are stripped from the host list API, the editor would
show empty password/key fields. Now uses exportSSHHostWithCredentials()
to fetch the full host data with credentials when opening the editor.
Applies to all paths: direct edit, sidebar click, and external navigation.

---------

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

* feat: update jsdoc comments for /host instead of /ssh

* fix: align OIDC login cookie maxAge with JWT expiration (2h → 24h) (#671)

* fix: persist OIDC JWT token to localStorage in Electron app (#672)

* fix: add error toast for empty file download and remove stray prop in tab bar (#674)

* fix: prevent server status failure from blocking host list loading (#673)

* fix: show server config dialog on first launch instead of auto-selecting embedded (#675)

* fix: remove unnecessary registration disabled toast on login page (#670)

* fix: add clipboard fallback and toast feedback for Copy Password button (#669)

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

* fix: allow file origin for packaged Electron desktop app (#676)

* Add AWS logo to README

* fix: allow file origin for packaged Electron desktop app

---------

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

* fix: backend compliation errors

* feat: remove theme selector from nav bar

* fix: validate and fallback credentialId during JSON host bulk import (#677)

* Add AWS logo to README

* fix: validate and fallback credentialId during JSON host bulk import

---------

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

* fix: await tunnel cleanup to prevent new connection from being killed (#678)

* Add AWS logo to README

* fix: await tunnel cleanup before creating new connection to prevent race condition

---------

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

* fix: disable keyboard-interactive when host auth is set to None (#682)

* Add AWS logo to README

* fix: disable keyboard-interactive auth when host authType is none

---------

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

* fix: use carriage return for mobile startup snippet execution (#680)

* Add AWS logo to README

* fix: use carriage return for mobile startup snippet execution

---------

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

* fix: prevent file upload from crashing backend on permission denied (#681)

* Add AWS logo to README

* fix: add stderr error handlers and connection check to prevent upload crash

---------

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

* fix: add missing stream error handlers in Docker console (#684)

* Add AWS logo to README

* fix: add missing stream error handlers in Docker console to prevent crashes

---------

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

* fix: use carriage return for snippet execution to support PowerShell (#679)

* Add AWS logo to README

* fix: use carriage return instead of line feed for snippet and command execution

---------

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

* fix: restrict remaining postMessage targetOrigin from wildcard to origin (#685)

* Add AWS logo to README

* fix: restrict postMessage targetOrigin to prevent JWT token leakage

---------

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

* feat: add SESSION_TIMEOUT_HOURS environment variable for customizable session duration (#662)

* feat: add SESSION_TIMEOUT_HOURS environment variable for session duration

Session timeout was hardcoded to 24h (JWT) and 2h (cookie). Now both
are configurable via SESSION_TIMEOUT_HOURS env var (default: 24).

Set in docker-compose.yml:
  environment:
    SESSION_TIMEOUT_HOURS: "72"

Also fixes the cookie maxAge mismatch (was 2h, now matches JWT).
Remember Me sessions remain at 30 days regardless of this setting.

Closes Termix-SSH/Support#609
Closes Termix-SSH/Support#595

* refactor: move session timeout from env var to Admin Settings

Replace SESSION_TIMEOUT_HOURS environment variable with a database-backed
setting configurable from Admin Settings UI. Default remains 24 hours.

---------

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

* feat: add port knocking support for SSH connections (#694)

* Add AWS logo to README

* feat: add port knocking support for SSH connections

Send TCP/UDP knock packets to a configurable port sequence before
establishing SSH connections. Configured per-host in the host editor
under a new Port Knocking accordion section. Supports custom protocol
(TCP/UDP) and delay between knocks. Knocking failures don't block
the connection attempt.

Closes Termix-SSH/Support#524

---------

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

* feat: add Wake-on-LAN support for hosts (#696)

* Add AWS logo to README

* feat: add Wake-on-LAN support for hosts

---------

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

* feat: add export all hosts as JSON (#688)

* Add AWS logo to README

* feat: add export all hosts as JSON

Add GET /ssh/db/hosts/export endpoint and Export All button in the host
manager toolbar. Exported format is compatible with existing bulk import.
Includes sensitive data warning confirmation before download.

Closes Termix-SSH/Support#582

---------

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

* feat: add snippet sharing with users and roles (#691)

* Add AWS logo to README

* feat: add snippet sharing with users and roles

Add snippetAccess table and RBAC routes for sharing snippets, following
the same pattern as host sharing. Users can share snippets with other
users or roles via a share dialog. Shared snippets appear in a dedicated
section in the snippets sidebar as read-only with copy support.

Closes Termix-SSH/Support#474

---------

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

* fix: auth errors and ws connection errors in dev env

* feat: add opt-in tmux integration for persistent terminal sessions (#683)

* Add backend tmux integration with native scrollback

Detect tmux on remote hosts via SSH exec channel, auto-attach or create
sessions with mouse mode, history-limit 50000, set-clipboard on, and
allow-passthrough on for native scrollback, OSC 52 clipboard sync, and
safe paste handling. Use && exit so the shell only closes if tmux
started successfully. Query session name after auto-creation.

* Add frontend tmux session handling and picker dialog

Desktop: handle tmux WebSocket messages, show session picker with window
count, attached clients, and last activity when multiple sessions exist.
Toast warning when Auto-tmux is enabled but tmux is missing on remote.
Mobile: auto-attach to first available session. All user-facing strings
are localized via i18n.

* Add Auto-tmux toggle in host settings and i18n strings

Per-host opt-in toggle following the existing autoMosh pattern.
English i18n strings for all tmux-related UI elements.

* Show a toast hint on first drag inside a tmux session

When the user drags the mouse inside a tmux-wrapped terminal, show a
localized toast ("Adjust selection and press Enter to copy") once per
tab session. Purely frontend so the hint is i18n-ready and doesn't
pollute the tmux status bar.

* chore: increment ver

* feat: add right-click context menu in terminal to open file manager (#695)

* Add AWS logo to README

* feat: add right-click context menu in terminal to open file manager

---------

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

* Fix/desktop guac connect flow (#687)

* fix: use direct guacamole websocket port in embedded electron mode

* Fix desktop remote token flow for redacted hosts

---------

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

* fix: allow file:// origin in shared cors middleware (#686)

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

* feat: add command history toggle and sensitive command filtering (#693)

* Add AWS logo to README

* feat: add command history toggle and sensitive command filtering

Add on/off toggle for command history recording in User Profile settings.
Commands matching sensitive patterns (passwords, secrets, tokens, API keys)
are automatically filtered on both frontend and backend, never stored.

Closes Termix-SSH/Support#461

---------

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

* OPKSSH proxy, certificate auth, and inline provider selection (#692)

* fix: OPKSSH proxy integration for remote deployments

Migrate proxy routes from /ssh/ to /host/ prefix. Use session's
remote redirect URI for callback path instead of hardcoded
/login-callback. Add OAuth callback fallback for external browser
redirects with state parameter binding to prevent cross-session
mixup. Reject cookie-less callbacks that can't be identified.

* fix: implement OPKSSH certificate authentication for ssh2

Extract OPKSSH certificate auth into shared module that works
around ssh2's lack of native certificate support: grafts cert
blob onto parsed key, wraps ECDSA sign() for DER-to-SSH
conversion, and patches Protocol.authPK for correct algorithm.
Applied across terminal, file-manager, docker, and server-stats.
Removes legacy temp file approach in favor of in-memory keys.

* feat: inline OPKSSH provider selection in dialog

Parse OIDC provider aliases and issuers from config.yml using
js-yaml and send them to the frontend via the WebSocket message.
The dialog renders a "Sign in with {Provider}" button per provider,
opening the browser directly to the OAuth flow and skipping the
external chooser page. Falls back to the existing "Open in Browser"
behavior when providers aren't available.

---------

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

* feat: add configurable log level via Admin Settings (#690)

* Add AWS logo to README

* feat: add configurable log level via Admin Settings

Add log verbosity control (debug/info/warn/error) through Admin Settings
UI and LOG_LEVEL environment variable. Database setting takes precedence.
Changes take effect immediately without restart.

Closes Termix-SSH/Support#499

---------

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

* feat: add reconnect button for disconnected SSH sessions (#689)

* Add AWS logo to README

* feat: add reconnect button for disconnected SSH sessions

When an SSH connection drops, show a reconnect overlay instead of
closing the tab. Users can click Reconnect to re-establish the
connection or Close to dismiss. Also triggers after auto-reconnect
attempts are exhausted.

Closes Termix-SSH/Support#596
Closes Termix-SSH/Support#542
Closes Termix-SSH/Support#604

---------

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

* feat: fix port knocking and mac address not saving to backend

* fix: fix snippets table not being created

* fix: command history logic error, snippet sharing failing and improved UI for it

* Reset stale trust state when TOTP is enabled (#697)

* Add AWS logo to README

* Reset stale trust state when TOTP is enabled

Enablement now updates the user record, revokes existing sessions, clears trusted devices, and persists the result using the existing route flow. The change stays narrow and avoids introducing a one-off auth-manager wrapper or changing the save helper contract.

Constraint: Keep the change close to the existing auth route and avoid extra abstractions
Rejected: Keep the dedicated auth-manager helper | it was single-use and widened the surface area
Confidence: high
Scope-risk: narrow
Directive: If this behavior changes again, keep the reset logic at the route boundary unless another caller appears
Tested: tsc -p tsconfig.node.json --pretty false, git diff --check
Not-tested: full frontend build

---------

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

* fix: version disabling in user profile not properly disabling and mac address not saving in host manager

* feat: improved reconnect ui for terminals

* feat: improve right click copy/paste

* fix: some themes not including all the needed colors

* chore: remove donate button

* fix: schema errors, password logic errors, sswapped line order in host.ts

* fix: cors being too strict

* fix: passphrase erorr and tmux error

* fix: guacd improvements, ui bugs, connection problems, etc

* fix: shrink image, opkssh fixes, desktop ui changes

* feat: dont require password to export and fixed export failures

* feat: opkssh fixes, guacamole ui fixes, update readme for release

* fix: tabs closing fast causiung no tab to be active and electron header persistance issue

* fix: guacd params getting malformed

* fix: desktop app header persistance

* fix: desktop app header persistance

* feat: desktop app not logging in

* feat: improve okpkssh implementation and fix redirect uri bug

* fix: opkssh redirect

* fix: backend hang (ongoing)

* fix: tunnels not being able to be saved

* fix: c2s networking stability (activity/log, metrics, status) (#701)

- /activity/log: the trim-over-100 path called SimpleDBOps.delete with a
  userId instead of a where clause and 500'd every call. Use inArray on
  the actual ids, best-effort (trim failures don't fail the log).

- /metrics/register-viewer: now a graceful 200 no-op when the host
  can't be found, metrics are disabled, or the connection type doesn't
  support metrics. Any internal error is reported as skipped instead
  of a 500, and the fire-and-forget startMetricsForHost can no longer
  leak an unhandled rejection.

- /metrics/🆔 treat 404 as "no metrics yet / disabled" rather than
  an error. Dashboard skips hosts known to be offline before asking
  for metrics.

- /status: retry with 2s/5s/8s timeouts and 3s/5s pauses (23s worst
  case, fits in the 30s poll cycle) before surfacing a network error;
  intermediate attempts stay silent.

- Replace the blocking "connection lost" overlay with a persistent,
  non-dismissible toast ("Unstable server connection, recovering…")
  carrying a Reload action. Users keep full access to the UI; if they
  try to connect to a host and it fails, that's on them. The toast
  clears to the usual "Server connection restored" success toast on
  the next healthy API response. The toast triggers on any of
  ERR_NETWORK / ECONNREFUSED / ECONNABORTED / ECONNRESET / ETIMEDOUT
  / ERR_CANCELED, "Request aborted"/timeout messages, or
  database/drizzle/sqlite errors.

* fix(guacamole): honor host RDP DPI in client and tab params (#703)

* fix(file-manager): preserve remote file mode after SFTP write (#704)

* fix(admin): target admin toggle APIs by user id (#705)

* fix(terminal): resolve Electron SSH websocket URL from server config (#706)

* fix(snippets): accept snippets or legacy updates in reorder API (#707)

* fix(admin): fetch users list when users tab is opened (#708)

* fix(docker): improve list layout and overflow for container cards (#709)

* fix(guacamole): gate keyboard capture on focus and visibility (#710)

* fix: remove snippets test file

* chore: run linter

* fix: increase macos memory for building

* Potential fix for pull request finding 'Unused variable, import, function or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

---------

Co-authored-by: Will Moore <will@clevercode.ca>
Co-authored-by: Gemini CLI <gemini@cli.local>
Co-authored-by: Chakyiu <49145984+Chakyiu@users.noreply.github.com>
Co-authored-by: Jozef Rebjak <jozefrebjak@icloud.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Daniel Quinan <68088383+DanielQuinan@users.noreply.github.com>
Co-authored-by: allxm4 <77125344+allxm4@users.noreply.github.com>
Co-authored-by: AllX <contact@alexmaftei.com>
Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com>
Co-authored-by: Dylan Ysmal <Xenthys@users.noreply.github.com>
Co-authored-by: vvbbnn00 <vvbbnn00@foxmail.com>
Co-authored-by: Lbubeer <Lbubeer1@gmail.com>
Co-authored-by: Dominik <DL6ER@users.noreply.github.com>
Co-authored-by: LukeGus <lukegustafson06@gmail.com>
Co-authored-by: TerrifiedBug <35064668+TerrifiedBug@users.noreply.github.com>
Co-authored-by: JIHUN <asdfgl98@naver.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-04-22 16:55:23 -05:00
Luke Gustafson 9b1cc3dbf4 Simplify language selection section in README 2026-04-20 17:23:51 -05:00
Luke Gustafson e87abe676e Update guacd image version to 1.6.0 2026-04-20 17:16:01 -05:00
Luke Gustafson 4f092f7c4b Delete .github/FUNDING.yml 2026-04-14 16:04:24 -05:00
Luke Gustafson 579fa81ac3 Remove sponsorship information from README
Removed sponsorship section from README.
2026-04-14 16:04:06 -05:00
Luke Gustafson 1492549d36 Add AWS logo to README 2026-04-09 16:36:13 -05:00
Luke Gustafson 8aa8911671 Update Akamai logo height in README.md 2026-04-06 22:19:03 -05:00
Luke Gustafson 76aad24f94 Add Akamai logo to README 2026-04-06 22:17:55 -05:00
Razvan Aurariu 2931fd71bb Update sha256 value for v2.0.0 universal dmg (#629) 2026-03-29 00:28:54 -05:00
Luke Gustafson 0656e1aee4 Update logo from DartNode to TailScale
Replaced DartNode logo with TailScale logo in README.
2026-03-25 16:27:41 -05:00
Luke Gustafson 4aa7d6e3d1 Update DartNode logo in README 2026-03-22 15:53:44 -05:00
Luke Gustafson dfa8f25299 Update image alt text and add DartNode logo 2026-03-22 15:50:41 -05:00
LukeGus 69eca2652b feat: add migration to fix host loading errors 2026-03-15 00:52:21 -05:00
Luke Gustafson aef1036ab0 New Crowdin updates (#624)
* Update source file en.json

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* Update source file en.json

* New translations en.json (Russian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Indonesian)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Hindi)
2026-03-14 21:55:14 -05:00
Luke Gustafson d695663e41 Add Guacamole integration settings to en.json 2026-03-14 21:49:17 -05:00
LukeGus 288e73cc20 chore: update casks 2026-03-14 21:18:00 -05:00
Luke Gustafson d7450d5867 New Crowdin updates (#622)
* Update source file en.json

* New translations en.json (Russian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

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

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Indonesian)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Hindi)
2026-03-14 20:07:53 -05:00
Luke Gustafson e9e30cd318 v2.0.0 (#621)
* Guacd, Docker-Compose, RDP (#475)

* fix select edit host but not update view (#438)

* fix: Checksum issue with chocolatey

* fix: Remove homebrew old stuff

* Add Korean translation (#439)

Co-authored-by: 송준우 <2484@coreit.co.kr>

* feat: Automate flatpak

* fix: Add imagemagik to electron builder to resolve build error

* fix: Build error with runtime repo flag

* fix: Flatpak runtime error and install freedesktop ver warning

* fix: Flatpak runtime error and install freedesktop ver warning

* feat: Re-add homebrew cask and move scripts to backend

* fix: No sandbox flag issue

* fix: Change name for electron macos cask output

* fix: Sandbox error with Linux

* fix: Remove comming soon for app stores in readme

* Adding Comment at the end of the public_key on the host on deploy (#440)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

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

---------

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

* Sudo auto fill password (#441)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Feature Sudo password auto-fill;

* Fix locale json shema;

---------

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

* Added Italian Language; (#445)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Added Italian Language;

---------

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

* Auto collapse snippet folders (#448)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* feat: Add collapsable snippets (customizable in user profile)

* Translations (#447)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Added Italian Language;

* Fix translations;

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

---------

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

* Remove PTY-level keepalive (#449)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

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

---------

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

* feat: add Guacamole support for RDP, VNC, and Telnet connections

- Implemented WebSocket support for Guacamole in Nginx configuration.
- Added REST API endpoints for generating connection tokens and checking guacd status.
- Created Guacamole server using guacamole-lite for handling connections.
- Developed frontend components for testing RDP/VNC connections and displaying the remote session.
- Updated package dependencies to include guacamole-common-js and guacamole-lite.
- Enhanced logging for Guacamole operations.

* feat: enhance Guacamole support with RDP and VNC connection settings and UI updates

* feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation

* fix: finalize adding docker to db

* fix: merge syntax errors

* feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay

* feat: add TypeScript definitions for guacamole-common-js module

* feat: enhance Mouse.State constructor to accept optional parameters and object destructuring

* feat: Add support for RDP and VNC connections in SSH host management

- Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures.
- Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert.
- Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options.
- Implemented token retrieval for RDP/VNC connections using Guacamole API.
- Updated UI components to reflect connection type changes and provide appropriate connection buttons.
- Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor.
- Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions.

* feat: Enhance Guacamole integration with extended configuration options

- Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings.
- Implemented logging for token requests and received options for better debugging.
- Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values.
- Integrated Guacamole configuration parsing in HostManagerViewer and Host components.
- Enhanced API requests to include extended Guacamole configuration parameters in the token request.
- Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API.

* feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes

---------

Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: junu <bigdwarf_@naver.com>
Co-authored-by: 송준우 <2484@coreit.co.kr>
Co-authored-by: SlimGary <trash.slim@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: rename api routes and files

* feat: improve guacd ui/backend

* feat: improve guacd ui/backend

* fix: state persistance issues causing refresh

* feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration

* feat: continue improving integration also with bug fixes

* Merge 2.0.0 with 2.0.0 that includes bug fixes (#620)

* Guacd, Docker-Compose, RDP (#475)

* fix select edit host but not update view (#438)

* fix: Checksum issue with chocolatey

* fix: Remove homebrew old stuff

* Add Korean translation (#439)

Co-authored-by: 송준우 <2484@coreit.co.kr>

* feat: Automate flatpak

* fix: Add imagemagik to electron builder to resolve build error

* fix: Build error with runtime repo flag

* fix: Flatpak runtime error and install freedesktop ver warning

* fix: Flatpak runtime error and install freedesktop ver warning

* feat: Re-add homebrew cask and move scripts to backend

* fix: No sandbox flag issue

* fix: Change name for electron macos cask output

* fix: Sandbox error with Linux

* fix: Remove comming soon for app stores in readme

* Adding Comment at the end of the public_key on the host on deploy (#440)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

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

---------

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

* Sudo auto fill password (#441)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Feature Sudo password auto-fill;

* Fix locale json shema;

---------

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

* Added Italian Language; (#445)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Added Italian Language;

---------

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

* Auto collapse snippet folders (#448)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* feat: Add collapsable snippets (customizable in user profile)

* Translations (#447)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

* Added Italian Language;

* Fix translations;

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

---------

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

* Remove PTY-level keepalive (#449)

* Add termix.rb Cask file

* Update Termix to version 1.9.0 with new checksum

* Update README to remove 'coming soon' notes

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

---------

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

* feat: add Guacamole support for RDP, VNC, and Telnet connections

- Implemented WebSocket support for Guacamole in Nginx configuration.
- Added REST API endpoints for generating connection tokens and checking guacd status.
- Created Guacamole server using guacamole-lite for handling connections.
- Developed frontend components for testing RDP/VNC connections and displaying the remote session.
- Updated package dependencies to include guacamole-common-js and guacamole-lite.
- Enhanced logging for Guacamole operations.

* feat: enhance Guacamole support with RDP and VNC connection settings and UI updates

* feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation

* fix: finalize adding docker to db

* fix: merge syntax errors

* feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay

* feat: add TypeScript definitions for guacamole-common-js module

* feat: enhance Mouse.State constructor to accept optional parameters and object destructuring

* feat: Add support for RDP and VNC connections in SSH host management

- Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures.
- Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert.
- Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options.
- Implemented token retrieval for RDP/VNC connections using Guacamole API.
- Updated UI components to reflect connection type changes and provide appropriate connection buttons.
- Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor.
- Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions.

* feat: Enhance Guacamole integration with extended configuration options

- Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings.
- Implemented logging for token requests and received options for better debugging.
- Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values.
- Integrated Guacamole configuration parsing in HostManagerViewer and Host components.
- Enhanced API requests to include extended Guacamole configuration parameters in the token request.
- Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API.

* feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes

---------

Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: junu <bigdwarf_@naver.com>
Co-authored-by: 송준우 <2484@coreit.co.kr>
Co-authored-by: SlimGary <trash.slim@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: rename api routes and files

* feat: improve guacd ui/backend

* feat: improve guacd ui/backend

* fix: state persistance issues causing refresh

* feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration

* feat: continue improving integration also with bug fixes

---------

Co-authored-by: Wesley Reid <starhound@lostsouls.org>
Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com>
Co-authored-by: junu <bigdwarf_@naver.com>
Co-authored-by: 송준우 <2484@coreit.co.kr>
Co-authored-by: SlimGary <trash.slim@gmail.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: allow customizing guacd backened url

* fix: ssh route mistmatching and guacamole url not changing

* chore: increment ver

* feat: change default to work with default compose, added splits creen support, updated readmes

* fix: linux app not starting due to better sqlite isuses, improved copy/paste system so no context menu, added oidc remember me toggle, improved OS detection for sessions, flatpak invalid key, and sharing hosts with other users errors

* fix: global settings not setting

* chore: update compose

* feat: improve the global status input

* chore: cleanup files

* chore: update export/improt with new host fields

* fix: file manager and docker not loading properly

---------

Co-authored-by: Wesley Reid <starhound@lostsouls.org>
Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com>
Co-authored-by: junu <bigdwarf_@naver.com>
Co-authored-by: 송준우 <2484@coreit.co.kr>
Co-authored-by: SlimGary <trash.slim@gmail.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-14 20:05:05 -05:00
LukeGus a255a08903 fix: oidc, global default, and remember me issues 2026-03-10 23:32:43 -05:00
LukeGus ea2e59abd8 fix: monitoring defaults not saving and OIDC redirect issues 2026-03-10 00:09:58 -05:00
LukeGus 5caadf1d5d fix: macOS submit error due to icon 2026-03-08 23:03:14 -05:00
LukeGus 98d3c86cc7 chore: update cask 2026-03-08 22:27:07 -05:00
LukeGus ee9824d47f fix: macos build error 2026-03-08 21:07:56 -05:00
LukeGus effe419d97 fix: macos build error 2026-03-08 20:21:20 -05:00
LukeGus bb5d104b52 fix: macos build error 2026-03-08 19:57:23 -05:00
LukeGus e4361b9bd1 fix: macos build error 2026-03-08 19:46:29 -05:00
LukeGus 1b8f6b54b4 fix: macos build error 2026-03-08 19:25:54 -05:00
LukeGus a0237dc155 fix: macos build error 2026-03-08 18:44:39 -05:00
165 changed files with 24124 additions and 7538 deletions
-1
View File
@@ -1 +0,0 @@
github: [LukeGus]
+3
View File
@@ -431,6 +431,7 @@ jobs:
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 }}"
@@ -488,6 +489,7 @@ jobs:
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
NODE_OPTIONS: --max-old-space-size=4096
run: |
if [ "${{ steps.check_certs.outputs.has_certs }}" != "true" ]; then
npm run build
@@ -956,6 +958,7 @@ jobs:
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 }}"
+2 -2
View File
@@ -1,6 +1,6 @@
cask "termix" do
version "1.11.1"
sha256 "0551e2bd7a3a030ffb967627d04320c1456259dd26b7a992fc02ea0fd9940315"
version "2.0.0"
sha256 "2e381ac504093bfa72d16ee20043640724394729b2ca0e6b26c9eac19aeb6d08"
url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg"
name "Termix"
+38 -24
View File
@@ -1,20 +1,7 @@
# Repo Stats
<p align="center">
<img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English ·
<a href="readme/README-CN.md"><img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文</a> ·
<a href="readme/README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="readme/README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="readme/README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="readme/README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="readme/README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="readme/README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="readme/README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="readme/README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="readme/README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="readme/README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="readme/README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="readme/README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
🇺🇸 English · <a href="readme/README-CN.md">🇨🇳 中文</a> · <a href="readme/README-JA.md">🇯🇵 日本語</a> · <a href="readme/README-KO.md">🇰🇷 한국어</a> · <a href="readme/README-FR.md">🇫🇷 Français</a> · <a href="readme/README-DE.md">🇩🇪 Deutsch</a> · <a href="readme/README-ES.md">🇪🇸 Español</a> · <a href="readme/README-PT.md">🇧🇷 Português</a> · <a href="readme/README-RU.md">🇷🇺 Русский</a> · <a href="readme/README-AR.md">🇸🇦 العربية</a> · <a href="readme/README-HI.md">🇮🇳 हिन्दी</a> · <a href="readme/README-TR.md">🇹🇷 Türkçe</a> · <a href="readme/README-VI.md">🇻🇳 Tiếng Việt</a> · <a href="readme/README-IT.md">🇮🇹 Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
@@ -34,9 +21,6 @@
<img alt="Termix Banner" src=./repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
If you would like, you can support the project here!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# Overview
<p align="center">
@@ -46,12 +30,13 @@ If you would like, you can support the project here!\
Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a multi-platform
solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal
access, SSH tunneling capabilities, remote file management, and many other tools. Termix is the perfect
access, remote desktop control (RDP, VNC, Telnet), SSH tunneling capabilities, remote SSH file management, and many other tools. Termix is the perfect
free and self-hosted alternative to Termius available for all platforms.
# Features
- **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) with a browser-like tab system. Includes support for customizing the terminal including common terminal themes, fonts, and other components.
- **Remote Desktop Access** - RDP, VNC, and Telnet support over the browser with complete customization and split screening
- **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring and support for -l or -r connections
- **Remote File Manager** - Manage files directly on remote servers with support for viewing and editing code, images, audio, and video. Upload, download, rename, delete, and move files seamlessly with sudo support.
- **Docker Management** - Start, stop, pause, remove containers. View container stats. Control container using docker exec terminal. It was not made to replace Portainer or Dockge but rather to simply manage your containers compared to creating them.
@@ -63,14 +48,14 @@ free and self-hosted alternative to Termius available for all platforms.
- **Database Encryption** - Backend stored as encrypted SQLite database files. View [docs](https://docs.termix.site/security) for more.
- **Data Export/Import** - Export and import SSH hosts, credentials, and file manager data
- **Automatic SSL Setup** - Built-in SSL certificate generation and management with HTTPS redirects
- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn. Choose between dark or light mode based UI. Use URL routes to open any connection in full-screen.
- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn. Choose between many different UI themes including light, dark, Dracula, etc. Use URL routes to open any connection in full-screen.
- **Languages** - Built-in support ~30 languages (managed by [Crowdin](https://docs.termix.site/translations))
- **Platform Support** - Available as a web app, desktop application (Windows, Linux, and macOS, can be run standalone without Termix backend), PWA, and dedicated mobile/tablet app for iOS and Android.
- **SSH Tools** - Create reusable command snippets that execute with a single click. Run one command simultaneously across multiple open terminals.
- **Command History** - Auto-complete and view previously ran SSH commands
- **Quick Connect** - Connect to a server without having to save the connection data
- **Command Palette** - Double tap left shift to quickly access SSH connections with your keyboard
- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, etc.
- **Network Graph** - Customize your Dashboard to visualize your homelab based off your SSH connections with status support
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
@@ -105,7 +90,7 @@ Supported Devices:
- APK
Visit the Termix [Docs](https://docs.termix.site/install) for more information on how to install Termix on all platforms. Otherwise, view
a sample Docker Compose file here:
a sample Docker Compose file here (you can omit guacd and the network if you don't plan on using remote desktop features):
```yaml
services:
@@ -119,10 +104,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:1.6.0
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# Sponsors
@@ -137,11 +139,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -153,7 +167,7 @@ channel, however, response times may be longer.
# Screenshots
[![YouTube](./repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](./repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="./repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
+33
View File
@@ -0,0 +1,33 @@
services:
termix-dev:
build:
context: ..
dockerfile: docker/Dockerfile
container_name: termix-dev
restart: unless-stopped
ports:
- "8081:8080"
volumes:
- termix-dev-data:/app/data
environment:
PORT: "8080"
NODE_ENV: development
depends_on:
- guacd-dev
networks:
- termix-dev-net
guacd-dev:
image: guacamole/guacd:1.6.0
container_name: guacd-dev
restart: unless-stopped
networks:
- termix-dev-net
volumes:
termix-dev-data:
driver: local
networks:
termix-dev-net:
driver: bridge
+32
View File
@@ -0,0 +1,32 @@
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
+111 -43
View File
@@ -38,7 +38,7 @@ http {
map $http_x_forwarded_port $proxy_x_forwarded_port {
default $http_x_forwarded_port;
'' $server_port;
'' '';
}
ssl_protocols TLSv1.2 TLSv1.3;
@@ -87,7 +87,7 @@ http {
location ~ ^/users/sessions(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -96,16 +96,18 @@ http {
location ~ ^/users(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
}
location ~ ^/version(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -114,7 +116,7 @@ http {
location ~ ^/releases(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -123,7 +125,7 @@ http {
location ~ ^/alerts(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -132,7 +134,7 @@ http {
location ~ ^/rbac(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -141,7 +143,7 @@ http {
location ~ ^/credentials(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -154,7 +156,7 @@ http {
location ~ ^/snippets(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -163,7 +165,7 @@ http {
location ~ ^/terminal(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -175,7 +177,7 @@ http {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -194,7 +196,7 @@ http {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -210,26 +212,26 @@ http {
location ~ ^/encryption(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/quick-connect {
location /host/quick-connect {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/ssh/opkssh-chooser(/.*)?$ {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args;
location ~ ^/host/opkssh-chooser(/.*)?$ {
proxy_pass http://127.0.0.1:30001/host/opkssh-chooser$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
@@ -243,8 +245,8 @@ http {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
location ~ ^/ssh/opkssh-callback(/.*)?$ {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args;
location ~ ^/host/opkssh-callback(/.*)?$ {
proxy_pass http://127.0.0.1:30001/host/opkssh-callback$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
@@ -258,10 +260,10 @@ http {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
location /ssh/ {
location /host/ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -292,52 +294,86 @@ http {
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
}
location /ssh/tunnel/ {
location ^~ /guacamole/websocket/ {
proxy_pass http://127.0.0.1:30008/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Host $http_host;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 10s;
proxy_buffering off;
proxy_request_buffering off;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
}
location ~ ^/guacamole(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /host/tunnel/ {
proxy_pass http://127.0.0.1:30003;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/file_manager/recent {
location /host/file_manager/recent {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/file_manager/pinned {
location /host/file_manager/pinned {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/file_manager/shortcuts {
location /host/file_manager/shortcuts {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/file_manager/sudo-password {
location /host/file_manager/sudo-password {
proxy_pass http://127.0.0.1:30004;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/file_manager/ssh/ {
location /ssh/file_manager/ {
client_max_body_size 5G;
client_body_timeout 300s;
@@ -345,7 +381,28 @@ http {
proxy_pass http://127.0.0.1:30004;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_request_buffering off;
proxy_buffering off;
}
location /host/file_manager/ssh/ {
client_max_body_size 5G;
client_body_timeout 300s;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
proxy_pass http://127.0.0.1:30004;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
@@ -361,7 +418,7 @@ http {
location ~ ^/network-topology(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -370,7 +427,7 @@ http {
location /health {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -379,7 +436,7 @@ http {
location ~ ^/status(/.*)?$ {
proxy_pass http://127.0.0.1:30005;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -388,7 +445,7 @@ http {
location ~ ^/metrics(/.*)?$ {
proxy_pass http://127.0.0.1:30005;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -398,10 +455,19 @@ http {
proxy_read_timeout 60s;
}
location ~ ^/global-settings(/.*)?$ {
proxy_pass http://127.0.0.1:30005;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/uptime(/.*)?$ {
proxy_pass http://127.0.0.1:30006;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -410,7 +476,7 @@ http {
location ~ ^/activity(/.*)?$ {
proxy_pass http://127.0.0.1:30006;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -419,24 +485,26 @@ http {
location ~ ^/dashboard/preferences(/.*)?$ {
proxy_pass http://127.0.0.1:30006;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /docker/console/ {
proxy_pass http://127.0.0.1:30008/;
proxy_pass http://127.0.0.1:30009/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Host $http_host;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
@@ -451,7 +519,7 @@ http {
location ~ ^/docker(/.*)?$ {
proxy_pass http://127.0.0.1:30007;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
+111 -43
View File
@@ -38,7 +38,7 @@ http {
map $http_x_forwarded_port $proxy_x_forwarded_port {
default $http_x_forwarded_port;
'' $server_port;
'' '';
}
ssl_protocols TLSv1.2 TLSv1.3;
@@ -76,7 +76,7 @@ http {
location ~ ^/users/sessions(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -85,16 +85,18 @@ http {
location ~ ^/users(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
}
location ~ ^/version(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -103,7 +105,7 @@ http {
location ~ ^/releases(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -112,7 +114,7 @@ http {
location ~ ^/alerts(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -121,7 +123,7 @@ http {
location ~ ^/rbac(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -130,7 +132,7 @@ http {
location ~ ^/credentials(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -143,7 +145,7 @@ http {
location ~ ^/snippets(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -152,7 +154,7 @@ http {
location ~ ^/terminal(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -164,7 +166,7 @@ http {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -183,7 +185,7 @@ http {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -199,26 +201,26 @@ http {
location ~ ^/encryption(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/quick-connect {
location /host/quick-connect {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/ssh/opkssh-chooser(/.*)?$ {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-chooser$1$is_args$args;
location ~ ^/host/opkssh-chooser(/.*)?$ {
proxy_pass http://127.0.0.1:30001/host/opkssh-chooser$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
@@ -232,8 +234,8 @@ http {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
location ~ ^/ssh/opkssh-callback(/.*)?$ {
proxy_pass http://127.0.0.1:30001/ssh/opkssh-callback$1$is_args$args;
location ~ ^/host/opkssh-callback(/.*)?$ {
proxy_pass http://127.0.0.1:30001/host/opkssh-callback$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
@@ -247,10 +249,10 @@ http {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
location /ssh/ {
location /host/ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -281,52 +283,86 @@ http {
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
}
location /ssh/tunnel/ {
location ^~ /guacamole/websocket/ {
proxy_pass http://127.0.0.1:30008/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Host $http_host;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 10s;
proxy_buffering off;
proxy_request_buffering off;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
}
location ~ ^/guacamole(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /host/tunnel/ {
proxy_pass http://127.0.0.1:30003;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/file_manager/recent {
location /host/file_manager/recent {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/file_manager/pinned {
location /host/file_manager/pinned {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/file_manager/shortcuts {
location /host/file_manager/shortcuts {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/file_manager/sudo-password {
location /host/file_manager/sudo-password {
proxy_pass http://127.0.0.1:30004;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/file_manager/ssh/ {
location /ssh/file_manager/ {
client_max_body_size 5G;
client_body_timeout 300s;
@@ -334,7 +370,28 @@ http {
proxy_pass http://127.0.0.1:30004;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_request_buffering off;
proxy_buffering off;
}
location /host/file_manager/ssh/ {
client_max_body_size 5G;
client_body_timeout 300s;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
proxy_pass http://127.0.0.1:30004;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
@@ -350,7 +407,7 @@ http {
location ~ ^/network-topology(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -359,7 +416,7 @@ http {
location /health {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -368,7 +425,7 @@ http {
location ~ ^/status(/.*)?$ {
proxy_pass http://127.0.0.1:30005;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -377,7 +434,7 @@ http {
location ~ ^/metrics(/.*)?$ {
proxy_pass http://127.0.0.1:30005;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -387,10 +444,19 @@ http {
proxy_read_timeout 60s;
}
location ~ ^/global-settings(/.*)?$ {
proxy_pass http://127.0.0.1:30005;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/uptime(/.*)?$ {
proxy_pass http://127.0.0.1:30006;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -399,7 +465,7 @@ http {
location ~ ^/activity(/.*)?$ {
proxy_pass http://127.0.0.1:30006;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
@@ -408,24 +474,26 @@ http {
location ~ ^/dashboard/preferences(/.*)?$ {
proxy_pass http://127.0.0.1:30006;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /docker/console/ {
proxy_pass http://127.0.0.1:30008/;
proxy_pass http://127.0.0.1:30009/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Host $http_host;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
@@ -440,7 +508,7 @@ http {
location ~ ^/docker(/.*)?$ {
proxy_pass http://127.0.0.1:30007;
proxy_http_version 1.1;
proxy_set_header Host $host;
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;
+12 -3
View File
@@ -18,14 +18,22 @@
"!node_modules/@napi-rs/canvas*/**/*",
"!node_modules/@rollup/rollup-darwin-*/**/*",
"!node_modules/@rollup/rollup-linux-*/**/*",
"!node_modules/@rollup/rollup-win32-*/**/*"
"!node_modules/@rollup/rollup-win32-*/**/*",
"!dist/icon-mac.png",
"!public/icon-mac.png",
"!dist/icon.ico",
"!public/icon.ico",
"!dist/icon.icns",
"!public/icon.icns",
"!dist/icons/**/*",
"!public/icons/**/*"
],
"extraMetadata": {
"main": "electron/main.cjs"
},
"buildDependenciesFromSource": false,
"nodeGypRebuild": false,
"npmRebuild": false,
"npmRebuild": true,
"win": {
"target": [
{
@@ -108,7 +116,8 @@
"type": "distribution",
"minimumSystemVersion": "10.15",
"mergeASARs": false,
"singleArchFiles": "node_modules/@tailwindcss/oxide-*/**/*"
"singleArchFiles": "**/*.node",
"x64ArchFiles": "**/*.node"
},
"dmg": {
"artifactName": "termix_macos_${arch}_dmg.${ext}",
+43 -9
View File
@@ -99,8 +99,6 @@ function getBackendEntryPath() {
if (isDev) {
return path.join(appRoot, "dist", "backend", "backend", "starter.js");
}
// In production, asar is disabled (asar: false in electron-builder.json)
// so backend is directly in appRoot/dist
return path.join(appRoot, "dist", "backend", "backend", "starter.js");
}
@@ -133,13 +131,11 @@ function startBackendServer() {
logToFile("Data directory:", dataDir);
logToFile("Backend cwd:", appRoot);
// Verify all required paths exist
logToFile("Checking paths...");
logToFile(" entryPath exists:", fs.existsSync(entryPath));
logToFile(" dataDir exists:", fs.existsSync(dataDir));
logToFile(" appRoot exists:", fs.existsSync(appRoot));
// List contents of dist directory
const distPath = path.join(appRoot, "dist");
if (fs.existsSync(distPath)) {
logToFile(" dist directory contents:", fs.readdirSync(distPath));
@@ -214,7 +210,6 @@ function stopBackendServer() {
console.log("Stopping embedded backend server...");
// Use IPC for graceful shutdown (SIGTERM doesn't work on Windows)
try {
backendProcess.send({ type: "shutdown" });
} catch {
@@ -256,7 +251,6 @@ function createTray() {
let trayIcon;
if (process.platform === "darwin") {
// macOS: use 16x16 Template image for menu bar
const iconPath = path.join(appRoot, "public", "icons", "16x16.png");
trayIcon = nativeImage.createFromPath(iconPath);
trayIcon.setTemplateImage(true);
@@ -304,7 +298,6 @@ function createTray() {
console.log("System tray created successfully");
} catch (err) {
console.error("Failed to create system tray:", err);
// Tray is non-critical; app still works without it
}
}
@@ -670,6 +663,49 @@ ipcMain.handle("set-setting", (event, key, value) => {
}
});
ipcMain.handle("get-iframe-jwt", async () => {
try {
if (!mainWindow) return null;
const frames = mainWindow.webContents.mainFrame.framesInSubtree;
logToFile(`[get-iframe-jwt] scanning ${frames.length} frames`);
for (const frame of frames) {
if (frame === mainWindow.webContents.mainFrame) continue;
try {
const token = await frame.executeJavaScript(
`(function() {
try {
const t = localStorage.getItem('jwt') || sessionStorage.getItem('jwt');
return t || null;
} catch(e) { return null; }
})()`,
);
logToFile(
`[get-iframe-jwt] frame url=${frame.url} token found=${!!token} length=${token?.length}`,
);
if (token && token.length > 20) return token;
} catch (err) {
logToFile(`[get-iframe-jwt] frame exec error:`, err.message);
}
}
return null;
} catch (error) {
logToFile("[get-iframe-jwt] error:", error.message);
return null;
}
});
ipcMain.handle("get-session-cookie", async (_event, name) => {
try {
const ses = mainWindow?.webContents?.session;
if (!ses) return null;
const cookies = await ses.cookies.get({ name });
return cookies.length > 0 ? cookies[0].value : null;
} catch (error) {
console.error("Failed to get session cookie:", error);
return null;
}
});
ipcMain.handle("clear-session-cookies", async () => {
try {
const ses = mainWindow?.webContents?.session;
@@ -871,7 +907,6 @@ app.whenReady().then(async () => {
);
createMenu();
// Start embedded backend server (skip in dev mode, backend runs separately via npm run dev:backend)
if (!isDev) {
const result = await startBackendServer();
logToFile("startBackendServer result:", result);
@@ -887,7 +922,6 @@ app.whenReady().then(async () => {
});
app.on("window-all-closed", () => {
// If tray exists, keep backend alive; otherwise quit normally
if (!tray || tray.isDestroyed()) {
app.quit();
}
-1
View File
@@ -4,7 +4,6 @@ Branch=stable
Title=Termix - SSH Server Management Platform
IsRuntime=false
Url=https://github.com/Termix-SSH/Termix/releases/download/VERSION_PLACEHOLDER/termix_linux_flatpak.flatpak
GPGKey=
RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo
Comment=Web-based server management platform with SSH terminal, tunneling, and file editing
Description=Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides SSH terminal access, tunneling capabilities, and remote file management.
+1906 -2024
View File
File diff suppressed because it is too large Load Diff
+58 -56
View File
@@ -1,7 +1,7 @@
{
"name": "termix",
"private": true,
"version": "1.11.2",
"version": "2.1.0",
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
"author": "Karmaa",
"main": "electron/main.cjs",
@@ -31,11 +31,42 @@
"build:mac": "npm run build && npm run electron:rebuild && electron-builder --mac --universal"
},
"dependencies": {
"axios": "^1.10.0",
"bcryptjs": "^3.0.2",
"better-sqlite3": "^12.2.0",
"body-parser": "^1.20.2",
"chalk": "^4.1.2",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^17.2.0",
"drizzle-orm": "^0.44.3",
"express": "^5.1.0",
"guacamole-lite": "^1.2.0",
"https-proxy-agent": "^7.0.6",
"js-yaml": "^4.1.1",
"jose": "^5.2.3",
"jsonwebtoken": "^9.0.2",
"jszip": "^3.10.1",
"multer": "^2.0.2",
"nanoid": "^5.1.5",
"node-fetch": "^3.3.2",
"qrcode": "^1.5.4",
"socks": "^2.8.7",
"speakeasy": "^2.0.0",
"ssh2": "^1.16.0",
"ws": "^8.18.3"
},
"devDependencies": {
"@codemirror/autocomplete": "^6.18.7",
"@codemirror/commands": "^6.3.3",
"@codemirror/search": "^6.5.11",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.23.1",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@electron/notarize": "^2.5.0",
"@electron/rebuild": "^3.7.2",
"@eslint/js": "^9.34.0",
"@hookform/resolvers": "^5.1.1",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.11",
@@ -56,48 +87,52 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.14",
"@types/bcryptjs": "^2.4.6",
"@types/better-sqlite3": "^7.6.13",
"@types/cookie-parser": "^1.4.9",
"@types/cors": "^2.8.19",
"@types/cytoscape": "^3.21.9",
"@types/express": "^5.0.3",
"@types/guacamole-common-js": "^1.5.5",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.10",
"@types/jszip": "^3.4.0",
"@types/multer": "^2.0.0",
"@types/node": "^24.3.0",
"@types/qrcode": "^1.5.5",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-grid-layout": "^1.3.6",
"@types/speakeasy": "^2.0.10",
"@types/ssh2": "^1.15.5",
"@types/ws": "^8.18.1",
"@uiw/codemirror-extensions-langs": "^4.24.1",
"@uiw/codemirror-theme-github": "^4.25.4",
"@uiw/react-codemirror": "^4.24.1",
"@vitejs/plugin-react": "^4.3.4",
"@xterm/addon-clipboard": "^0.2.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"axios": "^1.10.0",
"bcryptjs": "^3.0.2",
"better-sqlite3": "^12.2.0",
"body-parser": "^1.20.2",
"chalk": "^4.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"concurrently": "^9.2.1",
"cytoscape": "^3.33.1",
"dotenv": "^17.2.0",
"drizzle-orm": "^0.44.3",
"express": "^5.1.0",
"https-proxy-agent": "^7.0.6",
"i18n-auto-translation": "^2.2.3",
"electron": "^38.0.0",
"electron-builder": "^26.0.12",
"eslint": "^9.34.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"guacamole-common-js": "^1.5.0",
"husky": "^9.1.7",
"i18next": "^25.4.2",
"i18next-browser-languagedetector": "^8.2.0",
"jose": "^5.2.3",
"jsonwebtoken": "^9.0.2",
"jszip": "^3.10.1",
"lint-staged": "^16.2.3",
"lucide-react": "^0.525.0",
"multer": "^2.0.2",
"nanoid": "^5.1.5",
"next-themes": "^0.4.6",
"node-fetch": "^3.3.2",
"qrcode": "^1.5.4",
"prettier": "3.6.2",
"react": "^19.1.0",
"react-cytoscapejs": "^2.0.0",
"react-dom": "^19.1.0",
@@ -109,53 +144,20 @@
"react-markdown": "^10.1.0",
"react-pdf": "^10.1.0",
"react-photo-view": "^1.2.7",
"react-player": "^3.3.3",
"react-resizable-panels": "^3.0.3",
"react-simple-keyboard": "^3.8.120",
"react-syntax-highlighter": "^15.6.6",
"react-xtermjs": "^1.0.10",
"recharts": "^3.2.1",
"remark-gfm": "^4.0.1",
"socks": "^2.8.7",
"sonner": "^2.0.7",
"speakeasy": "^2.0.0",
"ssh2": "^1.16.0",
"swagger-jsdoc": "^6.2.8",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.14",
"wait-on": "^9.0.1",
"ws": "^8.18.3",
"zod": "^4.0.5"
},
"devDependencies": {
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@electron/notarize": "^2.5.0",
"@electron/rebuild": "^3.7.2",
"@eslint/js": "^9.34.0",
"@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.3.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/ssh2": "^1.15.5",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.3.4",
"concurrently": "^9.2.1",
"electron": "^38.0.0",
"electron-builder": "^26.0.12",
"eslint": "^9.34.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.3",
"prettier": "3.6.2",
"swagger-jsdoc": "^6.2.8",
"typescript": "~5.9.2",
"typescript-eslint": "^8.40.0",
"vite": "^7.1.5"
"vite": "^7.1.5",
"zod": "^4.0.5"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
+2 -2
View File
@@ -55,8 +55,8 @@ self.addEventListener("fetch", (event) => {
}
if (
url.pathname.startsWith("/ssh/opkssh-chooser/") ||
url.pathname.startsWith("/ssh/opkssh-callback/")
url.pathname.startsWith("/host/opkssh-chooser/") ||
url.pathname.startsWith("/host/opkssh-callback/")
) {
return;
}
+57 -27
View File
@@ -44,32 +44,33 @@
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix هي منصة مفتوحة المصدر ومجانية للأبد وذاتية الاستضافة لإدارة الخوادم بشكل شامل. توفر حلاً متعدد المنصات لإدارة خوادمك وبنيتك التحتية من خلال واجهة واحدة وسهلة الاستخدام. يوفر Termix الوصول إلى طرفية SSH، وقدرات إنشاء أنفاق SSH، وإدارة الملفات عن بُعد، والعديد من الأدوات الأخرى. يُعد Termix البديل المثالي المجاني وذاتي الاستضافة لـ Termius المتاح لجميع المنصات.
Termix هي منصة مفتوحة المصدر ومجانية للأبد وذاتية الاستضافة لإدارة الخوادم بشكل شامل. توفر حلاً متعدد المنصات لإدارة خوادمك وبنيتك التحتية من خلال واجهة واحدة وسهلة الاستخدام. يوفر Termix الوصول إلى طرفية SSH، والتحكم في سطح المكتب البعيد (RDP، VNC، Telnet)، وقدرات إنشاء أنفاق SSH، وإدارة ملفات SSH عن بُعد، والعديد من الأدوات الأخرى. يُعد Termix البديل المثالي المجاني وذاتي الاستضافة لـ Termius المتاح لجميع المنصات.
# الميزات
- **الوصول إلى طرفية SSH** - طرفية كاملة الميزات مع دعم تقسيم الشاشة (حتى 4 لوحات) مع نظام علامات تبويب شبيه بالمتصفح. يتضمن دعم تخصيص الطرفية بما في ذلك السمات الشائعة والخطوط والمكونات الأخرى
- **إدارة أنفاق SSH** - إنشاء وإدارة أنفاق SSH مع إعادة الاتصال التلقائي ومراقبة الحالة ودعم اتصالات -l أو -r
- **مدير الملفات عن بُعد** - إدارة الملفات مباشرة على الخوادم البعيدة مع دعم عرض وتحرير الكود والصور والصوت والفيديو. رفع وتنزيل وإعادة تسمية وحذف ونقل الملفات بسلاسة مع دعم sudo
- **إدارة Docker** - تشغيل وإيقاف وتعليق وحذف الحاويات. عرض إحصائيات الحاويات. التحكم في الحاوية باستخدام طرفية docker exec. لم يُصمم ليحل محل Portainer أو Dockge بل لإدارة حاوياتك ببساطة مقارنة بإنشائها
- **مدير مضيفات SSH** - حفظ وتنظيم وإدارة اتصالات SSH الخاصة بك باستخدام العلامات والمجلدات، وحفظ بيانات تسجيل الدخول القابلة لإعادة الاستخدام بسهولة مع إمكانية أتمتة نشر مفاتيح SSH
- **إحصائيات الخادم** - عرض استخدام المعالج والذاكرة والقرص إلى جانب الشبكة ووقت التشغيل ومعلومات النظام وجدار الحماية ومراقب المنافذ على معظم الخوادم المبنية على Linux
- **لوحة التحكم** - عرض معلومات الخادم بنظرة واحدة على لوحة التحكم
- **RBAC** - إنشاء الأدوار ومشاركة المضيفات عبر المستخدمين/الأدوار
- **مصادقة المستخدمين** - إدارة آمنة للمستخدمين مع ضوابط إدارية ودعم OIDC و 2FA (TOTP). عرض جلسات المستخدمين النشطة عبر جميع المنصات وإلغاء الصلاحيات. ربط حسابات OIDC/المحلية معاً
- **تشفير قاعدة البيانات** - يُخزَّن الخادم الخلفي كملفات قاعدة بيانات SQLite مشفرة. اطلع على [الوثائق](https://docs.termix.site/security) لمزيد من المعلومات
- **تصدير/استيراد البيانات** - تصدير واستيراد مضيفات SSH وبيانات الاعتماد وبيانات مدير الملفات
- **إعداد SSL تلقائي** - إنشاء وإدارة شهادات SSL مدمجة مع إعادة التوجيه إلى HTTPS
- **واجهة مستخدم حديثة** - واجهة نظيفة متوافقة مع سطح المكتب والهاتف المحمول مبنية بـ React و Tailwind CSS و Shadcn. الاختيار بين الوضع الداكن أو الفاتح. استخدام مسارات URL لفتح أي اتصال في وضع ملء الشاشة
- **اللغات** - دعم مدمج لحوالي 30 لغة (تُدار بواسطة [Crowdin](https://docs.termix.site/translations))
- **دعم المنصات** - متاح كتطبيق ويب، وتطبيق سطح مكتب (Windows و Linux و macOS)، و PWA، وتطبيق مخصص للهاتف المحمول/الجهاز اللوحي لـ iOS و Android
- **أدوات SSH** - إنشاء مقتطفات أوامر قابلة لإعادة الاستخدام تُنفَّذ بنقرة واحدة. تشغيل أمر واحد في وقت واحد عبر عدة طرفيات مفتوحة
- **سجل الأوامر** - الإكمال التلقائي وعرض أوامر SSH التي تم تنفيذها سابقاً
- **الاتصال السريع** - الاتصال بخادم دون الحاجة إلى حفظ بيانات الاتصال
- **لوحة الأوامر** - اضغط مرتين على Shift الأيسر للوصول السريع إلى اتصالات SSH باستخدام لوحة المفاتيح
- **ميزات SSH الغنية** - دعم مضيفات القفز، Warpgate، الاتصالات المبنية على TOTP، SOCKS5، التحقق من مفتاح المضيف، الملء التلقائي لكلمة المرور، [OPKSSH](https://github.com/openpubkey/opkssh)، وغيرها
- **الرسم البياني للشبكة** - تخصيص لوحة التحكم لتصور مختبرك المنزلي بناءً على اتصالات SSH مع دعم الحالة
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **الوصول إلى طرفية SSH** - طرفية كاملة الميزات مع دعم تقسيم الشاشة (حتى 4 لوحات) مع نظام علامات تبويب شبيه بالمتصفح. يتضمن دعم تخصيص الطرفية بما في ذلك سمات الطرفية الشائعة والخطوط والمكونات الأخرى.
- **الوصول إلى سطح المكتب البعيد** - دعم RDP و VNC و Telnet عبر المتصفح مع تخصيص كامل وتقسيم الشاشة.
- **إدارة أنفاق SSH** - إنشاء وإدارة أنفاق SSH مع إعادة الاتصال التلقائي ومراقبة الحالة ودعم اتصالات -l أو -r.
- **مدير الملفات عن بُعد** - إدارة الملفات مباشرة على الخوادم البعيدة مع دعم عرض وتحرير الكود والصور والصوت والفيديو. رفع وتنزيل وإعادة تسمية وحذف ونقل الملفات بسلاسة مع دعم sudo.
- **إدارة Docker** - تشغيل وإيقاف وتعليق وحذف الحاويات. عرض إحصائيات الحاويات. التحكم في الحاوية باستخدام طرفية docker exec. لم يُصمم ليحل محل Portainer أو Dockge بل لإدارة حاوياتك ببساطة مقارنة بإنشائها.
- **مدير مضيفات SSH** - حفظ وتنظيم وإدارة اتصالات SSH الخاصة بك باستخدام العلامات والمجلدات، وحفظ بيانات تسجيل الدخول القابلة لإعادة الاستخدام بسهولة مع إمكانية أتمتة نشر مفاتيح SSH.
- **إحصائيات الخادم** - عرض استخدام المعالج والذاكرة والقرص إلى جانب الشبكة ووقت التشغيل ومعلومات النظام وجدار الحماية ومراقب المنافذ على معظم الخوادم المبنية على Linux.
- **لوحة التحكم** - عرض معلومات الخادم بنظرة واحدة على لوحة التحكم.
- **RBAC** - إنشاء الأدوار ومشاركة المضيفات عبر المستخدمين/الأدوار.
- **مصادقة المستخدمين** - إدارة آمنة للمستخدمين مع ضوابط إدارية ودعم OIDC (مع التحكم في الوصول) و 2FA (TOTP). عرض جلسات المستخدمين النشطة عبر جميع المنصات وإلغاء الصلاحيات. ربط حسابات OIDC/المحلية معاً.
- **تشفير قاعدة البيانات** - يُخزَّن الخادم الخلفي كملفات قاعدة بيانات SQLite مشفرة. اطلع على [الوثائق](https://docs.termix.site/security) لمزيد من المعلومات.
- **تصدير/استيراد البيانات** - تصدير واستيراد مضيفات SSH وبيانات الاعتماد وبيانات مدير الملفات.
- **إعداد SSL تلقائي** - إنشاء وإدارة شهادات SSL مدمجة مع إعادة التوجيه إلى HTTPS.
- **واجهة مستخدم حديثة** - واجهة نظيفة متوافقة مع سطح المكتب والهاتف المحمول مبنية بـ React و Tailwind CSS و Shadcn. الاختيار بين العديد من سمات واجهة المستخدم بما في ذلك الفاتح والداكن و Dracula وغيرها. استخدام مسارات URL لفتح أي اتصال في وضع ملء الشاشة.
- **اللغات** - دعم مدمج لحوالي 30 لغة (تُدار بواسطة [Crowdin](https://docs.termix.site/translations)).
- **دعم المنصات** - متاح كتطبيق ويب، وتطبيق سطح مكتب (Windows و Linux و macOS، يمكن تشغيله بشكل مستقل بدون خادم Termix الخلفي)، و PWA، وتطبيق مخصص للهاتف المحمول/الجهاز اللوحي لـ iOS و Android.
- **أدوات SSH** - إنشاء مقتطفات أوامر قابلة لإعادة الاستخدام تُنفَّذ بنقرة واحدة. تشغيل أمر واحد في وقت واحد عبر عدة طرفيات مفتوحة.
- **سجل الأوامر** - الإكمال التلقائي وعرض أوامر SSH التي تم تنفيذها سابقاً.
- **الاتصال السريع** - الاتصال بخادم دون الحاجة إلى حفظ بيانات الاتصال.
- **لوحة الأوامر** - اضغط مرتين على Shift الأيسر للوصول السريع إلى اتصالات SSH باستخدام لوحة المفاتيح.
- **ميزات SSH الغنية** - دعم مضيفات القفز، Warpgate، الاتصالات المبنية على TOTP، SOCKS5، التحقق من مفتاح المضيف، الملء التلقائي لكلمة المرور، [OPKSSH](https://github.com/openpubkey/opkssh)، tmux، port knocking، إلخ.
- **الرسم البياني للشبكة** - تخصيص لوحة التحكم لتصور مختبرك المنزلي بناءً على اتصالات SSH مع دعم الحالة.
- **علامات التبويب الدائمة** - تبقى جلسات SSH وعلامات التبويب مفتوحة عبر الأجهزة/التحديثات إذا تم تفعيلها في ملف تعريف المستخدم.
# الميزات المخططة
@@ -101,7 +102,7 @@ Termix هي منصة مفتوحة المصدر ومجانية للأبد وذا
- Google Play Store
- APK
قم بزيارة [وثائق](https://docs.termix.site/install) Termix للحصول على مزيد من المعلومات حول كيفية تثبيت Termix على جميع المنصات. بخلاف ذلك، يمكنك الاطلاع على نموذج ملف Docker Compose هنا:
قم بزيارة [وثائق](https://docs.termix.site/install) Termix للحصول على مزيد من المعلومات حول كيفية تثبيت Termix على جميع المنصات. بخلاف ذلك، يمكنك الاطلاع على نموذج ملف Docker Compose هنا (يمكنك حذف guacd والشبكة إذا كنت لا تخطط لاستخدام ميزات سطح المكتب البعيد):
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# الرعاة
@@ -133,11 +151,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -148,7 +178,7 @@ volumes:
# لقطات الشاشة
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
+123 -95
View File
@@ -1,114 +1,114 @@
# 仓库统计
<p align="IPAenter">
<a href="../README.md"><img srIPA="https://flagIPAdn.IPAom/us.svg" alt="English" width="24" height="16"> English</a> ·
<img srIPA="https://flagIPAdn.IPAom/IPAn.svg" alt="中文" width="24" height="16"> 中文 ·
<a href="README-JA.md"><img srIPA="https://flagIPAdn.IPAom/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img srIPA="https://flagIPAdn.IPAom/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img srIPA="https://flagIPAdn.IPAom/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img srIPA="https://flagIPAdn.IPAom/de.svg" alt="DeutsIPAh" width="24" height="16"> DeutsIPAh</a> ·
<a href="README-ES.md"><img srIPA="https://flagIPAdn.IPAom/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img srIPA="https://flagIPAdn.IPAom/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img srIPA="https://flagIPAdn.IPAom/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img srIPA="https://flagIPAdn.IPAom/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img srIPA="https://flagIPAdn.IPAom/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img srIPA="https://flagIPAdn.IPAom/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img srIPA="https://flagIPAdn.IPAom/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img srIPA="https://flagIPAdn.IPAom/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
<p align="center">
<a href="../README.md"><img src="https://flagcdn.com/us.svg" alt="English" width="24" height="16"> English</a> ·
<img src="https://flagcdn.com/cn.svg" alt="中文" width="24" height="16"> 中文 ·
<a href="README-JA.md"><img src="https://flagcdn.com/jp.svg" alt="日本語" width="24" height="16"> 日本語</a> ·
<a href="README-KO.md"><img src="https://flagcdn.com/kr.svg" alt="한국어" width="24" height="16"> 한국어</a> ·
<a href="README-FR.md"><img src="https://flagcdn.com/fr.svg" alt="Français" width="24" height="16"> Français</a> ·
<a href="README-DE.md"><img src="https://flagcdn.com/de.svg" alt="Deutsch" width="24" height="16"> Deutsch</a> ·
<a href="README-ES.md"><img src="https://flagcdn.com/es.svg" alt="Español" width="24" height="16"> Español</a> ·
<a href="README-PT.md"><img src="https://flagcdn.com/br.svg" alt="Português" width="24" height="16"> Português</a> ·
<a href="README-RU.md"><img src="https://flagcdn.com/ru.svg" alt="Русский" width="24" height="16"> Русский</a> ·
<a href="README-AR.md"><img src="https://flagcdn.com/sa.svg" alt="العربية" width="24" height="16"> العربية</a> ·
<a href="README-HI.md"><img src="https://flagcdn.com/in.svg" alt="हिन्दी" width="24" height="16"> हिन्दी</a> ·
<a href="README-TR.md"><img src="https://flagcdn.com/tr.svg" alt="Türkçe" width="24" height="16"> Türkçe</a> ·
<a href="README-VI.md"><img src="https://flagcdn.com/vn.svg" alt="Tiếng Việt" width="24" height="16"> Tiếng Việt</a> ·
<a href="README-IT.md"><img src="https://flagcdn.com/it.svg" alt="Italiano" width="24" height="16"> Italiano</a>
</p>
![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars)
![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks)
![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release)
<a href="https://disIPAord.gg/jVQGdvHDrf"><img alt="DisIPAord" srIPA="https://img.shields.io/disIPAord/1347374268253470720"></a>
<a href="https://discord.gg/jVQGdvHDrf"><img alt="Discord" src="https://img.shields.io/discord/1347374268253470720"></a>
<p align="IPAenter">
<img srIPA="../repo-images/RepoOfTheDay.png" alt="Repo of the Day AIPAhievement" style="width: 300px; height: auto;">
<p align="center">
<img src="../repo-images/RepoOfTheDay.png" alt="Repo of the Day Achievement" style="width: 300px; height: auto;">
<br>
<small style="IPAolor: #666;">2025年9月1日获得</small>
<small style="color: #666;">获得于 2025年9月1日</small>
</p>
<br />
<p align="IPAenter">
<a href="https://github.IPAom/Termix-SSH/Termix">
<img alt="Termix Banner" srIPA=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../repo-images/HeaderImage.png style="width: auto; height: auto;"> </a>
</p>
如果你愿意,可以在这里支持这个项目!\
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoIPAolor=white)](https://github.IPAom/sponsors/LukeGus)
[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus)
# 概览
<p align="IPAenter">
<a href="https://github.IPAom/Termix-SSH/Termix">
<img alt="Termix Banner" srIPA=../publiIPA/iIPAon.svg style="width: 250px; height: 250px;"> </a>
<p align="center">
<a href="https://github.com/Termix-SSH/Termix">
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix
提供 SSH 终端访问、SSH 隧道功能以及远程文件管理,还会陆续添加更多工具。Termix 是适用于所有平台的完美免费自托管 Termius 替代品。
Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个多平台解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix 提供 SSH 终端访问、远程桌面控制(RDP、VNC、Telnet)、SSH 隧道功能、远程 SSH 文件管理以及许多其他工具。Termix 是适用于所有平台的完美免费自托管 Termius 替代品。
# 功能
- **SSH 终端访问** - 功能齐全的终端,具有分屏支持(最多 4 个面板)类似浏览器的选项卡系统。包括对自定义终端的支持,包括常见终端主题、字体和其他组件
- **SSH 隧道管理** - 创建和管理 SSH 隧道,具有自动重新连接和健康监控功能
- **远程文件管理** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。无缝上传、下载、重命名、删除和移动文件
- **DoIPAker 管理** - 启动、停止、暂停、删除容器。查看容器统计信息。使用 doIPAker exeIPA 终端控制容器。它不是用来替代 Portainer 或 DoIPAkge,而是用于简单管理你的容器而不是创建它们
- **SSH 主机管理** - 保存、组织和管理您的 SSH 连接,支持标签和文件夹,并轻松保存可重用的登录信息,同时能够自动部署 SSH 密钥
- **服务器统计** - 在任何 SSH 服务器上查看 IPAPU、内存和磁盘使用情况以及网络、正常运行时间和系统信息
- **仪表板** - 在仪表板上一目了然地查看服务器信息
- **RBAIPA** - 创建角色并在用户/角色之间共享主机
- **用户认证** - 安全的用户管理,具有管理员控制以及 OIDIPA 和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDIPA/本地帐户链接在一起
- **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://doIPAs.termix.site/seIPAurity)了解更多信息
- **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据
- **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向
- **现代用户界面** - 使用 ReaIPAt、Tailwind IPASS 和 ShadIPAn 构建的简洁的桌面/移动设备友好界面。可选择基于深色或浅色模式的用户界面
- **语言** - 内置支持约 30 种语言(由 [IPArowdin](https://doIPAs.termix.site/translations) 管理)
- **平台支持** - 可作为 Web 应用程序、桌面应用程序(Windows、Linux 和 maIPAOS)、PWA 以及适用于 iOS 和 Android 的专用移动/平板电脑应用程序
- **SSH 工具** - 创建可重用的命令片段,单击即可执行。在多个打开的终端上同时运行一个命令
- **命令历史** - 自动完成并查看以前运行的 SSH 命令
- **快速连接** - 无需保存连接数据即可连接到服务器
- **命令面板** - 双击左 Shift 键可快速使用键盘访问 SSH 连接
- **SSH 功能丰富** - 支持跳板机、Warpgate、基于 TOTP 的连接、SOIPAKS5、主机密钥验证、密码自动填充、[OPKSSH](https://github.IPAom/openpubkey/opkssh)等
- **网络图** - 自定义您的仪表板,根据您的 SSH 连接可视化您的家庭实验室,支持状态显示
- **持久标签页** - 如果在用户配置文件中启用,SSH 会话和标签页在设备/刷新后保持打开状态
- **SSH 终端访问** - 功能齐全的终端,支持分屏(最多 4 个面板),并配有类似浏览器的标签系统。包括对自定义终端的支持,如常用的终端主题、字体和其他组件
- **远程桌面访问** - 通过浏览器支持 RDP、VNC 和 Telnet,具有完整的自定义和分屏功能
- **SSH 隧道管理** - 创建和管理具有自动重连和健康监测功能的 SSH 隧道,支持 -l 或 -r 连接。
- **远程文件管理** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。支持通过 sudo 无缝上传、下载、重命名、删除和移动文件
- **Docker 管理** - 启动、停止、暂停、移除容器。查看容器统计信息。通过 docker exec 终端控制容器。它的初衷不是取代 Portainer 或 Dockge,而是为了比直接创建容器更简单地管理它们。
- **SSH 主机管理器** - 通过标签和文件夹保存、组织和管理您的 SSH 连接,轻松保存可重用的登录信息,并能自动化部署 SSH 密钥。
- **服务器统计** - 在大多数基于 Linux 的服务器上查看 CPU、内存、磁盘使用情况以及网络、运行时间、系统信息、防火墙和端口监控。
- **仪表板** - 在仪表板上一目了然地查看服务器信息。
- **RBAC** - 创建角色并在用户/角色之间共享主机
- **用户认证** - 安全的用户管理,具有管理员控制、OIDC(带访问控制)和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDC/本地账户链接在一起
- **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://docs.termix.site/security)了解更多。
- **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据。
- **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向
- **现代 UI** - 使用 React、Tailwind CSS 和 Shadcn 构建的整洁的桌面/移动友好界面。有多种 UI 主题可选,包括浅色、深色、Dracula 等。使用 URL 路由全屏打开任何连接。
- **语言** - 内置支持约 30 种语言(由 [Crowdin](https://docs.termix.site/translations) 管理)
- **平台支持** - 提供 Web 应用、桌面应用(Windows、Linux 和 macOS,可脱离 Termix 后端独立运行)、PWA 以及 iOS 和 Android 专用移动/平板应用
- **SSH 工具** - 创建可重用的命令片段,只需点击一下即可执行。在多个打开的终端中同时运行一个命令
- **命令历史** - 自动完成并查看之前运行过的 SSH 命令。
- **快速连接** - 无需保存连接数据即可连接到服务器。
- **命令面板** - 双击左 Shift 键即可通过键盘快速访问 SSH 连接
- **丰富的功能** - 支持跳转主机、Warpgate、基于 TOTP 的连接、SOCKS5、主机密钥验证、密码自动填充、[OPKSSH](https://github.com/openpubkey/opkssh)、tmux、端口敲击等。
- **网络图** - 自定义您的仪表板,根据您的 SSH 连接可视化您的家庭实验室,并支持状态监测。
- **持久标签页** - 如果在用户个人资料中启用,SSH 会话和标签页将在设备/刷新后保持打开状态。
# 计划功能
查看 [项目](https://github.IPAom/orgs/Termix-SSH/projeIPAts/2) 了解所有计划功能。如果想贡献代码,请参阅 [贡献指南](https://github.IPAom/Termix-SSH/Termix/blob/main/IPAONTRIBUTING.md)。
查看 [Projects](https://github.com/orgs/Termix-SSH/projects/2) 了解所有计划功能。如果想贡献代码,请参阅 [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)。
# 安装
支持的设备:
- 网站(任何平台上的任何现代浏览器,如 IPAhrome、Safari 和 Firefox)(包括 PWA 支持)
- Windowsx64/ia32
- 网站(任何平台上的任何现代浏览器,如 Chrome、Safari 和 Firefox)(包括 PWA 支持)
- Windows (x64/ia32)
- 便携版
- MSI 安装程序
- IPAhoIPAolatey 软件包管理器
- Linuxx64/ia32
- Chocolatey 软件包管理器
- Linux (x64/ia32)
- 便携版
- AUR
- AppImage
- Deb
- Flatpak
- maIPAOSx64/ia32 on v12.0+
- macOS (x64/ia32, v12.0+)
- Apple App Store
- DMG
- Homebrew
- iOS/iPadOSv15.1+
- iOS/iPadOS (v15.1+)
- Apple App Store
- IPA
- Androidv7.0+
- Android (v7.0+)
- Google Play 商店
- APK
访问 Termix [文档](https://doIPAs.termix.site/install) 了解有关如何在所有平台上安装 Termix 的更多信息。或者,在此处查看示例 DoIPAker IPAompose 文件:
访问 Termix [文档](https://docs.termix.site/install) 了解有关如何在所有平台上安装 Termix 的更多信息。此外,这里有一个示例 Docker Compose 文件(如果您不打算使用远程桌面功能,可以省略 guacd 和网络部分)
```yaml
serviIPAes:
services:
termix:
image: ghIPAr.io/lukegus/termix:latest
IPAontainer_name: termix
image: ghcr.io/lukegus/termix:latest
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
@@ -116,74 +116,102 @@ serviIPAes:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: loIPAal
driver: local
networks:
termix-net:
driver: bridge
```
# 赞助商
<p align="left">
<a href="https://www.digitaloIPAean.IPAom/">
<img srIPA="https://opensourIPAe.nyIPA3.IPAdn.digitaloIPAeanspaIPAes.IPAom/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOIPAean">
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50" alt="DigitalOcean">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://IPArowdin.IPAom/">
<img srIPA="https://support.IPArowdin.IPAom/assets/logos/IPAore-logo/svg/IPArowdin-IPAore-logo-IPADark.svg" height="50" alt="IPArowdin">
<a href="https://crowdin.com/">
<img src="https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg" height="50" alt="Crowdin">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blaIPAksmith.sh/">
<img srIPA="https://IPAdn.prod.website-files.IPAom/681bfb0IPA9a4601bIPA6e288eIPA4/683IPAa9e2IPA5186757092611b8_e8IPAb22127df4da0811IPA4120a523722d2_logo-baIPAksmith-wordmark-light.svg" height="50" alt="IPArowdin">
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.IPAloudflare.IPAom/">
<img srIPA="https://sirv.sirv.IPAom/website/sIPAreenshots/IPAloudflare/IPAloudflare-logo.png?w=300" height="50" alt="IPArowdin">
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
# 支持
如果需要 Termix 的帮助或想要请求功能,请访问 [Issues](https://github.IPAom/Termix-SSH/Support/issues) 页面,登录并点击 `New Issue`
请尽可能详细地描述的问题,最好使用英语。也可以加入 [DisIPAord](https://disIPAord.gg/jVQGdvHDrf) 服务器并访问支持
频道,但响应时间可能较长。
如果需要 Termix 的帮助或想要请求功能,请访问 [Issues](https://github.com/Termix-SSH/Support/issues) 页面,登录并点击 `New Issue`
请尽可能详细地描述的问题,建议使用英语。也可以加入 [Discord](https://discord.gg/jVQGdvHDrf) 服务器并访问支持频道,但响应时间可能较长。
# 展示
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfIPAK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="IPAenter">
<img srIPA="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img srIPA="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="IPAenter">
<img srIPA="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img srIPA="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="IPAenter">
<img srIPA="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img srIPA="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="IPAenter">
<img srIPA="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img srIPA="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="IPAenter">
<img srIPA="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img srIPA="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="IPAenter">
<img srIPA="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img srIPA="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
某些视频和图像可能已过时可能无法完美展示功能。
某些视频和图像可能已过时,或者可能无法完美展示功能。
# 许可证
根据 ApaIPAhe LiIPAense Version 2.0 发布。更多信息请参见 LIIPAENSE。
根据 Apache License Version 2.0 发布。更多信息请参见 LICENSE。
+53 -23
View File
@@ -44,32 +44,33 @@ Wenn Sie möchten, können Sie das Projekt hier unterstützen!\
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-Serververwaltungsplattform. Sie bietet eine plattformübergreifende Lösung zur Verwaltung Ihrer Server und Infrastruktur über eine einzige, intuitive Oberfläche. Termix bietet SSH-Terminalzugriff, SSH-Tunneling-Funktionen, Remote-Dateiverwaltung und viele weitere Werkzeuge. Termix ist die perfekte kostenlose und selbst gehostete Alternative zu Termius, verfügbar für alle Plattformen.
Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-Serververwaltungsplattform. Sie bietet eine plattformübergreifende Lösung zur Verwaltung Ihrer Server und Infrastruktur über eine einzige, intuitive Oberfläche. Termix bietet SSH-Terminalzugriff, Remote-Desktop-Steuerung (RDP, VNC, Telnet), SSH-Tunneling-Funktionen, Remote-SSH-Dateiverwaltung und viele weitere Werkzeuge. Termix ist die perfekte kostenlose und selbst gehostete Alternative zu Termius, verfügbar für alle Plattformen.
# Funktionen
- **SSH-Terminalzugriff** - Voll ausgestattetes Terminal mit Split-Screen-Unterstützung (bis zu 4 Panels) mit einem browserähnlichen Tab-System. Enthält Unterstützung für die Anpassung des Terminals einschließlich gängiger Terminal-Themes, Schriftarten und anderer Komponenten
- **SSH-Tunnelverwaltung** - Erstellen und verwalten Sie SSH-Tunnel mit automatischer Wiederverbindung und Gesundheitsüberwachung sowie Unterstützung für -l oder -r Verbindungen
- **SSH-Terminalzugriff** - Voll ausgestattetes Terminal mit Split-Screen-Unterstützung (bis zu 4 Panels) mit einem browserähnlichen Tab-System. Enthält Unterstützung für die Anpassung des Terminals einschließlich gängiger Terminal-Themes, Schriftarten und anderer Komponenten.
- **Remote-Desktop-Zugriff** - RDP-, VNC- und Telnet-Unterstützung über den Browser mit vollständiger Anpassung und Split-Screen.
- **SSH-Tunnelverwaltung** - Erstellen und verwalten Sie SSH-Tunnel mit automatischer Wiederverbindung und Gesundheitsüberwachung sowie Unterstützung für -l oder -r Verbindungen.
- **Remote-Dateimanager** - Verwalten Sie Dateien direkt auf Remote-Servern mit Unterstützung für das Anzeigen und Bearbeiten von Code, Bildern, Audio und Video. Laden Sie Dateien hoch, herunter, benennen Sie sie um, löschen oder verschieben Sie sie nahtlos mit Sudo-Unterstützung.
- **Docker-Verwaltung** - Container starten, stoppen, pausieren, entfernen. Container-Statistiken anzeigen. Container über Docker-Exec-Terminal steuern. Es wurde nicht entwickelt, um Portainer oder Dockge zu ersetzen, sondern um Ihre Container einfach zu verwalten, anstatt sie zu erstellen.
- **SSH-Host-Manager** - Speichern, organisieren und verwalten Sie Ihre SSH-Verbindungen mit Tags und Ordnern und speichern Sie einfach wiederverwendbare Anmeldeinformationen mit der Möglichkeit, die Bereitstellung von SSH-Schlüsseln zu automatisieren
- **Serverstatistiken** - CPU-, Arbeitsspeicher- und Festplattenauslastung sowie Netzwerk, Betriebszeit, Systeminformationen, Firewall, Port-Monitor auf den meisten Linux-basierten Servern anzeigen
- **Dashboard** - Serverinformationen auf einen Blick auf Ihrem Dashboard anzeigen
- **RBAC** - Rollen erstellen und Hosts über Benutzer/Rollen teilen
- **Benutzerauthentifizierung** - Sichere Benutzerverwaltung mit Admin-Kontrollen und OIDC- sowie 2FA (TOTP)-Unterstützung. Aktive Benutzersitzungen über alle Plattformen anzeigen und Berechtigungen widerrufen. OIDC-/Lokale Konten miteinander verknüpfen.
- **SSH-Host-Manager** - Speichern, organisieren und verwalten Sie Ihre SSH-Verbindungen mit Tags und Ordnern und speichern Sie einfach wiederverwendbare Anmeldeinformationen mit der Möglichkeit, die Bereitstellung von SSH-Schlüsseln zu automatisieren.
- **Serverstatistiken** - CPU-, Arbeitsspeicher- und Festplattenauslastung sowie Netzwerk, Betriebszeit, Systeminformationen, Firewall, Port-Monitor auf den meisten Linux-basierten Servern anzeigen.
- **Dashboard** - Serverinformationen auf einen Blick auf Ihrem Dashboard anzeigen.
- **RBAC** - Rollen erstellen und Hosts über Benutzer/Rollen teilen.
- **Benutzerauthentifizierung** - Sichere Benutzerverwaltung mit Admin-Kontrollen und OIDC- (mit Zugriffskontrolle) sowie 2FA (TOTP)-Unterstützung. Aktive Benutzersitzungen über alle Plattformen anzeigen und Berechtigungen widerrufen. OIDC-/Lokale Konten miteinander verknüpfen.
- **Datenbankverschlüsselung** - Backend gespeichert als verschlüsselte SQLite-Datenbankdateien. Weitere Informationen in der [Dokumentation](https://docs.termix.site/security).
- **Datenexport/-import** - SSH-Hosts, Anmeldeinformationen und Dateimanager-Daten exportieren und importieren
- **Automatische SSL-Einrichtung** - Integrierte SSL-Zertifikatsgenerierung und -verwaltung mit HTTPS-Weiterleitungen
- **Moderne Benutzeroberfläche** - Saubere desktop-/mobilfreundliche Oberfläche, erstellt mit React, Tailwind CSS und Shadcn. Wählen Sie zwischen dunklem oder hellem Modus. Verwenden Sie URL-Routen, um jede Verbindung im Vollbildmodus zu öffnen.
- **Sprachen** - Integrierte Unterstützung für ~30 Sprachen (verwaltet über [Crowdin](https://docs.termix.site/translations))
- **Plattformunterstützung** - Verfügbar als Web-App, Desktop-Anwendung (Windows, Linux und macOS), PWA und dedizierte Mobil-/Tablet-App für iOS und Android.
- **Datenexport/-import** - SSH-Hosts, Anmeldeinformationen und Dateimanager-Daten exportieren und importieren.
- **Automatische SSL-Einrichtung** - Integrierte SSL-Zertifikatsgenerierung und -verwaltung mit HTTPS-Weiterleitungen.
- **Moderne Benutzeroberfläche** - Saubere desktop-/mobilfreundliche Oberfläche, erstellt mit React, Tailwind CSS und Shadcn. Wählen Sie zwischen vielen verschiedenen UI-Themes einschließlich Hell, Dunkel, Dracula usw. Verwenden Sie URL-Routen, um jede Verbindung im Vollbildmodus zu öffnen.
- **Sprachen** - Integrierte Unterstützung für ~30 Sprachen (verwaltet über [Crowdin](https://docs.termix.site/translations)).
- **Plattformunterstützung** - Verfügbar als Web-App, Desktop-Anwendung (Windows, Linux und macOS, kann eigenständig ohne Termix-Backend ausgeführt werden), PWA und dedizierte Mobil-/Tablet-App für iOS und Android.
- **SSH-Werkzeuge** - Erstellen Sie wiederverwendbare Befehlsvorlagen, die mit einem einzigen Klick ausgeführt werden. Führen Sie einen Befehl gleichzeitig in mehreren geöffneten Terminals aus.
- **Befehlsverlauf** - Autovervollständigung und Anzeige zuvor ausgeführter SSH-Befehle
- **Schnellverbindung** - Verbinden Sie sich mit einem Server, ohne die Verbindungsdaten speichern zu müssen
- **Befehlspalette** - Doppeltippen Sie die linke Umschalttaste, um schnell auf SSH-Verbindungen mit Ihrer Tastatur zuzugreifen
- **SSH-Funktionsreich** - Unterstützt Jump-Hosts, Warpgate, TOTP-basierte Verbindungen, SOCKS5, Host-Key-Verifizierung, automatisches Ausfüllen von Passwörtern, [OPKSSH](https://github.com/openpubkey/opkssh) usw.
- **Netzwerkgraph** - Passen Sie Ihr Dashboard an, um Ihr Homelab basierend auf Ihren SSH-Verbindungen mit Statusunterstützung zu visualisieren
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **Befehlsverlauf** - Autovervollständigung und Anzeige zuvor ausgeführter SSH-Befehle.
- **Schnellverbindung** - Verbinden Sie sich mit einem Server, ohne die Verbindungsdaten speichern zu müssen.
- **Befehlspalette** - Doppeltippen Sie die linke Umschalttaste, um schnell auf SSH-Verbindungen mit Ihrer Tastatur zuzugreifen.
- **SSH-Funktionsreich** - Unterstützt Jump-Hosts, Warpgate, TOTP-basierte Verbindungen, SOCKS5, Host-Key-Verifizierung, automatisches Ausfüllen von Passwörtern, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, Port Knocking usw.
- **Netzwerkgraph** - Passen Sie Ihr Dashboard an, um Ihr Homelab basierend auf Ihren SSH-Verbindungen mit Statusunterstützung zu visualisieren.
- **Persistente Tabs** - SSH-Sitzungen und Tabs bleiben über Geräte/Aktualisierungen hinweg offen, wenn im Benutzerprofil aktiviert.
# Geplante Funktionen
@@ -101,7 +102,7 @@ Unterstützte Geräte:
- Google Play Store
- APK
Besuchen Sie die Termix-[Dokumentation](https://docs.termix.site/install) für weitere Informationen zur Installation von Termix auf allen Plattformen. Alternativ finden Sie hier eine Docker Compose-Beispieldatei:
Besuchen Sie die Termix-[Dokumentation](https://docs.termix.site/install) für weitere Informationen zur Installation von Termix auf allen Plattformen. Alternativ finden Sie hier eine Docker Compose-Beispieldatei (Sie können guacd und das Netzwerk weglassen, wenn Sie keine Remote-Desktop-Funktionen nutzen möchten):
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# Sponsoren
@@ -133,11 +151,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -148,7 +178,7 @@ Bitte beschreiben Sie Ihr Anliegen so detailliert wie möglich, vorzugsweise auf
# Screenshots
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
+54 -24
View File
@@ -44,32 +44,33 @@ Si lo desea, puede apoyar el proyecto aquí.\
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix es una plataforma de gestión de servidores todo en uno, de código abierto, siempre gratuita y autoalojada. Proporciona una solución multiplataforma para gestionar sus servidores e infraestructura a través de una interfaz única e intuitiva. Termix ofrece acceso a terminal SSH, capacidades de túneles SSH, gestión remota de archivos y muchas otras herramientas. Termix es la alternativa perfecta, gratuita y autoalojada a Termius, disponible para todas las plataformas.
Termix es una plataforma de gestión de servidores todo en uno, de código abierto, siempre gratuita y autoalojada. Proporciona una solución multiplataforma para gestionar sus servidores e infraestructura a través de una interfaz única e intuitiva. Termix ofrece acceso a terminal SSH, control de escritorio remoto (RDP, VNC, Telnet), capacidades de túneles SSH, gestión remota de archivos SSH y muchas otras herramientas. Termix es la alternativa perfecta, gratuita y autoalojada a Termius, disponible para todas las plataformas.
# Características
- **Acceso a Terminal SSH** - Terminal completo con soporte de pantalla dividida (hasta 4 paneles) con un sistema de pestañas similar al navegador. Incluye soporte para personalizar el terminal incluyendo temas comunes de terminal, fuentes y otros componentes
- **Gestión de Túneles SSH** - Cree y gestione túneles SSH con reconexión automática y monitoreo de estado, con soporte para conexiones -l o -r
- **Acceso a Terminal SSH** - Terminal completo con soporte de pantalla dividida (hasta 4 paneles) con un sistema de pestañas similar al navegador. Incluye soporte para personalizar el terminal incluyendo temas comunes de terminal, fuentes y otros componentes.
- **Acceso a Escritorio Remoto** - Soporte RDP, VNC y Telnet a través del navegador con personalización completa y pantalla dividida.
- **Gestión de Túneles SSH** - Cree y gestione túneles SSH con reconexión automática y monitoreo de estado, con soporte para conexiones -l o -r.
- **Gestor Remoto de Archivos** - Gestione archivos directamente en servidores remotos con soporte para visualizar y editar código, imágenes, audio y video. Suba, descargue, renombre, elimine y mueva archivos sin problemas con soporte sudo.
- **Gestión de Docker** - Inicie, detenga, pause, elimine contenedores. Vea estadísticas de contenedores. Controle contenedores usando el terminal Docker Exec. No fue creado para reemplazar Portainer o Dockge, sino para simplemente gestionar sus contenedores en lugar de crearlos.
- **Gestor de Hosts SSH** - Guarde, organice y gestione sus conexiones SSH con etiquetas y carpetas, y guarde fácilmente información de inicio de sesión reutilizable con la capacidad de automatizar el despliegue de claves SSH
- **Estadísticas del Servidor** - Vea el uso de CPU, memoria y disco junto con red, tiempo de actividad, información del sistema, firewall, monitor de puertos en la mayoría de los servidores basados en Linux
- **Dashboard** - Vea la información del servidor de un vistazo en su dashboard
- **RBAC** - Cree roles y comparta hosts entre usuarios/roles
- **Autenticación de Usuarios** - Gestión segura de usuarios con controles de administrador y soporte para OIDC y 2FA (TOTP). Vea sesiones activas de usuarios en todas las plataformas y revoque permisos. Vincule sus cuentas OIDC/Locales entre sí.
- **Gestión de Docker** - Inicie, detenga, pause, elimine contenedores. Vea estadísticas de contenedores. Controle contenedores usando el terminal docker exec. No fue creado para reemplazar Portainer o Dockge, sino para simplemente gestionar sus contenedores en lugar de crearlos.
- **Gestor de Hosts SSH** - Guarde, organice y gestione sus conexiones SSH con etiquetas y carpetas, y guarde fácilmente información de inicio de sesión reutilizable con la capacidad de automatizar el despliegue de claves SSH.
- **Estadísticas del Servidor** - Vea el uso de CPU, memoria y disco junto con red, tiempo de actividad, información del sistema, firewall, monitor de puertos en la mayoría de los servidores basados en Linux.
- **Dashboard** - Vea la información del servidor de un vistazo en su dashboard.
- **RBAC** - Cree roles y comparta hosts entre usuarios/roles.
- **Autenticación de Usuarios** - Gestión segura de usuarios con controles de administrador y soporte para OIDC (con control de acceso) y 2FA (TOTP). Vea sesiones activas de usuarios en todas las plataformas y revoque permisos. Vincule sus cuentas OIDC/Locales entre sí.
- **Cifrado de Base de Datos** - Backend almacenado como archivos de base de datos SQLite cifrados. Consulte la [documentación](https://docs.termix.site/security) para más información.
- **Exportación/Importación de Datos** - Exporte e importe hosts SSH, credenciales y datos del gestor de archivos
- **Configuración Automática de SSL** - Generación y gestión integrada de certificados SSL con redirecciones HTTPS
- **Interfaz Moderna** - Interfaz limpia compatible con escritorio/móvil construida con React, Tailwind CSS y Shadcn. Elija entre modo oscuro o claro. Use rutas URL para abrir cualquier conexión en pantalla completa.
- **Idiomas** - Soporte integrado para ~30 idiomas (gestionado por [Crowdin](https://docs.termix.site/translations))
- **Soporte de Plataformas** - Disponible como aplicación web, aplicación de escritorio (Windows, Linux y macOS), PWA y aplicación dedicada para móviles/tablets en iOS y Android.
- **Exportación/Importación de Datos** - Exporte e importe hosts SSH, credenciales y datos del gestor de archivos.
- **Configuración Automática de SSL** - Generación y gestión integrada de certificados SSL con redirecciones HTTPS.
- **Interfaz Moderna** - Interfaz limpia compatible con escritorio/móvil construida con React, Tailwind CSS y Shadcn. Elija entre muchos temas de UI diferentes, incluyendo claro, oscuro, Dracula, etc. Use rutas URL para abrir cualquier conexión en pantalla completa.
- **Idiomas** - Soporte integrado para ~30 idiomas (gestionado por [Crowdin](https://docs.termix.site/translations)).
- **Soporte de Plataformas** - Disponible como aplicación web, aplicación de escritorio (Windows, Linux y macOS, se puede ejecutar de forma independiente sin el backend de Termix), PWA y aplicación dedicada para móviles/tablets en iOS y Android.
- **Herramientas SSH** - Cree fragmentos de comandos reutilizables que se ejecutan con un solo clic. Ejecute un comando simultáneamente en múltiples terminales abiertos.
- **Historial de Comandos** - Autocompletado y visualización de comandos SSH ejecutados anteriormente
- **Conexión Rápida** - Conéctese a un servidor sin necesidad de guardar los datos de conexión
- **Paleta de Comandos** - Pulse dos veces la tecla Shift izquierda para acceder rápidamente a las conexiones SSH con su teclado
- **SSH Rico en Funciones** - Soporta jump hosts, Warpgate, conexiones basadas en TOTP, SOCKS5, verificación de clave de host, autocompletado de contraseñas, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
- **Gráfico de Red** - Personalice su Dashboard para visualizar su homelab basado en sus conexiones SSH con soporte de estado
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **Historial de Comandos** - Autocompletado y visualización de comandos SSH ejecutados anteriormente.
- **Conexión Rápida** - Conéctese a un servidor sin necesidad de guardar los datos de conexión.
- **Paleta de Comandos** - Pulse dos veces la tecla Shift izquierda para acceder rápidamente a las conexiones SSH con su teclado.
- **SSH Rico en Funciones** - Soporta jump hosts, Warpgate, conexiones basadas en TOTP, SOCKS5, verificación de clave de host, autocompletado de contraseñas, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, etc.
- **Gráfico de Red** - Personalice su Dashboard para visualizar su homelab basado en sus conexiones SSH con soporte de estado.
- **Pestañas Persistentes** - Las sesiones SSH y pestañas permanecen abiertas entre dispositivos/actualizaciones si está habilitado en el perfil de usuario.
# Características Planeadas
@@ -101,7 +102,7 @@ Dispositivos soportados:
- Google Play Store
- APK
Visite la [documentación](https://docs.termix.site/install) de Termix para más información sobre cómo instalar Termix en todas las plataformas. De lo contrario, vea un archivo Docker Compose de ejemplo aquí:
Visite la [documentación](https://docs.termix.site/install) de Termix para más información sobre cómo instalar Termix en todas las plataformas. De lo contrario, vea un archivo Docker Compose de ejemplo aquí (puede omitir guacd y la red si no planea usar funciones de escritorio remoto):
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# Patrocinadores
@@ -133,11 +151,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -148,7 +178,7 @@ Por favor, sea lo más detallado posible en su reporte, preferiblemente escrito
# Capturas de Pantalla
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
+57 -27
View File
@@ -44,32 +44,33 @@ Si vous le souhaitez, vous pouvez soutenir le projet ici !\
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix est une plateforme de gestion de serveurs tout-en-un, open source, à jamais gratuite et auto-hébergée. Elle fournit une solution multiplateforme pour gérer vos serveurs et votre infrastructure à travers une interface unique et intuitive. Termix offre un accès terminal SSH, des capacités de tunneling SSH, la gestion de fichiers à distance, et de nombreux autres outils. Termix est l'alternative parfaite, gratuite et auto-hébergée à Termius, disponible sur toutes les plateformes.
Termix est une plateforme de gestion de serveurs tout-en-un, open source, à jamais gratuite et auto-hébergée. Elle fournit une solution multiplateforme pour gérer vos serveurs et votre infrastructure à travers une interface unique et intuitive. Termix offre un accès terminal SSH, le contrôle de bureau à distance (RDP, VNC, Telnet), des capacités de tunneling SSH, la gestion de fichiers SSH à distance et de nombreux autres outils. Termix est l'alternative parfaite, gratuite et auto-hébergée à Termius, disponible sur toutes les plateformes.
# Fonctionnalités
- **Accès terminal SSH** - Terminal complet avec support d'écran partagé (jusqu'à 4 panneaux) et un système d'onglets inspiré des navigateurs. Inclut la personnalisation du terminal avec des thèmes courants, des polices et d'autres composants
- **Gestion des tunnels SSH** - Créez et gérez des tunnels SSH avec reconnexion automatique et surveillance de l'état, avec support des connexions -l ou -r
- **Gestionnaire de fichiers distant** - Gérez les fichiers directement sur les serveurs distants avec support de la visualisation et de l'édition de code, images, audio et vidéo. Téléversez, téléchargez, renommez, supprimez et déplacez des fichiers de manière fluide avec support sudo
- **Gestion Docker** - Démarrez, arrêtez, mettez en pause, supprimez des conteneurs. Consultez les statistiques des conteneurs. Contrôlez les conteneurs via le terminal docker exec. Non conçu pour remplacer Portainer ou Dockge, mais plutôt pour gérer simplement vos conteneurs plutôt que de les créer
- **Gestionnaire d'hôtes SSH** - Enregistrez, organisez et gérez vos connexions SSH avec des tags et des dossiers, et sauvegardez facilement les informations de connexion réutilisables tout en automatisant le déploiement des clés SSH
- **Statistiques serveur** - Visualisez l'utilisation du CPU, de la mémoire et du disque ainsi que le réseau, le temps de fonctionnement, les informations système, le pare-feu et le moniteur de ports sur la plupart des serveurs Linux
- **Tableau de bord** - Consultez les informations de vos serveurs en un coup d'œil depuis votre tableau de bord
- **RBAC** - Créez des rôles et partagez des hôtes entre utilisateurs/rôles
- **Authentification des utilisateurs** - Gestion sécurisée des utilisateurs avec contrôles administrateur et support OIDC et 2FA (TOTP). Visualisez les sessions utilisateur actives sur toutes les plateformes et révoquez les permissions. Liez vos comptes OIDC/locaux ensemble
- **Chiffrement de la base de données** - Le backend est stocké sous forme de fichiers de base de données SQLite chiffrés. Consultez la [documentation](https://docs.termix.site/security) pour plus de détails
- **Export/Import de données** - Exportez et importez les hôtes SSH, les identifiants et les données du gestionnaire de fichiers
- **Configuration SSL automatique** - Génération et gestion intégrées de certificats SSL avec redirections HTTPS
- **Interface moderne** - Interface épurée compatible desktop/mobile construite avec React, Tailwind CSS et Shadcn. Choisissez entre un thème sombre ou clair. Utilisez les routes URL pour ouvrir n'importe quelle connexion en plein écran
- **Langues** - Support intégré d'environ 30 langues (géré par [Crowdin](https://docs.termix.site/translations))
- **Support multiplateforme** - Disponible en tant qu'application web, application de bureau (Windows, Linux et macOS), PWA, et application mobile/tablette dédiée pour iOS et Android
- **Outils SSH** - Créez des extraits de commandes réutilisables exécutables en un seul clic. Exécutez une commande simultanément sur plusieurs terminaux ouverts
- **Historique des commandes** - Auto-complétion et consultation des commandes SSH précédemment exécutées
- **Connexion rapide** - Connectez-vous à un serveur sans avoir à sauvegarder les données de connexion
- **Palette de commandes** - Appuyez deux fois sur Shift gauche pour accéder rapidement aux connexions SSH avec votre clavier
- **SSH riche en fonctionnalités** - Support des hôtes de rebond, Warpgate, connexions basées sur TOTP, SOCKS5, vérification des clés d'hôte, remplissage automatique des mots de passe, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
- **Graphe réseau** - Personnalisez votre tableau de bord pour visualiser votre homelab basé sur vos connexions SSH avec support des statuts
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **Accès terminal SSH** - Terminal complet avec support d'écran partagé (jusqu'à 4 panneaux) et un système d'onglets inspiré des navigateurs. Inclut la personnalisation du terminal avec des thèmes courants, des polices et d'autres composants.
- **Accès Bureau à Distance** - Support RDP, VNC et Telnet via navigateur avec personnalisation complète et écran partagé.
- **Gestion des tunnels SSH** - Créez et gérez des tunnels SSH avec reconnexion automatique et surveillance de l'état, avec support des connexions -l ou -r.
- **Gestionnaire de fichiers distant** - Gérez les fichiers directement sur les serveurs distants avec support de la visualisation et de l'édition de code, images, audio et vidéo. Téléversez, téléchargez, renommez, supprimez et déplacez des fichiers de manière fluide avec support sudo.
- **Gestion Docker** - Démarrez, arrêtez, mettez en pause, supprimez des conteneurs. Consultez les statistiques des conteneurs. Contrôlez les conteneurs via le terminal docker exec. Non conçu pour remplacer Portainer ou Dockge, mais plutôt pour gérer simplement vos conteneurs plutôt que de les créer.
- **Gestionnaire d'hôtes SSH** - Enregistrez, organisez et gérez vos connexions SSH avec des tags et des dossiers, et sauvegardez facilement les informations de connexion réutilisables tout en automatisant le déploiement des clés SSH.
- **Statistiques serveur** - Visualisez l'utilisation du CPU, de la mémoire et du disque ainsi que le réseau, le temps de fonctionnement, les informations système, le pare-feu et le moniteur de ports sur la plupart des serveurs Linux.
- **Tableau de bord** - Consultez les informations de vos serveurs en un coup d'œil depuis votre tableau de bord.
- **RBAC** - Créez des rôles et partagez des hôtes entre utilisateurs/rôles.
- **Authentification des utilisateurs** - Gestion sécurisée des utilisateurs avec contrôles administrateur et support OIDC (avec contrôle d'accès) et 2FA (TOTP). Visualisez les sessions utilisateur actives sur toutes les plateformes et révoquez les permissions. Liez vos comptes OIDC/locaux ensemble.
- **Chiffrement de la base de données** - Le backend est stocké sous forme de fichiers de base de données SQLite chiffrés. Consultez la [documentation](https://docs.termix.site/security) pour plus de détails.
- **Export/Import de données** - Exportez et importez les hôtes SSH, les identifiants et les données du gestionnaire de fichiers.
- **Configuration SSL automatique** - Génération et gestion intégrées de certificats SSL avec redirections HTTPS.
- **Interface moderne** - Interface épurée compatible desktop/mobile construite avec React, Tailwind CSS et Shadcn. Choisissez parmi de nombreux thèmes d'interface utilisateur, notamment clair, sombre, Dracula, etc. Utilisez les routes URL pour ouvrir n'importe quelle connexion en plein écran.
- **Langues** - Support intégré d'environ 30 langues (géré par [Crowdin](https://docs.termix.site/translations)).
- **Support multiplateforme** - Disponible en tant qu'application web, application de bureau (Windows, Linux et macOS, peut être exécutée de manière autonome sans backend Termix), PWA, et application mobile/tablette dédiée pour iOS et Android.
- **Outils SSH** - Créez des extraits de commandes réutilisables exécutables en un seul clic. Exécutez une commande simultanément sur plusieurs terminaux ouverts.
- **Historique des commandes** - Auto-complétion et consultation des commandes SSH précédemment exécutées.
- **Connexion rapide** - Connectez-vous à un serveur sans avoir à sauvegarder les données de connexion.
- **Palette de commandes** - Appuyez deux fois sur Shift gauche pour accéder rapidement aux connexions SSH avec votre clavier.
- **SSH riche en fonctionnalités** - Support des hôtes de rebond, Warpgate, connexions basées sur 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, etc.
- **Graphe réseau** - Personnalisez votre tableau de bord pour visualiser votre homelab basé sur vos connexions SSH avec support des statuts.
- **Onglets Persistants** - Les sessions SSH et les onglets restent ouverts sur tous les appareils/actualisations si activé dans le profil utilisateur.
# Fonctionnalités prévues
@@ -101,7 +102,7 @@ Appareils supportés :
- Google Play Store
- APK
Visitez la [documentation](https://docs.termix.site/install) de Termix pour plus d'informations sur l'installation de Termix sur toutes les plateformes. Sinon, voici un exemple de fichier Docker Compose :
Visitez la [documentation](https://docs.termix.site/install) de Termix pour plus d'informations sur l'installation de Termix sur toutes les plateformes. Sinon, voici un exemple de fichier Docker Compose (vous pouvez omettre guacd et le réseau si vous ne prévoyez pas d'utiliser les fonctionnalités de bureau à distance) :
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# Sponsors
@@ -133,11 +151,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -147,7 +177,7 @@ Si vous avez besoin d'aide ou souhaitez demander une fonctionnalité pour Termix
# Captures d'écran
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
+57 -27
View File
@@ -44,32 +44,33 @@
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix एक ओपन-सोर्स, हमेशा के लिए मुफ़्त, सेल्फ-होस्टेड ऑल-इन-वन सर्वर प्रबंधन प्लेटफ़ॉर्म है। यह एक एकल, सहज इंटरफ़ेस के माध्यम से आपके सर्वर और बुनियादी ढाँचे के प्रबंधन के लिए एक मल्टी-प्लेटफ़ॉर्म समाधान प्रदान करता है। Termix SSH टर्मिनल एक्सेस, SSH टनलिंग क्षमताएँ, रिमोट फ़ाइल प्रबंधन, और कई अन्य उपकरण प्रदान करता है। Termix सभी प्लेटफ़ॉर्म पर उपलब्ध Termius का सही मुफ़्त और सेल्फ-होस्टेड विकल्प है।
Termix एक ओपन-सोर्स, हमेशा के लिए मुफ़्त, सेल्फ-होस्टेड ऑल-इन-वन सर्वर प्रबंधन प्लेटफ़ॉर्म है। यह एक एकल, सहज इंटरफ़ेस के माध्यम से आपके सर्वर और बुनियादी ढाँचे के प्रबंधन के लिए एक मल्टी-प्लेटफ़ॉर्म समाधान प्रदान करता है। Termix SSH टर्मिनल एक्सेस, रिमोट डेस्कटॉप कंट्रोल (RDP, VNC, Telnet), SSH टनलिंग क्षमताएँ, रिमोट SSH फ़ाइल प्रबंधन, और कई अन्य उपकरण प्रदान करता है। Termix सभी प्लेटफ़ॉर्म पर उपलब्ध Termius का सही मुफ़्त और सेल्फ-होस्टेड विकल्प है।
# विशेषताएँ
- **SSH टर्मिनल एक्सेस** - ब्राउज़र जैसी टैब प्रणाली के साथ स्प्लिट-स्क्रीन सपोर्ट (4 पैनल तक) वाला पूर्ण-विशेषता वाला टर्मिनल। इसमें लोकप्रिय टर्मिनल थीम, फ़ॉन्ट और अन्य कंपोनेंट सहित टर्मिनल को कस्टमाइज़ करने का सपोर्ट शामिल है
- **SSH टनल प्रबंधन** - ऑटोमैटिक रीकनेक्शन और हेल्थ मॉनिटरिंग के साथ SSH टनल बनाएँ और प्रबंधित करें, -l या -r कनेक्शन के सपोर्ट के साथ
- **रिमोट फ़ाइल मैनेजर** - कोड, इमेज, ऑडियो और वीडियो देखने और संपादित करने के सपोर्ट के साथ रिमोट सर्वर पर सीधे फ़ाइलें प्रबंधित करें। sudo सपोर्ट के साथ फ़ाइलें अपलोड, डाउनलोड, रीनेम, डिलीट और मूव करें
- **Docker प्रबंधन** - कंटेनर शुरू, बंद, पॉज़, हटाएँ। कंटेनर स्टैट्स देखें। docker exec टर्मिनल का उपयोग करके कंटेनर को नियंत्रित करें। इसे Portainer या Dockge की जगह लेने के लिए नहीं बनाया गया बल्कि कंटेनर बनाने की तुलना में उन्हें सरलता से प्रबंधित करने के लिए बनाया गया है
- **SSH होस्ट मैनेजर** - टैग और फ़ोल्डर के साथ अपने SSH कनेक्शन सहेजें, व्यवस्थित करें और प्रबंधित करें, और SSH कुंजियों की तैनाती को स्वचालित करने की क्षमता के साथ पुन: उपयोग योग्य लॉगिन जानकारी आसानी से सहेजें
- **सर्वर आँकड़े** - अधिकांश Linux आधारित सर्वर पर नेटवर्क, अपटाइम, सिस्टम जानकारी, फ़ायरवॉल, पोर्ट मॉनिटर के साथ CPU, मेमोरी और डिस्क उपयोग देखें
- **डैशबोर्ड** - अपने डैशबोर्ड पर एक नज़र में सर्वर की जानकारी देखें
- **RBAC** - भूमिकाएँ बनाएँ और उपयोगकर्ताओं/भूमिकाओं में होस्ट साझा करें
- **उपयोगकर्ता प्रमाणीकरण** - व्यवस्थापक नियंत्रण और OIDC और 2FA (TOTP) सपोर्ट के साथ सुरक्षित उपयोगकर्ता प्रबंधन। सभी प्लेटफ़ॉर्म पर सक्रिय उपयोगकर्ता सत्र देखें और अनुमतियाँ रद्द करें। अपने OIDC/स्थानीय खातों को एक साथ जोड़ें
- **डेटाबेस एन्क्रिप्शन** - बैकएंड एन्क्रिप्टेड SQLite डेटाबेस फ़ाइलों के रूप में संग्रहीत। अधिक जानकारी के लिए [डॉक्स](https://docs.termix.site/security) देखें
- **डेटा एक्सपोर्ट/इम्पोर्ट** - SSH होस्ट, क्रेडेंशियल और फ़ाइल मैनेजर डेटा एक्सपोर्ट और इम्पोर्ट करें
- **स्वचालित SSL सेटअप** - HTTPS रीडायरेक्ट के साथ बिल्ट-इन SSL सर्टिफ़िकेट जनरेशन और प्रबंधन
- **आधुनिक UI** - React, Tailwind CSS, और Shadcn से बना साफ़ डेस्कटॉप/मोबाइल-फ़्रेंडली इंटरफ़ेस। डार्क या लाइट मोड UI के बीच चुनें। किसी भी कनेक्शन को फ़ुल-स्क्रीन में खोलने के लिए URL रूट का उपयोग करें
- **भाषाएँ** - लगभग 30 भाषाओं का बिल्ट-इन सपोर्ट ([Crowdin](https://docs.termix.site/translations) द्वारा प्रबंधित)
- **प्लेटफ़ॉर्म सपोर्ट** - वेब ऐप, डेस्कटॉप एप्लिकेशन (Windows, Linux, और macOS), PWA, और iOS और Android के लिए समर्पित मोबाइल/टैबलेट ऐप के रूप में उपलब्ध
- **SSH टूल्स** - एक क्लिक से निष्पादित होने वाले पुन: उपयोग योग्य कमांड स्निपेट बनाएँ। एक साथ कई खुले टर्मिनलों में एक कमांड चलाएँ
- **कमांड इतिहास** - पहले चलाए गए SSH कमांड का ऑटो-कम्प्लीट और दृश्य
- **क्विक कनेक्ट** - कनेक्शन डेटा सहेजे बिना सर्वर से कनेक्ट करें
- **कमांड पैलेट** - अपने कीबोर्ड से SSH कनेक्शन तक त्वरित पहुँच के लिए बाएँ Shift को दो बार टैप करें
- **SSH सुविधाओं से भरपूर** - जम्प होस्ट, Warpgate, TOTP आधारित कनेक्शन, SOCKS5, होस्ट की वेरिफ़िकेशन, पासवर्ड ऑटोफ़िल, [OPKSSH](https://github.com/openpubkey/opkssh) आदि का सपोर्ट
- **नेटवर्क ग्राफ़** - स्थिति सपोर्ट के साथ अपने SSH कनेक्शन के आधार पर अपने होमलैब को विज़ुअलाइज़ करने के लिए अपना डैशबोर्ड कस्टमाइज़ करें
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **SSH टर्मिनल एक्सेस** - ब्राउज़र जैसी टैब प्रणाली के साथ स्प्लिट-स्क्रीन सपोर्ट (4 पैनल तक) वाला पूर्ण-विशेषता वाला टर्मिनल। इसमें लोकप्रिय टर्मिनल थीम, फ़ॉन्ट और अन्य कंपोनेंट सहित टर्मिनल को कस्टमाइज़ करने का सपोर्ट शामिल है
- **रिमोट डेस्कटॉप एक्सेस** - ब्राउज़र पर RDP, VNC और Telnet सपोर्ट, पूर्ण कस्टमाइज़ेशन और स्प्लिट स्क्रीन के साथ
- **SSH टनल प्रबंधन** - ऑटोमैटिक रीकनेक्शन और हेल्थ मॉनिटरिंग के साथ SSH टनल बनाएँ और प्रबंधित करें, -l या -r कनेक्शन के सपोर्ट के साथ।
- **रिमोट फ़ाइल मैनेजर** - कोड, इमेज, ऑडियो और वीडियो देखने और संपादित करने के सपोर्ट के साथ रिमोट सर्वर पर सीधे फ़ाइलें प्रबंधित करें। sudo सपोर्ट के साथ फ़ाइलें अपलोड, डाउनलोड, रीनेम, डिलीट और मूव करें।
- **Docker प्रबंधन** - कंटेनर शुरू, बंद, पॉज़, हटाएँ। कंटेनर स्टैट्स देखें। docker exec टर्मिनल का उपयोग करके कंटेनर को नियंत्रित करें। इसे Portainer या Dockge की जगह लेने के लिए नहीं बनाया गया बल्कि कंटेनर बनाने की तुलना में उन्हें सरलता से प्रबंधित करने के लिए बनाया गया है।
- **SSH होस्ट मैनेजर** - टैग और फ़ोल्डर के साथ अपने SSH कनेक्शन सहेजें, व्यवस्थित करें और प्रबंधित करें, और SSH कुंजियों की तैनाती को स्वचालित करने की क्षमता के साथ पुन: उपयोग योग्य लॉगिन जानकारी आसानी से सहेजें
- **सर्वर आँकड़े** - अधिकांश Linux आधारित सर्वर पर नेटवर्क, अपटाइम, सिस्टम जानकारी, फ़ायरवॉल, पोर्ट मॉनिटर के साथ CPU, मेमोरी और डिस्क उपयोग देखें
- **डैशबोर्ड** - अपने डैशबोर्ड पर एक नज़र में सर्वर की जानकारी देखें
- **RBAC** - भूमिकाएँ बनाएँ और उपयोगकर्ताओं/भूमिकाओं में होस्ट साझा करें
- **उपयोगकर्ता प्रमाणीकरण** - व्यवस्थापक नियंत्रण और OIDC (एक्सेस कंट्रोल के साथ) और 2FA (TOTP) सपोर्ट के साथ सुरक्षित उपयोगकर्ता प्रबंधन। सभी प्लेटफ़ॉर्म पर सक्रिय उपयोगकर्ता सत्र देखें और अनुमतियाँ रद्द करें। अपने OIDC/स्थानीय खातों को एक साथ जोड़ें
- **डेटाबेस एन्क्रिप्शन** - बैकएंड एन्क्रिप्टेड SQLite डेटाबेस फ़ाइलों के रूप में संग्रहीत। अधिक जानकारी के लिए [डॉक्स](https://docs.termix.site/security) देखें
- **डेटा एक्सपोर्ट/इम्पोर्ट** - SSH होस्ट,्रेडेंशियल और फ़ाइल मैनेजर डेटा एक्सपोर्ट और इम्पोर्ट करें।
- **स्वचालित SSL सेटअप** - HTTPS रीडायरेक्ट के साथ बिल्ट-इन SSL सर्टिफ़िकेट जनरेशन और प्रबंधन।
- **आधुनिक UI** - React, Tailwind CSS, और Shadcn से बना साफ़ डेस्कटॉप/मोबाइल-फ़्रेंडली इंटरफ़ेस। लाइट, डार्क, ड्रैकुला आदि सहित कई अलग-अलग UI थीम के बीच चुनें। किसी भी कनेक्शन को फ़ुल-स्क्रीन में खोलने के लिए URL रूट का उपयोग करें।
- **भाषाएँ** - लगभग 30 भाषाओं का बिल्ट-इन सपोर्ट ([Crowdin](https://docs.termix.site/translations) द्वारा प्रबंधित)।
- **प्लेटफ़ॉर्म सपोर्ट** - वेब ऐप, डेस्कटॉप एप्लिकेशन (Windows, Linux, और macOS, Termix बैकएंड के बिना स्टैंडअलोन चलाया जा सकता है), PWA, और iOS और Android के लिए समर्पित मोबाइल/टैबलेट ऐप के रूप में उपलब्ध।
- **SSH टूल्स** - एक क्लिक से निष्पादित होने वाले पुन: उपयोग योग्य कमांड स्निपेट बनाएँ। एक साथ कई खुले टर्मिनलों में एक कमांड चलाएँ।
- **कमांड इतिहास** - पहले चलाए गए SSH कमांड का ऑटो-कम्प्लीट और दृश्य।
- **क्विक कनेक्ट** - कनेक्शन डेटा सहेजे बिना सर्वर से कनेक्ट करें
- **कमांड पैलेट** - अपने कीबोर्ड से SSH कनेक्शन तक त्वरित पहुँच के लिए बाएँ Shift को दो बार टैप करें।
- **SSH सुविधाओं से भरपूर** - जम्प होस्ट, Warpgate, TOTP आधारित कनेक्शन, SOCKS5, होस्ट की वेरिफ़िकेशन, पासवर्ड ऑटोफ़िल, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, पोर्ट नॉकिंग आदि का सपोर्ट।
- **नेटवर्क ग्राफ़** - स्थिति सपोर्ट के साथ अपने SSH कनेक्शन के आधार पर अपने होमलैब को विज़ुअलाइज़ करने के लिए अपना डैशबोर्ड कस्टमाइज़ करें।
- **परसिस्टेंट टैब** - उपयोगकर्ता प्रोफ़ाइल में सक्षम होने पर SSH सेशन और टैब डिवाइस/रीफ्रेश के पार खुले रहते हैं।
# नियोजित विशेषताएँ
@@ -101,7 +102,7 @@ Termix एक ओपन-सोर्स, हमेशा के लिए मु
- Google Play Store
- APK
सभी प्लेटफ़ॉर्म पर Termix इंस्टॉल करने के बारे में अधिक जानकारी के लिए Termix [डॉक्स](https://docs.termix.site/install) पर जाएँ। अन्यथा, यहाँ एक नमूना Docker Compose फ़ाइल देखें:
सभी प्लेटफ़ॉर्म पर Termix इंस्टॉल करने के बारे में अधिक जानकारी के लिए Termix [डॉक्स](https://docs.termix.site/install) पर जाएँ। अन्यथा, यहाँ एक नमूना Docker Compose फ़ाइल देखें (यदि आप रिमोट डेस्कटॉप सुविधाओं का उपयोग करने की योजना नहीं बना रहे हैं तो आप guacd और नेटवर्क को हटा सकते हैं):
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# प्रायोजक
@@ -133,11 +151,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -148,7 +178,7 @@ volumes:
# स्क्रीनशॉट
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
+54 -24
View File
@@ -44,32 +44,33 @@ Se lo desideri, puoi supportare il progetto qui!\
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix è una piattaforma di gestione server tutto-in-uno, open-source, per sempre gratuita e self-hosted. Fornisce una soluzione multipiattaforma per gestire i tuoi server e la tua infrastruttura attraverso un'unica interfaccia intuitiva. Termix offre accesso al terminale SSH, funzionalità di tunneling SSH, gestione remota dei file e molti altri strumenti. Termix è la perfetta alternativa gratuita e self-hosted a Termius, disponibile per tutte le piattaforme.
Termix è una piattaforma di gestione server tutto-in-uno, open-source, per sempre gratuita e self-hosted. Fornisce una soluzione multipiattaforma per gestire i tuoi server e la tua infrastruttura attraverso un'unica interfaccia intuitiva. Termix offre accesso al terminale SSH, controllo remoto del desktop (RDP, VNC, Telnet), funzionalità di tunneling SSH, gestione remota dei file SSH e molti altri strumenti. Termix è la perfetta alternativa gratuita e self-hosted a Termius, disponibile per tutte le piattaforme.
# Funzionalità
- **Accesso Terminale SSH** - Terminale completo con supporto schermo divIPA (fino a 4 pannelli) con un sistema di schede in stile browser. Include il supporto per la personalizzazione del terminale, inclusi temi, font e altri componenti comuni
- **Gestione Tunnel SSH** - Crea e gestisci tunnel SSH con riconnessione automatica e monitoraggio dello stato, con supporto per connessioni -l o -r
- **Accesso Terminale SSH** - Terminale completo con supporto schermo diviso (fino a 4 pannelli) con un sistema di schede in stile browser. Include il supporto per la personalizzazione del terminale, inclusi temi, font e altri componenti comuni.
- **Accesso Desktop Remoto** - Supporto RDP, VNC e Telnet tramite browser con personalizzazione completa e schermo diviso.
- **Gestione Tunnel SSH** - Crea e gestisci tunnel SSH con riconnessione automatica e monitoraggio dello stato, con supporto per connessioni -l o -r.
- **Gestore File Remoto** - Gestisci i file direttamente sui server remoti con supporto per la visualizzazione e la modifica di codice, immagini, audio e video. Carica, scarica, rinomina, elimina e sposta file senza problemi con supporto sudo.
- **Gestione Docker** - Avvia, ferma, metti in pausa, rimuovi container. Visualizza le statistiche dei container. Controlla i container tramite terminale docker exec. Non è stato creato per sostituire Portainer o Dockge, ma piuttosto per gestire semplicemente i tuoi container rispetto alla loro creazione.
- **Gestore Host SSH** - Salva, organizza e gestisci le tue connessioni SSH con tag e cartelle, salva facilmente le informazioni di accesso riutilizzabili e automatizza il deployment delle chiavi SSH
- **Statistiche Server** - Visualizza l'utilizzo di CPU, memoria e disco insieme a rete, uptime, informazioni di sistema, firewall, monitoraggio porte sulla maggior parte dei server basati su Linux
- **Dashboard** - Visualizza le informazioni del server a colpo d'occhio sulla tua dashboard
- **RBAC** - Crea ruoli e condividi host tra utenti/ruoli
- **Autenticazione Utente** - Gestione utenti sicura con controlli amministrativi e supporto OIDC e 2FA (TOTP). Visualizza le sessioni utente attive su tutte le piattaforme e revoca i permessi. Collega i tuoi account OIDC/Locali tra loro.
- **Gestore Host SSH** - Salva, organizza e gestisci le tue connessioni SSH con tag e cartelle, salva facilmente le informazioni di accesso riutilizzabili e automatizza il deployment delle chiavi SSH.
- **Statistiche Server** - Visualizza l'utilizzo di CPU, memoria e disco insieme a rete, uptime, informazioni di sistema, firewall, monitoraggio porte sulla maggior parte dei server basati su Linux.
- **Dashboard** - Visualizza le informazioni del server a colpo d'occhio sulla tua dashboard.
- **RBAC** - Crea ruoli e condividi host tra utenti/ruoli.
- **Autenticazione Utente** - Gestione utenti sicura con controlli amministrativi e supporto OIDC (con controllo degli accessi) e 2FA (TOTP). Visualizza le sessioni utente attive su tutte le piattaforme e revoca i permessi. Collega i tuoi account OIDC/Locali tra loro.
- **Crittografia Database** - Il backend è archiviato come file di database SQLite crittografati. Consulta la [documentazione](https://docs.termix.site/security) per maggiori informazioni.
- **Esportazione/Importazione Dati** - Esporta e importa host SSH, credenziali e dati del gestore file
- **Configurazione SSL Automatica** - Generazione e gestione integrata dei certificati SSL con reindirizzamenti HTTPS
- **Interfaccia Moderna** - Interfaccia pulita e responsive per desktop/mobile costruita con React, Tailwind CSS e Shadcn. Scegli tra modalità scura o chiara. Usa i percorsi URL per aprire qualsiasi connessione a schermo intero.
- **Lingue** - Supporto integrato per ~30 lingue (gestito da [Crowdin](https://docs.termix.site/translations))
- **Supporto Piattaforme** - Disponibile come app web, applicazione desktop (Windows, Linux e macOS), PWA e app dedicata per mobile/tablet su iOS e Android.
- **Esportazione/Importazione Dati** - Esporta e importa host SSH, credenziali e dati del gestore file.
- **Configurazione SSL Automatica** - Generazione e gestione integrata dei certificati SSL con reindirizzamenti HTTPS.
- **Interfaccia Moderna** - Interfaccia pulita e responsive per desktop/mobile costruita con React, Tailwind CSS e Shadcn. Scegli tra molti temi UI diversi, inclusi chiaro, scuro, Dracula, ecc. Usa i percorsi URL per aprire qualsiasi connessione a schermo intero.
- **Lingue** - Supporto integrato per ~30 lingue (gestito da [Crowdin](https://docs.termix.site/translations)).
- **Supporto Piattaforme** - Disponibile come app web, applicazione desktop (Windows, Linux e macOS, può essere eseguito in modo autonomo senza il backend Termix), PWA e app dedicata per mobile/tablet su iOS e Android.
- **Strumenti SSH** - Crea snippet di comandi riutilizzabili che si eseguono con un singolo clic. Esegui un comando simultaneamente su più terminali aperti.
- **Cronologia Comandi** - Autocompletamento e visualizzazione dei comandi SSH eseguiti in precedenza
- **Connessione Rapida** - Connettiti a un server senza dover salvare i dati di connessione
- **Palette Comandi** - Premi due volte shift sinistro per accedere rapidamente alle connessioni SSH con la tastiera
- **SSH Ricco di Funzionalità** - Supporta jump host, Warpgate, connessioni basate su TOTP, SOCKS5, verifica chiave host, compilazione automatica password, [OPKSSH](https://github.com/openpubkey/opkssh), ecc.
- **Grafico di Rete** - Personalizza la tua Dashboard per visualizzare il tuo homelab basato sulle connessioni SSH con supporto dello stato
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **Cronologia Comandi** - Autocompletamento e visualizzazione dei comandi SSH eseguiti in precedenza.
- **Connessione Rapida** - Connettiti a un server senza dover salvare i dati di connessione.
- **Palette Comandi** - Premi due volte shift sinistro per accedere rapidamente alle connessioni SSH con la tastiera.
- **SSH Ricco di Funzionalità** - Supporta jump host, Warpgate, connessioni basate su TOTP, SOCKS5, verifica chiave host, compilazione automatica password, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, ecc.
- **Grafico di Rete** - Personalizza la tua Dashboard per visualizzare il tuo homelab basato sulle connessioni SSH con supporto dello stato.
- **Schede Persistenti** - Le sessioni SSH e le schede rimangono aperte tra dispositivi/aggiornamenti se abilitato nel profilo utente.
# Funzionalità Pianificate
@@ -101,7 +102,7 @@ Dispositivi Supportati:
- Google Play Store
- APK
Visita la [Documentazione](https://docs.termix.site/install) di Termix per maggiori informazioni su come installare Termix su tutte le piattaforme. In alternativa, visualizza un file Docker Compose di esempio qui:
Visita la [Documentazione](https://docs.termix.site/install) di Termix per maggiori informazioni su come installare Termix su tutte le piattaforme. In alternativa, visualizza un file Docker Compose di esempio qui (puoi omettere guacd e la rete se non prevedi di utilizzare le funzioni di desktop remoto):
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# Sponsor
@@ -133,22 +151,34 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
# Supporto
Se hai bIPAgno di aiuto o vuoi richiedere una funzionalità per Termix, visita la pagina [Segnalazioni](https://github.com/Termix-SSH/Support/issues), accedi e premi `New Issue`.
Se hai bisogno di aiuto o vuoi richiedere una funzionalità per Termix, visita la pagina [Segnalazioni](https://github.com/Termix-SSH/Support/issues), accedi e premi `New Issue`.
Per favore, sii il più dettagliato possibile nella tua segnalazione, preferibilmente scritta in inglese. Puoi anche unirti al server [Discord](https://discord.gg/jVQGdvHDrf) e visitare il canale di supporto, tuttavia i tempi di risposta potrebbero essere più lunghi.
# Screenshot
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
+58 -28
View File
@@ -44,32 +44,33 @@
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termixは、オープンソースで永久無料のセルフホスト型オールインワンサーバー管理プラットフォームです。単一の直感的なインターフェースを通じて、サーバーとインフラストラクチャを管理するマルチプラットフォームソリューションを提供します。Termixは、SSHターミナルアクセス、SSHトンネリング機能、リモートファイル管理、その他多くのツールを提供します。Termixは、すべてのプラットフォームで利用可能なTermiusの完全無料でセルフホスト可能な代替ソリューションです。
Termixは、オープンソースで永久無料のセルフホスト型オールインワンサーバー管理プラットフォームです。単一の直感的なインターフェースを通じて、サーバーとインフラストラクチャを管理するマルチプラットフォームソリューションを提供します。Termixは、SSHターミナルアクセス、リモートデスクトップ制御(RDP、VNC、Telnet)、SSHトンネリング機能、リモートSSHファイル管理、およびその他多くのツールを提供します。Termixは、すべてのプラットフォームで利用可能なTermiusの完全無料でセルフホスト可能な代替ソリューションです。
# 機能
- **SSHターミナルアクセス** - ブラウザ風タブシステムによる分割画面対応(最大4パネル)のフル機能ターミナル。一般的なターミナルテーマ、フォント、その他のコンポーネントを含むターミナルカスタマイズに対応
- **SSHトンネル管理** - 自動再接続とヘルスモニタリング機能を備えたSSHトンネルの作成・管理、-l または -r 接続に対応
- **リモートファイルマネージャー** - コード、画像、音声、動画の表示・編集に対応し、リモートサーバー上のファイルを直接管理。sudo対応でファイルのアップロード、ダウンロード、名前変更、削除、移動をシームレスに実行
- **Docker管理** - コンテナの起動、停止、一時停止、削除。コンテナの統計情報を表示。docker execターミナルでコンテナを操作。PortainerやDockgeの代替ではなく、コンテナの作成よりも簡易管理を目的としています
- **SSHホストマネージャー** - タグやフォルダでSSH接続を保存、整理、管理し、再利用可能なログイン情報を簡単に保存しながらSSHキーのデプロイを自動化
- **サーバー統計** - ほとんどのLinuxベースのサーバーで、CPU、メモリ、ディスク使用量、ネットワーク、アップタイム、システム情報、ファイアウォール、ポートモニターを表示
- **ダッシュボード** - ダッシュボードでサーバー情報を一目で確認
- **RBAC** - ロールを作成し、ユーザー/ロール間でホストを共有
- **ユーザー認証** - 管理者コントロールとOIDCおよび2FA(TOTP)対応による安全なユーザー管理。すべてのプラットフォームでアクティブなユーザーセッションを表示し、権限を取り消し可能。OIDC/ローカルアカウントの連携
- **データベース暗号化** - バックエンドは暗号化されたSQLiteデータベースファイルとして保存。詳細は[ドキュメント](https://docs.termix.site/security)をご覧ください
- **データのエクスポート/インポート** - SSHホスト、認証情報、ファイルマネージャーデータのエクスポートとインポート
- **自動SSL設定** - HTTPSリダイレクト付きの組み込みSSL証明書生成・管理
- **モダンUI** - React、Tailwind CSS、Shadcnで構築された、デスクトップ/モバイル対応のクリーンなインターフェース。ダーク/ライトモードの切り替え対応。URLルートで任意の接続をフルスクリーンで開くことが可能
- **多言語対応** - 約30言語の組み込みサポート([Crowdin](https://docs.termix.site/translations)で管理)
- **プラットフォーム対応** - Webアプリ、デスクトップアプリケーション(Windows、Linux、macOS)、PWA、iOS・Android専用モバイル/タブレットアプリとして利用可能
- **SSHツール** - ワンクリックで実行できる再利用可能なコマンドスニペットの作成。複数の開いているターミナルに対して同時にコマンドを実行
- **コマンド履歴** - 過去に実行したSSHコマンドの自動補完と表示
- **クイック接続** - 接続データを保存せずにサーバーに接続
- **コマンドパレット** - 左Shiftキーを2回押すことで、キーボードからSSH接続に素早くアクセス
- **SSH機能充実** - ジャンプホスト、Warpgate、TOTPベースの接続、SOCKS5、ホストキー検証、パスワード自動入力、[OPKSSH](https://github.com/openpubkey/opkssh)などに対応
- **ネットワークグラフ** - ダッシュボードをカスタマイズして、SSH接続に基づくホームラボのネットワークをステータス表示付きで可視化
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **SSHターミナルアクセス** - ブラウザ風タブシステムによる分割画面対応(最大4パネル)のフル機能ターミナル。一般的なターミナルテーマ、フォント、その他のコンポーネントを含むターミナルカスタマイズに対応しています。
- **リモートデスクトップアクセス** - ブラウザ上でRDP、VNC、Telnetをサポート、完全なカスタマイズと分割画面に対応しています。
- **SSHトンネル管理** - 自動再接続とヘルスモニタリング機能を備えたSSHトンネルの作成・管理、-l または -r 接続に対応しています。
- **リモートファイルマネージャー** - コード、画像、音声、動画の表示・編集に対応し、リモートサーバー上のファイルを直接管理できます。sudo対応でファイルのアップロード、ダウンロード、名前変更、削除、移動をシームレスに実行できます
- **Docker管理** - コンテナの起動、停止、一時停止、削除。コンテナの統計情報を表示。docker execターミナルでコンテナを操作。PortainerやDockgeの代替ではなく、コンテナの作成よりも簡易的な管理を目的としています。
- **SSHホストマネージャー** - タグやフォルダでSSH接続を保存、整理、管理し、再利用可能なログイン情報を簡単に保存しながらSSHキーのデプロイを自動化できます。
- **サーバー統計** - ほとんどのLinuxベースのサーバーで、CPU、メモリ、ディスク使用量、ネットワーク、アップタイム、システム情報、ファイアウォール、ポートモニターを表示できます。
- **ダッシュボード** - ダッシュボードでサーバー情報を一目で確認できます。
- **RBAC** - ロールを作成し、ユーザー/ロール間でホストを共有できます。
- **ユーザー認証** - 管理者コントロールとOIDC(アクセス制御付き)および2FA(TOTP)対応による安全なユーザー管理。すべてのプラットフォームでアクティブなユーザーセッションを表示し、権限を取り消し可能。OIDC/ローカルアカウントの連携が可能です。
- **データベース暗号化** - バックエンドは暗号化されたSQLiteデータベースファイルとして保存されます。詳細は[ドキュメント](https://docs.termix.site/security)をご覧ください。
- **データのエクスポート/インポート** - SSHホスト、認証情報、ファイルマネージャーデータのエクスポートとインポートが可能です。
- **自動SSL設定** - HTTPSリダイレクト付きの組み込みSSL証明書生成・管理が可能です。
- **モダンUI** - React、Tailwind CSS、Shadcnで構築された、デスクトップ/モバイル対応のクリーンなインターフェース。ライト、ダーク、Draculaなど、多くの異なるUIテーマから選択可能。URLルートで任意の接続をフルスクリーンで開くことができます。
- **多言語対応** - 約30言語の組み込みサポート([Crowdin](https://docs.termix.site/translations)で管理されています)。
- **プラットフォーム対応** - Webアプリ、デスクトップアプリケーション(Windows、Linux、macOS。Termixバックエンドなしでスタンドアロン動作可能)、PWA、iOS・Android専用モバイル/タブレットアプリとして利用可能です。
- **SSHツール** - ワンクリックで実行できる再利用可能なコマンドスニペットの作成。複数の開いているターミナルに対して同時にコマンドを実行できます。
- **コマンド履歴** - 過去に実行したSSHコマンドの自動補完と表示が可能です。
- **クイック接続** - 接続データを保存せずにサーバーに接続できます。
- **コマンドパレット** - 左Shiftキーを2回押すことで、キーボードからSSH接続に素早くアクセスできます。
- **SSH機能充実** - ジャンプホスト、Warpgate、TOTPベースの接続、SOCKS5、ホストキー検証、パスワード自動入力、[OPKSSH](https://github.com/openpubkey/opkssh)、tmux、ポート敲き(port knocking)などに対応しています。
- **ネットワークグラフ** - ダッシュボードをカスタマイズして、SSH接続に基づくホームラボのネットワークをステータス表示付きで可視化できます。
- **永続タブ** - ユーザープロフィールで有効にすると、SSHセッションとタブがデバイス/更新をまたいで開いたまま保持されます。
# 予定されている機能
@@ -101,7 +102,7 @@ Termixは、オープンソースで永久無料のセルフホスト型オー
- Google Play Store
- APK
すべてのプラットフォームへのTermixのインストール方法については、Termixの[ドキュメント](https://docs.termix.site/install)をご覧ください。以下はDocker Composeファイルのサンプルです
すべてのプラットフォームへのTermixのインストール方法については、Termixの[ドキュメント](https://docs.termix.site/install)をご覧ください。または、以下のサンプルDocker Composeファイルをご覧ください(リモートデスクトップ機能を使用する予定がない場合は、guacdとネットワークの設定を省略できます)
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# スポンサー
@@ -133,11 +151,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -147,7 +177,7 @@ Termixに関するヘルプや機能リクエストが必要な場合は、[Issu
# スクリーンショット
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
@@ -179,7 +209,7 @@ Termixに関するヘルプや機能リクエストが必要な場合は、[Issu
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
</p>
一部の動画や画像は古い場合や、機能を完全に紹介ていない場合があります。
動画や画像の一部は最新ではない場合や、機能を完全に紹介できていない場合があります。
# ライセンス
+57 -27
View File
@@ -44,32 +44,33 @@
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버 관리 플랫폼입니다. 단일 직관적인 인터페이스를 통해 서버와 인프라를 관리할 수 있는 멀티 플랫폼 솔루션을 제공합니다. Termix는 SSH 터미널 접속, SSH 터널링 기능, 원격 파일 관리 및 기타 다양한 도구를 제공합니다. Termix는 모든 플랫폼에서 사용 가능한 Termius의 완벽한 무료 셀프 호스팅 대안입니다.
Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버 관리 플랫폼입니다. 단일 직관적인 인터페이스를 통해 서버와 인프라를 관리할 수 있는 멀티 플랫폼 솔루션을 제공합니다. Termix는 SSH 터미널 접속, 원격 데스크톱 제어(RDP, VNC, Telnet), SSH 터널링 기능, 원격 SSH 파일 관리 및 기타 다양한 도구를 제공합니다. Termix는 모든 플랫폼에서 사용 가능한 Termius의 완벽한 무료 셀프 호스팅 대안입니다.
# 기능
- **SSH 터미널 접속** - 브라우저 스타일 탭 시스템과 분할 화면 지원(최대 4개 패널)을 갖춘 완전한 기능의 터미널. 일반 터미널 테마, 글꼴 및 기타 구성 요소를 포함한 터미널 사용자 정의 지원
- **SSH 터널 관리** - 자동 재연결 및 상태 모니터링 기능을 갖춘 SSH 터널 생성 및 관리, -l 또는 -r 연결 지원
- **원격 파일 관리** - 코드, 이미지, 오디오, 비디오의 보기 및 편집을 지원하여 원격 서버에서 파일을 직접 관리. sudo 지원으로 파일 업로드, 다운로드, 이름 변경, 삭제, 이동을 원활하게 수행
- **Docker 관리** - 컨테이너 시작, 중지, 일시 정지, 제거. 컨테이너 통계 보기. docker exec 터미널로 컨테이너 제어. Portainer나 Dockge를 대체하기 위한 것이 아니라 컨테이너 생성보다는 간편한 관리를 목적으로 합니다
- **SSH 호스트 관리** - 태그와 폴더로 SSH 연결을 저장, 정리, 관리하고, 재사용 가능한 로그인 정보를 쉽게 저장하면서 SSH 키 배포를 자동화
- **서버 통계** - 대부분의 Linux 기반 서버에서 CPU, 메모리, 디스크 사용량과 함께 네트워크, 업타임, 시스템 정보, 방화벽, 포트 모니터를 표시
- **대시보드** - 대시보드에서 서버 정보를 한눈에 확인
- **RBAC** - 역할을 생성하고 사용자/역할 간에 호스트 공유
- **사용자 인증** - 관리자 제어와 OIDC 및 2FA(TOTP) 지원을 통한 안전한 사용자 관리. 모든 플랫폼에서 활성 사용자 세션을 보고 권한을 취소 가능. OIDC/로컬 계정 연동
- **데이터베이스 암호화** - 백엔드가 암호화된 SQLite 데이터베이스 파일로 저장됨. 자세한 내용은 [문서](https://docs.termix.site/security)를 참조하세요
- **데이터 내보내기/가져오기** - SSH 호스트, 자격 증명, 파일 관리자 데이터의 내보내기 및 가져오기
- **자동 SSL 설정** - HTTPS 리디렉션을 포함한 내장 SSL 인증서 생성 및 관리
- **모던 UI** - React, Tailwind CSS, Shadcn으로 구축된 깔끔한 데스크톱/모바일 친화적 인터페이스. 다크 또는 라이트 모드 기반 UI 선택. URL 라우트를 사용하여 모든 연결을 전체 화면으로 열기 가능
- **다국어 지원** - 약 30개 언어 내장 지원([Crowdin](https://docs.termix.site/translations)으로 관리)
- **플랫폼 지원** - 웹 앱, 데스크톱 애플리케이션(Windows, Linux, macOS), PWA, iOS 및 Android 전용 모바일/태블릿 앱으로 제공
- **SSH 도구** - 한 번의 클릭으로 실행 가능한 재사용 가능 명령어 스니펫 생성. 여러 열린 터미널에서 동시에 하나의 명령어 실행
- **명령어 기록** - 이전에 실행한 SSH 명령어의 자동 완성 및 조회
- **빠른 연결** - 연결 데이터를 저장하지 않고 서버에 접속
- **명령어 팔레트** - 왼쪽 Shift 키를 두 번 눌러 키보드로 SSH 연결에 빠르게 접근
- **풍부한 SSH 기능** - 점프 호스트, Warpgate, TOTP 기반 연결, SOCKS5, 호스트 키 검증, 비밀번호 자동 입력, [OPKSSH](https://github.com/openpubkey/opkssh) 등 지원
- **네트워크 그래프** - 대시보드를 사용자 정의하여 SSH 연결 기반의 홈랩 네트워크를 상태 표시와 함께 시각화
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **SSH 터미널 접속** - 브라우저 스타일 탭 시스템과 분할 화면 지원(최대 4개 패널)을 갖춘 완전한 기능의 터미널. 일반 터미널 테마, 글꼴 및 기타 구성 요소를 포함한 터미널 사용자 정의 지원.
- **원격 데스크톱 접속** - 완전한 사용자 정의와 분할 화면을 지원하는 브라우저 기반 RDP, VNC, Telnet 지원.
- **SSH 터널 관리** - 자동 재연결 및 상태 모니터링 기능을 갖춘 SSH 터널 생성 및 관리, -l 또는 -r 연결 지원.
- **원격 파일 관리** - 코드, 이미지, 오디오, 비디오의 보기 및 편집을 지원하여 원격 서버에서 파일을 직접 관리. sudo 지원으로 파일 업로드, 다운로드, 이름 변경, 삭제, 이동을 원활하게 수행.
- **Docker 관리** - 컨테이너 시작, 중지, 일시 정지, 제거. 컨테이너 통계 보기. docker exec 터미널로 컨테이너 제어. Portainer나 Dockge를 대체하기 위한 것이 아니라 컨테이너 생성보다는 간편한 관리를 목적으로 합니다.
- **SSH 호스트 관리자** - 태그와 폴더로 SSH 연결을 저장, 정리, 관리하고, 재사용 가능한 로그인 정보를 쉽게 저장하면서 SSH 키 배포를 자동화.
- **서버 통계** - 대부분의 Linux 기반 서버에서 CPU, 메모리, 디스크 사용량과 함께 네트워크, 업타임, 시스템 정보, 방화벽, 포트 모니터를 표시.
- **대시보드** - 대시보드에서 서버 정보를 한눈에 확인.
- **RBAC** - 역할을 생성하고 사용자/역할 간에 호스트 공유.
- **사용자 인증** - 관리자 제어와 OIDC(액세스 제어 포함) 및 2FA(TOTP) 지원을 통한 안전한 사용자 관리. 모든 플랫폼에서 활성 사용자 세션을 보고 권한을 취소 가능. OIDC/로컬 계정 연동.
- **데이터베이스 암호화** - 백엔드가 암호화된 SQLite 데이터베이스 파일로 저장됨. 자세한 내용은 [문서](https://docs.termix.site/security)를 참조하세요.
- **데이터 내보내기/가져오기** - SSH 호스트, 자격 증명, 파일 관리자 데이터의 내보내기 및 가져오기.
- **자동 SSL 설정** - HTTPS 리디렉션을 포함한 내장 SSL 인증서 생성 및 관리.
- **모던 UI** - React, Tailwind CSS, Shadcn으로 구축된 깔끔한 데스크톱/모바일 친화적 인터페이스. 라이트, 다크, 드라큘라 등 다양한 UI 테마 선택 가능. URL 라우트를 사용하여 모든 연결을 전체 화면으로 열기 가능.
- **다국어 지원** - 약 30개 언어 내장 지원([Crowdin](https://docs.termix.site/translations)으로 관리).
- **플랫폼 지원** - 웹 앱, 데스크톱 애플리케이션(Windows, Linux, macOS, Termix 백엔드 없이 독립 실행 가능), PWA, iOS 및 Android 전용 모바일/태블릿 앱으로 제공.
- **SSH 도구** - 한 번의 클릭으로 실행 가능한 재사용 가능 명령어 스니펫 생성. 여러 열린 터미널에서 동시에 하나의 명령어 실행.
- **명령어 기록** - 이전에 실행한 SSH 명령어의 자동 완성 및 조회.
- **빠른 연결** - 연결 데이터를 저장하지 않고 서버에 접속.
- **명령어 팔레트** - 왼쪽 Shift 키를 두 번 눌러 키보드로 SSH 연결에 빠르게 접근.
- **풍부한 SSH 기능** - 점프 호스트, Warpgate, TOTP 기반 연결, SOCKS5, 호스트 키 검증, 비밀번호 자동 입력, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, 포트 노킹 등 지원.
- **네트워크 그래프** - 대시보드를 사용자 정의하여 SSH 연결 기반의 홈랩 네트워크를 상태 표시와 함께 시각화.
- **지속 탭** - 사용자 프로필에서 활성화된 경우 SSH 세션 및 탭이 기기/새로 고침 간에 열린 상태 유지.
# 계획된 기능
@@ -101,7 +102,7 @@ Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버
- Google Play Store
- APK
모든 플랫폼에 Termix를 설치하는 방법에 대한 자세한 내용은 Termix [문서](https://docs.termix.site/install)를 방문하세요. 다음은 Docker Compose 파일 예시입니다:
모든 플랫폼에 Termix를 설치하는 방법에 대한 자세한 내용은 Termix [문서](https://docs.termix.site/install)를 방문하세요. 다음은 Docker Compose 파일 예시입니다(원격 데스크톱 기능을 사용할 계획이 없다면 guacd와 네트워크를 생략할 수 있습니다):
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# 스폰서
@@ -133,11 +151,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -147,7 +177,7 @@ Termix에 대한 도움이 필요하거나 기능을 요청하려면 [Issues](ht
# 스크린샷
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
+53 -23
View File
@@ -44,11 +44,12 @@ Se desejar, você pode apoiar o projeto aqui!\
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código aberto, sempre gratuita e auto-hospedada. Ela fornece uma solução multiplataforma para gerenciar seus servidores e infraestrutura através de uma interface única e intuitiva. Termix oferece acesso a terminal SSH, capacidades de tunelamento SSH, gerenciamento remoto de arquivos e muitas outras ferramentas. Termix é a alternativa perfeita, gratuita e auto-hospedada ao Termius, disponível para todas as plataformas.
Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código aberto, sempre gratuita e auto-hospedada. Ela fornece uma solução multiplataforma para gerenciar seus servidores e infraestrutura através de uma interface única e intuitiva. Termix oferece acesso a terminal SSH, controle de desktop remoto (RDP, VNC, Telnet), capacidades de tunelamento SSH, gerenciamento remoto de arquivos SSH e muitas outras ferramentas. Termix é a alternativa perfeita, gratuita e auto-hospedada ao Termius, disponível para todas as plataformas.
# Funcionalidades
- **Acesso ao Terminal SSH** - Terminal completo com suporte a tela dividida (até 4 painéis) com um sistema de abas similar ao navegador. Inclui suporte para personalização do terminal incluindo temas comuns de terminal, fontes e outros componentes
- **Acesso ao Terminal SSH** - Terminal completo com suporte a tela dividida (até 4 painéis) com um sistema de abas similar ao navegador. Inclui suporte para personalização do terminal incluindo temas comuns de terminal, fontes e outros componentes.
- **Acesso à Área de Trabalho Remota** - Suporte a RDP, VNC e Telnet pelo navegador com personalização completa e tela dividida
- **Gerenciamento de Túneis SSH** - Crie e gerencie túneis SSH com reconexão automática e monitoramento de saúde, com suporte para conexões -l ou -r
- **Gerenciador Remoto de Arquivos** - Gerencie arquivos diretamente em servidores remotos com suporte para visualizar e editar código, imagens, áudio e vídeo. Faça upload, download, renomeie, exclua e mova arquivos facilmente com suporte sudo.
- **Gerenciamento de Docker** - Inicie, pare, pause, remova contêineres. Visualize estatísticas de contêineres. Controle contêineres usando o terminal Docker Exec. Não foi feito para substituir Portainer ou Dockge, mas sim para simplesmente gerenciar seus contêineres em vez de criá-los.
@@ -56,20 +57,20 @@ Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código a
- **Estatísticas do Servidor** - Visualize o uso de CPU, memória e disco junto com rede, tempo de atividade, informações do sistema, firewall, monitor de portas na maioria dos servidores baseados em Linux
- **Dashboard** - Visualize informações do servidor de relance no seu dashboard
- **RBAC** - Crie funções e compartilhe hosts entre usuários/funções
- **Autenticação de Usuários** - Gerenciamento seguro de usuários com controles de administrador e suporte para OIDC e 2FA (TOTP). Visualize sessões ativas de usuários em todas as plataformas e revogue permissões. Vincule suas contas OIDC/Locais entre si.
- **Autenticação de Usuários** - Gerenciamento seguro de usuários com controles de administrador e suporte para OIDC (com controle de acesso) e 2FA (TOTP). Visualize sessões ativas de usuários em todas as plataformas e revogue permissões. Vincule suas contas OIDC/Locais entre si.
- **Criptografia de Banco de Dados** - Backend armazenado como arquivos de banco de dados SQLite criptografados. Consulte a [documentação](https://docs.termix.site/security) para mais informações.
- **Exportação/Importação de Dados** - Exporte e importe hosts SSH, credenciais e dados do gerenciador de arquivos
- **Configuração Automática de SSL** - Geração e gerenciamento integrado de certificados SSL com redirecionamentos HTTPS
- **Interface Moderna** - Interface limpa compatível com desktop/mobile construída com React, Tailwind CSS e Shadcn. Escolha entre modo escuro ou claro. Use rotas de URL para abrir qualquer conexão em tela cheia.
- **Interface Moderna** - Interface limpa compatível com desktop/mobile construída com React, Tailwind CSS e Shadcn. Escolha entre muitos temas de interface diferentes, incluindo claro, escuro, Drácula, etc. Use rotas de URL para abrir qualquer conexão em tela cheia.
- **Idiomas** - Suporte integrado para ~30 idiomas (gerenciado pelo [Crowdin](https://docs.termix.site/translations))
- **Suporte a Plataformas** - Disponível como aplicação web, aplicação desktop (Windows, Linux e macOS), PWA e aplicativo dedicado para celular/tablet para iOS e Android.
- **Suporte a Plataformas** - Disponível como aplicação web, aplicação desktop (Windows, Linux e macOS, pode ser executado de forma independente sem o backend Termix), PWA e aplicativo dedicado para celular/tablet para iOS e Android.
- **Ferramentas SSH** - Crie trechos de comandos reutilizáveis que são executados com um único clique. Execute um comando simultaneamente em múltiplos terminais abertos.
- **Histórico de Comandos** - Autocompletar e visualizar comandos SSH executados anteriormente
- **Conexão Rápida** - Conecte-se a um servidor sem precisar salvar os dados de conexão
- **Paleta de Comandos** - Pressione duas vezes a tecla Shift esquerda para acessar rapidamente as conexões SSH com seu teclado
- **SSH Rico em Funcionalidades** - Suporta jump hosts, Warpgate, conexões baseadas em TOTP, SOCKS5, verificação de chave do host, preenchimento automático de senhas, [OPKSSH](https://github.com/openpubkey/opkssh), etc.
- **SSH Rico em Funcionalidades** - Suporta jump hosts, Warpgate, conexões baseadas em TOTP, SOCKS5, verificação de chave do host, preenchimento automático de senhas, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, etc.
- **Gráfico de Rede** - Personalize seu Dashboard para visualizar seu homelab baseado nas suas conexões SSH com suporte de status
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **Abas Persistentes** - Sessões SSH e abas permanecem abertas entre dispositivos/atualizações se habilitado no perfil do usuário
# Funcionalidades Planejadas
@@ -101,7 +102,7 @@ Dispositivos suportados:
- Google Play Store
- APK
Visite a [documentação](https://docs.termix.site/install) do Termix para mais informações sobre como instalar o Termix em todas as plataformas. Caso contrário, veja um arquivo Docker Compose de exemplo aqui:
Visite a [documentação](https://docs.termix.site/install) do Termix para mais informações sobre como instalar o Termix em todas as plataformas. Caso contrário, veja um arquivo Docker Compose de exemplo aqui (você pode omitir o guacd e a rede se não planeja usar recursos de área de trabalho remota):
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# Patrocinadores
@@ -133,11 +151,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -148,36 +178,36 @@ Por favor, seja o mais detalhado possível no seu relato, preferencialmente escr
# Capturas de Tela
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
<img src="../repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image 2.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
<img src="../repo-images/Image 3.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image 4.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
<img src="../repo-images/Image 5.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image 6.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
<img src="../repo-images/Image 7.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image 8.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
<img src="../repo-images/Image 9.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image 10.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
<img src="../repo-images/Image 11.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image 12.png" width="400" alt="Termix Demo 12"/>
</p>
Alguns vídeos e imagens podem estar desatualizados ou podem não mostrar perfeitamente as funcionalidades.
+56 -26
View File
@@ -44,32 +44,33 @@
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix — это платформа для управления серверами с открытым исходным кодом, навсегда бесплатная и размещаемая на собственном сервере. Она предоставляет мультиплатформенное решение для управления вашими серверами и инфраструктурой через единый интуитивно понятный интерфейс. Termix предлагает доступ к SSH-терминалу, возможности SSH-туннелирования, удалённое управление файлами и множество других инструментов. Termix — это идеальная бесплатная альтернатива Termius с возможностью размещения на собственном сервере, доступная для всех платформ.
Termix — это платформа для управления серверами с открытым исходным кодом, навсегда бесплатная и размещаемая на собственном сервере. Она предоставляет мультиплатформенное решение для управления вашими серверами и инфраструктурой через единый интуитивно понятный интерфейс. Termix предлагает доступ к SSH-терминалу, управление удаленным рабочим столом (RDP, VNC, Telnet), возможности SSH-туннелирования, удаленное управление файлами SSH и множество других инструментов. Termix — это идеальная бесплатная альтернатива Termius с возможностью размещения на собственном сервере, доступная для всех платформ.
# Возможности
- **Доступ к SSH-терминалу** — Полнофункциональный терминал с поддержкой разделения экрана (до 4 панелей) и системой вкладок, как в браузере. Включает поддержку настройки терминала, включая популярные темы, шрифты и другие компоненты
- **Доступ к SSH-терминалу** — Полнофункциональный терминал с поддержкой разделения экрана (до 4 панелей) и системой вкладок, как в браузере. Включает поддержку настройки терминала, включая популярные темы, шрифты и другие компоненты.
- **Доступ к удалённому рабочему столу** — Поддержка RDP, VNC и Telnet через браузер с полной настройкой и разделением экрана
- **Управление SSH-туннелями** — Создание и управление SSH-туннелями с автоматическим переподключением и мониторингом состояния, с поддержкой соединений -l и -r
- **Удалённый файловый менеджер** — Управление файлами непосредственно на удалённых серверах с поддержкой просмотра и редактирования кода, изображений, аудио и видео. Загрузка, скачивание, переименование, удаление и перемещение файлов с поддержкой sudo
- **Управление Docker** — Запуск, остановка, приостановка, удаление контейнеров. Просмотр статистики контейнеров. Управление контейнером через терминал docker exec. Не предназначен для замены Portainer или Dockge, а скорее для простого управления контейнерами по сравнению с их созданием
- **Удалённый файловый менеджер** — Управление файлами непосредственно на удалённых серверах с поддержкой просмотра и редактирования кода, изображений, аудио и видео. Загрузка, скачивание, переименование, удаление и перемещение файлов с поддержкой sudo.
- **Управление Docker** — Запуск, остановка, приостановка, удаление контейнеров. Просмотр статистики контейнеров. Управление контейнером через терминал docker exec. Не предназначен для замены Portainer или Dockge, а скорее для простого управления контейнерами по сравнению с их созданием.
- **Менеджер SSH-хостов** — Сохранение, организация и управление SSH-подключениями с помощью тегов и папок, с возможностью сохранения данных для повторного входа и автоматизации развёртывания SSH-ключей
- **Статистика сервера** — Просмотр использования CPU, памяти и диска, а также сети, времени работы, информации о системе, файрвола и монитора портов на большинстве серверов на базе Linux
- **Панель управления** — Просмотр информации о сервере на панели управления одним взглядом
- **RBAC** — Создание ролей и предоставление общего доступа к хостам для пользователей/ролей
- **Аутентификация пользователей** — Безопасное управление пользователями с административным контролем и поддержкой OIDC и 2FA (TOTP). Просмотр активных сессий пользователей на всех платформах и отзыв прав доступа. Связывание аккаунтов OIDC/локальных аккаунтов
- **Аутентификация пользователей** — Безопасное управление пользователями с административным контролем и поддержкой OIDC (с контролем доступа) и 2FA (TOTP). Просмотр активных сессий пользователей на всех платформах и отзыв прав доступа. Связывание аккаунтов OIDC/локальных аккаунтов.
- **Шифрование базы данных** — Бэкенд хранится в виде зашифрованных файлов базы данных SQLite. Подробнее в [документации](https://docs.termix.site/security)
- **Экспорт/импорт данных** — Экспорт и импорт SSH-хостов, учётных данных и данных файлового менеджера
- **Автоматическая настройка SSL** — Встроенная генерация и управление SSL-сертификатами с перенаправлением на HTTPS
- **Современный интерфейс** — Чистый интерфейс для десктопа и мобильных устройств, построенный на React, Tailwind CSS и Shadcn. Выбор между тёмной и светлой темой. Использование URL-маршрутов для открытия любого подключения в полноэкранном режиме
- **Современный интерфейс** — Чистый интерфейс для десктопа и мобильных устройств, построенный на React, Tailwind CSS и Shadcn. Выбор между множеством различных тем интерфейса, включая светлую, тёмную, Dracula и т. д. Использование URL-маршрутов для открытия любого подключения в полноэкранном режиме.
- **Языки** — Встроенная поддержка ~30 языков (управляется через [Crowdin](https://docs.termix.site/translations))
- **Поддержка платформ** — Доступен как веб-приложение, настольное приложение (Windows, Linux и macOS), PWA и специализированное мобильное/планшетное приложение для iOS и Android
- **Инструменты SSH** — Создание переиспользуемых фрагментов команд, выполняемых одним нажатием. Запуск одной команды одновременно в нескольких открытых терминалах
- **Поддержка платформ** — Доступен как веб-приложение, настольное приложение (Windows, Linux и macOS, может работать автономно без бэкенда Termix), PWA и специализированное мобильное/планшетное приложение для iOS и Android.
- **Инструменты SSH** — Создание переиспользуемых фрагментов команд, выполняемых одним нажатием. Запуск одной команды одновременно в нескольких открытых терминалах.
- **История команд** — Автодополнение и просмотр ранее выполненных SSH-команд
- **Быстрое подключение** — Подключение к серверу без необходимости сохранения данных подключения
- **Командная палитра** — Двойное нажатие левого Shift для быстрого доступа к SSH-подключениям с клавиатуры
- **Богатый функционал SSH** — Поддержка jump-хостов, Warpgate, подключений на основе TOTP, SOCKS5, верификации ключей хоста, автозаполнения паролей, [OPKSSH](https://github.com/openpubkey/opkssh) и др.
- **Богатый функционал SSH** — Поддержка jump-хостов, Warpgate, подключений на основе TOTP, SOCKS5, верификации ключей хоста, автозаполнения паролей, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking и др.
- **Сетевой граф** — Настройте панель управления для визуализации вашей домашней лаборатории на основе SSH-подключений с поддержкой статусов
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **Постоянные вкладки** SSH-сессии и вкладки остаются открытыми на всех устройствах/при обновлении страницы, если включено в профиле пользователя
# Запланированные функции
@@ -101,7 +102,7 @@ Termix — это платформа для управления сервера
- Google Play Store
- APK
Посетите [документацию](https://docs.termix.site/install) Termix для получения дополнительной информации об установке Termix на всех платформах. Также вы можете ознакомиться с примером файла Docker Compose здесь:
Посетите [документацию](https://docs.termix.site/install) Termix для получения дополнительной информации об установке Termix на всех платформах. Также вы можете ознакомиться с примером файла Docker Compose здесь (вы можете опустить guacd и сеть, если не планируете использовать функции удаленного рабочего стола):
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# Спонсоры
@@ -133,11 +151,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -148,36 +178,36 @@ volumes:
# Скриншоты
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
<img src="../repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image 2.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
<img src="../repo-images/Image 3.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image 4.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
<img src="../repo-images/Image 5.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image 6.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
<img src="../repo-images/Image 7.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image 8.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
<img src="../repo-images/Image 9.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image 10.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
<img src="../repo-images/Image 11.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image 12.png" width="400" alt="Termix Demo 12"/>
</p>
Некоторые видео и изображения могут быть устаревшими или не полностью отражать функциональность.
+53 -23
View File
@@ -44,11 +44,12 @@ Projeyi desteklemek isterseniz, buradan destek olabilirsiniz!\
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındırabileceğiniz hepsi bir arada sunucu yönetim platformudur. Sunucularınızı ve altyapınızı tek bir sezgisel arayüz üzerinden yönetmek için çok platformlu bir çözüm sunar. Termix, SSH terminal erişimi, SSH tünelleme yetenekleri, uzak dosya yönetimi ve daha birçok araç sağlar. Termix, tüm platformlarda kullanılabilen Termius'un mükemmel ücretsiz ve kendi barındırmalı alternatifidir.
Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındırabileceğiniz hepsi bir arada sunucu yönetim platformudur. Sunucularınızı ve altyapınızı tek bir sezgisel arayüz üzerinden yönetmek için çok platformlu bir çözüm sunar. Termix, SSH terminal erişimi, uzak masaüstü kontrolü (RDP, VNC, Telnet), SSH tünelleme yetenekleri, uzak SSH dosya yönetimi ve daha birçok araç sağlar. Termix, tüm platformlarda kullanılabilen Termius'un mükemmel ücretsiz ve kendi barındırmalı alternatifidir.
# Özellikler
- **SSH Terminal Erişimi** - Tarayıcı benzeri sekme sistemiyle bölünmüş ekran desteğine sahip (4 panele kadar) tam özellikli terminal. Yaygın terminal temaları, yazı tipleri ve diğer bileşenler dahil olmak üzere terminal özelleştirme desteği içerir
- **SSH Terminal Erişimi** - Tarayıcı benzeri sekme sistemiyle bölünmüş ekran desteğine sahip (4 panele kadar) tam özellikli terminal. Yaygın terminal temaları, yazı tipleri ve diğer bileşenler dahil olmak üzere terminal özelleştirme desteği içerir.
- **Uzak Masaüstü Erişimi** - Tam özelleştirme ve bölünmüş ekran ile tarayıcı üzerinden RDP, VNC ve Telnet desteği
- **SSH Tünel Yönetimi** - Otomatik yeniden bağlanma ve sağlık izleme ile SSH tünelleri oluşturun ve yönetin, -l veya -r bağlantıları desteğiyle
- **Uzak Dosya Yöneticisi** - Uzak sunuculardaki dosyaları doğrudan yönetin; kod, görüntü, ses ve video görüntüleme ve düzenleme desteğiyle. Sudo desteğiyle dosyaları sorunsuzca yükleyin, indirin, yeniden adlandırın, silin ve taşıyın.
- **Docker Yönetimi** - Konteynerleri başlatın, durdurun, duraklatın, kaldırın. Konteyner istatistiklerini görüntüleyin. Docker exec terminali kullanarak konteyneri kontrol edin. Portainer veya Dockge'nin yerini almak için değil, konteynerlerinizi oluşturmak yerine basitçe yönetmek için tasarlanmıştır.
@@ -56,20 +57,20 @@ Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındıra
- **Sunucu İstatistikleri** - Çoğu Linux tabanlı sunucularda CPU, bellek ve disk kullanımını ağ, çalışma süresi, sistem bilgisi, güvenlik duvarı, port izleme ile birlikte görüntüleyin
- **Kontrol Paneli** - Kontrol panelinizde sunucu bilgilerini bir bakışta görüntüleyin
- **RBAC** - Roller oluşturun ve ana bilgisayarları kullanıcılar/roller arasında paylaşın
- **Kullanıcı Kimlik Doğrulama** - Yönetici kontrolleri, OIDC ve 2FA (TOTP) desteğiyle güvenli kullanıcı yönetimi. Tüm platformlardaki aktif kullanıcı oturumlarını görüntüleyin ve izinleri iptal edin. OIDC/Yerel hesaplarınızı birbirine bağlayın.
- **Kullanıcı Kimlik Doğrulama** - Yönetici kontrolleri, OIDC (erişim kontrollü) ve 2FA (TOTP) desteğiyle güvenli kullanıcı yönetimi. Tüm platformlardaki aktif kullanıcı oturumlarını görüntüleyin ve izinleri iptal edin. OIDC/Yerel hesaplarınızı birbirine bağlayın.
- **Veritabanı Şifreleme** - Arka uç, şifrelenmiş SQLite veritabanı dosyaları olarak depolanır. Daha fazla bilgi için [belgelere](https://docs.termix.site/security) bakın.
- **Veri Dışa/İçe Aktarma** - SSH ana bilgisayarlarını, kimlik bilgilerini ve dosya yöneticisi verilerini dışa ve içe aktarın
- **Otomatik SSL Kurulumu** - HTTPS yönlendirmeleriyle yerleşik SSL sertifika oluşturma ve yönetimi
- **Modern Arayüz** - React, Tailwind CSS ve Shadcn ile oluşturulmuş temiz masaüstü/mobil uyumlu arayüz. Karanlık veya açık tema arasında seçim yapın. Herhangi bir bağlantıyı tam ekranda açmak için URL yollarını kullanın.
- **Modern Arayüz** - React, Tailwind CSS ve Shadcn ile oluşturulmuş temiz masaüstü/mobil uyumlu arayüz. Işık, karanlık, Dracula vb. dahil olmak üzere birçok farklı UI teması arasından seçim yapın. Herhangi bir bağlantıyı tam ekranda açmak için URL yollarını kullanın.
- **Diller** - ~30 dil için yerleşik destek ([Crowdin](https://docs.termix.site/translations) tarafından yönetilir)
- **Platform Desteği** - Web uygulaması, masaüstü uygulaması (Windows, Linux ve macOS), PWA ve iOS ile Android için özel mobil/tablet uygulaması olarak kullanılabilir.
- **Platform Desteği** - Web uygulaması, masaüstü uygulaması (Windows, Linux ve macOS, Termix arka ucu olmadan tek başına çalıştırılabilir), PWA ve iOS ile Android için özel mobil/tablet uygulaması olarak kullanılabilir.
- **SSH Araçları** - Tek tıklamayla çalıştırılan yeniden kullanılabilir komut parçacıkları oluşturun. Birden fazla açık terminalde aynı anda tek bir komut çalıştırın.
- **Komut Geçmişi** - Daha önce çalıştırılan SSH komutlarını otomatik tamamlayın ve görüntüleyin
- **Hızlı Bağlantı** - Bağlantı verilerini kaydetmeden bir sunucuya bağlanın
- **Komut Paleti** - Sol shift tuşuna iki kez basarak SSH bağlantılarına klavyenizle hızlıca erişin
- **SSH Zengin Özellikler** - Atlama ana bilgisayarları, Warpgate, TOTP tabanlı bağlantılar, SOCKS5, ana bilgisayar anahtar doğrulama, otomatik şifre doldurma, [OPKSSH](https://github.com/openpubkey/opkssh) vb. destekler.
- **SSH Zengin Özellikler** - Atlama ana bilgisayarları, Warpgate, TOTP tabanlı bağlantılar, SOCKS5, ana bilgisayar anahtar doğrulama, otomatik şifre doldurma, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking vb. destekler.
- **Ağ Grafiği** - Kontrol panelinizi, SSH bağlantılarınıza dayalı olarak ev laboratuvarınızı durum desteğiyle görselleştirmek için özelleştirin
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **Kalıcı Sekmeler** - Kullanıcı profilinde etkinleştirilmişse SSH oturumları ve sekmeler cihazlar/yenilemeler arasında açık kalır
# Planlanan Özellikler
@@ -101,7 +102,7 @@ Desteklenen Cihazlar:
- Google Play Store
- APK
Termix'i tüm platformlara nasıl kuracağınız hakkında daha fazla bilgi için Termix [Belgelerine](https://docs.termix.site/install) bakın. Aksi takdirde, örnek bir Docker Compose dosyasını burada görüntüleyin:
Termix'i tüm platformlara nasıl kuracağınız hakkında daha fazla bilgi için Termix [Belgelerine](https://docs.termix.site/install) bakın. Aksi takdirde, örnek bir Docker Compose dosyasını burada görüntüleyin (uzak masaüstü özelliklerini kullanmayı planlamıyorsanız guacd'yi ve ağı çıkarabilirsiniz):
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# Sponsorlar
@@ -133,11 +151,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -148,36 +178,36 @@ Lütfen sorununuzu mümkün olduğunca ayrıntılı yazın, tercihen İngilizce
# Ekran Görüntüleri
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
<img src="../repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image 2.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
<img src="../repo-images/Image 3.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image 4.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
<img src="../repo-images/Image 5.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image 6.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
<img src="../repo-images/Image 7.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image 8.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
<img src="../repo-images/Image 9.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image 10.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
<img src="../repo-images/Image 11.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image 12.png" width="400" alt="Termix Demo 12"/>
</p>
Bazı videolar ve görseller güncel olmayabilir veya özellikleri tam olarak yansıtmayabilir.
+53 -23
View File
@@ -44,11 +44,12 @@ Nếu bạn muốn, bạn có thể hỗ trợ dự án tại đây!\
<img alt="Termix Banner" src=../public/icon.svg style="width: 250px; height: 250px;"> </a>
</p>
Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồn mở, miễn phí vĩnh viễn, tự lưu trữ. Nó cung cấp giải pháp đa nền tảng để quản lý máy chủ và cơ sở hạ tầng của bạn thông qua một giao diện trực quan duy nhất. Termix cung cấp quyền truy cập terminal SSH, khả năng tạo đường hầm SSH, quản lý tệp từ xa và nhiều công cụ khác. Termix là giải pháp thay thế miễn phí và tự lưu trữ hoàn hảo cho Termius, khả dụng trên tất cả các nền tảng.
Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồn mở, miễn phí vĩnh viễn, tự lưu trữ. Nó cung cấp giải pháp đa nền tảng để quản lý máy chủ và cơ sở hạ tầng của bạn thông qua một giao diện trực quan duy nhất. Termix cung cấp quyền truy cập terminal SSH, điều khiển máy tính từ xa (RDP, VNC, Telnet), khả năng tạo đường hầm SSH, quản lý tệp SSH từ xa và nhiều công cụ khác. Termix là giải pháp thay thế miễn phí và tự lưu trữ hoàn hảo cho Termius, khả dụng trên tất cả các nền tảng.
# Tính Năng
- **Truy Cập Terminal SSH** - Terminal đầy đủ tính năng với hỗ trợ chia màn hình (lên đến 4 bảng) với hệ thống tab kiểu trình duyệt. Bao gồm hỗ trợ tùy chỉnh terminal bao gồm các chủ đề terminal phổ biến, phông chữ và các thành phần khác
- **Truy Cập Terminal SSH** - Terminal đầy đủ tính năng với hỗ trợ chia màn hình (lên đến 4 bảng) với hệ thống tab kiểu trình duyệt. Bao gồm hỗ trợ tùy chỉnh terminal bao gồm các chủ đề terminal phổ biến, phông chữ và các thành phần khác.
- **Truy Cập Màn Hình Từ Xa** - Hỗ trợ RDP, VNC và Telnet qua trình duyệt với đầy đủ tùy chỉnh và chia màn hình
- **Quản Lý Đường Hầm SSH** - Tạo và quản lý đường hầm SSH với tự động kết nối lại và giám sát sức khỏe, hỗ trợ kết nối -l hoặc -r
- **Trình Quản Lý Tệp Từ Xa** - Quản lý tệp trực tiếp trên máy chủ từ xa với hỗ trợ xem và chỉnh sửa mã, hình ảnh, âm thanh và video. Tải lên, tải xuống, đổi tên, xóa và di chuyển tệp liền mạch với hỗ trợ sudo.
- **Quản Lý Docker** - Khởi động, dừng, tạm dừng, xóa container. Xem thống kê container. Điều khiển container bằng terminal docker exec. Không được tạo ra để thay thế Portainer hay Dockge mà đơn giản là để quản lý container của bạn thay vì tạo mới chúng.
@@ -56,20 +57,20 @@ Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồ
- **Thống Kê Máy Chủ** - Xem mức sử dụng CPU, bộ nhớ và ổ đĩa cùng với mạng, thời gian hoạt động, thông tin hệ thống, tường lửa, giám sát cổng trên hầu hết các máy chủ chạy Linux
- **Bảng Điều Khiển** - Xem thông tin máy chủ trong nháy mắt trên bảng điều khiển của bạn
- **RBAC** - Tạo vai trò và chia sẻ máy chủ giữa người dùng/vai trò
- **Xác Thực Người Dùng** - Quản lý người dùng an toàn với quyền quản trị và hỗ trợ OIDC và 2FA (TOTP). Xem phiên hoạt động của người dùng trên tất cả các nền tảng và thu hồi quyền. Liên kết tài khoản OIDC/Nội bộ của bạn với nhau.
- **Xác Thực Người Dùng** - Quản lý người dùng an toàn với quyền quản trị và hỗ trợ OIDC (có kiểm soát truy cập) và 2FA (TOTP). Xem phiên hoạt động của người dùng trên tất cả các nền tảng và thu hồi quyền. Liên kết tài khoản OIDC/Nội bộ của bạn với nhau.
- **Mã Hóa Cơ Sở Dữ Liệu** - Backend được lưu trữ dưới dạng tệp cơ sở dữ liệu SQLite được mã hóa. Xem [tài liệu](https://docs.termix.site/security) để biết thêm.
- **Xuất/Nhập Dữ Liệu** - Xuất và nhập máy chủ SSH, thông tin xác thực và dữ liệu trình quản lý tệp
- **Thiết Lập SSL Tự Động** - Tạo và quản lý chứng chỉ SSL tích hợp với chuyển hướng HTTPS
- **Giao Diện Hiện Đại** - Giao diện sạch sẽ, thân thiện với máy tính/di động được xây dựng bằng React, Tailwind CSS và Shadcn. Chọn giữa giao diện chế độ tối hoặc sáng. Sử dụng đường dẫn URL để mở bất kỳ kết nối nào ở chế độ toàn màn hình.
- **Giao Diện Hiện Đại** - Giao diện sạch sẽ, thân thiện với máy tính/di động được xây dựng bằng React, Tailwind CSS và Shadcn. Chọn giữa nhiều ch đề UI khác nhau bao gồm sáng, tối, Dracula, v.v. Sử dụng đường dẫn URL để mở bất kỳ kết nối nào ở chế độ toàn màn hình.
- **Ngôn Ngữ** - Hỗ trợ tích hợp ~30 ngôn ngữ (được quản lý bởi [Crowdin](https://docs.termix.site/translations))
- **Hỗ Trợ Nền Tảng** - Khả dụng dưới dạng ứng dụng web, ứng dụng máy tính (Windows, Linux và macOS), PWA và ứng dụng di động/máy tính bảng chuyên dụng cho iOS và Android.
- **Hỗ Trợ Nền Tảng** - Khả dụng dưới dạng ứng dụng web, ứng dụng máy tính (Windows, Linux và macOS, có thể chạy độc lập mà không cần backend Termix), PWA và ứng dụng di động/máy tính bảng chuyên dụng cho iOS và Android.
- **Công Cụ SSH** - Tạo đoạn lệnh có thể tái sử dụng, thực thi chỉ với một cú nhấp chuột. Chạy một lệnh đồng thời trên nhiều terminal đang mở.
- **Lịch Sử Lệnh** - Tự động hoàn thành và xem các lệnh SSH đã chạy trước đó
- **Kết Nối Nhanh** - Kết nối đến máy chủ mà không cần lưu dữ liệu kết nối
- **Bảng Lệnh** - Nhấn đúp phím shift trái để truy cập nhanh các kết nối SSH bằng bàn phím
- **SSH Giàu Tính Năng** - Hỗ trợ jump host, Warpgate, kết nối dựa trên TOTP, SOCKS5, xác minh khóa máy chủ, tự động điền mật khẩu, [OPKSSH](https://github.com/openpubkey/opkssh), v.v.
- **SSH Giàu Tính Năng** - Hỗ trợ jump host, Warpgate, kết nối dựa trên TOTP, SOCKS5, xác minh khóa máy chủ, tự động điền mật khẩu, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, v.v.
- **Biểu Đồ Mạng** - Tùy chỉnh Bảng Điều Khiển để trực quan hóa homelab của bạn dựa trên các kết nối SSH với hỗ trợ trạng thái
- **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile
- **Tab Liên Tục** - Các phiên SSH và tab vẫn mở trên các thiết bị/lần làm mới nếu được bật trong hồ sơ người dùng
# Tính Năng Dự Kiến
@@ -101,7 +102,7 @@ Thiết Bị Được Hỗ Trợ:
- Google Play Store
- APK
Truy cập [Tài Liệu](https://docs.termix.site/install) Termix để biết thêm thông tin về cách cài đặt Termix trên tất cả các nền tảng. Ngoài ra, xem tệp Docker Compose mẫu tại đây:
Truy cập [Tài Liệu](https://docs.termix.site/install) Termix để biết thêm thông tin về cách cài đặt Termix trên tất cả các nền tảng. Ngoài ra, xem tệp Docker Compose mẫu tại đây (bạn có thể bỏ qua guacd và mạng nếu không có ý định sử dụng các tính năng điều khiển máy tính từ xa):
```yaml
services:
@@ -115,10 +116,27 @@ services:
- termix-data:/app/data
environment:
PORT: "8080"
depends_on:
- guacd
networks:
- termix-net
guacd:
image: guacamole/guacd:latest
container_name: guacd
restart: unless-stopped
ports:
- "4822:4822"
networks:
- termix-net
volumes:
termix-data:
driver: local
networks:
termix-net:
driver: bridge
```
# Nhà Tài Trợ
@@ -133,11 +151,23 @@ volumes:
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.blacksmith.sh/">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Crowdin">
<img src="https://cdn.prod.website-files.com/681bfb0c9a4601bc6e288ec4/683ca9e2c5186757092611b8_e8cb22127df4da0811c4120a523722d2_logo-backsmith-wordmark-light.svg" height="50" alt="Blacksmith">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.cloudflare.com/">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Crowdin">
<img src="https://sirv.sirv.com/website/screenshots/cloudflare/cloudflare-logo.png?w=300" height="50" alt="Cloudflare">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://tailscale.com/">
<img src="https://drive.google.com/uc?export=view&id=1lIxkJuX6M23bW-2FElhT0rQieTrzaVSL" height="50" alt="TailScale">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://akamai.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/Akamai_logo.svg" height="50" alt="Akamai">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://aws.amazon.com/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/960px-Amazon_Web_Services_Logo.svg.png" height="50" alt="AWS">
</a>
</p>
@@ -148,36 +178,36 @@ Vui lòng mô tả vấn đề càng chi tiết càng tốt, ưu tiên viết b
# Ảnh Chụp Màn Hình
[![YouTube](../repo-images/YouTube.jpg)](https://youtu.be/sjKIqfCK0NY)
[![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)
<p align="center">
<img src="../repo-images/Image%201.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image%202.png" width="400" alt="Termix Demo 2"/>
<img src="../repo-images/Image 1.png" width="400" alt="Termix Demo 1"/>
<img src="../repo-images/Image 2.png" width="400" alt="Termix Demo 2"/>
</p>
<p align="center">
<img src="../repo-images/Image%203.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image%204.png" width="400" alt="Termix Demo 4"/>
<img src="../repo-images/Image 3.png" width="400" alt="Termix Demo 3"/>
<img src="../repo-images/Image 4.png" width="400" alt="Termix Demo 4"/>
</p>
<p align="center">
<img src="../repo-images/Image%205.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image%206.png" width="400" alt="Termix Demo 6"/>
<img src="../repo-images/Image 5.png" width="400" alt="Termix Demo 5"/>
<img src="../repo-images/Image 6.png" width="400" alt="Termix Demo 6"/>
</p>
<p align="center">
<img src="../repo-images/Image%207.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image%208.png" width="400" alt="Termix Demo 8"/>
<img src="../repo-images/Image 7.png" width="400" alt="Termix Demo 7"/>
<img src="../repo-images/Image 8.png" width="400" alt="Termix Demo 8"/>
</p>
<p align="center">
<img src="../repo-images/Image%209.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image%2010.png" width="400" alt="Termix Demo 10"/>
<img src="../repo-images/Image 9.png" width="400" alt="Termix Demo 9"/>
<img src="../repo-images/Image 10.png" width="400" alt="Termix Demo 10"/>
</p>
<p align="center">
<img src="../repo-images/Image%2011.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image%2012.png" width="400" alt="Termix Demo 12"/>
<img src="../repo-images/Image 11.png" width="400" alt="Termix Demo 11"/>
<img src="../repo-images/Image 12.png" width="400" alt="Termix Demo 12"/>
</p>
Một số video và hình ảnh có thể đã lỗi thời hoặc không thể hiện chính xác hoàn toàn các tính năng.
+51 -51
View File
@@ -1,14 +1,14 @@
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import { createCorsMiddleware } from "./utils/cors-config.js";
import { getDb, DatabaseSaveTrigger } from "./database/db/index.js";
import {
recentActivity,
sshData,
hosts,
hostAccess,
dashboardPreferences,
} from "./database/db/schema.js";
import { eq, and, desc, sql } from "drizzle-orm";
import { eq, and, desc, sql, inArray } from "drizzle-orm";
import { dashboardLogger } from "./utils/logger.js";
import { SimpleDBOps } from "./utils/simple-db-ops.js";
import { AuthManager } from "./utils/auth-manager.js";
@@ -22,37 +22,7 @@ const serverStartTime = Date.now();
const activityRateLimiter = new Map<string, number>();
const RATE_LIMIT_MS = 1000;
app.use(
cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
if (origin.startsWith("https://")) {
return callback(null, true);
}
if (origin.startsWith("http://")) {
return callback(null, true);
}
callback(new Error("Not allowed by CORS"));
},
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: [
"Content-Type",
"Authorization",
"User-Agent",
"X-Electron-App",
],
}),
);
app.use(createCorsMiddleware());
app.use(cookieParser());
app.use(express.json({ limit: "1mb" }));
app.use((_req, res, next) => {
@@ -176,7 +146,7 @@ app.get("/activity/recent", async (req, res) => {
* properties:
* type:
* type: string
* enum: [terminal, file_manager, server_stats, tunnel, docker]
* enum: [terminal, file_manager, server_stats, tunnel, docker, telnet, vnc, rdp]
* hostId:
* type: integer
* hostName:
@@ -219,11 +189,14 @@ app.post("/activity/log", async (req, res) => {
"server_stats",
"tunnel",
"docker",
"telnet",
"vnc",
"rdp",
].includes(type)
) {
return res.status(400).json({
error:
"Invalid activity type. Must be 'terminal', 'file_manager', 'server_stats', 'tunnel', or 'docker'",
"Invalid activity type. Must be 'terminal', 'file_manager', 'server_stats', 'tunnel', 'docker', 'telnet', 'vnc', or 'rdp'",
});
}
@@ -252,8 +225,8 @@ app.post("/activity/log", async (req, res) => {
const ownedHosts = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
.from(hosts)
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
"ssh_data",
userId,
);
@@ -285,21 +258,48 @@ app.post("/activity/log", async (req, res) => {
userId,
)) as unknown as { id: number };
const allActivities = await SimpleDBOps.select(
getDb()
.select()
.from(recentActivity)
.where(eq(recentActivity.userId, userId))
.orderBy(desc(recentActivity.timestamp)),
"recent_activity",
userId,
);
// Best-effort trim of old activity entries; failures here should not
// cause the primary /activity/log request to 500.
try {
const allActivities = await SimpleDBOps.select<{
id: number;
timestamp: string;
}>(
getDb()
.select({
id: recentActivity.id,
timestamp: recentActivity.timestamp,
})
.from(recentActivity)
.where(eq(recentActivity.userId, userId))
.orderBy(desc(recentActivity.timestamp)),
"recent_activity",
userId,
);
if (allActivities.length > 100) {
const toDelete = allActivities.slice(100);
for (let i = 0; i < toDelete.length; i++) {
await SimpleDBOps.delete(recentActivity, "recent_activity", userId);
if (allActivities.length > 100) {
const idsToDelete = allActivities
.slice(100)
.map((a) => a.id)
.filter((id) => typeof id === "number");
if (idsToDelete.length > 0) {
await SimpleDBOps.delete(
recentActivity,
"recent_activity",
and(
eq(recentActivity.userId, userId),
inArray(recentActivity.id, idsToDelete),
),
);
}
}
} catch (trimErr) {
dashboardLogger.warn("Failed to trim recent_activity (non-fatal)", {
operation: "trim_recent_activity",
userId,
error: trimErr instanceof Error ? trimErr.message : String(trimErr),
});
}
res.json({ message: "Activity logged", id: result.id });
+84 -110
View File
@@ -1,16 +1,18 @@
import express from "express";
import http from "http";
import bodyParser from "body-parser";
import multer from "multer";
import cookieParser from "cookie-parser";
import userRoutes from "./routes/users.js";
import sshRoutes from "./routes/ssh.js";
import hostRoutes from "./routes/host.js";
import alertRoutes from "./routes/alerts.js";
import credentialsRoutes from "./routes/credentials.js";
import snippetsRoutes from "./routes/snippets.js";
import terminalRoutes from "./routes/terminal.js";
import guacamoleRoutes from "../guacamole/routes.js";
import networkTopologyRoutes from "./routes/network-topology.js";
import rbacRoutes from "./routes/rbac.js";
import cors from "cors";
import { createCorsMiddleware } from "../utils/cors-config.js";
import fetch from "node-fetch";
import fs from "fs";
import path from "path";
@@ -28,7 +30,7 @@ import { parseUserAgent } from "../utils/user-agent-parser.js";
import { getProxyAgent } from "../utils/proxy-agent.js";
import {
users,
sshData,
hosts,
sshCredentials,
fileManagerRecent,
fileManagerPinned,
@@ -57,39 +59,7 @@ app.set("trust proxy", true);
const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware();
const requireAdmin = authManager.createAdminMiddleware();
app.use(
cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
if (origin.startsWith("https://")) {
return callback(null, true);
}
if (origin.startsWith("http://")) {
return callback(null, true);
}
callback(new Error("Not allowed by CORS"));
},
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: [
"Content-Type",
"Authorization",
"User-Agent",
"X-Electron-App",
"Accept",
"Origin",
],
}),
);
app.use(createCorsMiddleware());
const storage = multer.diskStorage({
destination: (req, file, cb) => {
@@ -306,6 +276,10 @@ app.get("/version", authenticateJWT, async (req, res) => {
return res.status(404).send("Local Version Not Set");
}
if (req.query.checkRemote === "false") {
return res.json({ localVersion, status: "update_check_disabled" });
}
try {
const cacheKey = "latest_release";
const releaseData = await fetchGitHubAPI<GitHubRelease>(
@@ -602,7 +576,6 @@ app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => {
app.post("/database/export", authenticateJWT, async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body;
const deviceInfo = parseUserAgent(req);
const user = await getDb().select().from(users).where(eq(users.id, userId));
@@ -612,30 +585,20 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
const isOidcUser = !!user[0].isOidc;
if (!isOidcUser) {
if (!password) {
return res.status(400).json({
error: "Password required for export",
code: "PASSWORD_REQUIRED",
});
}
const unlocked = await authManager.authenticateUser(
userId,
password,
deviceInfo.type,
);
if (!unlocked) {
return res.status(401).json({ error: "Invalid password" });
}
} else if (!DataCrypto.getUserDataKey(userId)) {
const oidcUnlocked = await authManager.authenticateOIDCUser(
userId,
deviceInfo.type,
);
if (!oidcUnlocked) {
if (!DataCrypto.getUserDataKey(userId)) {
if (isOidcUser) {
const oidcUnlocked = await authManager.authenticateOIDCUser(
userId,
deviceInfo.type,
);
if (!oidcUnlocked) {
return res.status(403).json({
error: "Failed to unlock user data with SSO credentials",
});
}
} else {
return res.status(403).json({
error: "Failed to unlock user data with SSO credentials",
error: "User data is locked. Please log in again.",
});
}
}
@@ -650,10 +613,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
throw new Error("User data not unlocked");
}
const tempDir =
process.env.NODE_ENV === "production"
? path.join(process.env.DATA_DIR || "./db/data", ".temp", "exports")
: path.join(os.tmpdir(), "termix-exports");
const tempDir = path.join(os.tmpdir(), "termix-exports");
try {
if (!fs.existsSync(tempDir)) {
@@ -709,6 +669,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
CREATE TABLE ssh_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
connection_type TEXT NOT NULL DEFAULT 'ssh',
name TEXT,
ip TEXT NOT NULL,
port INTEGER NOT NULL,
@@ -741,6 +702,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0,
default_path TEXT,
stats_config TEXT,
docker_config TEXT,
terminal_config TEXT,
quick_actions TEXT,
notes TEXT,
@@ -750,6 +712,12 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
socks5_username TEXT,
socks5_password TEXT,
socks5_proxy_chain TEXT,
domain TEXT,
security TEXT,
ignore_cert INTEGER NOT NULL DEFAULT 0,
guacamole_config TEXT,
mac_address TEXT,
port_knock_sequence TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -762,7 +730,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
folder TEXT,
tags TEXT,
auth_type TEXT NOT NULL,
username TEXT NOT NULL,
username TEXT,
password TEXT,
key TEXT,
private_key TEXT,
@@ -846,11 +814,11 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
const sshHosts = await getDb()
.select()
.from(sshData)
.where(eq(sshData.userId, userId));
.from(hosts)
.where(eq(hosts.userId, userId));
const insertHost = exportDb.prepare(`
INSERT INTO ssh_data (id, user_id, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, stats_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO ssh_data (id, user_id, connection_type, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, stats_config, docker_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, domain, security, ignore_cert, guacamole_config, mac_address, port_knock_sequence, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const host of sshHosts) {
@@ -863,6 +831,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
insertHost.run(
decrypted.id,
decrypted.userId,
decrypted.connectionType || "ssh",
decrypted.name || null,
decrypted.ip,
decrypted.port,
@@ -895,6 +864,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.showServerStatsInSidebar ? 1 : 0,
decrypted.defaultPath || null,
decrypted.statsConfig || null,
decrypted.dockerConfig || null,
decrypted.terminalConfig || null,
decrypted.quickActions || null,
decrypted.notes || null,
@@ -904,6 +874,12 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.socks5Username || null,
decrypted.socks5Password || null,
decrypted.socks5ProxyChain || null,
decrypted.domain || null,
decrypted.security || null,
decrypted.ignoreCert ? 1 : 0,
decrypted.guacamoleConfig || null,
decrypted.macAddress || null,
decrypted.portKnockSequence || null,
decrypted.createdAt,
decrypted.updatedAt,
);
@@ -1145,7 +1121,6 @@ app.post(
}
const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body;
const mainDb = getDb();
const deviceInfo = parseUserAgent(req);
@@ -1160,30 +1135,20 @@ app.post(
const isOidcUser = !!userRecords[0].isOidc;
if (!isOidcUser) {
if (!password) {
return res.status(400).json({
error: "Password required for import",
code: "PASSWORD_REQUIRED",
});
}
const unlocked = await authManager.authenticateUser(
userId,
password,
deviceInfo.type,
);
if (!unlocked) {
return res.status(401).json({ error: "Invalid password" });
}
} else if (!DataCrypto.getUserDataKey(userId)) {
const oidcUnlocked = await authManager.authenticateOIDCUser(
userId,
deviceInfo.type,
);
if (!oidcUnlocked) {
if (!DataCrypto.getUserDataKey(userId)) {
if (isOidcUser) {
const oidcUnlocked = await authManager.authenticateOIDCUser(
userId,
deviceInfo.type,
);
if (!oidcUnlocked) {
return res.status(403).json({
error: "Failed to unlock user data with SSO credentials",
});
}
} else {
return res.status(403).json({
error: "Failed to unlock user data with SSO credentials",
error: "User data is locked. Please log in again.",
});
}
}
@@ -1197,15 +1162,6 @@ app.post(
});
let userDataKey = DataCrypto.getUserDataKey(userId);
if (!userDataKey && isOidcUser) {
const oidcUnlocked = await authManager.authenticateOIDCUser(
userId,
deviceInfo.type,
);
if (oidcUnlocked) {
userDataKey = DataCrypto.getUserDataKey(userId);
}
}
if (!userDataKey) {
throw new Error("User data not unlocked");
}
@@ -1267,13 +1223,13 @@ app.post(
try {
const existing = await mainDb
.select()
.from(sshData)
.from(hosts)
.where(
and(
eq(sshData.userId, userId),
eq(sshData.ip, host.ip),
eq(sshData.port, host.port),
eq(sshData.username, host.username),
eq(hosts.userId, userId),
eq(hosts.ip, host.ip),
eq(hosts.port, host.port),
eq(hosts.username, host.username),
),
);
@@ -1341,7 +1297,7 @@ app.post(
userId,
userDataKey,
);
await mainDb.insert(sshData).values(encrypted);
await mainDb.insert(hosts).values(encrypted);
result.summary.sshHostsImported++;
} catch (hostError) {
result.summary.errors.push(
@@ -1759,11 +1715,12 @@ app.post("/database/restore", requireAdmin, async (req, res) => {
});
app.use("/users", userRoutes);
app.use("/ssh", sshRoutes);
app.use("/host", hostRoutes);
app.use("/alerts", alertRoutes);
app.use("/credentials", credentialsRoutes);
app.use("/snippets", snippetsRoutes);
app.use("/terminal", terminalRoutes);
app.use("/guacamole", guacamoleRoutes);
app.use("/network-topology", networkTopologyRoutes);
app.use("/rbac", rbacRoutes);
@@ -1980,7 +1937,24 @@ app.get(
},
);
app.listen(HTTP_PORT, async () => {
const httpServer = http.createServer(app);
httpServer.on("error", (err: NodeJS.ErrnoException) => {
if (err.code === "EADDRINUSE") {
databaseLogger.error(
`Port ${HTTP_PORT} is already in use. Kill the existing process and retry.`,
err,
{
operation: "http_server_port_conflict",
port: HTTP_PORT,
},
);
process.exit(1);
}
throw err;
});
httpServer.listen(HTTP_PORT, async () => {
const uploadsDir = path.join(process.cwd(), "uploads");
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
+112 -4
View File
@@ -427,9 +427,7 @@ async function initializeCompleteDatabase(): Promise<void> {
`);
try {
sqlite
.prepare("DELETE FROM sessions WHERE expires_at < datetime('now')")
.run();
sqlite.prepare("DELETE FROM sessions").run();
} catch (e) {
databaseLogger.warn("Could not clear expired sessions on startup", {
operation: "db_init_session_cleanup_failed",
@@ -474,6 +472,42 @@ async function initializeCompleteDatabase(): Promise<void> {
error: e,
});
}
try {
const row = sqlite
.prepare("SELECT value FROM settings WHERE key = 'guac_enabled'")
.get();
if (!row) {
sqlite
.prepare(
"INSERT INTO settings (key, value) VALUES ('guac_enabled', 'true')",
)
.run();
}
} catch (e) {
databaseLogger.warn("Could not initialize guac_enabled setting", {
operation: "db_init",
error: e,
});
}
try {
const row = sqlite
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
.get();
if (!row) {
sqlite
.prepare(
"INSERT INTO settings (key, value) VALUES ('guac_url', 'guacd:4822')",
)
.run();
}
} catch (e) {
databaseLogger.warn("Could not initialize guac_url setting", {
operation: "db_init",
error: e,
});
}
}
const addColumnIfNotExists = (
@@ -591,6 +625,11 @@ const migrateSchema = () => {
);
addColumnIfNotExists("ssh_data", "docker_config", "TEXT");
addColumnIfNotExists("ssh_data", "connection_type", 'TEXT NOT NULL DEFAULT "ssh"');
addColumnIfNotExists("ssh_data", "domain", "TEXT");
addColumnIfNotExists("ssh_data", "security", "TEXT");
addColumnIfNotExists("ssh_data", "ignore_cert", "INTEGER NOT NULL DEFAULT 0");
addColumnIfNotExists("ssh_data", "guacamole_config", "TEXT");
addColumnIfNotExists("ssh_data", "notes", "TEXT");
addColumnIfNotExists("ssh_data", "use_socks5", "INTEGER");
@@ -736,6 +775,34 @@ const migrateSchema = () => {
}
}
try {
sqlite.prepare("SELECT id FROM snippet_access LIMIT 1").get();
} catch {
try {
sqlite.exec(`
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 (snippet_id) REFERENCES snippets (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE
);
`);
} catch (createError) {
databaseLogger.warn("Failed to create snippet_access table", {
operation: "schema_migration",
error: createError,
});
}
}
try {
sqlite
.prepare("SELECT id FROM sessions LIMIT 1")
@@ -894,6 +961,47 @@ const migrateSchema = () => {
}
}
const sshDataMigrations: Array<{ column: string; sql: string }> = [
{ column: "connection_type", sql: "ALTER TABLE ssh_data ADD COLUMN connection_type TEXT NOT NULL DEFAULT 'ssh'" },
{ column: "credential_id", sql: "ALTER TABLE ssh_data ADD COLUMN credential_id INTEGER" },
{ column: "override_credential_username", sql: "ALTER TABLE ssh_data ADD COLUMN override_credential_username INTEGER" },
{ column: "jump_hosts", sql: "ALTER TABLE ssh_data ADD COLUMN jump_hosts TEXT" },
{ column: "show_terminal_in_sidebar", sql: "ALTER TABLE ssh_data ADD COLUMN show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1" },
{ column: "show_file_manager_in_sidebar", sql: "ALTER TABLE ssh_data ADD COLUMN show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0" },
{ column: "show_tunnel_in_sidebar", sql: "ALTER TABLE ssh_data ADD COLUMN show_tunnel_in_sidebar INTEGER NOT NULL DEFAULT 0" },
{ column: "show_docker_in_sidebar", sql: "ALTER TABLE ssh_data ADD COLUMN show_docker_in_sidebar INTEGER NOT NULL DEFAULT 0" },
{ column: "show_server_stats_in_sidebar", sql: "ALTER TABLE ssh_data ADD COLUMN show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0" },
{ column: "quick_actions", sql: "ALTER TABLE ssh_data ADD COLUMN quick_actions TEXT" },
{ column: "domain", sql: "ALTER TABLE ssh_data ADD COLUMN domain TEXT" },
{ column: "security", sql: "ALTER TABLE ssh_data ADD COLUMN security TEXT" },
{ column: "ignore_cert", sql: "ALTER TABLE ssh_data ADD COLUMN ignore_cert INTEGER NOT NULL DEFAULT 0" },
{ column: "guacamole_config", sql: "ALTER TABLE ssh_data ADD COLUMN guacamole_config TEXT" },
{ column: "socks5_proxy_chain", sql: "ALTER TABLE ssh_data ADD COLUMN socks5_proxy_chain TEXT" },
{ column: "host_key_fingerprint", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_fingerprint TEXT" },
{ column: "host_key_type", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_type TEXT" },
{ column: "host_key_algorithm", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_algorithm TEXT NOT NULL DEFAULT 'sha256'" },
{ column: "host_key_first_seen", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_first_seen TEXT" },
{ column: "host_key_last_verified", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_last_verified TEXT" },
{ column: "host_key_changed_count", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_changed_count INTEGER NOT NULL DEFAULT 0" },
{ column: "mac_address", sql: "ALTER TABLE ssh_data ADD COLUMN mac_address TEXT" },
{ column: "port_knock_sequence", sql: "ALTER TABLE ssh_data ADD COLUMN port_knock_sequence TEXT" },
];
for (const migration of sshDataMigrations) {
try {
sqlite.prepare(`SELECT ${migration.column} FROM ssh_data LIMIT 1`).get();
} catch {
try {
sqlite.exec(migration.sql);
} catch (alterError) {
databaseLogger.warn(`Failed to add ${migration.column} column`, {
operation: "schema_migration",
error: alterError,
});
}
}
}
try {
sqlite.prepare("SELECT id FROM roles LIMIT 1").get();
} catch {
@@ -1177,7 +1285,7 @@ const migrateSchema = () => {
});
};
async function saveMemoryDatabaseToFile() {
async function saveMemoryDatabaseToFile(): Promise<void> {
if (!memoryDatabase) return;
try {
+43 -10
View File
@@ -64,11 +64,12 @@ export const trustedDevices = sqliteTable("trusted_devices", {
.default(sql`CURRENT_TIMESTAMP`),
});
export const sshData = sqliteTable("ssh_data", {
export const hosts = sqliteTable("ssh_data", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
connectionType: text("connection_type").notNull().default("ssh"),
name: text("name"),
ip: text("ip").notNull(),
port: integer("port").notNull(),
@@ -124,9 +125,14 @@ export const sshData = sqliteTable("ssh_data", {
.default(false),
defaultPath: text("default_path"),
statsConfig: text("stats_config"),
dockerConfig: text("docker_config"),
terminalConfig: text("terminal_config"),
quickActions: text("quick_actions"),
notes: text("notes"),
domain: text("domain"),
security: text("security"),
ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false),
guacamoleConfig: text("guacamole_config"),
useSocks5: integer("use_socks5", { mode: "boolean" }),
socks5Host: text("socks5_host"),
@@ -135,6 +141,9 @@ export const sshData = sqliteTable("ssh_data", {
socks5Password: text("socks5_password"),
socks5ProxyChain: text("socks5_proxy_chain"),
macAddress: text("mac_address"),
portKnockSequence: text("port_knock_sequence"),
hostKeyFingerprint: text("host_key_fingerprint"),
hostKeyType: text("host_key_type"),
hostKeyAlgorithm: text("host_key_algorithm").default("sha256"),
@@ -157,7 +166,7 @@ export const fileManagerRecent = sqliteTable("file_manager_recent", {
.references(() => users.id, { onDelete: "cascade" }),
hostId: integer("host_id")
.notNull()
.references(() => sshData.id, { onDelete: "cascade" }),
.references(() => hosts.id, { onDelete: "cascade" }),
name: text("name").notNull(),
path: text("path").notNull(),
lastOpened: text("last_opened")
@@ -172,7 +181,7 @@ export const fileManagerPinned = sqliteTable("file_manager_pinned", {
.references(() => users.id, { onDelete: "cascade" }),
hostId: integer("host_id")
.notNull()
.references(() => sshData.id, { onDelete: "cascade" }),
.references(() => hosts.id, { onDelete: "cascade" }),
name: text("name").notNull(),
path: text("path").notNull(),
pinnedAt: text("pinned_at")
@@ -187,7 +196,7 @@ export const fileManagerShortcuts = sqliteTable("file_manager_shortcuts", {
.references(() => users.id, { onDelete: "cascade" }),
hostId: integer("host_id")
.notNull()
.references(() => sshData.id, { onDelete: "cascade" }),
.references(() => hosts.id, { onDelete: "cascade" }),
name: text("name").notNull(),
path: text("path").notNull(),
createdAt: text("created_at")
@@ -246,7 +255,7 @@ export const sshCredentialUsage = sqliteTable("ssh_credential_usage", {
.references(() => sshCredentials.id, { onDelete: "cascade" }),
hostId: integer("host_id")
.notNull()
.references(() => sshData.id, { onDelete: "cascade" }),
.references(() => hosts.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
@@ -289,6 +298,30 @@ export const snippetFolders = sqliteTable("snippet_folders", {
.default(sql`CURRENT_TIMESTAMP`),
});
export const snippetAccess = sqliteTable("snippet_access", {
id: integer("id").primaryKey({ autoIncrement: true }),
snippetId: integer("snippet_id")
.notNull()
.references(() => snippets.id, { onDelete: "cascade" }),
userId: text("user_id").references(() => users.id, { onDelete: "cascade" }),
roleId: integer("role_id").references(() => roles.id, {
onDelete: "cascade",
}),
grantedBy: text("granted_by")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
permissionLevel: text("permission_level").notNull().default("view"),
expiresAt: text("expires_at"),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
export const sshFolders = sqliteTable("ssh_folders", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: text("user_id")
@@ -313,7 +346,7 @@ export const recentActivity = sqliteTable("recent_activity", {
type: text("type").notNull(),
hostId: integer("host_id")
.notNull()
.references(() => sshData.id, { onDelete: "cascade" }),
.references(() => hosts.id, { onDelete: "cascade" }),
hostName: text("host_name"),
timestamp: text("timestamp")
.notNull()
@@ -327,7 +360,7 @@ export const commandHistory = sqliteTable("command_history", {
.references(() => users.id, { onDelete: "cascade" }),
hostId: integer("host_id")
.notNull()
.references(() => sshData.id, { onDelete: "cascade" }),
.references(() => hosts.id, { onDelete: "cascade" }),
command: text("command").notNull(),
executedAt: text("executed_at")
.notNull()
@@ -367,7 +400,7 @@ export const hostAccess = sqliteTable("host_access", {
id: integer("id").primaryKey({ autoIncrement: true }),
hostId: integer("host_id")
.notNull()
.references(() => sshData.id, { onDelete: "cascade" }),
.references(() => hosts.id, { onDelete: "cascade" }),
userId: text("user_id")
.references(() => users.id, { onDelete: "cascade" }),
@@ -492,7 +525,7 @@ export const sessionRecordings = sqliteTable("session_recordings", {
hostId: integer("host_id")
.notNull()
.references(() => sshData.id, { onDelete: "cascade" }),
.references(() => hosts.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
@@ -523,7 +556,7 @@ export const opksshTokens = sqliteTable("opkssh_tokens", {
.references(() => users.id, { onDelete: "cascade" }),
hostId: integer("host_id")
.notNull()
.references(() => sshData.id, { onDelete: "cascade" }),
.references(() => hosts.id, { onDelete: "cascade" }),
sshCert: text("ssh_cert", { length: 8192 }).notNull(),
privateKey: text("private_key", { length: 8192 }).notNull(),
+17 -27
View File
@@ -7,7 +7,7 @@ import { db } from "../db/index.js";
import {
sshCredentials,
sshCredentialUsage,
sshData,
hosts,
hostAccess,
} from "../db/schema.js";
import { eq, and, desc, sql } from "drizzle-orm";
@@ -606,9 +606,8 @@ router.put(
userId,
);
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
await sharedCredManager.updateSharedCredentialsForOriginal(
parseInt(id),
@@ -691,17 +690,14 @@ router.delete(
const hostsUsingCredential = await db
.select()
.from(sshData)
.from(hosts)
.where(
and(
eq(sshData.credentialId, parseInt(id)),
eq(sshData.userId, userId),
),
and(eq(hosts.credentialId, parseInt(id)), eq(hosts.userId, userId)),
);
if (hostsUsingCredential.length > 0) {
await db
.update(sshData)
.update(hosts)
.set({
credentialId: null,
password: null,
@@ -710,10 +706,7 @@ router.delete(
authType: "password",
})
.where(
and(
eq(sshData.credentialId, parseInt(id)),
eq(sshData.userId, userId),
),
and(eq(hosts.credentialId, parseInt(id)), eq(hosts.userId, userId)),
);
for (const host of hostsUsingCredential) {
@@ -737,9 +730,8 @@ router.delete(
}
}
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
await sharedCredManager.deleteSharedCredentialsForOriginal(parseInt(id));
@@ -837,7 +829,7 @@ router.post(
const credential = credentials[0];
await db
.update(sshData)
.update(hosts)
.set({
credentialId: parseInt(credentialId),
username: (credential.username as string) || "",
@@ -848,9 +840,7 @@ router.post(
keyType: null,
updatedAt: new Date().toISOString(),
})
.where(
and(eq(sshData.id, parseInt(hostId)), eq(sshData.userId, userId)),
);
.where(and(eq(hosts.id, parseInt(hostId)), eq(hosts.userId, userId)));
await db.insert(sshCredentialUsage).values({
credentialId: parseInt(credentialId),
@@ -917,17 +907,17 @@ router.get(
}
try {
const hosts = await db
const hostsUsingCredential = await db
.select()
.from(sshData)
.from(hosts)
.where(
and(
eq(sshData.credentialId, parseInt(credentialId)),
eq(sshData.userId, userId),
eq(hosts.credentialId, parseInt(credentialId)),
eq(hosts.userId, userId),
),
);
res.json(hosts.map((host) => formatSSHHostOutput(host)));
res.json(hostsUsingCredential.map((host) => formatSSHHostOutput(host)));
} catch (err) {
authLogger.error("Failed to fetch hosts using credential", err);
res.status(500).json({
@@ -1942,7 +1932,7 @@ router.post(
});
}
const targetHost = await SimpleDBOps.select(
db.select().from(sshData).where(eq(sshData.id, targetHostId)).limit(1),
db.select().from(hosts).where(eq(hosts.id, targetHostId)).limit(1),
"ssh_data",
userId,
);
File diff suppressed because it is too large Load Diff
+390 -66
View File
@@ -3,11 +3,13 @@ import express from "express";
import { db } from "../db/index.js";
import {
hostAccess,
sshData,
hosts,
users,
roles,
userRoles,
sharedCredentials,
snippets,
snippetAccess,
} from "../db/schema.js";
import { eq, and, desc, sql, or, isNull, gte } from "drizzle-orm";
import type { Response } from "express";
@@ -111,8 +113,8 @@ router.post(
const host = await db
.select()
.from(sshData)
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId)))
.from(hosts)
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
.limit(1);
if (host.length === 0) {
@@ -201,9 +203,8 @@ router.post(
.delete(sharedCredentials)
.where(eq(sharedCredentials.hostAccessId, existing[0].id));
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
if (targetType === "user") {
await sharedCredManager.createSharedCredentialForUser(
@@ -244,9 +245,8 @@ router.post(
expiresAt,
});
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
if (targetType === "user") {
@@ -336,8 +336,8 @@ router.delete(
try {
const host = await db
.select()
.from(sshData)
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId)))
.from(hosts)
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
.limit(1);
if (host.length === 0) {
@@ -404,8 +404,8 @@ router.get(
try {
const host = await db
.select()
.from(sshData)
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId)))
.from(hosts)
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
.limit(1);
if (host.length === 0) {
@@ -484,21 +484,21 @@ router.get(
const sharedHosts = await db
.select({
id: sshData.id,
name: sshData.name,
ip: sshData.ip,
port: sshData.port,
username: sshData.username,
folder: sshData.folder,
tags: sshData.tags,
id: hosts.id,
name: hosts.name,
ip: hosts.ip,
port: hosts.port,
username: hosts.username,
folder: hosts.folder,
tags: hosts.tags,
permissionLevel: hostAccess.permissionLevel,
expiresAt: hostAccess.expiresAt,
grantedBy: hostAccess.grantedBy,
ownerUsername: users.username,
})
.from(hostAccess)
.innerJoin(sshData, eq(hostAccess.hostId, sshData.id))
.innerJoin(users, eq(sshData.userId, users.id))
.innerJoin(hosts, eq(hostAccess.hostId, hosts.id))
.innerJoin(users, eq(hosts.userId, users.id))
.where(
and(
eq(hostAccess.userId, userId),
@@ -518,46 +518,6 @@ router.get(
},
);
/**
* @openapi
* /rbac/roles:
* get:
* summary: Get all roles
* description: Retrieves a list of all roles.
* tags:
* - RBAC
* responses:
* 200:
* description: A list of roles.
* 500:
* description: Failed to get roles.
*/
router.get(
"/roles",
authenticateJWT,
permissionManager.requireAdmin(),
async (req: AuthenticatedRequest, res: Response) => {
try {
const allRoles = await db
.select()
.from(roles)
.orderBy(roles.isSystem, roles.name);
const rolesWithParsedPermissions = allRoles.map((role) => ({
...role,
permissions: JSON.parse(role.permissions),
}));
res.json({ roles: rolesWithParsedPermissions });
} catch (error) {
databaseLogger.error("Failed to get roles", error, {
operation: "get_roles",
});
res.status(500).json({ error: "Failed to get roles" });
}
},
);
/**
* @openapi
* /rbac/roles:
@@ -978,12 +938,11 @@ router.post(
const hostsSharedWithRole = await db
.select()
.from(hostAccess)
.innerJoin(sshData, eq(hostAccess.hostId, sshData.id))
.innerJoin(hosts, eq(hostAccess.hostId, hosts.id))
.where(eq(hostAccess.roleId, roleId));
const { SharedCredentialManager } = await import(
"../../utils/shared-credential-manager.js"
);
const { SharedCredentialManager } =
await import("../../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
for (const { host_access, ssh_data } of hostsSharedWithRole) {
@@ -1196,4 +1155,369 @@ router.get(
},
);
// ============================================================================
// SNIPPET SHARING
// ============================================================================
/**
* @openapi
* /rbac/snippet/{id}/share:
* post:
* summary: Share a snippet
* description: Shares a snippet with a user or role.
* tags:
* - RBAC
*/
router.post(
"/snippet/:id/share",
authenticateJWT,
async (req: AuthenticatedRequest, res: Response) => {
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
const snippetId = parseInt(id, 10);
const userId = req.userId!;
if (isNaN(snippetId)) {
return res.status(400).json({ error: "Invalid snippet ID" });
}
try {
const {
targetType = "user",
targetUserId,
targetRoleId,
durationHours,
} = req.body;
if (!["user", "role"].includes(targetType)) {
return res
.status(400)
.json({ error: "Invalid target type. Must be 'user' or 'role'" });
}
if (targetType === "user" && !isNonEmptyString(targetUserId)) {
return res
.status(400)
.json({ error: "Target user ID is required when sharing with user" });
}
if (targetType === "role" && !targetRoleId) {
return res
.status(400)
.json({ error: "Target role ID is required when sharing with role" });
}
const snippet = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, snippetId), eq(snippets.userId, userId)))
.limit(1);
if (snippet.length === 0) {
return res.status(403).json({ error: "Not snippet owner" });
}
if (targetType === "user") {
const targetUser = await db
.select({ id: users.id })
.from(users)
.where(eq(users.id, targetUserId))
.limit(1);
if (targetUser.length === 0) {
return res.status(404).json({ error: "Target user not found" });
}
} else {
const targetRole = await db
.select({ id: roles.id })
.from(roles)
.where(eq(roles.id, targetRoleId))
.limit(1);
if (targetRole.length === 0) {
return res.status(404).json({ error: "Target role not found" });
}
}
let expiresAt: string | null = null;
if (
durationHours &&
typeof durationHours === "number" &&
durationHours > 0
) {
const expiryDate = new Date();
expiryDate.setHours(expiryDate.getHours() + durationHours);
expiresAt = expiryDate.toISOString();
}
const whereConditions = [eq(snippetAccess.snippetId, snippetId)];
if (targetType === "user") {
whereConditions.push(eq(snippetAccess.userId, targetUserId));
} else {
whereConditions.push(eq(snippetAccess.roleId, targetRoleId));
}
const existing = await db
.select()
.from(snippetAccess)
.where(and(...whereConditions))
.limit(1);
if (existing.length > 0) {
await db
.update(snippetAccess)
.set({ expiresAt })
.where(eq(snippetAccess.id, existing[0].id));
return res.json({
success: true,
message: "Snippet access updated",
expiresAt,
});
}
await db.insert(snippetAccess).values({
snippetId,
userId: targetType === "user" ? targetUserId : null,
roleId: targetType === "role" ? targetRoleId : null,
grantedBy: userId,
permissionLevel: "view",
expiresAt,
});
databaseLogger.success("Snippet shared successfully", {
operation: "rbac_snippet_share",
userId,
});
res.json({
success: true,
message: `Snippet shared successfully with ${targetType}`,
expiresAt,
});
} catch (error) {
databaseLogger.error("Failed to share snippet", error, {
operation: "share_snippet",
userId,
});
res.status(500).json({ error: "Failed to share snippet" });
}
},
);
/**
* @openapi
* /rbac/snippet/{id}/access/{accessId}:
* delete:
* summary: Revoke snippet access
* description: Revokes a user's or role's access to a snippet.
* tags:
* - RBAC
*/
router.delete(
"/snippet/:id/access/:accessId",
authenticateJWT,
async (req: AuthenticatedRequest, res: Response) => {
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
const accessIdParam = Array.isArray(req.params.accessId)
? req.params.accessId[0]
: req.params.accessId;
const snippetId = parseInt(id, 10);
const accessId = parseInt(accessIdParam, 10);
const userId = req.userId!;
if (isNaN(snippetId) || isNaN(accessId)) {
return res.status(400).json({ error: "Invalid ID" });
}
try {
const snippet = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, snippetId), eq(snippets.userId, userId)))
.limit(1);
if (snippet.length === 0) {
return res.status(403).json({ error: "Not snippet owner" });
}
await db.delete(snippetAccess).where(eq(snippetAccess.id, accessId));
res.json({ success: true, message: "Snippet access revoked" });
} catch (error) {
databaseLogger.error("Failed to revoke snippet access", error, {
operation: "revoke_snippet_access",
userId,
});
res.status(500).json({ error: "Failed to revoke access" });
}
},
);
/**
* @openapi
* /rbac/snippet/{id}/access:
* get:
* summary: Get snippet access list
* description: Retrieves the list of users and roles with access to a snippet.
* tags:
* - RBAC
*/
router.get(
"/snippet/:id/access",
authenticateJWT,
async (req: AuthenticatedRequest, res: Response) => {
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
const snippetId = parseInt(id, 10);
const userId = req.userId!;
if (isNaN(snippetId)) {
return res.status(400).json({ error: "Invalid snippet ID" });
}
try {
const snippet = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, snippetId), eq(snippets.userId, userId)))
.limit(1);
if (snippet.length === 0) {
return res.status(403).json({ error: "Not snippet owner" });
}
const rawAccessList = await db
.select({
id: snippetAccess.id,
userId: snippetAccess.userId,
roleId: snippetAccess.roleId,
username: users.username,
roleName: roles.name,
roleDisplayName: roles.displayName,
grantedBy: snippetAccess.grantedBy,
grantedByUsername: sql<string>`(SELECT username FROM users WHERE id = ${snippetAccess.grantedBy})`,
permissionLevel: snippetAccess.permissionLevel,
expiresAt: snippetAccess.expiresAt,
createdAt: snippetAccess.createdAt,
})
.from(snippetAccess)
.leftJoin(users, eq(snippetAccess.userId, users.id))
.leftJoin(roles, eq(snippetAccess.roleId, roles.id))
.where(eq(snippetAccess.snippetId, snippetId))
.orderBy(desc(snippetAccess.createdAt));
const accessList = rawAccessList.map((access) => ({
id: access.id,
targetType: access.userId ? "user" : "role",
userId: access.userId,
roleId: access.roleId,
username: access.username,
roleName: access.roleName,
roleDisplayName: access.roleDisplayName,
grantedBy: access.grantedBy,
grantedByUsername: access.grantedByUsername,
permissionLevel: access.permissionLevel,
expiresAt: access.expiresAt,
createdAt: access.createdAt,
}));
res.json({ accessList });
} catch (error) {
databaseLogger.error("Failed to get snippet access list", error, {
operation: "get_snippet_access_list",
userId,
});
res.status(500).json({ error: "Failed to get access list" });
}
},
);
/**
* @openapi
* /rbac/shared-snippets:
* get:
* summary: Get shared snippets
* description: Retrieves snippets shared with the current user.
* tags:
* - RBAC
*/
router.get(
"/shared-snippets",
authenticateJWT,
async (req: AuthenticatedRequest, res: Response) => {
const userId = req.userId!;
try {
const now = new Date().toISOString();
const directShared = await db
.select({
id: snippets.id,
name: snippets.name,
content: snippets.content,
description: snippets.description,
folder: snippets.folder,
ownerUsername: users.username,
permissionLevel: snippetAccess.permissionLevel,
expiresAt: snippetAccess.expiresAt,
})
.from(snippetAccess)
.innerJoin(snippets, eq(snippetAccess.snippetId, snippets.id))
.innerJoin(users, eq(snippets.userId, users.id))
.where(
and(
eq(snippetAccess.userId, userId),
or(
isNull(snippetAccess.expiresAt),
gte(snippetAccess.expiresAt, now),
),
),
);
const userRoleRows = await db
.select({ roleId: userRoles.roleId })
.from(userRoles)
.where(eq(userRoles.userId, userId));
const roleIds = userRoleRows.map((r) => r.roleId);
let roleShared: typeof directShared = [];
if (roleIds.length > 0) {
const directIds = directShared.map((s) => s.id);
const roleResults = await db
.select({
id: snippets.id,
name: snippets.name,
content: snippets.content,
description: snippets.description,
folder: snippets.folder,
ownerUsername: users.username,
permissionLevel: snippetAccess.permissionLevel,
expiresAt: snippetAccess.expiresAt,
})
.from(snippetAccess)
.innerJoin(snippets, eq(snippetAccess.snippetId, snippets.id))
.innerJoin(users, eq(snippets.userId, users.id))
.where(
and(
or(
isNull(snippetAccess.expiresAt),
gte(snippetAccess.expiresAt, now),
),
sql`${snippetAccess.roleId} IN (${sql.join(
roleIds.map((id) => sql`${id}`),
sql`, `,
)})`,
),
);
roleShared = roleResults.filter((s) => !directIds.includes(s.id));
}
res.json({ sharedSnippets: [...directShared, ...roleShared] });
} catch (error) {
databaseLogger.error("Failed to get shared snippets", error, {
operation: "get_shared_snippets",
userId,
});
res.status(500).json({ error: "Failed to get shared snippets" });
}
},
);
export default router;
@@ -0,0 +1,33 @@
export interface SnippetReorderUpdate {
id: number;
order: number;
folder?: string;
}
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[];
}
+10 -20
View File
@@ -6,6 +6,8 @@ import { eq, and, desc, asc, sql } from "drizzle-orm";
import type { Request, Response } from "express";
import { authLogger, databaseLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import { SSH_ALGORITHMS } from "../../utils/ssh-algorithms.js";
import { extractSnippetReorderUpdates } from "./snippets-reorder.js";
const router = express.Router();
@@ -472,7 +474,8 @@ router.delete(
* /snippets/reorder:
* put:
* summary: Reorder snippets
* description: Bulk updates the order and folder of snippets.
* description: Bulk updates the order and folder of snippets. Accepts
* `snippets` and the legacy `updates` payload key.
* tags:
* - Snippets
* requestBody:
@@ -507,14 +510,14 @@ router.put(
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { snippets: snippetUpdates } = req.body;
const snippetUpdates = extractSnippetReorderUpdates(req.body);
if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for snippet reorder");
return res.status(400).json({ error: "Invalid userId" });
}
if (!Array.isArray(snippetUpdates) || snippetUpdates.length === 0) {
if (!snippetUpdates || snippetUpdates.length === 0) {
authLogger.warn("Invalid snippet reorder data", {
operation: "snippet_reorder",
userId,
@@ -632,17 +635,15 @@ router.post(
const snippet = snippetResult[0];
const { Client } = await import("ssh2");
const { sshData, sshCredentials } = await import("../db/schema.js");
const { hosts, sshCredentials } = await import("../db/schema.js");
const { SimpleDBOps } = await import("../../utils/simple-db-ops.js");
const hostResult = await SimpleDBOps.select(
db
.select()
.from(sshData)
.where(
and(eq(sshData.id, parseInt(hostId)), eq(sshData.userId, userId)),
),
.from(hosts)
.where(and(eq(hosts.id, parseInt(hostId)), eq(hosts.userId, userId))),
"ssh_data",
userId,
);
@@ -777,18 +778,7 @@ router.post(
"ssh-rsa",
"ssh-dss",
],
cipher: [
"chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
"aes128-gcm@openssh.com",
"aes256-ctr",
"aes192-ctr",
"aes128-ctr",
"aes256-cbc",
"aes192-cbc",
"aes128-cbc",
"3des-cbc",
],
cipher: SSH_ALGORITHMS.cipher,
hmac: [
"hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256-etm@openssh.com",
+27 -1
View File
@@ -62,11 +62,37 @@ router.post(
return res.status(400).json({ error: "Missing required parameters" });
}
const sensitivePatterns = [
/passw(or)?d/i,
/\bsecret\b/i,
/\btoken\b/i,
/\bapi.?key\b/i,
/PASS(WORD)?=/i,
/AWS_SECRET/i,
/mysql\b.*-p/i,
/sudo\s+-S\b/,
/htpasswd/i,
/sshpass/i,
/curl\b.*-u\s/i,
/export\b.*(?:PASSWORD|SECRET|TOKEN|KEY)=/i,
];
const trimmedCommand = command.trim();
if (sensitivePatterns.some((p: RegExp) => p.test(trimmedCommand))) {
return res.status(201).json({
id: 0,
userId,
hostId: parseInt(hostId, 10),
command: trimmedCommand,
executedAt: new Date().toISOString(),
});
}
try {
const insertData = {
userId,
hostId: parseInt(hostId, 10),
command: command.trim(),
command: trimmedCommand,
};
const result = await db
+391 -79
View File
@@ -1,11 +1,14 @@
import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express";
import { restartGuacServer } from "../../guacamole/guacamole-server.js";
import { setGlobalLogLevel, getGlobalLogLevel } from "../../utils/logger.js";
import crypto from "crypto";
import { db } from "../db/index.js";
import {
users,
sessions,
sshData,
trustedDevices,
hosts,
sshCredentials,
fileManagerRecent,
fileManagerPinned,
@@ -43,6 +46,7 @@ import {
generateDeviceFingerprint,
} from "../../utils/user-agent-parser.js";
import { loginRateLimiter } from "../../utils/login-rate-limiter.js";
import { getRequestOriginWithForceHTTPS } from "../../utils/request-origin.js";
const authManager = AuthManager.getInstance();
@@ -264,7 +268,7 @@ async function deleteUserAndRelatedData(userId: string): Promise<void> {
await db.delete(commandHistory).where(eq(commandHistory.userId, userId));
await db.delete(sshData).where(eq(sshData.userId, userId));
await db.delete(hosts).where(eq(hosts.userId, userId));
await db.delete(sshCredentials).where(eq(sshCredentials.userId, userId));
await db.delete(networkTopology).where(eq(networkTopology.userId, userId));
@@ -732,7 +736,8 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => {
.prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
.get();
if (!row) {
return res.json(null);
const envConfig = getOIDCConfigFromEnv();
return res.json(envConfig);
}
let config = JSON.parse((row as Record<string, unknown>).value as string);
@@ -788,6 +793,12 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => {
* description: Returns the OIDC authorization URL.
* tags:
* - Users
* parameters:
* - in: query
* name: rememberMe
* schema:
* type: boolean
* description: Whether to extend the session to 30 days instead of 2 hours.
* responses:
* 200:
* description: OIDC authorization URL.
@@ -798,16 +809,9 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => {
*/
router.get("/oidc/authorize", async (req, res) => {
try {
authLogger.info("OIDC authorize request headers", {
protocol: req.protocol,
host: req.get("Host"),
origin: req.get("Origin"),
referer: req.get("Referer"),
"x-forwarded-proto": req.get("X-Forwarded-Proto"),
"x-forwarded-host": req.get("X-Forwarded-Host"),
"x-forwarded-port": req.get("X-Forwarded-Port"),
secure: req.secure,
});
const { rememberMe } = req.query;
const origin = getRequestOriginWithForceHTTPS(req);
const backendCallbackUri = `${origin}/users/oidc/callback`;
const envConfig = getOIDCConfigFromEnv();
let config;
@@ -826,15 +830,6 @@ router.get("/oidc/authorize", async (req, res) => {
const state = nanoid();
const nonce = nanoid();
const protocol =
process.env.OIDC_FORCE_HTTPS === "true"
? "https"
: req.get("X-Forwarded-Proto") || req.protocol;
const host = req.get("Host");
const origin = `${protocol}://${host}`;
const backendCallbackUri = `${origin}/users/oidc/callback`;
const referer = req.get("Referer");
let frontendOrigin;
if (referer) {
@@ -856,12 +851,12 @@ router.get("/oidc/authorize", async (req, res) => {
.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
.run(`oidc_frontend_origin_${state}`, frontendOrigin);
authLogger.info("OIDC authorization initiated", {
operation: "oidc_authorize",
backendCallbackUri,
frontendOrigin,
referer,
});
db.$client
.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
.run(
`oidc_remember_me_${state}`,
rememberMe === "true" ? "true" : "false",
);
const authUrl = new URL(config.authorization_url);
authUrl.searchParams.set("client_id", config.client_id);
@@ -894,10 +889,6 @@ router.get("/oidc/authorize", async (req, res) => {
*/
router.get("/oidc/callback", async (req, res) => {
const { code, state } = req.query;
authLogger.info("OIDC login callback received", {
operation: "oidc_login_request",
state,
});
if (!isNonEmptyString(code) || !isNonEmptyString(state)) {
return res.status(400).json({ error: "Code and state are required" });
@@ -909,6 +900,9 @@ router.get("/oidc/callback", async (req, res) => {
const storedFrontendOriginRow = db.$client
.prepare("SELECT value FROM settings WHERE key = ?")
.get(`oidc_frontend_origin_${state}`);
const storedRememberMeRow = db.$client
.prepare("SELECT value FROM settings WHERE key = ?")
.get(`oidc_remember_me_${state}`);
if (!storedBackendCallbackRow || !storedFrontendOriginRow) {
return res
@@ -921,6 +915,8 @@ router.get("/oidc/callback", async (req, res) => {
).value as string;
const frontendOrigin = (storedFrontendOriginRow as Record<string, unknown>)
.value as string;
const storedRememberMe =
(storedRememberMeRow as Record<string, unknown> | null)?.value === "true";
try {
const storedNonce = db.$client
@@ -947,12 +943,6 @@ router.get("/oidc/callback", async (req, res) => {
);
}
authLogger.info("OIDC token exchange attempt", {
operation: "oidc_token_exchange",
backendCallbackUri,
frontendOrigin,
});
const tokenResponse = await fetch(config.token_url, {
method: "POST",
headers: {
@@ -994,6 +984,9 @@ router.get("/oidc/callback", async (req, res) => {
db.$client
.prepare("DELETE FROM settings WHERE key = ?")
.run(`oidc_frontend_origin_${state}`);
db.$client
.prepare("DELETE FROM settings WHERE key = ?")
.run(`oidc_remember_me_${state}`);
let userInfo: Record<string, unknown> = null;
const userInfoUrls: string[] = [];
@@ -1236,7 +1229,7 @@ router.get("/oidc/callback", async (req, res) => {
const sessionDurationMs =
deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
? 30 * 24 * 60 * 60 * 1000
: 2 * 60 * 60 * 1000;
: 24 * 60 * 60 * 1000;
await authManager.registerOIDCUser(id, sessionDurationMs);
} catch (encryptionError) {
await db.delete(users).where(eq(users.id, id));
@@ -1316,6 +1309,7 @@ router.get("/oidc/callback", async (req, res) => {
const token = await authManager.generateJWTToken(userRecord.id, {
deviceType: deviceInfo.type,
deviceInfo: deviceInfo.deviceInfo,
rememberMe: storedRememberMe,
});
authLogger.success("OIDC login successful", {
@@ -1327,10 +1321,16 @@ router.get("/oidc/callback", async (req, res) => {
const redirectUrl = new URL(frontendOrigin);
redirectUrl.searchParams.set("success", "true");
if (deviceInfo.type === "desktop" || deviceInfo.type === "mobile") {
redirectUrl.searchParams.set("token", token);
}
const maxAge =
deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
? 30 * 24 * 60 * 60 * 1000
: 2 * 60 * 60 * 1000;
: storedRememberMe
? 30 * 24 * 60 * 60 * 1000
: 24 * 60 * 60 * 1000;
res.clearCookie("jwt", authManager.getClearCookieOptions(req));
@@ -1529,9 +1529,10 @@ router.post("/login", async (req, res) => {
if (userRecord.totpEnabled) {
const deviceFingerprint = generateDeviceFingerprint(deviceInfo);
const isTrusted = rememberMe
? await authManager.isTrustedDevice(userRecord.id, deviceFingerprint)
: false;
const isTrusted = await authManager.isTrustedDevice(
userRecord.id,
deviceFingerprint,
);
if (isTrusted) {
authLogger.info("TOTP bypassed for trusted device", {
@@ -1583,7 +1584,13 @@ router.post("/login", async (req, res) => {
response.token = token;
}
const maxAge = rememberMe ? 30 * 24 * 60 * 60 * 1000 : 2 * 60 * 60 * 1000;
const timeoutRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'")
.get() as { value: string } | undefined;
const timeoutHours = timeoutRow ? parseInt(timeoutRow.value, 10) || 24 : 24;
const maxAge = rememberMe
? 30 * 24 * 60 * 60 * 1000
: timeoutHours * 60 * 60 * 1000;
return res
.cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge))
@@ -2161,12 +2168,16 @@ router.post("/initiate-reset", async (req, res) => {
authLogger.warn(
`Password reset attempted for non-existent user: ${username}`,
);
return res.status(404).json({ error: "User not found" });
return res.json({
message:
"If the user exists, a password reset code has been generated. Check docker logs for the code.",
});
}
if (user[0].isOidc) {
return res.status(403).json({
error: "Password reset not available for external authentication users",
return res.json({
message:
"If the user exists, a password reset code has been generated. Check docker logs for the code.",
});
}
@@ -2181,7 +2192,7 @@ router.post("/initiate-reset", async (req, res) => {
);
authLogger.info(
`Password reset code for user ${username}: ${resetCode} (expires at ${expiresAt.toLocaleString()})`,
`Password reset code generated for user ${username} (expires at ${expiresAt.toLocaleString()}). Check admin panel or database settings table for code.`,
);
res.json({
@@ -2483,7 +2494,7 @@ router.post("/complete-reset", async (req, res) => {
.delete(dismissedAlerts)
.where(eq(dismissedAlerts.userId, userId));
await db.delete(snippets).where(eq(snippets.userId, userId));
await db.delete(sshData).where(eq(sshData.userId, userId));
await db.delete(hosts).where(eq(hosts.userId, userId));
await db
.delete(sshCredentials)
.where(eq(sshCredentials.userId, userId));
@@ -2646,13 +2657,7 @@ router.post("/change-password", authenticateJWT, async (req, res) => {
* description: Failed to list users.
*/
router.get("/list", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].isAdmin) {
return res.status(403).json({ error: "Not authorized" });
}
const allUsers = await db
.select({
id: users.id,
@@ -2685,13 +2690,17 @@ router.get("/list", authenticateJWT, async (req, res) => {
* schema:
* type: object
* properties:
* userId:
* type: string
* description: Preferred unique user identifier.
* username:
* type: string
* description: Legacy fallback identifier.
* responses:
* 200:
* description: User is now an admin.
* 400:
* description: Username is required or user is already an admin.
* description: User ID or username is required, or the user is already an admin.
* 403:
* description: Not authorized.
* 404:
@@ -2701,10 +2710,14 @@ router.get("/list", authenticateJWT, async (req, res) => {
*/
router.post("/make-admin", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
const { username } = req.body;
const { userId: targetUserId, username } = req.body;
const resolvedUserId = isNonEmptyString(targetUserId)
? targetUserId.trim()
: null;
const resolvedUsername = isNonEmptyString(username) ? username.trim() : null;
if (!isNonEmptyString(username)) {
return res.status(400).json({ error: "Username is required" });
if (!resolvedUserId && !resolvedUsername) {
return res.status(400).json({ error: "User ID or username is required" });
}
try {
@@ -2716,7 +2729,12 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
const targetUser = await db
.select()
.from(users)
.where(eq(users.username, username));
.where(
resolvedUserId
? eq(users.id, resolvedUserId)
: eq(users.username, resolvedUsername!),
)
.limit(1);
if (!targetUser || targetUser.length === 0) {
return res.status(404).json({ error: "User not found" });
}
@@ -2728,7 +2746,11 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
await db
.update(users)
.set({ isAdmin: true })
.where(eq(users.username, username));
.where(
resolvedUserId
? eq(users.id, resolvedUserId)
: eq(users.username, resolvedUsername!),
);
try {
const { saveMemoryDatabaseToFile } = await import("../db/index.js");
@@ -2736,7 +2758,8 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
} catch (saveError) {
authLogger.error("Failed to persist admin promotion to disk", saveError, {
operation: "make_admin_save_failed",
username,
userId: targetUser[0].id,
username: targetUser[0].username,
});
}
@@ -2744,9 +2767,9 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
operation: "admin_grant",
adminId: userId,
targetUserId: targetUser[0].id,
targetUsername: username,
targetUsername: targetUser[0].username,
});
res.json({ message: `User ${username} is now an admin` });
res.json({ message: `User ${targetUser[0].username} is now an admin` });
} catch (err) {
authLogger.error("Failed to make user admin", err);
res.status(500).json({ error: "Failed to make user admin" });
@@ -2768,13 +2791,17 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
* schema:
* type: object
* properties:
* userId:
* type: string
* description: Preferred unique user identifier.
* username:
* type: string
* description: Legacy fallback identifier.
* responses:
* 200:
* description: Admin status removed from user.
* 400:
* description: Username is required or cannot remove your own admin status.
* description: User ID or username is required, or cannot remove your own admin status.
* 403:
* description: Not authorized.
* 404:
@@ -2784,10 +2811,14 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
*/
router.post("/remove-admin", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
const { username } = req.body;
const { userId: targetUserId, username } = req.body;
const resolvedUserId = isNonEmptyString(targetUserId)
? targetUserId.trim()
: null;
const resolvedUsername = isNonEmptyString(username) ? username.trim() : null;
if (!isNonEmptyString(username)) {
return res.status(400).json({ error: "Username is required" });
if (!resolvedUserId && !resolvedUsername) {
return res.status(400).json({ error: "User ID or username is required" });
}
try {
@@ -2796,7 +2827,10 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => {
return res.status(403).json({ error: "Not authorized" });
}
if (adminUser[0].username === username) {
if (
(resolvedUserId && adminUser[0].id === resolvedUserId) ||
(resolvedUsername && adminUser[0].username === resolvedUsername)
) {
return res
.status(400)
.json({ error: "Cannot remove your own admin status" });
@@ -2805,7 +2839,12 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => {
const targetUser = await db
.select()
.from(users)
.where(eq(users.username, username));
.where(
resolvedUserId
? eq(users.id, resolvedUserId)
: eq(users.username, resolvedUsername!),
)
.limit(1);
if (!targetUser || targetUser.length === 0) {
return res.status(404).json({ error: "User not found" });
}
@@ -2817,7 +2856,11 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => {
await db
.update(users)
.set({ isAdmin: false })
.where(eq(users.username, username));
.where(
resolvedUserId
? eq(users.id, resolvedUserId)
: eq(users.username, resolvedUsername!),
);
try {
const { saveMemoryDatabaseToFile } = await import("../db/index.js");
@@ -2825,7 +2868,8 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => {
} catch (saveError) {
authLogger.error("Failed to persist admin removal to disk", saveError, {
operation: "remove_admin_save_failed",
username,
userId: targetUser[0].id,
username: targetUser[0].username,
});
}
@@ -2833,9 +2877,11 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => {
operation: "admin_revoke",
adminId: userId,
targetUserId: targetUser[0].id,
targetUsername: username,
targetUsername: targetUser[0].username,
});
res.json({
message: `Admin status removed from ${targetUser[0].username}`,
});
res.json({ message: `Admin status removed from ${username}` });
} catch (err) {
authLogger.error("Failed to remove admin status", err);
res.status(500).json({ error: "Failed to remove admin status" });
@@ -2972,10 +3018,19 @@ router.post("/totp/enable", authenticateJWT, async (req, res) => {
totpBackupCodes: JSON.stringify(backupCodes),
})
.where(eq(users.id, userId));
authLogger.info("Two-factor authentication enabled", {
operation: "totp_enable",
userId,
});
await db.delete(sessions).where(eq(sessions.userId, userId));
await db.delete(trustedDevices).where(eq(trustedDevices.userId, userId));
try {
const { saveMemoryDatabaseToFile } = await import("../db/index.js");
await saveMemoryDatabaseToFile();
} catch (saveError) {
authLogger.error("Failed to persist TOTP enablement to disk", saveError, {
operation: "totp_enable_db_save_failed",
userId,
});
}
res.json({
message: "TOTP enabled successfully",
@@ -3365,7 +3420,13 @@ router.post("/totp/verify-login", async (req, res) => {
response.token = token;
}
const maxAge = rememberMe ? 30 * 24 * 60 * 60 * 1000 : 2 * 60 * 60 * 1000;
const timeoutRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'")
.get() as { value: string } | undefined;
const timeoutHours = timeoutRow ? parseInt(timeoutRow.value, 10) || 24 : 24;
const maxAge = rememberMe
? 30 * 24 * 60 * 60 * 1000
: timeoutHours * 60 * 60 * 1000;
return res
.cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge))
@@ -4121,4 +4182,255 @@ router.post("/unlink-oidc-from-password", authenticateJWT, async (req, res) => {
}
});
/**
* @openapi
* /users/guacamole-settings:
* get:
* summary: Get Guacamole settings
* description: Returns current guacd enabled status and host:port URL. No authentication required.
* tags:
* - Users
* responses:
* 200:
* description: Guacamole settings.
* content:
* application/json:
* schema:
* type: object
* properties:
* enabled:
* type: boolean
* url:
* type: string
* 500:
* description: Failed to get guacamole settings.
*/
router.get("/guacamole-settings", async (req, res) => {
try {
const enabledRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'guac_enabled'")
.get() as { value: string } | undefined;
const urlRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
.get() as { value: string } | undefined;
res.json({
enabled: enabledRow ? enabledRow.value !== "false" : true,
url: urlRow ? urlRow.value : "guacd:4822",
});
} catch (err) {
authLogger.error("Failed to get guacamole settings", err);
res.status(500).json({ error: "Failed to get guacamole settings" });
}
});
/**
* @openapi
* /users/guacamole-settings:
* patch:
* summary: Update Guacamole settings
* description: Admin-only. Updates guacd enabled status and/or host:port URL.
* tags:
* - Users
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* enabled:
* type: boolean
* url:
* type: string
* responses:
* 200:
* description: Guacamole settings updated.
* 403:
* description: Not authorized.
* 500:
* description: Failed to update guacamole settings.
*/
router.patch("/guacamole-settings", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].isAdmin) {
return res.status(403).json({ error: "Not authorized" });
}
const { enabled, url } = req.body;
if (typeof enabled === "boolean") {
db.$client
.prepare(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('guac_enabled', ?)",
)
.run(enabled ? "true" : "false");
}
if (typeof url === "string") {
db.$client
.prepare(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('guac_url', ?)",
)
.run(url);
try {
await restartGuacServer();
} catch (err) {
authLogger.error("Failed to restart guac server after URL update", err);
}
}
const enabledRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'guac_enabled'")
.get() as { value: string } | undefined;
const urlRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
.get() as { value: string } | undefined;
res.json({
enabled: enabledRow ? enabledRow.value !== "false" : true,
url: urlRow ? urlRow.value : "guacd:4822",
});
} catch (err) {
authLogger.error("Failed to update guacamole settings", err);
res.status(500).json({ error: "Failed to update guacamole settings" });
}
});
/**
* @openapi
* /users/log-level:
* get:
* summary: Get log level setting
* description: Returns the configured log verbosity level.
* tags:
* - Users
* responses:
* 200:
* description: Current log level.
*/
router.get("/log-level", async (_req, res) => {
try {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'log_level'")
.get() as { value: string } | undefined;
res.json({
level: row ? row.value : getGlobalLogLevel(),
});
} catch (err) {
authLogger.error("Failed to get log level", err);
res.status(500).json({ error: "Failed to get log level" });
}
});
/**
* @openapi
* /users/log-level:
* patch:
* summary: Update log level setting (admin only)
* description: Sets the log verbosity level.
* tags:
* - Users
* responses:
* 200:
* description: Log level updated.
* 400:
* description: Invalid log level.
* 403:
* description: Not authorized.
*/
router.patch("/log-level", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].isAdmin) {
return res.status(403).json({ error: "Not authorized" });
}
const { level } = req.body;
const validLevels = ["debug", "info", "warn", "error"];
if (typeof level !== "string" || !validLevels.includes(level)) {
return res
.status(400)
.json({ error: "level must be one of: debug, info, warn, error" });
}
db.$client
.prepare(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('log_level', ?)",
)
.run(level);
setGlobalLogLevel(level);
res.json({ level });
} catch (err) {
authLogger.error("Failed to set log level", err);
res.status(500).json({ error: "Failed to set log level" });
}
});
/**
* @openapi
* /users/session-timeout:
* get:
* summary: Get session timeout setting
* description: Returns the configured session timeout in hours.
* tags:
* - Users
* responses:
* 200:
* description: Current session timeout hours.
*/
router.get("/session-timeout", async (_req, res) => {
try {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'")
.get() as { value: string } | undefined;
res.json({
timeoutHours: row ? parseInt(row.value, 10) : 24,
});
} catch (err) {
authLogger.error("Failed to get session timeout", err);
res.status(500).json({ error: "Failed to get session timeout" });
}
});
/**
* @openapi
* /users/session-timeout:
* patch:
* summary: Update session timeout setting (admin only)
* description: Sets the session timeout in hours.
* tags:
* - Users
* responses:
* 200:
* description: Session timeout updated.
* 400:
* description: Invalid value.
* 403:
* description: Not authorized.
*/
router.patch("/session-timeout", authenticateJWT, async (req, res) => {
const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].isAdmin) {
return res.status(403).json({ error: "Not authorized" });
}
const { timeoutHours } = req.body;
if (
typeof timeoutHours !== "number" ||
timeoutHours < 1 ||
timeoutHours > 720
) {
return res
.status(400)
.json({ error: "timeoutHours must be between 1 and 720" });
}
db.$client
.prepare(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('session_timeout_hours', ?)",
)
.run(String(timeoutHours));
res.json({ timeoutHours });
} catch (err) {
authLogger.error("Failed to set session timeout", err);
res.status(500).json({ error: "Failed to set session timeout" });
}
});
export default router;
+154
View File
@@ -0,0 +1,154 @@
import GuacamoleLite from "guacamole-lite";
import { parse as parseUrl } from "url";
import { guacLogger } from "../utils/logger.js";
import { AuthManager } from "../utils/auth-manager.js";
import { GuacamoleTokenService } from "./token-service.js";
import { getDb } from "../database/db/index.js";
import type { IncomingMessage } from "http";
const authManager = AuthManager.getInstance();
const tokenService = GuacamoleTokenService.getInstance();
function parseGuacUrl(url: string): { host: string; port: number } {
const parts = url.split(":");
return {
host: parts[0] || "localhost",
port: parseInt(parts[1] || "4822", 10),
};
}
function readGuacdOptions(): { host: string; port: number } {
let host = process.env.GUACD_HOST || "localhost";
let port = parseInt(process.env.GUACD_PORT || "4822", 10);
try {
const db = getDb();
const urlRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
.get() as { value: string } | undefined;
if (urlRow?.value) {
const parsed = parseGuacUrl(urlRow.value);
host = parsed.host;
port = parsed.port;
}
} catch {
// DB not available yet, use env var defaults
}
return { host, port };
}
const GUAC_WS_PORT = 30008;
const websocketOptions = {
port: GUAC_WS_PORT,
};
const clientOptions = {
crypt: {
cypher: "AES-256-CBC",
key: tokenService.getEncryptionKey(),
},
log: {
level: "ERRORS",
stdLog: (...args: unknown[]) => {
guacLogger.info(args.join(" "));
},
errorLog: (...args: unknown[]) => {
guacLogger.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,
},
telnet: {
"terminal-type": "xterm-256color",
},
},
};
const _origConsoleLog = console.log;
console.log = (...args: unknown[]) => {
const msg = args[0];
if (typeof msg === "string" && msg.startsWith("New client connection"))
return;
_origConsoleLog(...args);
};
function createGuacServer(): GuacamoleLite {
const guacdOptions = readGuacdOptions();
const server = new GuacamoleLite(
websocketOptions,
guacdOptions,
clientOptions,
);
server.on(
"open",
(clientConnection: { connectionSettings?: Record<string, unknown> }) => {
guacLogger.info("Guacamole connection opened", {
operation: "guac_connection_open",
type: clientConnection.connectionSettings?.type,
});
},
);
server.on(
"close",
(clientConnection: { connectionSettings?: Record<string, unknown> }) => {
guacLogger.info("Guacamole connection closed", {
operation: "guac_connection_close",
type: clientConnection.connectionSettings?.type,
});
},
);
server.on(
"error",
(
clientConnection: { connectionSettings?: Record<string, unknown> },
error: Error,
) => {
guacLogger.error("Guacamole connection error", error, {
operation: "guac_connection_error",
type: clientConnection.connectionSettings?.type,
});
},
);
return server;
}
let guacServer = createGuacServer();
export async function restartGuacServer(): Promise<void> {
try {
guacServer.close();
} catch (err) {
guacLogger.error("Error closing guac server during restart", err as Error);
}
guacServer = createGuacServer();
}
export { guacServer, tokenService };
+303
View File
@@ -0,0 +1,303 @@
import express from "express";
import { GuacamoleTokenService } from "./token-service.js";
import { guacLogger } from "../utils/logger.js";
import { AuthManager } from "../utils/auth-manager.js";
import { PermissionManager } from "../utils/permission-manager.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { getDb } from "../database/db/index.js";
import { hosts } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import type { AuthenticatedRequest } from "../../types/index.js";
const router = express.Router();
const tokenService = GuacamoleTokenService.getInstance();
const authManager = AuthManager.getInstance();
router.use(authManager.createAuthMiddleware());
/**
* POST /guacamole/token
* Generate an encrypted connection token for guacamole-lite
*
* Body: {
* type: "rdp" | "vnc" | "telnet",
* hostname: string,
* port?: number,
* username?: string,
* password?: string,
* domain?: string,
* // Additional protocol-specific options
* }
*/
router.post("/token", async (req, res) => {
try {
const userId = (req as AuthenticatedRequest).userId;
const { type, hostname, port, username, password, domain, ...options } =
req.body;
if (!type || !hostname) {
return res
.status(400)
.json({ error: "Missing required fields: type and hostname" });
}
if (!["rdp", "vnc", "telnet"].includes(type)) {
return res.status(400).json({
error: "Invalid connection type. Must be rdp, vnc, or telnet",
});
}
let token: string;
switch (type) {
case "rdp":
token = tokenService.createRdpToken(
hostname,
username || "",
password || "",
{
port: port || 3389,
domain,
...options,
},
);
break;
case "vnc":
token = tokenService.createVncToken(hostname, password, {
port: port || 5900,
...options,
});
break;
case "telnet":
token = tokenService.createTelnetToken(hostname, username, password, {
port: port || 23,
...options,
});
break;
default:
return res.status(400).json({ error: "Invalid connection type" });
}
res.json({ token });
} catch (error) {
guacLogger.error("Failed to generate guacamole token", error, {
operation: "guac_token_error",
});
res.status(500).json({ error: "Failed to generate connection token" });
}
});
/**
* @openapi
* /guacamole/connect-host/{hostId}:
* post:
* summary: Generate Guacamole connection token from host configuration
* description: Fetches host configuration from database and generates a connection token for RDP/VNC/Telnet
* tags:
* - Guacamole
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: hostId
* required: true
* schema:
* type: integer
* description: Host ID to connect to
* responses:
* 200:
* description: Connection token generated successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* token:
* type: string
* description: Encrypted connection token
* 400:
* description: Invalid request or unsupported connection type
* 403:
* description: Access denied to host
* 404:
* description: Host not found
* 500:
* description: Server error
*/
router.post(
"/connect-host/:hostId",
async (req: express.Request, res: express.Response) => {
try {
const userId = (req as AuthenticatedRequest).userId!;
const hostId = parseInt(req.params.hostId, 10);
if (!hostId || isNaN(hostId)) {
return res.status(400).json({ error: "Invalid host ID" });
}
const hostResults = await SimpleDBOps.select(
getDb().select().from(hosts).where(eq(hosts.id, hostId)),
"ssh_data",
userId,
);
if (hostResults.length === 0) {
return res.status(404).json({ error: "Host not found" });
}
const host = hostResults[0];
if (host.userId !== userId) {
const permissionManager = PermissionManager.getInstance();
const accessInfo = await permissionManager.canAccessHost(
userId,
hostId,
"read",
);
if (!accessInfo.hasAccess) {
guacLogger.warn("User attempted to access host without permission", {
operation: "guac_access_denied",
userId,
hostId,
});
return res.status(403).json({ error: "Access denied to this host" });
}
}
const connectionType = (host.connectionType as string) || "ssh";
if (!["rdp", "vnc", "telnet"].includes(connectionType)) {
return res.status(400).json({
error: `Connection type '${connectionType}' is not supported for remote desktop. Only RDP, VNC, and Telnet are supported.`,
});
}
let guacConfig: Record<string, unknown> = {};
if (host.guacamoleConfig) {
try {
guacConfig =
typeof host.guacamoleConfig === "string"
? JSON.parse(host.guacamoleConfig as string)
: (host.guacamoleConfig as Record<string, unknown>);
} catch (error) {
guacLogger.warn("Failed to parse guacamole config", {
operation: "guac_config_parse_error",
hostId,
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
let token: string;
const hostname = host.ip as string;
const port = host.port as number;
const username = (host.username as string) || "";
const password = (host.password as string) || "";
const domain = (host.domain as string) || "";
switch (connectionType) {
case "rdp":
token = tokenService.createRdpToken(hostname, username, password, {
port: port || 3389,
domain,
security: (host.security as string) || undefined,
"ignore-cert": (host.ignoreCert as boolean) || false,
...guacConfig,
});
break;
case "vnc":
token = tokenService.createVncToken(hostname, password, {
port: port || 5900,
...guacConfig,
});
break;
case "telnet":
token = tokenService.createTelnetToken(hostname, username, password, {
port: port || 23,
...guacConfig,
});
break;
default:
return res.status(400).json({ error: "Invalid connection type" });
}
res.json({ token });
} catch (error) {
guacLogger.error("Failed to generate guacamole token for host", error, {
operation: "guac_host_token_error",
});
res.status(500).json({ error: "Failed to generate connection token" });
}
},
);
/**
* GET /guacamole/status
* Check if guacd is reachable
*/
router.get("/status", async (req, res) => {
try {
let guacdHost = process.env.GUACD_HOST || "localhost";
let guacdPort = parseInt(process.env.GUACD_PORT || "4822", 10);
try {
const db = getDb();
const urlRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'guac_url'")
.get() as { value: string } | undefined;
if (urlRow?.value) {
const parts = urlRow.value.split(":");
guacdHost = parts[0] || guacdHost;
guacdPort = parseInt(parts[1] || String(guacdPort), 10);
}
} catch {
// Fall back to env vars
}
const net = await import("net");
const checkConnection = (): Promise<boolean> => {
return new Promise((resolve) => {
const socket = new net.Socket();
socket.setTimeout(3000);
socket.on("connect", () => {
socket.destroy();
resolve(true);
});
socket.on("timeout", () => {
socket.destroy();
resolve(false);
});
socket.on("error", () => {
socket.destroy();
resolve(false);
});
socket.connect(guacdPort, guacdHost);
});
};
const isConnected = await checkConnection();
res.json({
guacd: {
host: guacdHost,
port: guacdPort,
status: isConnected ? "connected" : "disconnected",
},
websocket: {
port: 30008,
status: "running",
},
});
} catch (error) {
guacLogger.error("Failed to check guacamole status", error, {
operation: "guac_status_error",
});
res.status(500).json({ error: "Failed to check status" });
}
});
export default router;
+181
View File
@@ -0,0 +1,181 @@
import crypto from "crypto";
import { guacLogger } from "../utils/logger.js";
export interface GuacamoleConnectionSettings {
type: "rdp" | "vnc" | "telnet";
settings: {
hostname: string;
port?: number;
username?: string;
password?: string;
domain?: string;
width?: number;
height?: number;
dpi?: number;
security?: string;
"ignore-cert"?: 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 GuacamoleToken {
connection: GuacamoleConnectionSettings;
}
const CIPHER = "aes-256-cbc";
const KEY_LENGTH = 32;
export class GuacamoleTokenService {
private static instance: GuacamoleTokenService;
private encryptionKey: Buffer;
private constructor() {
this.encryptionKey = this.initializeKey();
}
static getInstance(): GuacamoleTokenService {
if (!GuacamoleTokenService.instance) {
GuacamoleTokenService.instance = new GuacamoleTokenService();
}
return GuacamoleTokenService.instance;
}
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");
}
}
const jwtSecret = process.env.JWT_SECRET;
if (jwtSecret) {
return crypto
.createHash("sha256")
.update(jwtSecret + "_guacamole")
.digest();
}
guacLogger.warn(
"No persistent encryption key found, generating random key",
{
operation: "guac_key_generation",
},
);
return crypto.randomBytes(KEY_LENGTH);
}
getEncryptionKey(): Buffer {
return this.encryptionKey;
}
encryptToken(tokenObject: GuacamoleToken): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(CIPHER, this.encryptionKey, iv);
let encrypted = cipher.update(
JSON.stringify(tokenObject),
"utf8",
"base64",
);
encrypted += cipher.final("base64");
const data = {
iv: iv.toString("base64"),
value: encrypted,
};
return Buffer.from(JSON.stringify(data)).toString("base64");
}
decryptToken(token: string): GuacamoleToken | null {
try {
const data = JSON.parse(Buffer.from(token, "base64").toString("utf8"));
const iv = Buffer.from(data.iv, "base64");
const decipher = crypto.createDecipheriv(CIPHER, this.encryptionKey, iv);
let decrypted = decipher.update(data.value, "base64", "utf8");
decrypted += decipher.final("utf8");
return JSON.parse(decrypted) as GuacamoleToken;
} catch (error) {
guacLogger.error("Failed to decrypt guacamole token", error, {
operation: "guac_token_decrypt_error",
});
return null;
}
}
createRdpToken(
hostname: string,
username: string,
password: string,
options: Partial<GuacamoleConnectionSettings["settings"]> = {},
): string {
const token: GuacamoleToken = {
connection: {
type: "rdp",
settings: {
hostname,
username,
password,
port: 3389,
security: "nla",
"ignore-cert": true,
...options,
},
},
};
return this.encryptToken(token);
}
createVncToken(
hostname: string,
password?: string,
options: Partial<GuacamoleConnectionSettings["settings"]> = {},
): string {
const token: GuacamoleToken = {
connection: {
type: "vnc",
settings: {
hostname,
password,
port: 5900,
...options,
},
},
};
return this.encryptToken(token);
}
createTelnetToken(
hostname: string,
username?: string,
password?: string,
options: Partial<GuacamoleConnectionSettings["settings"]> = {},
): string {
const token: GuacamoleToken = {
connection: {
type: "telnet",
settings: {
hostname,
username,
password,
port: 23,
...options,
},
},
};
return this.encryptToken(token);
}
}
+122 -83
View File
@@ -2,7 +2,7 @@ import { Client as SSHClient } from "ssh2";
import { WebSocketServer, WebSocket } from "ws";
import { parse as parseUrl } from "url";
import { AuthManager } from "../utils/auth-manager.js";
import { sshData, sshCredentials } from "../database/db/schema.js";
import { hosts, sshCredentials } from "../database/db/schema.js";
import { and, eq } from "drizzle-orm";
import { getDb } from "../database/db/index.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
@@ -24,11 +24,19 @@ const activeSessions = new Map<string, SSHSession>();
const wss = new WebSocketServer({
host: "0.0.0.0",
port: 30008,
port: 30009,
verifyClient: async (info) => {
try {
const url = parseUrl(info.req.url || "", true);
const token = url.query.token as string;
let token = url.query.token as string;
if (!token) {
const cookieHeader = info.req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
}
}
if (!token) {
return false;
@@ -75,8 +83,10 @@ async function detectShell(
}
});
stream.stderr.on("data", () => {
// Ignore stderr
stream.stderr.on("data", () => {});
stream.stderr.on("error", () => {});
stream.on("error", (streamErr) => {
reject(streamErr);
});
},
);
@@ -107,8 +117,8 @@ async function createJumpHostChain(
const jumpHostData = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.where(and(eq(sshData.id, jumpHostId), eq(sshData.userId, userId))),
.from(hosts)
.where(and(eq(hosts.id, jumpHostId), eq(hosts.userId, userId))),
"ssh_data",
userId,
);
@@ -229,7 +239,30 @@ async function createJumpHostChain(
}
wss.on("connection", async (ws: WebSocket, req) => {
const userId = (req as unknown as { userId: string }).userId;
const url = parseUrl(req.url || "", true);
let token = url.query.token as string;
if (!token) {
const cookieHeader = req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
}
}
if (!token) {
ws.close(1008, "Authentication required");
return;
}
const authManagerInstance = AuthManager.getInstance();
const payload = await authManagerInstance.verifyJWTToken(token);
if (!payload || !payload.userId) {
ws.close(1008, "Authentication required");
return;
}
const userId = payload.userId;
const sessionId = `docker-console-${Date.now()}-${Math.random()}`;
sshLogger.info("Docker console WebSocket connected", {
operation: "docker_console_connect",
@@ -260,21 +293,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
rows?: number;
};
if (
typeof hostConfig.jumpHosts === "string" &&
hostConfig.jumpHosts
) {
try {
hostConfig.jumpHosts = JSON.parse(hostConfig.jumpHosts);
} catch (e) {
sshLogger.error("Failed to parse jump hosts", e, {
hostId: hostConfig.id,
});
hostConfig.jumpHosts = [];
}
}
const hostId = hostConfig?.id;
if (!hostConfig || !containerId) {
if (!hostId || !containerId) {
ws.send(
JSON.stringify({
type: "error",
@@ -284,62 +305,69 @@ wss.on("connection", async (ws: WebSocket, req) => {
return;
}
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(containerId)) {
ws.send(
JSON.stringify({
type: "error",
message: "Invalid container ID",
}),
);
return;
}
const allowedShells = ["bash", "sh", "ash", "zsh"];
if (shell && !allowedShells.includes(shell)) {
ws.send(
JSON.stringify({
type: "error",
message: "Invalid shell",
}),
);
return;
}
if (!hostConfig.enableDocker) {
ws.send(
JSON.stringify({
type: "error",
message:
"Docker is not enabled for this host. Enable it in Host Settings.",
message: "Docker is not enabled on this host",
}),
);
return;
}
try {
let resolvedCredentials: {
password?: string;
sshKey?: string;
keyPassword?: string;
authType?: string;
} = {
password: hostConfig.password,
sshKey: hostConfig.key,
keyPassword: hostConfig.keyPassword,
authType: hostConfig.authType,
};
// Resolve host with credentials server-side
const { resolveHostById } = await import("./host-resolver.js");
const resolvedHost = await resolveHostById(hostId, userId);
if (hostConfig.credentialId) {
const credentials = await SimpleDBOps.select(
getDb()
.select()
.from(sshCredentials)
.where(
and(
eq(sshCredentials.id, hostConfig.credentialId as number),
eq(sshCredentials.userId, userId),
),
),
"ssh_credentials",
userId,
if (!resolvedHost) {
ws.send(
JSON.stringify({
type: "error",
message: "Host not found",
}),
);
return;
}
if (credentials.length > 0) {
const credential = credentials[0];
resolvedCredentials = {
password: credential.password as string | undefined,
sshKey: credential.privateKey as string | undefined,
keyPassword: credential.keyPassword as string | undefined,
authType: credential.authType as string | undefined,
};
}
if (!resolvedHost.enableDocker) {
ws.send(
JSON.stringify({
type: "error",
message:
"Docker is not enabled for this host. Enable it in Host Settings.",
}),
);
return;
}
const client = new SSHClient();
const config: Record<string, unknown> = {
host: hostConfig.ip?.replace(/^\[|\]$/g, "") || hostConfig.ip,
port: hostConfig.port || 22,
username: hostConfig.username,
host: resolvedHost.ip?.replace(/^\[|\]$/g, "") || resolvedHost.ip,
port: resolvedHost.port || 22,
username: resolvedHost.username,
tryKeyboard: true,
readyTimeout: 60000,
keepaliveInterval: 30000,
@@ -348,28 +376,22 @@ wss.on("connection", async (ws: WebSocket, req) => {
tcpKeepAliveInitialDelay: 30000,
};
if (
resolvedCredentials.authType === "password" &&
resolvedCredentials.password
) {
config.password = resolvedCredentials.password;
} else if (
resolvedCredentials.authType === "key" &&
resolvedCredentials.sshKey
) {
const cleanKey = resolvedCredentials.sshKey
if (resolvedHost.authType === "password" && resolvedHost.password) {
config.password = resolvedHost.password;
} else if (resolvedHost.authType === "key" && resolvedHost.key) {
const cleanKey = resolvedHost.key
.trim()
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
config.privateKey = Buffer.from(cleanKey, "utf8");
if (resolvedCredentials.keyPassword) {
config.passphrase = resolvedCredentials.keyPassword;
if (resolvedHost.keyPassword) {
config.passphrase = resolvedHost.keyPassword;
}
}
if (hostConfig.jumpHosts && hostConfig.jumpHosts.length > 0) {
if (resolvedHost.jumpHosts && resolvedHost.jumpHosts.length > 0) {
const jumpClient = await createJumpHostChain(
hostConfig.jumpHosts,
resolvedHost.jumpHosts,
userId,
);
if (jumpClient) {
@@ -378,8 +400,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
jumpClient.forwardOut(
"127.0.0.1",
0,
hostConfig.ip,
hostConfig.port || 22,
resolvedHost.ip,
resolvedHost.port || 22,
(err, stream) => {
if (err) return reject(err);
resolve(stream);
@@ -402,7 +424,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
stream: null,
isConnected: true,
containerId,
hostId: hostConfig.id,
hostId: resolvedHost.id,
};
activeSessions.set(sessionId, sshSession);
@@ -430,8 +452,10 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
});
stream.stderr.on("data", () => {
// Ignore stderr
stream.stderr.on("data", () => {});
stream.stderr.on("error", () => {});
stream.on("error", (streamErr) => {
reject(streamErr);
});
},
);
@@ -459,7 +483,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
operation: "docker_attach",
sessionId,
userId,
hostId: hostConfig.id,
hostId: resolvedHost.id,
containerId,
});
@@ -494,7 +518,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
operation: "docker_attach_success",
sessionId,
userId,
hostId: hostConfig.id,
hostId: resolvedHost.id,
containerId,
});
@@ -509,8 +533,23 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
});
stream.stderr.on("data", () => {
// stderr output ignored
stream.stderr.on("data", () => {});
stream.stderr.on("error", () => {});
stream.on("error", (streamErr) => {
sshLogger.error("Docker console stream error", streamErr, {
operation: "docker_console_stream_error",
sessionId,
containerId,
});
if (ws.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
type: "error",
message: `Console error: ${streamErr.message}`,
}),
);
}
});
stream.on("close", () => {
+58 -103
View File
@@ -1,14 +1,15 @@
import express from "express";
import cors from "cors";
import { createCorsMiddleware } from "../utils/cors-config.js";
import cookieParser from "cookie-parser";
import axios from "axios";
import { Client as SSHClient } from "ssh2";
import { getDb } from "../database/db/index.js";
import { sshData, sshCredentials } from "../database/db/schema.js";
import { hosts, sshCredentials } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { logger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js";
import type { AuthenticatedRequest } from "../../types/index.js";
import {
createSocks5Connection,
type SOCKS5Config,
@@ -40,6 +41,7 @@ interface SSHSession {
timeout?: NodeJS.Timeout;
activeOperations: number;
hostId?: number;
userId?: string;
}
interface PendingTOTPSession {
@@ -136,20 +138,20 @@ async function resolveJumpHost(
userId: string,
): Promise<JumpHostConfig | null> {
try {
const hosts = await SimpleDBOps.select(
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
.from(hosts)
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
"ssh_data",
userId,
);
if (hosts.length === 0) {
if (hostResults.length === 0) {
return null;
}
const host = hosts[0];
const host = hostResults[0];
if (host.credentialId) {
const credentials = await SimpleDBOps.select(
@@ -424,39 +426,7 @@ async function executeDockerCommand(
const app = express();
app.use(
cors({
origin: (origin, callback) => {
if (!origin) {
return callback(null, true);
}
if (origin.startsWith("https://")) {
return callback(null, true);
}
if (origin.startsWith("http://")) {
return callback(null, true);
}
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
return callback(new Error("Not allowed by CORS"));
},
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: [
"Content-Type",
"Authorization",
"User-Agent",
"X-Electron-App",
],
}),
);
app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"]));
app.use(cookieParser());
app.use(express.json({ limit: "100mb" }));
@@ -469,6 +439,16 @@ app.use((_req, res, next) => {
const authManager = AuthManager.getInstance();
app.use(authManager.createAuthMiddleware());
const CONTAINER_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
const DOCKER_TIMESTAMP_RE = /^[0-9T:.Z+-]+$/;
app.param("containerId", (req, res, next, value) => {
if (!CONTAINER_ID_RE.test(value)) {
return res.status(400).json({ error: "Invalid container ID" });
}
next();
});
/**
* @openapi
* /docker/ssh/connect:
@@ -570,25 +550,24 @@ app.post("/docker/ssh/connect", async (req, res) => {
);
try {
const hosts = await SimpleDBOps.select(
getDb().select().from(sshData).where(eq(sshData.id, hostId)),
const hostResults = await SimpleDBOps.select(
getDb().select().from(hosts).where(eq(hosts.id, hostId)),
"ssh_data",
userId,
);
if (hosts.length === 0) {
if (hostResults.length === 0) {
connectionLogs.push(
createConnectionLog("error", "docker_connecting", "Host not found"),
);
return res.status(404).json({ error: "Host not found", connectionLogs });
}
const host = hosts[0] as unknown as SSHHost;
const host = hostResults[0] as unknown as SSHHost;
if (host.userId !== userId) {
const { PermissionManager } = await import(
"../utils/permission-manager.js"
);
const { PermissionManager } =
await import("../utils/permission-manager.js");
const permissionManager = PermissionManager.getInstance();
const accessInfo = await permissionManager.canAccessHost(
userId,
@@ -693,9 +672,8 @@ app.post("/docker/ssh/connect", async (req, res) => {
if (userId !== ownerId) {
try {
const { SharedCredentialManager } = await import(
"../utils/shared-credential-manager.js"
);
const { SharedCredentialManager } =
await import("../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
host.id,
@@ -793,18 +771,13 @@ app.post("/docker/ssh/connect", async (req, res) => {
});
}
const { promises: fs } = await import("fs");
const path = await import("path");
const os = await import("os");
const tempDir = os.tmpdir();
const keyPath = path.join(tempDir, `opkssh-docker-${userId}-${hostId}`);
const certPath = `${keyPath}-cert.pub`;
await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 });
await fs.writeFile(certPath, token.sshCert, { mode: 0o600 });
config.privateKey = await fs.readFile(keyPath);
const { setupOPKSSHCertAuth } = await import("./opkssh-cert-auth.js");
await setupOPKSSHCertAuth(
config as import("ssh2").ConnectConfig,
client,
token,
host.username,
);
connectionLogs.push(
createConnectionLog(
"info",
@@ -812,32 +785,6 @@ app.post("/docker/ssh/connect", async (req, res) => {
"Using OPKSSH certificate authentication",
),
);
setTimeout(async () => {
try {
const cleanupResults = await Promise.allSettled([
fs.unlink(keyPath),
fs.unlink(certPath),
]);
cleanupResults.forEach((result, index) => {
if (result.status === "rejected") {
sshLogger.warn(`Failed to cleanup OPKSSH temp file`, {
operation: "opkssh_temp_cleanup_failed",
file: index === 0 ? "keyPath" : "certPath",
sessionId,
error: result.reason,
});
}
});
} catch (error) {
sshLogger.error("Failed to cleanup OPKSSH temp files", {
operation: "opkssh_temp_cleanup_error",
sessionId,
error,
});
}
}, 60000);
} catch (opksshError) {
sshLogger.error("OPKSSH authentication error for Docker", {
operation: "docker_connect",
@@ -985,6 +932,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
lastActive: Date.now(),
activeOperations: 0,
hostId,
userId,
};
scheduleSessionCleanup(sessionId);
@@ -1667,6 +1615,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
lastActive: Date.now(),
activeOperations: 0,
hostId: session.hostId,
userId,
};
scheduleSessionCleanup(sessionId);
@@ -1678,14 +1627,14 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
if (session.hostId && session.userId) {
(async () => {
try {
const hosts = await SimpleDBOps.select(
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.from(hosts)
.where(
and(
eq(sshData.id, session.hostId!),
eq(sshData.userId, session.userId!),
eq(hosts.id, session.hostId!),
eq(hosts.userId, session.userId!),
),
),
"ssh_data",
@@ -1693,8 +1642,8 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
);
const hostName =
hosts.length > 0 && hosts[0].name
? hosts[0].name
hostResults.length > 0 && hostResults[0].name
? hostResults[0].name
: `${session.username}@${session.ip}:${session.port}`;
await axios.post(
@@ -1852,6 +1801,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
lastActive: Date.now(),
activeOperations: 0,
hostId: session.hostId,
userId,
};
scheduleSessionCleanup(sessionId);
@@ -1863,14 +1813,14 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
if (session.hostId && session.userId) {
(async () => {
try {
const hosts = await SimpleDBOps.select(
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.from(hosts)
.where(
and(
eq(sshData.id, session.hostId!),
eq(sshData.userId, session.userId!),
eq(hosts.id, session.hostId!),
eq(hosts.userId, session.userId!),
),
),
"ssh_data",
@@ -1878,8 +1828,8 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
);
const hostName =
hosts.length > 0 && hosts[0].name
? hosts[0].name
hostResults.length > 0 && hostResults[0].name
? hostResults[0].name
: `${session.username}@${session.ip}:${session.port}`;
await axios.post(
@@ -1955,6 +1905,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
*/
app.post("/docker/ssh/keepalive", async (req, res) => {
const { sessionId } = req.body;
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -1969,6 +1920,10 @@ app.post("/docker/ssh/keepalive", async (req, res) => {
});
}
if (session.userId && session.userId !== userId) {
return res.status(403).json({ error: "Session access denied" });
}
session.lastActive = Date.now();
scheduleSessionCleanup(sessionId);
@@ -3018,18 +2973,18 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => {
let command = `docker logs ${containerId}`;
if (tail && tail > 0) {
command += ` --tail ${tail}`;
command += ` --tail ${Math.floor(tail)}`;
}
if (timestamps) {
command += " --timestamps";
}
if (since) {
if (since && DOCKER_TIMESTAMP_RE.test(since)) {
command += ` --since ${since}`;
}
if (until) {
if (until && DOCKER_TIMESTAMP_RE.test(until)) {
command += ` --until ${until}`;
}
+359 -251
View File
@@ -1,10 +1,11 @@
import express from "express";
import cors from "cors";
import { createCorsMiddleware } from "../utils/cors-config.js";
import cookieParser from "cookie-parser";
import axios from "axios";
import { Client as SSHClient } from "ssh2";
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
import { getDb } from "../database/db/index.js";
import { sshCredentials, sshData } from "../database/db/schema.js";
import { sshCredentials, hosts } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { fileLogger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
@@ -117,37 +118,7 @@ function formatMtime(mtime: number): string {
const app = express();
app.use(
cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (origin.startsWith("https://")) {
return callback(null, true);
}
if (origin.startsWith("http://")) {
return callback(null, true);
}
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
callback(new Error("Not allowed by CORS"));
},
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: [
"Content-Type",
"Authorization",
"User-Agent",
"X-Electron-App",
],
}),
);
app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"]));
app.use(cookieParser());
app.use(express.json({ limit: "1gb" }));
app.use(express.urlencoded({ limit: "1gb", extended: true }));
@@ -179,20 +150,20 @@ async function resolveJumpHost(
userId: string,
): Promise<JumpHostConfig | null> {
try {
const hosts = await SimpleDBOps.select(
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
.from(hosts)
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
"ssh_data",
userId,
);
if (hosts.length === 0) {
if (hostResults.length === 0) {
return null;
}
const host = hosts[0];
const host = hostResults[0];
if (host.credentialId) {
const credentials = await SimpleDBOps.select(
@@ -399,6 +370,7 @@ interface SSHSession {
sudoPassword?: string;
sftp?: import("ssh2").SFTPWrapper;
poolKey?: string;
userId?: string;
}
interface PendingTOTPSession {
@@ -531,6 +503,10 @@ function scheduleSessionCleanup(sessionId: string) {
}
}
function verifySessionOwnership(session: SSHSession, userId: string): boolean {
return !session.userId || session.userId === userId;
}
function getMimeType(fileName: string): string {
const ext = fileName.split(".").pop()?.toLowerCase();
const mimeTypes: Record<string, string> = {
@@ -801,30 +777,45 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
),
);
// Resolve credentials server-side when frontend doesn't provide them
let resolvedCredentials = { password, sshKey, keyPassword, authType };
if (credentialId && hostId && userId) {
if (hostId && userId && !password && !sshKey) {
try {
const credentials = await SimpleDBOps.select(
getDb()
.select()
.from(sshCredentials)
.where(
and(
eq(sshCredentials.id, credentialId),
eq(sshCredentials.userId, userId),
),
),
"ssh_credentials",
userId,
);
if (credentials.length > 0) {
const credential = credentials[0];
const { resolveHostById } = await import("./host-resolver.js");
const resolvedHost = await resolveHostById(hostId, userId);
if (resolvedHost) {
resolvedCredentials = {
password: credential.password,
sshKey: credential.privateKey,
keyPassword: credential.keyPassword,
authType: credential.authType,
password: resolvedHost.password,
sshKey: resolvedHost.key,
keyPassword: resolvedHost.keyPassword,
authType: resolvedHost.authType,
};
connectionLogs.push(
createConnectionLog(
"info",
"sftp_auth",
"Credentials resolved from server-side host data",
),
);
}
} catch (error) {
fileLogger.warn(`Failed to resolve host credentials for ${hostId}`, {
operation: "ssh_credentials",
hostId,
error: error instanceof Error ? error.message : "Unknown error",
});
}
} else if (credentialId && hostId && userId) {
// Legacy: credential resolution from credentialId
try {
const { resolveHostById } = await import("./host-resolver.js");
const resolvedHost = await resolveHostById(hostId, userId);
if (resolvedHost) {
resolvedCredentials = {
password: resolvedHost.password,
sshKey: resolvedHost.key,
keyPassword: resolvedHost.keyPassword,
authType: resolvedHost.authType,
};
connectionLogs.push(
createConnectionLog(
@@ -833,20 +824,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
"Credentials resolved from credential store",
),
);
} else {
fileLogger.warn(`No credentials found for host ${hostId}`, {
operation: "ssh_credentials",
hostId,
credentialId,
userId,
});
connectionLogs.push(
createConnectionLog(
"warning",
"sftp_auth",
"No stored credentials found, using provided credentials",
),
);
}
} catch (error) {
fileLogger.warn(`Failed to resolve credentials for host ${hostId}`, {
@@ -855,24 +832,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
credentialId,
error: error instanceof Error ? error.message : "Unknown error",
});
connectionLogs.push(
createConnectionLog(
"warning",
"sftp_auth",
`Failed to resolve credentials: ${error instanceof Error ? error.message : "Unknown error"}`,
),
);
}
} else if (credentialId && hostId) {
fileLogger.warn(
"Missing userId for credential resolution in file manager",
{
operation: "ssh_credentials",
hostId,
credentialId,
hasUserId: !!userId,
},
);
}
const config: Record<string, unknown> = {
@@ -928,18 +888,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
"ssh-rsa",
"ssh-dss",
],
cipher: [
"chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
"aes128-gcm@openssh.com",
"aes256-ctr",
"aes192-ctr",
"aes128-ctr",
"aes256-cbc",
"aes192-cbc",
"aes128-cbc",
"3des-cbc",
],
cipher: SSH_ALGORITHMS.cipher,
hmac: [
"hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256-etm@openssh.com",
@@ -1039,18 +988,13 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
});
}
const { promises: fs } = await import("fs");
const path = await import("path");
const os = await import("os");
const tempDir = os.tmpdir();
const keyPath = path.join(tempDir, `opkssh-fm-${userId}-${hostId}`);
const certPath = `${keyPath}-cert.pub`;
await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 });
await fs.writeFile(certPath, token.sshCert, { mode: 0o600 });
config.privateKey = await fs.readFile(keyPath);
const { setupOPKSSHCertAuth } = await import("./opkssh-cert-auth.js");
await setupOPKSSHCertAuth(
config as import("ssh2").ConnectConfig,
client,
token,
username,
);
connectionLogs.push(
createConnectionLog(
"info",
@@ -1058,32 +1002,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
"Using OPKSSH certificate authentication",
),
);
setTimeout(async () => {
try {
const cleanupResults = await Promise.allSettled([
fs.unlink(keyPath),
fs.unlink(certPath),
]);
cleanupResults.forEach((result, index) => {
if (result.status === "rejected") {
fileLogger.warn(`Failed to cleanup OPKSSH temp file`, {
operation: "opkssh_temp_cleanup_failed",
file: index === 0 ? "keyPath" : "certPath",
sessionId,
error: result.reason,
});
}
});
} catch (error) {
fileLogger.error("Failed to cleanup OPKSSH temp files", {
operation: "opkssh_temp_cleanup_error",
sessionId,
error,
});
}
}, 60000);
} catch (opksshError) {
fileLogger.error("OPKSSH authentication error for file manager", {
operation: "file_connect",
@@ -1190,6 +1108,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
isConnected: true,
lastActive: Date.now(),
activeOperations: 0,
userId,
};
scheduleSessionCleanup(sessionId);
res.json({
@@ -1201,18 +1120,18 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
if (hostId && userId) {
(async () => {
try {
const hosts = await SimpleDBOps.select(
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
.from(hosts)
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
"ssh_data",
userId,
);
const hostName =
hosts.length > 0 && hosts[0].name
? hosts[0].name
hostResults.length > 0 && hostResults[0].name
? hostResults[0].name
: `${username}@${ip}:${port}`;
const authManager = AuthManager.getInstance();
@@ -1833,6 +1752,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
isConnected: true,
lastActive: Date.now(),
activeOperations: 0,
userId,
};
scheduleSessionCleanup(sessionId);
@@ -1844,14 +1764,14 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
if (session.hostId && session.userId) {
(async () => {
try {
const hosts = await SimpleDBOps.select(
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.from(hosts)
.where(
and(
eq(sshData.id, session.hostId!),
eq(sshData.userId, session.userId!),
eq(hosts.id, session.hostId!),
eq(hosts.userId, session.userId!),
),
),
"ssh_data",
@@ -1859,8 +1779,8 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
);
const hostName =
hosts.length > 0 && hosts[0].name
? hosts[0].name
hostResults.length > 0 && hostResults[0].name
? hostResults[0].name
: `${session.username}@${session.ip}:${session.port}`;
const authManager = AuthManager.getInstance();
@@ -2034,6 +1954,7 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => {
isConnected: true,
lastActive: Date.now(),
activeOperations: 0,
userId,
};
scheduleSessionCleanup(sessionId);
@@ -2045,14 +1966,14 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => {
if (session.hostId && session.userId) {
(async () => {
try {
const hosts = await SimpleDBOps.select(
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.from(hosts)
.where(
and(
eq(sshData.id, session.hostId!),
eq(sshData.userId, session.userId!),
eq(hosts.id, session.hostId!),
eq(hosts.userId, session.userId!),
),
),
"ssh_data",
@@ -2060,8 +1981,8 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => {
);
const hostName =
hosts.length > 0 && hosts[0].name
? hosts[0].name
hostResults.length > 0 && hostResults[0].name
? hostResults[0].name
: `${session.username}@${session.ip}:${session.port}`;
await axios.post(
@@ -2163,10 +2084,14 @@ app.post("/ssh/file_manager/ssh/disconnect", (req, res) => {
*/
app.post("/ssh/file_manager/sudo-password", (req, res) => {
const { sessionId, password } = req.body;
const userId = (req as AuthenticatedRequest).userId;
const session = sshSessions[sessionId];
if (!session || !session.isConnected) {
return res.status(400).json({ error: "Invalid or disconnected session" });
}
if (!verifySessionOwnership(session, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
session.sudoPassword = password;
session.lastActive = Date.now();
res.json({ status: "success", message: "Sudo password set" });
@@ -2192,7 +2117,12 @@ app.post("/ssh/file_manager/sudo-password", (req, res) => {
*/
app.get("/ssh/file_manager/ssh/status", (req, res) => {
const sessionId = req.query.sessionId as string;
const isConnected = !!sshSessions[sessionId]?.isConnected;
const userId = (req as AuthenticatedRequest).userId;
const session = sshSessions[sessionId];
if (session && !verifySessionOwnership(session, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
const isConnected = !!session?.isConnected;
res.json({ status: "success", connected: isConnected });
});
@@ -2221,6 +2151,7 @@ app.get("/ssh/file_manager/ssh/status", (req, res) => {
*/
app.post("/ssh/file_manager/ssh/keepalive", (req, res) => {
const { sessionId } = req.body;
const userId = (req as AuthenticatedRequest).userId;
if (!sessionId) {
return res.status(400).json({ error: "Session ID is required" });
@@ -2235,6 +2166,10 @@ app.post("/ssh/file_manager/ssh/keepalive", (req, res) => {
});
}
if (!verifySessionOwnership(session, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
session.lastActive = Date.now();
scheduleSessionCleanup(sessionId);
@@ -2287,6 +2222,10 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
return res.status(400).json({ error: "SSH connection not established" });
}
if (!verifySessionOwnership(sshConn, userId)) {
return res.status(403).json({ error: "Session access denied" });
}
sshConn.lastActive = Date.now();
sshConn.activeOperations++;
const trySFTP = () => {
@@ -2399,6 +2338,10 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
};
const tryFallbackMethod = () => {
if (!sshConn?.isConnected) {
sshConn.activeOperations--;
return res.status(500).json({ error: "SSH session disconnected" });
}
try {
const escapedPath = sshPath.replace(/'/g, "'\"'\"'");
sshConn.client.exec(
@@ -3026,7 +2969,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
* /ssh/file_manager/ssh/writeFile:
* post:
* summary: Write to a file
* description: Writes content to a file on the remote host.
* description: Writes content to a file on the remote host and preserves the existing permissions when the file already exists.
* tags:
* - File Manager
* requestBody:
@@ -3082,6 +3025,112 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
});
sshConn.lastActive = Date.now();
let preservedMode: number | undefined;
const restoreOriginalMode = (
sftp: import("ssh2").SFTPWrapper | null,
onComplete: () => void,
) => {
if (preservedMode === undefined) {
onComplete();
return;
}
const permissions = preservedMode.toString(8);
if (sftp) {
sftp.chmod(filePath, preservedMode, (chmodErr) => {
if (chmodErr) {
fileLogger.warn("Failed to restore file permissions after save", {
operation: "file_write_restore_permissions",
sessionId,
userId,
path: filePath,
permissions,
error: chmodErr.message,
});
} else {
fileLogger.info("Restored file permissions after save", {
operation: "file_write_restore_permissions",
sessionId,
userId,
path: filePath,
permissions,
});
}
onComplete();
});
return;
}
const escapedPath = filePath.replace(/'/g, "'\"'\"'");
const chmodCommand = `chmod ${permissions} '${escapedPath}' && echo "SUCCESS"`;
sshConn.client.exec(chmodCommand, (err, stream) => {
if (err) {
fileLogger.warn("Failed to restore file permissions after save", {
operation: "file_write_restore_permissions",
sessionId,
userId,
path: filePath,
permissions,
error: err.message,
});
onComplete();
return;
}
let outputData = "";
let errorData = "";
stream.on("data", (chunk: Buffer) => {
outputData += chunk.toString();
});
stream.stderr.on("data", (chunk: Buffer) => {
errorData += chunk.toString();
});
stream.on("close", (code) => {
if (outputData.includes("SUCCESS")) {
fileLogger.info("Restored file permissions after save", {
operation: "file_write_restore_permissions",
sessionId,
userId,
path: filePath,
permissions,
});
} else {
fileLogger.warn("Failed to restore file permissions after save", {
operation: "file_write_restore_permissions",
sessionId,
userId,
path: filePath,
permissions,
exitCode: code,
error:
errorData || "Permission restore command did not report success",
});
}
onComplete();
});
stream.on("error", (streamErr) => {
fileLogger.warn("Failed to restore file permissions after save", {
operation: "file_write_restore_permissions",
sessionId,
userId,
path: filePath,
permissions,
error: streamErr.message,
});
onComplete();
});
});
};
const trySFTP = () => {
try {
fileLogger.info("Opening SFTP channel", {
@@ -3120,75 +3169,88 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
return;
}
const writeStream = sftp.createWriteStream(filePath);
let hasError = false;
let hasFinished = false;
writeStream.on("error", (streamErr) => {
if (hasError || hasFinished) return;
hasError = true;
fileLogger.warn(
`SFTP write failed, trying fallback method: ${streamErr.message}`,
);
tryFallbackMethod();
});
writeStream.on("finish", () => {
if (hasError || hasFinished) return;
hasFinished = true;
fileLogger.success("File written successfully", {
operation: "file_write_success",
sessionId,
userId,
path: filePath,
bytes: fileBuffer.length,
});
if (!res.headersSent) {
res.json({
message: "File written successfully",
path: filePath,
toast: {
type: "success",
message: `File written: ${filePath}`,
sftp.stat(filePath, (statErr, stats) => {
if (statErr) {
fileLogger.warn(
"Failed to read existing file permissions before save",
{
operation: "file_write_stat",
sessionId,
userId,
path: filePath,
error: statErr.message,
},
);
} else if (stats.isFile()) {
preservedMode = stats.mode & 0o7777;
}
const writeStream = sftp.createWriteStream(filePath);
let hasError = false;
let hasFinished = false;
let isFinalizing = false;
const finalizeSuccess = () => {
if (hasError || hasFinished) return;
hasFinished = true;
isFinalizing = false;
fileLogger.success("File written successfully", {
operation: "file_write_success",
sessionId,
userId,
path: filePath,
bytes: fileBuffer.length,
});
if (!res.headersSent) {
res.json({
message: "File written successfully",
path: filePath,
toast: {
type: "success",
message: `File written: ${filePath}`,
},
});
}
};
writeStream.on("error", (streamErr) => {
if (hasError || hasFinished || isFinalizing) return;
hasError = true;
isFinalizing = false;
fileLogger.warn(
`SFTP write failed, trying fallback method: ${streamErr.message}`,
);
tryFallbackMethod();
});
const finishWrite = () => {
if (hasError || hasFinished || isFinalizing) return;
isFinalizing = true;
restoreOriginalMode(sftp, finalizeSuccess);
};
writeStream.on("finish", () => {
finishWrite();
});
writeStream.on("close", () => {
finishWrite();
});
try {
writeStream.write(fileBuffer);
writeStream.end();
} catch (writeErr) {
if (hasError || hasFinished) return;
hasError = true;
isFinalizing = false;
fileLogger.warn(
`SFTP write operation failed, trying fallback method: ${writeErr.message}`,
);
tryFallbackMethod();
}
});
writeStream.on("close", () => {
if (hasError || hasFinished) return;
hasFinished = true;
fileLogger.success("File written successfully", {
operation: "file_write_success",
sessionId,
userId,
path: filePath,
bytes: fileBuffer.length,
});
if (!res.headersSent) {
res.json({
message: "File written successfully",
path: filePath,
toast: {
type: "success",
message: `File written: ${filePath}`,
},
});
}
});
try {
writeStream.write(fileBuffer);
writeStream.end();
} catch (writeErr) {
if (hasError || hasFinished) return;
hasError = true;
fileLogger.warn(
`SFTP write operation failed, trying fallback method: ${writeErr.message}`,
);
tryFallbackMethod();
}
})
.catch((err: Error) => {
fileLogger.warn(
@@ -3205,6 +3267,10 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
};
const tryFallbackMethod = () => {
if (!sshConn?.isConnected) {
sshConn.activeOperations--;
return res.status(500).json({ error: "SSH session disconnected" });
}
try {
let contentBuffer: Buffer;
if (typeof content === "string") {
@@ -3254,16 +3320,18 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
stream.on("close", (code) => {
if (outputData.includes("SUCCESS")) {
if (!res.headersSent) {
res.json({
message: "File written successfully",
path: filePath,
toast: {
type: "success",
message: `File written: ${filePath}`,
},
});
}
restoreOriginalMode(null, () => {
if (!res.headersSent) {
res.json({
message: "File written successfully",
path: filePath,
toast: {
type: "success",
message: `File written: ${filePath}`,
},
});
}
});
} else {
fileLogger.error(
`Fallback write failed with code ${code}: ${errorData}`,
@@ -3491,6 +3559,10 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
};
const tryFallbackMethod = () => {
if (!sshConn?.isConnected) {
sshConn.activeOperations--;
return res.status(500).json({ error: "SSH session disconnected" });
}
try {
let contentBuffer: Buffer;
if (typeof content === "string") {
@@ -3515,6 +3587,20 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
chunks.push(base64Content.slice(i, i + chunkSize));
}
if (!sshConn?.isConnected) {
fileLogger.error("SSH connection lost before fallback upload", {
operation: "file_upload_fallback",
sessionId,
path: fullPath,
});
if (!res.headersSent) {
return res
.status(500)
.json({ error: "SSH connection lost during upload" });
}
return;
}
if (chunks.length === 1) {
const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
@@ -3542,6 +3628,10 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
errorData += chunk.toString();
});
stream.stderr.on("error", (stderrErr) => {
fileLogger.error("Fallback upload stderr error:", stderrErr);
});
stream.on("close", (code) => {
if (outputData.includes("SUCCESS")) {
if (!res.headersSent) {
@@ -3612,6 +3702,13 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
errorData += chunk.toString();
});
stream.stderr.on("error", (stderrErr) => {
fileLogger.error(
"Chunked fallback upload stderr error:",
stderrErr,
);
});
stream.on("close", (code) => {
if (outputData.includes("SUCCESS")) {
if (!res.headersSent) {
@@ -5155,26 +5252,32 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => {
const targetPath =
extractPath || archivePath.substring(0, archivePath.lastIndexOf("/"));
const escapedArchive = archivePath.replace(/'/g, "'\"'\"'");
const escapedTarget = targetPath.replace(/'/g, "'\"'\"'");
const escapedDecompressed = archivePath
.replace(/\.gz$/, "")
.replace(/'/g, "'\"'\"'");
if (fileExt.endsWith(".tar.gz") || fileExt.endsWith(".tgz")) {
extractCommand = `tar -xzf "${archivePath}" -C "${targetPath}"`;
extractCommand = `tar -xzf '${escapedArchive}' -C '${escapedTarget}'`;
} else if (fileExt.endsWith(".tar.bz2") || fileExt.endsWith(".tbz2")) {
extractCommand = `tar -xjf "${archivePath}" -C "${targetPath}"`;
extractCommand = `tar -xjf '${escapedArchive}' -C '${escapedTarget}'`;
} else if (fileExt.endsWith(".tar.xz")) {
extractCommand = `tar -xJf "${archivePath}" -C "${targetPath}"`;
extractCommand = `tar -xJf '${escapedArchive}' -C '${escapedTarget}'`;
} else if (fileExt.endsWith(".tar")) {
extractCommand = `tar -xf "${archivePath}" -C "${targetPath}"`;
extractCommand = `tar -xf '${escapedArchive}' -C '${escapedTarget}'`;
} else if (fileExt.endsWith(".zip")) {
extractCommand = `unzip -o "${archivePath}" -d "${targetPath}"`;
extractCommand = `unzip -o '${escapedArchive}' -d '${escapedTarget}'`;
} else if (fileExt.endsWith(".gz") && !fileExt.endsWith(".tar.gz")) {
extractCommand = `gunzip -c "${archivePath}" > "${archivePath.replace(/\.gz$/, "")}"`;
extractCommand = `gunzip -c '${escapedArchive}' > '${escapedDecompressed}'`;
} else if (fileExt.endsWith(".bz2") && !fileExt.endsWith(".tar.bz2")) {
extractCommand = `bunzip2 -k "${archivePath}"`;
extractCommand = `bunzip2 -k '${escapedArchive}'`;
} else if (fileExt.endsWith(".xz") && !fileExt.endsWith(".tar.xz")) {
extractCommand = `unxz -k "${archivePath}"`;
extractCommand = `unxz -k '${escapedArchive}'`;
} else if (fileExt.endsWith(".7z")) {
extractCommand = `7z x "${archivePath}" -o"${targetPath}"`;
extractCommand = `7z x '${escapedArchive}' -o'${escapedTarget}'`;
} else if (fileExt.endsWith(".rar")) {
extractCommand = `unrar x "${archivePath}" "${targetPath}/"`;
extractCommand = `unrar x '${escapedArchive}' '${escapedTarget}/'`;
} else {
return res.status(400).json({ error: "Unsupported archive format" });
}
@@ -5360,10 +5463,12 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => {
const firstPath = paths[0];
const workingDir = firstPath.substring(0, firstPath.lastIndexOf("/")) || "/";
const escapeShell = (s: string) => s.replace(/'/g, "'\"'\"'");
const fileNames = paths
.map((p) => {
const name = p.split("/").pop();
return `"${name}"`;
return `'${escapeShell(name || "")}'`;
})
.join(" ");
@@ -5376,18 +5481,21 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => {
: `${workingDir}/${archiveName}`;
}
const escapedDir = escapeShell(workingDir);
const escapedArchive = escapeShell(archivePath);
if (compressionFormat === "zip") {
compressCommand = `cd "${workingDir}" && zip -r "${archivePath}" ${fileNames}`;
compressCommand = `cd '${escapedDir}' && zip -r '${escapedArchive}' ${fileNames}`;
} else if (compressionFormat === "tar.gz" || compressionFormat === "tgz") {
compressCommand = `cd "${workingDir}" && tar -czf "${archivePath}" ${fileNames}`;
compressCommand = `cd '${escapedDir}' && tar -czf '${escapedArchive}' ${fileNames}`;
} else if (compressionFormat === "tar.bz2" || compressionFormat === "tbz2") {
compressCommand = `cd "${workingDir}" && tar -cjf "${archivePath}" ${fileNames}`;
compressCommand = `cd '${escapedDir}' && tar -cjf '${escapedArchive}' ${fileNames}`;
} else if (compressionFormat === "tar.xz") {
compressCommand = `cd "${workingDir}" && tar -cJf "${archivePath}" ${fileNames}`;
compressCommand = `cd '${escapedDir}' && tar -cJf '${escapedArchive}' ${fileNames}`;
} else if (compressionFormat === "tar") {
compressCommand = `cd "${workingDir}" && tar -cf "${archivePath}" ${fileNames}`;
compressCommand = `cd '${escapedDir}' && tar -cf '${escapedArchive}' ${fileNames}`;
} else if (compressionFormat === "7z") {
compressCommand = `cd "${workingDir}" && 7z a "${archivePath}" ${fileNames}`;
compressCommand = `cd '${escapedDir}' && 7z a '${escapedArchive}' ${fileNames}`;
} else {
return res.status(400).json({ error: "Unsupported compression format" });
}
+9 -20
View File
@@ -1,6 +1,6 @@
import type { WebSocket } from "ws";
import { db } from "../database/db/index.js";
import { sshData } from "../database/db/schema.js";
import { hosts } from "../database/db/schema.js";
import { eq } from "drizzle-orm";
import { sshLogger } from "../utils/logger.js";
@@ -21,17 +21,6 @@ interface VerificationResponse {
}
export class SSHHostKeyVerifier {
/**
* Creates a hostVerifier callback for ssh2 Client.connect()
*
* @param hostId - Database ID of the host (null for quick connect)
* @param ip - IP address or hostname
* @param port - SSH port
* @param ws - WebSocket for user prompts (null for non-interactive connections)
* @param userId - User ID for logging
* @param isJumpHost - If true, auto-accepts without prompting
* @returns async hostVerifier callback
*/
static async createHostVerifier(
hostId: number | null,
ip: string,
@@ -63,8 +52,8 @@ export class SSHHostKeyVerifier {
return;
}
const host = await db.query.sshData.findFirst({
where: eq(sshData.id, hostId),
const host = await db.query.hosts.findFirst({
where: eq(hosts.id, hostId),
});
if (!host) {
@@ -153,11 +142,11 @@ export class SSHHostKeyVerifier {
if (host.hostKeyFingerprint === fingerprint) {
await db
.update(sshData)
.update(hosts)
.set({
hostKeyLastVerified: new Date().toISOString(),
})
.where(eq(sshData.id, hostId));
.where(eq(hosts.id, hostId));
sshLogger.info("Host key verified successfully", {
operation: "host_key_verified",
@@ -287,7 +276,7 @@ export class SSHHostKeyVerifier {
algorithm: string,
): Promise<void> {
await db
.update(sshData)
.update(hosts)
.set({
hostKeyFingerprint: fingerprint,
hostKeyType: keyType,
@@ -295,7 +284,7 @@ export class SSHHostKeyVerifier {
hostKeyFirstSeen: new Date().toISOString(),
hostKeyLastVerified: new Date().toISOString(),
})
.where(eq(sshData.id, hostId));
.where(eq(hosts.id, hostId));
}
private static async updateHostKey(
@@ -306,7 +295,7 @@ export class SSHHostKeyVerifier {
currentChangeCount: number,
): Promise<void> {
await db
.update(sshData)
.update(hosts)
.set({
hostKeyFingerprint: fingerprint,
hostKeyType: keyType,
@@ -314,7 +303,7 @@ export class SSHHostKeyVerifier {
hostKeyLastVerified: new Date().toISOString(),
hostKeyChangedCount: currentChangeCount + 1,
})
.where(eq(sshData.id, hostId));
.where(eq(hosts.id, hostId));
}
private static async promptUserForNewKey(
+173
View File
@@ -0,0 +1,173 @@
import { getDb } from "../database/db/index.js";
import { hosts, sshCredentials } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { logger } from "../utils/logger.js";
import type { SSHHost } from "../../types/index.js";
const sshLogger = logger;
/**
* Resolve a host with its credentials server-side by hostId.
* This avoids passing credentials through the frontend.
*/
export async function resolveHostById(
hostId: number,
userId: string,
): Promise<SSHHost | null> {
const db = getDb();
const hostResults = await SimpleDBOps.select(
db.select().from(hosts).where(eq(hosts.id, hostId)),
"ssh_data",
userId,
);
if (hostResults.length === 0) return null;
const host = hostResults[0] as Record<string, unknown>;
// Parse JSON fields
if (typeof host.jumpHosts === "string" && host.jumpHosts) {
try {
host.jumpHosts = JSON.parse(host.jumpHosts as string);
} catch {
host.jumpHosts = [];
}
}
if (typeof host.tunnelConnections === "string") {
try {
host.tunnelConnections = JSON.parse(host.tunnelConnections as string);
} catch {
host.tunnelConnections = [];
}
}
if (typeof host.statsConfig === "string" && host.statsConfig) {
try {
host.statsConfig = JSON.parse(host.statsConfig as string);
} catch {
host.statsConfig = undefined;
}
}
if (typeof host.terminalConfig === "string" && host.terminalConfig) {
try {
host.terminalConfig = JSON.parse(host.terminalConfig as string);
} catch {
host.terminalConfig = undefined;
}
}
if (typeof host.socks5ProxyChain === "string" && host.socks5ProxyChain) {
try {
host.socks5ProxyChain = JSON.parse(host.socks5ProxyChain as string);
} catch {
host.socks5ProxyChain = [];
}
}
if (typeof host.quickActions === "string" && host.quickActions) {
try {
host.quickActions = JSON.parse(host.quickActions as string);
} catch {
host.quickActions = [];
}
}
// Resolve credential if using credential-based auth
if (host.credentialId) {
const ownerId = (host.userId || userId) as string;
try {
// Try shared credential first for non-owner users
if (userId !== ownerId) {
try {
const { SharedCredentialManager } =
await import("../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
hostId,
userId,
);
if (sharedCred) {
host.password = sharedCred.password;
host.key = sharedCred.key;
host.keyPassword = sharedCred.keyPassword;
host.keyType = sharedCred.keyType;
if (!host.overrideCredentialUsername) {
host.username = sharedCred.username;
}
host.authType = sharedCred.key
? "key"
: sharedCred.password
? "password"
: "none";
return host as unknown as SSHHost;
}
} catch (e) {
sshLogger.warn("Failed to get shared credential, falling back", {
operation: "host_resolver_shared_credential",
hostId,
error: e instanceof Error ? e.message : "Unknown",
});
}
}
const credentials = await SimpleDBOps.select(
db
.select()
.from(sshCredentials)
.where(
and(
eq(sshCredentials.id, host.credentialId as number),
eq(sshCredentials.userId, ownerId),
),
),
"ssh_credentials",
ownerId,
);
if (credentials.length > 0) {
const cred = credentials[0] as Record<string, unknown>;
host.password = cred.password;
host.key = cred.key;
host.keyPassword = cred.keyPassword;
host.keyType = cred.keyType;
if (!host.overrideCredentialUsername) {
host.username = cred.username;
}
host.authType = cred.key ? "key" : cred.password ? "password" : "none";
}
} catch (e) {
sshLogger.warn("Failed to resolve credential for host", {
operation: "host_resolver_credential",
hostId,
error: e instanceof Error ? e.message : "Unknown",
});
}
}
return host as unknown as SSHHost;
}
/**
* Check if a user has access to a host (owner or shared access).
*/
export async function checkHostAccess(
hostId: number,
userId: string,
hostUserId: string,
requiredPermission: "read" | "execute" = "execute",
): Promise<boolean> {
if (userId === hostUserId) return true;
try {
const { PermissionManager } =
await import("../utils/permission-manager.js");
const permissionManager = PermissionManager.getInstance();
const accessInfo = await permissionManager.canAccessHost(
userId,
hostId,
requiredPermission,
);
return accessInfo.hasAccess;
} catch {
return false;
}
}
+239 -73
View File
@@ -12,9 +12,13 @@ import { FieldCrypto } from "../utils/field-crypto.js";
import { promises as fs } from "fs";
import path from "path";
import axios from "axios";
import yaml from "js-yaml";
import { getRequestOrigin } from "../utils/request-origin.js";
const AUTH_TIMEOUT = 60 * 1000;
export const OPKSSH_CALLBACK_PATH = "/host/opkssh-callback";
interface OPKSSHAuthSession {
requestId: string;
userId: string;
@@ -24,6 +28,7 @@ interface OPKSSHAuthSession {
localPort: number;
callbackPort: number;
remoteRedirectUri: string;
providers: Array<{ alias: string; issuer: string }>;
status:
| "starting"
| "waiting_for_auth"
@@ -46,45 +51,9 @@ interface OPKSSHAuthSession {
}
const activeAuthSessions = new Map<string, OPKSSHAuthSession>();
const oauthStateToRequestId = new Map<string, string>();
const cleanupInProgress = new Set<string>();
export function getRequestOrigin(req: IncomingMessage): string {
const protoHeader =
req.headers["x-forwarded-proto"] ||
((req.socket as unknown as { encrypted?: boolean }).encrypted
? "https"
: "http");
const proto =
typeof protoHeader === "string"
? protoHeader.split(",")[0].trim()
: String(protoHeader);
const portHeader = req.headers["x-forwarded-port"];
const port =
typeof portHeader === "string"
? portHeader.split(",")[0].trim()
: undefined;
const hostHeaderRaw =
req.headers["x-forwarded-host"] || req.headers.host || "localhost";
const hostHeader =
typeof hostHeaderRaw === "string"
? hostHeaderRaw.split(",")[0].trim()
: String(hostHeaderRaw);
if (port) {
const hostWithoutPort = hostHeader.split(":")[0];
const isDefaultPort =
(proto === "http" && port === "80") ||
(proto === "https" && port === "443");
return isDefaultPort
? `${proto}://${hostWithoutPort}`
: `${proto}://${hostWithoutPort}:${port}`;
}
return `${proto}://${hostHeader}`;
}
function getOPKConfigPath(): string {
const dataDir =
process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
@@ -114,10 +83,17 @@ async function createTemplateConfig(): Promise<void> {
}
}
interface ProviderRedirectInfo {
alias: string;
issuer: string;
redirectUris: string[];
}
async function checkOPKConfigExists(): Promise<{
exists: boolean;
error?: string;
configPath?: string;
providers?: ProviderRedirectInfo[];
}> {
const configPath = getOPKConfigPath();
const isDocker =
@@ -155,15 +131,44 @@ async function checkOPKConfigExists(): Promise<{
};
}
if (!content.includes("redirect_uris:")) {
return {
exists: false,
configPath,
error: `OPKSSH configuration is missing 'redirect_uris' field. This field must contain the Termix callback URL that you registered with your OAuth provider (e.g., http://localhost:8080/ssh/opkssh-callback for Docker). The static callback route will internally redirect to the dynamic route for proper URL rewriting.`,
let providers: ProviderRedirectInfo[] = [];
try {
const parsed = yaml.load(content) as {
providers?: Array<{
alias?: string;
issuer?: string;
redirect_uris?: string[];
}>;
};
if (parsed?.providers && Array.isArray(parsed.providers)) {
providers = parsed.providers
.filter(
(
p,
): p is {
alias: string;
issuer: string;
redirect_uris?: string[];
} => typeof p.alias === "string" && typeof p.issuer === "string",
)
.map((p) => ({
alias: p.alias,
issuer: p.issuer.replace(/^https?:\/\//, ""),
redirectUris: Array.isArray(p.redirect_uris)
? p.redirect_uris.filter(
(u): u is string => typeof u === "string",
)
: [],
}));
}
} catch (e) {
sshLogger.warn("Failed to parse OPKSSH config for providers", {
operation: "opkssh_config_parse_providers_error",
error: e,
});
}
return { exists: true, configPath };
return { exists: true, configPath, providers };
} catch {
await createTemplateConfig();
return {
@@ -174,6 +179,63 @@ async function checkOPKConfigExists(): Promise<{
}
}
// OPKSSH's `redirect_uris` field lists candidate LOCAL ports for the callback listener
// that OPKSSH binds on the host running the binary. The openpubkey library enforces these
// must be localhost, a non-localhost entry causes ECONNRESET on /select/ at runtime.
// The publicly registered OAuth redirect URI is what Termix passes via --remote-redirect-uri
// (derived from request origin); users do NOT put that URL in this config field.
function validateRedirectUrisAreLocalhost(
providers: ProviderRedirectInfo[],
): { ok: true } | { ok: false; message: string } {
const isLocalHost = (host: string): boolean => {
const bare = host.replace(/^\[|\]$/g, "");
return (
bare === "localhost" ||
bare === "127.0.0.1" ||
bare === "::1" ||
bare === "0:0:0:0:0:0:0:1" ||
bare.startsWith("localhost:") ||
bare.startsWith("127.0.0.1:")
);
};
const issues: string[] = [];
for (const p of providers) {
const uris = p.redirectUris || [];
if (uris.length === 0) continue;
const nonLocal = uris.filter((u) => {
try {
return !isLocalHost(new URL(u).hostname);
} catch {
return true;
}
});
if (nonLocal.length > 0) {
issues.push(
`Provider '${p.alias}': non-localhost entries in redirect_uris: ${nonLocal.join(", ")}`,
);
}
}
if (issues.length > 0) {
return {
ok: false,
message:
`OPKSSH configuration error: 'redirect_uris' must only contain localhost URLs.\n\n` +
`${issues.join("\n")}\n\n` +
`This field is OPKSSH's local callback listener, it must be localhost (or omitted to use ` +
`the defaults http://localhost:3000/login-callback, :10001, :11110). ` +
`The public Termix callback URL is supplied automatically by Termix via --remote-redirect-uri; ` +
`you do not put it here. Register the PUBLIC Termix URL with your OAuth provider instead ` +
`(e.g. https://your-domain${OPKSSH_CALLBACK_PATH}).\n\n` +
`Fix: remove the non-localhost entries above, or delete the whole 'redirect_uris' block to use defaults.\n\n` +
`Docs: https://docs.termix.site/opkssh`,
};
}
return { ok: true };
}
export async function startOPKSSHAuth(
userId: string,
hostId: number,
@@ -214,8 +276,37 @@ export async function startOPKSSHAuth(
return "";
}
const redirectValidation = validateRedirectUrisAreLocalhost(
configCheck.providers || [],
);
if (redirectValidation.ok === false) {
sshLogger.warn("OPKSSH config redirect_uris validation failed", {
operation: "opkssh_config_redirect_uris_not_localhost",
configPath: configCheck.configPath,
});
ws.send(
JSON.stringify({
type: "opkssh_config_error",
requestId: "",
error: redirectValidation.message,
instructions: redirectValidation.message,
}),
);
return "";
}
const requestId = randomUUID();
const remoteRedirectUri = `${requestOrigin}/ssh/opkssh-callback`;
const remoteRedirectUri = `${requestOrigin}${OPKSSH_CALLBACK_PATH}`;
sshLogger.info("Starting OPKSSH auth session", {
operation: "opkssh_start_auth_remote_redirect_uri",
requestId,
userId,
hostId,
requestOrigin,
remoteRedirectUri,
providerAliases: (configCheck.providers || []).map((p) => p.alias),
});
const session: Partial<OPKSSHAuthSession> = {
requestId,
@@ -225,6 +316,7 @@ export async function startOPKSSHAuth(
localPort: 0,
callbackPort: 0,
remoteRedirectUri,
providers: configCheck.providers || [],
status: "starting",
ws,
stdoutBuffer: "",
@@ -297,7 +389,80 @@ export async function startOPKSSHAuth(
handleOPKSSHOutput(requestId, stderr);
}
if (stderr.includes("provider not found") || stderr.includes("config")) {
const lowerStderr = stderr.toLowerCase();
// OPKSSH's openpubkey library rejects non-localhost `redirect_uris` at runtime
// with the distinctive message "redirectURI must be localhost". Surface that
// directly with actionable guidance.
if (lowerStderr.includes("redirecturi must be localhost")) {
sshLogger.warn("OPKSSH rejected non-localhost entry in redirect_uris", {
operation: "opkssh_stderr_redirect_uris_not_localhost",
requestId,
remoteRedirectUri,
stderrSnippet: stderr.slice(0, 500),
});
ws.send(
JSON.stringify({
type: "opkssh_config_error",
requestId,
error:
`OPKSSH rejected the local callback URI: every entry in 'redirect_uris' must be localhost.\n\n` +
`OPKSSH output:\n${stderr.trim()}\n\n` +
`The 'redirect_uris' config field is OPKSSH's LOCAL listener — it is not the public Termix callback. ` +
`Remove any non-localhost entries from redirect_uris (or delete the whole block to use OPKSSH's ` +
`defaults of :3000, :10001, :11110). Register the public Termix callback URL with your OAuth ` +
`provider instead, Termix passes it to OPKSSH automatically via --remote-redirect-uri.`,
instructions: "See documentation: https://docs.termix.site/opkssh",
}),
);
await cleanup();
return;
}
// Generic redirect-uri/mismatch errors (OAuth provider side, OPKSSH config side, etc.)
const genericRedirectIndicators = [
"redirect_uri",
"redirect uri",
"invalid redirect",
"no matching redirect",
"allowed redirect",
"mismatching redirection",
];
const hasGenericRedirectError = genericRedirectIndicators.some((s) =>
lowerStderr.includes(s),
);
if (hasGenericRedirectError) {
sshLogger.warn("OPKSSH stderr reported redirect_uri error", {
operation: "opkssh_stderr_redirect_uri_error",
requestId,
remoteRedirectUri,
stderrSnippet: stderr.slice(0, 500),
});
ws.send(
JSON.stringify({
type: "opkssh_config_error",
requestId,
error:
`OPKSSH or the OAuth provider rejected the redirect URI.\n\n` +
`Computed Termix callback URI (sent to provider): ${remoteRedirectUri}\n\n` +
`OPKSSH output:\n${stderr.trim()}\n\n` +
`Register '${remoteRedirectUri}' as an authorized redirect URI with your OAuth provider ` +
`(e.g. in Google Cloud Console → OAuth client). ` +
`Also confirm any 'redirect_uris' in your OPKSSH config contain ONLY localhost URLs.`,
instructions: "See documentation: https://docs.termix.site/opkssh",
}),
);
await cleanup();
return;
}
if (
stderr.includes("provider not found") ||
stderr.includes("config error") ||
stderr.includes("invalid config") ||
stderr.includes("config not found")
) {
ws.send(
JSON.stringify({
type: "opkssh_config_error",
@@ -376,19 +541,19 @@ function handleOPKSSHOutput(requestId: string, output: string): void {
session.stdoutBuffer += output;
const chooserUrlMatch = session.stdoutBuffer.match(
/(?:Opening browser to|Open your browser to:)\s*http:\/\/localhost:(\d+)\/chooser/,
/(?:Opening browser to|Open your browser to:)\s*http:\/\/(?:localhost|127\.0\.0\.1):(\d+)\/chooser/,
);
if (chooserUrlMatch && session.status === "starting") {
const actualPort = parseInt(chooserUrlMatch[1], 10);
const localChooserUrl = `http://localhost:${actualPort}/chooser`;
const localChooserUrl = `http://127.0.0.1:${actualPort}/chooser`;
session.localPort = actualPort;
const baseUrl = session.remoteRedirectUri.replace(
/\/ssh\/opkssh-callback$/,
"",
);
const proxiedChooserUrl = `${baseUrl}/ssh/opkssh-chooser/${requestId}`;
const baseUrl = session.remoteRedirectUri
.replace(/\/host\/opkssh-callback$/, "")
// In direct dev mode the WS server (30002) is separate from the HTTP API (30001)
.replace(/:30002\b/, ":30001");
const proxiedChooserUrl = `${baseUrl}/host/opkssh-chooser/${requestId}`;
session.status = "waiting_for_auth";
session.ws.send(
@@ -397,6 +562,7 @@ function handleOPKSSHOutput(requestId: string, output: string): void {
requestId,
stage: "chooser",
url: proxiedChooserUrl,
providers: session.providers,
localUrl: localChooserUrl,
message: "Please authenticate in your browser",
}),
@@ -404,7 +570,7 @@ function handleOPKSSHOutput(requestId: string, output: string): void {
}
const callbackPortMatch = session.stdoutBuffer.match(
/listening on http:\/\/127\.0\.0\.1:(\d+)\//,
/listening on http:\/\/(?:127\.0\.0\.1|localhost):(\d+)\//,
);
if (callbackPortMatch && !session.callbackPort) {
session.callbackPort = parseInt(callbackPortMatch[1], 10);
@@ -545,25 +711,6 @@ async function storeOPKSSHToken(session: OPKSSHAuthSession): Promise<void> {
}),
);
try {
await axios.post(
"http://localhost:30006/activity/log",
{
type: "opkssh_authentication",
hostId: session.hostId,
hostName: session.hostname,
status: "approved",
},
{
headers: {
Authorization: `Bearer ${process.env.INTERNAL_AUTH_TOKEN}`,
},
},
);
} catch (activityError) {
sshLogger.warn("Failed to log OPKSSH activity", activityError);
}
await session.cleanup();
} catch (error) {
sshLogger.error(
@@ -693,7 +840,7 @@ export async function handleOAuthCallback(
}
try {
const callbackUrl = `http://localhost:${session.localPort}/login-callback?${queryString}`;
const callbackUrl = `http://127.0.0.1:${session.localPort}/login-callback?${queryString}`;
await axios.get(callbackUrl, {
timeout: 10000,
validateStatus: () => true,
@@ -756,6 +903,13 @@ async function cleanupAuthSession(requestId: string): Promise<void> {
}
}
// Clean up any OAuth state mappings for this session
for (const [state, reqId] of oauthStateToRequestId.entries()) {
if (reqId === requestId) {
oauthStateToRequestId.delete(state);
}
}
activeAuthSessions.delete(requestId);
} finally {
cleanupInProgress.delete(requestId);
@@ -789,6 +943,18 @@ export function getActiveSessionsAll(): OPKSSHAuthSession[] {
return Array.from(activeAuthSessions.values());
}
export function registerOAuthState(state: string, requestId: string): void {
oauthStateToRequestId.set(state, requestId);
}
export function getRequestIdByOAuthState(state: string): string | undefined {
return oauthStateToRequestId.get(state);
}
export function clearOAuthState(state: string): void {
oauthStateToRequestId.delete(state);
}
export async function getUserIdFromRequest(req: {
cookies?: Record<string, string>;
headers: Record<string, string | undefined>;
+236
View File
@@ -0,0 +1,236 @@
// OPKSSH certificate authentication workarounds for ssh2.
// ssh2 doesn't support OpenSSH cert auth natively — this module grafts
// the certificate onto the parsed key, wraps ECDSA signing to convert
// DER → SSH wire format, and patches Protocol.authPK to use the base
// algorithm in the signature wrapper (required by OpenSSH's sshkey_check_sigtype).
import type { Client, ConnectConfig } from "ssh2";
interface OPKSSHToken {
privateKey: string;
sshCert: string;
}
export async function setupOPKSSHCertAuth(
config: ConnectConfig,
client: Client,
token: OPKSSHToken,
username: string,
): Promise<void> {
const { createRequire } = await import("node:module");
const esmRequire = createRequire(import.meta.url);
const {
utils: { parseKey },
} = esmRequire("ssh2");
const parsed = parseKey(Buffer.from(token.privateKey));
if (parsed instanceof Error || !parsed) {
throw new Error("Failed to parse OPKSSH private key");
}
const privKey: any = Array.isArray(parsed) ? parsed[0] : parsed;
// Extract cert type and blob from the stored certificate
const certParts = token.sshCert.trim().split(/\s+/);
const certType = certParts[0];
privKey.type = certType;
const certBlob = Buffer.from(certParts[1], "base64");
// Replace the internal public SSH blob with the full certificate
const pubSSHSym = Object.getOwnPropertySymbols(privKey).find(
(s) => String(s) === "Symbol(Public key SSH)",
);
if (!pubSSHSym) {
throw new Error(
"Cannot find public SSH symbol on parsed key, ssh2 internals may have changed",
);
}
privKey[pubSSHSym] = certBlob;
// Wrap sign() for ECDSA cert keys
if (privKey.type.startsWith("ecdsa-")) {
const origSign = privKey.sign.bind(privKey);
privKey.sign = (data: Buffer, algo?: string) => {
const sigAlgo = algo?.includes("-cert-")
? algo.replace(/-cert-v\d+@openssh\.com$/, "")
: algo;
const sig = origSign(data, sigAlgo);
if (sig instanceof Error || sig[0] !== 0x30) return sig;
// Convert DER-encoded ECDSA signature to SSH wire format
try {
let pos = 2;
if (sig[1] & 0x80) pos += sig[1] & 0x7f;
pos++;
const rLen = sig[pos++];
const r = sig.subarray(pos, pos + rLen);
pos += rLen + 1;
const sLen = sig[pos++];
const s = sig.subarray(pos, pos + sLen);
const out = Buffer.allocUnsafe(4 + r.length + 4 + s.length);
out.writeUInt32BE(r.length, 0);
r.copy(out, 4);
out.writeUInt32BE(s.length, 4 + r.length);
s.copy(out, 4 + r.length + 4);
return out;
} catch {
return sig;
}
};
}
// Set up authHandler to bypass ssh2's cert type rejection
let certAuthAttempted = false;
config.authHandler = (
methodsLeft: string[],
_partialSuccess: boolean,
callback: (authInfo: any) => void,
) => {
if (
!certAuthAttempted &&
(!methodsLeft || methodsLeft.includes("publickey"))
) {
certAuthAttempted = true;
callback({ type: "publickey", username, key: privKey });
} else {
callback(false);
}
};
// Monkey-patch Protocol.authPK after connect() to fix the signature
// wrapper algorithm for cert types.
const baseAlgo = certType.replace(/-cert-v\d+@openssh\.com$/, "");
const origConnect = client.connect.bind(client);
(client as any).connect = (cfg: any) => {
origConnect(cfg);
const proto = (client as any)._protocol;
if (!proto) return;
const origAuthPK = proto.authPK.bind(proto);
proto.authPK = (
user: string,
pubKey: any,
keyAlgo: string | undefined,
cbSign?: Function,
) => {
const isCertAuth = !!cbSign && pubKey?.type?.includes("-cert-");
if (!isCertAuth) {
return origAuthPK(user, pubKey, keyAlgo, cbSign);
}
// Signed auth with cert type: rebuild packet with base algo in
// the signature wrapper. keyAlgo may be undefined for ECDSA.
const certAlgo = keyAlgo || pubKey.type;
const pubSSH = pubKey.getPublicSSH();
const sessionID = proto._kex.sessionID;
const sesLen = sessionID.length;
const userLen = Buffer.byteLength(user);
const certAlgoLen = Buffer.byteLength(certAlgo);
const baseAlgoLen = Buffer.byteLength(baseAlgo);
const pubKeyLen = pubSSH.length;
// Build data to sign (uses cert algo — matches server verification)
const sigDataLen =
4 +
sesLen +
1 +
4 +
userLen +
4 +
14 +
4 +
9 +
1 +
4 +
certAlgoLen +
4 +
pubKeyLen;
const sigData = Buffer.allocUnsafe(sigDataLen);
let sp = 0;
sigData.writeUInt32BE(sesLen, sp);
sp += 4;
sessionID.copy(sigData, sp);
sp += sesLen;
sigData[sp++] = 50; // SSH_MSG_USERAUTH_REQUEST
sigData.writeUInt32BE(userLen, sp);
sp += 4;
sigData.write(user, sp, userLen, "utf8");
sp += userLen;
sigData.writeUInt32BE(14, sp);
sp += 4;
sigData.write("ssh-connection", sp, 14, "utf8");
sp += 14;
sigData.writeUInt32BE(9, sp);
sp += 4;
sigData.write("publickey", sp, 9, "utf8");
sp += 9;
sigData[sp++] = 1; // TRUE
sigData.writeUInt32BE(certAlgoLen, sp);
sp += 4;
sigData.write(certAlgo, sp, certAlgoLen, "utf8");
sp += certAlgoLen;
sigData.writeUInt32BE(pubKeyLen, sp);
sp += 4;
pubSSH.copy(sigData, sp);
cbSign(sigData, (signature: Buffer) => {
const sigLen = signature.length;
const payloadLen =
1 +
4 +
userLen +
4 +
14 +
4 +
9 +
1 +
4 +
certAlgoLen +
4 +
pubKeyLen +
4 +
4 +
baseAlgoLen +
4 +
sigLen;
const packet = proto._packetRW.write.alloc(payloadLen);
let pp = proto._packetRW.write.allocStart;
packet[pp] = 50; // SSH_MSG_USERAUTH_REQUEST
packet.writeUInt32BE(userLen, ++pp);
pp += 4;
packet.write(user, pp, userLen, "utf8");
pp += userLen;
packet.writeUInt32BE(14, pp);
pp += 4;
packet.write("ssh-connection", pp, 14, "utf8");
pp += 14;
packet.writeUInt32BE(9, pp);
pp += 4;
packet.write("publickey", pp, 9, "utf8");
pp += 9;
packet[pp++] = 1; // TRUE
// Header: cert type
packet.writeUInt32BE(certAlgoLen, pp);
pp += 4;
packet.write(certAlgo, pp, certAlgoLen, "utf8");
pp += certAlgoLen;
// Public key blob
packet.writeUInt32BE(pubKeyLen, pp);
pp += 4;
pubSSH.copy(packet, pp);
pp += pubKeyLen;
// Signature wrapper: base algo (NOT cert type)
packet.writeUInt32BE(4 + baseAlgoLen + 4 + sigLen, pp);
pp += 4;
packet.writeUInt32BE(baseAlgoLen, pp);
pp += 4;
packet.write(baseAlgo, pp, baseAlgoLen, "utf8");
pp += baseAlgoLen;
packet.writeUInt32BE(sigLen, pp);
pp += 4;
signature.copy(packet, pp);
proto._authsQueue.push("publickey");
proto._debug?.("Outbound: Sending USERAUTH_REQUEST (publickey)");
const finalized = proto._packetRW.write.finalize(packet);
proto._cipher.encrypt(finalized);
});
};
};
}
+264 -81
View File
@@ -1,10 +1,11 @@
import express from "express";
import net from "net";
import cors from "cors";
import { createCorsMiddleware } from "../utils/cors-config.js";
import cookieParser from "cookie-parser";
import { Client, type ConnectConfig } from "ssh2";
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
import { getDb } from "../database/db/index.js";
import { sshData, sshCredentials } from "../database/db/schema.js";
import { hosts, sshCredentials } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { statsLogger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
@@ -29,6 +30,17 @@ import {
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
import { connectionPool, withConnection } from "./ssh-connection-pool.js";
function supportsMetrics(host: SSHHostWithCredentials): boolean {
const connectionType = host.connectionType || "ssh";
if (connectionType !== "ssh") return false;
if (host.authType === "none" || host.authType === "opkssh") return false;
return true;
}
function isTcpPingEnabled(statsConfig: StatsConfig): boolean {
return statsConfig.statusCheckEnabled && !statsConfig.disableTcpPing;
}
function createConnectionLog(
type: "info" | "success" | "warning" | "error",
stage: ConnectionStage,
@@ -62,20 +74,20 @@ async function resolveJumpHost(
userId: string,
): Promise<JumpHostConfig | null> {
try {
const hosts = await SimpleDBOps.select(
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
.from(hosts)
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
"ssh_data",
userId,
);
if (hosts.length === 0) {
if (hostResults.length === 0) {
return null;
}
const host = hosts[0];
const host = hostResults[0];
if (host.credentialId) {
const credentials = await SimpleDBOps.select(
@@ -644,6 +656,7 @@ interface SSHHostWithCredentials {
socks5Username?: string;
socks5Password?: string;
socks5ProxyChain?: ProxyNode[];
connectionType?: "ssh" | "rdp" | "vnc" | "telnet";
}
type StatusEntry = {
@@ -659,6 +672,7 @@ interface StatsConfig {
metricsEnabled: boolean;
metricsInterval: number;
useGlobalMetricsInterval?: boolean;
disableTcpPing?: boolean;
}
const DEFAULT_STATS_CONFIG: StatsConfig = {
@@ -783,9 +797,13 @@ class PollingManager {
const statusOnly = options?.statusOnly ?? false;
const viewerUserId = options?.viewerUserId;
const canCollectMetrics = supportsMetrics(host);
const enabledCollectors: string[] = [];
if (statsConfig.statusCheckEnabled) enabledCollectors.push("status");
if (!statusOnly && statsConfig.metricsEnabled) {
if (isTcpPingEnabled(statsConfig)) {
enabledCollectors.push("status");
}
if (!statusOnly && statsConfig.metricsEnabled && canCollectMetrics) {
enabledCollectors.push(
"cpu",
"memory",
@@ -810,7 +828,7 @@ class PollingManager {
}
}
if (!statsConfig.statusCheckEnabled && !statsConfig.metricsEnabled) {
if (!isTcpPingEnabled(statsConfig) && !statsConfig.metricsEnabled) {
this.pollingConfigs.delete(host.id);
this.statusStore.delete(host.id);
this.metricsStore.delete(host.id);
@@ -823,14 +841,14 @@ class PollingManager {
viewerUserId,
};
if (statsConfig.statusCheckEnabled) {
if (isTcpPingEnabled(statsConfig)) {
const intervalMs = statsConfig.statusCheckInterval * 1000;
this.pollHostStatus(host, viewerUserId);
config.statusTimer = setInterval(() => {
const latestConfig = this.pollingConfigs.get(host.id);
if (latestConfig && latestConfig.statsConfig.statusCheckEnabled) {
if (latestConfig && isTcpPingEnabled(latestConfig.statsConfig)) {
this.pollHostStatus(latestConfig.host, latestConfig.viewerUserId);
}
}, intervalMs);
@@ -838,15 +856,27 @@ class PollingManager {
this.statusStore.delete(host.id);
}
if (!statusOnly && statsConfig.metricsEnabled) {
if (!statusOnly && statsConfig.metricsEnabled && canCollectMetrics) {
const intervalMs = statsConfig.metricsInterval * 1000;
await this.pollHostMetrics(host, viewerUserId);
config.metricsTimer = setInterval(() => {
const latestConfig = this.pollingConfigs.get(host.id);
if (latestConfig && latestConfig.statsConfig.metricsEnabled) {
this.pollHostMetrics(latestConfig.host, latestConfig.viewerUserId);
if (
latestConfig &&
latestConfig.statsConfig.metricsEnabled &&
supportsMetrics(latestConfig.host)
) {
this.pollHostMetrics(
latestConfig.host,
latestConfig.viewerUserId,
).catch((err) => {
statsLogger.error("Metrics polling failed", err, {
operation: "metrics_poll_unhandled",
hostId: host.id,
});
});
}
}, intervalMs);
} else {
@@ -896,14 +926,25 @@ class PollingManager {
return;
}
if (!supportsMetrics(refreshedHost)) {
statsLogger.debug("Skipping metrics collection for non-SSH host", {
operation: "poll_host_metrics_skipped",
hostId: refreshedHost.id,
connectionType: refreshedHost.connectionType || "ssh",
});
return;
}
const config = this.pollingConfigs.get(refreshedHost.id);
if (!config || !config.statsConfig.metricsEnabled) {
return;
}
const hasExistingMetrics = this.metricsStore.has(refreshedHost.id);
if (authFailureTracker.shouldSkip(host.id)) {
return;
}
if (hasExistingMetrics && pollingBackoff.shouldSkip(host.id)) {
if (pollingBackoff.shouldSkip(host.id)) {
return;
}
@@ -914,16 +955,38 @@ class PollingManager {
timestamp: Date.now(),
});
pollingBackoff.reset(refreshedHost.id);
authFailureTracker.reset(refreshedHost.id);
} catch (error) {
const isAuthError =
error instanceof Error &&
(error.message.includes("authentication") ||
error.message.includes("Authentication") ||
error.message.includes("permission denied") ||
error.message.includes("Permission denied"));
if (isAuthError) {
// authFailureTracker already handles auth errors inside collectMetrics;
// only log on the first occurrence to avoid repeated spam
const alreadyTracked = authFailureTracker.shouldSkip(host.id);
if (!alreadyTracked) {
statsLogger.error("Stats collector connection failed", error, {
operation: "stats_connect_failed",
hostId: refreshedHost.id,
});
}
return;
}
pollingBackoff.recordFailure(refreshedHost.id);
const latestConfig = this.pollingConfigs.get(refreshedHost.id);
if (latestConfig && latestConfig.statsConfig.metricsEnabled) {
const backoffInfo = pollingBackoff.getBackoffInfo(refreshedHost.id);
// Only log when a new retry window opens, not on every skipped poll
const backoff = pollingBackoff.getBackoffInfo(refreshedHost.id);
const isNewFailure =
backoff !== null && !backoff.includes("polling suspended");
if (isNewFailure) {
statsLogger.error("Stats collector connection failed", error, {
operation: "stats_connect_failed",
hostId: refreshedHost.id,
retryInfo: backoffInfo,
});
}
}
@@ -1001,6 +1064,34 @@ class PollingManager {
}
}
async refreshAllPolling(): Promise<void> {
const hostsToRefresh: Array<{
host: SSHHostWithCredentials;
viewerUserId?: string;
}> = [];
for (const [hostId, config] of this.pollingConfigs.entries()) {
const status = this.statusStore.get(hostId);
if (!status || status.status === "online") {
hostsToRefresh.push({
host: config.host,
viewerUserId: config.viewerUserId,
});
}
}
for (const hostId of this.pollingConfigs.keys()) {
this.stopPollingForHost(hostId, false);
}
for (const { host, viewerUserId } of hostsToRefresh) {
await this.startPollingForHost(host, { statusOnly: true, viewerUserId });
}
const skipped = this.pollingConfigs.size - hostsToRefresh.length;
}
registerViewer(hostId: number, sessionId: string, userId: string): void {
if (!this.activeViewers.has(hostId)) {
this.activeViewers.set(hostId, new Set());
@@ -1015,7 +1106,18 @@ class PollingManager {
});
if (this.activeViewers.get(hostId)!.size === 1) {
this.startMetricsForHost(hostId, userId);
// Fire-and-forget: never let background metrics start-up failures
// propagate up to the HTTP handler that registered the viewer.
Promise.resolve()
.then(() => this.startMetricsForHost(hostId, userId))
.catch((err) => {
statsLogger.warn("startMetricsForHost rejected (non-fatal)", {
operation: "start_metrics_unhandled",
hostId,
userId,
error: err instanceof Error ? err.message : String(err),
});
});
}
}
@@ -1097,37 +1199,7 @@ function validateHostId(
}
const app = express();
app.use(
cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
if (origin.startsWith("https://")) {
return callback(null, true);
}
if (origin.startsWith("http://")) {
return callback(null, true);
}
callback(new Error("Not allowed by CORS"));
},
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: [
"Content-Type",
"Authorization",
"User-Agent",
"X-Electron-App",
],
}),
);
app.use(createCorsMiddleware());
app.use(cookieParser());
app.use(express.json({ limit: "1mb" }));
app.use((_req, res, next) => {
@@ -1142,14 +1214,14 @@ async function fetchAllHosts(
userId: string,
): Promise<SSHHostWithCredentials[]> {
try {
const hosts = await SimpleDBOps.select(
getDb().select().from(sshData).where(eq(sshData.userId, userId)),
const hostResults = await SimpleDBOps.select(
getDb().select().from(hosts).where(eq(hosts.userId, userId)),
"ssh_data",
userId,
);
const hostsWithCredentials: SSHHostWithCredentials[] = [];
for (const host of hosts) {
for (const host of hostResults) {
try {
const hostWithCreds = await resolveHostCredentials(host, userId);
if (hostWithCreds) {
@@ -1193,17 +1265,17 @@ async function fetchHostById(
return undefined;
}
const hosts = await SimpleDBOps.select(
getDb().select().from(sshData).where(eq(sshData.id, id)),
const hostResults = await SimpleDBOps.select(
getDb().select().from(hosts).where(eq(hosts.id, id)),
"ssh_data",
userId,
);
if (hosts.length === 0) {
if (hostResults.length === 0) {
return undefined;
}
const host = hosts[0];
const host = hostResults[0];
return await resolveHostCredentials(host, userId);
} catch (err) {
statsLogger.error(`Failed to fetch host ${id}`, err);
@@ -1318,8 +1390,13 @@ async function resolveHostCredentials(
if (credential.password) {
baseHost.password = credential.password;
}
if (credential.key) {
baseHost.key = credential.key;
if (
credential.key ||
(credential as Record<string, unknown>).privateKey
) {
baseHost.key =
credential.key ||
((credential as Record<string, unknown>).privateKey as string);
}
if (credential.keyPassword) {
baseHost.keyPassword = credential.keyPassword;
@@ -1430,18 +1507,7 @@ async function buildSshConfig(
"ssh-rsa",
"ssh-dss",
],
cipher: [
"chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
"aes128-gcm@openssh.com",
"aes256-ctr",
"aes192-ctr",
"aes128-ctr",
"aes256-cbc",
"aes192-cbc",
"aes128-cbc",
"3des-cbc",
],
cipher: SSH_ALGORITHMS.cipher,
hmac: [
"hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256-etm@openssh.com",
@@ -1491,7 +1557,7 @@ async function buildSshConfig(
} else if (host.authType === "none") {
// no credentials needed
} else if (host.authType === "opkssh") {
// handled externally
// cert auth setup happens in createSshFactory (needs client instance)
} else if (host.authType === "credential") {
if (host.password) {
base.password = host.password;
@@ -1531,6 +1597,19 @@ function createSshFactory(host: SSHHostWithCredentials): () => Promise<Client> {
const config = await buildSshConfig(host);
const client = new Client();
// Set up OPKSSH cert auth if needed (requires client instance)
if (host.authType === "opkssh" && host.userId) {
const { getOPKSSHToken } = await import("./opkssh-auth.js");
const token = await getOPKSSHToken(host.userId, host.id);
if (!token) {
throw new Error(
"OPKSSH authentication required. Please open a Terminal connection first.",
);
}
const { setupOPKSSHCertAuth } = await import("./opkssh-cert-auth.js");
await setupOPKSSHCertAuth(config, client, token, host.username);
}
const proxyConfig: SOCKS5Config | null =
host.useSocks5 &&
(host.socks5Host ||
@@ -1730,6 +1809,10 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
os: string | null;
};
}> {
if (!supportsMetrics(host)) {
throw new Error("Metrics collection only supported for SSH hosts");
}
if (authFailureTracker.shouldSkip(host.id)) {
const reason = authFailureTracker.getSkipReason(host.id);
throw new Error(reason || "Authentication failed");
@@ -1854,7 +1937,8 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
} else if (
error.message.includes("No password available") ||
error.message.includes("Unsupported authentication type") ||
error.message.includes("No SSH key available")
error.message.includes("No SSH key available") ||
error.message.includes("Invalid SSH key format")
) {
authFailureTracker.recordFailure(host.id, "AUTH", true);
} else if (
@@ -1908,7 +1992,8 @@ function tcpPing(
socket.once("data", (data) => {
clearTimeout(dataTimeout);
if (data.toString().startsWith("SSH-")) {
const dataStr = data.toString("utf8");
if (dataStr.startsWith("SSH-")) {
try {
socket.end("SSH-2.0-TermixHealthCheck\r\n");
} catch {
@@ -2353,6 +2438,27 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => {
const config = await buildSshConfig(host);
const client = new Client();
if (host.authType === "opkssh" && host.userId) {
const { getOPKSSHToken } = await import("./opkssh-auth.js");
const token = await getOPKSSHToken(host.userId, host.id);
if (!token) {
connectionLogs.push(
createConnectionLog(
"error",
"auth",
"OPKSSH authentication required. Please open a Terminal connection first.",
),
);
return res.status(401).json({
error: "OPKSSH authentication required",
requiresOPKSSHAuth: true,
connectionLogs,
});
}
const { setupOPKSSHCertAuth } = await import("./opkssh-cert-auth.js");
await setupOPKSSHCertAuth(config, client, token, host.username);
}
const connectionPromise = new Promise<{
success: boolean;
requires_totp?: boolean;
@@ -2968,8 +3074,70 @@ app.post("/metrics/register-viewer", async (req, res) => {
}
try {
// Graceful no-op if host is inaccessible, metrics disabled, or host type
// does not support metrics. The client may call this speculatively, so
// avoid returning 5xx for expected "no metrics available" scenarios.
let host: SSHHostWithCredentials | undefined;
try {
host = await fetchHostById(hostId, userId);
} catch (lookupErr) {
statsLogger.warn(
"register-viewer host lookup failed (treating as no-op)",
{
operation: "register_viewer_lookup",
hostId,
userId,
error:
lookupErr instanceof Error ? lookupErr.message : String(lookupErr),
},
);
}
if (!host) {
return res.json({
success: true,
skipped: true,
reason: "host_not_found",
});
}
if (!supportsMetrics(host)) {
return res.json({
success: true,
skipped: true,
reason: "metrics_unsupported",
});
}
const statsConfig = pollingManager.parseStatsConfig(host.statsConfig);
if (!statsConfig.metricsEnabled) {
return res.json({
success: true,
skipped: true,
reason: "metrics_disabled",
});
}
const viewerSessionId = `viewer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
pollingManager.registerViewer(hostId, viewerSessionId, userId);
try {
pollingManager.registerViewer(hostId, viewerSessionId, userId);
} catch (regErr) {
statsLogger.warn(
"pollingManager.registerViewer threw (treating as no-op)",
{
operation: "register_viewer_internal",
hostId,
userId,
error: regErr instanceof Error ? regErr.message : String(regErr),
},
);
return res.json({
success: true,
skipped: true,
reason: "register_failed_noop",
});
}
res.json({ success: true, viewerSessionId });
} catch (error) {
statsLogger.error("Failed to register viewer", {
@@ -2978,7 +3146,14 @@ app.post("/metrics/register-viewer", async (req, res) => {
userId,
error: error instanceof Error ? error.message : String(error),
});
res.status(500).json({ error: "Failed to register viewer" });
// Even on unexpected errors we prefer a graceful client experience: the
// viewer-registration is purely an optimization and should never break
// the UI. Report success:false but HTTP 200 so the client can decide.
res.status(200).json({
success: false,
skipped: true,
reason: "internal_error",
});
}
});
@@ -3185,13 +3360,21 @@ app.post("/global-settings", requireAdmin, async (req, res) => {
.run(String(metricsInterval));
}
res.json({ success: true });
await pollingManager.refreshAllPolling();
res.json({
success: true,
message: "Settings updated and polling refreshed",
});
} catch (error) {
statsLogger.error("Failed to save global settings", {
operation: "global_settings_save_error",
error: error instanceof Error ? error.message : String(error),
});
res.status(500).json({ error: "Failed to save global settings" });
res.status(500).json({
error: "Failed to save global settings",
details: error instanceof Error ? error.message : String(error),
});
}
});
+2 -36
View File
@@ -19,7 +19,6 @@ export interface TerminalSession {
sshConn: Client | null;
sshStream: ClientChannel | null;
jumpClient: Client | null;
opksshTempFiles: { keyPath: string; certPath: string } | null;
cols: number;
rows: number;
@@ -32,6 +31,7 @@ export interface TerminalSession {
outputBuffer: string[];
outputBufferBytes: number;
tmuxSessionName: string | null;
}
class TerminalSessionManager {
@@ -99,7 +99,6 @@ class TerminalSessionManager {
sshConn: null,
sshStream: null,
jumpClient: null,
opksshTempFiles: null,
cols,
rows,
isConnected: false,
@@ -109,6 +108,7 @@ class TerminalSessionManager {
detachTimeout: null,
outputBuffer: [],
outputBufferBytes: 0,
tmuxSessionName: null,
};
this.sessions.set(id, session);
@@ -132,7 +132,6 @@ class TerminalSessionManager {
conn: Client,
stream: ClientChannel,
jumpClient?: Client | null,
opksshTempFiles?: { keyPath: string; certPath: string } | null,
): void {
const session = this.sessions.get(sessionId);
if (!session) return;
@@ -140,7 +139,6 @@ class TerminalSessionManager {
session.sshConn = conn;
session.sshStream = stream;
session.jumpClient = jumpClient ?? null;
session.opksshTempFiles = opksshTempFiles ?? null;
session.isConnected = true;
}
@@ -326,12 +324,6 @@ class TerminalSessionManager {
session.jumpClient = null;
}
if (session.opksshTempFiles) {
const tempFiles = session.opksshTempFiles;
session.opksshTempFiles = null;
this.cleanupOpksshFiles(tempFiles);
}
session.isConnected = false;
session.outputBuffer = [];
session.outputBufferBytes = 0;
@@ -449,32 +441,6 @@ class TerminalSessionManager {
}
}
private async cleanupOpksshFiles(tempFiles: {
keyPath: string;
certPath: string;
}): Promise<void> {
try {
const { promises: fs } = await import("fs");
const results = await Promise.allSettled([
fs.unlink(tempFiles.keyPath),
fs.unlink(tempFiles.certPath),
]);
results.forEach((result, index) => {
if (result.status === "rejected") {
sshLogger.warn("Failed to cleanup OPKSSH temp file", {
operation: "opkssh_temp_cleanup_failed",
file: index === 0 ? "keyPath" : "certPath",
});
}
});
} catch (error) {
sshLogger.error("Failed to cleanup OPKSSH temp files", {
operation: "opkssh_temp_cleanup_error",
error,
});
}
}
destroyAll(): void {
for (const id of [...this.sessions.keys()]) {
this.destroySession(id);
+318 -95
View File
@@ -1,9 +1,12 @@
import { WebSocketServer, WebSocket, type RawData } from "ws";
import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2";
import net from "net";
import dgram from "dgram";
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
import { parse as parseUrl } from "url";
import axios from "axios";
import { getDb } from "../database/db/index.js";
import { sshCredentials, sshData } from "../database/db/schema.js";
import { sshCredentials, hosts } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { sshLogger, authLogger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
@@ -17,6 +20,46 @@ import { SSHAuthManager } from "./auth-manager.js";
import type { ProxyNode } from "../../types/index.js";
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
import { sessionManager } from "./terminal-session-manager.js";
import {
detectTmux,
attachOrCreateTmuxSession,
queryNewestTmuxSession,
} from "./tmux-helper.js";
async function performPortKnocking(
host: string,
sequence: Array<{ port: number; protocol?: string; delay?: number }>,
): Promise<void> {
for (const knock of sequence) {
const protocol = knock.protocol || "tcp";
const delay = knock.delay ?? 100;
await new Promise<void>((resolve) => {
if (protocol === "udp") {
const client = dgram.createSocket("udp4");
client.send(Buffer.alloc(0), knock.port, host, () => {
client.close();
resolve();
});
} else {
const socket = new net.Socket();
socket.once("connect", () => {
socket.destroy();
resolve();
});
socket.once("error", () => {
socket.destroy();
resolve();
});
socket.connect(knock.port, host);
}
});
if (delay > 0) {
await new Promise<void>((r) => setTimeout(r, delay));
}
}
}
interface ConnectToHostData {
cols: number;
@@ -42,6 +85,11 @@ interface ConnectToHostData {
socks5Username?: string;
socks5Password?: string;
socks5ProxyChain?: unknown;
portKnockSequence?: Array<{
port: number;
protocol?: "tcp" | "udp";
delay?: number;
}>;
terminalConfig?: {
keepaliveInterval?: number;
keepaliveCountMax?: number;
@@ -97,20 +145,20 @@ async function resolveJumpHost(
hostId,
});
try {
const hosts = await SimpleDBOps.select(
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
.from(hosts)
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
"ssh_data",
userId,
);
if (hosts.length === 0) {
if (hostResults.length === 0) {
return null;
}
const host = hosts[0];
const host = hostResults[0];
if (host.credentialId) {
const credentials = await SimpleDBOps.select(
@@ -255,7 +303,7 @@ async function createJumpHostChain(
host: jumpHostConfig.ip?.replace(/^\[|\]$/g, "") || jumpHostConfig.ip,
port: jumpHostConfig.port || 22,
username: jumpHostConfig.username,
tryKeyboard: true,
tryKeyboard: jumpHostConfig.authType !== "none",
readyTimeout: 30000,
hostVerifier: jumpHostVerifier,
};
@@ -320,7 +368,15 @@ const wss = new WebSocketServer({
verifyClient: async (info) => {
try {
const url = parseUrl(info.req.url!, true);
const token = url.query.token as string;
let token = url.query.token as string;
if (!token) {
const cookieHeader = info.req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
}
}
if (!token) {
return false;
@@ -338,7 +394,7 @@ const wss = new WebSocketServer({
const existingConnections = userConnections.get(payload.userId);
if (existingConnections && existingConnections.size >= 3) {
if (existingConnections && existingConnections.size >= 10) {
return false;
}
@@ -359,7 +415,15 @@ wss.on("connection", async (ws: WebSocket, req) => {
try {
const url = parseUrl(req.url!, true);
const token = url.query.token as string;
let token = url.query.token as string;
if (!token) {
const cookieHeader = req.headers.cookie;
if (cookieHeader) {
const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/);
if (match) token = decodeURIComponent(match[1]);
}
}
if (!token) {
ws.close(1008, "Authentication required");
@@ -427,10 +491,28 @@ wss.on("connection", async (ws: WebSocket, req) => {
let warpgateAuthPromptSent = false;
let warpgateAuthTimeout: NodeJS.Timeout | null = null;
let isAwaitingAuthCredentials = false;
let opksshTempFiles: { keyPath: string; certPath: string } | null = null;
let wsAlive = true;
ws.on("pong", () => {
wsAlive = true;
});
const wsPingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
if (!wsAlive) {
sshLogger.warn(
"WebSocket pong timeout - terminating zombie connection",
{
operation: "ws_pong_timeout",
userId,
sessionId: currentSessionId,
},
);
ws.terminate();
return;
}
wsAlive = false;
ws.ping();
}
}, 30000);
@@ -624,6 +706,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
hostName: s.hostName,
createdAt: s.createdAt,
lastDetachedAt: s.lastDetachedAt,
tmuxSessionName: s.tmuxSessionName,
})),
}),
);
@@ -678,6 +761,52 @@ wss.on("connection", async (ws: WebSocket, req) => {
ws.send(JSON.stringify({ type: "pong" }));
break;
case "tmux_attach": {
const tmuxData = data as { sessionName: string };
const session = currentSessionId
? sessionManager.getSession(currentSessionId)
: null;
if (session?.sshStream) {
const existingName = tmuxData.sessionName || undefined;
attachOrCreateTmuxSession(session.sshStream, existingName);
if (existingName) {
session.tmuxSessionName = existingName;
sshLogger.info("User selected tmux session to attach", {
operation: "tmux_user_attach",
sessionName: existingName,
hostId: session.hostId,
});
ws.send(
JSON.stringify({
type: "tmux_session_attached",
sessionName: existingName,
}),
);
} else {
// New session from picker -- query name after startup
const sshConn = session.sshConn;
setTimeout(async () => {
const sessionName = sshConn
? await queryNewestTmuxSession(sshConn)
: null;
session.tmuxSessionName = sessionName;
sshLogger.info("User requested new tmux session", {
operation: "tmux_user_create",
sessionName,
hostId: session.hostId,
});
ws.send(
JSON.stringify({
type: "tmux_session_created",
sessionName,
}),
);
}, 500);
}
}
break;
}
case "totp_response": {
const totpData = data as TOTPResponseData;
if (keyboardInteractiveFinish && totpData?.code) {
@@ -808,14 +937,14 @@ wss.on("connection", async (ws: WebSocket, req) => {
case "opkssh_start_auth": {
const opksshData = data as { hostId: number };
try {
const { startOPKSSHAuth, getRequestOrigin } = await import(
"./opkssh-auth.js"
);
const { startOPKSSHAuth } = await import("./opkssh-auth.js");
const { getRequestOrigin } =
await import("../utils/request-origin.js");
const db = getDb();
const hostRow = await db
.select()
.from(sshData)
.where(eq(sshData.id, opksshData.hostId))
.from(hosts)
.where(eq(hosts.id, opksshData.hostId))
.limit(1);
if (!hostRow || hostRow.length === 0) {
sshLogger.error(
@@ -1048,6 +1177,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
}, 120000);
// Resolve credentials server-side when frontend doesn't provide them
let resolvedCredentials = {
username,
password,
@@ -1057,39 +1187,45 @@ wss.on("connection", async (ws: WebSocket, req) => {
authType,
};
const authMethodNotAvailable = false;
if (credentialId && id && hostConfig.userId) {
if (id && userId && !password && !key) {
try {
const credentials = await SimpleDBOps.select(
getDb()
.select()
.from(sshCredentials)
.where(
and(
eq(sshCredentials.id, credentialId),
eq(sshCredentials.userId, hostConfig.userId),
),
),
"ssh_credentials",
hostConfig.userId,
);
if (credentials.length > 0) {
const credential = credentials[0];
const { resolveHostById } = await import("./host-resolver.js");
const resolvedHost = await resolveHostById(id, userId);
if (resolvedHost) {
resolvedCredentials = {
username: (credential.username as string | undefined) || username,
password: credential.password as string | undefined,
key: credential.privateKey as string | undefined,
keyPassword: credential.keyPassword as string | undefined,
keyType: credential.keyType as string | undefined,
authType: credential.authType as string | undefined,
username: resolvedHost.username || username,
password: resolvedHost.password,
key: resolvedHost.key,
keyPassword: resolvedHost.keyPassword,
keyType: resolvedHost.keyType,
authType: resolvedHost.authType,
};
sendLog(
"auth",
"info",
"Credentials resolved from server-side host data",
);
}
} catch (error) {
sshLogger.warn(`Failed to resolve host credentials for ${id}`, {
operation: "ssh_credentials",
hostId: id,
error: error instanceof Error ? error.message : "Unknown error",
});
}
} else if (credentialId && id && userId) {
try {
const { resolveHostById } = await import("./host-resolver.js");
const resolvedHost = await resolveHostById(id, userId);
if (resolvedHost) {
resolvedCredentials = {
username: resolvedHost.username || username,
password: resolvedHost.password,
key: resolvedHost.key,
keyPassword: resolvedHost.keyPassword,
keyType: resolvedHost.keyType,
authType: resolvedHost.authType,
};
} else {
sshLogger.warn(`No credentials found for host ${id}`, {
operation: "ssh_credentials",
hostId: id,
credentialId,
userId: hostConfig.userId,
});
}
} catch (error) {
sshLogger.warn(`Failed to resolve credentials for host ${id}`, {
@@ -1099,13 +1235,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
error: error instanceof Error ? error.message : "Unknown error",
});
}
} else if (credentialId && id) {
sshLogger.warn("Missing userId for credential resolution in terminal", {
operation: "ssh_credentials",
hostId: id,
credentialId,
hasUserId: !!hostConfig.userId,
});
}
sshConn.on("ready", () => {
@@ -1290,7 +1419,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshConn!,
stream,
lastJumpClient,
opksshTempFiles,
);
sessionManager.attachWs(currentSessionId, userId, ws);
@@ -1381,16 +1509,115 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
});
if (initialPath && initialPath.trim() !== "") {
const cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}" && pwd\n`;
stream.write(cdCommand);
}
const autoTmux = hostConfig.terminalConfig?.autoTmux === true;
if (executeCommand && executeCommand.trim() !== "") {
// Helper to run initialPath/executeCommand after the shell
// (or tmux session) is ready
const runPostShellCommands = (delay: number) => {
setTimeout(() => {
const command = `${executeCommand}\n`;
stream.write(command);
}, 500);
if (initialPath && initialPath.trim() !== "") {
const cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}" && pwd\r`;
stream.write(cdCommand);
}
if (executeCommand && executeCommand.trim() !== "") {
setTimeout(() => {
stream.write(`${executeCommand}\r`);
}, 300);
}
}, delay);
};
if (autoTmux && conn) {
(async () => {
try {
const detection = await detectTmux(conn);
if (!detection.available) {
sshLogger.warn("tmux not found on remote host", {
operation: "tmux_detection",
hostId: id,
});
ws.send(
JSON.stringify({
type: "tmux_unavailable",
message:
"tmux is not installed on the remote host. Falling back to standard shell.",
}),
);
// tmux unavailable, run commands in plain shell
runPostShellCommands(0);
} else if (detection.sessions.length === 0) {
attachOrCreateTmuxSession(stream);
// Query the name tmux assigned after a short delay
setTimeout(async () => {
const sessionName = await queryNewestTmuxSession(conn);
const session = sessionManager.getSession(boundSessionId);
if (session) {
session.tmuxSessionName = sessionName;
}
sshLogger.info("Created new tmux session", {
operation: "tmux_new_session",
sessionName,
hostId: id,
});
ws.send(
JSON.stringify({
type: "tmux_session_created",
sessionName,
}),
);
}, 500);
// Wait for tmux to start before running commands inside it
runPostShellCommands(500);
} else if (detection.sessions.length === 1) {
attachOrCreateTmuxSession(stream, detection.sessions[0].name);
const sessionName = detection.sessions[0].name;
const session = sessionManager.getSession(boundSessionId);
if (session) {
session.tmuxSessionName = sessionName;
}
sshLogger.info("Auto-attached to existing tmux session", {
operation: "tmux_auto_attach",
sessionName,
hostId: id,
});
ws.send(
JSON.stringify({
type: "tmux_session_attached",
sessionName,
}),
);
// Reattaching to existing session -- don't re-run
// initialPath/executeCommand since the session already
// has its own state
} else {
sshLogger.info(
"Multiple tmux sessions found, sending list to frontend",
{
operation: "tmux_sessions_available",
sessions: detection.sessions,
hostId: id,
},
);
ws.send(
JSON.stringify({
type: "tmux_sessions_available",
sessions: detection.sessions,
}),
);
// Commands deferred until user picks a session
}
} catch (error) {
sshLogger.error("tmux detection failed", error, {
operation: "tmux_detection_error",
hostId: id,
});
// Fallback: run commands in plain shell
runPostShellCommands(0);
}
})();
} else {
// No tmux -- run commands directly as before
runPostShellCommands(0);
}
ws.send(
@@ -1400,14 +1627,14 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (id && hostConfig.userId) {
(async () => {
try {
const hosts = await SimpleDBOps.select(
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(sshData)
.from(hosts)
.where(
and(
eq(sshData.id, id),
eq(sshData.userId, hostConfig.userId!),
eq(hosts.id, id),
eq(hosts.userId, hostConfig.userId!),
),
),
"ssh_data",
@@ -1415,8 +1642,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
);
const hostName =
hosts.length > 0 && hosts[0].name
? hosts[0].name
hostResults.length > 0 && hostResults[0].name
? hostResults[0].name
: `${username}@${ip}:${port}`;
await axios.post(
@@ -1760,7 +1987,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
host: ip,
port,
username,
tryKeyboard: true,
tryKeyboard: resolvedCredentials.authType !== "none",
keepaliveInterval:
typeof hostKeepaliveInterval === "number"
? hostKeepaliveInterval
@@ -1818,18 +2045,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
"ssh-rsa",
"ssh-dss",
],
cipher: [
"chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
"aes128-gcm@openssh.com",
"aes256-ctr",
"aes192-ctr",
"aes128-ctr",
"aes256-cbc",
"aes192-cbc",
"aes128-cbc",
"3des-cbc",
],
cipher: SSH_ALGORITHMS.cipher,
hmac: [
"hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256-etm@openssh.com",
@@ -1933,19 +2149,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
sendLog("auth", "info", "Using cached OPKSSH certificate");
const { promises: fs } = await import("fs");
const path = await import("path");
const os = await import("os");
const tempDir = os.tmpdir();
const keyPath = path.join(tempDir, `opkssh-${userId}-${id}`);
const certPath = `${keyPath}-cert.pub`;
await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 });
await fs.writeFile(certPath, token.sshCert, { mode: 0o600 });
opksshTempFiles = { keyPath, certPath };
connectConfig.privateKey = await fs.readFile(keyPath);
const { setupOPKSSHCertAuth } = await import("./opkssh-cert-auth.js");
await setupOPKSSHCertAuth(connectConfig, sshConn, token, username);
} catch (opksshError) {
sshLogger.error("OPKSSH authentication error", opksshError, {
operation: "opkssh_auth_error",
@@ -1976,6 +2181,24 @@ wss.on("connection", async (ws: WebSocket, req) => {
return;
}
if (
hostConfig.portKnockSequence &&
hostConfig.portKnockSequence.length > 0
) {
try {
sshLogger.info(
`Port knocking ${hostConfig.ip} (${hostConfig.portKnockSequence.length} ports)`,
{ operation: "port_knock", hostId: hostConfig.id },
);
await performPortKnocking(hostConfig.ip, hostConfig.portKnockSequence);
} catch (err) {
sshLogger.warn("Port knocking failed, attempting connection anyway", {
operation: "port_knock",
hostId: hostConfig.id,
});
}
}
const proxyConfig: SOCKS5Config | null =
hostConfig.useSocks5 &&
(hostConfig.socks5Host ||
@@ -2130,6 +2353,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
} else {
sendLog("handshake", "info", "Starting SSH session");
sendLog("auth", "info", `Authenticating as ${username}`);
sshLogger.info("Initiating SSH connection", {
operation: "terminal_ssh_connect_attempt",
sessionId,
@@ -2178,7 +2402,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshStream = null;
sshConn = null;
lastJumpClient = null;
opksshTempFiles = null;
resetConnectionState();
isCleaningUp = false;
+156
View File
@@ -0,0 +1,156 @@
import type { Client, ClientChannel } from "ssh2";
import { sshLogger } from "../utils/logger.js";
export interface TmuxSessionInfo {
name: string;
created: number;
lastActivity: number;
windows: number;
attachedClients: number;
}
export interface TmuxDetectionResult {
available: boolean;
sessions: TmuxSessionInfo[];
}
/**
* Run a command on the remote host via a separate exec channel.
* Returns stdout as a string. Does not pollute the interactive shell.
*/
export function execCommand(conn: Client, command: string): Promise<string> {
return new Promise((resolve, reject) => {
conn.exec(command, (err, stream) => {
if (err) {
reject(err);
return;
}
let stdout = "";
let stderr = "";
stream.on("data", (data: Buffer) => {
stdout += data.toString("utf-8");
});
stream.stderr.on("data", (data: Buffer) => {
stderr += data.toString("utf-8");
});
stream.on("error", (err: Error) => {
reject(err);
});
stream.on("close", (code: number) => {
if (code !== 0 && stdout === "") {
reject(
new Error(stderr.trim() || `Command exited with code ${code}`),
);
} else {
resolve(stdout.trim());
}
});
});
});
}
/**
* Detect whether tmux is installed and list all existing sessions with details.
*/
export async function detectTmux(conn: Client): Promise<TmuxDetectionResult> {
try {
await execCommand(conn, "command -v tmux");
} catch {
return { available: false, sessions: [] };
}
let sessions: TmuxSessionInfo[] = [];
try {
const output = await execCommand(
conn,
`tmux list-sessions -F "#{session_name}|#{session_created}|#{session_activity}|#{session_windows}|#{session_attached}" 2>/dev/null`,
);
if (output) {
sessions = output
.split("\n")
.filter((line) => line.length > 0)
.map((line) => {
const [name, created, activity, windows, attached] = line.split("|");
return {
name,
created: parseInt(created, 10) || 0,
lastActivity: parseInt(activity, 10) || 0,
windows: parseInt(windows, 10) || 0,
attachedClients: parseInt(attached, 10) || 0,
};
});
}
} catch {
// tmux server not running yet -- no sessions exist
}
return { available: true, sessions };
}
// tmux options applied on every attach/create:
// - mouse on: enables mouse wheel / touch scrollback through tmux history
// - history-limit: deep scrollback buffer on the remote host
// - set-clipboard on: use OSC 52 to sync tmux selections to the client clipboard
// - mode-keys vi: use vi-style keys in copy mode
// - MouseDragEnd: stop the selection but keep it highlighted so the user can
// adjust and press Enter to copy (or drag again)
// - Enter: copy the (possibly adjusted) selection and exit copy mode
// - pane-mode-changed hook: on copy-mode entry, show a brief hint so users
// know to press Enter to copy the selection
// Using -q on set/set-hook to suppress errors on older tmux versions that don't support
// a particular option (e.g. set-clipboard on tmux < 2.5). Note: set-hook doesn't support -q.
const TMUX_OPTS =
`set -gq mouse on` +
` \\; set -gq history-limit 50000` +
` \\; set -gq set-clipboard on` +
` \\; set -gq mode-keys vi` +
` \\; bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X stop-selection` +
` \\; bind-key -T copy-mode-vi Enter send-keys -X copy-selection-and-cancel` +
` \\; set-hook -g pane-mode-changed` +
` 'if -F "#{pane_in_mode}"` +
` "display-message -d 2500 \\"Adjust selection and press Enter to copy\\""'`;
/**
* Write tmux attach or new-session command to the interactive shell stream.
* Uses && exit so the shell only closes if tmux started successfully.
*/
export function attachOrCreateTmuxSession(
stream: ClientChannel,
existingSessionName?: string,
): void {
let command: string;
if (existingSessionName) {
command = `tmux ${TMUX_OPTS} \\; attach-session -t ${shellEscape(existingSessionName)} && exit\r`;
} else {
command = `tmux ${TMUX_OPTS} \\; new-session && exit\r`;
}
sshLogger.info("Writing tmux command to shell", {
operation: "tmux_attach_or_create",
sessionName: existingSessionName || "(auto)",
isReattach: !!existingSessionName,
});
stream.write(command);
}
/**
* Query the name of the most recently created tmux session via exec channel.
*/
export async function queryNewestTmuxSession(
conn: Client,
): Promise<string | null> {
try {
const output = await execCommand(
conn,
`tmux list-sessions -F "#{session_created}:#{session_name}" 2>/dev/null | sort -rn | head -1 | cut -d: -f2-`,
);
return output || null;
} catch {
return null;
}
}
function shellEscape(s: string): string {
return "'" + s.replace(/'/g, "'\\''") + "'";
}
+101 -157
View File
@@ -1,7 +1,8 @@
import express, { type Response } from "express";
import cors from "cors";
import { createCorsMiddleware } from "../utils/cors-config.js";
import cookieParser from "cookie-parser";
import { Client } from "ssh2";
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
import { ChildProcess } from "child_process";
import axios from "axios";
import { getDb } from "../database/db/index.js";
@@ -26,40 +27,7 @@ import { PermissionManager } from "../utils/permission-manager.js";
import { withConnection } from "./ssh-connection-pool.js";
const app = express();
app.use(
cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
if (origin.startsWith("https://")) {
return callback(null, true);
}
if (origin.startsWith("http://")) {
return callback(null, true);
}
callback(new Error("Not allowed by CORS"));
},
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: [
"Origin",
"X-Requested-With",
"Content-Type",
"Accept",
"Authorization",
"User-Agent",
"X-Electron-App",
],
}),
);
app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"]));
app.use(cookieParser());
app.use(express.json());
app.use((_req, res, next) => {
@@ -546,7 +514,7 @@ async function connectSSHTunnel(
tunnelConnecting.add(tunnelName);
cleanupTunnelResources(tunnelName, true);
await cleanupTunnelResources(tunnelName, true);
if (retryAttempt === 0) {
retryExhaustedTunnels.delete(tunnelName);
@@ -602,71 +570,53 @@ async function connectSSHTunnel(
const effectiveUserId =
tunnelConfig.requestingUserId || tunnelConfig.sourceUserId;
if (tunnelConfig.sourceCredentialId && effectiveUserId) {
// Resolve source credentials server-side when not provided by frontend
if (
tunnelConfig.sourceHostId &&
effectiveUserId &&
!tunnelConfig.sourcePassword &&
!tunnelConfig.sourceSSHKey
) {
try {
if (
tunnelConfig.requestingUserId &&
tunnelConfig.requestingUserId !== tunnelConfig.sourceUserId
) {
const { SharedCredentialManager } = await import(
"../utils/shared-credential-manager.js"
const { resolveHostById } = await import("./host-resolver.js");
const resolvedHost = await resolveHostById(
tunnelConfig.sourceHostId,
effectiveUserId,
);
if (resolvedHost) {
resolvedSourceCredentials = {
password: resolvedHost.password,
sshKey: resolvedHost.key,
keyPassword: resolvedHost.keyPassword,
keyType: resolvedHost.keyType,
authMethod: resolvedHost.authType,
};
}
} catch (error) {
tunnelLogger.warn("Failed to resolve source host credentials", {
operation: "tunnel_connect",
tunnelName,
sourceHostId: tunnelConfig.sourceHostId,
error: error instanceof Error ? error.message : "Unknown error",
});
}
} else if (tunnelConfig.sourceCredentialId && effectiveUserId) {
// Legacy: credential resolution from credentialId
try {
if (tunnelConfig.sourceHostId) {
const { resolveHostById } = await import("./host-resolver.js");
const resolvedHost = await resolveHostById(
tunnelConfig.sourceHostId,
effectiveUserId,
);
const sharedCredManager = SharedCredentialManager.getInstance();
if (tunnelConfig.sourceHostId) {
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
tunnelConfig.sourceHostId,
tunnelConfig.requestingUserId,
);
if (sharedCred) {
resolvedSourceCredentials = {
password: sharedCred.password,
sshKey: sharedCred.key,
keyPassword: sharedCred.keyPassword,
keyType: sharedCred.keyType,
authMethod: sharedCred.authType,
};
} else {
const errorMessage = `Cannot connect tunnel '${tunnelName}': shared credentials not available`;
tunnelLogger.error(errorMessage, undefined, {
operation: "tunnel_shared_credentials_unavailable",
tunnelName,
requestingUserId: tunnelConfig.requestingUserId,
sourceUserId: tunnelConfig.sourceUserId,
sourceHostId: tunnelConfig.sourceHostId,
});
broadcastTunnelStatus(tunnelName, {
connected: false,
status: CONNECTION_STATES.FAILED,
reason: errorMessage,
});
tunnelConnecting.delete(tunnelName);
return;
}
}
} else {
const userDataKey = DataCrypto.getUserDataKey(effectiveUserId);
if (userDataKey) {
const credentials = await SimpleDBOps.select(
getDb()
.select()
.from(sshCredentials)
.where(eq(sshCredentials.id, tunnelConfig.sourceCredentialId)),
"ssh_credentials",
effectiveUserId,
);
if (credentials.length > 0) {
const credential = credentials[0];
resolvedSourceCredentials = {
password: credential.password as string | undefined,
sshKey: credential.privateKey as string | undefined,
keyPassword: credential.keyPassword as string | undefined,
keyType: credential.keyType as string | undefined,
authMethod: credential.authType as string,
};
}
if (resolvedHost) {
resolvedSourceCredentials = {
password: resolvedHost.password,
sshKey: resolvedHost.key,
keyPassword: resolvedHost.keyPassword,
keyType: resolvedHost.keyType,
authMethod: resolvedHost.authType,
};
}
}
} catch (error) {
@@ -1153,18 +1103,7 @@ async function connectSSHTunnel(
"ssh-rsa",
"ssh-dss",
],
cipher: [
"chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
"aes128-gcm@openssh.com",
"aes256-ctr",
"aes192-ctr",
"aes128-ctr",
"aes256-cbc",
"aes192-cbc",
"aes128-cbc",
"3des-cbc",
],
cipher: SSH_ALGORITHMS.cipher,
hmac: [
"hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256-etm@openssh.com",
@@ -1329,34 +1268,31 @@ async function killRemoteTunnelByMarker(
authMethod: tunnelConfig.sourceAuthMethod,
};
if (tunnelConfig.sourceCredentialId && tunnelConfig.sourceUserId) {
if (
tunnelConfig.sourceHostId &&
tunnelConfig.sourceUserId &&
!tunnelConfig.sourcePassword &&
!tunnelConfig.sourceSSHKey
) {
try {
const userDataKey = DataCrypto.getUserDataKey(tunnelConfig.sourceUserId);
if (userDataKey) {
const credentials = await SimpleDBOps.select(
getDb()
.select()
.from(sshCredentials)
.where(eq(sshCredentials.id, tunnelConfig.sourceCredentialId)),
"ssh_credentials",
tunnelConfig.sourceUserId,
);
if (credentials.length > 0) {
const credential = credentials[0];
resolvedSourceCredentials = {
password: credential.password as string | undefined,
sshKey: credential.privateKey as string | undefined,
keyPassword: credential.keyPassword as string | undefined,
keyType: credential.keyType as string | undefined,
authMethod: credential.authType as string,
};
}
const { resolveHostById } = await import("./host-resolver.js");
const resolvedHost = await resolveHostById(
tunnelConfig.sourceHostId,
tunnelConfig.sourceUserId,
);
if (resolvedHost) {
resolvedSourceCredentials = {
password: resolvedHost.password,
sshKey: resolvedHost.key,
keyPassword: resolvedHost.keyPassword,
keyType: resolvedHost.keyType,
authMethod: resolvedHost.authType,
};
}
} catch (error) {
tunnelLogger.warn("Failed to resolve source credentials for cleanup", {
tunnelName,
credentialId: tunnelConfig.sourceCredentialId,
sourceHostId: tunnelConfig.sourceHostId,
error: error instanceof Error ? error.message : "Unknown error",
});
}
@@ -1750,7 +1686,7 @@ app.post(
const internalAuthToken = await systemCrypto.getInternalAuthToken();
const allHostsResponse = await axios.get(
"http://localhost:30001/ssh/db/host/internal/all",
"http://localhost:30001/host/db/host/internal/all",
{
headers: {
"Content-Type": "application/json",
@@ -1775,13 +1711,38 @@ app.post(
tunnelConfig.endpointIP = endpointHost.ip;
tunnelConfig.endpointSSHPort = endpointHost.port;
tunnelConfig.endpointUsername = endpointHost.username;
tunnelConfig.endpointPassword = endpointHost.password;
tunnelConfig.endpointAuthMethod = endpointHost.authType;
tunnelConfig.endpointSSHKey = endpointHost.key;
tunnelConfig.endpointKeyPassword = endpointHost.keyPassword;
tunnelConfig.endpointKeyType = endpointHost.keyType;
tunnelConfig.endpointCredentialId = endpointHost.credentialId;
tunnelConfig.endpointUserId = endpointHost.userId;
// Resolve credentials server-side instead of from HTTP response
if (endpointHost.id && endpointHost.userId) {
try {
const { resolveHostById } = await import("./host-resolver.js");
const resolved = await resolveHostById(
endpointHost.id,
endpointHost.userId,
);
if (resolved) {
tunnelConfig.endpointPassword = resolved.password;
tunnelConfig.endpointSSHKey = resolved.key;
tunnelConfig.endpointKeyPassword = resolved.keyPassword;
}
} catch (credError) {
tunnelLogger.warn(
"Failed to resolve endpoint credentials from DB",
{
operation: "tunnel_endpoint_credential_resolve",
endpointHostId: endpointHost.id,
error:
credError instanceof Error
? credError.message
: "Unknown",
},
);
}
}
} catch (resolveError) {
tunnelLogger.error(
"Failed to resolve endpoint host",
@@ -2028,7 +1989,7 @@ async function initializeAutoStartTunnels(): Promise<void> {
const internalAuthToken = await systemCrypto.getInternalAuthToken();
const autostartResponse = await axios.get(
"http://localhost:30001/ssh/db/host/internal",
"http://localhost:30001/host/db/host/internal",
{
headers: {
"Content-Type": "application/json",
@@ -2038,7 +1999,7 @@ async function initializeAutoStartTunnels(): Promise<void> {
);
const allHostsResponse = await axios.get(
"http://localhost:30001/ssh/db/host/internal/all",
"http://localhost:30001/host/db/host/internal/all",
{
headers: {
"Content-Type": "application/json",
@@ -2050,7 +2011,6 @@ async function initializeAutoStartTunnels(): Promise<void> {
const autostartHosts: SSHHost[] = autostartResponse.data || [];
const allHosts: SSHHost[] = allHostsResponse.data || [];
const autoStartTunnels: TunnelConfig[] = [];
tunnelLogger.info(
`Found ${autostartHosts.length} autostart hosts and ${allHosts.length} total hosts for endpointHost resolution`,
);
@@ -2084,11 +2044,7 @@ async function initializeAutoStartTunnels(): Promise<void> {
sourceIP: host.ip,
sourceSSHPort: host.port,
sourceUsername: host.username,
sourcePassword: host.autostartPassword || host.password,
sourceAuthMethod: host.authType,
sourceSSHKey: host.autostartKey || host.key,
sourceKeyPassword:
host.autostartKeyPassword || host.keyPassword,
sourceKeyType: host.keyType,
sourceCredentialId: host.credentialId,
sourceUserId: host.userId,
@@ -2096,20 +2052,8 @@ async function initializeAutoStartTunnels(): Promise<void> {
endpointSSHPort: endpointHost.port,
endpointUsername: endpointHost.username,
endpointHost: tunnelConnection.endpointHost,
endpointPassword:
tunnelConnection.endpointPassword ||
endpointHost.autostartPassword ||
endpointHost.password,
endpointAuthMethod:
tunnelConnection.endpointAuthType || endpointHost.authType,
endpointSSHKey:
tunnelConnection.endpointKey ||
endpointHost.autostartKey ||
endpointHost.key,
endpointKeyPassword:
tunnelConnection.endpointKeyPassword ||
endpointHost.autostartKeyPassword ||
endpointHost.keyPassword,
endpointKeyType:
tunnelConnection.endpointKeyType || endpointHost.keyType,
endpointCredentialId: endpointHost.credentialId,
@@ -50,10 +50,10 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
try {
const date = new Date(timeStr);
parsedTime = isNaN(date.getTime())
? new Date().toISOString()
? timeStr || "unknown"
: date.toISOString();
} catch {
parsedTime = new Date().toISOString();
parsedTime = timeStr || "unknown";
}
recentLogins.push({
@@ -99,21 +99,30 @@ export async function collectLoginStats(client: Client): Promise<LoginStats> {
ip = ipMatch[1];
}
const dateMatch = line.match(/^(\w+\s+\d+\s+\d+:\d+:\d+)/);
const dateMatch = line.match(/^(\w+)\s+(\d+)\s+(\d+:\d+:\d+)/);
if (dateMatch) {
const currentYear = new Date().getFullYear();
timeStr = `${currentYear} ${dateMatch[1]}`;
const [, month, day, time] = dateMatch;
const now = new Date();
const currentYear = now.getFullYear();
const candidate = new Date(`${month} ${day}, ${currentYear} ${time}`);
if (!isNaN(candidate.getTime()) && candidate > now) {
// If parsed date is in the future, it's from last year
timeStr = `${month} ${day}, ${currentYear - 1} ${time}`;
} else {
timeStr = `${month} ${day}, ${currentYear} ${time}`;
}
}
if (user && ip) {
let parsedTime: string;
try {
const date = timeStr ? new Date(timeStr) : new Date();
parsedTime = isNaN(date.getTime())
? new Date().toISOString()
: date.toISOString();
const date = timeStr ? new Date(timeStr) : null;
parsedTime =
date && !isNaN(date.getTime())
? date.toISOString()
: timeStr || "unknown";
} catch {
parsedTime = new Date().toISOString();
parsedTime = timeStr || "unknown";
}
failedLogins.push({
+64 -21
View File
@@ -7,7 +7,11 @@ import { AutoSSLSetup } from "./utils/auto-ssl-setup.js";
import { AuthManager } from "./utils/auth-manager.js";
import { DataCrypto } from "./utils/data-crypto.js";
import { SystemCrypto } from "./utils/system-crypto.js";
import { systemLogger, versionLogger } from "./utils/logger.js";
import {
systemLogger,
versionLogger,
setGlobalLogLevel,
} from "./utils/logger.js";
(async () => {
const initStartTime = Date.now();
@@ -112,29 +116,27 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
await authManager.initialize();
DataCrypto.initialize();
const { OPKSSHBinaryManager } = await import(
"./utils/opkssh-binary-manager.js"
import("./utils/opkssh-binary-manager.js").then(
({ OPKSSHBinaryManager }) => {
OPKSSHBinaryManager.ensureBinary().catch((error) => {
const dataDir =
process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
systemLogger.warn(
"Failed to initialize OPKSSH binary - OPKSSH authentication will not be available",
{
operation: "opkssh_binary_init_failed",
error: error instanceof Error ? error.message : "Unknown error",
stack: error instanceof Error ? error.stack : undefined,
platform: process.platform,
arch: process.arch,
dataDir,
},
);
});
},
);
try {
await OPKSSHBinaryManager.ensureBinary();
} catch (error) {
const dataDir =
process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
systemLogger.warn(
"Failed to initialize OPKSSH binary - OPKSSH authentication will not be available",
{
operation: "opkssh_binary_init_failed",
error: error instanceof Error ? error.message : "Unknown error",
stack: error instanceof Error ? error.stack : undefined,
platform: process.platform,
arch: process.arch,
dataDir,
},
);
}
await import("./database/database.js");
await import("./ssh/terminal.js");
await import("./ssh/tunnel.js");
await import("./ssh/file-manager.js");
@@ -143,6 +145,47 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
await import("./ssh/docker-console.js");
await import("./dashboard.js");
// Initialize log level from database settings
const { getDb: getDbForSettings } = await import("./database/db/index.js");
const settingsDb = getDbForSettings();
const logLevelRow = settingsDb.$client
.prepare("SELECT value FROM settings WHERE key = 'log_level'")
.get() as { value: string } | undefined;
if (logLevelRow) {
setGlobalLogLevel(logLevelRow.value);
systemLogger.info(`Log level set to: ${logLevelRow.value}`, {
operation: "log_level_init",
});
}
// Initialize Guacamole server for RDP/VNC/Telnet support
const { getDb: getDbForGuac } = await import("./database/db/index.js");
const guacDb = getDbForGuac();
const guacEnabledRow = guacDb.$client
.prepare("SELECT value FROM settings WHERE key = 'guac_enabled'")
.get() as { value: string } | undefined;
const guacEnabled = guacEnabledRow
? guacEnabledRow.value !== "false"
: true;
if (process.env.ENABLE_GUACAMOLE !== "false" && guacEnabled) {
import("./guacamole/guacamole-server.js")
.then(() => {
systemLogger.info("Guacamole server initialized", {
operation: "guac_init",
});
})
.catch((error) => {
systemLogger.warn(
"Failed to initialize Guacamole server (guacd may not be available)",
{
operation: "guac_init_skip",
error: error instanceof Error ? error.message : "Unknown error",
},
);
});
}
systemLogger.success("Termix backend started successfully", {
operation: "backend_init_complete",
port: process.env.PORT || 4090,
+63 -41
View File
@@ -4,7 +4,11 @@ import { SystemCrypto } from "./system-crypto.js";
import { DataCrypto } from "./data-crypto.js";
import { databaseLogger, authLogger } from "./logger.js";
import type { Request, Response, NextFunction } from "express";
import { db } from "../database/db/index.js";
import {
db,
getSqlite,
saveMemoryDatabaseToFile,
} from "../database/db/index.js";
import { sessions, trustedDevices } from "../database/db/schema.js";
import { eq, and, sql } from "drizzle-orm";
import { nanoid } from "nanoid";
@@ -99,7 +103,7 @@ class AuthManager {
const sessionDurationMs =
deviceType === "desktop" || deviceType === "mobile"
? 30 * 24 * 60 * 60 * 1000
: 2 * 60 * 60 * 1000;
: 24 * 60 * 60 * 1000;
const authenticated = await this.userCrypto.authenticateOIDCUser(
userId,
@@ -121,7 +125,7 @@ class AuthManager {
const sessionDurationMs =
deviceType === "desktop" || deviceType === "mobile"
? 30 * 24 * 60 * 60 * 1000
: 2 * 60 * 60 * 1000;
: 24 * 60 * 60 * 1000;
const authenticated = await this.userCrypto.authenticateUser(
userId,
@@ -154,9 +158,8 @@ class AuthManager {
return;
}
const { getSqlite, saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { getSqlite, saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
const sqlite = getSqlite();
@@ -171,9 +174,8 @@ class AuthManager {
}
try {
const { CredentialSystemEncryptionMigration } = await import(
"./credential-system-encryption-migration.js"
);
const { CredentialSystemEncryptionMigration } =
await import("./credential-system-encryption-migration.js");
const credMigration = new CredentialSystemEncryptionMigration();
const credResult = await credMigration.migrateUserCredentials(userId);
@@ -208,15 +210,20 @@ class AuthManager {
): Promise<string> {
const jwtSecret = await this.systemCrypto.getJWTSecret();
const timeoutRow = db.$client
.prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'")
.get() as { value: string } | undefined;
const defaultExpiry = `${timeoutRow ? parseInt(timeoutRow.value, 10) || 24 : 24}h`;
let expiresIn = options.expiresIn;
if (!expiresIn && !options.pendingTOTP) {
if (options.rememberMe) {
expiresIn = "30d";
} else {
expiresIn = "2h";
expiresIn = defaultExpiry;
}
} else if (!expiresIn) {
expiresIn = "2h";
expiresIn = defaultExpiry;
}
const payload: JWTPayload = { userId };
@@ -250,9 +257,8 @@ class AuthManager {
});
try {
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
} catch (saveError) {
databaseLogger.error(
@@ -280,7 +286,7 @@ class AuthManager {
private parseExpiresIn(expiresIn: string): number {
const match = expiresIn.match(/^(\d+)([smhd])$/);
if (!match) return 2 * 60 * 60 * 1000;
if (!match) return 24 * 60 * 60 * 1000;
const value = parseInt(match[1]);
const unit = match[2];
@@ -295,7 +301,7 @@ class AuthManager {
case "d":
return value * 24 * 60 * 60 * 1000;
default:
return 2 * 60 * 60 * 1000;
return 24 * 60 * 60 * 1000;
}
}
@@ -364,9 +370,8 @@ class AuthManager {
await db.delete(sessions).where(eq(sessions.id, sessionId));
try {
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
} catch (saveError) {
databaseLogger.error(
@@ -423,9 +428,8 @@ class AuthManager {
}
try {
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
} catch (saveError) {
databaseLogger.error(
@@ -466,9 +470,8 @@ class AuthManager {
.where(sql`${sessions.expiresAt} < datetime('now')`);
try {
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
} catch (saveError) {
databaseLogger.error(
@@ -531,7 +534,7 @@ class AuthManager {
getSecureCookieOptions(
req: RequestWithHeaders,
maxAge: number = 2 * 60 * 60 * 1000,
maxAge: number = 24 * 60 * 60 * 1000,
) {
return {
httpOnly: false,
@@ -573,6 +576,13 @@ class AuthManager {
return res.status(401).json({ error: "Invalid token" });
}
if (payload.pendingTOTP) {
return res.status(401).json({
error: "TOTP verification required",
code: "TOTP_REQUIRED",
});
}
if (payload.sessionId) {
try {
const sessionRecords = await db
@@ -613,9 +623,8 @@ class AuthManager {
.where(eq(sessions.id, payload.sessionId))
.then(async () => {
try {
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
const remainingSessions = await db
@@ -715,6 +724,13 @@ class AuthManager {
return res.status(401).json({ error: "Invalid token" });
}
if (payload.pendingTOTP) {
return res.status(401).json({
error: "TOTP verification required",
code: "TOTP_REQUIRED",
});
}
try {
const { db } = await import("../database/db/index.js");
const { users } = await import("../database/db/schema.js");
@@ -759,9 +775,8 @@ class AuthManager {
await db.delete(sessions).where(eq(sessions.id, sessionId));
try {
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
} catch (saveError) {
databaseLogger.error(
@@ -793,6 +808,22 @@ class AuthManager {
});
}
} else {
try {
await db.delete(sessions).where(eq(sessions.userId, userId));
try {
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
} catch {
// best effort
}
} catch (error) {
databaseLogger.error("Failed to revoke all sessions on logout", error, {
operation: "session_revoke_all_failed",
userId,
});
}
this.userCrypto.logoutUser(userId);
}
}
@@ -827,9 +858,6 @@ class AuthManager {
);
}
/**
* Check if device is trusted for TOTP bypass
*/
async isTrustedDevice(
userId: string,
deviceFingerprint: string,
@@ -875,9 +903,6 @@ class AuthManager {
}
}
/**
* Add device to trusted list for TOTP bypass
*/
async addTrustedDevice(
userId: string,
deviceFingerprint: string,
@@ -925,9 +950,6 @@ class AuthManager {
}
}
/**
* Remove trusted device
*/
async removeTrustedDevice(
userId: string,
deviceFingerprint: string,
+71
View File
@@ -0,0 +1,71 @@
import cors from "cors";
import type { Request, Response, NextFunction } from "express";
import { getRequestOrigin } from "./request-origin.js";
const DEV_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
const ELECTRON_FILE_ORIGIN = "file://";
function getAllowedOrigins(): string[] {
const envOrigins = process.env.CORS_ALLOWED_ORIGINS;
if (!envOrigins) return [];
return envOrigins
.split(",")
.map((o) => o.trim())
.filter(Boolean);
}
function isLocalRequest(req: Request): boolean {
const remoteAddr = req.socket?.remoteAddress || req.ip || "";
return (
remoteAddr === "127.0.0.1" ||
remoteAddr === "::1" ||
remoteAddr === "::ffff:127.0.0.1"
);
}
export function createCorsMiddleware(
methods: string[] = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
extraHeaders: string[] = [],
) {
const allowedHeaders = [
"Origin",
"X-Requested-With",
"Content-Type",
"Accept",
"Authorization",
"User-Agent",
"X-Electron-App",
"Cache-Control",
...extraHeaders,
];
return (req: Request, res: Response, next: NextFunction) => {
const handler = cors({
origin: (origin, callback) => {
// No origin = same-origin or non-browser request (curl, internal service calls)
if (!origin) return callback(null, true);
// Requests coming from localhost (nginx proxy, internal service calls)
if (isLocalRequest(req)) return callback(null, true);
if (DEV_ORIGINS.includes(origin)) return callback(null, true);
if (origin.startsWith(ELECTRON_FILE_ORIGIN))
return callback(null, true);
const configured = getAllowedOrigins();
if (configured.length === 0) return callback(null, true);
if (configured.includes("*") || configured.includes(origin))
return callback(null, true);
const sameOrigin = getRequestOrigin(req);
if (origin === sameOrigin) return callback(null, true);
callback(new Error("Not allowed by CORS"));
},
credentials: true,
methods,
allowedHeaders,
});
handler(req, res, next);
};
}
@@ -107,24 +107,24 @@ export class CredentialSystemEncryptionMigration {
migrated++;
} catch (error) {
databaseLogger.error("Failed to migrate credential", error, {
credentialId: cred.id,
userId,
});
databaseLogger.warn(
`Skipping credential migration for credential ${cred.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
{
operation: "credential_migration_skip",
credentialId: cred.id,
userId,
},
);
failed++;
}
}
return { migrated, failed, skipped };
} catch (error) {
databaseLogger.error(
"Credential system encryption migration failed",
error,
{
operation: "credential_migration_failed",
userId,
error: error instanceof Error ? error.message : "Unknown error",
},
);
databaseLogger.warn("Credential system encryption migration incomplete", {
operation: "credential_migration_incomplete",
userId,
error: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
}
+29 -1
View File
@@ -2,6 +2,33 @@ import chalk from "chalk";
export type LogLevel = "debug" | "info" | "warn" | "error" | "success";
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
debug: 0,
info: 1,
success: 1,
warn: 2,
error: 3,
};
let globalLogLevel: LogLevel = "info";
export function setGlobalLogLevel(level: string): void {
const normalized = level.toLowerCase();
if (normalized in LOG_LEVEL_PRIORITY) {
globalLogLevel = normalized as LogLevel;
}
}
export function getGlobalLogLevel(): LogLevel {
return globalLogLevel;
}
// Initialize from environment variable if set
const envLogLevel = process.env.LOG_LEVEL?.toLowerCase();
if (envLogLevel && envLogLevel in LOG_LEVEL_PRIORITY) {
globalLogLevel = envLogLevel as LogLevel;
}
export interface LogContext {
service?: string;
operation?: string;
@@ -140,7 +167,7 @@ export class Logger {
}
private shouldLog(level: LogLevel, message: string): boolean {
if (level === "debug" && process.env.NODE_ENV === "production") {
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[globalLogLevel]) {
return false;
}
@@ -254,5 +281,6 @@ export const authLogger = new Logger("AUTH", "🔐", "#ef4444");
export const systemLogger = new Logger("SYSTEM", "🚀", "#14b8a6");
export const versionLogger = new Logger("VERSION", "📦", "#8b5cf6");
export const dashboardLogger = new Logger("DASHBOARD", "📊", "#ec4899");
export const guacLogger = new Logger("GUACAMOLE", "🖼️", "#ff6b6b");
export const logger = systemLogger;
+2 -2
View File
@@ -168,7 +168,7 @@ export class OPKSSHBinaryManager {
const osMap: Record<string, string> = {
win32: "windows",
linux: "linux",
darwin: "darwin",
darwin: "osx",
};
const archMap: Record<string, string> = {
@@ -209,7 +209,7 @@ export class OPKSSHBinaryManager {
const osMap: Record<string, string> = {
win32: "windows",
linux: "linux",
darwin: "darwin",
darwin: "osx",
};
const archMap: Record<string, string> = {
+6 -6
View File
@@ -4,7 +4,7 @@ import {
hostAccess,
roles,
userRoles,
sshData,
hosts,
users,
} from "../database/db/schema.js";
import { eq, and, or, isNull, gte, sql } from "drizzle-orm";
@@ -167,8 +167,8 @@ class PermissionManager {
try {
const host = await db
.select()
.from(sshData)
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId)))
.from(hosts)
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId)))
.limit(1);
if (host.length > 0) {
@@ -210,9 +210,9 @@ class PermissionManager {
const access = sharedAccess[0];
const hostOwnerCheck = await db
.select({ ownerId: sshData.userId })
.from(sshData)
.where(eq(sshData.id, hostId))
.select({ ownerId: hosts.userId })
.from(hosts)
.where(eq(hosts.id, hostId))
.limit(1);
if (hostOwnerCheck.length > 0 && hostOwnerCheck[0].ownerId === userId) {
+65
View File
@@ -0,0 +1,65 @@
import type { Request } from "express";
import type { IncomingMessage } from "http";
export function getRequestOrigin(req: Request | IncomingMessage): string {
let protocol: string;
const protoHeader = req.headers["x-forwarded-proto"];
if (protoHeader) {
const raw =
typeof protoHeader === "string"
? protoHeader.split(",")[0].trim()
: protoHeader[0];
// Normalize WebSocket protocols to their HTTP equivalents
protocol = raw === "wss" ? "https" : raw === "ws" ? "http" : raw;
} else if ("protocol" in req && req.protocol) {
protocol = req.protocol;
} else {
protocol = (req.socket as unknown as { encrypted?: boolean }).encrypted
? "https"
: "http";
}
const portHeader = req.headers["x-forwarded-port"];
let port: string | undefined =
typeof portHeader === "string"
? portHeader.split(",")[0].trim()
: undefined;
const hostHeaderRaw =
req.headers["x-forwarded-host"] || req.headers.host || "localhost";
const hostHeader =
typeof hostHeaderRaw === "string"
? hostHeaderRaw.split(",")[0].trim()
: String(hostHeaderRaw);
if (!port && hostHeader.includes(":")) {
const parts = hostHeader.split(":");
if (parts.length === 2 && !parts[0].includes("[")) {
port = parts[1];
}
}
const hostWithoutPort = hostHeader.split(":")[0];
if (port) {
const isDefaultPort =
(protocol === "http" && port === "80") ||
(protocol === "https" && port === "443");
return isDefaultPort
? `${protocol}://${hostWithoutPort}`
: `${protocol}://${hostWithoutPort}:${port}`;
}
return `${protocol}://${hostWithoutPort}`;
}
export function getRequestOriginWithForceHTTPS(
req: Request | IncomingMessage,
): string {
if (process.env.OIDC_FORCE_HTTPS === "true") {
const origin = getRequestOrigin(req);
return origin.replace(/^http:/, "https:");
}
return getRequestOrigin(req);
}
+24 -12
View File
@@ -4,7 +4,7 @@ import {
sshCredentials,
hostAccess,
userRoles,
sshData,
hosts,
} from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { DataCrypto } from "./data-crypto.js";
@@ -190,15 +190,27 @@ class SharedCredentialManager {
const cred = sharedCred[0].shared_credentials;
if (cred.needsReEncryption) {
databaseLogger.warn(
"Shared credential needs re-encryption but cannot be accessed yet",
{
operation: "get_shared_credential_pending",
hostId,
userId,
},
);
return null;
await this.reEncryptSharedCredential(cred.id, userId);
const refreshed = await db
.select()
.from(sharedCredentials)
.where(eq(sharedCredentials.id, cred.id))
.limit(1);
if (refreshed.length === 0 || refreshed[0].needsReEncryption) {
databaseLogger.warn(
"Shared credential needs re-encryption but cannot be accessed yet",
{
operation: "get_shared_credential_pending",
hostId,
userId,
},
);
return null;
}
return this.decryptSharedCredential(refreshed[0], userDEK);
}
return this.decryptSharedCredential(cred, userDEK);
@@ -367,7 +379,7 @@ class SharedCredentialManager {
cred.keyPassword,
ownerDEK,
credentialId,
"key_password",
"keyPassword",
)
: undefined,
keyType: cred.keyType,
@@ -587,7 +599,7 @@ class SharedCredentialManager {
const access = await db
.select()
.from(hostAccess)
.innerJoin(sshData, eq(hostAccess.hostId, sshData.id))
.innerJoin(hosts, eq(hostAccess.hostId, hosts.id))
.where(eq(hostAccess.id, cred.hostAccessId))
.limit(1);
+71
View File
@@ -0,0 +1,71 @@
import crypto from "crypto";
import type { ConnectConfig, CipherAlgorithm } from "ssh2";
// Maps SSH cipher names to their OpenSSL equivalents (as used by ssh2 internally)
const SSH_CIPHER_SSL_NAME: Partial<Record<CipherAlgorithm, string>> = {
"chacha20-poly1305@openssh.com": "chacha20",
"aes256-gcm@openssh.com": "aes-256-gcm",
"aes128-gcm@openssh.com": "aes-128-gcm",
"aes256-ctr": "aes-256-ctr",
"aes192-ctr": "aes-192-ctr",
"aes128-ctr": "aes-128-ctr",
"aes256-cbc": "aes-256-cbc",
"aes192-cbc": "aes-192-cbc",
"aes128-cbc": "aes-128-cbc",
"3des-cbc": "des-ede3-cbc",
};
const availableCiphers = new Set(crypto.getCiphers());
function filterCiphers(list: CipherAlgorithm[]): CipherAlgorithm[] {
return list.filter((name) => {
const sslName = SSH_CIPHER_SSL_NAME[name];
return !sslName || availableCiphers.has(sslName);
});
}
export const SSH_ALGORITHMS: NonNullable<ConnectConfig["algorithms"]> = {
kex: [
"curve25519-sha256",
"curve25519-sha256@libssh.org",
"ecdh-sha2-nistp521",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp256",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group14-sha256",
"diffie-hellman-group14-sha1",
"diffie-hellman-group-exchange-sha1",
"diffie-hellman-group1-sha1",
],
serverHostKey: [
"ssh-ed25519",
"ecdsa-sha2-nistp521",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp256",
"rsa-sha2-512",
"rsa-sha2-256",
"ssh-rsa",
"ssh-dss",
],
cipher: filterCiphers([
"chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
"aes128-gcm@openssh.com",
"aes256-ctr",
"aes192-ctr",
"aes128-ctr",
"aes256-cbc",
"aes192-cbc",
"aes128-cbc",
"3des-cbc",
]),
hmac: [
"hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512",
"hmac-sha2-256",
"hmac-sha1",
"hmac-md5",
],
compress: ["none", "zlib@openssh.com", "zlib"],
};
+12 -1
View File
@@ -23,7 +23,18 @@ export function detectPlatform(req: Request): DeviceType {
return "mobile";
}
if (userAgent.includes("Android")) {
const isDesktopOS =
userAgent.includes("Windows") ||
userAgent.includes("Macintosh") ||
userAgent.includes("Mac OS X") ||
userAgent.includes("X11") ||
userAgent.includes("Linux x86_64");
if (
(userAgent.includes("Android") && !isDesktopOS) ||
userAgent.includes("iPhone") ||
userAgent.includes("iPad")
) {
return "mobile";
}
+6 -9
View File
@@ -303,9 +303,8 @@ class UserCrypto {
await this.storeKEKSalt(userId, newKekSalt);
await this.storeEncryptedDEK(userId, newEncryptedDEK);
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
oldKEK.fill(0);
@@ -341,9 +340,8 @@ class UserCrypto {
await this.storeKEKSalt(userId, newKekSalt);
await this.storeEncryptedDEK(userId, newEncryptedDEK);
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
newKEK.fill(0);
@@ -418,9 +416,8 @@ class UserCrypto {
},
);
const { saveMemoryDatabaseToFile } = await import(
"../database/db/index.js"
);
const { saveMemoryDatabaseToFile } =
await import("../database/db/index.js");
await saveMemoryDatabaseToFile();
} catch (error) {
databaseLogger.error("Failed to convert to OIDC encryption", error, {
+3 -3
View File
@@ -1,7 +1,7 @@
import { getDb } from "../database/db/index.js";
import {
users,
sshData,
hosts,
sshCredentials,
fileManagerRecent,
fileManagerPinned,
@@ -74,8 +74,8 @@ class UserDataExport {
const sshHosts = await getDb()
.select()
.from(sshData)
.where(eq(sshData.userId, userId));
.from(hosts)
.where(eq(hosts.userId, userId));
const processedSshHosts =
format === "plaintext" && userDataKey
? sshHosts.map((host) =>
+11 -13
View File
@@ -1,7 +1,7 @@
import { getDb } from "../database/db/index.js";
import {
users,
sshData,
hosts,
sshCredentials,
fileManagerRecent,
fileManagerPinned,
@@ -179,13 +179,13 @@ class UserDataImport {
const existing = await getDb()
.select()
.from(sshData)
.from(hosts)
.where(
and(
eq(sshData.userId, targetUserId),
eq(sshData.ip, host.ip as string),
eq(sshData.port, host.port as number),
eq(sshData.username, host.username as string),
eq(hosts.userId, targetUserId),
eq(hosts.ip, host.ip as string),
eq(hosts.port, host.port as number),
eq(hosts.username, host.username as string),
),
);
@@ -218,15 +218,13 @@ class UserDataImport {
if (existing.length > 0 && options.replaceExisting) {
await getDb()
.update(sshData)
.set(processedHostData as unknown as typeof sshData.$inferInsert)
.where(eq(sshData.id, existing[0].id));
.update(hosts)
.set(processedHostData as unknown as typeof hosts.$inferInsert)
.where(eq(hosts.id, existing[0].id));
} else {
await getDb()
.insert(sshData)
.values(
processedHostData as unknown as typeof sshData.$inferInsert,
);
.insert(hosts)
.values(processedHostData as unknown as typeof hosts.$inferInsert);
}
imported++;
} catch (error) {
+46
View File
@@ -0,0 +1,46 @@
import dgram from "dgram";
const MAC_REGEX = /^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$/;
function parseMac(mac: string): Buffer {
return Buffer.from(mac.replace(/[:-]/g, ""), "hex");
}
function buildMagicPacket(mac: string): Buffer {
const macBytes = parseMac(mac);
const packet = Buffer.alloc(102);
packet.fill(0xff, 0, 6);
for (let i = 0; i < 16; i++) {
macBytes.copy(packet, 6 + i * 6);
}
return packet;
}
export function isValidMac(mac: string): boolean {
return MAC_REGEX.test(mac);
}
export function sendWakeOnLan(mac: string): Promise<void> {
return new Promise((resolve, reject) => {
if (!isValidMac(mac)) {
return reject(new Error("Invalid MAC address"));
}
const packet = buildMagicPacket(mac);
const socket = dgram.createSocket("udp4");
socket.once("error", (err) => {
socket.close();
reject(err);
});
socket.bind(() => {
socket.setBroadcast(true);
socket.send(packet, 0, packet.length, 9, "255.255.255.255", (err) => {
socket.close();
if (err) reject(err);
else resolve();
});
});
});
}
+37 -5
View File
@@ -1,7 +1,14 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
type Theme =
| "dark"
| "light"
| "system"
| "dracula"
| "gentlemansChoice"
| "midnightEspresso"
| "catppuccinMocha";
type ThemeProviderProps = {
children: React.ReactNode;
@@ -12,11 +19,13 @@ type ThemeProviderProps = {
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
setThemePreview: (theme: Theme | null) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
setThemePreview: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
@@ -30,13 +39,23 @@ export function ThemeProvider({
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
);
const [previewTheme, setPreviewTheme] = useState<Theme | null>(null);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
root.classList.remove(
"light",
"dark",
"dracula",
"gentlemansChoice",
"midnightEspresso",
"catppuccinMocha",
);
if (theme === "system") {
const activeTheme = previewTheme || theme;
if (activeTheme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
@@ -46,8 +65,18 @@ export function ThemeProvider({
return;
}
root.classList.add(theme);
}, [theme]);
root.classList.add(activeTheme);
const darkCustomThemes: Theme[] = [
"dracula",
"gentlemansChoice",
"midnightEspresso",
"catppuccinMocha",
];
if (darkCustomThemes.includes(activeTheme)) {
root.classList.add("dark");
}
}, [theme, previewTheme]);
const value = {
theme,
@@ -55,6 +84,9 @@ export function ThemeProvider({
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
setThemePreview: (theme: Theme | null) => {
setPreviewTheme(theme);
},
};
return (
+1 -2
View File
@@ -37,8 +37,7 @@ const buttonVariants = cva(
);
export interface ButtonProps
extends React.ComponentProps<"button">,
VariantProps<typeof buttonVariants> {
extends React.ComponentProps<"button">, VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
+11 -1
View File
@@ -39,9 +39,19 @@ const Toaster = ({ ...props }: ToasterProps) => {
message: rateLimitedToast,
});
const darkCustomThemes = [
"dracula",
"gentlemansChoice",
"midnightEspresso",
"catppuccinMocha",
];
const sonnerTheme: ToasterProps["theme"] = darkCustomThemes.includes(theme)
? "dark"
: (theme as ToasterProps["theme"]);
return (
<Sonner
theme={theme as ToasterProps["theme"]}
theme={sonnerTheme}
className="toaster group"
style={
{
+57
View File
@@ -671,6 +671,62 @@ export const TERMINAL_THEMES: Record<string, TerminalTheme> = {
brightWhite: "#a6adc8",
},
},
gentlemansChoice: {
name: "Gentleman's Choice",
category: "dark",
colors: {
background: "#1a1c1a",
foreground: "#d1c7a3",
cursor: "#d1c7a3",
cursorAccent: "#1a1c1a",
selectionBackground: "#3e4437",
black: "#1a1c1a",
red: "#9d3a3a",
green: "#5a7a3a",
yellow: "#b39a3a",
blue: "#3a5a7a",
magenta: "#7a3a5a",
cyan: "#3a7a7a",
white: "#d1c7a3",
brightBlack: "#3e4437",
brightRed: "#bf4a4a",
brightGreen: "#7a9a4a",
brightYellow: "#d1b34a",
brightBlue: "#4a7abf",
brightMagenta: "#9a4abf",
brightCyan: "#4abfbf",
brightWhite: "#e3dbc3",
},
},
midnightEspresso: {
name: "Midnight Espresso",
category: "dark",
colors: {
background: "#120f0d",
foreground: "#ceb195",
cursor: "#ceb195",
cursorAccent: "#120f0d",
selectionBackground: "#3d2b1f",
black: "#120f0d",
red: "#a05a4a",
green: "#7a8a5a",
yellow: "#b08a4a",
blue: "#5a7a9a",
magenta: "#8a5a7a",
cyan: "#5a8a8a",
white: "#ceb195",
brightBlack: "#3d2b1f",
brightRed: "#c07a6a",
brightGreen: "#9aaa7a",
brightYellow: "#d0aa6a",
brightBlue: "#7a9aba",
brightMagenta: "#aa7aba",
brightCyan: "#7ababa",
brightWhite: "#e0cbb5",
},
},
};
export const TERMINAL_FONTS = [
@@ -764,6 +820,7 @@ export const DEFAULT_TERMINAL_CONFIG = {
sudoPasswordAutoFill: false,
keepaliveInterval: undefined as number | undefined,
keepaliveCountMax: undefined as number | undefined,
autoTmux: false,
};
export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG;
+260
View File
@@ -233,6 +233,266 @@
--bg-overlay: rgba(0, 0, 0, 0.7);
}
.dracula {
--background: #282a36;
--foreground: #ffffff;
--card: #343746;
--card-foreground: #ffffff;
--popover: #343746;
--popover-foreground: #ffffff;
--primary: #ffffff;
--primary-foreground: #282a36;
--secondary: #44475a;
--secondary-foreground: #ffffff;
--muted: #343746;
--muted-foreground: #a6accd;
--accent: #44475a;
--accent-foreground: #ffffff;
--destructive: #ff5555;
--border: #44475a;
--input: #343746;
--ring: #8be9fd;
--chart-1: #8be9fd;
--chart-2: #50fa7b;
--chart-3: #ffb86c;
--chart-4: #bd93f9;
--chart-5: #ff79c6;
--sidebar: #1e1f29;
--sidebar-foreground: #ffffff;
--sidebar-primary: #8be9fd;
--sidebar-primary-foreground: #1e1f29;
--sidebar-accent: #44475a;
--sidebar-accent-foreground: #ffffff;
--sidebar-border: #44475a;
--sidebar-ring: #8be9fd;
--bg-base: #282a36;
--bg-elevated: #343746;
--bg-surface: #343746;
--bg-surface-hover: #44475a;
--bg-input: #343746;
--bg-deepest: #1e1f29;
--bg-header: #1e1f29;
--bg-button: #343746;
--bg-active: #44475a;
--bg-light: #343746;
--bg-subtle: #1e1f29;
--bg-interact: #44475a;
--border-base: #44475a;
--border-panel: #1e1f29;
--border-subtle: #44475a;
--border-medium: #44475a;
--bg-hover: #44475a;
--bg-hover-alt: #44475a;
--bg-pressed: #1e1f29;
--border-hover: #6272a4;
--border-active: #8be9fd;
--foreground-secondary: #a6accd;
--foreground-subtle: #6272a4;
--scrollbar-thumb: #44475a;
--scrollbar-thumb-hover: #6272a4;
--scrollbar-track: #282a36;
--bg-overlay: rgba(0, 0, 0, 0.8);
}
.gentlemansChoice {
--background: #1a1c1a;
--foreground: #d1c7a3;
--card: #2a2c2a;
--card-foreground: #d1c7a3;
--popover: #2a2c2a;
--popover-foreground: #d1c7a3;
--primary: #d1c7a3;
--primary-foreground: #1a1c1a;
--secondary: #3e4437;
--secondary-foreground: #d1c7a3;
--muted: #2a2c2a;
--muted-foreground: #8e8463;
--accent: #3e4437;
--accent-foreground: #d1c7a3;
--destructive: #9d3a3a;
--border: #3e4437;
--input: #2a2c2a;
--ring: #5a7a3a;
--chart-1: #5a7a3a;
--chart-2: #3a5a7a;
--chart-3: #b39a3a;
--chart-4: #7a3a5a;
--chart-5: #3a7a7a;
--sidebar: #151715;
--sidebar-foreground: #d1c7a3;
--sidebar-primary: #5a7a3a;
--sidebar-primary-foreground: #151715;
--sidebar-accent: #3e4437;
--sidebar-accent-foreground: #d1c7a3;
--sidebar-border: #3e4437;
--sidebar-ring: #5a7a3a;
--bg-base: #1a1c1a;
--bg-elevated: #2a2c2a;
--bg-surface: #2a2c2a;
--bg-surface-hover: #3e4437;
--bg-input: #2a2c2a;
--bg-deepest: #151715;
--bg-header: #151715;
--bg-button: #2a2c2a;
--bg-active: #3e4437;
--bg-light: #2a2c2a;
--bg-subtle: #151715;
--bg-interact: #3e4437;
--border-base: #3e4437;
--border-panel: #151715;
--border-subtle: #3e4437;
--border-medium: #3e4437;
--bg-hover: #3e4437;
--bg-hover-alt: #3e4437;
--bg-pressed: #151715;
--border-hover: #5a7a3a;
--border-active: #d1c7a3;
--foreground-secondary: #8e8463;
--foreground-subtle: #5e5841;
--scrollbar-thumb: #3e4437;
--scrollbar-thumb-hover: #5e5841;
--scrollbar-track: #1a1c1a;
--bg-overlay: rgba(0, 0, 0, 0.8);
}
.midnightEspresso {
--background: #120f0d;
--foreground: #ceb195;
--card: #1f1a17;
--card-foreground: #ceb195;
--popover: #1f1a17;
--popover-foreground: #ceb195;
--primary: #ceb195;
--primary-foreground: #120f0d;
--secondary: #3d2b1f;
--secondary-foreground: #ceb195;
--muted: #1f1a17;
--muted-foreground: #9a8a7a;
--accent: #3d2b1f;
--accent-foreground: #ceb195;
--destructive: #a05a4a;
--border: #3d2b1f;
--input: #1f1a17;
--ring: #7a8a5a;
--chart-1: #7a8a5a;
--chart-2: #5a7a9a;
--chart-3: #b08a4a;
--chart-4: #8a5a7a;
--chart-5: #5a8a8a;
--sidebar: #0d0b0a;
--sidebar-foreground: #ceb195;
--sidebar-primary: #7a8a5a;
--sidebar-primary-foreground: #0d0b0a;
--sidebar-accent: #3d2b1f;
--sidebar-accent-foreground: #ceb195;
--sidebar-border: #3d2b1f;
--sidebar-ring: #7a8a5a;
--bg-base: #120f0d;
--bg-elevated: #1f1a17;
--bg-surface: #1f1a17;
--bg-surface-hover: #3d2b1f;
--bg-input: #1f1a17;
--bg-deepest: #0d0b0a;
--bg-header: #0d0b0a;
--bg-button: #1f1a17;
--bg-active: #3d2b1f;
--bg-light: #1f1a17;
--bg-subtle: #0d0b0a;
--bg-interact: #3d2b1f;
--border-base: #3d2b1f;
--border-panel: #0d0b0a;
--border-subtle: #3d2b1f;
--border-medium: #3d2b1f;
--bg-hover: #3d2b1f;
--bg-hover-alt: #3d2b1f;
--bg-pressed: #0d0b0a;
--border-hover: #7a8a5a;
--border-active: #ceb195;
--foreground-secondary: #9a8a7a;
--foreground-subtle: #6a5a4a;
--scrollbar-thumb: #3d2b1f;
--scrollbar-thumb-hover: #6a5a4a;
--scrollbar-track: #120f0d;
--bg-overlay: rgba(0, 0, 0, 0.8);
}
.catppuccinMocha {
--background: #1e1e2e;
--foreground: #cdd6f4;
--card: #181825;
--card-foreground: #cdd6f4;
--popover: #181825;
--popover-foreground: #cdd6f4;
--primary: #cdd6f4;
--primary-foreground: #1e1e2e;
--secondary: #313244;
--secondary-foreground: #cdd6f4;
--muted: #181825;
--muted-foreground: #a6adc8;
--accent: #313244;
--accent-foreground: #cdd6f4;
--destructive: #f38ba8;
--border: #313244;
--input: #181825;
--ring: #89b4fa;
--chart-1: #89b4fa;
--chart-2: #a6e3a1;
--chart-3: #f9e2af;
--chart-4: #f5c2e7;
--chart-5: #94e2d5;
--sidebar: #11111b;
--sidebar-foreground: #cdd6f4;
--sidebar-primary: #89b4fa;
--sidebar-primary-foreground: #11111b;
--sidebar-accent: #313244;
--sidebar-accent-foreground: #cdd6f4;
--sidebar-border: #313244;
--sidebar-ring: #89b4fa;
--bg-base: #1e1e2e;
--bg-elevated: #181825;
--bg-surface: #181825;
--bg-surface-hover: #313244;
--bg-input: #181825;
--bg-deepest: #11111b;
--bg-header: #11111b;
--bg-button: #181825;
--bg-active: #313244;
--bg-light: #181825;
--bg-subtle: #11111b;
--bg-interact: #313244;
--border-base: #313244;
--border-panel: #11111b;
--border-subtle: #313244;
--border-medium: #313244;
--bg-hover: #313244;
--bg-hover-alt: #313244;
--bg-pressed: #11111b;
--border-hover: #89b4fa;
--border-active: #cdd6f4;
--foreground-secondary: #a6adc8;
--foreground-subtle: #7f849c;
--scrollbar-thumb: #313244;
--scrollbar-thumb-hover: #45475a;
--scrollbar-track: #1e1e2e;
--bg-overlay: rgba(0, 0, 0, 0.8);
}
@layer base {
html,
body {
+42 -45
View File
@@ -1,13 +1,25 @@
/**
* DatabaseHealthMonitor
*
* Non-blocking health tracker for backend/database connectivity. The
* monitor no longer gates the whole UI: there is no full-screen overlay.
* When a transient failure is observed we emit a "degraded" event so the
* UI can surface a persistent but non-intrusive toast. A success from any
* API request clears the state. Session-expired events are also relayed
* to the UI.
*
* The previous "database-connection-lost" / "database-connection-restored"
* events have been retired along with the overlay. Listeners should use
* "database-connection-degraded" / "database-connection-degraded-cleared"
* to reflect the current UX contract: users can keep working regardless
* of backend hiccups and are simply informed via a toast.
*/
type EventListener = (...args: any[]) => void;
class DatabaseHealthMonitor {
private static instance: DatabaseHealthMonitor;
private dbHealthy: boolean = true;
private lastCheckTime: number = 0;
private checkInProgress: boolean = false;
private listeners: Map<string, EventListener[]> = new Map();
private consecutiveErrorTimer: ReturnType<typeof setTimeout> | null = null;
private confirmedUnhealthy: boolean = false;
private degradedActive: boolean = false;
private constructor() {}
@@ -49,71 +61,56 @@ class DatabaseHealthMonitor {
reportDatabaseError(error: any, _wasAuthenticated: boolean = false) {
const errorMessage = error?.response?.data?.error || error?.message || "";
const errorCode = error?.response?.data?.code || error?.code;
const lowerMessage = errorMessage.toLowerCase();
const isDatabaseError =
errorMessage.toLowerCase().includes("database") ||
errorMessage.toLowerCase().includes("sqlite") ||
errorMessage.toLowerCase().includes("drizzle") ||
lowerMessage.includes("database") ||
lowerMessage.includes("sqlite") ||
lowerMessage.includes("drizzle") ||
errorCode === "DATABASE_ERROR" ||
errorCode === "DB_CONNECTION_FAILED";
const isBackendUnreachable =
errorCode === "ERR_NETWORK" ||
errorCode === "ECONNREFUSED" ||
(errorMessage.toLowerCase().includes("network error") &&
error?.response === undefined);
errorCode === "ECONNABORTED" ||
errorCode === "ECONNRESET" ||
errorCode === "ETIMEDOUT" ||
errorCode === "ERR_CANCELED" ||
(lowerMessage.includes("network error") &&
error?.response === undefined) ||
lowerMessage.includes("request aborted") ||
lowerMessage.includes("timeout");
if (!(isDatabaseError || isBackendUnreachable)) {
return;
}
if (this.dbHealthy && !this.consecutiveErrorTimer) {
this.consecutiveErrorTimer = setTimeout(() => {
this.consecutiveErrorTimer = null;
if (this.dbHealthy) {
this.dbHealthy = false;
this.confirmedUnhealthy = true;
this.emit("database-connection-lost", {
error: errorMessage || "Backend server unreachable",
code: errorCode,
timestamp: Date.now(),
});
}
}, 10000);
if (!this.degradedActive) {
this.degradedActive = true;
this.emit("database-connection-degraded", {
error: errorMessage || "Background request failed",
code: errorCode,
timestamp: Date.now(),
});
}
}
reportDatabaseSuccess() {
this.clearErrorTimer();
if (this.confirmedUnhealthy) {
this.dbHealthy = true;
this.confirmedUnhealthy = false;
this.emit("database-connection-restored", {
if (this.degradedActive) {
this.degradedActive = false;
this.emit("database-connection-degraded-cleared", {
timestamp: Date.now(),
});
} else if (!this.dbHealthy) {
this.dbHealthy = true;
}
}
private clearErrorTimer(): void {
if (this.consecutiveErrorTimer !== null) {
clearTimeout(this.consecutiveErrorTimer);
this.consecutiveErrorTimer = null;
}
}
isDatabaseHealthy(): boolean {
return this.dbHealthy;
isDegraded(): boolean {
return this.degradedActive;
}
reset() {
this.dbHealthy = true;
this.confirmedUnhealthy = false;
this.lastCheckTime = 0;
this.checkInProgress = false;
this.clearErrorTimer();
this.degradedActive = false;
}
}
+205 -6
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Copy snippet to clipboard",
"editTooltip": "Edit this snippet",
"deleteTooltip": "Delete this snippet",
"shareTooltip": "Share this snippet",
"sharedWithYou": "Shared",
"sharedBy": "Shared by {{username}}",
"shareSnippet": "Share Snippet",
"user": "User",
"role": "Role",
"selectTarget": "Select...",
"currentAccess": "Current Access",
"shareSuccess": "Snippet shared successfully",
"shareFailed": "Failed to share snippet",
"revokeSuccess": "Access revoked",
"revokeFailed": "Failed to revoke access",
"failedToLoadShareData": "Failed to load sharing data",
"newFolder": "New Folder",
"reorderSameFolder": "Can only reorder snippets within the same folder",
"reorderSuccess": "Snippets reordered successfully",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Authentication required. Please refresh the page.",
"dataAccessLockedReauth": "Data access locked. Please re-authenticate.",
"loading": "Loading command history...",
"error": "Error Loading History"
"error": "Error Loading History",
"disabledTitle": "Command History is Disabled",
"disabledDescription": "Enable Command History Tracking in your profile under Appearance settings."
},
"splitScreen": {
"title": "Split Screen",
@@ -510,6 +525,8 @@
"checkingDatabase": "Checking database connection...",
"checkingAuthentication": "Checking authentication...",
"backendReconnected": "Server connection restored",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "Actions",
"remove": "Remove",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Copy Sudo Password",
"passwordCopied": "Password copied to clipboard",
"sudoPasswordCopied": "Sudo password copied to clipboard",
"noPasswordAvailable": "No password available"
"noPasswordAvailable": "No password available",
"failedToCopyPassword": "Failed to copy password"
},
"admin": {
"title": "Admin Settings",
@@ -810,6 +828,29 @@
"globalSettingsSaved": "Global monitoring settings saved",
"failedToSaveGlobalSettings": "Failed to save global monitoring settings",
"failedToLoadGlobalSettings": "Failed to load global monitoring settings",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Configure how long user sessions last before requiring re-authentication. 'Remember Me' sessions are unaffected (always 30 days).",
"sessionTimeoutHours": "Timeout Duration",
"hours": "hours",
"sessionTimeoutNote": "Valid range: 1720 hours. Changes apply to new sessions only.",
"sessionTimeoutSaved": "Session timeout saved",
"failedToSaveSessionTimeout": "Failed to save session timeout",
"guacamoleIntegration": "Remote Desktop Integration (Guacamole)",
"guacamoleIntegrationDesc": "Enable RDP, VNC, and Telnet connections via guacd. Requires a guacd instance to be running.",
"enableGuacamole": "Enable RDP/VNC/Telnet support",
"guacdUrl": "guacd URL (host:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Changes to the guacd host/port require a server restart to take effect.",
"guacamoleSettingsSaved": "Guacamole settings saved",
"failedToSaveGuacamoleSettings": "Failed to save guacamole settings",
"failedToLoadGuacamoleSettings": "Failed to load guacamole settings",
"logLevel": "Log Level",
"logLevelDesc": "Control the verbosity of server logs. Higher levels reduce log output. Can also be set via the LOG_LEVEL environment variable.",
"logVerbosity": "Verbosity",
"logLevelNote": "Changes take effect immediately. Debug level may impact performance.",
"logLevelSaved": "Log level saved",
"failedToSaveLogLevel": "Failed to save log level",
"clampedToValidRange": "was adjusted to valid range",
"sessionManagement": "Session Management",
"loadingSessions": "Loading sessions...",
"noActiveSessions": "No active sessions found.",
@@ -851,6 +892,11 @@
"hostsCount": "{{count}} hosts",
"importJson": "Import JSON",
"importing": "Importing...",
"exportAllJson": "Export All",
"exporting": "Exporting...",
"exportedAllHosts": "Exported {{count}} hosts",
"failedToExportAllHosts": "Failed to export hosts. Please ensure you're logged in and have access to the host data.",
"exportAllSensitiveWarning": "The exported file will contain sensitive authentication data (passwords/SSH keys) in plaintext. Please keep the file secure and delete it after use.",
"importJsonTitle": "Import SSH Hosts from JSON",
"importJsonDesc": "Upload a JSON file to bulk import multiple SSH hosts (max 100).",
"downloadSample": "Download Sample",
@@ -874,11 +920,26 @@
"importError": "Import error",
"failedToImportJson": "Failed to import JSON file",
"connectionDetails": "Connection Details",
"connectionType": "Connection Type",
"ssh": "SSH",
"rdp": "Remote Desktop (RDP)",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Remote Desktop",
"guacamoleSettings": "Remote Desktop Settings",
"organization": "Organization",
"ipAddress": "IP Address or Hostname",
"macAddress": "MAC Address",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake on LAN",
"wolSent": "Wake-on-LAN packet sent",
"wolFailed": "Failed to send Wake-on-LAN packet",
"ipRequired": "IP address is required",
"portRequired": "Port is required",
"port": "Port",
"name": "Name",
"username": "Username",
"usernameRequired": "Username is required (SSH/Telnet only)",
"folder": "Folder",
"tags": "Tags",
"pin": "Pin",
@@ -987,6 +1048,8 @@
"tunnel": "Tunnel",
"fileManager": "File Manager",
"serverStats": "Server Stats",
"status": "Status",
"statistics": "Statistics",
"hostViewer": "Host Viewer",
"enableServerStats": "Enable Server Stats",
"enableServerStatsDesc": "Enable/disable server statistics collection for this host",
@@ -1033,6 +1096,9 @@
"dragToMoveBetweenFolders": "Drag to move between folders",
"exportedHostConfig": "Exported host configuration for {{name}}",
"openTerminal": "Open Terminal",
"openRdp": "Open RDP",
"openVnc": "Open VNC",
"openTelnet": "Open Telnet",
"openFileManager": "Open File Manager",
"openTunnels": "Open Tunnels",
"openServerDetails": "Open Server Details",
@@ -1048,13 +1114,18 @@
"statusCheckEnabledDesc": "Check if the server is online or offline",
"statusCheckInterval": "Status Check Interval",
"statusCheckIntervalDesc": "How often to check if host is online (5s - 1h)",
"statusChecks": "Status Checks",
"disableTcpPing": "Disable TCP Ping",
"disableTcpPingDescription": "Turn off status checks (online/offline detection) for this host",
"useGlobalStatusInterval": "Use global default",
"useGlobalMetricsInterval": "Use global default",
"usingGlobalDefault": "Using global default ({{value}}s)",
"metricsCollection": "Metrics Collection",
"metricsEnabled": "Enable Metrics Monitoring",
"metricsEnabledDesc": "Collect CPU, RAM, disk, and other system statistics",
"metricsInterval": "Metrics Collection Interval",
"metricsIntervalDesc": "How often to collect server statistics (5s - 1h)",
"metricsNotAvailableForConnectionType": "Server metrics are only available for SSH hosts. TCP ping status checks can still be enabled above.",
"intervalSeconds": "seconds",
"intervalMinutes": "minutes",
"intervalValidation": "Monitoring intervals must be between 5 seconds and 1 hour (3600 seconds)",
@@ -1143,6 +1214,10 @@
"noServerFound": "No server found",
"jumpHostsOrder": "Connections will be made in order: Jump Host 1 → Jump Host 2 → ... → Target Server",
"socks5Proxy": "SOCKS5 Proxy",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Send a sequence of connection attempts to specified ports before connecting. Used to open firewall rules dynamically.",
"addKnock": "Add Port",
"delayMs": "Delay",
"socks5Description": "Configure SOCKS5 proxy for SSH connection. All traffic will be routed through the specified proxy server.",
"enableSocks5": "Enable SOCKS5 Proxy",
"enableSocks5Description": "Use SOCKS5 proxy for this SSH connection",
@@ -1227,6 +1302,8 @@
"autoMoshDesc": "Automatically run MOSH command on connect",
"moshCommand": "MOSH Command",
"moshCommandDesc": "The MOSH command to execute",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automatically attach to an existing (or start a new) tmux session for persistent sessions and cross-device continuity",
"environmentVariables": "Environment Variables",
"environmentVariablesDesc": "Set custom environment variables for the terminal session",
"variableName": "Variable name",
@@ -1242,6 +1319,7 @@
"copyTunnelUrl": "Copy Tunnel URL",
"copyServerStatsUrl": "Copy Server Stats URL",
"copyDockerUrl": "Copy Docker URL",
"copyRemoteDesktopUrl": "Copy Remote Desktop URL",
"fullScreenUrlTooltip": "Copy URL to open this app in full-screen mode",
"notEnabled": "Docker is not enabled for this host. Enable it in Host Settings to use Docker features.",
"validating": "Validating Docker...",
@@ -1360,7 +1438,103 @@
"selectAll": "Select all",
"deselectAll": "Deselect all",
"useGlobalStatusDefault": "Use Global Default (Status)",
"useGlobalMetricsDefault": "Use Global Default (Metrics)"
"useGlobalMetricsDefault": "Use Global Default (Metrics)",
"connectionType": "Connection Type",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"ssh": "SSH",
"remoteDesktop": "Remote Desktop",
"remoteDesktopSettings": "Remote Desktop Settings",
"domain": "Domain",
"securityMode": "Security Mode",
"ignoreCert": "Ignore Certificate",
"ignoreCertDesc": "Skip TLS certificate verification for this connection",
"displaySettings": "Display Settings",
"colorDepth": "Color Depth",
"width": "Width",
"height": "Height",
"dpi": "DPI",
"resizeMethod": "Resize Method",
"forceLossless": "Force Lossless",
"audioSettings": "Audio Settings",
"disableAudio": "Disable Audio",
"enableAudioInput": "Enable Audio Input",
"rdpPerformance": "Performance",
"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",
"enableGfx": "Enable GFX",
"deviceRedirection": "Device Redirection",
"enablePrinting": "Enable Printing",
"enableDrive": "Enable Drive Redirection",
"driveName": "Drive Name",
"drivePath": "Drive Path",
"createDrivePath": "Create Drive Path",
"disableDownload": "Disable Download",
"disableUpload": "Disable Upload",
"enableTouch": "Enable Touch",
"rdpSession": "Session",
"clientName": "Client Name",
"consoleSession": "Console Session",
"initialProgram": "Initial Program",
"serverLayout": "Server Keyboard Layout",
"timezone": "Timezone",
"gatewaySettings": "Gateway",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Gateway Port",
"gatewayUsername": "Gateway Username",
"gatewayPassword": "Gateway Password",
"gatewayDomain": "Gateway Domain",
"remoteApp": "RemoteApp",
"remoteAppProgram": "Remote Application",
"remoteAppDir": "Remote App Directory",
"remoteAppArgs": "Remote App Arguments",
"clipboardSettings": "Clipboard",
"normalizeClipboard": "Normalize Clipboard",
"disableCopy": "Disable Copy",
"disablePaste": "Disable Paste",
"vncSettings": "VNC Settings",
"cursorMode": "Cursor Mode",
"swapRedBlue": "Swap Red/Blue",
"readOnly": "Read Only",
"recordingSettings": "Recording",
"recordingPath": "Recording Path",
"recordingName": "Recording Name",
"createRecordingPath": "Create Recording Path",
"excludeOutput": "Exclude Output",
"excludeMouse": "Exclude Mouse",
"includeKeys": "Include Keys",
"wakeOnLan": "Wake-on-LAN",
"sendWolPacket": "Send WoL Packet",
"wolMacAddr": "MAC Address",
"wolBroadcastAddr": "Broadcast Address",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Wait Time (seconds)",
"connectionSettings": "Connection Settings",
"rdpOnly": "RDP only",
"vncOnly": "VNC only",
"telnetTerminalSettings": "Terminal Settings",
"terminalType": "Terminal Type",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Color Scheme",
"guacBackspaceKey": "Backspace Key"
},
"guacamole": {
"connecting": "Connecting to {{type}} session...",
"rdpConnecting": "Connecting to RDP server...",
"vncConnecting": "Connecting to VNC server...",
"telnetConnecting": "Connecting to Telnet server...",
"connectionError": "Connection error",
"connectionFailed": "Connection failed",
"failedToConnect": "Failed to get connection token"
},
"terminal": {
"title": "Split Screen",
@@ -1407,6 +1581,7 @@
"connected": "Connected",
"clipboardWriteFailed": "Failed to copy to clipboard. Make sure the page is served over HTTPS or localhost.",
"clipboardReadFailed": "Failed to read from clipboard. Make sure clipboard permissions are granted.",
"clipboardHttpWarning": "Paste requires HTTPS. Use Ctrl+Shift+V or serve Termix over HTTPS.",
"sshConnected": "SSH connection established",
"authError": "Authentication failed: {{message}}",
"unknownError": "Unknown error occurred",
@@ -1415,7 +1590,27 @@
"connecting": "Connecting...",
"reconnecting": "Reconnecting... ({{attempt}}/{{max}})",
"reconnected": "Reconnected successfully",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.",
"tmuxWindows": "Windows",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Attached clients",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "Last activity",
"tmuxTimeJustNow": "just now",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Start new session",
"tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard",
"maxReconnectAttemptsReached": "Maximum reconnection attempts reached",
"connectionLost": "Connection lost",
"reconnect": "Reconnect",
"closeTab": "Close",
"connectionTimeout": "Connection timeout",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1439,6 +1634,7 @@
"opksshTimeout": "Authentication timed out. Please try again.",
"opksshAuthFailed": "Authentication failed. Please check your credentials and try again.",
"opksshConfigMissing": "OPKSSH configuration not found. Please create ~/.opk/config.yml with your OIDC provider settings. See documentation: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Sign in with {{provider}}",
"sudoPasswordPopupTitle": "Insert Password?",
"sudoPasswordPopupHint": "Press Enter to insert, Esc to dismiss",
"sudoPasswordPopupConfirm": "Insert",
@@ -1463,7 +1659,8 @@
"connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.",
"hostKeyRejected": "SSH host key verification rejected. Connection cancelled.",
"sessionTakenOver": "Session was opened in another tab. Reconnecting...",
"sessionAttachTimeout": "Session attachment timed out. Creating new connection..."
"sessionAttachTimeout": "Session attachment timed out. Creating new connection...",
"openFileManagerHere": "Open File Manager Here"
},
"fileManager": {
"title": "File Manager",
@@ -2167,6 +2364,8 @@
"fileColorCodingDesc": "Color-code files by type: folders (red), files (blue), symlinks (green)",
"commandAutocomplete": "Command Autocomplete",
"commandAutocompleteDesc": "Enable Tab key autocomplete suggestions for terminal commands based on your command history",
"commandHistoryTracking": "Save Command History",
"commandHistoryTrackingDesc": "Store terminal commands in history for autocomplete and sidebar. Disabled by default for privacy.",
"defaultSnippetFoldersCollapsed": "Collapse Snippet Folders by Default",
"defaultSnippetFoldersCollapsedDesc": "When enabled, all snippet folders will be collapsed when you open the snippets tab",
"terminalSyntaxHighlighting": "Terminal Syntax Highlighting",
@@ -2195,7 +2394,7 @@
"terminalSyntaxHighlightingDesc": "Automatically highlight commands, paths, IPs, and log levels in terminal output",
"enableCommandPaletteShortcut": "Enable Command Palette Shortcut",
"enableCommandPaletteShortcutDesc": "Double-tap left Shift to open the Command Palette for quick access to hosts",
"enableTerminalSessionPersistence": "Persistent Terminal Sessions",
"enableTerminalSessionPersistence": "Persistent Tabs/Sessions",
"enableTerminalSessionPersistenceDesc": "Maintain SSH connections when switching tabs or closing the browser (may be unstable)"
},
"user": {
@@ -2405,7 +2604,7 @@
"database": "Database",
"healthy": "Healthy",
"error": "Error",
"totalServers": "Total Servers",
"totalHosts": "Total Hosts",
"totalTunnels": "Total Tunnels",
"totalCredentials": "Total Credentials",
"recentActivity": "Recent Activity",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Kopieer stukkie na knipbord",
"editTooltip": "Wysig hierdie brokkie",
"deleteTooltip": "Vee hierdie brokkie uit",
"shareTooltip": "Share this snippet",
"sharedWithYou": "Shared",
"sharedBy": "Shared by {{username}}",
"shareSnippet": "Share Snippet",
"user": "User",
"role": "Role",
"selectTarget": "Select...",
"currentAccess": "Current Access",
"shareSuccess": "Snippet shared successfully",
"shareFailed": "Failed to share snippet",
"revokeSuccess": "Access revoked",
"revokeFailed": "Failed to revoke access",
"failedToLoadShareData": "Failed to load sharing data",
"newFolder": "Nuwe vouer",
"reorderSameFolder": "Kan slegs brokkies binne dieselfde vouer herrangskik",
"reorderSuccess": "Brokkies suksesvol herrangskik",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Verifikasie word vereis. Verfris asseblief die bladsy.",
"dataAccessLockedReauth": "Datatoegang gesluit. Herverifieer asseblief.",
"loading": "Laai opdraggeskiedenis...",
"error": "Fout by die laai van geskiedenis"
"error": "Fout by die laai van geskiedenis",
"disabledTitle": "Command History is Disabled",
"disabledDescription": "Enable Command History Tracking in your profile under Appearance settings."
},
"splitScreen": {
"title": "Gesplete skerm",
@@ -510,6 +525,8 @@
"checkingDatabase": "Kontroleer databasisverbinding...",
"checkingAuthentication": "Kontroleer tans verifikasie...",
"backendReconnected": "Bedienerverbinding herstel",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "Aksies",
"remove": "Verwyder",
"revoke": "Herroep",
@@ -541,7 +558,8 @@
"copySudoPassword": "Kopieer Sudo-wagwoord",
"passwordCopied": "Wagwoord gekopieer na knipbord",
"sudoPasswordCopied": "Sudo-wagwoord na knipbord gekopieer",
"noPasswordAvailable": "Geen wagwoord beskikbaar nie"
"noPasswordAvailable": "Geen wagwoord beskikbaar nie",
"failedToCopyPassword": "Failed to copy password"
},
"admin": {
"title": "Admin-instellings",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Globale moniteringsinstellings gestoor",
"failedToSaveGlobalSettings": "Kon nie globale moniteringsinstellings stoor nie",
"failedToLoadGlobalSettings": "Kon nie globale moniteringsinstellings laai nie",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Configure how long user sessions last before requiring re-authentication. 'Remember Me' sessions are unaffected (always 30 days).",
"sessionTimeoutHours": "Timeout Duration",
"hours": "hours",
"sessionTimeoutNote": "Valid range: 1720 hours. Changes apply to new sessions only.",
"sessionTimeoutSaved": "Session timeout saved",
"failedToSaveSessionTimeout": "Failed to save session timeout",
"guacamoleIntegration": "Integrasie van afstandrekenaars (Guacamole)",
"guacamoleIntegrationDesc": "Aktiveer RDP-, VNC- en Telnet-verbindings via guacd. Vereis dat 'n guacd-instansie loop.",
"enableGuacamole": "Aktiveer RDP/VNC/Telnet-ondersteuning",
"guacdUrl": "guacd URL (gasheer:poort)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Veranderinge aan die guacd-gasheer/poort vereis 'n herbegin van die bediener om in werking te tree.",
"guacamoleSettingsSaved": "Guacamole-instellings gestoor",
"failedToSaveGuacamoleSettings": "Kon nie guacamole-instellings stoor nie",
"failedToLoadGuacamoleSettings": "Kon nie guacamole-instellings laai nie",
"logLevel": "Log Level",
"logLevelDesc": "Control the verbosity of server logs. Higher levels reduce log output. Can also be set via the LOG_LEVEL environment variable.",
"logVerbosity": "Verbosity",
"logLevelNote": "Changes take effect immediately. Debug level may impact performance.",
"logLevelSaved": "Log level saved",
"failedToSaveLogLevel": "Failed to save log level",
"clampedToValidRange": "is aangepas na geldige reeks",
"loadingSessions": "Laai sessies...",
"noActiveSessions": "Geen aktiewe sessies gevind nie.",
"device": "Toestel",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} gashere",
"importJson": "Voer JSON in",
"importing": "Voer tans in...",
"exportAllJson": "Export All",
"exporting": "Exporting...",
"exportedAllHosts": "Exported {{count}} hosts",
"failedToExportAllHosts": "Failed to export hosts. Please ensure you're logged in and have access to the host data.",
"exportAllSensitiveWarning": "The exported file will contain sensitive authentication data (passwords/SSH keys) in plaintext. Please keep the file secure and delete it after use.",
"importJsonTitle": "Voer SSH-gashere vanaf JSON in",
"importJsonDesc": "Laai 'n JSON-lêer op om verskeie SSH-gashere in grootmaat in te voer (maks. 100).",
"downloadSample": "Laai voorbeeld af",
@@ -866,11 +912,26 @@
"importError": "Invoerfout",
"failedToImportJson": "Kon nie JSON-lêer invoer nie",
"connectionDetails": "Verbindingsbesonderhede",
"connectionType": "Verbindingstipe",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Afstandslessenaar",
"guacamoleSettings": "Instellings vir afstandwerkskerm",
"organization": "Organisasie",
"ipAddress": "IP-adres of gasheernaam",
"macAddress": "MAC Address",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Wake-on-LAN packet sent",
"wolFailed": "Failed to send Wake-on-LAN packet",
"ipRequired": "IP-adres word vereis",
"portRequired": "Poort is vereis",
"port": "Hawe",
"name": "Naam",
"username": "Gebruikersnaam",
"usernameRequired": "Gebruikersnaam word vereis (slegs SSH/Telnet)",
"folder": "Vouer",
"tags": "Etikette",
"pin": "Speld vas",
@@ -979,6 +1040,8 @@
"tunnel": "Tonnel",
"fileManager": "Lêerbestuurder",
"serverStats": "Bedienerstatistieke",
"status": "Status",
"statistics": "Statistiek",
"hostViewer": "Gasheerkyker",
"enableServerStats": "Aktiveer bedienerstatistieke",
"enableServerStatsDesc": "Aktiveer/deaktiveer bedienerstatistiekversameling vir hierdie gasheer",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Sleep om tussen dopgehou te beweeg",
"exportedHostConfig": "Uitgevoerde gasheerkonfigurasie vir {{name}}",
"openTerminal": "Maak Terminaal Oop",
"openRdp": "Open RDP",
"openVnc": "Open VNC",
"openTelnet": "Open Telnet",
"openFileManager": "Maak Lêerbestuurder oop",
"openTunnels": "Oop Tonnels",
"openServerDetails": "Maak bedienerbesonderhede oop",
"statistics": "Statistiek",
"enabledWidgets": "Geaktiveerde Widgets",
"openServerStats": "Maak bedienerstatistieke oop",
"enabledWidgetsDesc": "Kies watter statistiek-widgets vir hierdie gasheer vertoon moet word",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Kontroleer of die bediener aanlyn of vanlyn is",
"statusCheckInterval": "Statuskontrole-interval",
"statusCheckIntervalDesc": "Hoe gereeld om te kyk of die gasheer aanlyn is (5s - 1u)",
"statusChecks": "Statuskontroles",
"disableTcpPing": "Deaktiveer TCP Ping",
"disableTcpPingDescription": "Skakel statuskontroles (aanlyn/vanlyn-opsporing) vir hierdie gasheer af",
"useGlobalStatusInterval": "Gebruik globale verstekwaarde",
"useGlobalMetricsInterval": "Gebruik globale verstekwaarde",
"usingGlobalDefault": "Gebruik globale verstekwaarde ({{value}}s)",
"metricsCollection": "Metrieke-versameling",
"metricsEnabled": "Aktiveer Metrieke Monitering",
"metricsEnabledDesc": "Versamel SVE-, RAM-, skyf- en ander stelselstatistieke",
"metricsInterval": "Metrieke-insamelingsinterval",
"metricsIntervalDesc": "Hoe gereeld om bedienerstatistieke in te samel (5s - 1u)",
"metricsNotAvailableForConnectionType": "Bedienermetrieke is slegs beskikbaar vir SSH-gashere. TCP-pingstatuskontroles kan steeds hierbo geaktiveer word.",
"intervalSeconds": "sekondes",
"intervalMinutes": "minute",
"intervalValidation": "Moniteringsintervalle moet tussen 5 sekondes en 1 uur (3600 sekondes) wees.",
@@ -1133,6 +1203,10 @@
"noServerFound": "Geen bediener gevind nie",
"jumpHostsOrder": "Verbindings sal in volgorde gemaak word: Spring Gasheer 1 → Spring Gasheer 2 → ... → Teikenbediener",
"socks5Proxy": "SOCKS5 Proxy",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Send a sequence of connection attempts to specified ports before connecting. Used to open firewall rules dynamically.",
"addKnock": "Add Port",
"delayMs": "Delay",
"socks5Description": "Konfigureer SOCKS5-instaanbediener vir SSH-verbinding. Alle verkeer sal deur die gespesifiseerde instaanbediener gelei word.",
"enableSocks5": "Aktiveer SOCKS5-instaanbediener",
"enableSocks5Description": "Gebruik SOCKS5-instaanbediener vir hierdie SSH-verbinding",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Voer outomaties die MOSH-opdrag uit tydens verbinding",
"moshCommand": "MOSH-opdrag",
"moshCommandDesc": "Die MOSH-opdrag om uit te voer",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automatically attach to an existing (or start a new) tmux session for persistent sessions and cross-device continuity",
"environmentVariables": "Omgewingsveranderlikes",
"environmentVariablesDesc": "Stel persoonlike omgewingveranderlikes vir die terminaalsessie",
"variableName": "Veranderlike naam",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Kopieer Tonnel URL",
"copyServerStatsUrl": "Kopieer bedienerstatistieke-URL",
"copyDockerUrl": "Kopieer Docker URL",
"copyRemoteDesktopUrl": "Kopieer Afstandslessenaar URL",
"fullScreenUrlTooltip": "Kopieer URL om hierdie program in volskermmodus oop te maak",
"notEnabled": "Docker is nie vir hierdie gasheer geaktiveer nie. Aktiveer dit in Gasheerinstellings om Docker-funksies te gebruik.",
"validating": "Valideer Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Kies alles",
"deselectAll": "Deselekteer alles",
"useGlobalStatusDefault": "Gebruik globale verstekwaarde (status)",
"useGlobalMetricsDefault": "Gebruik globale verstekwaarde (metrieke)"
"useGlobalMetricsDefault": "Gebruik globale verstekwaarde (metrieke)",
"remoteDesktopSettings": "Instellings vir afstandwerkskerm",
"domain": "Domein",
"securityMode": "Sekuriteitsmodus",
"ignoreCert": "Ignoreer Sertifikaat",
"ignoreCertDesc": "Slaan TLS-sertifikaatverifikasie vir hierdie verbinding oor",
"displaySettings": "Skerminstellings",
"colorDepth": "Kleurdiepte",
"width": "Breedte",
"height": "Hoogte",
"dpi": "DPI",
"resizeMethod": "Vergrootingsmetode",
"forceLossless": "Forseer verliesloos",
"audioSettings": "Oudio-instellings",
"disableAudio": "Deaktiveer oudio",
"enableAudioInput": "Aktiveer oudio-invoer",
"rdpPerformance": "Prestasie",
"enableWallpaper": "Aktiveer agtergrond",
"enableTheming": "Aktiveer Temavorming",
"enableFontSmoothing": "Aktiveer lettertipe-gladmaak",
"enableFullWindowDrag": "Aktiveer Volle Venster Sleep",
"enableDesktopComposition": "Aktiveer lessenaarkomposisie",
"enableMenuAnimations": "Aktiveer Menu-animasies",
"disableBitmapCaching": "Deaktiveer Bitmap-kasgeheue",
"disableOffscreenCaching": "Deaktiveer buiteskerm-kasgeheue",
"disableGlyphCaching": "Deaktiveer glifkasgeheue",
"enableGfx": "Aktiveer GFX",
"deviceRedirection": "Toestelherleiding",
"enablePrinting": "Aktiveer drukwerk",
"enableDrive": "Aktiveer dryfherleiding",
"driveName": "Skyfnaam",
"drivePath": "Rypad",
"createDrivePath": "Skep 'n rylaanpad",
"disableDownload": "Deaktiveer aflaai",
"disableUpload": "Deaktiveer oplaai",
"enableTouch": "Aktiveer Aanraking",
"rdpSession": "Sessie",
"clientName": "Kliëntnaam",
"consoleSession": "Konsolesessie",
"initialProgram": "Aanvanklike Program",
"serverLayout": "Bedienersleutelborduitleg",
"timezone": "Tydsone",
"gatewaySettings": "Poort",
"gatewayHostname": "Gateway-gasheernaam",
"gatewayPort": "Poortpoort",
"gatewayUsername": "Gateway-gebruikersnaam",
"gatewayPassword": "Gateway-wagwoord",
"gatewayDomain": "Gateway-domein",
"remoteApp": "Afstandsprogram",
"remoteAppProgram": "Afstandtoepassing",
"remoteAppDir": "Afstandsprogramgids",
"remoteAppArgs": "Argumente vir afgeleë toepassings",
"clipboardSettings": "Klembord",
"normalizeClipboard": "Normaliseer knipbord",
"disableCopy": "Deaktiveer Kopieer",
"disablePaste": "Deaktiveer plak",
"vncSettings": "VNC-instellings",
"cursorMode": "Wysermodus",
"swapRedBlue": "Ruil Rooi/Blou",
"readOnly": "Lees Slegs",
"recordingSettings": "Opname",
"recordingPath": "Opnamepad",
"recordingName": "Opnamenaam",
"createRecordingPath": "Skep Opnamepad",
"excludeOutput": "Sluit Uitvoer Uit",
"excludeMouse": "Sluit Muis uit",
"includeKeys": "Sluit sleutels in",
"sendWolPacket": "Stuur WoL-pakket",
"wolMacAddr": "MAC-adres",
"wolBroadcastAddr": "Uitsaaiadres",
"wolUdpPort": "UDP-poort",
"wolWaitTime": "Wagtyd (sekondes)",
"connectionSettings": "Verbindingsinstellings",
"rdpOnly": "Slegs RDP",
"vncOnly": "Slegs VNC",
"telnetTerminalSettings": "Terminaalinstellings",
"terminalType": "Terminaaltipe",
"guacFontName": "Lettertipe Naam",
"guacFontSize": "Lettergrootte",
"guacColorScheme": "Kleurskema",
"guacBackspaceKey": "Terugspasie-sleutel"
},
"guacamole": {
"connecting": "Verbind met {{type}} sessie...",
"rdpConnecting": "Koppel aan RDP-bediener...",
"vncConnecting": "Verbind met VNC-bediener...",
"telnetConnecting": "Koppel aan Telnet-bediener...",
"connectionError": "Verbindingsfout",
"connectionFailed": "Verbinding het misluk",
"failedToConnect": "Kon nie verbindingstoken kry nie"
},
"terminal": {
"title": "Terminaal",
@@ -1364,7 +1530,7 @@
"closePanel": "Maak paneel toe",
"reconnect": "Herkoppel",
"sessionEnded": "Sessie geëindig",
"connectionLost": "Verbinding verloor",
"connectionLost": "Connection lost",
"error": "FOUT: {{message}}",
"disconnected": "Ontkoppel",
"connectionClosed": "Verbinding gesluit",
@@ -1372,6 +1538,7 @@
"connected": "Verbonde",
"clipboardWriteFailed": "Kon nie na knipbord kopieer nie. Maak seker dat die bladsy oor HTTPS of localhost bedien word.",
"clipboardReadFailed": "Kon nie vanaf knipbord lees nie. Maak seker dat knipbordtoestemmings toegestaan is.",
"clipboardHttpWarning": "Plak vereis HTTPS. Gebruik Ctrl+Shift+V of bedien Termix oor HTTPS.",
"sshConnected": "SSH-verbinding gevestig",
"authError": "Verifikasie het misluk: {{message}}",
"unknownError": "Onbekende fout het voorgekom",
@@ -1380,7 +1547,25 @@
"connecting": "Verbind...",
"reconnecting": "Herverbind... ({{attempt}}/{{max}})",
"reconnected": "Herverbind suksesvol",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.",
"tmuxWindows": "Windows",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Attached clients",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "Last activity",
"tmuxTimeJustNow": "just now",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Start new session",
"tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard",
"maxReconnectAttemptsReached": "Maksimum herverbindingspogings bereik",
"closeTab": "Close",
"connectionTimeout": "Verbindingstydverstryking",
"terminalTitle": "Terminaal - {{host}}",
"terminalWithPath": "Terminaal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Verifikasie het verstryk. Probeer asseblief weer.",
"opksshAuthFailed": "Verifikasie het misluk. Kontroleer asseblief jou geloofsbriewe en probeer weer.",
"opksshConfigMissing": "OPKSSH-konfigurasie nie gevind nie. Skep asseblief ~/.opk/config.yml met jou OIDC-verskafferinstellings. Sien dokumentasie: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Sign in with {{provider}}",
"sudoPasswordPopupTitle": "Wagwoord invoeg?",
"sudoPasswordPopupHint": "Druk Enter om in te voeg, Esc om te verwerp",
"sudoPasswordPopupConfirm": "Invoeg",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Verbinding deur bediener verwerp. Kontroleer asseblief u verifikasie en netwerkkonfigurasie.",
"hostKeyRejected": "SSH-gasheersleutelverifikasie verwerp. Verbinding gekanselleer.",
"sessionTakenOver": "Sessie is in 'n ander oortjie oopgemaak. Herverbind tans...",
"sessionAttachTimeout": "Sessie-aanhegsel het uitgetel. Skep nuwe verbinding..."
"sessionAttachTimeout": "Sessie-aanhegsel het uitgetel. Skep nuwe verbinding...",
"openFileManagerHere": "Open File Manager Here"
},
"fileManager": {
"title": "Lêerbestuurder",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Kleurkodeer lêers volgens tipe: lêers (rooi), lêers (blou), simskakels (groen)",
"commandAutocomplete": "Opdrag Outomatiese Voltooiing",
"commandAutocompleteDesc": "Aktiveer Tab-sleutel outovoltooiingsvoorstelle vir terminaalopdragte gebaseer op jou opdraggeskiedenis",
"commandHistoryTracking": "Save Command History",
"commandHistoryTrackingDesc": "Store terminal commands in history for autocomplete and sidebar. Disabled by default for privacy.",
"defaultSnippetFoldersCollapsed": "Vou brokkie-vouers standaard in",
"defaultSnippetFoldersCollapsedDesc": "Wanneer dit geaktiveer is, sal alle brokkie-lêergidse ingevou word wanneer jy die brokkie-oortjie oopmaak.",
"terminalSyntaxHighlighting": "Terminale Sintaksis Uitlig",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Merk outomaties opdragte, paaie, IP's en logvlakke in terminaaluitvoer",
"enableCommandPaletteShortcut": "Aktiveer die opdragpalet-kortpad",
"enableCommandPaletteShortcutDesc": "Dubbeltik links Shift om die opdragpalet oop te maak vir vinnige toegang tot gashere",
"enableTerminalSessionPersistence": "Aanhoudende terminale sessies",
"enableTerminalSessionPersistence": "Aanhoudende oortjies/sessies",
"enableTerminalSessionPersistenceDesc": "Handhaaf SSH-verbindings wanneer jy oortjies wissel of die blaaier toemaak (dit kan onstabiel wees)."
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Databasis",
"healthy": "Gesond",
"error": "Fout",
"totalServers": "Totale bedieners",
"totalHosts": "Total Hosts",
"totalTunnels": "Totale tonnels",
"totalCredentials": "Totale geloofsbriewe",
"recentActivity": "Onlangse Aktiwiteit",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "نسخ كتلة الكود إلى الحافظة",
"editTooltip": "تعديل كتلة الكود هذه",
"deleteTooltip": "حذف هذه الكتلة",
"shareTooltip": "شارك كتلة الكود هذه",
"sharedWithYou": "مشترك",
"sharedBy": "مشترك بواسطة {{username}}",
"shareSnippet": "مشاركة كتلة الكود",
"user": "المستخدم",
"role": "دور",
"selectTarget": "إختيار...",
"currentAccess": "الوصول الحالي",
"shareSuccess": "تم مشاركة كتلة الكود بنجاح",
"shareFailed": "فشل مشاركة كتلة الكود",
"revokeSuccess": "تم إلغاء الوصول",
"revokeFailed": "فشل في إلغاء الوصول",
"failedToLoadShareData": "فشل تحميل بيانات المشاركة",
"newFolder": "مجلد جديد",
"reorderSameFolder": "يمكن فقط إعادة ترتيب كتل الكود داخل نفس المجلد",
"reorderSuccess": "تم إعادة ترتيب الكتل بنجاح",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "المصادقة مطلوبة. الرجاء تحديث الصفحة.",
"dataAccessLockedReauth": "تم تأمين الوصول إلى البيانات. الرجاء إعادة المصادقة.",
"loading": "جاري تحميل سجل الأوامر...",
"error": "خطأ في تحميل المحفوظات"
"error": "خطأ في تحميل المحفوظات",
"disabledTitle": "تاريخ الأوامر معطل",
"disabledDescription": "تمكين تتبع تاريخ الأوامر في ملفك الشخصي تحت إعدادات الظهر.format@@0"
},
"splitScreen": {
"title": "تقسيم الشاشة",
@@ -510,6 +525,8 @@
"checkingDatabase": "التحقق من اتصال قاعدة البيانات...",
"checkingAuthentication": "التحقق من المصادقة...",
"backendReconnected": "تم استعادة اتصال الخادم",
"connectionDegraded": "فقد اتصال الخادم، واسترداد…",
"reload": "Reload",
"actions": "الإجراءات",
"remove": "إزالة",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "نسخ كلمة مرور سودو",
"passwordCopied": "تم نسخ كلمة المرور إلى الحافظة",
"sudoPasswordCopied": "تم نسخ كلمة المرور إلى الحافظة",
"noPasswordAvailable": "لا توجد كلمة مرور متاحة"
"noPasswordAvailable": "لا توجد كلمة مرور متاحة",
"failedToCopyPassword": "فشل في نسخ كلمة المرور"
},
"admin": {
"title": "إعدادات المدير",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "تم حفظ إعدادات المراقبة العالمية",
"failedToSaveGlobalSettings": "فشل في حفظ إعدادات المراقبة العالمية",
"failedToLoadGlobalSettings": "فشل تحميل إعدادات المراقبة العالمية",
"sessionTimeout": "مهلة الجلسة",
"sessionTimeoutDesc": "قم بتهيئة طول مدة جلسات المستخدم قبل طلب إعادة المصادقة. 'تذكريني' جلسات غير متأثرة (دائما 30 يوما).",
"sessionTimeoutHours": "مدة المهلة",
"hours": "ساعات",
"sessionTimeoutNote": "النطاق الصالح: 1-720 ساعة. التغييرات تنطبق على الجلسات الجديدة فقط.",
"sessionTimeoutSaved": "تم حفظ مهلة الجلسة",
"failedToSaveSessionTimeout": "فشل في حفظ مهلة الجلسة",
"guacamoleIntegration": "تكامل سطح المكتب عن بعد (غواكامول)",
"guacamoleIntegrationDesc": "تمكين اتصالات RDP و VNC و Telnet عبر guacd. يتطلب مثيل Gacd ليتم تشغيله.",
"enableGuacamole": "تمكين دعم RDP/VNC/Telnet",
"guacdUrl": "رابط guacd (المضيف:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "تتطلب التغييرات على مضيف/منفذ guacd إعادة تشغيل الخادم لتفعيلها.",
"guacamoleSettingsSaved": "تم حفظ إعدادات غواكامول",
"failedToSaveGuacamoleSettings": "فشل في حفظ إعدادات الGuacamole",
"failedToLoadGuacamoleSettings": "فشل تحميل إعدادات Guacamole",
"logLevel": "مستوى السجل",
"logLevelDesc": "التحكم في لفظية سجلات الخادم. المستويات الأعلى تقلل من إخراج السجل. يمكن أيضا تعيينها عبر متغير بيئة LOG_LEVEL.",
"logVerbosity": "عملاق",
"logLevelNote": "التغييرات تصبح سارية المفعول على الفور. مستوى التصحيح قد يؤثر على الأداء.",
"logLevelSaved": "تم حفظ مستوى السجل",
"failedToSaveLogLevel": "فشل حفظ مستوى السجل",
"clampedToValidRange": "تم تعديله إلى نطاق صحيح",
"loadingSessions": "جاري تحميل الجلسات...",
"noActiveSessions": "لم يتم العثور على جلسات نشطة.",
"device": "الجهاز",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} المضيفون",
"importJson": "استيراد JSON",
"importing": "استيراد...",
"exportAllJson": "تصدير الكل",
"exporting": "تصدير...",
"exportedAllHosts": "تم تصدير مضيفين {{count}}",
"failedToExportAllHosts": "فشل في تصدير المضيفين. الرجاء التأكد من تسجيل الدخول والوصول إلى بيانات المضيف.",
"exportAllSensitiveWarning": "سيحتوي الملف المصدّر على بيانات مصادقة حساسة (كلمات المرور/مفاتيح SH) في plaintext. الرجاء الحفاظ على أمن الملف وحذفه بعد الاستخدام.",
"importJsonTitle": "استيراد مضيف SSH من JSON",
"importJsonDesc": "تحميل ملف JSON لاستيراد مستضيفات SSH متعددة (الحد الأقصى 100).",
"downloadSample": "تحميل عينة",
@@ -866,11 +912,26 @@
"importError": "خطأ في الاستيراد",
"failedToImportJson": "فشل استيراد ملف JSON",
"connectionDetails": "تفاصيل الاتصال",
"connectionType": "نوع الاتصال",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "سطح المكتب عن بعد",
"guacamoleSettings": "إعدادات سطح المكتب عن بعد",
"organization": "المنظمة",
"ipAddress": "عنوان IP أو اسم المضيف",
"macAddress": "عنوان MAC",
"macAddressDesc": "صيغة Wake-on-LAN: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "إيقاظ-الشبكة المحلية",
"wolSent": "تم إرسال حزمة التنبيه بالشبكة المحلية",
"wolFailed": "فشل في إرسال حزمة Wke-on-LAN",
"ipRequired": "عنوان IP مطلوب",
"portRequired": "المنفذ مطلوب",
"port": "المنفذ",
"name": "الاسم",
"username": "اسم المستخدم",
"usernameRequired": "اسم المستخدم مطلوب (SSH/Telnet فقط)",
"folder": "مجلد",
"tags": "الوسوم",
"pin": "تثبيت",
@@ -979,6 +1040,8 @@
"tunnel": "نفق",
"fileManager": "مدير الملفات",
"serverStats": "إحصائيات الخادم",
"status": "الحالة",
"statistics": "الإحصائيات",
"hostViewer": "عارض المضيف",
"enableServerStats": "تمكين إحصائيات الخادم",
"enableServerStatsDesc": "تمكين/تعطيل مجموعة إحصائيات الخادم لهذا المضيف",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "اسحب للانتقال بين المجلدات",
"exportedHostConfig": "تصدير إعدادات المضيف ل {{name}}",
"openTerminal": "فتح محطة طرفية",
"openRdp": "فتح RDP",
"openVnc": "فتح VNC",
"openTelnet": "فتح Telnet",
"openFileManager": "فتح مدير الملفات",
"openTunnels": "فتح الأنفاق",
"openServerDetails": "فتح تفاصيل الخادم",
"statistics": "الإحصائيات",
"enabledWidgets": "تمكين الودجت",
"openServerStats": "فتح إحصائيات الخادم",
"enabledWidgetsDesc": "حدد الاحصائيات التي سيتم عرضها لهذا المضيف",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "تحقق مما إذا كان الخادم متصلاً أو غير متصل",
"statusCheckInterval": "فترة التحقق من الحالة",
"statusCheckIntervalDesc": "كم عدد المرات للتحقق مما إذا كان المضيف متصل بالإنترنت (5s - 1ساعة)",
"statusChecks": "التحقق من الحالة",
"disableTcpPing": "تعطيل TCP Ping",
"disableTcpPingDescription": "إيقاف التحقق من الحالة (على الإنترنت/دون اتصال) لهذا المضيف",
"useGlobalStatusInterval": "استخدام الافتراضي العام",
"useGlobalMetricsInterval": "استخدام الافتراضي العام",
"usingGlobalDefault": "استخدام الافتراضي العالمي ({{value}})",
"metricsCollection": "مجموعة القياسات",
"metricsEnabled": "تمكين مراقبة القياسات",
"metricsEnabledDesc": "جمع إحصائيات المعالجة، ذاكرة الوصول العشوائي، القرص وغيرها من إحصائيات النظام",
"metricsInterval": "الفاصل الزمني لمجموعة القياسات",
"metricsIntervalDesc": "كم عدد المرات لجمع إحصائيات الخادم (5s - 1ساعة)",
"metricsNotAvailableForConnectionType": "مقاييس الخادم متوفرة فقط لمضيفي SSH. لا يزال يمكن تمكين التحقق من حالة TCP في الأعلى.",
"intervalSeconds": "ثواني",
"intervalMinutes": "دقائق",
"intervalValidation": "يجب أن تكون فترات الرصد بين 5 ثوان وساعة واحدة (3600 ثانية)",
@@ -1133,6 +1203,10 @@
"noServerFound": "لا يوجد خادم",
"jumpHostsOrder": "سيتم إجراء الاتصالات بالترتيب: قفز المضيف 1 → قفز المضيف 2 → ... → هدف الخادم",
"socks5Proxy": "وكيل SOCKS5",
"portKnocking": "منصة المنفذ",
"portKnockingDesc": "إرسال سلسلة من محاولات الاتصال إلى منافذ محددة قبل الاتصال. تستخدم لفتح قواعد جدار الحماية بشكل ديناميكي.",
"addKnock": "إضافة منفذ",
"delayMs": "التأخير",
"socks5Description": "تكوين وكيل SOCKS5 للاتصال SSH. سيتم توجيه جميع حركة المرور من خلال خادم الوكيل المحدد.",
"enableSocks5": "تمكين وكيل SOCKS5",
"enableSocks5Description": "استخدم وكيل SOCKS5 لهذا الاتصال SSH",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "تشغيل أمر MOSH تلقائياً عند الاتصال",
"moshCommand": "قيادة MOSH",
"moshCommandDesc": "أمر MOSH لتنفيذ",
"autoTmux": "التموكس التلقائي",
"autoTmuxDesc": "إرفاق تلقائياً لجلسة Tmux موجودة (أو بدء جلسة جديدة) للجلسات المستمرة والاستمرارية عبر الأجهزة",
"environmentVariables": "المتغيرات البيئية",
"environmentVariablesDesc": "تعيين متغيرات البيئة المخصصة للجلسة النهائية",
"variableName": "اسم المتغير",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "نسخ رابط النفق",
"copyServerStatsUrl": "نسخ رابط إحصائيات الخادم",
"copyDockerUrl": "نسخ رابط Docker",
"copyRemoteDesktopUrl": "نسخ رابط سطح المكتب عن بعد",
"fullScreenUrlTooltip": "نسخ عنوان URL لفتح هذا التطبيق في وضع ملء الشاشة",
"notEnabled": "Docker غير مفعل لهذا المضيف. قم بتمكينه في إعدادات المضيف لاستخدام ميزات Docker.",
"validating": "التحقق من دوكر...",
@@ -1348,7 +1425,96 @@
"selectAll": "حدد الكل",
"deselectAll": "إلغاء تحديد الكل",
"useGlobalStatusDefault": "استخدام الافتراضي العام (الحالة)",
"useGlobalMetricsDefault": "استخدام الافتراضي العام (القياسات)"
"useGlobalMetricsDefault": "استخدام الافتراضي العام (القياسات)",
"remoteDesktopSettings": "إعدادات سطح المكتب عن بعد",
"domain": "النطاق",
"securityMode": "وضع الأمان",
"ignoreCert": "تجاهل الشهادة",
"ignoreCertDesc": "تخطي التحقق من شهادة TLS لهذا الاتصال",
"displaySettings": "اعدادات العرض",
"colorDepth": "عمق اللون",
"width": "Width",
"height": "الارتفاع",
"dpi": "إدارة",
"resizeMethod": "طريقة تغيير الحجم",
"forceLossless": "إجبار بلا خسارة",
"audioSettings": "إعدادات الصوت",
"disableAudio": "تعطيل الصوت",
"enableAudioInput": "تمكين إدخال الصوت",
"rdpPerformance": "الأداء",
"enableWallpaper": "تمكين الخلفية",
"enableTheming": "تمكين القالب",
"enableFontSmoothing": "تمكين الخط المزحم",
"enableFullWindowDrag": "تمكين سحب النافذة الكاملة",
"enableDesktopComposition": "تمكين تكوين سطح المكتب",
"enableMenuAnimations": "تمكين الرسوم المتحركة للقائمة",
"disableBitmapCaching": "تعطيل التخزين المؤقت لخرائط Bitmap",
"disableOffscreenCaching": "تعطيل التخزين المؤقت لإغلاق الشاشة",
"disableGlyphCaching": "تعطيل التخزين المؤقت لـ Glyph",
"enableGfx": "Enable GFX",
"deviceRedirection": "إعادة توجيه الجهاز",
"enablePrinting": "تمكين الطباعة",
"enableDrive": "تمكين إعادة توجيه القرص",
"driveName": "اسم القرص",
"drivePath": "مسار القيادة",
"createDrivePath": "إنشاء مسار محرك الأقراص",
"disableDownload": "تعطيل التحميل",
"disableUpload": "تعطيل التحميل",
"enableTouch": "تمكين اللمس",
"rdpSession": "الجلسة",
"clientName": "اسم العميل",
"consoleSession": "وحدة التحكم",
"initialProgram": "البرنامج الأولي",
"serverLayout": "تخطيط لوحة مفاتيح الخادم",
"timezone": "Timezone",
"gatewaySettings": "البوابة",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "منفذ البوابة",
"gatewayUsername": "اسم مستخدم البوابة",
"gatewayPassword": "كلمة مرور البوابة",
"gatewayDomain": "نطاق البوابة",
"remoteApp": "التطبيق البعيد",
"remoteAppProgram": "التطبيق عن بعد",
"remoteAppDir": "دليل التطبيق عن بعد",
"remoteAppArgs": "حروف التطبيق البعيد",
"clipboardSettings": "الحافظة",
"normalizeClipboard": "تطبيع الحافظة",
"disableCopy": "تعطيل النسخ",
"disablePaste": "تعطيل اللصق",
"vncSettings": "إعدادات VNC",
"cursorMode": "وضع المؤشر",
"swapRedBlue": "مبادلة Red/Bأزرق",
"readOnly": "قراءة فقط",
"recordingSettings": "التسجيل",
"recordingPath": "مسار التسجيل",
"recordingName": "اسم التسجيل",
"createRecordingPath": "إنشاء مسار التسجيل",
"excludeOutput": "استبعاد المخرجات",
"excludeMouse": "استبعاد الفأرة",
"includeKeys": "تضمين المفاتيح",
"sendWolPacket": "إرسال حزمة وول",
"wolMacAddr": "عنوان MAC",
"wolBroadcastAddr": "عنوان البث",
"wolUdpPort": "UDP Port",
"wolWaitTime": "وقت الانتظار (بالثواني)",
"connectionSettings": "إعدادات الاتصال",
"rdpOnly": "RDP فقط",
"vncOnly": "VNC فقط",
"telnetTerminalSettings": "إعدادات المحطة الطرفية",
"terminalType": "نوع المحطة الطرفية",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "مخطط الألوان",
"guacBackspaceKey": "مفتاح الخلفية"
},
"guacamole": {
"connecting": "جاري الاتصال بجلسة {{type}}...",
"rdpConnecting": "جاري الاتصال بخادم RDP...",
"vncConnecting": "الاتصال بخادم VNC...",
"telnetConnecting": "جار الاتصال بخادم Telnet...",
"connectionError": "خطأ في الاتصال",
"connectionFailed": "فشل الاتصال",
"failedToConnect": "فشل الحصول على رمز الاتصال"
},
"terminal": {
"title": "المحطة",
@@ -1364,7 +1530,7 @@
"closePanel": "أغلق اللوحة",
"reconnect": "إعادة الاتصال",
"sessionEnded": "انتهت الجلسة",
"connectionLost": "فقدان الاتصال",
"connectionLost": "تم فقدان الاتصال",
"error": "خطأ: {{message}}",
"disconnected": "قطع",
"connectionClosed": "تم إغلاق الاتصال",
@@ -1372,6 +1538,7 @@
"connected": "متصل",
"clipboardWriteFailed": "فشل النسخ إلى الحافظة. تأكد من خدمة الصفحة عبر HTTPS أو localhost.",
"clipboardReadFailed": "فشل في القراءة من الحافظة. تأكد من منح أذونات الحافظة.",
"clipboardHttpWarning": "لصق يتطلب HTTPS. استخدم Ctrl+Shift+V أو خدمة Termix عبر HTTPS.",
"sshConnected": "تم إنشاء اتصال SSH",
"authError": "فشل المصادقة: {{message}}",
"unknownError": "حدث خطأ غير معروف",
@@ -1380,7 +1547,25 @@
"connecting": "جاري الاتصال...",
"reconnecting": "إعادة الاتصال... ({{attempt}}/{{max}})",
"reconnected": "تمت إعادة الاتصال بنجاح",
"tmuxSessionCreated": "تم إنشاء جلسة tmux: {{name}}",
"tmuxSessionAttached": "جلسة tmux المرفقة: {{name}}",
"tmuxUnavailable": "لم يتم تثبيت tmux على المضيف البعيد، العودة إلى قذيفة عادية",
"tmuxSessionPickerTitle": "جلسات tmux",
"tmuxSessionPickerDesc": "جلسات tmux الموجودة على هذا المضيف. حدد واحدة لإعادة إرفاق أو إنشاء جلسة جديدة.",
"tmuxWindows": "ويندوز",
"tmuxWindowCount": "{{count}} نافذة",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "العملاء الملحقون",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "آخر نشاط",
"tmuxTimeJustNow": "الآن فقط",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "بدء جلسة جديدة",
"tmuxCopyHint": "ضبط التحديد واضغط على Enter للنسخ إلى الحافظة",
"maxReconnectAttemptsReached": "تم بلوغ الحد الأقصى لمحاولات إعادة الاتصال",
"closeTab": "أغلق",
"connectionTimeout": "مهلة الاتصال",
"terminalTitle": "المحطة الطرفية - {{host}}",
"terminalWithPath": "المحطة الطرفية - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "انتهت مهلة المصادقة. الرجاء المحاولة مرة أخرى.",
"opksshAuthFailed": "فشلت المصادقة. الرجاء التحقق من بيانات الاعتماد الخاصة بك وحاول مرة أخرى.",
"opksshConfigMissing": "لم يتم العثور على تكوين الفتح. الرجاء إنشاء ~/.opk/config.yml مع إعدادات موفر OIDC. انظر الوثائق: https://github.com/openpubkey/opkssh#config.",
"opksshSignInWith": "تسجيل الدخول باستخدام {{provider}}",
"sudoPasswordPopupTitle": "إدراج كلمة المرور؟",
"sudoPasswordPopupHint": "اضغط Enter للإدراج ، Esc للإلغاء",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "تم رفض الاتصال بواسطة الخادم. الرجاء التحقق من المصادقة الخاصة بك وتكوين الشبكة.",
"hostKeyRejected": "تم رفض التحقق من مفتاح المضيف SSH. تم إلغاء الاتصال.",
"sessionTakenOver": "تم فتح الجلسة في علامة تبويب أخرى. إعادة الاتصال...",
"sessionAttachTimeout": "انتهت مهلة مرفق الجلسة. إنشاء اتصال جديد..."
"sessionAttachTimeout": "انتهت مهلة مرفق الجلسة. إنشاء اتصال جديد...",
"openFileManagerHere": "فتح مدير الملفات هنا"
},
"fileManager": {
"title": "مدير الملفات",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "ملفات رمز اللون حسب النوع: مجلدات (أحمر)، ملفات (أزرق)، روابط رمزية (خضراء)",
"commandAutocomplete": "إكمال الأوامر التلقائي",
"commandAutocompleteDesc": "تمكين اقتراحات الإكمال التلقائي لمفتاح علامة التبويب للأوامر الطرفية استناداً إلى سجل الأوامر الخاصة بك",
"commandHistoryTracking": "حفظ سجل الأوامر",
"commandHistoryTrackingDesc": "تخزين الأوامر الطرفية في التاريخ للإكمال التلقائي والشريط الجانبي. معطلة افتراضيا للخصوصية.",
"defaultSnippetFoldersCollapsed": "طي مجلدات كتلة الكود بشكل افتراضي",
"defaultSnippetFoldersCollapsedDesc": "عند التمكين، سيتم انهيار جميع مجلدات كتلة الكود عند فتح علامة تبويب كتل الكود",
"terminalSyntaxHighlighting": "تسليط الضوء على بناء الجملة",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "تسليط الضوء تلقائياً على الأوامر والمسارات وIP ومستويات السجل في المخرجات الطرفية",
"enableCommandPaletteShortcut": "تمكين اختصار قفل لوحة الأوامر",
"enableCommandPaletteShortcutDesc": "انقر نقرًا مزدوجًا على نوبات اليسار لفتح لوحة القيادة للوصول السريع إلى المضيفين",
"enableTerminalSessionPersistence": "الدورات الطرفية المستمرة",
"enableTerminalSessionPersistence": "تبويب/جلسات مستمرة",
"enableTerminalSessionPersistenceDesc": "الحفاظ على اتصالات SSH عند تبديل علامات التبويب أو إغلاق المتصفح (قد يكون غير مستقر)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "قاعدة البيانات",
"healthy": "صحي",
"error": "خطأ",
"totalServers": "مجموع الخوادم",
"totalHosts": "إجمالي المضيفين",
"totalTunnels": "إجمالي الأنفاق",
"totalCredentials": "مجموع بيانات الاعتماد",
"recentActivity": "النشاط الأخير",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Копиране на фрагмент в клипборда",
"editTooltip": "Редактирайте този фрагмент",
"deleteTooltip": "Изтрий този фрагмент",
"shareTooltip": "Share this snippet",
"sharedWithYou": "Shared",
"sharedBy": "Shared by {{username}}",
"shareSnippet": "Share Snippet",
"user": "User",
"role": "Role",
"selectTarget": "Select...",
"currentAccess": "Current Access",
"shareSuccess": "Snippet shared successfully",
"shareFailed": "Failed to share snippet",
"revokeSuccess": "Access revoked",
"revokeFailed": "Failed to revoke access",
"failedToLoadShareData": "Failed to load sharing data",
"newFolder": "Нова папка",
"reorderSameFolder": "Може да се пренареждат фрагменти само в рамките на една и съща папка",
"reorderSuccess": "Фрагментите са пренаредени успешно",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Изисква се удостоверяване. Моля, обновете страницата.",
"dataAccessLockedReauth": "Достъпът до данните е заключен. Моля, удостоверете се отново.",
"loading": "Зареждане на историята на командите...",
"error": "Грешка при зареждане на историята"
"error": "Грешка при зареждане на историята",
"disabledTitle": "Command History is Disabled",
"disabledDescription": "Enable Command History Tracking in your profile under Appearance settings."
},
"splitScreen": {
"title": "Разделен екран",
@@ -510,6 +525,8 @@
"checkingDatabase": "Проверка на връзката с базата данни...",
"checkingAuthentication": "Проверка на удостоверяването...",
"backendReconnected": "Връзката със сървъра е възстановена",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "Действия",
"remove": "Премахване",
"revoke": "Отмяна",
@@ -541,7 +558,8 @@
"copySudoPassword": "Копиране на парола за Sudo",
"passwordCopied": "Паролата е копирана в клипборда",
"sudoPasswordCopied": "Паролата за Sudo е копирана в клипборда",
"noPasswordAvailable": "Няма налична парола"
"noPasswordAvailable": "Няма налична парола",
"failedToCopyPassword": "Failed to copy password"
},
"admin": {
"title": "Административни настройки",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Глобалните настройки за наблюдение са запазени",
"failedToSaveGlobalSettings": "Запазването на настройките за глобално наблюдение не бе успешно",
"failedToLoadGlobalSettings": "Зареждането на настройките за глобално наблюдение не бе успешно",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Configure how long user sessions last before requiring re-authentication. 'Remember Me' sessions are unaffected (always 30 days).",
"sessionTimeoutHours": "Timeout Duration",
"hours": "hours",
"sessionTimeoutNote": "Valid range: 1720 hours. Changes apply to new sessions only.",
"sessionTimeoutSaved": "Session timeout saved",
"failedToSaveSessionTimeout": "Failed to save session timeout",
"guacamoleIntegration": "Интеграция с отдалечен работен плот (Guacamole)",
"guacamoleIntegrationDesc": "Активиране на RDP, VNC и Telnet връзки чрез guacd. Изисква се работещ екземпляр на guacd.",
"enableGuacamole": "Активиране на поддръжката на RDP/VNC/Telnet",
"guacdUrl": "URL адрес на guacd (хост:порт)",
"guacdUrlPlaceholder": "гуакд:4822",
"guacdUrlNote": "Промените в хоста/порта на guacd изискват рестартиране на сървъра, за да влязат в сила.",
"guacamoleSettingsSaved": "Настройките за гуакамоле са запазени",
"failedToSaveGuacamoleSettings": "Запазването на настройките за гуакамоле не бе успешно",
"failedToLoadGuacamoleSettings": "Зареждането на настройките за гуакамоле не бе успешно",
"logLevel": "Log Level",
"logLevelDesc": "Control the verbosity of server logs. Higher levels reduce log output. Can also be set via the LOG_LEVEL environment variable.",
"logVerbosity": "Verbosity",
"logLevelNote": "Changes take effect immediately. Debug level may impact performance.",
"logLevelSaved": "Log level saved",
"failedToSaveLogLevel": "Failed to save log level",
"clampedToValidRange": "беше коригиран до валиден диапазон",
"loadingSessions": "Зареждане на сесиите...",
"noActiveSessions": "Няма намерени активни сесии.",
"device": "Устройство",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} хостове",
"importJson": "Импортиране на JSON",
"importing": "Импортиране...",
"exportAllJson": "Export All",
"exporting": "Exporting...",
"exportedAllHosts": "Exported {{count}} hosts",
"failedToExportAllHosts": "Failed to export hosts. Please ensure you're logged in and have access to the host data.",
"exportAllSensitiveWarning": "The exported file will contain sensitive authentication data (passwords/SSH keys) in plaintext. Please keep the file secure and delete it after use.",
"importJsonTitle": "Импортиране на SSH хостове от JSON",
"importJsonDesc": "Качете JSON файл за групов импорт на множество SSH хостове (максимум 100).",
"downloadSample": "Изтегляне на пример",
@@ -866,11 +912,26 @@
"importError": "Грешка при импортиране",
"failedToImportJson": "Импортирането на JSON файл не бе успешно",
"connectionDetails": "Детайли за връзката",
"connectionType": "Тип връзка",
"ssh": "SSH",
"rdp": "ПРСР",
"vnc": "VNC",
"telnet": "Телнет",
"remoteDesktop": "Отдалечен работен плот",
"guacamoleSettings": "Настройки на отдалечен работен плот",
"organization": "Организация",
"ipAddress": "IP адрес или име на хост",
"macAddress": "MAC Address",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Събуждане по локална мрежа",
"wolSent": "Wake-on-LAN packet sent",
"wolFailed": "Failed to send Wake-on-LAN packet",
"ipRequired": "Изисква се IP адрес",
"portRequired": "Изисква се порт",
"port": "Порт",
"name": "Име",
"username": "Потребителско име",
"usernameRequired": "Потребителското име е задължително (само за SSH/Telnet)",
"folder": "Папка",
"tags": "Етикети",
"pin": "Закачи",
@@ -979,6 +1040,8 @@
"tunnel": "Тунел",
"fileManager": "Файлов мениджър",
"serverStats": "Статистика на сървъра",
"status": "Статус",
"statistics": "Статистика",
"hostViewer": "Преглед на хоста",
"enableServerStats": "Активиране на статистиката на сървъра",
"enableServerStatsDesc": "Активиране/деактивиране на събирането на статистика за сървъра за този хост",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Плъзнете, за да се придвижвате между папките",
"exportedHostConfig": "Експортирана е конфигурация на хоста за {{name}}",
"openTerminal": "Отвори терминал",
"openRdp": "Open RDP",
"openVnc": "Open VNC",
"openTelnet": "Open Telnet",
"openFileManager": "Отворете файловия мениджър",
"openTunnels": "Отворени тунели",
"openServerDetails": "Отворете подробности за сървъра",
"statistics": "Статистика",
"enabledWidgets": "Активирани джаджи",
"openServerStats": "Отворете статистиката на сървъра",
"enabledWidgetsDesc": "Изберете кои статистически уиджети да се показват за този хост",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Проверете дали сървърът е онлайн или офлайн",
"statusCheckInterval": "Интервал за проверка на състоянието",
"statusCheckIntervalDesc": "Колко често да проверявам дали хостът е онлайн (5 секунди - 1 час)",
"statusChecks": "Проверки на състоянието",
"disableTcpPing": "Деактивиране на TCP Ping",
"disableTcpPingDescription": "Изключете проверките за състояние (откриване на онлайн/офлайн) за този хост",
"useGlobalStatusInterval": "Използване на глобално по подразбиране",
"useGlobalMetricsInterval": "Използване на глобално по подразбиране",
"usingGlobalDefault": "Използване на глобалното по подразбиране ({{value}}s)",
"metricsCollection": "Колекция от показатели",
"metricsEnabled": "Активиране на мониторинг на показатели",
"metricsEnabledDesc": "Събиране на статистически данни за процесора, RAM, диска и други системни данни",
"metricsInterval": "Интервал на събиране на показатели",
"metricsIntervalDesc": "Колко често да се събира статистика за сървъра (5 секунди - 1 час)",
"metricsNotAvailableForConnectionType": "Метриките на сървъра са достъпни само за SSH хостове. Проверките за състоянието на TCP ping все още могат да бъдат активирани по-горе.",
"intervalSeconds": "секунди",
"intervalMinutes": "минути",
"intervalValidation": "Интервалите за наблюдение трябва да бъдат между 5 секунди и 1 час (3600 секунди)",
@@ -1133,6 +1203,10 @@
"noServerFound": "Не е намерен сървър",
"jumpHostsOrder": "Връзките ще бъдат осъществени в следния ред: Преход към хост 1 → Преход към хост 2 → ... → Целеви сървър",
"socks5Proxy": "SOCKS5 прокси",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Send a sequence of connection attempts to specified ports before connecting. Used to open firewall rules dynamically.",
"addKnock": "Add Port",
"delayMs": "Delay",
"socks5Description": "Конфигурирайте SOCKS5 прокси за SSH връзка. Целият трафик ще бъде пренасочен през посочения прокси сървър.",
"enableSocks5": "Активиране на SOCKS5 прокси",
"enableSocks5Description": "Използвайте SOCKS5 прокси за тази SSH връзка",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Автоматично изпълнение на MOSH команда при свързване",
"moshCommand": "MOSH команда",
"moshCommandDesc": "Командата MOSH за изпълнение",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automatically attach to an existing (or start a new) tmux session for persistent sessions and cross-device continuity",
"environmentVariables": "Променливи на средата",
"environmentVariablesDesc": "Задаване на персонализирани променливи на средата за терминалната сесия",
"variableName": "Име на променлива",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Копиране на URL адреса на тунела",
"copyServerStatsUrl": "Копиране на URL адреса за статистика на сървъра",
"copyDockerUrl": "Копиране на URL адреса на Docker",
"copyRemoteDesktopUrl": "Копиране на URL адреса на отдалечен работен плот",
"fullScreenUrlTooltip": "Копирайте URL адреса, за да отворите това приложение в режим на цял екран",
"notEnabled": "Docker не е активиран за този хост. Активирайте го в настройките на хоста, за да използвате функциите на Docker.",
"validating": "Валидиране на Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Избери всички",
"deselectAll": "Премахване на избора от всички",
"useGlobalStatusDefault": "Използване на глобално по подразбиране (състояние)",
"useGlobalMetricsDefault": "Използване на глобални настройки по подразбиране (метрики)"
"useGlobalMetricsDefault": "Използване на глобални настройки по подразбиране (метрики)",
"remoteDesktopSettings": "Настройки на отдалечен работен плот",
"domain": "Домейн",
"securityMode": "Режим на сигурност",
"ignoreCert": "Игнориране на сертификата",
"ignoreCertDesc": "Пропускане на проверката на TLS сертификата за тази връзка",
"displaySettings": "Настройки на дисплея",
"colorDepth": "Дълбочина на цвета",
"width": "Ширина",
"height": "Височина",
"dpi": "DPI",
"resizeMethod": "Метод за преоразмеряване",
"forceLossless": "Принудително беззагубно",
"audioSettings": "Аудио настройки",
"disableAudio": "Деактивиране на звука",
"enableAudioInput": "Активиране на аудио вход",
"rdpPerformance": "Производителност",
"enableWallpaper": "Активиране на тапет",
"enableTheming": "Активиране на тема",
"enableFontSmoothing": "Активиране на изглаждането на шрифта",
"enableFullWindowDrag": "Активиране на плъзгане през целия прозорец",
"enableDesktopComposition": "Активиране на композицията на работния плот",
"enableMenuAnimations": "Активиране на анимации на менюта",
"disableBitmapCaching": "Деактивиране на кеширането на растерни изображения",
"disableOffscreenCaching": "Деактивиране на кеширането извън екрана",
"disableGlyphCaching": "Деактивиране на кеширането на глифи",
"enableGfx": "Активиране на графични ефекти",
"deviceRedirection": "Пренасочване на устройството",
"enablePrinting": "Активиране на печат",
"enableDrive": "Активиране на пренасочване на устройството",
"driveName": "Име на устройството",
"drivePath": "Път на устройството",
"createDrivePath": "Създаване на път за устройство",
"disableDownload": "Деактивиране на изтеглянето",
"disableUpload": "Деактивиране на качването",
"enableTouch": "Активиране на докосване",
"rdpSession": "Сесия",
"clientName": "Име на клиента",
"consoleSession": "Конзолна сесия",
"initialProgram": "Първоначална програма",
"serverLayout": "Подредба на клавиатурата на сървъра",
"timezone": "Часова зона",
"gatewaySettings": "Портал",
"gatewayHostname": "Име на хост на шлюз",
"gatewayPort": "Порт на шлюза",
"gatewayUsername": "Потребителско име на шлюз",
"gatewayPassword": "Парола за шлюз",
"gatewayDomain": "Домейн на шлюз",
"remoteApp": "Отдалечено приложение",
"remoteAppProgram": "Отдалечено приложение",
"remoteAppDir": "Директория на отдалечени приложения",
"remoteAppArgs": "Аргументи за отдалечено приложение",
"clipboardSettings": "Клипборд",
"normalizeClipboard": "Нормализиране на клипборда",
"disableCopy": "Деактивиране на копирането",
"disablePaste": "Деактивиране на поставянето",
"vncSettings": "VNC настройки",
"cursorMode": "Режим на курсора",
"swapRedBlue": "Разменете червено/синьо",
"readOnly": "Само за четене",
"recordingSettings": "Запис",
"recordingPath": "Път на запис",
"recordingName": "Име на записа",
"createRecordingPath": "Създаване на път за запис",
"excludeOutput": "Изключване на изхода",
"excludeMouse": "Изключване на мишката",
"includeKeys": "Включване на ключове",
"sendWolPacket": "Изпращане на WoL пакет",
"wolMacAddr": "MAC адрес",
"wolBroadcastAddr": "Адрес за излъчване",
"wolUdpPort": "UDP порт",
"wolWaitTime": "Време за изчакване (секунди)",
"connectionSettings": "Настройки за връзка",
"rdpOnly": "Само ПРСР",
"vncOnly": "Само VNC",
"telnetTerminalSettings": "Настройки на терминала",
"terminalType": "Тип терминал",
"guacFontName": "Име на шрифта",
"guacFontSize": "Размер на шрифта",
"guacColorScheme": "Цветова схема",
"guacBackspaceKey": "Клавиш Backspace"
},
"guacamole": {
"connecting": "Свързване към {{type}} сесия...",
"rdpConnecting": "Свързване с RDP сървър...",
"vncConnecting": "Свързване с VNC сървър...",
"telnetConnecting": "Свързване с Telnet сървър...",
"connectionError": "Грешка при свързване",
"connectionFailed": "Връзката не бе успешна",
"failedToConnect": "Неуспешно получаване на токен за връзка"
},
"terminal": {
"title": "Терминал",
@@ -1364,7 +1530,7 @@
"closePanel": "Затвори панела",
"reconnect": "Свържете се отново",
"sessionEnded": "Сесията приключи",
"connectionLost": "Връзката е загубена",
"connectionLost": "Connection lost",
"error": "ГРЕШКА: {{message}}",
"disconnected": "Изключен",
"connectionClosed": "Връзката е затворена",
@@ -1372,6 +1538,7 @@
"connected": "Свързан",
"clipboardWriteFailed": "Копирането в клипборда не бе успешно. Уверете се, че страницата се предоставя през HTTPS или localhost.",
"clipboardReadFailed": "Четенето от клипборда не бе успешно. Уверете се, че са предоставени разрешения за достъп до клипборда.",
"clipboardHttpWarning": "Поставянето изисква HTTPS. Използвайте Ctrl+Shift+V или обслужвайте Termix през HTTPS.",
"sshConnected": "SSH връзка е установена",
"authError": "Удостоверяването не бе успешно: {{message}}",
"unknownError": "Възникна неизвестна грешка",
@@ -1380,7 +1547,25 @@
"connecting": "Свързване...",
"reconnecting": "Повторно свързване... ({{attempt}}/{{max}})",
"reconnected": "Успешно повторно свързване",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.",
"tmuxWindows": "Windows",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Attached clients",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "Last activity",
"tmuxTimeJustNow": "just now",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Start new session",
"tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard",
"maxReconnectAttemptsReached": "Достигнат е максималният брой опити за повторно свързване",
"closeTab": "Close",
"connectionTimeout": "Време за изчакване на връзката",
"terminalTitle": "Терминал - {{host}}",
"terminalWithPath": "Терминал - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Времето за изчакване на удостоверяването изтече. Моля, опитайте отново.",
"opksshAuthFailed": "Удостоверяването не бе успешно. Моля, проверете идентификационните си данни и опитайте отново.",
"opksshConfigMissing": "Конфигурацията на OPKSSH не е намерена. Моля, създайте ~/.opk/config.yml с настройките на вашия OIDC доставчик. Вижте документацията: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Sign in with {{provider}}",
"sudoPasswordPopupTitle": "Въвеждане на парола?",
"sudoPasswordPopupHint": "Натиснете Enter за вмъкване, Esc за отхвърляне",
"sudoPasswordPopupConfirm": "Вмъкване",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Връзката е отхвърлена от сървъра. Моля, проверете удостоверяването и мрежовата си конфигурация.",
"hostKeyRejected": "Проверката на SSH ключа за хост е отхвърлена. Връзката е прекратена.",
"sessionTakenOver": "Сесията беше отворена в друг раздел. Повторно свързване...",
"sessionAttachTimeout": "Времето за изчакване на прикачения файл към сесията изтече. Създаване на нова връзка..."
"sessionAttachTimeout": "Времето за изчакване на прикачения файл към сесията изтече. Създаване на нова връзка...",
"openFileManagerHere": "Open File Manager Here"
},
"fileManager": {
"title": "Файлов мениджър",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Цветово кодиране на файлове по тип: папки (червено), файлове (синьо), символни връзки (зелено)",
"commandAutocomplete": "Автоматично довършване на команди",
"commandAutocompleteDesc": "Активирайте предложения за автоматично довършване с клавиша Tab за команди в терминала въз основа на историята на вашите команди",
"commandHistoryTracking": "Save Command History",
"commandHistoryTrackingDesc": "Store terminal commands in history for autocomplete and sidebar. Disabled by default for privacy.",
"defaultSnippetFoldersCollapsed": "Свиване на папките с фрагменти по подразбиране",
"defaultSnippetFoldersCollapsedDesc": "Когато е активирано, всички папки с фрагменти ще бъдат свити, когато отворите раздела с фрагменти.",
"terminalSyntaxHighlighting": "Подчертаване на синтаксиса на терминала",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Автоматично маркиране на команди, пътища, IP адреси и нива на лог файлове в изхода на терминала",
"enableCommandPaletteShortcut": "Активиране на пряк път за палитра с команди",
"enableCommandPaletteShortcutDesc": "Докоснете двукратно левия Shift, за да отворите палитрата с команди за бърз достъп до хостове",
"enableTerminalSessionPersistence": "Постоянни терминални сесии",
"enableTerminalSessionPersistence": "Постоянни раздели/сесии",
"enableTerminalSessionPersistenceDesc": "Поддържане на SSH връзки при превключване на раздели или затваряне на браузъра (може да е нестабилно)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "База данни",
"healthy": "Здравословен",
"error": "Грешка",
"totalServers": "Общо сървъри",
"totalHosts": "Total Hosts",
"totalTunnels": "Общо тунели",
"totalCredentials": "Общо идентификационни данни",
"recentActivity": "Последна активност",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "ক্লিপবোর্ডে স্নিপেট কপি করুন",
"editTooltip": "এই স্নিপেটটি সম্পাদনা করুন",
"deleteTooltip": "এই স্নিপেটটি মুছুন",
"shareTooltip": "Share this snippet",
"sharedWithYou": "Shared",
"sharedBy": "Shared by {{username}}",
"shareSnippet": "Share Snippet",
"user": "User",
"role": "Role",
"selectTarget": "Select...",
"currentAccess": "Current Access",
"shareSuccess": "Snippet shared successfully",
"shareFailed": "Failed to share snippet",
"revokeSuccess": "Access revoked",
"revokeFailed": "Failed to revoke access",
"failedToLoadShareData": "Failed to load sharing data",
"newFolder": "নতুন ফোল্ডার",
"reorderSameFolder": "শুধুমাত্র একই ফোল্ডারের মধ্যে স্নিপেটগুলি পুনরায় সাজাতে পারে",
"reorderSuccess": "স্নিপেটগুলি সফলভাবে পুনঃক্রম করা হয়েছে",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "প্রমাণীকরণ প্রয়োজন। অনুগ্রহ করে পৃষ্ঠাটি রিফ্রেশ করুন।",
"dataAccessLockedReauth": "ডেটা অ্যাক্সেস লক করা আছে। অনুগ্রহ করে পুনরায় প্রমাণীকরণ করুন।",
"loading": "কমান্ডের ইতিহাস লোড হচ্ছে...",
"error": "ইতিহাস লোড করার সময় ত্রুটি"
"error": "ইতিহাস লোড করার সময় ত্রুটি",
"disabledTitle": "Command History is Disabled",
"disabledDescription": "Enable Command History Tracking in your profile under Appearance settings."
},
"splitScreen": {
"title": "স্প্লিট স্ক্রিন",
@@ -510,6 +525,8 @@
"checkingDatabase": "ডাটাবেস সংযোগ পরীক্ষা করা হচ্ছে...",
"checkingAuthentication": "প্রমাণীকরণ পরীক্ষা করা হচ্ছে...",
"backendReconnected": "সার্ভার সংযোগ পুনরুদ্ধার করা হয়েছে",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "কর্ম",
"remove": "অপসারণ",
"revoke": "প্রত্যাহার করুন",
@@ -541,7 +558,8 @@
"copySudoPassword": "সুডো পাসওয়ার্ড কপি করুন",
"passwordCopied": "পাসওয়ার্ড ক্লিপবোর্ডে কপি করা হয়েছে",
"sudoPasswordCopied": "সুডো পাসওয়ার্ড ক্লিপবোর্ডে কপি করা হয়েছে",
"noPasswordAvailable": "কোনও পাসওয়ার্ড নেই"
"noPasswordAvailable": "কোনও পাসওয়ার্ড নেই",
"failedToCopyPassword": "Failed to copy password"
},
"admin": {
"title": "অ্যাডমিন সেটিংস",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "বিশ্বব্যাপী পর্যবেক্ষণ সেটিংস সংরক্ষণ করা হয়েছে",
"failedToSaveGlobalSettings": "বিশ্বব্যাপী পর্যবেক্ষণ সেটিংস সংরক্ষণ করতে ব্যর্থ হয়েছে",
"failedToLoadGlobalSettings": "বিশ্বব্যাপী পর্যবেক্ষণ সেটিংস লোড করা যায়নি",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Configure how long user sessions last before requiring re-authentication. 'Remember Me' sessions are unaffected (always 30 days).",
"sessionTimeoutHours": "Timeout Duration",
"hours": "hours",
"sessionTimeoutNote": "Valid range: 1720 hours. Changes apply to new sessions only.",
"sessionTimeoutSaved": "Session timeout saved",
"failedToSaveSessionTimeout": "Failed to save session timeout",
"guacamoleIntegration": "রিমোট ডেস্কটপ ইন্টিগ্রেশন (গুয়াকামোল)",
"guacamoleIntegrationDesc": "guacd এর মাধ্যমে RDP, VNC, এবং Telnet সংযোগ সক্ষম করুন। চালানোর জন্য একটি guacd ইনস্ট্যান্স প্রয়োজন।",
"enableGuacamole": "RDP/VNC/Telnet সাপোর্ট সক্ষম করুন",
"guacdUrl": "guacd URL (হোস্ট:পোর্ট)",
"guacdUrlPlaceholder": "গুয়াকডি:৪৮২২",
"guacdUrlNote": "guacd হোস্ট/পোর্টে পরিবর্তন কার্যকর হওয়ার জন্য সার্ভার পুনঃসূচনা প্রয়োজন।",
"guacamoleSettingsSaved": "গুয়াকামোলের সেটিংস সেভ করা হয়েছে",
"failedToSaveGuacamoleSettings": "গুয়াকামোলের সেটিংস সংরক্ষণ করা যায়নি",
"failedToLoadGuacamoleSettings": "গুয়াকামোল সেটিংস লোড করা যায়নি",
"logLevel": "Log Level",
"logLevelDesc": "Control the verbosity of server logs. Higher levels reduce log output. Can also be set via the LOG_LEVEL environment variable.",
"logVerbosity": "Verbosity",
"logLevelNote": "Changes take effect immediately. Debug level may impact performance.",
"logLevelSaved": "Log level saved",
"failedToSaveLogLevel": "Failed to save log level",
"clampedToValidRange": "বৈধ পরিসরে সামঞ্জস্য করা হয়েছে",
"loadingSessions": "সেশন লোড হচ্ছে...",
"noActiveSessions": "কোন সক্রিয় সেশন পাওয়া যায়নি।",
"device": "যন্ত্র",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} হোস্ট",
"importJson": "JSON আমদানি করুন",
"importing": "আমদানি করা হচ্ছে...",
"exportAllJson": "Export All",
"exporting": "Exporting...",
"exportedAllHosts": "Exported {{count}} hosts",
"failedToExportAllHosts": "Failed to export hosts. Please ensure you're logged in and have access to the host data.",
"exportAllSensitiveWarning": "The exported file will contain sensitive authentication data (passwords/SSH keys) in plaintext. Please keep the file secure and delete it after use.",
"importJsonTitle": "JSON থেকে SSH হোস্ট আমদানি করুন",
"importJsonDesc": "একাধিক SSH হোস্ট (সর্বোচ্চ ১০০) বাল্ক ইম্পোর্ট করতে একটি JSON ফাইল আপলোড করুন।",
"downloadSample": "নমুনা ডাউনলোড করুন",
@@ -866,11 +912,26 @@
"importError": "আমদানি ত্রুটি",
"failedToImportJson": "JSON ফাইল আমদানি করা যায়নি",
"connectionDetails": "সংযোগের বিবরণ",
"connectionType": "সংযোগের ধরণ",
"ssh": "এসএসএইচ",
"rdp": "আরডিপি",
"vnc": "ভিএনসি",
"telnet": "টেলনেট",
"remoteDesktop": "রিমোট ডেস্কটপ",
"guacamoleSettings": "রিমোট ডেস্কটপ সেটিংস",
"organization": "সংগঠন",
"ipAddress": "আইপি ঠিকানা অথবা হোস্টনেম",
"macAddress": "MAC Address",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "ওয়েক-অন-ল্যান",
"wolSent": "Wake-on-LAN packet sent",
"wolFailed": "Failed to send Wake-on-LAN packet",
"ipRequired": "আইপি ঠিকানা প্রয়োজন",
"portRequired": "পোর্ট প্রয়োজন",
"port": "বন্দর",
"name": "নাম",
"username": "ব্যবহারকারীর নাম",
"usernameRequired": "ব্যবহারকারীর নাম আবশ্যক (শুধুমাত্র SSH/টেলনেট)",
"folder": "ফোল্ডার",
"tags": "ট্যাগ",
"pin": "পিন",
@@ -979,6 +1040,8 @@
"tunnel": "টানেল",
"fileManager": "ফাইল ম্যানেজার",
"serverStats": "সার্ভার পরিসংখ্যান",
"status": "অবস্থা",
"statistics": "পরিসংখ্যান",
"hostViewer": "হোস্ট ভিউয়ার",
"enableServerStats": "সার্ভার পরিসংখ্যান সক্ষম করুন",
"enableServerStatsDesc": "এই হোস্টের জন্য সার্ভার পরিসংখ্যান সংগ্রহ সক্ষম/অক্ষম করুন",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "ফোল্ডার থেকে অন্য ফোল্ডারে যেতে টেনে আনুন",
"exportedHostConfig": "{{name}}এর জন্য হোস্ট কনফিগারেশন রপ্তানি করা হয়েছে",
"openTerminal": "টার্মিনাল খুলুন",
"openRdp": "Open RDP",
"openVnc": "Open VNC",
"openTelnet": "Open Telnet",
"openFileManager": "ফাইল ম্যানেজার খুলুন",
"openTunnels": "খোলা টানেল",
"openServerDetails": "সার্ভারের বিবরণ খুলুন",
"statistics": "পরিসংখ্যান",
"enabledWidgets": "সক্রিয় উইজেট",
"openServerStats": "ওপেন সার্ভার পরিসংখ্যান",
"enabledWidgetsDesc": "এই হোস্টের জন্য কোন পরিসংখ্যান উইজেটগুলি প্রদর্শন করা হবে তা নির্বাচন করুন।",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "সার্ভারটি অনলাইন নাকি অফলাইন তা পরীক্ষা করুন।",
"statusCheckInterval": "স্থিতি পরীক্ষা ব্যবধান",
"statusCheckIntervalDesc": "হোস্ট অনলাইন আছে কিনা তা কতবার পরীক্ষা করতে হবে (৫ সেকেন্ড - ১ ঘন্টা)",
"statusChecks": "স্থিতি পরীক্ষা",
"disableTcpPing": "টিসিপি পিং অক্ষম করুন",
"disableTcpPingDescription": "এই হোস্টের জন্য স্ট্যাটাস চেক (অনলাইন/অফলাইন সনাক্তকরণ) বন্ধ করুন",
"useGlobalStatusInterval": "গ্লোবাল ডিফল্ট ব্যবহার করুন",
"useGlobalMetricsInterval": "গ্লোবাল ডিফল্ট ব্যবহার করুন",
"usingGlobalDefault": "গ্লোবাল ডিফল্ট ব্যবহার করে ({{value}}s)",
"metricsCollection": "মেট্রিক্স সংগ্রহ",
"metricsEnabled": "মেট্রিক্স মনিটরিং সক্ষম করুন",
"metricsEnabledDesc": "CPU, RAM, ডিস্ক এবং অন্যান্য সিস্টেম পরিসংখ্যান সংগ্রহ করুন",
"metricsInterval": "মেট্রিক্স সংগ্রহের ব্যবধান",
"metricsIntervalDesc": "সার্ভারের পরিসংখ্যান কত ঘন ঘন সংগ্রহ করতে হবে (৫ সেকেন্ড - ১ ঘন্টা)",
"metricsNotAvailableForConnectionType": "সার্ভার মেট্রিক্স শুধুমাত্র SSH হোস্টের জন্য উপলব্ধ। TCP পিং স্ট্যাটাস চেকগুলি এখনও উপরে সক্ষম করা যেতে পারে।",
"intervalSeconds": "সেকেন্ড",
"intervalMinutes": "মিনিট",
"intervalValidation": "পর্যবেক্ষণের ব্যবধান ৫ সেকেন্ড থেকে ১ ঘন্টা (৩৬০০ সেকেন্ড) এর মধ্যে হতে হবে।",
@@ -1133,6 +1203,10 @@
"noServerFound": "কোন সার্ভার পাওয়া যায়নি।",
"jumpHostsOrder": "সংযোগগুলি ক্রমানুসারে তৈরি করা হবে: জাম্প হোস্ট ১ → জাম্প হোস্ট ২ → ... → টার্গেট সার্ভার",
"socks5Proxy": "SOCKS5 প্রক্সি",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Send a sequence of connection attempts to specified ports before connecting. Used to open firewall rules dynamically.",
"addKnock": "Add Port",
"delayMs": "Delay",
"socks5Description": "SSH সংযোগের জন্য SOCKS5 প্রক্সি কনফিগার করুন। সমস্ত ট্র্যাফিক নির্দিষ্ট প্রক্সি সার্ভারের মাধ্যমে রাউট করা হবে।",
"enableSocks5": "SOCKS5 প্রক্সি সক্ষম করুন",
"enableSocks5Description": "এই SSH সংযোগের জন্য SOCKS5 প্রক্সি ব্যবহার করুন",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "কানেক্টে স্বয়ংক্রিয়ভাবে MOSH কমান্ড চালান",
"moshCommand": "মোশ কমান্ড",
"moshCommandDesc": "MOSH কমান্ডটি কার্যকর করার জন্য",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automatically attach to an existing (or start a new) tmux session for persistent sessions and cross-device continuity",
"environmentVariables": "পরিবেশগত পরিবর্তনশীল",
"environmentVariablesDesc": "টার্মিনাল সেশনের জন্য কাস্টম পরিবেশ ভেরিয়েবল সেট করুন",
"variableName": "চলকের নাম",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "টানেলের URL কপি করুন",
"copyServerStatsUrl": "সার্ভার পরিসংখ্যান URL কপি করুন",
"copyDockerUrl": "ডকার ইউআরএল কপি করুন",
"copyRemoteDesktopUrl": "রিমোট ডেস্কটপ URL কপি করুন",
"fullScreenUrlTooltip": "এই অ্যাপটি পূর্ণ-স্ক্রিন মোডে খুলতে URL কপি করুন",
"notEnabled": "এই হোস্টের জন্য ডকার সক্ষম করা নেই। ডকার বৈশিষ্ট্যগুলি ব্যবহার করতে হোস্ট সেটিংসে এটি সক্ষম করুন।",
"validating": "ডকার যাচাই করা হচ্ছে...",
@@ -1348,7 +1425,96 @@
"selectAll": "সবগুলো নির্বাচন করুন",
"deselectAll": "সবগুলো অনির্বাচন করুন",
"useGlobalStatusDefault": "গ্লোবাল ডিফল্ট (স্থিতি) ব্যবহার করুন",
"useGlobalMetricsDefault": "গ্লোবাল ডিফল্ট (মেট্রিক্স) ব্যবহার করুন"
"useGlobalMetricsDefault": "গ্লোবাল ডিফল্ট (মেট্রিক্স) ব্যবহার করুন",
"remoteDesktopSettings": "রিমোট ডেস্কটপ সেটিংস",
"domain": "ডোমেইন",
"securityMode": "নিরাপত্তা মোড",
"ignoreCert": "সার্টিফিকেট উপেক্ষা করুন",
"ignoreCertDesc": "এই সংযোগের জন্য TLS সার্টিফিকেট যাচাইকরণ এড়িয়ে যান",
"displaySettings": "প্রদর্শন সেটিংস",
"colorDepth": "রঙের গভীরতা",
"width": "প্রস্থ",
"height": "উচ্চতা",
"dpi": "ডিপিআই",
"resizeMethod": "আকার পরিবর্তনের পদ্ধতি",
"forceLossless": "ফোর্স লসলেস",
"audioSettings": "অডিও সেটিংস",
"disableAudio": "অডিও অক্ষম করুন",
"enableAudioInput": "অডিও ইনপুট সক্ষম করুন",
"rdpPerformance": "কর্মক্ষমতা",
"enableWallpaper": "ওয়ালপেপার সক্ষম করুন",
"enableTheming": "থিমিং সক্ষম করুন",
"enableFontSmoothing": "ফন্ট স্মুথিং সক্ষম করুন",
"enableFullWindowDrag": "সম্পূর্ণ উইন্ডো টেনে আনা সক্ষম করুন",
"enableDesktopComposition": "ডেস্কটপ রচনা সক্ষম করুন",
"enableMenuAnimations": "মেনু অ্যানিমেশন সক্রিয় করুন",
"disableBitmapCaching": "বিটম্যাপ ক্যাশিং অক্ষম করুন",
"disableOffscreenCaching": "অফস্ক্রিন ক্যাশিং অক্ষম করুন",
"disableGlyphCaching": "গ্লিফ ক্যাশিং অক্ষম করুন",
"enableGfx": "GFX সক্ষম করুন",
"deviceRedirection": "ডিভাইস পুনঃনির্দেশনা",
"enablePrinting": "মুদ্রণ সক্ষম করুন",
"enableDrive": "ড্রাইভ পুনঃনির্দেশনা সক্ষম করুন",
"driveName": "ড্রাইভের নাম",
"drivePath": "ড্রাইভ পাথ",
"createDrivePath": "ড্রাইভ পাথ তৈরি করুন",
"disableDownload": "ডাউনলোড বন্ধ করুন",
"disableUpload": "আপলোড অক্ষম করুন",
"enableTouch": "টাচ সক্ষম করুন",
"rdpSession": "অধিবেশন",
"clientName": "ক্লায়েন্টের নাম",
"consoleSession": "কনসোল সেশন",
"initialProgram": "প্রাথমিক প্রোগ্রাম",
"serverLayout": "সার্ভার কীবোর্ড লেআউট",
"timezone": "সময় অঞ্চল",
"gatewaySettings": "প্রবেশপথ",
"gatewayHostname": "গেটওয়ে হোস্টনেম",
"gatewayPort": "গেটওয়ে পোর্ট",
"gatewayUsername": "গেটওয়ে ব্যবহারকারীর নাম",
"gatewayPassword": "গেটওয়ে পাসওয়ার্ড",
"gatewayDomain": "গেটওয়ে ডোমেইন",
"remoteApp": "রিমোটঅ্যাপ",
"remoteAppProgram": "রিমোট অ্যাপ্লিকেশন",
"remoteAppDir": "রিমোট অ্যাপ ডিরেক্টরি",
"remoteAppArgs": "রিমোট অ্যাপ আর্গুমেন্ট",
"clipboardSettings": "ক্লিপবোর্ড",
"normalizeClipboard": "ক্লিপবোর্ড স্বাভাবিক করুন",
"disableCopy": "অনুলিপি অক্ষম করুন",
"disablePaste": "পেস্ট অক্ষম করুন",
"vncSettings": "ভিএনসি সেটিংস",
"cursorMode": "কার্সার মোড",
"swapRedBlue": "লাল/নীল অদলবদল করুন",
"readOnly": "কেবল পঠনযোগ্য",
"recordingSettings": "রেকর্ডিং",
"recordingPath": "রেকর্ডিং পাথ",
"recordingName": "রেকর্ডিং নাম",
"createRecordingPath": "রেকর্ডিং পাথ তৈরি করুন",
"excludeOutput": "আউটপুট বাদ দিন",
"excludeMouse": "মাউস বাদ দিন",
"includeKeys": "কী অন্তর্ভুক্ত করুন",
"sendWolPacket": "ওওএল প্যাকেট পাঠান",
"wolMacAddr": "ম্যাক ঠিকানা",
"wolBroadcastAddr": "সম্প্রচার ঠিকানা",
"wolUdpPort": "ইউডিপি পোর্ট",
"wolWaitTime": "অপেক্ষার সময় (সেকেন্ড)",
"connectionSettings": "সংযোগ সেটিংস",
"rdpOnly": "শুধুমাত্র আরডিপি",
"vncOnly": "শুধুমাত্র VNC",
"telnetTerminalSettings": "টার্মিনাল সেটিংস",
"terminalType": "টার্মিনালের ধরণ",
"guacFontName": "ফন্টের নাম",
"guacFontSize": "ফন্ট সাইজ",
"guacColorScheme": "রঙের স্কিম",
"guacBackspaceKey": "ব্যাকস্পেস কী"
},
"guacamole": {
"connecting": "{{type}} সেশনে সংযোগ করা হচ্ছে...",
"rdpConnecting": "RDP সার্ভারের সাথে সংযোগ করা হচ্ছে...",
"vncConnecting": "VNC সার্ভারের সাথে সংযোগ করা হচ্ছে...",
"telnetConnecting": "টেলনেট সার্ভারের সাথে সংযোগ স্থাপন করা হচ্ছে...",
"connectionError": "সংযোগ ত্রুটি",
"connectionFailed": "সংযোগ ব্যর্থ হয়েছে",
"failedToConnect": "সংযোগ টোকেন পেতে ব্যর্থ হয়েছে"
},
"terminal": {
"title": "টার্মিনাল",
@@ -1364,7 +1530,7 @@
"closePanel": "প্যানেল বন্ধ করুন",
"reconnect": "পুনঃসংযোগ করুন",
"sessionEnded": "অধিবেশন শেষ হয়েছে",
"connectionLost": "সংযোগ বিচ্ছিন্ন",
"connectionLost": "Connection lost",
"error": "ত্রুটি: {{message}}",
"disconnected": "সংযোগ বিচ্ছিন্ন",
"connectionClosed": "সংযোগ বন্ধ আছে",
@@ -1372,6 +1538,7 @@
"connected": "সংযুক্ত",
"clipboardWriteFailed": "ক্লিপবোর্ডে কপি করা যায়নি। নিশ্চিত করুন যে পৃষ্ঠাটি HTTPS অথবা localhost এর মাধ্যমে পরিবেশিত হচ্ছে।",
"clipboardReadFailed": "ক্লিপবোর্ড থেকে পড়া যায়নি। ক্লিপবোর্ডের অনুমতি মঞ্জুর করা হয়েছে কিনা তা নিশ্চিত করুন।",
"clipboardHttpWarning": "পেস্ট করার জন্য HTTPS প্রয়োজন। Ctrl+Shift+V ব্যবহার করুন অথবা HTTPS এর মাধ্যমে Termix পরিবেশন করুন।",
"sshConnected": "SSH সংযোগ স্থাপন করা হয়েছে",
"authError": "প্রমাণীকরণ ব্যর্থ হয়েছে: {{message}}",
"unknownError": "অজানা ত্রুটি ঘটেছে",
@@ -1380,7 +1547,25 @@
"connecting": "সংযোগ করা হচ্ছে...",
"reconnecting": "পুনঃসংযোগ করা হচ্ছে... ({{attempt}}/{{max}})",
"reconnected": "সফলভাবে পুনরায় সংযোগ করা হয়েছে",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.",
"tmuxWindows": "Windows",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Attached clients",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "Last activity",
"tmuxTimeJustNow": "just now",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Start new session",
"tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard",
"maxReconnectAttemptsReached": "পুনঃসংযোগের সর্বোচ্চ প্রচেষ্টায় পৌঁছেছেন",
"closeTab": "Close",
"connectionTimeout": "সংযোগের সময়সীমা শেষ",
"terminalTitle": "টার্মিনাল - {{host}}",
"terminalWithPath": "টার্মিনাল - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "প্রমাণীকরণের সময় শেষ। অনুগ্রহ করে আবার চেষ্টা করুন।",
"opksshAuthFailed": "প্রমাণীকরণ ব্যর্থ হয়েছে। অনুগ্রহ করে আপনার শংসাপত্রগুলি পরীক্ষা করে আবার চেষ্টা করুন।",
"opksshConfigMissing": "OPKSSH কনফিগারেশন খুঁজে পাওয়া যায়নি। আপনার OIDC প্রোভাইডার সেটিংস দিয়ে ~/.opk/config.yml তৈরি করুন। ডকুমেন্টেশন দেখুন: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Sign in with {{provider}}",
"sudoPasswordPopupTitle": "পাসওয়ার্ড ঢোকাবেন?",
"sudoPasswordPopupHint": "সন্নিবেশ করতে Enter টিপুন এবং খারিজ করতে Esc টিপুন",
"sudoPasswordPopupConfirm": "ঢোকান",
@@ -1428,7 +1614,8 @@
"connectionRejected": "সার্ভার সংযোগ প্রত্যাখ্যান করেছে। অনুগ্রহ করে আপনার প্রমাণীকরণ এবং নেটওয়ার্ক কনফিগারেশন পরীক্ষা করুন।",
"hostKeyRejected": "SSH হোস্ট কী যাচাইকরণ প্রত্যাখ্যাত হয়েছে। সংযোগ বাতিল করা হয়েছে।",
"sessionTakenOver": "অন্য ট্যাবে সেশন খোলা হয়েছে। পুনরায় সংযোগ করা হচ্ছে...",
"sessionAttachTimeout": "সেশন সংযুক্তির সময় শেষ। নতুন সংযোগ তৈরি করা হচ্ছে..."
"sessionAttachTimeout": "সেশন সংযুক্তির সময় শেষ। নতুন সংযোগ তৈরি করা হচ্ছে...",
"openFileManagerHere": "Open File Manager Here"
},
"fileManager": {
"title": "ফাইল ম্যানেজার",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "প্রকার অনুসারে রঙ-কোড ফাইল: ফোল্ডার (লাল), ফাইল (নীল), সিমলিঙ্ক (সবুজ)",
"commandAutocomplete": "কমান্ড স্বয়ংসম্পূর্ণ",
"commandAutocompleteDesc": "আপনার কমান্ড ইতিহাসের উপর ভিত্তি করে টার্মিনাল কমান্ডের জন্য ট্যাব কী স্বয়ংসম্পূর্ণ পরামর্শ সক্ষম করুন",
"commandHistoryTracking": "Save Command History",
"commandHistoryTrackingDesc": "Store terminal commands in history for autocomplete and sidebar. Disabled by default for privacy.",
"defaultSnippetFoldersCollapsed": "ডিফল্টভাবে স্নিপেট ফোল্ডারগুলি সঙ্কুচিত করুন",
"defaultSnippetFoldersCollapsedDesc": "সক্রিয় থাকা অবস্থায়, স্নিপেট ট্যাব খুললে সমস্ত স্নিপেট ফোল্ডার আড়াল হয়ে যাবে।",
"terminalSyntaxHighlighting": "টার্মিনাল সিনট্যাক্স হাইলাইটিং",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "টার্মিনাল আউটপুটে স্বয়ংক্রিয়ভাবে কমান্ড, পাথ, আইপি এবং লগ লেভেল হাইলাইট করুন",
"enableCommandPaletteShortcut": "কমান্ড প্যালেট শর্টকাট সক্ষম করুন",
"enableCommandPaletteShortcutDesc": "হোস্টগুলিতে দ্রুত অ্যাক্সেসের জন্য কমান্ড প্যালেট খুলতে বাম দিকে Shift-এ দুবার ট্যাপ করুন।",
"enableTerminalSessionPersistence": "স্থায়ী টার্মিনাল সেশন",
"enableTerminalSessionPersistence": "স্থায়ী ট্যাব/সেশন",
"enableTerminalSessionPersistenceDesc": "ট্যাব স্যুইচ করার সময় বা ব্রাউজার বন্ধ করার সময় SSH সংযোগ বজায় রাখুন (অস্থির হতে পারে)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "ডাটাবেস",
"healthy": "সুস্থ",
"error": "ত্রুটি",
"totalServers": "মোট সার্ভার",
"totalHosts": "Total Hosts",
"totalTunnels": "মোট টানেল",
"totalCredentials": "মোট প্রমাণপত্রাদি",
"recentActivity": "সাম্প্রতিক কার্যকলাপ",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Copia el fragment al porta-retalls",
"editTooltip": "Edita aquest fragment",
"deleteTooltip": "Suprimeix aquest fragment",
"shareTooltip": "Share this snippet",
"sharedWithYou": "Shared",
"sharedBy": "Shared by {{username}}",
"shareSnippet": "Share Snippet",
"user": "User",
"role": "Role",
"selectTarget": "Select...",
"currentAccess": "Current Access",
"shareSuccess": "Snippet shared successfully",
"shareFailed": "Failed to share snippet",
"revokeSuccess": "Access revoked",
"revokeFailed": "Failed to revoke access",
"failedToLoadShareData": "Failed to load sharing data",
"newFolder": "Nova carpeta",
"reorderSameFolder": "Només es poden reordenar fragments dins de la mateixa carpeta",
"reorderSuccess": "Fragments reordenats correctament",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Cal autenticació. Si us plau, actualitzeu la pàgina.",
"dataAccessLockedReauth": "Accés a les dades bloquejat. Si us plau, torneu a autenticar-vos.",
"loading": "S'està carregant l'historial d'ordres...",
"error": "S'ha produït un error en carregar l'historial"
"error": "S'ha produït un error en carregar l'historial",
"disabledTitle": "Command History is Disabled",
"disabledDescription": "Enable Command History Tracking in your profile under Appearance settings."
},
"splitScreen": {
"title": "Pantalla dividida",
@@ -510,6 +525,8 @@
"checkingDatabase": "S'està comprovant la connexió a la base de dades...",
"checkingAuthentication": "S'està comprovant l'autenticació...",
"backendReconnected": "Connexió al servidor restaurada",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "Accions",
"remove": "Elimina",
"revoke": "Revocar",
@@ -541,7 +558,8 @@
"copySudoPassword": "Copia la contrasenya de Sudo",
"passwordCopied": "La contrasenya s'ha copiat al porta-retalls",
"sudoPasswordCopied": "Contrasenya de Sudo copiada al porta-retalls",
"noPasswordAvailable": "No hi ha cap contrasenya disponible"
"noPasswordAvailable": "No hi ha cap contrasenya disponible",
"failedToCopyPassword": "Failed to copy password"
},
"admin": {
"title": "Configuració de l'administrador",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "S'ha desat la configuració de monitorització global",
"failedToSaveGlobalSettings": "No s'ha pogut desar la configuració de monitorització global",
"failedToLoadGlobalSettings": "No s'ha pogut carregar la configuració de monitorització global",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Configure how long user sessions last before requiring re-authentication. 'Remember Me' sessions are unaffected (always 30 days).",
"sessionTimeoutHours": "Timeout Duration",
"hours": "hours",
"sessionTimeoutNote": "Valid range: 1720 hours. Changes apply to new sessions only.",
"sessionTimeoutSaved": "Session timeout saved",
"failedToSaveSessionTimeout": "Failed to save session timeout",
"guacamoleIntegration": "Integració d'escriptori remot (Guacamole)",
"guacamoleIntegrationDesc": "Habilita les connexions RDP, VNC i Telnet mitjançant guacd. Requereix que hi hagi una instància de guacd en execució.",
"enableGuacamole": "Habilita la compatibilitat amb RDP/VNC/Telnet",
"guacdUrl": "URL guacd (amfitrió:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Els canvis a l'amfitrió/port guacd requereixen un reinici del servidor per tenir efecte.",
"guacamoleSettingsSaved": "Configuració del guacamole desada",
"failedToSaveGuacamoleSettings": "No s'ha pogut desar la configuració del guacamole",
"failedToLoadGuacamoleSettings": "No s'ha pogut carregar la configuració del guacamole",
"logLevel": "Log Level",
"logLevelDesc": "Control the verbosity of server logs. Higher levels reduce log output. Can also be set via the LOG_LEVEL environment variable.",
"logVerbosity": "Verbosity",
"logLevelNote": "Changes take effect immediately. Debug level may impact performance.",
"logLevelSaved": "Log level saved",
"failedToSaveLogLevel": "Failed to save log level",
"clampedToValidRange": "s'ha ajustat al rang vàlid",
"loadingSessions": "S'estan carregant les sessions...",
"noActiveSessions": "No s'han trobat sessions actives.",
"device": "Dispositiu",
@@ -843,6 +884,11 @@
"hostsCount": "amfitrions {{count}}",
"importJson": "Importa JSON",
"importing": "Important...",
"exportAllJson": "Export All",
"exporting": "Exporting...",
"exportedAllHosts": "Exported {{count}} hosts",
"failedToExportAllHosts": "Failed to export hosts. Please ensure you're logged in and have access to the host data.",
"exportAllSensitiveWarning": "The exported file will contain sensitive authentication data (passwords/SSH keys) in plaintext. Please keep the file secure and delete it after use.",
"importJsonTitle": "Importa hosts SSH des de JSON",
"importJsonDesc": "Puja un fitxer JSON per importar de manera massiva diversos hosts SSH (màxim 100).",
"downloadSample": "Descarrega la mostra",
@@ -866,11 +912,26 @@
"importError": "Error d'importació",
"failedToImportJson": "No s'ha pogut importar el fitxer JSON",
"connectionDetails": "Detalls de la connexió",
"connectionType": "Tipus de connexió",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Escriptori remot",
"guacamoleSettings": "Configuració de l'escriptori remot",
"organization": "Organització",
"ipAddress": "Adreça IP o nom d'amfitrió",
"macAddress": "MAC Address",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Wake-on-LAN packet sent",
"wolFailed": "Failed to send Wake-on-LAN packet",
"ipRequired": "Cal una adreça IP",
"portRequired": "El port és obligatori",
"port": "Port",
"name": "Nom",
"username": "Nom d'usuari",
"usernameRequired": "Cal un nom d'usuari (només SSH/Telnet)",
"folder": "Carpeta",
"tags": "Etiquetes",
"pin": "Fixar",
@@ -979,6 +1040,8 @@
"tunnel": "Túnel",
"fileManager": "Gestor de fitxers",
"serverStats": "Estadístiques del servidor",
"status": "Estat",
"statistics": "Estadístiques",
"hostViewer": "Visualitzador de l'amfitrió",
"enableServerStats": "Habilita les estadístiques del servidor",
"enableServerStatsDesc": "Activa/desactiva la recopilació d'estadístiques del servidor per a aquest amfitrió",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Arrossega per moure't entre carpetes",
"exportedHostConfig": "Configuració d'amfitrió exportada per a {{name}}",
"openTerminal": "Obre el terminal",
"openRdp": "Open RDP",
"openVnc": "Open VNC",
"openTelnet": "Open Telnet",
"openFileManager": "Obre el gestor de fitxers",
"openTunnels": "Túnels oberts",
"openServerDetails": "Obre els detalls del servidor",
"statistics": "Estadístiques",
"enabledWidgets": "Widgets activats",
"openServerStats": "Estadístiques del servidor obert",
"enabledWidgetsDesc": "Seleccioneu els widgets d'estadístiques que voleu mostrar per a aquest amfitrió",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Comprova si el servidor està en línia o fora de línia",
"statusCheckInterval": "Interval de comprovació d'estat",
"statusCheckIntervalDesc": "Amb quina freqüència cal comprovar si l'amfitrió està en línia (5 segons - 1 hora)",
"statusChecks": "Comprovacions d'estat",
"disableTcpPing": "Desactiva el ping TCP",
"disableTcpPingDescription": "Desactiva les comprovacions d'estat (detecció en línia/fora de línia) per a aquest amfitrió",
"useGlobalStatusInterval": "Utilitza el valor predeterminat global",
"useGlobalMetricsInterval": "Utilitza el valor predeterminat global",
"usingGlobalDefault": "Utilitzant el valor per defecte global ({{value}}s)",
"metricsCollection": "Col·lecció de mètriques",
"metricsEnabled": "Activa la supervisió de mètriques",
"metricsEnabledDesc": "Recopila estadístiques de CPU, RAM, disc i altres estadístiques del sistema",
"metricsInterval": "Interval de recopilació de mètriques",
"metricsIntervalDesc": "Amb quina freqüència s'han de recopilar estadístiques del servidor (5 segons - 1 hora)",
"metricsNotAvailableForConnectionType": "Les mètriques del servidor només estan disponibles per a hosts SSH. Les comprovacions d'estat del ping TCP encara es poden habilitar més amunt.",
"intervalSeconds": "segons",
"intervalMinutes": "minuts",
"intervalValidation": "Els intervals de monitorització han d'estar entre 5 segons i 1 hora (3600 segons)",
@@ -1133,6 +1203,10 @@
"noServerFound": "No s'ha trobat cap servidor",
"jumpHostsOrder": "Les connexions es faran en aquest ordre: Jump Host 1 → Jump Host 2 → ... → Servidor de destinació",
"socks5Proxy": "Proxy SOCKS5",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Send a sequence of connection attempts to specified ports before connecting. Used to open firewall rules dynamically.",
"addKnock": "Add Port",
"delayMs": "Delay",
"socks5Description": "Configura el servidor intermediari SOCKS5 per a la connexió SSH. Tot el trànsit es dirigirà a través del servidor intermediari especificat.",
"enableSocks5": "Habilita el servidor intermediari SOCKS5",
"enableSocks5Description": "Utilitza el proxy SOCKS5 per a aquesta connexió SSH",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Executa automàticament l'ordre MOSH en connectar-se",
"moshCommand": "Comandament MOSH",
"moshCommandDesc": "L'ordre MOSH a executar",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automatically attach to an existing (or start a new) tmux session for persistent sessions and cross-device continuity",
"environmentVariables": "Variables d'entorn",
"environmentVariablesDesc": "Estableix variables d'entorn personalitzades per a la sessió de terminal",
"variableName": "Nom de la variable",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Copia l'URL del túnel",
"copyServerStatsUrl": "Copia l'URL de les estadístiques del servidor",
"copyDockerUrl": "Copia l'URL de Docker",
"copyRemoteDesktopUrl": "Copia l'URL de l'escriptori remot",
"fullScreenUrlTooltip": "Copia l'URL per obrir aquesta aplicació en mode de pantalla completa",
"notEnabled": "Docker no està habilitat per a aquest amfitrió. Activeu-lo a la configuració de l'amfitrió per utilitzar les funcions de Docker.",
"validating": "Validant Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Selecciona-ho tot",
"deselectAll": "Desselecciona-ho tot",
"useGlobalStatusDefault": "Utilitza el valor per defecte global (estat)",
"useGlobalMetricsDefault": "Utilitza el valor predeterminat global (mètriques)"
"useGlobalMetricsDefault": "Utilitza el valor predeterminat global (mètriques)",
"remoteDesktopSettings": "Configuració de l'escriptori remot",
"domain": "Domini",
"securityMode": "Mode de seguretat",
"ignoreCert": "Ignora el certificat",
"ignoreCertDesc": "Omet la verificació del certificat TLS per a aquesta connexió",
"displaySettings": "Configuració de la pantalla",
"colorDepth": "Profunditat de color",
"width": "Amplada",
"height": "Alçada",
"dpi": "DPI",
"resizeMethod": "Mètode de redimensionament",
"forceLossless": "Força sense pèrdues",
"audioSettings": "Configuració d'àudio",
"disableAudio": "Desactiva l'àudio",
"enableAudioInput": "Activa l'entrada d'àudio",
"rdpPerformance": "Rendiment",
"enableWallpaper": "Activa el fons de pantalla",
"enableTheming": "Activa els temes",
"enableFontSmoothing": "Activa el suavització de la font",
"enableFullWindowDrag": "Activa l'arrossegament de tota la finestra",
"enableDesktopComposition": "Activa la composició de l'escriptori",
"enableMenuAnimations": "Activa les animacions del menú",
"disableBitmapCaching": "Desactiva la memòria cau de mapes de bits",
"disableOffscreenCaching": "Desactiva la memòria cau fora de pantalla",
"disableGlyphCaching": "Desactiva la memòria cau de glifs",
"enableGfx": "Activa els efectes especials",
"deviceRedirection": "Redirecció de dispositius",
"enablePrinting": "Habilita la impressió",
"enableDrive": "Activa la redirecció de la unitat",
"driveName": "Nom de la unitat",
"drivePath": "Camí de conducció",
"createDrivePath": "Crea una ruta de conducció",
"disableDownload": "Desactiva la descàrrega",
"disableUpload": "Desactiva la càrrega",
"enableTouch": "Activa el tacte",
"rdpSession": "Sessió",
"clientName": "Nom del client",
"consoleSession": "Sessió de consola",
"initialProgram": "Programa inicial",
"serverLayout": "Disposició del teclat del servidor",
"timezone": "Zona horària",
"gatewaySettings": "Porta d'entrada",
"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",
"remoteApp": "Aplicació remota",
"remoteAppProgram": "Aplicació remota",
"remoteAppDir": "Directori d'aplicacions remotes",
"remoteAppArgs": "Arguments d'aplicacions remotes",
"clipboardSettings": "Porta-retalls",
"normalizeClipboard": "Normalitzar el porta-retalls",
"disableCopy": "Desactiva la còpia",
"disablePaste": "Desactiva l'enganxament",
"vncSettings": "Configuració de VNC",
"cursorMode": "Mode del cursor",
"swapRedBlue": "Intercanvia vermell/blau",
"readOnly": "Només lectura",
"recordingSettings": "Enregistrament",
"recordingPath": "Ruta d'enregistrament",
"recordingName": "Nom de l'enregistrament",
"createRecordingPath": "Crea una ruta d'enregistrament",
"excludeOutput": "Exclou la sortida",
"excludeMouse": "Exclou el ratolí",
"includeKeys": "Inclou claus",
"sendWolPacket": "Enviar paquet WoL",
"wolMacAddr": "Adreça MAC",
"wolBroadcastAddr": "Adreça de difusió",
"wolUdpPort": "Port UDP",
"wolWaitTime": "Temps d'espera (segons)",
"connectionSettings": "Configuració de connexió",
"rdpOnly": "Només RDP",
"vncOnly": "Només VNC",
"telnetTerminalSettings": "Configuració del terminal",
"terminalType": "Tipus de terminal",
"guacFontName": "Nom de la font",
"guacFontSize": "Mida de la lletra",
"guacColorScheme": "Esquema de colors",
"guacBackspaceKey": "Tecla Retrocés"
},
"guacamole": {
"connecting": "Connectant a la sessió {{type}}...",
"rdpConnecting": "Connectant al servidor RDP...",
"vncConnecting": "Connectant al servidor VNC...",
"telnetConnecting": "Connectant al servidor Telnet...",
"connectionError": "Error de connexió",
"connectionFailed": "La connexió ha fallat",
"failedToConnect": "No s'ha pogut obtenir el testimoni de connexió"
},
"terminal": {
"title": "Terminal",
@@ -1364,7 +1530,7 @@
"closePanel": "Tanca el panell",
"reconnect": "Reconnecta",
"sessionEnded": "Sessió finalitzada",
"connectionLost": "Connexió perduda",
"connectionLost": "Connection lost",
"error": "ERROR: {{message}}",
"disconnected": "Desconnectat",
"connectionClosed": "Connexió tancada",
@@ -1372,6 +1538,7 @@
"connected": "Connectat",
"clipboardWriteFailed": "No s'ha pogut copiar al porta-retalls. Assegureu-vos que la pàgina es serveix per HTTPS o localhost.",
"clipboardReadFailed": "No s'ha pogut llegir del porta-retalls. Assegureu-vos que els permisos del porta-retalls estiguin concedits.",
"clipboardHttpWarning": "Per enganxar cal HTTPS. Feu servir Ctrl+Maj+V o publiqueu Termix sobre HTTPS.",
"sshConnected": "Connexió SSH establerta",
"authError": "L'autenticació ha fallat: {{message}}",
"unknownError": "S'ha produït un error desconegut",
@@ -1380,7 +1547,25 @@
"connecting": "Connectant...",
"reconnecting": "Reconnectant... ({{attempt}}/{{max}})",
"reconnected": "S'ha reconnectat correctament",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.",
"tmuxWindows": "Windows",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Attached clients",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "Last activity",
"tmuxTimeJustNow": "just now",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Start new session",
"tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard",
"maxReconnectAttemptsReached": "S'ha arribat al màxim d'intents de reconnexió",
"closeTab": "Close",
"connectionTimeout": "Temps d'espera de connexió",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "S'ha esgotat el temps d'autenticació. Torna-ho a provar.",
"opksshAuthFailed": "L'autenticació ha fallat. Si us plau, comproveu les vostres credencials i torneu-ho a intentar.",
"opksshConfigMissing": "No s'ha trobat la configuració d'OPKSSH. Si us plau, creeu ~/.opk/config.yml amb la configuració del vostre proveïdor OIDC. Vegeu la documentació: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Sign in with {{provider}}",
"sudoPasswordPopupTitle": "Voleu inserir la contrasenya?",
"sudoPasswordPopupHint": "Premeu Intro per inserir, Esc per tancar",
"sudoPasswordPopupConfirm": "Insereix",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Connexió rebutjada pel servidor. Si us plau, comproveu l'autenticació i la configuració de xarxa.",
"hostKeyRejected": "Verificació de la clau de l'amfitrió SSH rebutjada. Connexió cancel·lada.",
"sessionTakenOver": "La sessió s'ha obert en una altra pestanya. S'està tornant a connectar...",
"sessionAttachTimeout": "S'ha esgotat el temps d'espera per al fitxer adjunt de la sessió. S'està creant una connexió nova..."
"sessionAttachTimeout": "S'ha esgotat el temps d'espera per al fitxer adjunt de la sessió. S'està creant una connexió nova...",
"openFileManagerHere": "Open File Manager Here"
},
"fileManager": {
"title": "Gestor de fitxers",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Codifica els fitxers per colors per tipus: carpetes (vermell), fitxers (blau), enllaços simbòlics (verd)",
"commandAutocomplete": "Autocompletar ordres",
"commandAutocompleteDesc": "Activa els suggeriments d'autocompleció de la tecla Tab per a les ordres del terminal basant-te en l'historial d'ordres",
"commandHistoryTracking": "Save Command History",
"commandHistoryTrackingDesc": "Store terminal commands in history for autocomplete and sidebar. Disabled by default for privacy.",
"defaultSnippetFoldersCollapsed": "Replega les carpetes de fragments per defecte",
"defaultSnippetFoldersCollapsedDesc": "Quan està activat, totes les carpetes de fragments es reduiran quan obriu la pestanya de fragments.",
"terminalSyntaxHighlighting": "Ressaltat de la sintaxi del terminal",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Ressaltar automàticament les ordres, les rutes, les IP i els nivells de registre a la sortida del terminal",
"enableCommandPaletteShortcut": "Activa la drecera de la paleta d'ordres",
"enableCommandPaletteShortcutDesc": "Toqueu dues vegades la tecla Majúscules esquerra per obrir la paleta d'ordres i accedir ràpidament als amfitrions.",
"enableTerminalSessionPersistence": "Sessions terminals persistents",
"enableTerminalSessionPersistence": "Pestanyes/Sessions persistents",
"enableTerminalSessionPersistenceDesc": "Mantingueu les connexions SSH en canviar de pestanya o tancar el navegador (pot ser inestable)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Base de dades",
"healthy": "Saludable",
"error": "Error",
"totalServers": "Servidors totals",
"totalHosts": "Total Hosts",
"totalTunnels": "Túnels totals",
"totalCredentials": "Credencials totals",
"recentActivity": "Activitat recent",
+234 -45
View File
@@ -244,14 +244,14 @@
}
},
"snippets": {
"title": "Výstřižky bloků",
"new": "Nový snippet",
"create": "Vytvořit snippet",
"edit": "Upravit snippet",
"title": "Úryvky",
"new": "Nový úryvek",
"create": "Vytvořit úryvek",
"edit": "Upravit úryvek",
"run": "Spustit",
"empty": "Zatím žádné textové bloky",
"emptyHint": "Vytvořit snippet pro uložení běžně používaných příkazů",
"searchSnippets": "Hledat snippety...",
"empty": "Zatím žádné úryvky",
"emptyHint": "Vytvořit úryvek pro uložení běžně používaných příkazů",
"searchSnippets": "Hledat úryvky...",
"name": "Název",
"description": "L 343, 22.12.2009, s. 1).",
"content": "Příkaz",
@@ -260,29 +260,42 @@
"contentPlaceholder": "Např. sudo systemctl restart nginx",
"nameRequired": "Název je povinný",
"contentRequired": "Příkaz je povinný",
"createDescription": "Vytvořit nový snippet příkazu pro rychlé spuštění",
"editDescription": "Upravit tento snippet příkazu",
"deleteConfirmTitle": "Odstranit snippet",
"createDescription": "Vytvořit nový úryvek příkazu pro rychlé spuštění",
"editDescription": "Upravit tento úryvek příkazu",
"deleteConfirmTitle": "Odstranit úryvek",
"deleteConfirmDescription": "Jste si jisti, že chcete odstranit \"{{name}}\"?",
"createSuccess": "Výstřižek byl úspěšně vytvořen",
"createSuccess": "Úryvek byl úspěšně vytvořen",
"updateSuccess": "Úryvek byl úspěšně aktualizován",
"deleteSuccess": "Textový blok byl úspěšně odstraněn",
"createFailed": "Nepodařilo se vytvořit snippet",
"updateFailed": "Nepodařilo se aktualizovat snippet",
"deleteFailed": "Nepodařilo se odstranit snippet",
"failedToFetch": "Nepodařilo se načíst snippety",
"deleteSuccess": "Úryvek byl úspěšně odstraněn",
"createFailed": "Nepodařilo se vytvořit úryvek",
"updateFailed": "Nepodařilo se aktualizovat úryvek",
"deleteFailed": "Nepodařilo se odstranit úryvek",
"failedToFetch": "Nepodařilo se načíst úryvky",
"executeSuccess": "Probíhá: {{name}}",
"copySuccess": "Zkopírováno \"{{name}}\" do schránky",
"runTooltip": "Spustit tento snippet v terminálu",
"copyTooltip": "Zkopírovat snippet do schránky",
"runTooltip": "Spustit tento úryvek v terminálu",
"copyTooltip": "Zkopírovat úryvek do schránky",
"editTooltip": "Upravit tento úryvek",
"deleteTooltip": "Odstranit tento úryvek",
"shareTooltip": "Sdílet tento úryvek",
"sharedWithYou": "Sdíleno",
"sharedBy": "Sdíleno {{username}}",
"shareSnippet": "Sdílet snippet",
"user": "Uživatel",
"role": "Role",
"selectTarget": "Vybrat...",
"currentAccess": "Aktuální přístup",
"shareSuccess": "Úryvek byl úspěšně sdílen",
"shareFailed": "Sdílení snippetu se nezdařilo",
"revokeSuccess": "Přístup zrušen",
"revokeFailed": "Nepodařilo se zrušit přístup",
"failedToLoadShareData": "Nepodařilo se načíst sdílená data",
"newFolder": "Nová složka",
"reorderSameFolder": "Lze změnit pořadí snippetů ve stejné složce",
"reorderSuccess": "Úryvky úspěšně seřazeny",
"reorderFailed": "Nepodařilo se změnit pořadí snippetů",
"deleteFolderConfirm": "Odstranit složku \"{{name}}\"? Všechny textové bloky budou přesunuty do nekategorizované.",
"deleteFolderSuccess": "Složka byla úspěšně smazána",
"reorderSameFolder": "Lze změnit pořadí úryvků pouze ve stejné složce",
"reorderSuccess": "Pořadí úryvků bylo úspěšně změněno",
"reorderFailed": "Nepodařilo se změnit pořadí úryvků",
"deleteFolderConfirm": "Odstranit složku \"{{name}}\"? Všechny úryvky budou přesunuty do Nezařazené.",
"deleteFolderSuccess": "Složka byla úspěšně odstraněna",
"deleteFolderFailed": "Nepodařilo se odstranit složku",
"updateFolderSuccess": "Složka byla úspěšně aktualizována",
"createFolderSuccess": "Složka byla úspěšně vytvořena",
@@ -293,7 +306,7 @@
"executeOnCurrent": "Spustit na aktuálním terminálu (klepnutím vyberte více možností)",
"folder": "Složka",
"selectFolder": "Vyberte složku nebo ponechte prázdné",
"noFolder": "Žádná složka (nekategorizovaná)",
"noFolder": "Žádná složka (Nezařazené)",
"folderName": "Název složky",
"folderNameRequired": "Název složky je povinný",
"folderColor": "Barva složky",
@@ -302,10 +315,10 @@
"updateFolder": "Aktualizovat složku",
"createFolder": "Vytvořit složku",
"editFolder": "Upravit složku",
"editFolderDescription": "Přizpůsobit složku snippetu",
"editFolderDescription": "Přizpůsobit složku úryvku",
"createFolderDescription": "Uspořádat úryvky do složek",
"confirmExecution": "Spustit \"{{name}}\"?",
"confirmExecutionDesc": "Jste si jisti, že chcete spustit tento snippet?"
"confirmExecutionDesc": "Opravdu chcete spustit tento úryvek?"
},
"commandHistory": {
"title": "Historie",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Je vyžadováno ověření. Obnovte stránku.",
"dataAccessLockedReauth": "Přístup k údajům je uzamčen. Prosím znovu se autentizujte.",
"loading": "Načítání historie příkazů...",
"error": "Chyba při načítání historie"
"error": "Chyba při načítání historie",
"disabledTitle": "Historie příkazů je zakázána",
"disabledDescription": "Povolit sledování historie příkazů v profilu v nastavení vzhledu."
},
"splitScreen": {
"title": "Rozdělit obrazovku",
@@ -510,6 +525,8 @@
"checkingDatabase": "Kontrola připojení k databázi...",
"checkingAuthentication": "Kontrola ověřování...",
"backendReconnected": "Připojení k serveru obnoveno",
"connectionDegraded": "Připojení k serveru bylo ztraceno, obnovuje se…",
"reload": "Reload",
"actions": "Akce",
"remove": "Odebrat",
"revoke": "Revoke",
@@ -528,7 +545,7 @@
"admin": "Admin",
"userProfile": "Profil uživatele",
"tools": "Nástroje",
"snippets": "Výstřižky bloků",
"snippets": "Úryvky",
"newTab": "New Tab",
"splitScreen": "Rozdělit obrazovku",
"closeTab": "Zavřít kartu",
@@ -541,7 +558,8 @@
"copySudoPassword": "Kopírovat heslo Sudo",
"passwordCopied": "Heslo zkopírováno do schránky",
"sudoPasswordCopied": "Sudo heslo zkopírováno do schránky",
"noPasswordAvailable": "Žádné heslo není k dispozici"
"noPasswordAvailable": "Žádné heslo není k dispozici",
"failedToCopyPassword": "Kopírování hesla se nezdařilo"
},
"admin": {
"title": "Nastavení správce",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Globální nastavení monitorování uloženo",
"failedToSaveGlobalSettings": "Nepodařilo se uložit globální nastavení monitorování",
"failedToLoadGlobalSettings": "Nepodařilo se načíst globální nastavení monitorování",
"sessionTimeout": "Časový limit relace",
"sessionTimeoutDesc": "Nakonfigurujte, jak dlouho trvají uživatelské relace před vyžadováním opětovného ověření. Relace 'Zapamatovat si mě' nejsou ovlivněny (vždy 30 dní).",
"sessionTimeoutHours": "Doba trvání vypršení časového limitu",
"hours": "hodiny",
"sessionTimeoutNote": "Platný rozsah: 1720 hodin. Změny se vztahují pouze na nové relace.",
"sessionTimeoutSaved": "Časový limit relace uložen",
"failedToSaveSessionTimeout": "Nepodařilo se uložit časový limit relace",
"guacamoleIntegration": "Integrace vzdálené plochy (Guacamole)",
"guacamoleIntegrationDesc": "Povolit připojení RDP, VNC a Telnet pomocí guacdu. Vyžaduje spuštění instance guacd.",
"enableGuacamole": "Povolit podporu RDP/VNC/Telnet",
"guacdUrl": "guacd URL (host:port)",
"guacdUrlPlaceholder": "guakd:4822",
"guacdUrlNote": "Změny hostitele/portu guacd vyžadují restartování serveru.",
"guacamoleSettingsSaved": "Nastavení Guacamole uloženo",
"failedToSaveGuacamoleSettings": "Nepodařilo se uložit nastavení guacamole",
"failedToLoadGuacamoleSettings": "Nepodařilo se načíst nastavení guacamole",
"logLevel": "Úroveň logu",
"logLevelDesc": "Ovládání verbosity serverových protokolů. Vyšší úrovně snižují výstup. Lze také nastavit pomocí proměnné prostředí LOG_LEVEL.",
"logVerbosity": "Verbosita",
"logLevelNote": "Změny se projeví okamžitě. Úroveň ladění může ovlivnit výkon.",
"logLevelSaved": "Úroveň záznamu uložena",
"failedToSaveLogLevel": "Nepodařilo se uložit úroveň protokolu",
"clampedToValidRange": "byl upraven na platný rozsah",
"loadingSessions": "Načítání relací...",
"noActiveSessions": "Nebyly nalezeny žádné aktivní relace.",
"device": "Zařízení",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} hostitelé",
"importJson": "Importovat JSON",
"importing": "Importování...",
"exportAllJson": "Exportovat vše",
"exporting": "Exportování...",
"exportedAllHosts": "Exportováno {{count}} hostitelů",
"failedToExportAllHosts": "Export hostitelů se nezdařil. Ujistěte se, že jste přihlášeni a že máte přístup k hostitelským datům.",
"exportAllSensitiveWarning": "Exportovaný soubor bude obsahovat citlivá autentizační data (hesla/SSH klíče) v prostém textu. Uchovávejte soubor v bezpečí a odstraňte jej po použití.",
"importJsonTitle": "Importovat SSH hostitele z JSON",
"importJsonDesc": "Nahrajte soubor JSON pro hromadný import více SSH hostitelů (max. 100).",
"downloadSample": "Stáhnout vzorek",
@@ -866,11 +912,26 @@
"importError": "Chyba importu",
"failedToImportJson": "Nepodařilo se importovat soubor JSON",
"connectionDetails": "Detaily připojení",
"connectionType": "Typ připojení",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Vzdálená plocha",
"guacamoleSettings": "Nastavení vzdálené plochy",
"organization": "Organizace",
"ipAddress": "IP adresa nebo název hostitele",
"macAddress": "MAC adresa",
"macAddressDesc": "Pro wake-on-LAN. Formát: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "paket probuzení na síti LAN odeslán",
"wolFailed": "Nepodařilo se odeslat paket buzení on-LAN",
"ipRequired": "Je vyžadována IP adresa",
"portRequired": "Port je povinný",
"port": "Port",
"name": "Název",
"username": "Uživatelské jméno",
"usernameRequired": "Uživatelské jméno je vyžadováno (pouze (SSH/Telnet)",
"folder": "Složka",
"tags": "Štítky",
"pin": "Připnout",
@@ -979,6 +1040,8 @@
"tunnel": "Tunel",
"fileManager": "Správce souborů",
"serverStats": "Statistiky serveru",
"status": "Stav",
"statistics": "Statistiky",
"hostViewer": "Prohlížeč hostitelů",
"enableServerStats": "Povolit statistiky serveru",
"enableServerStatsDesc": "Povolit/zakázat sběr statistik serveru pro tohoto hostitele",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Přetažením přesunout mezi složkami",
"exportedHostConfig": "Exportováno nastavení hostitele pro {{name}}",
"openTerminal": "Otevřít terminál",
"openRdp": "Otevřít RDP",
"openVnc": "Otevřít VNC",
"openTelnet": "Otevřít telefonní síť",
"openFileManager": "Otevřít správce souborů",
"openTunnels": "Otevřené tunely",
"openServerDetails": "Detaily otevřeného serveru",
"statistics": "Statistiky",
"enabledWidgets": "Povolené widgety",
"openServerStats": "Otevřené statistiky serveru",
"enabledWidgetsDesc": "Vyberte, které widgety statistiky se zobrazí pro tohoto hostitele",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Zkontrolujte, zda je server online nebo offline",
"statusCheckInterval": "Interval kontroly stavu",
"statusCheckIntervalDesc": "Jak často kontrolovat, zda je hostitel online (5s - 1h)",
"statusChecks": "Kontrola stavu",
"disableTcpPing": "Zakázat TCP Ping",
"disableTcpPingDescription": "Vypnout kontrolu stavu (online/offline detekce) pro tohoto hostitele",
"useGlobalStatusInterval": "Použít globální výchozí nastavení",
"useGlobalMetricsInterval": "Použít globální výchozí nastavení",
"usingGlobalDefault": "Použití globálního výchozího ({{value}}s)",
"metricsCollection": "Metrika kolekce",
"metricsEnabled": "Povolit sledování metriky",
"metricsEnabledDesc": "Sbírat CPU, RAM, disky a další systémové statistiky",
"metricsInterval": "Interval sběru metrik",
"metricsIntervalDesc": "Jak často shromažďovat statistiky serveru (5s - 1h)",
"metricsNotAvailableForConnectionType": "Měřiče serveru jsou dostupné pouze pro SSH hostitele. Kontrola stavu TCP může být povolena výše.",
"intervalSeconds": "sekundy",
"intervalMinutes": "minuty",
"intervalValidation": "Monitorovací intervaly musí být mezi 5 sekundami a 1 hodinou (3600 sekund)",
@@ -1112,9 +1182,9 @@
"backspaceModeNormal": "Normální (DEL)",
"backspaceModeControlH": "Ovládání H (^H)",
"backspaceModeDesc": "Chování tlačítka Backspace pro kompatibilitu",
"startupSnippet": "Spustit snippet",
"startupSnippet": "Spustit úryvek",
"selectSnippet": "Vybrat úryvek",
"searchSnippets": "Hledat snippety...",
"searchSnippets": "Hledat úryvky...",
"snippetNone": "Nic",
"noneAuthTitle": "Klávesnice interaktivní ověření",
"noneAuthDescription": "Tato metoda ověřování bude při připojování k serveru SSH používat klávesnice-interaktivní ověřování.",
@@ -1133,6 +1203,10 @@
"noServerFound": "Nenalezen žádný server",
"jumpHostsOrder": "Spojení bude provedeno v pořadí: Skok hostitel 1 → Skok 2 → ... → Cílový server",
"socks5Proxy": "SOCKS5 proxy",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Poslat sled pokusů o připojení do určených portů před připojením. Používá se pro dynamické otevření pravidel firewallu.",
"addKnock": "Přidat port",
"delayMs": "Zpoždění",
"socks5Description": "Konfigurace SOCKS5 proxy pro SSH připojení. Veškerý provoz bude směrován přes zadaný proxy server.",
"enableSocks5": "Povolit SOCKS5 Proxy",
"enableSocks5Description": "Použít SOCKS5 proxy pro toto SSH připojení",
@@ -1174,11 +1248,11 @@
"proxyNode": "Proxy uzel",
"proxyType": "Typ proxy",
"quickActions": "Rychlé akce",
"quickActionsDescription": "Rychlé akce vám umožní vytvořit vlastní tlačítka, která na tomto serveru spustí snippety SSH. Tato tlačítka se zobrazí v horní části statistiky serveru pro rychlý přístup.",
"quickActionsDescription": "Rychlé akce vám umožní vytvořit vlastní tlačítka, která na tomto serveru spustí úryvky SSH. Tato tlačítka se zobrazí v horní části statistiky serveru pro rychlý přístup.",
"quickActionsList": "Seznam rychlých akcí",
"addQuickAction": "Přidat rychlou akci",
"quickActionName": "Název akce",
"noSnippetFound": "Úryvek nenalezen",
"noSnippetFound": "Nebyl nalezen žádný úryvek",
"quickActionsOrder": "Tlačítka pro rychlé akce se zobrazí v pořadí uvedeném výše na stránce Statistiky serveru",
"sidebarCustomization": "Přizpůsobení postranního panelu",
"sidebarCustomizationDesc": "Vyberte, které akce se zobrazí jako rychlá tlačítka v postranním panelu. Akce nezobrazené jako tlačítka se zobrazí v rozbalovacím menu.",
@@ -1212,11 +1286,13 @@
"proxyTestSuccess": "Proxy připojení úspěšné ({{latency}}ms)",
"proxyTestFailed": "Proxy test selhal: {{error}}",
"connectionPath": "Cesta k připojení",
"executeSnippetOnConnect": "Spustit snippet, když se terminál připojí",
"executeSnippetOnConnect": "Spustit úryvek, když se terminál připojí",
"autoMosh": "Automatické MOSH",
"autoMoshDesc": "Automaticky spustit MOSH příkaz při připojení",
"moshCommand": "Příkaz MOSH",
"moshCommandDesc": "Příkaz MOSH k provedení",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automaticky připojit existující (nebo spustit novou) tmux relaci pro trvalou relaci a spojitost přes zařízení",
"environmentVariables": "Proměnné prostředí",
"environmentVariablesDesc": "Nastavit vlastní proměnné prostředí pro relaci terminálu",
"variableName": "Název proměnné",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Kopírovat URL tunelu",
"copyServerStatsUrl": "Kopírovat URL statistiky serveru",
"copyDockerUrl": "Kopírovat URL Dockeru",
"copyRemoteDesktopUrl": "Kopírovat URL vzdálené plochy",
"fullScreenUrlTooltip": "Kopírovat URL pro otevření této aplikace v režimu celé obrazovky",
"notEnabled": "Docker není pro tohoto hostitele povolen. Povolte jej v nastavení hostitele pro použití funkcí Dockeru.",
"validating": "Ověřování Dockeru...",
@@ -1348,7 +1425,96 @@
"selectAll": "Vybrat vše",
"deselectAll": "Zrušit výběr všech",
"useGlobalStatusDefault": "Použít globální výchozí (stav)",
"useGlobalMetricsDefault": "Použít globální výchozí (Metrics)"
"useGlobalMetricsDefault": "Použít globální výchozí (Metrics)",
"remoteDesktopSettings": "Nastavení vzdálené plochy",
"domain": "Doména",
"securityMode": "Bezpečnostní režim",
"ignoreCert": "Ignorovat certifikát",
"ignoreCertDesc": "Přeskočit ověření TLS certifikátu pro toto připojení",
"displaySettings": "Nastavení zobrazení",
"colorDepth": "Hloubka barev",
"width": "Width",
"height": "Výška",
"dpi": "DPI",
"resizeMethod": "Metoda změny velikosti",
"forceLossless": "Vynutit ztrátu",
"audioSettings": "Nastavení zvuku",
"disableAudio": "Zakázat zvuk",
"enableAudioInput": "Povolit vstup zvuku",
"rdpPerformance": "Výkon",
"enableWallpaper": "Povolit tapetu",
"enableTheming": "Povolit motivy",
"enableFontSmoothing": "Povolit vyhlazování písma",
"enableFullWindowDrag": "Povolit přetažení přes celé okno",
"enableDesktopComposition": "Povolit složení plochy",
"enableMenuAnimations": "Povolit animace menu",
"disableBitmapCaching": "Vypnout mezipaměť bitmap",
"disableOffscreenCaching": "Zakázat ukládání do mezipaměti mimo obrazovku",
"disableGlyphCaching": "Zakázat ukládání do mezipaměti Glyph",
"enableGfx": "Enable GFX",
"deviceRedirection": "Přesměrování zařízení",
"enablePrinting": "Povolit tisk",
"enableDrive": "Povolit přesměrování disku",
"driveName": "Název disku",
"drivePath": "Cesta k disku",
"createDrivePath": "Vytvořit cestu k disku",
"disableDownload": "Zakázat stahování",
"disableUpload": "Zakázat nahrávání",
"enableTouch": "Povolit dotyk",
"rdpSession": "Relace",
"clientName": "Název klienta",
"consoleSession": "Relace konzole",
"initialProgram": "Počáteční program",
"serverLayout": "Rozložení klávesnice serveru",
"timezone": "Timezone",
"gatewaySettings": "Brána",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Port brány",
"gatewayUsername": "Uživatelské jméno brány",
"gatewayPassword": "Heslo brány",
"gatewayDomain": "Doména brány",
"remoteApp": "Vzdálená aplikace",
"remoteAppProgram": "Vzdálená aplikace",
"remoteAppDir": "Adresář vzdálených aplikací",
"remoteAppArgs": "Argumenty vzdálené aplikace",
"clipboardSettings": "Schránka",
"normalizeClipboard": "Normalizovat schránku",
"disableCopy": "Zakázat kopírování",
"disablePaste": "Zakázat vložení",
"vncSettings": "Nastavení VNC",
"cursorMode": "Režim kurzoru",
"swapRedBlue": "Prohodit červenou/modrou",
"readOnly": "Pouze pro čtení",
"recordingSettings": "Nahrávání",
"recordingPath": "Cesta k nahrávání",
"recordingName": "Název záznamu",
"createRecordingPath": "Vytvořit cestu k nahrávání",
"excludeOutput": "Vynechat výstup",
"excludeMouse": "Vyloučit myš",
"includeKeys": "Zahrnout klíče",
"sendWolPacket": "Poslat WoL paket",
"wolMacAddr": "MAC adresa",
"wolBroadcastAddr": "Vysílací adresa",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Čas čekání (v sekundách)",
"connectionSettings": "Nastavení připojení",
"rdpOnly": "Pouze RDP",
"vncOnly": "Pouze VNC",
"telnetTerminalSettings": "Nastavení terminálu",
"terminalType": "Typ terminálu",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Barevné schéma",
"guacBackspaceKey": "Klíč Backspace"
},
"guacamole": {
"connecting": "Připojování k {{type}} relaci...",
"rdpConnecting": "Připojování k RDP serveru...",
"vncConnecting": "Připojování k VNC serveru...",
"telnetConnecting": "Připojování k serveru Telnet...",
"connectionError": "Chyba připojení",
"connectionFailed": "Připojení se nezdařilo",
"failedToConnect": "Nepodařilo se získat token pro připojení"
},
"terminal": {
"title": "Terminál",
@@ -1364,7 +1530,7 @@
"closePanel": "Zavřít panel",
"reconnect": "Znovu připojit",
"sessionEnded": "Relace skončila",
"connectionLost": "Připojení ztraceno",
"connectionLost": "Spojení ztraceno",
"error": "CHYBA: {{message}}",
"disconnected": "Odpojeno",
"connectionClosed": "Připojení bylo uzavřeno",
@@ -1372,6 +1538,7 @@
"connected": "Připojeno",
"clipboardWriteFailed": "Kopírování do schránky se nezdařilo. Ujistěte se, že stránka je vedena přes HTTPS nebo localhost.",
"clipboardReadFailed": "Nepodařilo se přečíst ze schránky. Ujistěte se, že máte oprávnění na schránku.",
"clipboardHttpWarning": "Vložit vyžaduje HTTPS. Použijte Ctrl+Shift+V nebo použijte Termix přes HTTPS.",
"sshConnected": "SSH připojení navázáno",
"authError": "Ověření se nezdařilo: {{message}}",
"unknownError": "Došlo k neznámé chybě",
@@ -1380,7 +1547,25 @@
"connecting": "Připojování...",
"reconnecting": "Znovu se připojuji... ({{attempt}}/{{max}})",
"reconnected": "Znovu připojeno",
"tmuxSessionCreated": "Tmux relace vytvořena: {{name}}",
"tmuxSessionAttached": "připojena relace tmuxu: {{name}}",
"tmuxUnavailable": "tmux není na vzdáleném hostiteli nainstalován, vrátí se zpět na standardní shell",
"tmuxSessionPickerTitle": "tmux relace",
"tmuxSessionPickerDesc": "Existující tmux relace nalezeny na tomto hostiteli. Vyberte jednu pro připojení nebo vytvoření nové relace.",
"tmuxWindows": "Okna",
"tmuxWindowCount": "Okno {{count}}",
"tmuxWindowCount_other": "Okna {{count}}",
"tmuxAttached": "Připojení klienti",
"tmuxAttachedCount": "{{count}} připojeno",
"tmuxLastActivity": "Poslední aktivita",
"tmuxTimeJustNow": "právě teď",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Začít novou relaci",
"tmuxCopyHint": "Upravte výběr a stiskněte klávesu Enter pro kopírování do schránky",
"maxReconnectAttemptsReached": "Dosažen maximální počet pokusů o opětovné připojení",
"closeTab": "Zavřít",
"connectionTimeout": "Časový limit připojení",
"terminalTitle": "Terminál - {{host}}",
"terminalWithPath": "Terminál - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Vypršel časový limit ověření. Zkuste to prosím znovu.",
"opksshAuthFailed": "Ověření se nezdařilo. Zkontrolujte prosím vaše přihlašovací údaje a zkuste to znovu.",
"opksshConfigMissing": "Nastavení OPKSSH nebylo nalezeno. Vytvořte prosím ~/.opk/config.yml s nastavením OIDC poskytovatele. Viz dokumentace: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Přihlásit se pomocí {{provider}}",
"sudoPasswordPopupTitle": "Vložit heslo?",
"sudoPasswordPopupHint": "Stiskněte Enter pro vložení, Esc pro zamítnutí",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Připojení odmítnuto serverem. Zkontrolujte prosím ověření a nastavení sítě.",
"hostKeyRejected": "Ověření SSH hostitele bylo zamítnuto. Připojení bylo zrušeno.",
"sessionTakenOver": "Relace byla otevřena v jiné kartě. Znovu připojuji...",
"sessionAttachTimeout": "Vypršel časový limit přílohy relace. Vytváření nového připojení..."
"sessionAttachTimeout": "Vypršel časový limit přílohy relace. Vytváření nového připojení...",
"openFileManagerHere": "Otevřít správce souborů"
},
"fileManager": {
"title": "Správce souborů",
@@ -2126,8 +2313,10 @@
"fileColorCodingDesc": "Barevné soubory podle typu: složky (červené), soubory (modrá), symbolické odkazy (zelené)",
"commandAutocomplete": "Automatické dokončování příkazu",
"commandAutocompleteDesc": "Povolit návrhy automatického dokončování tlačítek záložek pro terminálové příkazy na základě vaší historie příkazů",
"defaultSnippetFoldersCollapsed": "Sbalit složky snippet ve výchozím nastavení",
"defaultSnippetFoldersCollapsedDesc": "Pokud je povoleno, všechny složky snippetů se sbalí, když otevřete záložku snippetů",
"commandHistoryTracking": "Uložit historii příkazů",
"commandHistoryTrackingDesc": "Ukládat terminálové příkazy v historii pro automatické dokončování a postranní panel. Ve výchozím nastavení je zakázáno pro soukromí.",
"defaultSnippetFoldersCollapsed": "Sbalit složky úryvků ve výchozím nastavení",
"defaultSnippetFoldersCollapsedDesc": "Když je povoleno, všechny složky úryvků se sbalí, když otevřete záložku úryvků",
"terminalSyntaxHighlighting": "Zvýraznění syntaxe terminálu",
"showHostTags": "Zobrazit štítky hostitele",
"showHostTagsDesc": "Zobrazit štítky pod každým hostitelem v postranním panelu. Zakažte pro skrytí všech štítků.",
@@ -2137,9 +2326,9 @@
"fileManagerSettings": "Správce souborů",
"terminalSettings": "Terminál",
"hostSidebarSettings": "Hostitel & postranní panel",
"snippetsSettings": "Výstřižky bloků",
"confirmSnippetExecution": "Potvrdit spuštění snippetu",
"confirmSnippetExecutionDesc": "Zobrazit potvrzovací dialog před spuštěním snippetů",
"snippetsSettings": "Úryvky",
"confirmSnippetExecution": "Potvrdit spuštění úryvku",
"confirmSnippetExecutionDesc": "Zobrazit potvrzovací dialogové okno před spuštěním úryvků",
"updateSettings": "Aktualizace",
"disableUpdateCheck": "Zakázat kontrolu aktualizací",
"disableUpdateCheckDesc": "Zastavit kontrolu nových verzí na startu a řídicím panelu. Sníží síťové požadavky.",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Automaticky zvýraznit příkazy, cesty, IP a logovací úrovně v terminálu",
"enableCommandPaletteShortcut": "Povolit zástupce pro Palety",
"enableCommandPaletteShortcutDesc": "Dvojitým poklepáním vlevo otevřít Příkazovou paletu pro rychlý přístup k hostům",
"enableTerminalSessionPersistence": "Trvalé relace terminálů",
"enableTerminalSessionPersistence": "Trvalé záložky/relace",
"enableTerminalSessionPersistenceDesc": "Udržovat SSH připojení při přepínání karet nebo zavření prohlížeče (může být nestabilní)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Databáze",
"healthy": "Zdravé",
"error": "Chyba",
"totalServers": "Celkem serverů",
"totalHosts": "Celkem hostitelů",
"totalTunnels": "Celkem tunelů",
"totalCredentials": "Pověření celkem",
"recentActivity": "Nedávná aktivita",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Kopier snippet til udklipsholder",
"editTooltip": "Rediger denne snippet",
"deleteTooltip": "Slet denne snippet",
"shareTooltip": "Del denne snippet",
"sharedWithYou": "Delt",
"sharedBy": "Delt af {{username}}",
"shareSnippet": "Del Snippet",
"user": "Bruger",
"role": "Rolle",
"selectTarget": "Vælg...",
"currentAccess": "Nuværende Adgang",
"shareSuccess": "Snippet delt med succes",
"shareFailed": "Kunne ikke dele snippet",
"revokeSuccess": "Adgang tilbagekaldt",
"revokeFailed": "Mislykkedes at tilbagekalde adgang",
"failedToLoadShareData": "Kunne ikke indlæse delingsdata",
"newFolder": "Ny Mappe",
"reorderSameFolder": "Kan kun omarrangere uddrag inden for samme mappe",
"reorderSuccess": "Snippets genbestilt",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Godkendelse kræves. Opdater venligst siden.",
"dataAccessLockedReauth": "Data adgang låst. Venligst gengodkende.",
"loading": "Indlæser kommandohistorik...",
"error": "Fejl Ved Indlæsning Af Historik"
"error": "Fejl Ved Indlæsning Af Historik",
"disabledTitle": "Kommandohistorik er deaktiveret",
"disabledDescription": "Aktiver Command History Tracking i din profil under Udseende indstillinger."
},
"splitScreen": {
"title": "Opdel Skærm",
@@ -510,6 +525,8 @@
"checkingDatabase": "Kontrollerer databaseforbindelse...",
"checkingAuthentication": "Kontrollerer godkendelse...",
"backendReconnected": "Server forbindelse gendannet",
"connectionDegraded": "Serverforbindelse tabt, genoprette…",
"reload": "Reload",
"actions": "Handlinger",
"remove": "Fjern",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Kopier Sudo Adgangskode",
"passwordCopied": "Adgangskode kopieret til udklipsholder",
"sudoPasswordCopied": "Sudo adgangskode kopieret til udklipsholder",
"noPasswordAvailable": "Ingen adgangskode tilgængelig"
"noPasswordAvailable": "Ingen adgangskode tilgængelig",
"failedToCopyPassword": "Kunne ikke kopiere adgangskode"
},
"admin": {
"title": "Admin Indstillinger",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Globale overvågningsindstillinger gemt",
"failedToSaveGlobalSettings": "Kunne ikke gemme globale overvågningsindstillinger",
"failedToLoadGlobalSettings": "Kunne ikke indlæse globale overvågningsindstillinger",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Konfigurer hvor lange brugersessioner varer før der kræves gengodkendelse. 'Husk mig' sessioner er upåvirkede (altid 30 dage).",
"sessionTimeoutHours": "Timeout Varighed",
"hours": "timer",
"sessionTimeoutNote": "Gyldigt område: 1-720 timer. Ændringer gælder kun for nye sessioner.",
"sessionTimeoutSaved": "Session timeout gemt",
"failedToSaveSessionTimeout": "Kunne ikke gemme sessions timeout",
"guacamoleIntegration": "Remote Desktop Integration (Guacamol)",
"guacamoleIntegrationDesc": "Aktivér RDP, VNC og Telnet forbindelser via guacd. Kræver en guacd instans for at køre.",
"enableGuacamole": "Aktiver RDP/VNC/Telnet support",
"guacdUrl": "guacd URL (vært: port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Ændringer af guacd værten / port kræver en server genstart til at træde i kraft.",
"guacamoleSettingsSaved": "Guacamole indstillinger gemt",
"failedToSaveGuacamoleSettings": "Kunne ikke gemme guacamole indstillinger",
"failedToLoadGuacamoleSettings": "Kunne ikke indlæse guacamole indstillinger",
"logLevel": "Log Niveau",
"logLevelDesc": "Styr verbositeten af serverlogs. Højere niveauer reducerer log output. Kan også indstilles via LOG_LEVEL miljøvariablen.",
"logVerbosity": "Verbosity",
"logLevelNote": "Ændringer træder i kraft straks. Fejlfindingsniveau kan påvirke ydeevnen.",
"logLevelSaved": "Logniveau gemt",
"failedToSaveLogLevel": "Kunne ikke gemme logniveau",
"clampedToValidRange": "blev justeret til gyldigt interval",
"loadingSessions": "Indlæser sessioner...",
"noActiveSessions": "Ingen aktive sessioner fundet.",
"device": "Enhed",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} værter",
"importJson": "Importér JSON",
"importing": "Importerer...",
"exportAllJson": "Eksporter Alle",
"exporting": "Eksporterer...",
"exportedAllHosts": "Eksporterede {{count}} værter",
"failedToExportAllHosts": "Kunne ikke eksportere værter. Kontroller, at du er logget ind og har adgang til værtsdata.",
"exportAllSensitiveWarning": "Den eksporterede fil vil indeholde følsomme godkendelsesdata (adgangskoder/SSH-nøgler) i klar. Behold venligst filen sikker og slet den efter brug.",
"importJsonTitle": "Importér SSH-værter fra JSON",
"importJsonDesc": "Upload en JSON-fil til bulk import af flere SSH værter (max 100).",
"downloadSample": "Hent Eksempel",
@@ -866,11 +912,26 @@
"importError": "Import fejl",
"failedToImportJson": "Kunne ikke importere JSON- fil",
"connectionDetails": "Forbindelse Detaljer",
"connectionType": "Forbindelsestype",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Fjernskrivebord",
"guacamoleSettings": "Fjernskrivebordsindstillinger",
"organization": "Organisation",
"ipAddress": "Ip adresse eller værtsnavn",
"macAddress": "MAC- Adresse",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Wake-on-LAN-pakke sendt",
"wolFailed": "Kunne ikke sende Wake-on-LAN-pakke",
"ipRequired": "IP adresse er påkrævet",
"portRequired": "Port er påkrævet",
"port": "Port",
"name": "Navn",
"username": "Brugernavn",
"usernameRequired": "Brugernavn er kun påkrævet (SSH/Telnet)",
"folder": "Mappe",
"tags": "Mærker",
"pin": "Fastgør",
@@ -979,6 +1040,8 @@
"tunnel": "Tunnel",
"fileManager": "Filhåndtering",
"serverStats": "Server Statistik",
"status": "Status",
"statistics": "Statistik",
"hostViewer": "Vært Fremviser",
"enableServerStats": "Aktiver Serverstatistik",
"enableServerStatsDesc": "Aktiver/deaktiver server statistik samling for denne vært",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Træk for at flytte mellem mapper",
"exportedHostConfig": "Eksporteret værtskonfiguration for {{name}}",
"openTerminal": "Åben Terminal",
"openRdp": "Åbn RDP",
"openVnc": "Åbn VNC",
"openTelnet": "Åbn Telnet",
"openFileManager": "Åbn Filhåndtering",
"openTunnels": "Åbne Tunneler",
"openServerDetails": "Åbn Serverdetaljer",
"statistics": "Statistik",
"enabledWidgets": "Aktiverede Widgets",
"openServerStats": "Åbn Serverstatistik",
"enabledWidgetsDesc": "Vælg hvilke statistikwidgets der skal vises for denne vært",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Tjek om serveren er online eller offline",
"statusCheckInterval": "Status Tjek Interval",
"statusCheckIntervalDesc": "Hvor ofte til at kontrollere, om værten er online (5s - 1h)",
"statusChecks": "Status Kontrol",
"disableTcpPing": "Deaktivér TCP Ping",
"disableTcpPingDescription": "Slå statuskontroller fra (online/offline afsløring) for denne vært",
"useGlobalStatusInterval": "Brug global standard",
"useGlobalMetricsInterval": "Brug global standard",
"usingGlobalDefault": "Bruger global standard ({{value}}s)",
"metricsCollection": "Metrik Samling",
"metricsEnabled": "Aktiver Måleovervågning",
"metricsEnabledDesc": "Indsaml CPU, RAM, disk, og andre systemstatistikker",
"metricsInterval": "Interval For Metrikelsamling",
"metricsIntervalDesc": "Hvor ofte at indsamle server statistik (5s - 1h)",
"metricsNotAvailableForConnectionType": "Servermålinger er kun tilgængelige for SSH-værter. TCP-ping statuskontroller kan stadig aktiveres ovenfor.",
"intervalSeconds": "sekunder",
"intervalMinutes": "minutter",
"intervalValidation": "Overvågningsintervallerne skal være mellem 5 sekunder og 1 time (3600 sekunder)",
@@ -1133,6 +1203,10 @@
"noServerFound": "Ingen server fundet",
"jumpHostsOrder": "Forbindelser vil blive foretaget i rækkefølge: Hop Host 1 → Host 2 → ... → Målserver",
"socks5Proxy": "SOCKS5 Proxy",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Send en sekvens af forbindelsesforsøg til angivne porte før tilslutning. Bruges til at åbne firewall regler dynamisk.",
"addKnock": "Tilføj Port",
"delayMs": "Forsinkelse",
"socks5Description": "Indstil SOCKS5 proxy for SSH forbindelse. Al trafik vil blive dirigeret gennem den angivne proxy server.",
"enableSocks5": "Aktiver SOCKS5 Proxy",
"enableSocks5Description": "Brug SOCKS5 proxy til denne SSH forbindelse",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Kør automatisk MOSH kommando ved tilslutning",
"moshCommand": "MOSH Kommando",
"moshCommandDesc": "MOSH - kommandoen der skal udføres",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Vedhæft automatisk til en eksisterende (eller start en ny) tmux-session for vedvarende sessioner og kontinuitet på tværs af enheder",
"environmentVariables": "Miljø Variabler",
"environmentVariablesDesc": "Sæt brugerdefinerede miljøvariabler for terminalsessionen",
"variableName": "Variabel navn",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Kopier Tunnel URL",
"copyServerStatsUrl": "Kopier Server Statistik URL",
"copyDockerUrl": "Kopier Docker-URL",
"copyRemoteDesktopUrl": "Kopiér Fjernskrivebords URL",
"fullScreenUrlTooltip": "Kopier URL for at åbne denne app i fuld skærm",
"notEnabled": "Docker er ikke aktiveret for denne vært. Aktivér den i værtsindstillinger for at bruge Docker-funktioner.",
"validating": "Validerer Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Vælg alle",
"deselectAll": "Fravælg alle",
"useGlobalStatusDefault": "Brug Global Standard (Status)",
"useGlobalMetricsDefault": "Brug Global Standard (Metrics)"
"useGlobalMetricsDefault": "Brug Global Standard (Metrics)",
"remoteDesktopSettings": "Fjernskrivebordsindstillinger",
"domain": "Domæne",
"securityMode": "Sikkerheds Tilstand",
"ignoreCert": "Ignorer Certifikat",
"ignoreCertDesc": "Spring TLS-certifikatverifikation over for denne forbindelse",
"displaySettings": "Vis Indstillinger",
"colorDepth": "Farve Dybde",
"width": "Width",
"height": "Højde",
"dpi": "DPI",
"resizeMethod": "Ændr Størrelsesmetode",
"forceLossless": "Gennemtving Tabsfri",
"audioSettings": "Indstillinger For Lyd",
"disableAudio": "Deaktivér Lyd",
"enableAudioInput": "Aktiver Lyd Input",
"rdpPerformance": "Ydeevne",
"enableWallpaper": "Aktivér Baggrund",
"enableTheming": "Aktiver Temaer",
"enableFontSmoothing": "Aktiver Skrifttype Udjævning",
"enableFullWindowDrag": "Aktivér Fuldt Vinduestræk",
"enableDesktopComposition": "Aktiver Skrivebord Sammensætning",
"enableMenuAnimations": "Aktiver Menu Animationer",
"disableBitmapCaching": "Deaktivér Bitmap- Caching",
"disableOffscreenCaching": "Deaktivér Offscreen Caching",
"disableGlyphCaching": "Deaktivér Symbol Caching",
"enableGfx": "Enable GFX",
"deviceRedirection": "Enhed Omdirigering",
"enablePrinting": "Aktiver Udskrivning",
"enableDrive": "Aktiver Drev Omdirigering",
"driveName": "Drev Navn",
"drivePath": "Kør Sti",
"createDrivePath": "Opret Kørselssti",
"disableDownload": "Deaktivér Download",
"disableUpload": "Deaktivér Upload",
"enableTouch": "Aktivér Touch",
"rdpSession": "Session",
"clientName": "Klient Navn",
"consoleSession": "Konsol Session",
"initialProgram": "Oprindeligt Program",
"serverLayout": "Server Tastaturlayout",
"timezone": "Timezone",
"gatewaySettings": "Port",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Port Til Port",
"gatewayUsername": "Gateway Brugernavn",
"gatewayPassword": "Gateway Adgangskode",
"gatewayDomain": "Gateway Domæne",
"remoteApp": "RemoteApp",
"remoteAppProgram": "Fjernt Program",
"remoteAppDir": "Ekstern App Mappe",
"remoteAppArgs": "Ekstern App-Argumenter",
"clipboardSettings": "Udklipsholder",
"normalizeClipboard": "Normalisér Udklipsholder",
"disableCopy": "Deaktivér Kopiering",
"disablePaste": "Deaktivér Indsæt",
"vncSettings": "Vnc Indstillinger",
"cursorMode": "Markør Tilstand",
"swapRedBlue": "Byt Rød/Blå",
"readOnly": "Kun Læs",
"recordingSettings": "Optager",
"recordingPath": "Optagelsessti",
"recordingName": "Optagelses Navn",
"createRecordingPath": "Opret Optagelsessti",
"excludeOutput": "Udeluk Output",
"excludeMouse": "Udeluk Mus",
"includeKeys": "Inkludér Nøgler",
"sendWolPacket": "Send WoL Pakke",
"wolMacAddr": "MAC- Adresse",
"wolBroadcastAddr": "Broadcast- Adresse",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Vent Tid (sekunder)",
"connectionSettings": "Forbindelsesindstillinger",
"rdpOnly": "Kun RDP",
"vncOnly": "Kun VNC",
"telnetTerminalSettings": "Terminalindstillinger",
"terminalType": "Terminaltype",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Farvesammensætning",
"guacBackspaceKey": "Backspace Nøgle"
},
"guacamole": {
"connecting": "Forbinder til {{type}} session...",
"rdpConnecting": "Forbinder til RDP- server...",
"vncConnecting": "Forbinder til VNC- server...",
"telnetConnecting": "Forbinder til Telnet server...",
"connectionError": "Forbindelsesfejl",
"connectionFailed": "Forbindelse mislykkedes",
"failedToConnect": "Kunne ikke hente forbindelsestoken"
},
"terminal": {
"title": "Terminal",
@@ -1364,7 +1530,7 @@
"closePanel": "Luk Panel",
"reconnect": "Genopret",
"sessionEnded": "Session Afsluttet",
"connectionLost": "Forbindelse Mistet",
"connectionLost": "Forbindelse tabt",
"error": "FEJL: {{message}}",
"disconnected": "Afbrudt",
"connectionClosed": "Forbindelse lukket",
@@ -1372,6 +1538,7 @@
"connected": "Forbundet",
"clipboardWriteFailed": "Kunne ikke kopiere til udklipsholderen. Sørg for, at siden er serveret over HTTPS eller localhost.",
"clipboardReadFailed": "Kunne ikke læse fra udklipsholderen. Sørg for, at udklipsholdertilladelser er tildelt.",
"clipboardHttpWarning": "Indsæt kræver HTTPS. Brug Ctrl + Shift + V eller servere Termix over HTTPS.",
"sshConnected": "SSH-forbindelse oprettet",
"authError": "Godkendelse mislykkedes: {{message}}",
"unknownError": "Ukendt fejl opstod",
@@ -1380,7 +1547,25 @@
"connecting": "Forbinder...",
"reconnecting": "Genforbindelse... ({{attempt}}/{{max}})",
"reconnected": "Genoprettet succesfuldt",
"tmuxSessionCreated": "tmux session oprettet: {{name}}",
"tmuxSessionAttached": "tmux session vedhæftet: {{name}}",
"tmuxUnavailable": "tmux er ikke installeret på den eksterne vært, falder tilbage til standard shell",
"tmuxSessionPickerTitle": "tmux sessioner",
"tmuxSessionPickerDesc": "Eksisterende tmux sessioner fundet på denne vært. Vælg en for at vedhæfte eller oprette en ny session.",
"tmuxWindows": "Vinduer",
"tmuxWindowCount": "{{count}} vindue",
"tmuxWindowCount_other": "{{count}} vinduer",
"tmuxAttached": "Vedhæftede klienter",
"tmuxAttachedCount": "{{count}} vedhæftet",
"tmuxLastActivity": "Seneste aktivitet",
"tmuxTimeJustNow": "lige nu",
"tmuxTimeMinutes": "{{count}}m siden",
"tmuxTimeHours": "{{count}}h siden",
"tmuxTimeDays": "{{count}}d siden",
"tmuxCreateNew": "Start ny session",
"tmuxCopyHint": "Justér markering og tryk på Enter for at kopiere til udklipsholder",
"maxReconnectAttemptsReached": "Maksimal genforbindelsesforsøg nået",
"closeTab": "Luk",
"connectionTimeout": "Forbindelse timeout",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Godkendelse fik timeout. Prøv venligst igen.",
"opksshAuthFailed": "Godkendelse mislykkedes. Kontroller dine legitimationsoplysninger og prøv igen.",
"opksshConfigMissing": "OPKSSH konfiguration ikke fundet. Opret venligst ~/.opk/config.yml med dine OIDC udbyder indstillinger. Se dokumentation: https://github.com/openpubkey/opkssh#konfiguration",
"opksshSignInWith": "Log ind med {{provider}}",
"sudoPasswordPopupTitle": "Indsæt Adgangskode?",
"sudoPasswordPopupHint": "Tryk på Enter for at indsætte, Esc for at afvise",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Forbindelse afvist af serveren. Tjek venligst din godkendelse og netværkskonfiguration.",
"hostKeyRejected": "SSH-værtnøglebekræftelse afvist. Forbindelse annulleret.",
"sessionTakenOver": "Session blev åbnet i et andet faneblad. Genjusterer...",
"sessionAttachTimeout": "Session vedhæftet fil timeout. Oprettelse af ny forbindelse..."
"sessionAttachTimeout": "Session vedhæftet fil timeout. Oprettelse af ny forbindelse...",
"openFileManagerHere": "Åbn Filhåndtering Her"
},
"fileManager": {
"title": "Filhåndtering",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Farvekode filer efter type: mapper (rød), filer (blå), symlinks (grøn)",
"commandAutocomplete": "Kommando Autofuldfør",
"commandAutocompleteDesc": "Aktivér autofuldfør tabulatornøgle forslag til terminalkommandoer baseret på din kommandohistorik",
"commandHistoryTracking": "Gem Kommandohistorik",
"commandHistoryTrackingDesc": "Gem terminalkommandoer i historik for autofuldførelse og sidebar. Deaktiveret som standard for privatlivets fred.",
"defaultSnippetFoldersCollapsed": "Kollaps Snippet mapper som standard",
"defaultSnippetFoldersCollapsedDesc": "Når aktiveret, vil alle snippet mapper blive kollapset når du åbner snippets fanen",
"terminalSyntaxHighlighting": "Terminal Syntaksfremhævning",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Fremhæv automatisk kommandoer, stier, IP'er og logniveauer i terminaloutput",
"enableCommandPaletteShortcut": "Aktivér Kommandopalettegenvej",
"enableCommandPaletteShortcutDesc": "Dobbelttryk på venstre Skift for at åbne kommandopaletten for hurtig adgang til værter",
"enableTerminalSessionPersistence": "Vedvarende Terminalsessioner",
"enableTerminalSessionPersistence": "Vedvarende Faneblade/Sessioner",
"enableTerminalSessionPersistenceDesc": "Oprethold SSH-forbindelser, når du skifter faner eller lukker browseren (kan være ustabil)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Database",
"healthy": "Sunde",
"error": "Fejl",
"totalServers": "Samlede Servere",
"totalHosts": "Total Værter",
"totalTunnels": "Tunneler I Alt",
"totalCredentials": "Total Legitimationsoplysninger",
"recentActivity": "Seneste Aktivitet",
+196 -7
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Snippet in Zwischenablage kopieren",
"editTooltip": "Dieses Snippet bearbeiten",
"deleteTooltip": "Dieses Snippet löschen",
"shareTooltip": "Dieses Snippet teilen",
"sharedWithYou": "Geteilt",
"sharedBy": "Geteilt von {{username}}",
"shareSnippet": "Snippet teilen",
"user": "Benutzer",
"role": "Rolle",
"selectTarget": "Auswählen...",
"currentAccess": "Aktueller Zugriff",
"shareSuccess": "Snippet erfolgreich freigegeben",
"shareFailed": "Snippet konnte nicht geteilt werden",
"revokeSuccess": "Zugriff widerrufen",
"revokeFailed": "Fehler beim Entfernen des Zugriffs",
"failedToLoadShareData": "Fehler beim Laden der Freigabedaten",
"newFolder": "Neuer Ordner",
"reorderSameFolder": "Snippets können nur im selben Ordner neu sortiert werden",
"reorderSuccess": "Snippets erfolgreich neu sortiert",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Authentifizierung erforderlich. Bitte aktualisieren Sie die Seite.",
"dataAccessLockedReauth": "Datenzugriff gesperrt. Bitte erneut authentifizieren.",
"loading": "Lade Befehlshistorie...",
"error": "Fehler beim Laden des Verlaufs"
"error": "Fehler beim Laden des Verlaufs",
"disabledTitle": "Befehlshistorie ist deaktiviert",
"disabledDescription": "Aktivieren Sie das Tracking von Befehlshistorie in Ihrem Profil unter Anzeigeeinstellungen."
},
"splitScreen": {
"title": "Bildschirm teilen",
@@ -510,6 +525,8 @@
"checkingDatabase": "Überprüfe Datenbankverbindung...",
"checkingAuthentication": "Authentifizierung wird überprüft...",
"backendReconnected": "Serververbindung wiederhergestellt",
"connectionDegraded": "Serververbindung verloren, Wiederherstellen…",
"reload": "Reload",
"actions": "Aktionen",
"remove": "Entfernen",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Sudo-Passwort kopieren",
"passwordCopied": "Passwort in Zwischenablage kopiert",
"sudoPasswordCopied": "Sudo-Passwort in Zwischenablage kopiert",
"noPasswordAvailable": "Kein Passwort verfügbar"
"noPasswordAvailable": "Kein Passwort verfügbar",
"failedToCopyPassword": "Fehler beim Kopieren des Passworts"
},
"admin": {
"title": "Admin-Einstellungen",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Globale Überwachungseinstellungen gespeichert",
"failedToSaveGlobalSettings": "Fehler beim Speichern der globalen Überwachungseinstellungen",
"failedToLoadGlobalSettings": "Fehler beim Laden der globalen Überwachungseinstellungen",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Konfigurieren, wie lange die Benutzersitzungen dauern, bevor eine erneute Authentifizierung erforderlich ist. 'Ich merken' Sitzungen bleiben davon unberührt (immer 30 Tage).",
"sessionTimeoutHours": "Timeout-Dauer",
"hours": "std",
"sessionTimeoutNote": "Gültiger Bereich: 1720 Stunden. Änderungen gelten nur für neue Sitzungen.",
"sessionTimeoutSaved": "Sitzungs-Timeout gespeichert",
"failedToSaveSessionTimeout": "Speichern der Sitzungszeit fehlgeschlagen",
"guacamoleIntegration": "Remote-Desktop-Integration (Guacamole)",
"guacamoleIntegrationDesc": "Aktiviere RDP, VNC und Telnet-Verbindungen via Guacd. Erfordert eine Guacd-Instanz zum Laufen.",
"enableGuacamole": "RDP/VNC/Telnet-Support aktivieren",
"guacdUrl": "guacd-URL (host:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Änderungen am Guacd-Host/Port erfordern einen Neustart des Servers.",
"guacamoleSettingsSaved": "Guacamole Einstellungen gespeichert",
"failedToSaveGuacamoleSettings": "Fehler beim Speichern der Guacamol-Einstellungen",
"failedToLoadGuacamoleSettings": "Fehler beim Laden der Guacamol-Einstellungen",
"logLevel": "Log-Level",
"logLevelDesc": "Steuern Sie die Ausführlichkeit der Serverlogs. Höhere Level reduzieren die Logausgabe. Kann auch über die LOG_LEVEL Umgebungsvariable gesetzt werden.",
"logVerbosity": "Ausführlichkeit",
"logLevelNote": "Änderungen treten sofort in Kraft. Debug-Level kann die Leistung beeinflussen.",
"logLevelSaved": "Log-Level gespeichert",
"failedToSaveLogLevel": "Fehler beim Speichern der Log-Ebene",
"clampedToValidRange": "wurde an gültigen Bereich angepasst",
"loadingSessions": "Lade Sitzungen...",
"noActiveSessions": "Keine aktiven Sitzungen gefunden.",
"device": "Gerät",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} Hosts",
"importJson": "JSON importieren",
"importing": "Importieren...",
"exportAllJson": "Alle exportieren",
"exporting": "Exportiere...",
"exportedAllHosts": "Exportierte {{count}} Hosts",
"failedToExportAllHosts": "Fehler beim Exportieren der Hosts. Bitte stellen Sie sicher, dass Sie eingeloggt sind und Zugriff auf die Hostdaten haben.",
"exportAllSensitiveWarning": "Die exportierte Datei enthält sensible Authentifizierungsdaten (Passwörter/SSH-Schlüssel) im Klartext. Bitte bewahren Sie die Datei sicher und löschen Sie sie nach der Verwendung.",
"importJsonTitle": "SSH-Hosts von JSON importieren",
"importJsonDesc": "Laden Sie eine JSON-Datei hoch, um mehrere SSH-Hosts zu importieren (max. 100).",
"downloadSample": "Beispiel herunterladen",
@@ -866,11 +912,26 @@
"importError": "Importfehler",
"failedToImportJson": "Import der JSON-Datei fehlgeschlagen",
"connectionDetails": "Verbindungsdetails",
"connectionType": "Verbindungstyp",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Remote-Desktop",
"guacamoleSettings": "Remote-Desktop-Einstellungen",
"organization": "Organisation",
"ipAddress": "IP-Adresse oder Hostname",
"macAddress": "MAC-Adresse",
"macAddressDesc": "Für Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Wake-on-LAN Paket gesendet",
"wolFailed": "Fehler beim Senden des Wake-on-LAN Pakets",
"ipRequired": "IP-Adresse ist erforderlich",
"portRequired": "Port ist erforderlich",
"port": "Port",
"name": "Name",
"username": "Benutzername",
"usernameRequired": "Benutzername ist erforderlich (nur SSH/Telnet)",
"folder": "Ordner",
"tags": "Tags",
"pin": "Pin",
@@ -979,6 +1040,8 @@
"tunnel": "Tunnel",
"fileManager": "Datei-Manager",
"serverStats": "Serverstatistik",
"status": "Status",
"statistics": "Statistiken",
"hostViewer": "Hostbetrachter",
"enableServerStats": "Serverstatistik aktivieren",
"enableServerStatsDesc": "Server-Statistiksammlung für diesen Host aktivieren/deaktivieren",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Ziehen, um zwischen Ordnern zu bewegen",
"exportedHostConfig": "Exportierte Host-Konfiguration für {{name}}",
"openTerminal": "Terminal öffnen",
"openRdp": "RDP öffnen",
"openVnc": "VNC öffnen",
"openTelnet": "Telnet öffnen",
"openFileManager": "Dateimanager öffnen",
"openTunnels": "Tunnel öffnen",
"openServerDetails": "Öffne Server-Details",
"statistics": "Statistiken",
"enabledWidgets": "Aktivierte Widgets",
"openServerStats": "Serverstatistik öffnen",
"enabledWidgetsDesc": "Auswählen, welche Statistik-Widgets für diesen Host angezeigt werden sollen",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Prüfen, ob der Server online oder offline ist",
"statusCheckInterval": "Statusüberprüfungsintervall",
"statusCheckIntervalDesc": "Wie oft wird überprüft, ob der Host online ist (5s - 1h)",
"statusChecks": "Statusprüfungen",
"disableTcpPing": "TCP Ping deaktivieren",
"disableTcpPingDescription": "Statusprüfungen (Online/Offline-Erkennung) für diesen Host ausschalten",
"useGlobalStatusInterval": "Globale Standard verwenden",
"useGlobalMetricsInterval": "Globale Standard verwenden",
"usingGlobalDefault": "Globale Standardeinstellung verwenden ({{value}}s)",
"metricsCollection": "Metrik-Sammlung",
"metricsEnabled": "Metrik-Überwachung aktivieren",
"metricsEnabledDesc": "Erfassen Sie CPU, RAM, Festplatte und andere Systemstatistiken",
"metricsInterval": "Metrik-Sammlungsintervall",
"metricsIntervalDesc": "Wie oft Serverstatistiken gesammelt werden (5s - 1h)",
"metricsNotAvailableForConnectionType": "Server-Metriken sind nur für SSH-Hosts verfügbar. TCP-Ping-Statusprüfungen können weiterhin oben aktiviert werden.",
"intervalSeconds": "Sekunden",
"intervalMinutes": "minuten",
"intervalValidation": "Überwachungsintervalle müssen zwischen 5 Sekunden und 1 Stunde (3600 Sekunden) betragen",
@@ -1133,6 +1203,10 @@
"noServerFound": "Kein Server gefunden",
"jumpHostsOrder": "Verbindungen werden in der Reihenfolge getätigt: Springe Host 1 → Springe Host 2 → ... → Zielserver",
"socks5Proxy": "SOCKS5 Proxy",
"portKnocking": "Port Klopfen",
"portKnockingDesc": "Sende eine Sequenz von Verbindungsversuchen an bestimmte Ports vor der Verbindung. Wird verwendet, um Firewall-Regeln dynamisch zu öffnen.",
"addKnock": "Port hinzufügen",
"delayMs": "Verzögerung",
"socks5Description": "SOCKS5-Proxy für SSH-Verbindung konfigurieren. Alle Datenverkehr werden über den angegebenen Proxy-Server geleitet.",
"enableSocks5": "SOCKS5-Proxy aktivieren",
"enableSocks5Description": "SOCKS5-Proxy für diese SSH-Verbindung verwenden",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "MOSH Befehl automatisch beim Verbinden ausführen",
"moshCommand": "MOSH Befehl",
"moshCommandDesc": "Der MOSH Befehl zum Ausführen",
"autoTmux": "Auto-Tmux",
"autoTmuxDesc": "Automatisch an eine existierende (oder eine neue) Tmux-Sitzung anhängen für persistente Sitzungen und konstante Kontinuität",
"environmentVariables": "Umgebungsvariablen",
"environmentVariablesDesc": "Benutzerdefinierte Umgebungsvariablen für die Terminal-Sitzung festlegen",
"variableName": "Variablenname",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Tunnel-URL kopieren",
"copyServerStatsUrl": "Server-Statistik-URL kopieren",
"copyDockerUrl": "Docker URL kopieren",
"copyRemoteDesktopUrl": "Remote-Desktop-URL kopieren",
"fullScreenUrlTooltip": "URL kopieren, um diese App im Vollbildmodus zu öffnen",
"notEnabled": "Docker ist für diesen Host nicht aktiviert. Aktivieren Sie ihn in den Host-Einstellungen, um Docker Features zu verwenden.",
"validating": "Docker wird überprüft...",
@@ -1348,7 +1425,96 @@
"selectAll": "Alles auswählen",
"deselectAll": "Alle abwählen",
"useGlobalStatusDefault": "Globale Standardeinstellung verwenden (Status)",
"useGlobalMetricsDefault": "Globale Standardeinstellung (Metriks) verwenden"
"useGlobalMetricsDefault": "Globale Standardeinstellung (Metriks) verwenden",
"remoteDesktopSettings": "Remote-Desktop-Einstellungen",
"domain": "Domäne",
"securityMode": "Sicherheitsmodus",
"ignoreCert": "Zertifikat ignorieren",
"ignoreCertDesc": "TLS Zertifikatsüberprüfung für diese Verbindung überspringen",
"displaySettings": "Anzeigeeinstellungen",
"colorDepth": "Farbtiefe",
"width": "Width",
"height": "Höhe",
"dpi": "DPI",
"resizeMethod": "Methode ändern",
"forceLossless": "Verlustfrei erzwingen",
"audioSettings": "Audio-Einstellungen",
"disableAudio": "Audio deaktivieren",
"enableAudioInput": "Audio-Eingang aktivieren",
"rdpPerformance": "Leistung",
"enableWallpaper": "Hintergrundbild aktivieren",
"enableTheming": "Theming aktivieren",
"enableFontSmoothing": "Schriftglättung aktivieren",
"enableFullWindowDrag": "Volles Fenster Drag aktivieren",
"enableDesktopComposition": "Desktop-Komposition aktivieren",
"enableMenuAnimations": "Menü-Animationen aktivieren",
"disableBitmapCaching": "Bitmap-Cache deaktivieren",
"disableOffscreenCaching": "Offscreen Cache deaktivieren",
"disableGlyphCaching": "Glyph-Cache deaktivieren",
"enableGfx": "Enable GFX",
"deviceRedirection": "Umleitung des Geräts",
"enablePrinting": "Drucken aktivieren",
"enableDrive": "Drive-Umleitung aktivieren",
"driveName": "Laufwerkname",
"drivePath": "Fahrpfad",
"createDrivePath": "Laufwerkspfad erstellen",
"disableDownload": "Download deaktivieren",
"disableUpload": "Upload deaktivieren",
"enableTouch": "Berührung aktivieren",
"rdpSession": "Sitzung",
"clientName": "Kundenname",
"consoleSession": "Konsolen-Sitzung",
"initialProgram": "Initiales Programm",
"serverLayout": "Server-Tastaturlayout",
"timezone": "Timezone",
"gatewaySettings": "Gateway",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Gateway Port",
"gatewayUsername": "Gateway-Benutzername",
"gatewayPassword": "Gateway-Passwort",
"gatewayDomain": "Gateway-Domain",
"remoteApp": "RemoteApp",
"remoteAppProgram": "Remote-Anwendung",
"remoteAppDir": "Remote-App-Verzeichnis",
"remoteAppArgs": "Remote-App-Argumente",
"clipboardSettings": "Zwischenablage",
"normalizeClipboard": "Zwischenablage normalisieren",
"disableCopy": "Kopieren deaktivieren",
"disablePaste": "Einfügen deaktivieren",
"vncSettings": "VNC Einstellungen",
"cursorMode": "Cursor-Modus",
"swapRedBlue": "Rot/Blau tauschen",
"readOnly": "Nur lesen",
"recordingSettings": "Aufnahme",
"recordingPath": "Aufnahmepfad",
"recordingName": "Aufzeichnungsname",
"createRecordingPath": "Aufnahmepfad erstellen",
"excludeOutput": "Ausgabe ausschließen",
"excludeMouse": "Maus ausschließen",
"includeKeys": "Schlüssel einbeziehen",
"sendWolPacket": "WoL-Paket senden",
"wolMacAddr": "MAC-Adresse",
"wolBroadcastAddr": "Broadcast-Adresse",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Wartezeit (Sekunden)",
"connectionSettings": "Verbindungseinstellungen",
"rdpOnly": "Nur RDP",
"vncOnly": "Nur VNC",
"telnetTerminalSettings": "Terminaleinstellungen",
"terminalType": "Terminaltyp",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Farbschema",
"guacBackspaceKey": "Backspace-Taste"
},
"guacamole": {
"connecting": "Verbinde mit {{type}} Sitzung...",
"rdpConnecting": "Verbindung zum RDP-Server...",
"vncConnecting": "Verbindung zum VNC-Server...",
"telnetConnecting": "Verbindung zum Telnet-Server...",
"connectionError": "Verbindungsfehler",
"connectionFailed": "Verbindung fehlgeschlagen",
"failedToConnect": "Verbindungs-Token konnte nicht abgerufen werden"
},
"terminal": {
"title": "Terminal",
@@ -1372,6 +1538,7 @@
"connected": "Verbunden",
"clipboardWriteFailed": "Fehler beim Kopieren in die Zwischenablage. Stellen Sie sicher, dass die Seite über HTTPS oder localhost ausgeliefert wird.",
"clipboardReadFailed": "Lesen aus der Zwischenablage fehlgeschlagen. Stellen Sie sicher, dass die Zwischenablage Berechtigungen gewährt werden.",
"clipboardHttpWarning": "Einfügen erfordert HTTPS. Verwenden Sie Strg+Umschalt+V oder bedienen Termix über HTTPS.",
"sshConnected": "SSH-Verbindung hergestellt",
"authError": "Authentifizierung fehlgeschlagen: {{message}}",
"unknownError": "Unbekannter Fehler aufgetreten",
@@ -1380,7 +1547,25 @@
"connecting": "Verbinden...",
"reconnecting": "Erneut verbinden... ({{attempt}}/{{max}})",
"reconnected": "Wiederverbindung erfolgreich",
"tmuxSessionCreated": "tmux Sitzung erstellt: {{name}}",
"tmuxSessionAttached": "tmux Sitzung angehängt: {{name}}",
"tmuxUnavailable": "tmux ist nicht auf dem entfernten Host installiert, zurück zur Standard-Shell",
"tmuxSessionPickerTitle": "tmux Sitzungen",
"tmuxSessionPickerDesc": "Vorhandene Tmux-Sitzungen auf diesem Host gefunden. Wählen Sie eine, um eine Sitzung neu anzuhängen oder zu erstellen.",
"tmuxWindows": "Fenster",
"tmuxWindowCount": "{{count}} Fenster",
"tmuxWindowCount_other": "{{count}} Fenster",
"tmuxAttached": "Angehängte Clients",
"tmuxAttachedCount": "{{count}} hinzugefügt",
"tmuxLastActivity": "Letzte Aktivität",
"tmuxTimeJustNow": "gerade jetzt",
"tmuxTimeMinutes": "{{count}}m vor",
"tmuxTimeHours": "{{count}}h vor",
"tmuxTimeDays": "{{count}}d vor",
"tmuxCreateNew": "Neue Sitzung starten",
"tmuxCopyHint": "Auswahl anpassen und Enter drücken, um in die Zwischenablage zu kopieren",
"maxReconnectAttemptsReached": "Maximale Wiederholungsversuche erreicht",
"closeTab": "Schließen",
"connectionTimeout": "Verbindungs-Timeout",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Timeout der Authentifizierung. Bitte versuchen Sie es erneut.",
"opksshAuthFailed": "Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten und versuchen Sie es erneut.",
"opksshConfigMissing": "OPKSSH Konfiguration nicht gefunden. Bitte erstellen Sie ~/.opk/config.yml mit Ihren OIDC Providereinstellungen. Siehe Dokumentation: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Mit {{provider}} anmelden",
"sudoPasswordPopupTitle": "Passwort einfügen?",
"sudoPasswordPopupHint": "Drücken Sie die Eingabetaste, Esc zum Verwerfen",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Verbindung vom Server abgelehnt. Bitte überprüfen Sie Ihre Authentifizierung und Netzwerkkonfiguration.",
"hostKeyRejected": "Überprüfung des SSH-Host-Schlüssels abgelehnt. Verbindung abgebrochen.",
"sessionTakenOver": "Sitzung wurde in einem anderen Tab geöffnet. Wiederverbinden...",
"sessionAttachTimeout": "Zeitüberschreitung beim Anhang. Neue Verbindung wird erstellt..."
"sessionAttachTimeout": "Zeitüberschreitung beim Anhang. Neue Verbindung wird erstellt...",
"openFileManagerHere": "Dateimanager hier öffnen"
},
"fileManager": {
"title": "Datei-Manager",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Farbcode-Dateien nach Typ: Ordner (rot), Dateien (blau), Symlinks (grün)",
"commandAutocomplete": "Befehl Autovervollständigung",
"commandAutocompleteDesc": "Aktiviere Autovervollständigungsvorschläge für Terminal-Befehle basierend auf deiner Befehlshistorie",
"commandHistoryTracking": "Befehlsverlauf speichern",
"commandHistoryTrackingDesc": "Speichere Terminal-Befehle im Verlauf für Autovervollständigung und Seitenleiste. Standardmäßig deaktiviert für Privatsphäre.",
"defaultSnippetFoldersCollapsed": "Snippet Ordner standardmäßig einklappen",
"defaultSnippetFoldersCollapsedDesc": "Wenn aktiviert, werden alle Snippet-Ordner eingeblendet, wenn Sie den Snippet-Tab öffnen",
"terminalSyntaxHighlighting": "Terminal-Syntax-Hervorhebung",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Hervorheben von Befehlen, Pfaden, IPs und Log-Ebenen in Terminal-Ausgabe",
"enableCommandPaletteShortcut": "Tastenkürzel für Kommandopalette aktivieren",
"enableCommandPaletteShortcutDesc": "Doppeltippen Sie nach links, um die Befehlspalette zu öffnen, um schnell auf Hosts zuzugreifen",
"enableTerminalSessionPersistence": "Dauerhafte Terminalsitzungen",
"enableTerminalSessionPersistence": "Dauerhafte Tabs/Sitzungen",
"enableTerminalSessionPersistenceDesc": "SSH-Verbindungen beim Wechseln von Tabs oder Schließen des Browsers beibehalten (kann instabil sein)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Datenbank",
"healthy": "Gesund",
"error": "Fehler",
"totalServers": "Gesamt Server",
"totalHosts": "Hosts gesamt",
"totalTunnels": "Gesamte Tunnel",
"totalCredentials": "Gesamte Zugangsdaten",
"recentActivity": "Letzte Aktivität",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Αντιγραφή αποσπάσματος στο πρόχειρο",
"editTooltip": "Επεξεργαστείτε αυτό το απόσπασμα",
"deleteTooltip": "Διαγραφή αυτού του αποσπάσματος",
"shareTooltip": "Μοιραστείτε αυτό το απόσπασμα",
"sharedWithYou": "Κοινόχρηστο",
"sharedBy": "Μοιράστηκε από {{username}}",
"shareSnippet": "Μοιραστείτε Δείγμα",
"user": "Χρήστης",
"role": "Ρόλος",
"selectTarget": "Επιλογή...",
"currentAccess": "Τρέχουσα Πρόσβαση",
"shareSuccess": "Το Snippet κοινοποιήθηκε με επιτυχία",
"shareFailed": "Αποτυχία κοινοποίησης αποσπάσματος",
"revokeSuccess": "Η πρόσβαση ανακλήθηκε",
"revokeFailed": "Αποτυχία ανάκλησης πρόσβασης",
"failedToLoadShareData": "Αποτυχία φόρτωσης δεδομένων κοινής χρήσης",
"newFolder": "Νέος Φάκελος",
"reorderSameFolder": "Μπορεί μόνο να αναδιατάξει τα αποσπάσματα εντός του ίδιου φακέλου",
"reorderSuccess": "Δείγματα αναπαραγγέλθηκαν με επιτυχία",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Απαιτείται έλεγχος ταυτότητας. Παρακαλώ ανανεώστε τη σελίδα.",
"dataAccessLockedReauth": "Η πρόσβαση στα δεδομένα κλειδώθηκε. Παρακαλώ επαναλάβετε τον έλεγχο ταυτότητας.",
"loading": "Φόρτωση ιστορικού εντολών...",
"error": "Σφάλμα Φόρτωσης Ιστορικού"
"error": "Σφάλμα Φόρτωσης Ιστορικού",
"disabledTitle": "Το ιστορικό εντολών είναι απενεργοποιημένο",
"disabledDescription": "Ενεργοποιήστε την παρακολούθηση ιστορικού εντολών στο προφίλ σας στις ρυθμίσεις εμφάνισης."
},
"splitScreen": {
"title": "Διαίρεση Οθόνης",
@@ -510,6 +525,8 @@
"checkingDatabase": "Έλεγχος σύνδεσης βάσης δεδομένων...",
"checkingAuthentication": "Έλεγχος ταυτοποίησης...",
"backendReconnected": "Έγινε επαναφορά σύνδεσης διακομιστή",
"connectionDegraded": "Η σύνδεση διακομιστή χάθηκε, ανάκτηση…",
"reload": "Reload",
"actions": "Ενέργειες",
"remove": "Αφαίρεση",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Αντιγραφή Κωδικού Πρόσβασης Sudo",
"passwordCopied": "Ο κωδικός αντιγράφηκε στο πρόχειρο",
"sudoPasswordCopied": "Ο κωδικός Sudo αντιγράφηκε στο πρόχειρο",
"noPasswordAvailable": "Δεν υπάρχει διαθέσιμος κωδικός πρόσβασης"
"noPasswordAvailable": "Δεν υπάρχει διαθέσιμος κωδικός πρόσβασης",
"failedToCopyPassword": "Αποτυχία αντιγραφής κωδικού πρόσβασης"
},
"admin": {
"title": "Ρυθμίσεις Διαχειριστή",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Οι καθολικές ρυθμίσεις παρακολούθησης αποθηκεύτηκαν",
"failedToSaveGlobalSettings": "Αποτυχία αποθήκευσης καθολικών ρυθμίσεων παρακολούθησης",
"failedToLoadGlobalSettings": "Αποτυχία φόρτωσης καθολικών ρυθμίσεων παρακολούθησης",
"sessionTimeout": "Χρονικό Όριο Συνεδρίας",
"sessionTimeoutDesc": "Ρυθμίστε τη διάρκεια των περιόδων λειτουργίας των χρηστών πριν από την απαίτηση επαναταυτοποίησης. 'Να με θυμάσαι' οι συνεδρίες δεν επηρεάζονται (πάντα 30 ημέρες).",
"sessionTimeoutHours": "Διάρκεια Χρονικού Ορίου",
"hours": "ώρες",
"sessionTimeoutNote": "Έγκυρο εύρος: 1-720 ώρες. Οι αλλαγές ισχύουν μόνο για νέες συνεδρίες.",
"sessionTimeoutSaved": "Χρονικό όριο συνεδρίας αποθηκεύτηκε",
"failedToSaveSessionTimeout": "Αποτυχία αποθήκευσης χρονικού ορίου συνεδρίας",
"guacamoleIntegration": "Απομακρυσμένη Ενσωμάτωση Επιφάνειας Εργασίας (Guacamole)",
"guacamoleIntegrationDesc": "Ενεργοποίηση συνδέσεων RDP, VNC και Telnet μέσω guacd. Απαιτεί μια παρουσία guacd να εκτελείται.",
"enableGuacamole": "Ενεργοποίηση υποστήριξης RDP/VNC/Telnet",
"guacdUrl": "guacd URL (υπολογιστής:θύρα)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Οι αλλαγές στο διακομιστή guacd απαιτούν επανεκκίνηση του διακομιστή για να τεθούν σε ισχύ.",
"guacamoleSettingsSaved": "Οι ρυθμίσεις Guacamole αποθηκεύτηκαν",
"failedToSaveGuacamoleSettings": "Αποτυχία αποθήκευσης ρυθμίσεων guacamole",
"failedToLoadGuacamoleSettings": "Αποτυχία φόρτωσης των ρυθμίσεων guacamole",
"logLevel": "Επίπεδο Καταγραφής",
"logLevelDesc": "Ελέγξτε τη λεκτικότητα των αρχείων καταγραφής διακομιστή. Υψηλότερα επίπεδα μειώνουν την έξοδο καταγραφής. Μπορεί επίσης να ρυθμιστεί μέσω της μεταβλητής περιβάλλοντος LOG_ LEVEL.",
"logVerbosity": "Λάθος",
"logLevelNote": "Οι αλλαγές εφαρμόζονται αμέσως. Το επίπεδο αποσφαλμάτωσης μπορεί να επηρεάσει τις επιδόσεις.",
"logLevelSaved": "Το επίπεδο καταγραφής αποθηκεύτηκε",
"failedToSaveLogLevel": "Αποτυχία αποθήκευσης επιπέδου καταγραφής",
"clampedToValidRange": "προσαρμόστηκε σε έγκυρο εύρος",
"loadingSessions": "Φόρτωση συνεδριών...",
"noActiveSessions": "Δεν βρέθηκαν ενεργές συνεδρίες.",
"device": "Συσκευή",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} κεντρικοί υπολογιστές",
"importJson": "Εισαγωγή JSON",
"importing": "Εισαγωγή...",
"exportAllJson": "Εξαγωγή Όλων",
"exporting": "Εξαγωγή...",
"exportedAllHosts": "Exported {{count}} hosts",
"failedToExportAllHosts": "Αποτυχία εξαγωγής hosts. Παρακαλώ βεβαιωθείτε ότι είστε συνδεδεμένοι και έχετε πρόσβαση στα δεδομένα του κεντρικού υπολογιστή.",
"exportAllSensitiveWarning": "Το εξαγόμενο αρχείο θα περιέχει ευαίσθητα δεδομένα ελέγχου ταυτότητας (κωδικοί πρόσβασης/SSH κλειδιά) στο plaintext. Παρακαλώ κρατήστε το αρχείο ασφαλές και διαγράψτε το μετά τη χρήση.",
"importJsonTitle": "Εισαγωγή SSH Hosts από JSON",
"importJsonDesc": "Ανεβάστε ένα αρχείο JSON για μαζική εισαγωγή πολλαπλών υπολογιστών SSH (max 100).",
"downloadSample": "Λήψη Δείγματος",
@@ -866,11 +912,26 @@
"importError": "Σφάλμα εισαγωγής",
"failedToImportJson": "Αποτυχία εισαγωγής αρχείου JSON",
"connectionDetails": "Λεπτομέρειες Σύνδεσης",
"connectionType": "Τύπος Σύνδεσης",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Απομακρυσμένη Επιφάνεια Εργασίας",
"guacamoleSettings": "Ρυθμίσεις Απομακρυσμένης Επιφάνειας Εργασίας",
"organization": "Οργανισμός",
"ipAddress": "Διεύθυνση IP ή όνομα κεντρικού υπολογιστή",
"macAddress": "Διεύθυνση MAC",
"macAddressDesc": "Για Wake-on-LAN Μορφή: AA: Bb: Cc: DD: Ee: Ff",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Το πακέτο Wake-on-LAN εστάλη",
"wolFailed": "Αποτυχία αποστολής πακέτου Wake-on-LAN",
"ipRequired": "Η διεύθυνση IP απαιτείται",
"portRequired": "Η θύρα απαιτείται",
"port": "Θύρα",
"name": "Όνομα",
"username": "Όνομα Χρήστη",
"usernameRequired": "Το όνομα χρήστη απαιτείται (SSH/Telnet μόνο)",
"folder": "Φάκελος",
"tags": "Ετικέτες",
"pin": "Καρφίτσα",
@@ -979,6 +1040,8 @@
"tunnel": "Σήραγγα",
"fileManager": "Διαχειριστής Αρχείων",
"serverStats": "Στατιστικά Διακομιστή",
"status": "Κατάσταση",
"statistics": "Στατιστικά",
"hostViewer": "Προβολέας Υπολογιστή",
"enableServerStats": "Ενεργοποίηση Στατιστικών Διακομιστή",
"enableServerStatsDesc": "Ενεργοποίηση/απενεργοποίηση συλλογής στατιστικών του διακομιστή για αυτόν τον υπολογιστή",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Σύρετε για μετακίνηση μεταξύ φακέλων",
"exportedHostConfig": "Εξαγόμενη διαμόρφωση κεντρικού υπολογιστή για το {{name}}",
"openTerminal": "Άνοιγμα Τερματικού",
"openRdp": "Άνοιγμα RDP",
"openVnc": "Άνοιγμα VNC",
"openTelnet": "Άνοιγμα Telnet",
"openFileManager": "Άνοιγμα Διαχειριστή Αρχείων",
"openTunnels": "Άνοιγμα Σηράγγων",
"openServerDetails": "Άνοιγμα Λεπτομερειών Διακομιστή",
"statistics": "Στατιστικά",
"enabledWidgets": "Ενεργοποιημένα Widgets",
"openServerStats": "Άνοιγμα Στατιστικών Διακομιστή",
"enabledWidgetsDesc": "Επιλέξτε ποια γραφικά στοιχεία θα εμφανίζονται για αυτόν τον υπολογιστή",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Ελέγξτε αν ο διακομιστής είναι συνδεδεμένος ή εκτός σύνδεσης",
"statusCheckInterval": "Διάστημα Ελέγχου Κατάστασης",
"statusCheckIntervalDesc": "Πόσο συχνά πρέπει να ελέγξετε αν ο οικοδεσπότης είναι σε απευθείας σύνδεση (5s - 1h)",
"statusChecks": "Έλεγχοι Κατάστασης",
"disableTcpPing": "Απενεργοποίηση TCP Ping",
"disableTcpPingDescription": "Απενεργοποίηση των ελέγχων κατάστασης (online/offline ανίχνευση) για αυτόν τον κεντρικό υπολογιστή",
"useGlobalStatusInterval": "Χρήση καθολικής προεπιλογής",
"useGlobalMetricsInterval": "Χρήση καθολικής προεπιλογής",
"usingGlobalDefault": "Χρήση καθολικής προεπιλογής ({{value}}s)",
"metricsCollection": "Συλλογή Μετρικών",
"metricsEnabled": "Ενεργοποίηση Παρακολούθησης Μετρήσεων",
"metricsEnabledDesc": "Συλλογή CPU, RAM, δίσκου και άλλων στατιστικών συστημάτων",
"metricsInterval": "Διάστημα Συλλογής Μετρήσεων",
"metricsIntervalDesc": "Πόσο συχνά συλλέγουν στατιστικά στοιχεία διακομιστή (5s - 1h)",
"metricsNotAvailableForConnectionType": "Οι μετρήσεις του διακομιστή είναι διαθέσιμες μόνο για SSH hosts. Οι έλεγχοι κατάστασης ping TCP μπορούν ακόμα να ενεργοποιηθούν παραπάνω.",
"intervalSeconds": "δευτερόλεπτα",
"intervalMinutes": "λεπτά",
"intervalValidation": "Τα διαστήματα παρακολούθησης πρέπει να είναι μεταξύ 5 και 1 ώρα (3600 δευτερόλεπτα)",
@@ -1133,6 +1203,10 @@
"noServerFound": "Δεν βρέθηκε διακομιστής",
"jumpHostsOrder": "Συνδέσεις θα γίνουν με τη σειρά: Άλμα οικοδεσπότη 1 → Μετάβαση Host 2 → → Στόχος διακομιστή",
"socks5Proxy": "Διακομιστής Μεσολάβησης SOCKS5",
"portKnocking": "Λιμάνι Knocking",
"portKnockingDesc": "Αποστολή μιας ακολουθίας συνδέσεων προσπαθεί να καθορισμένες θύρες πριν από τη σύνδεση. Χρησιμοποιείται για να ανοίξει δυναμικά τους κανόνες του τείχους προστασίας.",
"addKnock": "Προσθήκη Θύρας",
"delayMs": "Καθυστέρηση",
"socks5Description": "Ρύθμιση διαμεσολαβητή SOCKS5 για τη σύνδεση SSH. Όλη η κίνηση θα δρομολογηθεί μέσω του καθορισμένου διακομιστή μεσολάβησης.",
"enableSocks5": "Ενεργοποίηση Διακομιστή Μεσολάβησης SOCKS5",
"enableSocks5Description": "Χρήση του διακομιστή μεσολάβησης SOCKS5 για αυτήν τη σύνδεση SSH",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Αυτόματη εκτέλεση της εντολής MOSH κατά τη σύνδεση",
"moshCommand": "Διοίκηση MOSH",
"moshCommandDesc": "Η εντολή MOSH για εκτέλεση",
"autoTmux": "Αυτόματο-tmux",
"autoTmuxDesc": "Αυτόματη επισύναψη σε μια υπάρχουσα (ή εκκίνηση μιας νέας) συνεδρίας tmux για επίμονες συνεδρίες και τη συνέχεια μεταξύ συσκευών",
"environmentVariables": "Μεταβλητές Περιβάλλοντος",
"environmentVariablesDesc": "Ορίστε προσαρμοσμένες μεταβλητές περιβάλλοντος για τη συνεδρία τερματικού",
"variableName": "Όνομα μεταβλητής",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Αντιγραφή Διεύθυνσης Tunnel",
"copyServerStatsUrl": "Αντιγραφή Url Στατιστικών Διακομιστή",
"copyDockerUrl": "Αντιγραφή URL Προσάρτησης",
"copyRemoteDesktopUrl": "Αντιγραφή URL Απομακρυσμένης Επιφάνειας Εργασίας",
"fullScreenUrlTooltip": "Αντιγράψτε το URL για να ανοίξετε αυτήν την εφαρμογή σε πλήρη οθόνη",
"notEnabled": "Το Docker δεν είναι ενεργοποιημένο για αυτόν τον υπολογιστή. Ενεργοποιήστε το στις ρυθμίσεις κεντρικού υπολογιστή για να χρησιμοποιήσετε τα χαρακτηριστικά Docker.",
"validating": "Επικύρωση Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Επιλογή όλων",
"deselectAll": "Αποεπιλογή όλων",
"useGlobalStatusDefault": "Χρήση Καθολικής Προεπιλογής (Κατάσταση)",
"useGlobalMetricsDefault": "Χρήση Καθολικής Προεπιλογής (Μετρήσεις)"
"useGlobalMetricsDefault": "Χρήση Καθολικής Προεπιλογής (Μετρήσεις)",
"remoteDesktopSettings": "Ρυθμίσεις Απομακρυσμένης Επιφάνειας Εργασίας",
"domain": "Τομέας",
"securityMode": "Λειτουργία Ασφαλείας",
"ignoreCert": "Αγνόηση Πιστοποιητικού",
"ignoreCertDesc": "Παράλειψη επαλήθευσης πιστοποιητικού TLS για αυτή τη σύνδεση",
"displaySettings": "Ρυθμίσεις Εμφάνισης",
"colorDepth": "Βάθος Χρώματος",
"width": "Width",
"height": "Ύψος",
"dpi": "DPI",
"resizeMethod": "Μέθοδος Αλλαγή Μεγέθους",
"forceLossless": "Εξαναγκασμός Απώλειας",
"audioSettings": "Ρυθμίσεις Ήχου",
"disableAudio": "Απενεργοποίηση Ήχου",
"enableAudioInput": "Ενεργοποίηση Εισόδου Ήχου",
"rdpPerformance": "Επιδόσεις",
"enableWallpaper": "Ενεργοποίηση Ταπετσαρίας",
"enableTheming": "Ενεργοποίηση Θέματος",
"enableFontSmoothing": "Ενεργοποίηση Εξομάλυνσης Γραμματοσειρών",
"enableFullWindowDrag": "Ενεργοποίηση Σύρματος Πλήρους Παραθύρου",
"enableDesktopComposition": "Ενεργοποίηση Σύνθεσης Επιφάνειας Εργασίας",
"enableMenuAnimations": "Ενεργοποίηση Εφέ Μενού",
"disableBitmapCaching": "Απενεργοποίηση Προσωρινής Αποθήκευσης Bitmap",
"disableOffscreenCaching": "Απενεργοποίηση Προσωρινής Αποθήκευσης Offscreen",
"disableGlyphCaching": "Απενεργοποίηση Προσωρινής Αποθήκευσης Glyph",
"enableGfx": "Enable GFX",
"deviceRedirection": "Ανακατεύθυνση Συσκευής",
"enablePrinting": "Ενεργοποίηση Εκτύπωσης",
"enableDrive": "Ενεργοποίηση Ανακατεύθυνσης Δίσκου",
"driveName": "Όνομα Δίσκου",
"drivePath": "Διαδρομή Οδηγού",
"createDrivePath": "Δημιουργία Διαδρομής Δίσκου",
"disableDownload": "Απενεργοποίηση Λήψης",
"disableUpload": "Απενεργοποίηση Αποστολής",
"enableTouch": "Ενεργοποίηση Αφής",
"rdpSession": "Συνεδρία",
"clientName": "Όνομα Πελάτη",
"consoleSession": "Συνεδρία Κονσόλας",
"initialProgram": "Αρχικό Πρόγραμμα",
"serverLayout": "Διάταξη Πληκτρολογίου Διακομιστή",
"timezone": "Timezone",
"gatewaySettings": "Πύλη",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Θύρα Πύλης",
"gatewayUsername": "Όνομα Χρήστη Πύλης",
"gatewayPassword": "Κωδικός Πύλης",
"gatewayDomain": "Τομέας Πύλης",
"remoteApp": "Απομακρυσμένη Εφαρμογή",
"remoteAppProgram": "Απομακρυσμένη Εφαρμογή",
"remoteAppDir": "Απομακρυσμένος Κατάλογος Εφαρμογών",
"remoteAppArgs": "Παράμετροι Απομακρυσμένης Εφαρμογής",
"clipboardSettings": "Πρόχειρο",
"normalizeClipboard": "Κανονικοποίηση Πρόχειρου",
"disableCopy": "Απενεργοποίηση Αντιγραφής",
"disablePaste": "Απενεργοποίηση Επικόλλησης",
"vncSettings": "Ρυθμίσεις VNC",
"cursorMode": "Λειτουργία Δρομέα",
"swapRedBlue": "Εναλλαγή Κόκκινο/Μπλε",
"readOnly": "Μόνο Για Ανάγνωση",
"recordingSettings": "Εγγραφή",
"recordingPath": "Διαδρομή Καταγραφής",
"recordingName": "Όνομα Εγγραφής",
"createRecordingPath": "Δημιουργία Διαδρομής Καταγραφής",
"excludeOutput": "Εξαίρεση Εξόδου",
"excludeMouse": "Εξαίρεση Ποντικιού",
"includeKeys": "Συμπερίληψη Πλήκτρων",
"sendWolPacket": "Αποστολή Πακέτου WoL",
"wolMacAddr": "Διεύθυνση MAC",
"wolBroadcastAddr": "Διεύθυνση Μετάδοσης",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Χρόνος Αναμονής (δευτερόλεπτα)",
"connectionSettings": "Ρυθμίσεις Σύνδεσης",
"rdpOnly": "RDP μόνο",
"vncOnly": "Μόνο VNC",
"telnetTerminalSettings": "Ρυθμίσεις Τερματικού",
"terminalType": "Τύπος Τερματικού",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Θέμα Χρωμάτων",
"guacBackspaceKey": "Κλειδί Backspace"
},
"guacamole": {
"connecting": "Σύνδεση σε συνεδρία {{type}}...",
"rdpConnecting": "Σύνδεση στον εξυπηρετητή RDP...",
"vncConnecting": "Σύνδεση σε διακομιστή VNC...",
"telnetConnecting": "Σύνδεση σε διακομιστή Telnet...",
"connectionError": "Σφάλμα σύνδεσης",
"connectionFailed": "Αποτυχία σύνδεσης",
"failedToConnect": "Αποτυχία λήψης διακριτικού σύνδεσης"
},
"terminal": {
"title": "Τερματικό",
@@ -1364,7 +1530,7 @@
"closePanel": "Κλείσιμο Πίνακα",
"reconnect": "Επανασύνδεση",
"sessionEnded": "Η Συνεδρία Έληξε",
"connectionLost": "Η Σύνδεση Χάθηκε",
"connectionLost": "Η σύνδεση χάθηκε",
"error": "ΣΦΑΛΜΑ: {{message}}",
"disconnected": "Αποσυνδέθηκε",
"connectionClosed": "Η σύνδεση έκλεισε",
@@ -1372,6 +1538,7 @@
"connected": "Συνδεδεμένο",
"clipboardWriteFailed": "Αποτυχία αντιγραφής στο πρόχειρο. Βεβαιωθείτε ότι η σελίδα εξυπηρετείται μέσω HTTPS ή localhost.",
"clipboardReadFailed": "Αποτυχία ανάγνωσης από το πρόχειρο. Βεβαιωθείτε ότι έχουν χορηγηθεί δικαιώματα πρόχειρου.",
"clipboardHttpWarning": "Η Επικόλληση απαιτεί HTTPS. Χρησιμοποιήστε Ctrl+Shift+V ή εξυπηρετήστε Termix μέσω HTTPS.",
"sshConnected": "Η σύνδεση SSH δημιουργήθηκε",
"authError": "Η πιστοποίηση απέτυχε: {{message}}",
"unknownError": "Προέκυψε άγνωστο σφάλμα",
@@ -1380,7 +1547,25 @@
"connecting": "Σύνδεση...",
"reconnecting": "Επανασύνδεση... ({{attempt}}/{{max}})",
"reconnected": "Επιτυχής επανασύνδεση",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "το tmux δεν είναι εγκατεστημένο στον απομακρυσμένο υπολογιστή, επιστρέφοντας στο τυπικό κέλυφος",
"tmuxSessionPickerTitle": "συνεδρίες tmux",
"tmuxSessionPickerDesc": "Υπάρχουσες συνεδρίες tmux που βρέθηκαν σε αυτόν τον υπολογιστή. Επιλέξτε μία για να επαναφέρετε ή να δημιουργήσετε μια νέα συνεδρία.",
"tmuxWindows": "Παράθυρα",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Συνημμένοι πελάτες",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "Τελευταία δραστηριότητα",
"tmuxTimeJustNow": "μόλις τώρα",
"tmuxTimeMinutes": "{{count}}m πριν",
"tmuxTimeHours": "{{count}}h πριν",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Έναρξη νέας συνεδρίας",
"tmuxCopyHint": "Προσαρμόστε την επιλογή και πατήστε Enter για αντιγραφή στο πρόχειρο",
"maxReconnectAttemptsReached": "Επιτεύχθηκαν μέγιστες προσπάθειες επανασύνδεσης",
"closeTab": "Κλείσιμο",
"connectionTimeout": "Χρονικό όριο σύνδεσης",
"terminalTitle": "Τερματικό - {{host}}",
"terminalWithPath": "Τερματικό - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Λήξη χρονικού ορίου επαλήθευσης. Παρακαλώ προσπαθήστε ξανά.",
"opksshAuthFailed": "Ο έλεγχος ταυτότητας απέτυχε. Παρακαλώ ελέγξτε τα στοιχεία σας και προσπαθήστε ξανά.",
"opksshConfigMissing": "Η ρύθμιση παραμέτρων OPKSSH δεν βρέθηκε. Παρακαλώ δημιουργήστε ~/.opk/config.yml με τις ρυθμίσεις παρόχου OIDC. Δείτε την τεκμηρίωση: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Σύνδεση με {{provider}}",
"sudoPasswordPopupTitle": "Εισαγωγή Κωδικού Πρόσβασης?",
"sudoPasswordPopupHint": "Πατήστε Enter για εισαγωγή, Esc για απόρριψη",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Η σύνδεση απορρίφθηκε από το διακομιστή. Παρακαλώ ελέγξτε τον έλεγχο ταυτότητας και τις ρυθμίσεις δικτύου.",
"hostKeyRejected": "Η επαλήθευση SSH κεντρικού υπολογιστή απορρίφθηκε. Η σύνδεση ακυρώθηκε.",
"sessionTakenOver": "Η συνεδρία άνοιξε σε άλλη καρτέλα. Επανασύνδεση...",
"sessionAttachTimeout": "Το συνημμένο περιόδου σύνδεσης έληξε. Δημιουργία νέας σύνδεσης..."
"sessionAttachTimeout": "Το συνημμένο περιόδου σύνδεσης έληξε. Δημιουργία νέας σύνδεσης...",
"openFileManagerHere": "Άνοιγμα Διαχειριστή Αρχείων Εδώ"
},
"fileManager": {
"title": "Διαχειριστής Αρχείων",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Αρχεία χρωμάτων-κώδικα ανά τύπο: φάκελοι (κόκκινο), αρχεία (μπλε), symlinks (πράσινο)",
"commandAutocomplete": "Αυτόματη Ολοκλήρωση Εντολής",
"commandAutocompleteDesc": "Ενεργοποίηση προτάσεων αυτόματης συμπλήρωσης πλήκτρων Tab για εντολές τερματικού βάσει του ιστορικού εντολών σας",
"commandHistoryTracking": "Αποθήκευση Ιστορικού Εντολών",
"commandHistoryTrackingDesc": "Αποθήκευση εντολών τερματικού στο ιστορικό για αυτόματη συμπλήρωση και πλευρική μπάλα. Απενεργοποιημένη από προεπιλογή για την ιδιωτικότητα.",
"defaultSnippetFoldersCollapsed": "Σύμπτυξη φακέλων αποσπώμενων από προεπιλογή",
"defaultSnippetFoldersCollapsedDesc": "Όταν ενεργοποιηθεί, όλοι οι φάκελοι αποκοπής θα καταρρεύσουν όταν ανοίξετε την καρτέλα αποσπασμάτων",
"terminalSyntaxHighlighting": "Επισήμανση Σύνταξης Τερματικού",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Αυτόματη επισήμανση εντολών, μονοπατιών, IP και επιπέδων καταγραφής στην έξοδο τερματικού",
"enableCommandPaletteShortcut": "Ενεργοποίηση Συντόμευσης Παλέτας Εντολών",
"enableCommandPaletteShortcutDesc": "Διπλό χτύπημα αριστερά Shift για άνοιγμα της παλέτας εντολών για γρήγορη πρόσβαση στους υπολογιστές",
"enableTerminalSessionPersistence": "Επίμονες Συνεδρίες Τερματικού",
"enableTerminalSessionPersistence": "Επίμονες Τάπας/Συνεδρίες",
"enableTerminalSessionPersistenceDesc": "Διατήρηση συνδέσεων SSH κατά την εναλλαγή καρτελών ή το κλείσιμο του προγράμματος περιήγησης (μπορεί να είναι ασταθές)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Βάση Δεδομένων",
"healthy": "Υγιείς",
"error": "Σφάλμα",
"totalServers": "Σύνολο Εξυπηρετητών",
"totalHosts": "Σύνολο Υπολογιστών",
"totalTunnels": "Σύνολο Σηράγγων",
"totalCredentials": "Σύνολο Διαπιστευτηρίων",
"recentActivity": "Πρόσφατη Δραστηριότητα",
+196 -7
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Copiar fragmento al portapapeles",
"editTooltip": "Editar este fragmento",
"deleteTooltip": "Eliminar este fragmento",
"shareTooltip": "Compartir este fragmento",
"sharedWithYou": "Compartido",
"sharedBy": "Compartido por {{username}}",
"shareSnippet": "Compartir Snippet",
"user": "Usuario",
"role": "Rol",
"selectTarget": "Seleccionar...",
"currentAccess": "Acceso actual",
"shareSuccess": "Fragmento compartido correctamente",
"shareFailed": "Error al compartir el snippet",
"revokeSuccess": "Acceso revocado",
"revokeFailed": "Error al revocar el acceso",
"failedToLoadShareData": "Error al cargar los datos compartidos",
"newFolder": "Nueva carpeta",
"reorderSameFolder": "Sólo se pueden reordenar fragmentos dentro de la misma carpeta",
"reorderSuccess": "Fragmentos reordenados correctamente",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Autenticación requerida. Por favor, actualiza la página.",
"dataAccessLockedReauth": "Acceso de datos bloqueado. Por favor vuelva a autenticar.",
"loading": "Cargando historial de comandos...",
"error": "Error al cargar el historial"
"error": "Error al cargar el historial",
"disabledTitle": "Historial de comandos desactivado",
"disabledDescription": "Habilita el seguimiento del historial de comandos en tu perfil bajo Configuración de Apariencia."
},
"splitScreen": {
"title": "Dividir pantalla",
@@ -510,6 +525,8 @@
"checkingDatabase": "Comprobando conexión a la base de datos...",
"checkingAuthentication": "Comprobando autenticación...",
"backendReconnected": "Conexión con el servidor restaurada",
"connectionDegraded": "Se perdió la conexión del servidor, recuperando…",
"reload": "Reload",
"actions": "Acciones",
"remove": "Eliminar",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Copiar contraseña de Sudo",
"passwordCopied": "Contraseña copiada al portapapeles",
"sudoPasswordCopied": "Contraseña de Sudo copiada al portapapeles",
"noPasswordAvailable": "Contraseña no disponible"
"noPasswordAvailable": "Contraseña no disponible",
"failedToCopyPassword": "Error al copiar la contraseña"
},
"admin": {
"title": "Configuración de Admin",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Ajustes globales de monitoreo guardados",
"failedToSaveGlobalSettings": "Error al guardar la configuración global de monitoreo",
"failedToLoadGlobalSettings": "Error al cargar la configuración global de monitoreo",
"sessionTimeout": "Tiempo de espera agotado",
"sessionTimeoutDesc": "Configurar cuánto duran las sesiones de usuario antes de requerir la re-autenticación. Las sesiones 'Recordarme' no se verán afectadas (siempre 30 días).",
"sessionTimeoutHours": "Duración de tiempo agotado",
"hours": "horas",
"sessionTimeoutNote": "Rango válido: de 1 a 720 horas. Los cambios se aplican únicamente a las nuevas sesiones.",
"sessionTimeoutSaved": "Tiempo de espera de sesión guardado",
"failedToSaveSessionTimeout": "Error al guardar el tiempo de espera de sesión",
"guacamoleIntegration": "Integración de escritorio remoto (Guacamole)",
"guacamoleIntegrationDesc": "Habilitar conexiones RDP, VNC y Telnet vía guacd. Requiere que una instancia guacd esté ejecutándose.",
"enableGuacamole": "Habilitar soporte RDP/VNC/Telnet",
"guacdUrl": "URL guacd (host:puerto)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Los cambios al host/puerto de guacd requieren un reinicio del servidor para que surta efecto.",
"guacamoleSettingsSaved": "Ajustes de Guacamole guardados",
"failedToSaveGuacamoleSettings": "Error al guardar la configuración de guacamole",
"failedToLoadGuacamoleSettings": "Error al cargar la configuración de guacamole",
"logLevel": "Nivel de Log",
"logLevelDesc": "Controla la verbosidad de los registros del servidor. Los niveles más altos reducen la salida del registro. También puede establecerse a través de la variable de entorno LOG_LEVEL.",
"logVerbosity": "Verbosidad",
"logLevelNote": "Los cambios surten efecto inmediatamente. El nivel de depuración puede afectar al rendimiento.",
"logLevelSaved": "Nivel de registro guardado",
"failedToSaveLogLevel": "Error al guardar el nivel de registro",
"clampedToValidRange": "se ajustó al rango válido",
"loadingSessions": "Cargando sesiones...",
"noActiveSessions": "No se encontraron sesiones activas.",
"device": "Dispositivo",
@@ -843,6 +884,11 @@
"hostsCount": "Equipos {{count}}",
"importJson": "Importar JSON",
"importing": "Importando...",
"exportAllJson": "Exportar todo",
"exporting": "Exportando...",
"exportedAllHosts": "Equipos {{count}} exportados",
"failedToExportAllHosts": "Error al exportar los hosts. Por favor, asegúrate de haber iniciado sesión y tener acceso a los datos del host.",
"exportAllSensitiveWarning": "El archivo exportado contendrá datos de autenticación sensibles (contraseñas/claves SSH) en listo. Por favor, mantenga el archivo seguro y elimínelo después de usarlo.",
"importJsonTitle": "Importar SSH Hosts desde JSON",
"importJsonDesc": "Subir un archivo JSON para importar múltiples hosts SSH (máx. 100).",
"downloadSample": "Descargar muestra",
@@ -866,11 +912,26 @@
"importError": "Error al importar",
"failedToImportJson": "Error al importar el archivo JSON",
"connectionDetails": "Detalles de la conexión",
"connectionType": "Tipo de conexión",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Escritorio remoto",
"guacamoleSettings": "Ajustes de escritorio remoto",
"organization": "Organización",
"ipAddress": "Dirección IP o nombre de host",
"macAddress": "Dirección MAC",
"macAddressDesc": "Para Wake-on-LAN. Formato: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Paquete Wake-on-LAN enviado",
"wolFailed": "Error al enviar el paquete Wake-on-LAN",
"ipRequired": "La dirección IP es necesaria",
"portRequired": "Se requiere un puerto",
"port": "Puerto",
"name": "Nombre",
"username": "Usuario",
"usernameRequired": "Nombre de usuario requerido (SSH/Telnet solamente)",
"folder": "Carpeta",
"tags": "Etiquetas",
"pin": "Fijar",
@@ -979,6 +1040,8 @@
"tunnel": "Túnel",
"fileManager": "Gestor de archivos",
"serverStats": "Estadísticas del Servidor",
"status": "Estado",
"statistics": "Estadísticas",
"hostViewer": "Visor de host",
"enableServerStats": "Habilitar estadísticas del servidor",
"enableServerStatsDesc": "Activar/desactivar la recopilación de estadísticas del servidor para este host",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Arrastre para mover entre carpetas",
"exportedHostConfig": "Configuración de host exportada para {{name}}",
"openTerminal": "Abrir Terminal",
"openRdp": "Abrir RDP",
"openVnc": "Abrir VNC",
"openTelnet": "Abrir Telnet",
"openFileManager": "Abrir gestor de archivos",
"openTunnels": "Abrir túneles",
"openServerDetails": "Abrir detalles del servidor",
"statistics": "Estadísticas",
"enabledWidgets": "Widgets habilitados",
"openServerStats": "Abrir estadísticas del servidor",
"enabledWidgetsDesc": "Seleccione qué widgets de estadísticas mostrar para este host",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Comprobar si el servidor está conectado o fuera de línea",
"statusCheckInterval": "Intervalo de verificación de estado",
"statusCheckIntervalDesc": "Con qué frecuencia comprobar si el host está en línea (5s - 1h)",
"statusChecks": "Comprobaciones de estado",
"disableTcpPing": "Desactivar TCP Ping",
"disableTcpPingDescription": "Desactivar comprobaciones de estado (en línea/sin conexión) para este host",
"useGlobalStatusInterval": "Usar global por defecto",
"useGlobalMetricsInterval": "Usar global por defecto",
"usingGlobalDefault": "Uso global por defecto ({{value}}s)",
"metricsCollection": "Colección de métricas",
"metricsEnabled": "Habilitar monitoreo de métricas",
"metricsEnabledDesc": "Recoge CPU, RAM, disco y otras estadísticas del sistema",
"metricsInterval": "Intervalo de recolección de métricas",
"metricsIntervalDesc": "Con qué frecuencia recolectar estadísticas del servidor (5s - 1h)",
"metricsNotAvailableForConnectionType": "Las métricas del servidor sólo están disponibles para hosts SSH. Las comprobaciones del estado de ping TCP todavía pueden activarse arriba.",
"intervalSeconds": "segundos",
"intervalMinutes": "minutos",
"intervalValidation": "Los intervalos de monitoreo deben estar entre 5 segundos y 1 hora (3600 segundos)",
@@ -1133,6 +1203,10 @@
"noServerFound": "Servidor no encontrado",
"jumpHostsOrder": "Las conexiones se harán por orden: Salto Host 1 → Salto Host 2 → ... → Servidor de destino",
"socks5Proxy": "Proxy SOCKS5",
"portKnocking": "Puesta de puerto",
"portKnockingDesc": "Enviar una secuencia de intentos de conexión a puertos especificados antes de conectarse. Utilizado para abrir reglas de firewall dinámicamente.",
"addKnock": "Añadir Puerto",
"delayMs": "Retraso",
"socks5Description": "Configure el proxy SOCKS5 para conexión SSH. Todo el tráfico se enrutará a través del servidor proxy especificado.",
"enableSocks5": "Habilitar proxy SOCKS5",
"enableSocks5Description": "Usar proxy SOCKS5 para esta conexión SSH",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Ejecutar automáticamente el comando MOSH al conectar",
"moshCommand": "MOSH comando",
"moshCommandDesc": "El comando MOSH a ejecutar",
"autoTmux": "Autotmux",
"autoTmuxDesc": "Adjuntar automáticamente a una sesión tmux existente (o iniciar una nueva) para sesiones persistentes y continuidad entre dispositivos",
"environmentVariables": "Variables de entorno",
"environmentVariablesDesc": "Establecer variables de entorno personalizadas para la sesión de terminal",
"variableName": "Nombre variable",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Copiar URL del túnel",
"copyServerStatsUrl": "Copiar URL de las estadísticas del servidor",
"copyDockerUrl": "Copiar URL de Docker",
"copyRemoteDesktopUrl": "Copiar URL de escritorio remoto",
"fullScreenUrlTooltip": "Copiar URL para abrir esta aplicación en modo de pantalla completa",
"notEnabled": "Docker no está habilitado para este host. Actívalo en la configuración del host para usar las funciones de Docker.",
"validating": "Validando Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Seleccionar todo",
"deselectAll": "Deseleccionar todo",
"useGlobalStatusDefault": "Usar predeterminado global (Estado)",
"useGlobalMetricsDefault": "Usar predeterminado global (métricas)"
"useGlobalMetricsDefault": "Usar predeterminado global (métricas)",
"remoteDesktopSettings": "Ajustes de escritorio remoto",
"domain": "Dominio",
"securityMode": "Modo de seguridad",
"ignoreCert": "Ignorar certificado",
"ignoreCertDesc": "Omitir verificación de certificado TLS para esta conexión",
"displaySettings": "Mostrar ajustes",
"colorDepth": "Profundidad de color",
"width": "Width",
"height": "Altura",
"dpi": "DPI",
"resizeMethod": "Redimensionar Método",
"forceLossless": "Forzar sin pérdida",
"audioSettings": "Ajustes de audio",
"disableAudio": "Desactivar audio",
"enableAudioInput": "Activar entrada de audio",
"rdpPerformance": "Rendimiento",
"enableWallpaper": "Activar fondo de pantalla",
"enableTheming": "Habilitar tema",
"enableFontSmoothing": "Activar suavizado de fuentes",
"enableFullWindowDrag": "Activar arrastre de ventana completa",
"enableDesktopComposition": "Habilitar composición de escritorio",
"enableMenuAnimations": "Habilitar animaciones de menú",
"disableBitmapCaching": "Desactivar Caché de Bitmap",
"disableOffscreenCaching": "Desactivar caché offscreen",
"disableGlyphCaching": "Desactivar el caché de glifos",
"enableGfx": "Enable GFX",
"deviceRedirection": "Redirección del dispositivo",
"enablePrinting": "Habilitar impresión",
"enableDrive": "Habilitar redirección de Drive",
"driveName": "Nombre de Unidad",
"drivePath": "Ruta de la Unidad",
"createDrivePath": "Crear ruta de unidad",
"disableDownload": "Desactivar descarga",
"disableUpload": "Desactivar subida",
"enableTouch": "Activar Toque",
"rdpSession": "Sesión",
"clientName": "Nombre del cliente",
"consoleSession": "Sesión de consola",
"initialProgram": "Programa inicial",
"serverLayout": "Diseño de teclado del servidor",
"timezone": "Timezone",
"gatewaySettings": "Pasarela",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Puerto de pasarela",
"gatewayUsername": "Usuario de Gateway",
"gatewayPassword": "Contraseña del Gateway",
"gatewayDomain": "Dominio de Gateway",
"remoteApp": "Aplicación remota",
"remoteAppProgram": "Aplicación remota",
"remoteAppDir": "Directorio de aplicaciones remotas",
"remoteAppArgs": "Argumentos de aplicación remota",
"clipboardSettings": "Portapapeles",
"normalizeClipboard": "Normalizar portapapeles",
"disableCopy": "Desactivar copia",
"disablePaste": "Desactivar Pegar",
"vncSettings": "Ajustes de VNC",
"cursorMode": "Modo cursor",
"swapRedBlue": "Intercambiar rojo/azul",
"readOnly": "Sólo lectura",
"recordingSettings": "Grabando",
"recordingPath": "Ruta de grabación",
"recordingName": "Nombre de grabación",
"createRecordingPath": "Crear ruta de grabación",
"excludeOutput": "Excluir salida",
"excludeMouse": "Excluir ratón",
"includeKeys": "Incluye claves",
"sendWolPacket": "Enviar paquete WoL",
"wolMacAddr": "Dirección MAC",
"wolBroadcastAddr": "Dirección de Broadcast",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Tiempo de espera (segundos)",
"connectionSettings": "Configuración de conexión",
"rdpOnly": "Sólo RDP",
"vncOnly": "Sólo VNC",
"telnetTerminalSettings": "Ajustes de Terminal",
"terminalType": "Tipo de terminal",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Esquema de color",
"guacBackspaceKey": "Retroceso clave"
},
"guacamole": {
"connecting": "Conectando a la sesión {{type}}...",
"rdpConnecting": "Conectando al servidor RDP...",
"vncConnecting": "Conectando al servidor VNC...",
"telnetConnecting": "Conectando al servidor Telnet...",
"connectionError": "Error de conexión",
"connectionFailed": "Conexión fallida",
"failedToConnect": "Error al obtener el token de conexión"
},
"terminal": {
"title": "Terminal",
@@ -1372,6 +1538,7 @@
"connected": "Conectado",
"clipboardWriteFailed": "No se pudo copiar al portapapeles. Asegúrese de que la página se sirve sobre HTTPS o localhost.",
"clipboardReadFailed": "Error al leer desde el portapapeles. Asegúrate de que los permisos del portapapeles estén garantizados.",
"clipboardHttpWarning": "Pegar requiere HTTPS. Usar Ctrl+Shift+V o servir Termix sobre HTTPS.",
"sshConnected": "Conexión SSH establecida",
"authError": "Autenticación fallida: {{message}}",
"unknownError": "Ocurrió un error desconocido",
@@ -1380,7 +1547,25 @@
"connecting": "Conectando...",
"reconnecting": "Reconectando... ({{attempt}}/{{max}})",
"reconnected": "Reconectado correctamente",
"tmuxSessionCreated": "sesión tmux creada: {{name}}",
"tmuxSessionAttached": "sesión tmux adjunta: {{name}}",
"tmuxUnavailable": "tmux no está instalado en el host remoto, volviendo a la shell estándar",
"tmuxSessionPickerTitle": "sesiones de tmux",
"tmuxSessionPickerDesc": "Sesiones de tmux existentes encontradas en este host. Seleccione una para reconstruir o crear una nueva sesión.",
"tmuxWindows": "Ventanas",
"tmuxWindowCount": "Ventana {{count}}",
"tmuxWindowCount_other": "{{count}} ventanas",
"tmuxAttached": "Clientes adjuntos",
"tmuxAttachedCount": "{{count}} adjunto",
"tmuxLastActivity": "Última actividad",
"tmuxTimeJustNow": "justo ahora",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Iniciar nueva sesión",
"tmuxCopyHint": "Ajustar selección y presionar Enter para copiar al portapapeles",
"maxReconnectAttemptsReached": "Máximo de intentos de reconexión alcanzados",
"closeTab": "Cerrar",
"connectionTimeout": "Tiempo de conexión agotado",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Se ha agotado el tiempo de autenticación. Por favor, inténtalo de nuevo.",
"opksshAuthFailed": "Error de autenticación. Por favor, comprueba tus credenciales e inténtalo de nuevo.",
"opksshConfigMissing": "Configuración OPKSSH no encontrada. Por favor cree ~/.opk/config.yml con la configuración de su proveedor OIDC. Vea la documentación: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Iniciar sesión con {{provider}}",
"sudoPasswordPopupTitle": "¿Insertar contraseña?",
"sudoPasswordPopupHint": "Presione Enter para insertar, Esc para descartar",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Conexión rechazada por el servidor. Por favor, compruebe su autenticación y configuración de red.",
"hostKeyRejected": "Verificación de clave del host SSH rechazada. Conexión cancelada.",
"sessionTakenOver": "La sesión fue abierta en otra pestaña. Volviendo a conectar...",
"sessionAttachTimeout": "Se ha agotado el tiempo de espera del archivo adjunto. Creando una nueva conexión..."
"sessionAttachTimeout": "Se ha agotado el tiempo de espera del archivo adjunto. Creando una nueva conexión...",
"openFileManagerHere": "Abrir Administrador de Archivos Aquí"
},
"fileManager": {
"title": "Gestor de archivos",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Archivos de código de color por tipo: carpetas (rojo), archivos (azul), enlaces simbólicos (verde)",
"commandAutocomplete": "Comando autocompletado",
"commandAutocompleteDesc": "Habilitar sugerencias de autocompletado de la clave Tab para comandos de terminal basado en su historial de comandos",
"commandHistoryTracking": "Guardar historial de comandos",
"commandHistoryTrackingDesc": "Almacena comandos en el historial para autocompletar y barra lateral. Desactivado por defecto para privacidad.",
"defaultSnippetFoldersCollapsed": "Colapse Carpetas de Snippet por defecto",
"defaultSnippetFoldersCollapsedDesc": "Cuando está habilitado, todas las carpetas de fragmentos se contraerán cuando abra la pestaña de fragmentos",
"terminalSyntaxHighlighting": "Resaltado sintáctico de Terminal",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Destacar automáticamente comandos, rutas, direcciones IP y niveles de registro en la salida del terminal",
"enableCommandPaletteShortcut": "Habilitar acceso directo a la paleta de comandos",
"enableCommandPaletteShortcutDesc": "Toque dos veces a la izquierda para abrir la paleta de comandos para acceder rápidamente a los hosts",
"enableTerminalSessionPersistence": "Sesiones de Terminal persistentes",
"enableTerminalSessionPersistence": "Pestañas/Sesiones persistentes",
"enableTerminalSessionPersistenceDesc": "Mantener conexiones SSH al cambiar pestañas o cerrar el navegador (puede ser inestable)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Base de datos",
"healthy": "Saludable",
"error": "Error",
"totalServers": "Total de servidores",
"totalHosts": "Total de hosts",
"totalTunnels": "Total de túneles",
"totalCredentials": "Total de credenciales",
"recentActivity": "Actividad reciente",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Kopioi tiedosto leikepöydälle",
"editTooltip": "Muokkaa tätä komentosarjaa",
"deleteTooltip": "Poista tämä tiedosto",
"shareTooltip": "Jaa tämä tiedosto",
"sharedWithYou": "Jaettu",
"sharedBy": "Jaettu {{username}}",
"shareSnippet": "Jaa Projekti",
"user": "Käyttäjä",
"role": "Rooli",
"selectTarget": "Valitse...",
"currentAccess": "Nykyinen Käyttöoikeus",
"shareSuccess": "Projekti jaettu onnistuneesti",
"shareFailed": "Snippetin jakaminen epäonnistui",
"revokeSuccess": "Pääsy peruttu",
"revokeFailed": "Käyttöoikeuksien peruuttaminen epäonnistui",
"failedToLoadShareData": "Jakamisen tietojen lataaminen epäonnistui",
"newFolder": "Uusi Kansio",
"reorderSameFolder": "Voi järjestää leikkauksia vain saman kansion sisällä",
"reorderSuccess": "Projektin uudelleenjärjestely onnistui",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Todennus vaaditaan. Ole hyvä ja päivitä sivu.",
"dataAccessLockedReauth": "Datakäyttö lukittu. Ole hyvä ja tarkista.",
"loading": "Ladataan komennon historiaa...",
"error": "Virhe Sivuhistorian Lataamisessa"
"error": "Virhe Sivuhistorian Lataamisessa",
"disabledTitle": "Komennon historia on poistettu käytöstä",
"disabledDescription": "Ota käyttöön komentohistorian seuranta profiilissasi ulkoasun asetuksista."
},
"splitScreen": {
"title": "Jaettu Näyttö",
@@ -510,6 +525,8 @@
"checkingDatabase": "Tarkistetaan tietokantayhteyttä...",
"checkingAuthentication": "Tarkistetaan todennusta...",
"backendReconnected": "Palvelinyhteys palautettu",
"connectionDegraded": "Palvelinyhteys katkesi, palautetaan…",
"reload": "Reload",
"actions": "Toiminnot",
"remove": "Poista",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Kopioi Sudo Salasana",
"passwordCopied": "Salasana kopioitu leikepöydälle",
"sudoPasswordCopied": "Sudo salasana kopioitu leikepöydälle",
"noPasswordAvailable": "Salasanaa ei ole saatavilla"
"noPasswordAvailable": "Salasanaa ei ole saatavilla",
"failedToCopyPassword": "Salasanan kopiointi epäonnistui"
},
"admin": {
"title": "Ylläpitäjän Asetukset",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Yleiset seurantaasetukset tallennettu",
"failedToSaveGlobalSettings": "Yleisten seuranta-asetusten tallentaminen epäonnistui",
"failedToLoadGlobalSettings": "Globaalien valvontaasetusten lataaminen epäonnistui",
"sessionTimeout": "Istunnon Aikakatkaisu",
"sessionTimeoutDesc": "Määritä kuinka pitkiä käyttäjäistuntoja kestää ennen kuin vaatii uudelleentunnistamisen. 'Muista minut' istunnot eivät vaikuta (aina 30 päivää).",
"sessionTimeoutHours": "Aikakatkaisu Kesto",
"hours": "tuntia",
"sessionTimeoutNote": "Kelvollinen alue: 1720 tuntia. Muutokset koskevat vain uusia istuntoja.",
"sessionTimeoutSaved": "Istunnon aikakatkaisu tallennettu",
"failedToSaveSessionTimeout": "Istunnon aikakatkaisun tallennus epäonnistui",
"guacamoleIntegration": "Etätyöpöydän Integraatio (Guacamole)",
"guacamoleIntegrationDesc": "Ota käyttöön RDP, VNC ja Telnet-yhteydet guacdilla. Vaatii guacd-ilmentymän käynnissä olevaksi.",
"enableGuacamole": "Ota käyttöön RD- NC/Telnet- tuki",
"guacdUrl": "guacd URL (host:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Muutokset guacd isäntä/portti vaativat palvelimen uudelleenkäynnistyksen voimaan.",
"guacamoleSettingsSaved": "Guacamolen asetukset tallennettu",
"failedToSaveGuacamoleSettings": "Guacamole-asetusten tallennus epäonnistui",
"failedToLoadGuacamoleSettings": "Guacamole-asetusten lataaminen epäonnistui",
"logLevel": "Lokin Taso",
"logLevelDesc": "Ohjaa palvelinlokien verbosya. Korkeampi taso vähentää lokia. Voidaan asettaa myös LOG_LEVEL ympäristömuuttujan kautta.",
"logVerbosity": "Verbositeetti",
"logLevelNote": "Muutokset tulevat voimaan välittömästi. Virheenkorjaus saattaa vaikuttaa suorituskykyyn.",
"logLevelSaved": "Lokin taso tallennettu",
"failedToSaveLogLevel": "Lokitason tallennus epäonnistui",
"clampedToValidRange": "oli mukautettu kelvolliseen alueeseen",
"loadingSessions": "Ladataan istuntoja...",
"noActiveSessions": "Aktiivisia istuntoja ei löytynyt.",
"device": "Laite",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} isäntää",
"importJson": "Tuo JSON",
"importing": "Tuoda...",
"exportAllJson": "Vie Kaikki",
"exporting": "Viedään...",
"exportedAllHosts": "Viety {{count}} isäntä",
"failedToExportAllHosts": "Palvelinten vienti epäonnistui. Varmista, että olet kirjautunut sisään ja että sinulla on pääsy isäntätietoihin.",
"exportAllSensitiveWarning": "Viety tiedosto sisältää luottamuksellisia todennustietoja (salasanat / SSH avaimet) plaintext. Pidä tiedosto turvassa ja poista se käytön jälkeen.",
"importJsonTitle": "Tuo SSH isännät JSONista",
"importJsonDesc": "Lataa JSON tiedosto tuomaan irtotavarana useita SSH isäntiä (max 100).",
"downloadSample": "Lataa Näyte",
@@ -866,11 +912,26 @@
"importError": "Tuontivirhe",
"failedToImportJson": "JSON-tiedoston tuonti epäonnistui",
"connectionDetails": "Yhteyden Tiedot",
"connectionType": "Yhteyden Tyyppi",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Etätyöpöytä",
"guacamoleSettings": "Etätyöpöydän Asetukset",
"organization": "Organisaatio",
"ipAddress": "IP-osoite tai isäntänimi",
"macAddress": "Mapc Osoite",
"macAddressDesc": "For Wake-on-LAN. Formaatti: AA:Bb:Cc:DD:Ee:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Wake-on-LAN-paketti lähetetty",
"wolFailed": "Wake-on-LAN-pakettia ei voitu lähettää",
"ipRequired": "IP-osoite on pakollinen",
"portRequired": "Portti on pakollinen",
"port": "Portti",
"name": "Nimi",
"username": "Käyttäjätunnus",
"usernameRequired": "Käyttäjätunnus vaaditaan vain (SSH/Telnet)",
"folder": "Kansio",
"tags": "Tunnisteet",
"pin": "Kiinnitä",
@@ -979,6 +1040,8 @@
"tunnel": "Tunneli",
"fileManager": "Tiedostojen Hallinta",
"serverStats": "Palvelimen Tilastot",
"status": "Tila",
"statistics": "Tilastot",
"hostViewer": "Isäntäkatseluohjelma",
"enableServerStats": "Ota käyttöön palvelimen tilastot",
"enableServerStatsDesc": "Ota käyttöön/poista käytöstä palvelimen tilastotietojen kerääminen tälle isännälle",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Vedä liikkuaksesi kansioiden välillä",
"exportedHostConfig": "Viedyt isäntä asetukset {{name}}",
"openTerminal": "Avaa pääte",
"openRdp": "Avaa RDP",
"openVnc": "Avaa VNC",
"openTelnet": "Avaa Telnet",
"openFileManager": "Avaa tiedostonhallinta",
"openTunnels": "Avoimet tunnelit",
"openServerDetails": "Avaa palvelimen tiedot",
"statistics": "Tilastot",
"enabledWidgets": "Käytössä olevat widgetit",
"openServerStats": "Avaa palvelimen tilastot",
"enabledWidgetsDesc": "Valitse tälle isännälle näytettävät tilastowidgetit",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Tarkista, onko palvelin online-tilassa vai offline-tilassa",
"statusCheckInterval": "Tilan tarkistusväli",
"statusCheckIntervalDesc": "Kuinka usein tarkistaa, onko isäntä verkossa (5 s - 1 h)",
"statusChecks": "Tilan Tarkastukset",
"disableTcpPing": "Poista Tcp Ping Käytöstä",
"disableTcpPingDescription": "Poista tilatarkastukset käytöstä (online/offline-havainto) tällä palvelimella",
"useGlobalStatusInterval": "Käytä yleistä oletusta",
"useGlobalMetricsInterval": "Käytä yleistä oletusta",
"usingGlobalDefault": "Käytetään yleistä oletusta ({{value}}s)",
"metricsCollection": "Metriikan Kokoelma",
"metricsEnabled": "Ota käyttöön mittareiden seuranta",
"metricsEnabledDesc": "Kerää prosessorin, RAMin, levyn ja muiden järjestelmätilastojen tiedot",
"metricsInterval": "Mittarien keräysväli",
"metricsIntervalDesc": "Palvelintilastojen keräämisen tiheys (5 s - 1 h)",
"metricsNotAvailableForConnectionType": "Palvelimen mittarit ovat käytettävissä vain SSH hosteille. TCP ping-tilatarkastukset voidaan silti ottaa käyttöön edellä.",
"intervalSeconds": "sekuntia",
"intervalMinutes": "minuuttia",
"intervalValidation": "Seurantavälien on oltava 5 sekunnista 1 tuntiin (3600 sekuntia).",
@@ -1133,6 +1203,10 @@
"noServerFound": "Palvelinta ei löytynyt",
"jumpHostsOrder": "Yhteydet muodostetaan järjestyksessä: Hyppypalvelin 1 → Hyppypalvelin 2 → ... → Kohdepalvelin",
"socks5Proxy": "SOCKS5-välityspalvelin",
"portKnocking": "Sataman Nukkuminen",
"portKnockingDesc": "Lähetä joukko yhteysyrityksiä määritettyihin portteihin ennen yhdistämistä. Käytetään palomuurin sääntöjen dynaamiseksi.",
"addKnock": "Lisää Portti",
"delayMs": "Viive",
"socks5Description": "Määritä SOCKS5-välityspalvelin SSH-yhteyttä varten. Kaikki liikenne reititetään määritetyn välityspalvelimen kautta.",
"enableSocks5": "Ota SOCKS5-välityspalvelin käyttöön",
"enableSocks5Description": "Käytä SOCKS5-välityspalvelinta tälle SSH-yhteydelle",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Suorita MOSH-komento automaattisesti yhteyden muodostamisen yhteydessä",
"moshCommand": "MOSH-komento",
"moshCommandDesc": "Suoritettava MOSH-komento",
"autoTmux": "Automaattinen tmux",
"autoTmuxDesc": "Liitä automaattisesti olemassa olevaan (tai aloita uusi) tmux-istuntoon pysyvien istuntojen ja laitteiden välisen jatkuvuuden varmistamiseksi",
"environmentVariables": "Ympäristömuuttujat",
"environmentVariablesDesc": "Aseta mukautettuja ympäristömuuttujia pääteistunnolle",
"variableName": "Muuttujan nimi",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Kopioi Tunnelin URL",
"copyServerStatsUrl": "Kopioi Palvelimen Tilastot Url",
"copyDockerUrl": "Kopioi Docker URL",
"copyRemoteDesktopUrl": "Kopioi Työpöydän URL",
"fullScreenUrlTooltip": "Kopioi URL-osoite avataksesi tämän sovelluksen koko näytön tilassa",
"notEnabled": "Docker ei ole käytössä tässä isännässä. Ota se käyttöön isännän asetuksissa käyttääksesi Dockerin ominaisuuksia.",
"validating": "Dockerin validointi...",
@@ -1348,7 +1425,96 @@
"selectAll": "Valitse kaikki",
"deselectAll": "Poista kaikki valinnat",
"useGlobalStatusDefault": "Käytä Globaalia Oletusta (Tila)",
"useGlobalMetricsDefault": "Käytä Globaaleja Oletuksia (Metrics)"
"useGlobalMetricsDefault": "Käytä Globaaleja Oletuksia (Metrics)",
"remoteDesktopSettings": "Etätyöpöydän Asetukset",
"domain": "Verkkotunnus",
"securityMode": "Turvallisuus Tila",
"ignoreCert": "Ohita Sertifikaatti",
"ignoreCertDesc": "Ohita TLS-varmenteen vahvistus tälle yhteydelle",
"displaySettings": "Näytä Asetukset",
"colorDepth": "Värin Syvyys",
"width": "Width",
"height": "Korkeus",
"dpi": "DPI",
"resizeMethod": "Muuta Metodin Kokoa",
"forceLossless": "Pakota Häviötön",
"audioSettings": "Äänen Asetukset",
"disableAudio": "Poista Ääni Käytöstä",
"enableAudioInput": "Ota Äänisyöttö Käyttöön",
"rdpPerformance": "Suorituskyky",
"enableWallpaper": "Ota Taustakuva Käyttöön",
"enableTheming": "Ota Teemat Käyttöön",
"enableFontSmoothing": "Ota Kirjasimen Tasaus Käyttöön",
"enableFullWindowDrag": "Ota Käyttöön Koko Ikkunan Vedä",
"enableDesktopComposition": "Ota Työpöydän Kokoonpano Käyttöön",
"enableMenuAnimations": "Ota Valikon Animaatiot Käyttöön",
"disableBitmapCaching": "Poista Bittikarttavälimuisti Käytöstä",
"disableOffscreenCaching": "Poista Offscreen Välimuisti Käytöstä",
"disableGlyphCaching": "Poista Glyph Välimuisti Käytöstä",
"enableGfx": "Enable GFX",
"deviceRedirection": "Laitteen Uudelleenohjaus",
"enablePrinting": "Käytä Tulostusta",
"enableDrive": "Ota Käyttöön Aseman Uudelleenohjaus",
"driveName": "Aseman Nimi",
"drivePath": "Aseman Polku",
"createDrivePath": "Luo Asemapolku",
"disableDownload": "Poista Lataus Käytöstä",
"disableUpload": "Poista Lataus Käytöstä",
"enableTouch": "Ota Kosketus Käyttöön",
"rdpSession": "Istunto",
"clientName": "Asiakkaan Nimi",
"consoleSession": "Konsolin Istunto",
"initialProgram": "Alkuperäinen Ohjelma",
"serverLayout": "Palvelimen Näppäimistön Asettelu",
"timezone": "Timezone",
"gatewaySettings": "Yhdyskäytävä",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Yhdyskäytävän Portti",
"gatewayUsername": "Yhdyskäytävän Käyttäjänimi",
"gatewayPassword": "Gatewayn Salasana",
"gatewayDomain": "Yhdyskäytävän Verkkotunnus",
"remoteApp": "Etäsovellus",
"remoteAppProgram": "Etäsovellus",
"remoteAppDir": "Sovelluksen Etähakemisto",
"remoteAppArgs": "Sovelluksen Etä Argumentit",
"clipboardSettings": "Leikepöytä",
"normalizeClipboard": "Normalisoi Leikepöytä",
"disableCopy": "Poista Kopiointi Käytöstä",
"disablePaste": "Poista Liitä",
"vncSettings": "Vnc Asetukset",
"cursorMode": "Kohdistimen Tila",
"swapRedBlue": "Vaihda Punainen/Sininen",
"readOnly": "Vain Luettu",
"recordingSettings": "Nauhoitetaan",
"recordingPath": "Tallennuksen Polku",
"recordingName": "Tallennuksen Nimi",
"createRecordingPath": "Luo Tallennuksen Polku",
"excludeOutput": "Sulje Ulostulo",
"excludeMouse": "Ohita Hiiri",
"includeKeys": "Sisällytä Avaimet",
"sendWolPacket": "Lähetä WoL-paketti",
"wolMacAddr": "Mapc Osoite",
"wolBroadcastAddr": "Lähetyksen Osoite",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Odotusaika (sekuntia)",
"connectionSettings": "Yhteyden Asetukset",
"rdpOnly": "Vain RDP",
"vncOnly": "Vain VNC",
"telnetTerminalSettings": "Pääteikkunan Asetukset",
"terminalType": "Päätteen Tyyppi",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Väriteema",
"guacBackspaceKey": "Askelpalautin Avain"
},
"guacamole": {
"connecting": "Yhdistetään {{type}} istuntoon...",
"rdpConnecting": "Yhdistetään RDP-palvelimeen...",
"vncConnecting": "Yhdistetään VNC palvelimeen...",
"telnetConnecting": "Yhdistetään Telnet- palvelimeen...",
"connectionError": "Virhe yhteydenpidossa",
"connectionFailed": "Yhteys epäonnistui",
"failedToConnect": "Yhteyden tunnuksen haku epäonnistui"
},
"terminal": {
"title": "Pääte",
@@ -1364,7 +1530,7 @@
"closePanel": "Sulje paneeli",
"reconnect": "Yhdistä uudelleen",
"sessionEnded": "Istunto päättyi",
"connectionLost": "Yhteys katkennut",
"connectionLost": "Yhteys katkesi",
"error": "VIRHE: {{message}}",
"disconnected": "Yhteys Katkaistu",
"connectionClosed": "Yhteys suljettu",
@@ -1372,6 +1538,7 @@
"connected": "Yhdistetty",
"clipboardWriteFailed": "Kopiointi leikepöydälle epäonnistui. Varmista, että sivu on tarjolla HTTPS:n tai localhostin kautta.",
"clipboardReadFailed": "Leikepöydältä ei voitu lukea. Varmista, että leikepöydän oikeudet on myönnetty.",
"clipboardHttpWarning": "Liitä vaatii HTTPS. Käytä Ctrl+Shift+V tai tarjoile Termix HTTPS:n päällä.",
"sshConnected": "SSH-yhteys muodostettu",
"authError": "Todennus epäonnistui: {{message}}",
"unknownError": "Tuntematon virhe tapahtui",
@@ -1380,7 +1547,25 @@
"connecting": "Yhdistetään...",
"reconnecting": "Yhdistetään Uudelleen... ({{attempt}}/{{max}})",
"reconnected": "Yhdistäminen uudelleen onnistui",
"tmuxSessionCreated": "tmux-istunto luotu: {{name}}",
"tmuxSessionAttached": "tmux-istunto liitteenä: {{name}}",
"tmuxUnavailable": "tmuxia ei ole asennettu kaukosäätimessä olevaan isäntään, joka palautuu vakiomuotoiseen komentotulkkiin",
"tmuxSessionPickerTitle": "tmux-istunnot",
"tmuxSessionPickerDesc": "Olemassa olevat tmux-istunnot löytyvät tästä isännästä. Valitse istunto, joka liitetään uudelleen tai luodaan uusi istunto.",
"tmuxWindows": "Ikkunat",
"tmuxWindowCount": "{{count}} ikkuna",
"tmuxWindowCount_other": "{{count}} ikkunaa",
"tmuxAttached": "Liitetyt asiakkaat",
"tmuxAttachedCount": "{{count}} liitetty",
"tmuxLastActivity": "Viimeisin toiminta",
"tmuxTimeJustNow": "juuri nyt",
"tmuxTimeMinutes": "{{count}}m sitten",
"tmuxTimeHours": "{{count}}tuntia sitten",
"tmuxTimeDays": "{{count}}pv sitten",
"tmuxCreateNew": "Aloita uusi istunto",
"tmuxCopyHint": "Säädä valintaa ja paina Enter kopioidaksesi leikepöydälle",
"maxReconnectAttemptsReached": "Yhteyden muodostamisen uudelleenyritysten enimmäismäärä saavutettu",
"closeTab": "Sulje",
"connectionTimeout": "Yhteyden aikakatkaisu",
"terminalTitle": "Pääte - {{host}}",
"terminalWithPath": "Pääte - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Todennus aikakatkaistiin. Yritä uudelleen.",
"opksshAuthFailed": "Todennus epäonnistui. Tarkista käyttäjätunnuksesi ja yritä uudelleen.",
"opksshConfigMissing": "OPKSSH konfiguraatiota ei löytynyt. Luo ~/.opk/config.yml OIDC palveluntarjoajan asetuksissa. Katso dokumentaatio: https://github.com/openpubkey/opkssh#konfiguraatio",
"opksshSignInWith": "Kirjaudu sisään {{provider}} avulla",
"sudoPasswordPopupTitle": "Syötä salasana?",
"sudoPasswordPopupHint": "Lisää painamalla Enter, hylkää painamalla Esc",
"sudoPasswordPopupConfirm": "Lisää",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Palvelin hylkäsi yhteyden. Tarkista todennus ja verkon asetukset.",
"hostKeyRejected": "SSH isäntäavaimen vahvistus hylätty. Yhteys peruutettu.",
"sessionTakenOver": "Istunto avattiin toisessa välilehdessä. Yhdistetään uudelleen...",
"sessionAttachTimeout": "Istunnon liite aikakatkaistiin. Uuden yhteyden luonti..."
"sessionAttachTimeout": "Istunnon liite aikakatkaistiin. Uuden yhteyden luonti...",
"openFileManagerHere": "Avaa Tiedostonhallinta Tähän"
},
"fileManager": {
"title": "Tiedostojen Hallinta",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Värikoodaa tiedostot tyypin mukaan: kansiot (punainen), tiedostot (sininen), symboliset linkit (vihreä)",
"commandAutocomplete": "Komennon automaattinen täydennys",
"commandAutocompleteDesc": "Ota käyttöön Tab-näppäimen automaattisen täydennyksen ehdotukset päätekomennoille komentohistoriasi perusteella",
"commandHistoryTracking": "Tallenna Komentohistoria",
"commandHistoryTrackingDesc": "Tallenna pääte komentoja historiassa automaattisen täydennyksen ja sivupalkin vuoksi. Oletuksena poistettu käytöstä yksityisyyden suojaamiseksi.",
"defaultSnippetFoldersCollapsed": "Pienennä katkelmakansiot oletusarvoisesti",
"defaultSnippetFoldersCollapsedDesc": "Kun tämä on käytössä, kaikki katkelmakansiot kutistetaan, kun avaat katkelmavälilehden.",
"terminalSyntaxHighlighting": "Päätelaitteen syntaksin korostus",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Korosta automaattisesti komennot, polut, IP-osoitteet ja lokitasot päätetulosteessa",
"enableCommandPaletteShortcut": "Ota Käyttöön Komentopaletin Pikakuvake",
"enableCommandPaletteShortcutDesc": "Kaksoisnapauta vasen Shift avataksesi komentopaletin saadaksesi nopean pääsyn isäntiin",
"enableTerminalSessionPersistence": "Pysyvät Pääteistunnot",
"enableTerminalSessionPersistence": "Pysyvät Välilehdet",
"enableTerminalSessionPersistenceDesc": "Säilytä SSH yhteydet kun välilehtiä vaihdetaan tai selain suljetaan (voi olla epävakaa)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Tietokanta",
"healthy": "Terveellinen",
"error": "Virhe",
"totalServers": "Palvelimia yhteensä",
"totalHosts": "Isännät Yhteensä",
"totalTunnels": "Tunneleita yhteensä",
"totalCredentials": "Yhteensä valtakirjoja",
"recentActivity": "Viimeaikaiset toiminnot",
+196 -7
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Copier le snippet dans le presse-papiers",
"editTooltip": "Modifier ce snippet",
"deleteTooltip": "Supprimer ce snippet",
"shareTooltip": "Partager ce snippet",
"sharedWithYou": "Partagé",
"sharedBy": "Partagé par {{username}}",
"shareSnippet": "Partager le Snippet",
"user": "Utilisateur",
"role": "Rôle",
"selectTarget": "Sélectionner...",
"currentAccess": "Accès actuel",
"shareSuccess": "Snippet partagé avec succès",
"shareFailed": "Impossible de partager le snippet",
"revokeSuccess": "Accès révoqué",
"revokeFailed": "Impossible de révoquer l'accès",
"failedToLoadShareData": "Impossible de charger les données de partage",
"newFolder": "Nouveau dossier",
"reorderSameFolder": "Ne peut que réorganiser les extraits dans le même dossier",
"reorderSuccess": "Extraits réordonnés avec succès",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Authentification requise. Veuillez actualiser la page.",
"dataAccessLockedReauth": "Accès aux données verrouillé. Veuillez vous authentifier.",
"loading": "Chargement de l'historique des commandes...",
"error": "Erreur lors du chargement de l'historique"
"error": "Erreur lors du chargement de l'historique",
"disabledTitle": "L'historique de commande est désactivé",
"disabledDescription": "Activer le suivi de l'historique de commandes dans votre profil dans les paramètres d'apparence."
},
"splitScreen": {
"title": "Écran partagé",
@@ -510,6 +525,8 @@
"checkingDatabase": "Vérification de la connexion à la base de données...",
"checkingAuthentication": "Vérification de l'authentification...",
"backendReconnected": "Connexion au serveur restaurée",
"connectionDegraded": "Connexion au serveur perdue, récupération de…",
"reload": "Reload",
"actions": "Actions",
"remove": "Retirer",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Copier le mot de passe Sudo",
"passwordCopied": "Mot de passe copié dans le presse-papiers",
"sudoPasswordCopied": "Mot de passe Sudo copié dans le presse-papiers",
"noPasswordAvailable": "Aucun mot de passe disponible"
"noPasswordAvailable": "Aucun mot de passe disponible",
"failedToCopyPassword": "Impossible de copier le mot de passe"
},
"admin": {
"title": "Paramètres de l'administrateur",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Paramètres de surveillance globale enregistrés",
"failedToSaveGlobalSettings": "Impossible d'enregistrer les paramètres de surveillance globale",
"failedToLoadGlobalSettings": "Impossible de charger les paramètres de surveillance globale",
"sessionTimeout": "Délai de session dépassé",
"sessionTimeoutDesc": "Configurer la durée des sessions utilisateur avant de nécessiter une ré-authentification. Les sessions 'Se souvenir de moi' ne sont pas affectées (toujours 30 jours).",
"sessionTimeoutHours": "Durée d'expiration",
"hours": "heures",
"sessionTimeoutNote": "Plage valide : 1 à 720 heures, les modifications ne s'appliquent qu'aux nouvelles sessions.",
"sessionTimeoutSaved": "Délai de session enregistré",
"failedToSaveSessionTimeout": "Impossible d'enregistrer le délai d'attente de la session",
"guacamoleIntegration": "Intégration du bureau distant (Guacamole)",
"guacamoleIntegrationDesc": "Activer les connexions RDP, VNC et Telnet via guacd. Nécessite une instance guacd pour être en cours d'exécution.",
"enableGuacamole": "Activer le support RDP/VNC/Telnet",
"guacdUrl": "URL guacd (hôte:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Les modifications apportées à l'hôte/port guacd nécessitent un redémarrage du serveur pour prendre effet.",
"guacamoleSettingsSaved": "Paramètres Guacamole enregistrés",
"failedToSaveGuacamoleSettings": "Échec de l'enregistrement des paramètres guacamole",
"failedToLoadGuacamoleSettings": "Impossible de charger les paramètres de guacamole",
"logLevel": "Niveau du journal",
"logLevelDesc": "Contrôle la verbosité des logs du serveur. Des niveaux plus élevés réduisent la sortie du journal. Peut également être défini via la variable d'environnement LOG_LEVEL.",
"logVerbosity": "Verbosité",
"logLevelNote": "Les changements prennent effet immédiatement. Le niveau de débogage peut affecter les performances.",
"logLevelSaved": "Niveau de journal enregistré",
"failedToSaveLogLevel": "Impossible d'enregistrer le niveau du journal",
"clampedToValidRange": "a été ajusté à une plage valide",
"loadingSessions": "Chargement des sessions...",
"noActiveSessions": "Aucune session active trouvée.",
"device": "Appareil",
@@ -843,6 +884,11 @@
"hostsCount": "Hôtes {{count}}",
"importJson": "Importer JSON",
"importing": "Importation en cours...",
"exportAllJson": "Exporter tout",
"exporting": "Exportation en cours...",
"exportedAllHosts": "Hôtes {{count}} exportés",
"failedToExportAllHosts": "Impossible d'exporter les hôtes. Veuillez vous assurer que vous êtes connecté et que vous avez accès aux données de l'hôte.",
"exportAllSensitiveWarning": "Le fichier exporté contiendra des données d'authentification sensibles (mots de passe/clés SSH) en clair. Veuillez garder le fichier sécurisé et le supprimer après utilisation.",
"importJsonTitle": "Importer les hôtes SSH depuis JSON",
"importJsonDesc": "Télécharger un fichier JSON pour importer en masse plusieurs hôtes SSH (max 100).",
"downloadSample": "Télécharger un exemple",
@@ -866,11 +912,26 @@
"importError": "Erreur d'importation",
"failedToImportJson": "Impossible d'importer le fichier JSON",
"connectionDetails": "Détails de la connexion",
"connectionType": "Type de connexion",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Bureau distant",
"guacamoleSettings": "Paramètres du bureau distant",
"organization": "Organisation",
"ipAddress": "Adresse IP ou nom d'hôte",
"macAddress": "Adresse MAC",
"macAddressDesc": "Pour Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Paquet Wake-on-LAN envoyé",
"wolFailed": "Impossible d'envoyer le paquet Wake-on-LAN",
"ipRequired": "L'adresse IP est requise",
"portRequired": "Le port est requis",
"port": "Port",
"name": "Nom",
"username": "Nom d'utilisateur",
"usernameRequired": "Le nom d'utilisateur est requis (SSH/Telnet uniquement)",
"folder": "Répertoire",
"tags": "Tags",
"pin": "Épingler",
@@ -979,6 +1040,8 @@
"tunnel": "Tunnel",
"fileManager": "Gestionnaire de fichiers",
"serverStats": "Statistiques du serveur",
"status": "Statut",
"statistics": "Statistiques",
"hostViewer": "Visionneuse Hôte",
"enableServerStats": "Activer les statistiques du serveur",
"enableServerStatsDesc": "Activer/désactiver la collecte des statistiques du serveur pour cet hôte",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Faites glisser pour vous déplacer entre les dossiers",
"exportedHostConfig": "Configuration de l'hôte exporté pour {{name}}",
"openTerminal": "Ouvrir le terminal",
"openRdp": "Ouvrir RDP",
"openVnc": "Ouvrir VNC",
"openTelnet": "Ouvrir Telnet",
"openFileManager": "Ouvrir le gestionnaire de fichiers",
"openTunnels": "Tunnels ouverts",
"openServerDetails": "Détails de l'Open Server",
"statistics": "Statistiques",
"enabledWidgets": "Widgets activés",
"openServerStats": "Statistiques du serveur ouvert",
"enabledWidgetsDesc": "Sélectionnez les widgets de statistiques à afficher pour cet hôte",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Vérifier si le serveur est en ligne ou hors ligne",
"statusCheckInterval": "Intervalle de vérification de l'état",
"statusCheckIntervalDesc": "À quelle fréquence vérifier si l'hôte est en ligne (5s - 1h)",
"statusChecks": "Contrôles de statut",
"disableTcpPing": "Désactiver le Ping TCP",
"disableTcpPingDescription": "Désactiver la vérification des statuts (détection en ligne/hors ligne) pour cet hôte",
"useGlobalStatusInterval": "Utiliser la valeur par défaut globale",
"useGlobalMetricsInterval": "Utiliser la valeur par défaut globale",
"usingGlobalDefault": "Utiliser la valeur globale par défaut ({{value}}s)",
"metricsCollection": "Collection de métriques",
"metricsEnabled": "Activer la surveillance des métriques",
"metricsEnabledDesc": "Collecter des statistiques CPU, RAM, disque et autres systèmes",
"metricsInterval": "Intervalle de collecte des métriques",
"metricsIntervalDesc": "Fréquence de collecte des statistiques du serveur (5s - 1h)",
"metricsNotAvailableForConnectionType": "Les métriques de serveur ne sont disponibles que pour les hôtes SSH. Les vérifications d'état de ping TCP peuvent toujours être activées ci-dessus.",
"intervalSeconds": "secondes",
"intervalMinutes": "minutes",
"intervalValidation": "Les intervalles de surveillance doivent être compris entre 5 secondes et 1 heure (3600 secondes)",
@@ -1133,6 +1203,10 @@
"noServerFound": "Aucun serveur trouvé",
"jumpHostsOrder": "Les connexions seront effectuées dans l'ordre : Saut Hôte 1 → Hôte de saut 2 → ... → Serveur cible",
"socks5Proxy": "Proxy SOCKS5",
"portKnocking": "Saut du port",
"portKnockingDesc": "Envoyer une séquence de tentatives de connexion à des ports spécifiés avant de se connecter. Utilisé pour ouvrir dynamiquement les règles de pare-feu.",
"addKnock": "Ajouter un port",
"delayMs": "Délai",
"socks5Description": "Configurer le proxy SOCKS5 pour la connexion SSH. Tout le trafic sera acheminé via le serveur proxy spécifié.",
"enableSocks5": "Activer le proxy SOCKS5",
"enableSocks5Description": "Utiliser le proxy SOCKS5 pour cette connexion SSH",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Lancer automatiquement la commande MOSH à la connexion",
"moshCommand": "Commande MOSH",
"moshCommandDesc": "La commande MOSH à exécuter",
"autoTmux": "Tmux automatique",
"autoTmuxDesc": "Se connecter automatiquement à une session tmux existante (ou démarrer une nouvelle) pour les sessions persistantes et la continuité entre périphériques",
"environmentVariables": "Variables d'environnement",
"environmentVariablesDesc": "Définir des variables d'environnement personnalisées pour la session de terminal",
"variableName": "Nom de la variable",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Copier l'URL du tunnel",
"copyServerStatsUrl": "Copier l'URL des statistiques du serveur",
"copyDockerUrl": "Copier l'URL de Docker",
"copyRemoteDesktopUrl": "Copier l'URL du bureau distant",
"fullScreenUrlTooltip": "Copier l'URL pour ouvrir cette application en mode plein écran",
"notEnabled": "Docker n'est pas activé pour cet hôte. Activez-le dans les paramètres de l'hôte pour utiliser les fonctionnalités Docker.",
"validating": "Validation de Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Tout sélectionner",
"deselectAll": "Désélectionner tout",
"useGlobalStatusDefault": "Utiliser la valeur globale par défaut (Status)",
"useGlobalMetricsDefault": "Utiliser la valeur globale par défaut (métriques)"
"useGlobalMetricsDefault": "Utiliser la valeur globale par défaut (métriques)",
"remoteDesktopSettings": "Paramètres du bureau distant",
"domain": "Domaine",
"securityMode": "Mode de sécurité",
"ignoreCert": "Ignorer le certificat",
"ignoreCertDesc": "Ignorer la vérification du certificat TLS pour cette connexion",
"displaySettings": "Paramètres d'affichage",
"colorDepth": "Profondeur des couleurs",
"width": "Width",
"height": "Hauteur",
"dpi": "DPI",
"resizeMethod": "Redimensionner la méthode",
"forceLossless": "Forcer sans perte",
"audioSettings": "Paramètres audio",
"disableAudio": "Désactiver l'audio",
"enableAudioInput": "Activer la saisie audio",
"rdpPerformance": "Performances",
"enableWallpaper": "Activer le fond d'écran",
"enableTheming": "Activer le thème",
"enableFontSmoothing": "Activer le lissage de police",
"enableFullWindowDrag": "Activer le glissement de la fenêtre complète",
"enableDesktopComposition": "Activer la composition du bureau",
"enableMenuAnimations": "Activer les animations du menu",
"disableBitmapCaching": "Désactiver la mise en cache des Bitmaps",
"disableOffscreenCaching": "Désactiver la mise en cache hors écran",
"disableGlyphCaching": "Désactiver la mise en cache des glyphes",
"enableGfx": "Enable GFX",
"deviceRedirection": "Redirection de l'appareil",
"enablePrinting": "Activer l'impression",
"enableDrive": "Activer la redirection du lecteur",
"driveName": "Nom du lecteur",
"drivePath": "Chemin du lecteur",
"createDrivePath": "Créer un chemin d'accès",
"disableDownload": "Désactiver le téléchargement",
"disableUpload": "Désactiver le téléchargement",
"enableTouch": "Activer le Toucher",
"rdpSession": "Séance",
"clientName": "Nom du client",
"consoleSession": "Session de la console",
"initialProgram": "Programme initial",
"serverLayout": "Disposition du clavier du serveur",
"timezone": "Timezone",
"gatewaySettings": "Passerelle",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Port de la passerelle",
"gatewayUsername": "Nom d'utilisateur de la passerelle",
"gatewayPassword": "Mot de passe de la passerelle",
"gatewayDomain": "Domaine de la passerelle",
"remoteApp": "Application distante",
"remoteAppProgram": "Application distante",
"remoteAppDir": "Répertoire des applications distantes",
"remoteAppArgs": "Arguments de l'application à distance",
"clipboardSettings": "Presse-papiers",
"normalizeClipboard": "Normaliser le presse-papier",
"disableCopy": "Désactiver la copie",
"disablePaste": "Désactiver Coller",
"vncSettings": "Paramètres VNC",
"cursorMode": "Mode Curseur",
"swapRedBlue": "Échanger Rouge/Bleu",
"readOnly": "Lecture seule",
"recordingSettings": "Enregistrement en cours",
"recordingPath": "Chemin de l'enregistrement",
"recordingName": "Nom de l'enregistrement",
"createRecordingPath": "Créer un chemin d'enregistrement",
"excludeOutput": "Exclure la sortie",
"excludeMouse": "Exclure la souris",
"includeKeys": "Inclure les clés",
"sendWolPacket": "Envoyer un paquet WoL",
"wolMacAddr": "Adresse MAC",
"wolBroadcastAddr": "Adresse de diffusion",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Temps d'attente (secondes)",
"connectionSettings": "Paramètres de connexion",
"rdpOnly": "RDP seulement",
"vncOnly": "VNC uniquement",
"telnetTerminalSettings": "Paramètres du terminal",
"terminalType": "Type de terminal",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Schéma de couleurs",
"guacBackspaceKey": "Clé de Retour arrière"
},
"guacamole": {
"connecting": "Connexion à la session {{type}}...",
"rdpConnecting": "Connexion au serveur RD...",
"vncConnecting": "Connexion au serveur VNC...",
"telnetConnecting": "Connexion au serveur Telnet...",
"connectionError": "Erreur de connexion",
"connectionFailed": "Échec de la connexion",
"failedToConnect": "Impossible d'obtenir le jeton de connexion"
},
"terminal": {
"title": "Terminal",
@@ -1372,6 +1538,7 @@
"connected": "Connecté",
"clipboardWriteFailed": "Échec de la copie dans le presse-papiers. Assurez-vous que la page est servie via HTTPS ou localhost.",
"clipboardReadFailed": "Échec de lecture du presse-papiers. Assurez-vous que les permissions du presse-papiers sont accordées.",
"clipboardHttpWarning": "Coller nécessite HTTPS. Utilisez Ctrl+Maj+V ou servir Termix via HTTPS.",
"sshConnected": "Connexion SSH établie",
"authError": "Échec de l'authentification : {{message}}",
"unknownError": "Une erreur inconnue s'est produite",
@@ -1380,7 +1547,25 @@
"connecting": "Connexion en cours...",
"reconnecting": "Reconnexion en cours... ({{attempt}}/{{max}})",
"reconnected": "Reconnexion réussie",
"tmuxSessionCreated": "session tmux créée : {{name}}",
"tmuxSessionAttached": "Session tmux attachée : {{name}}",
"tmuxUnavailable": "tmux n'est pas installé sur l'hôte distant, passant à l'interpréteur de commandes standard",
"tmuxSessionPickerTitle": "Sessions tmux",
"tmuxSessionPickerDesc": "Sessions tmux existantes trouvées sur cet hôte. Sélectionnez-en une pour réattacher ou créer une nouvelle session.",
"tmuxWindows": "Fenêtres",
"tmuxWindowCount": "Fenêtre {{count}}",
"tmuxWindowCount_other": "Fenêtres {{count}}",
"tmuxAttached": "Clients attachés",
"tmuxAttachedCount": "{{count}} attaché",
"tmuxLastActivity": "Dernière activité",
"tmuxTimeJustNow": "à l'instant",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Démarrer une nouvelle session",
"tmuxCopyHint": "Ajuster la sélection et appuyer sur Entrée pour copier dans le presse-papiers",
"maxReconnectAttemptsReached": "Nombre maximum de tentatives de reconnexion atteint",
"closeTab": "Fermer",
"connectionTimeout": "Délai de connexion dépassé",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Délai d'authentification dépassé. Veuillez réessayer.",
"opksshAuthFailed": "L'authentification a échoué. Veuillez vérifier vos identifiants et réessayer.",
"opksshConfigMissing": "La configuration OPKSSH est introuvable. Veuillez créer ~/.opk/config.yml avec vos paramètres de fournisseur OIDC. Voir la documentation : https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Connectez-vous avec {{provider}}",
"sudoPasswordPopupTitle": "Insérer un mot de passe ?",
"sudoPasswordPopupHint": "Appuyez sur Entrée pour insérer, Échap pour rejeter",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Connexion refusée par le serveur. Veuillez vérifier votre authentification et la configuration du réseau.",
"hostKeyRejected": "La vérification de la clé d'hôte SSH a été rejetée. La connexion a été annulée.",
"sessionTakenOver": "La session a été ouverte dans un autre onglet. Reconnexion...",
"sessionAttachTimeout": "La session a expiré. Création d'une nouvelle connexion..."
"sessionAttachTimeout": "La session a expiré. Création d'une nouvelle connexion...",
"openFileManagerHere": "Ouvrir le gestionnaire de fichiers ici"
},
"fileManager": {
"title": "Gestionnaire de fichiers",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Fichiers de code couleur par type: dossiers (rouge), fichiers (bleu), liens symboliques (vert)",
"commandAutocomplete": "Auto-complétion de commande",
"commandAutocompleteDesc": "Activer les suggestions de saisie automatique des touches Tab pour les commandes de terminal basées sur l'historique de vos commandes",
"commandHistoryTracking": "Enregistrer l'historique des commandes",
"commandHistoryTrackingDesc": "Stocker les commandes de terminal dans l'historique pour l'auto-complétion et la barre latérale. Désactivé par défaut pour la confidentialité.",
"defaultSnippetFoldersCollapsed": "Réduire les dossiers de snippet par défaut",
"defaultSnippetFoldersCollapsedDesc": "Lorsque cette option est activée, tous les dossiers de snippet seront réduits lorsque vous ouvrez l'onglet snippets",
"terminalSyntaxHighlighting": "Coloration syntaxique du terminal",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Mettre automatiquement en surbrillance les commandes, chemins, IPs et niveaux de log dans la sortie du terminal",
"enableCommandPaletteShortcut": "Activer le raccourci de la palette de commandes",
"enableCommandPaletteShortcutDesc": "Tapotez deux fois vers la gauche pour ouvrir la palette de commandes pour un accès rapide aux hôtes",
"enableTerminalSessionPersistence": "Sessions terminales persistantes",
"enableTerminalSessionPersistence": "Onglets/Sessions persistantes",
"enableTerminalSessionPersistenceDesc": "Maintenir les connexions SSH lors du changement d'onglets ou de la fermeture du navigateur (peut être instable)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Base de données",
"healthy": "Sain",
"error": "Erreur",
"totalServers": "Serveurs totaux",
"totalHosts": "Nombre total d'hôtes",
"totalTunnels": "Tunnels totaux",
"totalCredentials": "Identifiants totaux",
"recentActivity": "Activité récente",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "העתקת קטע ללוח",
"editTooltip": "ערוך קטע זה",
"deleteTooltip": "מחק את הקטע הזה",
"shareTooltip": "Share this snippet",
"sharedWithYou": "Shared",
"sharedBy": "Shared by {{username}}",
"shareSnippet": "Share Snippet",
"user": "User",
"role": "Role",
"selectTarget": "Select...",
"currentAccess": "Current Access",
"shareSuccess": "Snippet shared successfully",
"shareFailed": "Failed to share snippet",
"revokeSuccess": "Access revoked",
"revokeFailed": "Failed to revoke access",
"failedToLoadShareData": "Failed to load sharing data",
"newFolder": "תיקייה חדשה",
"reorderSameFolder": "ניתן לסדר מחדש קטעי טקסט רק באותה תיקייה",
"reorderSuccess": "סידור מחדש של הקטעים בוצע בהצלחה",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "נדרש אימות. אנא רענן את הדף.",
"dataAccessLockedReauth": "גישת הנתונים נעולה. אנא אימות מחדש.",
"loading": "טוען היסטוריית פקודות...",
"error": "שגיאה בטעינת ההיסטוריה"
"error": "שגיאה בטעינת ההיסטוריה",
"disabledTitle": "Command History is Disabled",
"disabledDescription": "Enable Command History Tracking in your profile under Appearance settings."
},
"splitScreen": {
"title": "מסך מפוצל",
@@ -510,6 +525,8 @@
"checkingDatabase": "בודק חיבור למסד הנתונים...",
"checkingAuthentication": "בודק אימות...",
"backendReconnected": "חיבור השרת שוחזר",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "פעולות",
"remove": "לְהַסִיר",
"revoke": "לְבַטֵל",
@@ -541,7 +558,8 @@
"copySudoPassword": "העתקת סיסמת סודו",
"passwordCopied": "הסיסמה הועתקה ללוח",
"sudoPasswordCopied": "סיסמת הסודו הועתקה ללוח",
"noPasswordAvailable": "אין סיסמה זמינה"
"noPasswordAvailable": "אין סיסמה זמינה",
"failedToCopyPassword": "Failed to copy password"
},
"admin": {
"title": "הגדרות מנהל מערכת",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "הגדרות ניטור גלובליות נשמרו",
"failedToSaveGlobalSettings": "נכשלה שמירת הגדרות הניטור הגלובליות",
"failedToLoadGlobalSettings": "נכשלה טעינת הגדרות הניטור הגלובליות",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Configure how long user sessions last before requiring re-authentication. 'Remember Me' sessions are unaffected (always 30 days).",
"sessionTimeoutHours": "Timeout Duration",
"hours": "hours",
"sessionTimeoutNote": "Valid range: 1720 hours. Changes apply to new sessions only.",
"sessionTimeoutSaved": "Session timeout saved",
"failedToSaveSessionTimeout": "Failed to save session timeout",
"guacamoleIntegration": "שילוב שולחן עבודה מרוחק (גוואקמולי)",
"guacamoleIntegrationDesc": "הפעל חיבורי RDP, VNC ו-Telnet דרך guacd. נדרש מופע guacd כדי לפעול.",
"enableGuacamole": "הפעל תמיכה ב-RDP/VNC/Telnet",
"guacdUrl": "כתובת URL של guacd (מארח:יציאה)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "שינויים במארח/פורט של guacd דורשים הפעלה מחדש של השרת כדי להיכנס לתוקף.",
"guacamoleSettingsSaved": "הגדרות הגוואקמולי נשמרו",
"failedToSaveGuacamoleSettings": "שמירת הגדרות הגוואקמולי נכשלה",
"failedToLoadGuacamoleSettings": "טעינת הגדרות הגוואקמולי נכשלה",
"logLevel": "Log Level",
"logLevelDesc": "Control the verbosity of server logs. Higher levels reduce log output. Can also be set via the LOG_LEVEL environment variable.",
"logVerbosity": "Verbosity",
"logLevelNote": "Changes take effect immediately. Debug level may impact performance.",
"logLevelSaved": "Log level saved",
"failedToSaveLogLevel": "Failed to save log level",
"clampedToValidRange": "הותאם לטווח תקף",
"loadingSessions": "טוען סשנים...",
"noActiveSessions": "לא נמצאו סשנים פעילים.",
"device": "הֶתקֵן",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} hosts",
"importJson": "ייבוא JSON",
"importing": "מייבא...",
"exportAllJson": "Export All",
"exporting": "Exporting...",
"exportedAllHosts": "Exported {{count}} hosts",
"failedToExportAllHosts": "Failed to export hosts. Please ensure you're logged in and have access to the host data.",
"exportAllSensitiveWarning": "The exported file will contain sensitive authentication data (passwords/SSH keys) in plaintext. Please keep the file secure and delete it after use.",
"importJsonTitle": "ייבוא מארחי SSH מ-JSON",
"importJsonDesc": "העלה קובץ JSON לייבוא בכמות גדולה של מארחי SSH מרובים (מקסימום 100).",
"downloadSample": "הורד דוגמה",
@@ -866,11 +912,26 @@
"importError": "שגיאת ייבוא",
"failedToImportJson": "ייבוא קובץ JSON נכשל",
"connectionDetails": "פרטי חיבור",
"connectionType": "סוג חיבור",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "טלנט",
"remoteDesktop": "שולחן עבודה מרוחק",
"guacamoleSettings": "הגדרות שולחן עבודה מרוחק",
"organization": "אִרגוּן",
"ipAddress": "כתובת IP או שם מארח",
"macAddress": "MAC Address",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "התעוררות ברשת LAN",
"wolSent": "Wake-on-LAN packet sent",
"wolFailed": "Failed to send Wake-on-LAN packet",
"ipRequired": "נדרשת כתובת IP",
"portRequired": "נדרש פורט",
"port": "נָמָל",
"name": "שֵׁם",
"username": "שם משתמש",
"usernameRequired": "נדרש שם משתמש (SSH/Telnet בלבד)",
"folder": "תיקייה",
"tags": "תגיות",
"pin": "פִּין",
@@ -979,6 +1040,8 @@
"tunnel": "מִנהָרָה",
"fileManager": "מנהל הקבצים",
"serverStats": "סטטיסטיקות שרת",
"status": "סטָטוּס",
"statistics": "סטָטִיסטִיקָה",
"hostViewer": "צופה מארח",
"enableServerStats": "הפעל סטטיסטיקות שרת",
"enableServerStatsDesc": "הפעלה/השבתה של איסוף סטטיסטיקות שרת עבור מארח זה",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "גרור כדי לעבור בין תיקיות",
"exportedHostConfig": "ייצוא תצורת מארח עבור {{name}}",
"openTerminal": "פתח את הטרמינל",
"openRdp": "Open RDP",
"openVnc": "Open VNC",
"openTelnet": "Open Telnet",
"openFileManager": "פתח את מנהל הקבצים",
"openTunnels": "מנהרות פתוחות",
"openServerDetails": "פתיחת פרטי שרת",
"statistics": "סטָטִיסטִיקָה",
"enabledWidgets": "ווידג'טים מופעלים",
"openServerStats": "סטטיסטיקות שרת פתוחות",
"enabledWidgetsDesc": "בחר אילו ווידג'טים של סטטיסטיקות להציג עבור מארח זה",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "בדוק אם השרת מחובר או לא מחובר",
"statusCheckInterval": "מרווח זמן לבדיקת סטטוס",
"statusCheckIntervalDesc": "באיזו תדירות לבדוק אם המארח מחובר (5 שניות - שעה)",
"statusChecks": "בדיקות סטטוס",
"disableTcpPing": "השבתת פינג של TCP",
"disableTcpPingDescription": "כבה בדיקות סטטוס (זיהוי מקוון/לא מקוון) עבור מארח זה",
"useGlobalStatusInterval": "השתמש בברירת מחדל גלובלית",
"useGlobalMetricsInterval": "השתמש בברירת מחדל גלובלית",
"usingGlobalDefault": "שימוש בברירת מחדל גלובלית ({{value}}s)",
"metricsCollection": "אוסף מדדים",
"metricsEnabled": "הפעל ניטור מדדים",
"metricsEnabledDesc": "איסוף סטטיסטיקות של מעבד, זיכרון RAM, דיסק ונתונים סטטיסטיים אחרים של המערכת",
"metricsInterval": "מרווח איסוף מדדים",
"metricsIntervalDesc": "באיזו תדירות לאסוף סטטיסטיקות שרת (5 שניות - שעה)",
"metricsNotAvailableForConnectionType": "מדדי שרת זמינים רק עבור מארחי SSH. עדיין ניתן להפעיל בדיקות סטטוס פינג של TCP למעלה.",
"intervalSeconds": "שניות",
"intervalMinutes": "פּרוֹטוֹקוֹל",
"intervalValidation": "מרווחי הניטור חייבים להיות בין 5 שניות לשעה (3600 שניות)",
@@ -1133,6 +1203,10 @@
"noServerFound": "לא נמצא שרת",
"jumpHostsOrder": "החיבורים יתבצעו לפי הסדר: קפיצה למארח 1 → קפיצה למארח 2 → ... → שרת יעד",
"socks5Proxy": "פרוקסי SOCKS5",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Send a sequence of connection attempts to specified ports before connecting. Used to open firewall rules dynamically.",
"addKnock": "Add Port",
"delayMs": "Delay",
"socks5Description": "הגדר את פרוקסי SOCKS5 עבור חיבור SSH. כל התעבורה תנותב דרך שרת הפרוקסי שצוין.",
"enableSocks5": "הפעל את פרוקסי SOCKS5",
"enableSocks5Description": "השתמש בפרוקסי SOCKS5 עבור חיבור SSH זה",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "הפעלת פקודת MOSH באופן אוטומטי בעת חיבור",
"moshCommand": "פיקוד MOSH",
"moshCommandDesc": "פקודת MOSH לביצוע",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automatically attach to an existing (or start a new) tmux session for persistent sessions and cross-device continuity",
"environmentVariables": "משתני סביבה",
"environmentVariablesDesc": "הגדר משתני סביבה מותאמים אישית עבור סשן הטרמינל",
"variableName": "שם משתנה",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "העתקת כתובת URL של המנהרה",
"copyServerStatsUrl": "העתקת כתובת URL של סטטיסטיקות שרת",
"copyDockerUrl": "העתקת כתובת URL של Docker",
"copyRemoteDesktopUrl": "העתקת כתובת URL של שולחן עבודה מרוחק",
"fullScreenUrlTooltip": "העתק כתובת URL כדי לפתוח אפליקציה זו במצב מסך מלא",
"notEnabled": "Docker אינו מופעל עבור מארח זה. הפעל אותו בהגדרות המארח כדי להשתמש בתכונות Docker.",
"validating": "מאמת את Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "בחר הכל",
"deselectAll": "בטל את הבחירה של הכל",
"useGlobalStatusDefault": "השתמש בברירת מחדל גלובלית (סטטוס)",
"useGlobalMetricsDefault": "השתמש בברירת מחדל גלובלית (מדדים)"
"useGlobalMetricsDefault": "השתמש בברירת מחדל גלובלית (מדדים)",
"remoteDesktopSettings": "הגדרות שולחן עבודה מרוחק",
"domain": "תְחוּם",
"securityMode": "מצב אבטחה",
"ignoreCert": "התעלם מהתעודה",
"ignoreCertDesc": "דלג על אימות אישור TLS עבור חיבור זה",
"displaySettings": "הגדרות תצוגה",
"colorDepth": "עומק צבע",
"width": "רוֹחַב",
"height": "גוֹבַה",
"dpi": "DPI",
"resizeMethod": "שיטת שינוי גודל",
"forceLossless": "כפיית אובדן נתונים",
"audioSettings": "הגדרות שמע",
"disableAudio": "השבתת שמע",
"enableAudioInput": "הפעל קלט שמע",
"rdpPerformance": "ביצועים",
"enableWallpaper": "הפעל טפט",
"enableTheming": "הפעלת ערכות נושא",
"enableFontSmoothing": "הפעל החלקת גופנים",
"enableFullWindowDrag": "הפעל גרירה של חלון מלא",
"enableDesktopComposition": "הפעלת קומפוזיציה בשולחן העבודה",
"enableMenuAnimations": "הפעל אנימציות תפריט",
"disableBitmapCaching": "השבתת אחסון במטמון של מפת סיביות",
"disableOffscreenCaching": "השבתת אחסון במטמון מחוץ למסך",
"disableGlyphCaching": "השבתת אחסון גליפים במטמון",
"enableGfx": "הפעלת GFX",
"deviceRedirection": "ניתוב מחדש של מכשירים",
"enablePrinting": "הפעל הדפסה",
"enableDrive": "הפעל ניתוב מחדש של כונן",
"driveName": "שם הכונן",
"drivePath": "נתיב הנסיעה",
"createDrivePath": "צור נתיב כונן",
"disableDownload": "השבתת הורדה",
"disableUpload": "השבתת העלאה",
"enableTouch": "הפעל מגע",
"rdpSession": "מוֹשָׁב",
"clientName": "שם הלקוח",
"consoleSession": "סשן קונסולה",
"initialProgram": "תוכנית ראשונית",
"serverLayout": "פריסת מקלדת השרת",
"timezone": "אזור זמן",
"gatewaySettings": "כְּנִיסָה",
"gatewayHostname": "שם מארח השער",
"gatewayPort": "שער היציאה",
"gatewayUsername": "שם משתמש של השער",
"gatewayPassword": "סיסמת השער",
"gatewayDomain": "דומיין שער",
"remoteApp": "אפליקציה מרחוק",
"remoteAppProgram": "יישום מרחוק",
"remoteAppDir": "ספריית אפליקציות מרוחקות",
"remoteAppArgs": "ארגומנטים של אפליקציה מרוחקת",
"clipboardSettings": "לוח גזירה",
"normalizeClipboard": "נרמול לוח הגזירים",
"disableCopy": "השבתת העתקה",
"disablePaste": "השבתת הדבקה",
"vncSettings": "הגדרות VNC",
"cursorMode": "מצב סמן",
"swapRedBlue": "החלפת אדום/כחול",
"readOnly": "קריאה בלבד",
"recordingSettings": "הַקלָטָה",
"recordingPath": "נתיב ההקלטה",
"recordingName": "שם ההקלטה",
"createRecordingPath": "צור נתיב הקלטה",
"excludeOutput": "אי הכללת פלט",
"excludeMouse": "אי הכללת עכבר",
"includeKeys": "כלול מפתחות",
"sendWolPacket": "שלח חבילת WoL",
"wolMacAddr": "כתובת MAC",
"wolBroadcastAddr": "כתובת שידור",
"wolUdpPort": "יציאת UDP",
"wolWaitTime": "זמן המתנה (שניות)",
"connectionSettings": "הגדרות חיבור",
"rdpOnly": "RDP בלבד",
"vncOnly": "VNC בלבד",
"telnetTerminalSettings": "הגדרות מסוף",
"terminalType": "סוג הטרמינל",
"guacFontName": "שם הגופן",
"guacFontSize": "גודל גופן",
"guacColorScheme": "ערכת צבעים",
"guacBackspaceKey": "מקש Backspace"
},
"guacamole": {
"connecting": "מתחבר לסשן {{type}}...",
"rdpConnecting": "מתחבר לשרת RDP...",
"vncConnecting": "מתחבר לשרת VNC...",
"telnetConnecting": "מתחבר לשרת טלנט...",
"connectionError": "שגיאת חיבור",
"connectionFailed": "החיבור נכשל",
"failedToConnect": "נכשל בקבלת אסימון החיבור"
},
"terminal": {
"title": "מָסוֹף",
@@ -1364,7 +1530,7 @@
"closePanel": "סגור את הפאנל",
"reconnect": "התחבר מחדש",
"sessionEnded": "הסשן הסתיים",
"connectionLost": "החיבור אבד",
"connectionLost": "Connection lost",
"error": "שגיאה: {{message}}",
"disconnected": "מְנוּתָק",
"connectionClosed": "החיבור נסגר",
@@ -1372,6 +1538,7 @@
"connected": "מְחוּבָּר",
"clipboardWriteFailed": "ההעתקה ללוח נכשלה. ודא שהדף מוגש דרך HTTPS או localhost.",
"clipboardReadFailed": "הקריאה מהלוח נכשלה. ודא שהרשאות הלוח ניתנות.",
"clipboardHttpWarning": "הדבקה דורשת HTTPS. השתמשו ב-Ctrl+Shift+V או הגשת Termix דרך HTTPS.",
"sshConnected": "נוצר חיבור SSH",
"authError": "האימות נכשל: {{message}}",
"unknownError": "אירעה שגיאה לא ידועה",
@@ -1380,7 +1547,25 @@
"connecting": "מְקַשֵׁר...",
"reconnecting": "מתחבר מחדש... ({{attempt}}/{{max}})",
"reconnected": "התחבר מחדש בהצלחה",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.",
"tmuxWindows": "Windows",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Attached clients",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "Last activity",
"tmuxTimeJustNow": "just now",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Start new session",
"tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard",
"maxReconnectAttemptsReached": "הגעת למספר המקסימלי של ניסיונות חיבור מחדש",
"closeTab": "Close",
"connectionTimeout": "זמן קצוב לחיבור",
"terminalTitle": "טרמינל - {{host}}",
"terminalWithPath": "טרמינל - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "הזמן שהוקצב לאימות הסתיים. אנא נסה שוב.",
"opksshAuthFailed": "האימות נכשל. אנא בדוק את פרטי הגישה שלך ונסה שוב.",
"opksshConfigMissing": "תצורת OPKSSH לא נמצאה. אנא צור ~/.opk/config.yml עם הגדרות ספק ה-OIDC שלך. עיין בתיעוד: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Sign in with {{provider}}",
"sudoPasswordPopupTitle": "להכניס סיסמה?",
"sudoPasswordPopupHint": "לחץ על Enter כדי להוסיף, Esc כדי לסגור",
"sudoPasswordPopupConfirm": "לְהַכנִיס",
@@ -1428,7 +1614,8 @@
"connectionRejected": "החיבור נדחה על ידי השרת. אנא בדוק את האימות ותצורת הרשת שלך.",
"hostKeyRejected": "אימות מפתח מארח SSH נדחה. החיבור בוטל.",
"sessionTakenOver": "הסשן נפתח בכרטיסייה אחרת. מתחבר מחדש...",
"sessionAttachTimeout": "תם הזמן שהוקצב לקובץ המצורף לסשן. יצירת חיבור חדש..."
"sessionAttachTimeout": "תם הזמן שהוקצב לקובץ המצורף לסשן. יצירת חיבור חדש...",
"openFileManagerHere": "Open File Manager Here"
},
"fileManager": {
"title": "מנהל הקבצים",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "קידוד צבע של קבצים לפי סוג: תיקיות (אדום), קבצים (כחול), קישורים סימבוליים (ירוק)",
"commandAutocomplete": "השלמה אוטומטית של הפקודה",
"commandAutocompleteDesc": "הפעל הצעות להשלמה אוטומטית של מקש Tab עבור פקודות מסוף בהתבסס על היסטוריית הפקודות שלך",
"commandHistoryTracking": "Save Command History",
"commandHistoryTrackingDesc": "Store terminal commands in history for autocomplete and sidebar. Disabled by default for privacy.",
"defaultSnippetFoldersCollapsed": "כיווץ תיקיות קטעי טקסט כברירת מחדל",
"defaultSnippetFoldersCollapsedDesc": "כאשר הפעולה מופעלת, כל תיקיות הקטעים יכוסו כשתפתחו את לשונית הקטעים",
"terminalSyntaxHighlighting": "הדגשת תחביר הטרמינל",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "סמן אוטומטית פקודות, נתיבים, כתובות IP ורמות יומן בפלט הטרמינל",
"enableCommandPaletteShortcut": "הפעל קיצור דרך של לוח הפקודות",
"enableCommandPaletteShortcutDesc": "הקש פעמיים על Shift שמאלי כדי לפתוח את לוח הפקודות לגישה מהירה למארחים",
"enableTerminalSessionPersistence": "הפעלות טרמינל מתמשכות",
"enableTerminalSessionPersistence": "כרטיסיות/הפעלות קבועות",
"enableTerminalSessionPersistenceDesc": "שמרו על חיבורי SSH בעת מעבר בין כרטיסיות או סגירת הדפדפן (ייתכן שזה לא יציב)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "מסד נתונים",
"healthy": "בָּרִיא",
"error": "שְׁגִיאָה",
"totalServers": "סך השרתים",
"totalHosts": "Total Hosts",
"totalTunnels": "סך המנהרות",
"totalCredentials": "סך כל האישורים",
"recentActivity": "פעילות אחרונה",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "इस अंश को क्लिपबोर्ड पर कॉपी करें",
"editTooltip": "इस अंश को संपादित करें",
"deleteTooltip": "इस अंश को हटा दें",
"shareTooltip": "Share this snippet",
"sharedWithYou": "Shared",
"sharedBy": "Shared by {{username}}",
"shareSnippet": "Share Snippet",
"user": "User",
"role": "Role",
"selectTarget": "Select...",
"currentAccess": "Current Access",
"shareSuccess": "Snippet shared successfully",
"shareFailed": "Failed to share snippet",
"revokeSuccess": "Access revoked",
"revokeFailed": "Failed to revoke access",
"failedToLoadShareData": "Failed to load sharing data",
"newFolder": "नया फ़ोल्डर",
"reorderSameFolder": "केवल एक ही फ़ोल्डर के भीतर स्निपेट्स को ही पुनर्व्यवस्थित किया जा सकता है",
"reorderSuccess": "स्निपेट्स को सफलतापूर्वक पुनर्व्यवस्थित कर दिया गया है।",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "प्रमाणीकरण आवश्यक है। कृपया पृष्ठ को रीफ़्रेश करें।",
"dataAccessLockedReauth": "डेटा एक्सेस लॉक हो गया है। कृपया पुनः प्रमाणीकरण करें।",
"loading": "कमांड इतिहास लोड हो रहा है...",
"error": "इतिहास लोड करने में त्रुटि"
"error": "इतिहास लोड करने में त्रुटि",
"disabledTitle": "Command History is Disabled",
"disabledDescription": "Enable Command History Tracking in your profile under Appearance settings."
},
"splitScreen": {
"title": "स्प्लिट स्क्रीन",
@@ -510,6 +525,8 @@
"checkingDatabase": "डेटाबेस कनेक्शन की जाँच की जा रही है...",
"checkingAuthentication": "प्रमाणीकरण की जाँच की जा रही है...",
"backendReconnected": "सर्वर कनेक्शन बहाल हो गया",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "कार्रवाई",
"remove": "निकालना",
"revoke": "रद्द करना",
@@ -541,7 +558,8 @@
"copySudoPassword": "सूडो पासवर्ड कॉपी करें",
"passwordCopied": "पासवर्ड क्लिपबोर्ड पर कॉपी हो गया है",
"sudoPasswordCopied": "सूडो पासवर्ड क्लिपबोर्ड पर कॉपी हो गया है",
"noPasswordAvailable": "कोई पासवर्ड उपलब्ध नहीं है"
"noPasswordAvailable": "कोई पासवर्ड उपलब्ध नहीं है",
"failedToCopyPassword": "Failed to copy password"
},
"admin": {
"title": "व्यवस्थापक सेटिंग्स",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "वैश्विक निगरानी सेटिंग्स सहेजी गईं",
"failedToSaveGlobalSettings": "वैश्विक निगरानी सेटिंग्स को सहेजने में विफल।",
"failedToLoadGlobalSettings": "वैश्विक निगरानी सेटिंग्स लोड करने में विफल",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Configure how long user sessions last before requiring re-authentication. 'Remember Me' sessions are unaffected (always 30 days).",
"sessionTimeoutHours": "Timeout Duration",
"hours": "hours",
"sessionTimeoutNote": "Valid range: 1720 hours. Changes apply to new sessions only.",
"sessionTimeoutSaved": "Session timeout saved",
"failedToSaveSessionTimeout": "Failed to save session timeout",
"guacamoleIntegration": "रिमोट डेस्कटॉप इंटीग्रेशन (गुआकामोल)",
"guacamoleIntegrationDesc": "guacd के माध्यम से RDP, VNC और Telnet कनेक्शन सक्षम करें। इसके लिए guacd का एक इंस्टेंस चालू होना आवश्यक है।",
"enableGuacamole": "RDP/VNC/Telnet समर्थन सक्षम करें",
"guacdUrl": "गुआकडी यूआरएल (होस्ट:पोर्ट)",
"guacdUrlPlaceholder": "गुआकडी:4822",
"guacdUrlNote": "guacd होस्ट/पोर्ट में किए गए बदलावों को प्रभावी होने के लिए सर्वर को रीस्टार्ट करना आवश्यक है।",
"guacamoleSettingsSaved": "गुआकामोल की सेटिंग सहेज ली गई",
"failedToSaveGuacamoleSettings": "गुआकामोल सेटिंग्स को सहेजने में विफल।",
"failedToLoadGuacamoleSettings": "गुआकामोल सेटिंग्स लोड करने में विफल",
"logLevel": "Log Level",
"logLevelDesc": "Control the verbosity of server logs. Higher levels reduce log output. Can also be set via the LOG_LEVEL environment variable.",
"logVerbosity": "Verbosity",
"logLevelNote": "Changes take effect immediately. Debug level may impact performance.",
"logLevelSaved": "Log level saved",
"failedToSaveLogLevel": "Failed to save log level",
"clampedToValidRange": "वैध सीमा के अनुसार समायोजित किया गया",
"loadingSessions": "सेशन लोड हो रहे हैं...",
"noActiveSessions": "कोई सक्रिय सत्र नहीं मिला।",
"device": "उपकरण",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} मेजबान",
"importJson": "JSON आयात करें",
"importing": "आयात हो रहा है...",
"exportAllJson": "Export All",
"exporting": "Exporting...",
"exportedAllHosts": "Exported {{count}} hosts",
"failedToExportAllHosts": "Failed to export hosts. Please ensure you're logged in and have access to the host data.",
"exportAllSensitiveWarning": "The exported file will contain sensitive authentication data (passwords/SSH keys) in plaintext. Please keep the file secure and delete it after use.",
"importJsonTitle": "JSON से SSH होस्ट आयात करें",
"importJsonDesc": "एक JSON फ़ाइल अपलोड करके कई SSH होस्ट (अधिकतम 100) को बल्क में इम्पोर्ट करें।",
"downloadSample": "नमूना डाउनलोड करें",
@@ -866,11 +912,26 @@
"importError": "आयात त्रुटि",
"failedToImportJson": "JSON फ़ाइल आयात करने में विफल",
"connectionDetails": "कनेक्शन विवरण",
"connectionType": "रिश्ते का प्रकार",
"ssh": "एसएसएच",
"rdp": "आरडीपी",
"vnc": "वीएनसी",
"telnet": "टेलनेट",
"remoteDesktop": "दूरवर्ती डेस्कटॉप",
"guacamoleSettings": "रिमोट डेस्कटॉप सेटिंग्स",
"organization": "संगठन",
"ipAddress": "आईपी पता या होस्टनाम",
"macAddress": "MAC Address",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "लैन पर जागो",
"wolSent": "Wake-on-LAN packet sent",
"wolFailed": "Failed to send Wake-on-LAN packet",
"ipRequired": "आईपी पते की आवश्यकता है",
"portRequired": "पोर्ट आवश्यक है",
"port": "पत्तन",
"name": "नाम",
"username": "उपयोगकर्ता नाम",
"usernameRequired": "उपयोगकर्ता नाम आवश्यक है (केवल SSH/Telnet के लिए)",
"folder": "फ़ोल्डर",
"tags": "टैग",
"pin": "नत्थी करना",
@@ -979,6 +1040,8 @@
"tunnel": "सुरंग",
"fileManager": "फ़ाइल मैनेजर",
"serverStats": "सर्वर आँकड़े",
"status": "स्थिति",
"statistics": "आंकड़े",
"hostViewer": "मेज़बान दर्शक",
"enableServerStats": "सर्वर सांख्यिकी सक्षम करें",
"enableServerStatsDesc": "इस होस्ट के लिए सर्वर सांख्यिकी संग्रह को सक्षम/अक्षम करें",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "फ़ोल्डरों के बीच जाने के लिए ड्रैग करें",
"exportedHostConfig": "{{name}}के लिए निर्यातित होस्ट कॉन्फ़िगरेशन",
"openTerminal": "टर्मिनल खोलें",
"openRdp": "Open RDP",
"openVnc": "Open VNC",
"openTelnet": "Open Telnet",
"openFileManager": "फ़ाइल प्रबंधक खोलें",
"openTunnels": "खुली सुरंगें",
"openServerDetails": "सर्वर विवरण खोलें",
"statistics": "आंकड़े",
"enabledWidgets": "सक्षम विजेट",
"openServerStats": "ओपन सर्वर आँकड़े",
"enabledWidgetsDesc": "इस होस्ट के लिए कौन से सांख्यिकी विजेट प्रदर्शित करने हैं, यह चुनें।",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "जांचें कि सर्वर ऑनलाइन है या ऑफलाइन।",
"statusCheckInterval": "स्थिति जांच अंतराल",
"statusCheckIntervalDesc": "होस्ट ऑनलाइन है या नहीं, यह कितनी बार जांचना चाहिए (5 सेकंड - 1 घंटा)",
"statusChecks": "स्थिति जांच",
"disableTcpPing": "TCP पिंग को अक्षम करें",
"disableTcpPingDescription": "इस होस्ट के लिए स्टेटस चेक (ऑनलाइन/ऑफलाइन पहचान) बंद करें",
"useGlobalStatusInterval": "वैश्विक डिफ़ॉल्ट का उपयोग करें",
"useGlobalMetricsInterval": "वैश्विक डिफ़ॉल्ट का उपयोग करें",
"usingGlobalDefault": "वैश्विक डिफ़ॉल्ट का उपयोग करना ({{value}}s)",
"metricsCollection": "मैट्रिक्स संग्रह",
"metricsEnabled": "मैट्रिक्स मॉनिटरिंग सक्षम करें",
"metricsEnabledDesc": "सीपीयू, रैम, डिस्क और अन्य सिस्टम सांख्यिकी एकत्र करें",
"metricsInterval": "मैट्रिक्स संग्रह अंतराल",
"metricsIntervalDesc": "सर्वर सांख्यिकी को कितनी बार एकत्र करना है (5 सेकंड - 1 घंटा)",
"metricsNotAvailableForConnectionType": "सर्वर मेट्रिक्स केवल SSH होस्ट के लिए उपलब्ध हैं। TCP पिंग स्टेटस चेक को ऊपर दिए गए विकल्प से चालू किया जा सकता है।",
"intervalSeconds": "सेकंड",
"intervalMinutes": "मिनट",
"intervalValidation": "निगरानी अंतराल 5 सेकंड और 1 घंटे (3600 सेकंड) के बीच होना चाहिए।",
@@ -1133,6 +1203,10 @@
"noServerFound": "कोई सर्वर नहीं मिला",
"jumpHostsOrder": "कनेक्शन इस क्रम में स्थापित किए जाएंगे: जंप होस्ट 1 → जंप होस्ट 2 → ... → लक्ष्य सर्वर",
"socks5Proxy": "SOCKS5 प्रॉक्सी",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Send a sequence of connection attempts to specified ports before connecting. Used to open firewall rules dynamically.",
"addKnock": "Add Port",
"delayMs": "Delay",
"socks5Description": "SSH कनेक्शन के लिए SOCKS5 प्रॉक्सी कॉन्फ़िगर करें। सभी ट्रैफ़िक निर्दिष्ट प्रॉक्सी सर्वर के माध्यम से रूट किया जाएगा।",
"enableSocks5": "SOCKS5 प्रॉक्सी को सक्षम करें",
"enableSocks5Description": "इस SSH कनेक्शन के लिए SOCKS5 प्रॉक्सी का उपयोग करें",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "कनेक्ट होने पर MOSH कमांड स्वचालित रूप से चलाएँ",
"moshCommand": "MOSH कमांड",
"moshCommandDesc": "MOSH कमांड को निष्पादित करने के लिए",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automatically attach to an existing (or start a new) tmux session for persistent sessions and cross-device continuity",
"environmentVariables": "पर्यावरण चर",
"environmentVariablesDesc": "टर्मिनल सत्र के लिए कस्टम पर्यावरण चर सेट करें",
"variableName": "चर का नाम",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "टनल यूआरएल कॉपी करें",
"copyServerStatsUrl": "सर्वर सांख्यिकी यूआरएल कॉपी करें",
"copyDockerUrl": "डॉकर यूआरएल कॉपी करें",
"copyRemoteDesktopUrl": "रिमोट डेस्कटॉप यूआरएल कॉपी करें",
"fullScreenUrlTooltip": "इस ऐप को फुल-स्क्रीन मोड में खोलने के लिए URL कॉपी करें",
"notEnabled": "इस होस्ट के लिए डॉकर सक्षम नहीं है। डॉकर सुविधाओं का उपयोग करने के लिए इसे होस्ट सेटिंग्स में सक्षम करें।",
"validating": "डॉकर का सत्यापन किया जा रहा है...",
@@ -1348,7 +1425,96 @@
"selectAll": "सबका चयन करें",
"deselectAll": "सबको अचयनित करो",
"useGlobalStatusDefault": "वैश्विक डिफ़ॉल्ट (स्थिति) का उपयोग करें",
"useGlobalMetricsDefault": "वैश्विक डिफ़ॉल्ट (मैट्रिक्स) का उपयोग करें"
"useGlobalMetricsDefault": "वैश्विक डिफ़ॉल्ट (मैट्रिक्स) का उपयोग करें",
"remoteDesktopSettings": "रिमोट डेस्कटॉप सेटिंग्स",
"domain": "कार्यक्षेत्र",
"securityMode": "सुरक्षा मोड",
"ignoreCert": "प्रमाणपत्र को अनदेखा करें",
"ignoreCertDesc": "इस कनेक्शन के लिए TLS प्रमाणपत्र सत्यापन छोड़ें",
"displaySettings": "प्रदर्शन सेटिंग्स",
"colorDepth": "रंग की गहराई",
"width": "चौड़ाई",
"height": "ऊंचाई",
"dpi": "डीपीआई",
"resizeMethod": "आकार बदलने की विधि",
"forceLossless": "बल हानि रहित",
"audioSettings": "श्रव्य विन्यास",
"disableAudio": "ऑडियो अक्षम करें",
"enableAudioInput": "ऑडियो इनपुट सक्षम करें",
"rdpPerformance": "प्रदर्शन",
"enableWallpaper": "वॉलपेपर सक्षम करें",
"enableTheming": "थीमिंग सक्षम करें",
"enableFontSmoothing": "फ़ॉन्ट स्मूथिंग सक्षम करें",
"enableFullWindowDrag": "विंडो को पूरी तरह से ड्रैग करने की सुविधा चालू करें",
"enableDesktopComposition": "डेस्कटॉप कंपोज़िशन को सक्षम करें",
"enableMenuAnimations": "मेनू एनिमेशन सक्षम करें",
"disableBitmapCaching": "बिटमैप कैशिंग को अक्षम करें",
"disableOffscreenCaching": "ऑफस्क्रीन कैशिंग को अक्षम करें",
"disableGlyphCaching": "ग्लिफ़ कैशिंग को अक्षम करें",
"enableGfx": "जीएफएक्स सक्षम करें",
"deviceRedirection": "डिवाइस पुनर्निर्देशन",
"enablePrinting": "मुद्रण सक्षम करें",
"enableDrive": "ड्राइव रीडायरेक्शन सक्षम करें",
"driveName": "ड्राइव का नाम",
"drivePath": "ड्राइव पथ",
"createDrivePath": "ड्राइव पथ बनाएँ",
"disableDownload": "डाउनलोड अक्षम करें",
"disableUpload": "अपलोड अक्षम करें",
"enableTouch": "टच सक्षम करें",
"rdpSession": "सत्र",
"clientName": "ग्राहक नाम",
"consoleSession": "कंसोल सत्र",
"initialProgram": "प्रारंभिक कार्यक्रम",
"serverLayout": "सर्वर कीबोर्ड लेआउट",
"timezone": "समय क्षेत्र",
"gatewaySettings": "द्वार",
"gatewayHostname": "गेटवे होस्टनाम",
"gatewayPort": "गेटवे पोर्ट",
"gatewayUsername": "गेटवे उपयोगकर्ता नाम",
"gatewayPassword": "गेटवे पासवर्ड",
"gatewayDomain": "गेटवे डोमेन",
"remoteApp": "रिमोटऐप",
"remoteAppProgram": "रिमोट एप्लिकेशन",
"remoteAppDir": "रिमोट ऐप निर्देशिका",
"remoteAppArgs": "रिमोट ऐप तर्क",
"clipboardSettings": "क्लिपबोर्ड",
"normalizeClipboard": "क्लिपबोर्ड को सामान्य करें",
"disableCopy": "कॉपी अक्षम करें",
"disablePaste": "पेस्ट अक्षम करें",
"vncSettings": "वीएनसी सेटिंग्स",
"cursorMode": "कर्सर मोड",
"swapRedBlue": "लाल/नीले रंग की अदला-बदली करें",
"readOnly": "केवल पढ़ने के लिए",
"recordingSettings": "रिकॉर्डिंग",
"recordingPath": "रिकॉर्डिंग पथ",
"recordingName": "रिकॉर्डिंग नाम",
"createRecordingPath": "रिकॉर्डिंग पथ बनाएँ",
"excludeOutput": "आउटपुट को बाहर रखें",
"excludeMouse": "माउस को बाहर रखें",
"includeKeys": "चाबियाँ शामिल करें",
"sendWolPacket": "WoL पैकेट भेजें",
"wolMacAddr": "मैक पता",
"wolBroadcastAddr": "ब्रॉडकास्ट पता",
"wolUdpPort": "यूडीपी पोर्ट",
"wolWaitTime": "प्रतीक्षा समय (सेकंड में)",
"connectionSettings": "कनेक्शन सेटिंग्स",
"rdpOnly": "केवल आरडीपी",
"vncOnly": "केवल वीएनसी",
"telnetTerminalSettings": "टर्मिनल सेटिंग्स",
"terminalType": "टर्मिनल प्रकार",
"guacFontName": "फ़ॉन्ट नाम",
"guacFontSize": "फ़ॉन्ट आकार",
"guacColorScheme": "रंग योजना",
"guacBackspaceKey": "बैकस्पेस कुंजी"
},
"guacamole": {
"connecting": "{{type}} सत्र से कनेक्ट हो रहा है...",
"rdpConnecting": "आरडीपी सर्वर से कनेक्ट हो रहा है...",
"vncConnecting": "VNC सर्वर से कनेक्ट हो रहा है...",
"telnetConnecting": "टेलनेट सर्वर से कनेक्ट हो रहा है...",
"connectionError": "संपर्क त्रुटि",
"connectionFailed": "कनेक्शन विफल",
"failedToConnect": "कनेक्शन टोकन प्राप्त करने में विफल"
},
"terminal": {
"title": "टर्मिनल",
@@ -1364,7 +1530,7 @@
"closePanel": "पैनल बंद करें",
"reconnect": "रिकनेक्ट",
"sessionEnded": "सत्र समाप्त हुआ",
"connectionLost": "कनेक्शन टूट गया",
"connectionLost": "Connection lost",
"error": "त्रुटि: {{message}}",
"disconnected": "डिस्कनेक्ट किया गया",
"connectionClosed": "कनेक्शन बंद हो गया",
@@ -1372,6 +1538,7 @@
"connected": "जुड़े हुए",
"clipboardWriteFailed": "क्लिपबोर्ड पर कॉपी करने में विफल। सुनिश्चित करें कि पृष्ठ HTTPS या लोकलहोस्ट पर चल रहा है।",
"clipboardReadFailed": "क्लिपबोर्ड से पढ़ने में विफलता। सुनिश्चित करें कि क्लिपबोर्ड की अनुमतियाँ दी गई हैं।",
"clipboardHttpWarning": "पेस्ट करने के लिए HTTPS आवश्यक है। Ctrl+Shift+V का उपयोग करें या HTTPS पर Termix को सर्व करें।",
"sshConnected": "SSH कनेक्शन स्थापित हो गया",
"authError": "प्रमाणीकरण विफल: {{message}}",
"unknownError": "अज्ञात त्रुटि उत्पन्न हुई",
@@ -1380,7 +1547,25 @@
"connecting": "कनेक्ट हो रहा है...",
"reconnecting": "पुनः कनेक्ट हो रहा है... ({{attempt}}/{{max}})",
"reconnected": "सफलतापूर्वक पुनः कनेक्ट हो गया",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.",
"tmuxWindows": "Windows",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Attached clients",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "Last activity",
"tmuxTimeJustNow": "just now",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Start new session",
"tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard",
"maxReconnectAttemptsReached": "अधिकतम पुनः कनेक्शन प्रयासों की सीमा पूरी हो गई है।",
"closeTab": "Close",
"connectionTimeout": "रिश्तों का समय बाहर",
"terminalTitle": "टर्मिनल - {{host}}",
"terminalWithPath": "टर्मिनल - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "प्रमाणीकरण का समय समाप्त हो गया। कृपया पुनः प्रयास करें।",
"opksshAuthFailed": "प्रमाणीकरण विफल रहा। कृपया अपनी पहचान सत्यापित करें और पुनः प्रयास करें।",
"opksshConfigMissing": "OPKSSH कॉन्फ़िगरेशन नहीं मिला। कृपया अपने OIDC प्रदाता सेटिंग्स के साथ ~/.opk/config.yml फ़ाइल बनाएँ। दस्तावेज़ देखें: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Sign in with {{provider}}",
"sudoPasswordPopupTitle": "पासवर्ड डालें?",
"sudoPasswordPopupHint": "सम्मिलित करने के लिए Enter दबाएँ, हटाने के लिए Esc दबाएँ",
"sudoPasswordPopupConfirm": "डालना",
@@ -1428,7 +1614,8 @@
"connectionRejected": "सर्वर द्वारा कनेक्शन अस्वीकृत कर दिया गया है। कृपया अपनी प्रमाणीकरण और नेटवर्क कॉन्फ़िगरेशन की जाँच करें।",
"hostKeyRejected": "SSH होस्ट कुंजी सत्यापन अस्वीकृत। कनेक्शन रद्द।",
"sessionTakenOver": "सेशन दूसरे टैब में खुल गया था। पुनः कनेक्ट हो रहा है...",
"sessionAttachTimeout": "सेशन अटैचमेंट का समय समाप्त हो गया। नया कनेक्शन बनाया जा रहा है..."
"sessionAttachTimeout": "सेशन अटैचमेंट का समय समाप्त हो गया। नया कनेक्शन बनाया जा रहा है...",
"openFileManagerHere": "Open File Manager Here"
},
"fileManager": {
"title": "फ़ाइल मैनेजर",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "फ़ाइलों को उनके प्रकार के अनुसार रंग-कोडित करें: फ़ोल्डर (लाल), फ़ाइलें (नीला), सिम्लिंक (हरा)",
"commandAutocomplete": "कमांड ऑटो-कंप्लीट",
"commandAutocompleteDesc": "अपने कमांड इतिहास के आधार पर टर्मिनल कमांड के लिए टैब कुंजी स्वतः पूर्ण होने के सुझाव सक्षम करें",
"commandHistoryTracking": "Save Command History",
"commandHistoryTrackingDesc": "Store terminal commands in history for autocomplete and sidebar. Disabled by default for privacy.",
"defaultSnippetFoldersCollapsed": "डिफ़ॉल्ट रूप से स्निपेट फ़ोल्डर को संक्षिप्त करें",
"defaultSnippetFoldersCollapsedDesc": "इस विकल्प को चालू करने पर, स्निपेट टैब खोलने पर सभी स्निपेट फ़ोल्डर सिकुड़ जाएंगे।",
"terminalSyntaxHighlighting": "टर्मिनल सिंटैक्स हाइलाइटिंग",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "टर्मिनल आउटपुट में कमांड, पाथ, आईपी और लॉग लेवल को स्वचालित रूप से हाइलाइट करें",
"enableCommandPaletteShortcut": "कमांड पैलेट शॉर्टकट सक्षम करें",
"enableCommandPaletteShortcutDesc": "होस्ट तक त्वरित पहुंच के लिए कमांड पैलेट खोलने के लिए बाएं Shift को दो बार टैप करें।",
"enableTerminalSessionPersistence": "निरंतर टर्मिनल सत्र",
"enableTerminalSessionPersistence": "लगातार टैब/सेशन",
"enableTerminalSessionPersistenceDesc": "टैब बदलते समय या ब्राउज़र बंद करते समय SSH कनेक्शन बनाए रखें (यह अस्थिर हो सकता है)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "डेटाबेस",
"healthy": "स्वस्थ",
"error": "गलती",
"totalServers": "कुल सर्वर",
"totalHosts": "Total Hosts",
"totalTunnels": "कुल सुरंगें",
"totalCredentials": "कुल प्रमाण पत्र",
"recentActivity": "हाल की गतिविधि",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Kódrészlet másolása a vágólapra",
"editTooltip": "Kódrészlet szerkesztése",
"deleteTooltip": "Törölje ezt a részletet",
"shareTooltip": "Share this snippet",
"sharedWithYou": "Shared",
"sharedBy": "Shared by {{username}}",
"shareSnippet": "Share Snippet",
"user": "User",
"role": "Role",
"selectTarget": "Select...",
"currentAccess": "Current Access",
"shareSuccess": "Snippet shared successfully",
"shareFailed": "Failed to share snippet",
"revokeSuccess": "Access revoked",
"revokeFailed": "Failed to revoke access",
"failedToLoadShareData": "Failed to load sharing data",
"newFolder": "Új mappa",
"reorderSameFolder": "Csak ugyanazon a mappán belül lehet átrendezni a kódrészleteket",
"reorderSuccess": "A kódrészletek átrendezése sikeresen megtörtént.",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Hitelesítés szükséges. Kérjük, frissítse az oldalt.",
"dataAccessLockedReauth": "Adathozzáférés zárolva. Kérjük, hitelesítse magát újra.",
"loading": "Parancselőzmények betöltése...",
"error": "Hiba az előzmények betöltése során"
"error": "Hiba az előzmények betöltése során",
"disabledTitle": "Command History is Disabled",
"disabledDescription": "Enable Command History Tracking in your profile under Appearance settings."
},
"splitScreen": {
"title": "Osztott képernyő",
@@ -510,6 +525,8 @@
"checkingDatabase": "Adatbázis-kapcsolat ellenőrzése...",
"checkingAuthentication": "Hitelesítés ellenőrzése...",
"backendReconnected": "Szerverkapcsolat helyreállt",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "Műveletek",
"remove": "Eltávolítás",
"revoke": "Visszavonás",
@@ -541,7 +558,8 @@
"copySudoPassword": "Sudo jelszó másolása",
"passwordCopied": "Jelszó a vágólapra másolva",
"sudoPasswordCopied": "A Sudo jelszó a vágólapra másolva",
"noPasswordAvailable": "Nincs elérhető jelszó"
"noPasswordAvailable": "Nincs elérhető jelszó",
"failedToCopyPassword": "Failed to copy password"
},
"admin": {
"title": "Adminisztrátori beállítások",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Globális monitorozási beállítások mentve",
"failedToSaveGlobalSettings": "Nem sikerült menteni a globális megfigyelési beállításokat",
"failedToLoadGlobalSettings": "Nem sikerült betölteni a globális megfigyelési beállításokat",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Configure how long user sessions last before requiring re-authentication. 'Remember Me' sessions are unaffected (always 30 days).",
"sessionTimeoutHours": "Timeout Duration",
"hours": "hours",
"sessionTimeoutNote": "Valid range: 1720 hours. Changes apply to new sessions only.",
"sessionTimeoutSaved": "Session timeout saved",
"failedToSaveSessionTimeout": "Failed to save session timeout",
"guacamoleIntegration": "Távoli asztali integráció (Guacamole)",
"guacamoleIntegrationDesc": "RDP, VNC és Telnet kapcsolatok engedélyezése guacd-n keresztül. Futó guacd példány szükséges hozzá.",
"enableGuacamole": "RDP/VNC/Telnet támogatás engedélyezése",
"guacdUrl": "guacd URL (hoszt:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "A guacd gazdagép/port módosításainak érvénybe léptetéséhez újra kell indítani a szervert.",
"guacamoleSettingsSaved": "Guacamole beállítások mentve",
"failedToSaveGuacamoleSettings": "Nem sikerült menteni a guacamole beállításait",
"failedToLoadGuacamoleSettings": "Nem sikerült betölteni a guacamole beállításait",
"logLevel": "Log Level",
"logLevelDesc": "Control the verbosity of server logs. Higher levels reduce log output. Can also be set via the LOG_LEVEL environment variable.",
"logVerbosity": "Verbosity",
"logLevelNote": "Changes take effect immediately. Debug level may impact performance.",
"logLevelSaved": "Log level saved",
"failedToSaveLogLevel": "Failed to save log level",
"clampedToValidRange": "érvényes tartományra lett beállítva",
"loadingSessions": "Munkamenetek betöltése...",
"noActiveSessions": "Nincsenek aktív munkamenetek.",
"device": "Eszköz",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} gazdagépek",
"importJson": "JSON importálása",
"importing": "Importálás...",
"exportAllJson": "Export All",
"exporting": "Exporting...",
"exportedAllHosts": "Exported {{count}} hosts",
"failedToExportAllHosts": "Failed to export hosts. Please ensure you're logged in and have access to the host data.",
"exportAllSensitiveWarning": "The exported file will contain sensitive authentication data (passwords/SSH keys) in plaintext. Please keep the file secure and delete it after use.",
"importJsonTitle": "SSH-hosztok importálása JSON-ból",
"importJsonDesc": "Töltsön fel egy JSON fájlt több SSH-hoszt tömeges importálásához (maximum 100).",
"downloadSample": "Minta letöltése",
@@ -866,11 +912,26 @@
"importError": "Importálási hiba",
"failedToImportJson": "Nem sikerült importálni a JSON fájlt",
"connectionDetails": "Kapcsolat részletei",
"connectionType": "Kapcsolat típusa",
"ssh": "SSH",
"rdp": "Vidékfejlesztési Program",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Távoli asztal",
"guacamoleSettings": "Távoli asztal beállításai",
"organization": "Szervezet",
"ipAddress": "IP-cím vagy hostnév",
"macAddress": "MAC Address",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Wake-on-LAN packet sent",
"wolFailed": "Failed to send Wake-on-LAN packet",
"ipRequired": "IP-cím megadása kötelező",
"portRequired": "Port megadása kötelező",
"port": "Kikötő",
"name": "Név",
"username": "Felhasználónév",
"usernameRequired": "Felhasználónév megadása kötelező (csak SSH/Telnet)",
"folder": "Mappa",
"tags": "Címkék",
"pin": "Pin",
@@ -979,6 +1040,8 @@
"tunnel": "Alagút",
"fileManager": "Fájlkezelő",
"serverStats": "Szerver statisztikák",
"status": "Állapot",
"statistics": "Statisztika",
"hostViewer": "Gazdagép-megjelenítő",
"enableServerStats": "Szerverstatisztikák engedélyezése",
"enableServerStatsDesc": "Szerverstatisztikák gyűjtésének engedélyezése/letiltása ehhez a gazdagéphez",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Húzással mozoghat a mappák között",
"exportedHostConfig": "Exportált host konfiguráció a következőhöz: {{name}}",
"openTerminal": "Nyissa meg a terminált",
"openRdp": "Open RDP",
"openVnc": "Open VNC",
"openTelnet": "Open Telnet",
"openFileManager": "Fájlkezelő megnyitása",
"openTunnels": "Nyílt alagutak",
"openServerDetails": "Nyissa meg a szerver részleteit",
"statistics": "Statisztika",
"enabledWidgets": "Engedélyezett widgetek",
"openServerStats": "Szerver statisztikák megnyitása",
"enabledWidgetsDesc": "Válassza ki, hogy mely statisztikai widgeteket jelenítse meg ehhez a gazdagéphez",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Ellenőrizd, hogy a szerver online vagy offline állapotban van-e",
"statusCheckInterval": "Állapotellenőrzési intervallum",
"statusCheckIntervalDesc": "Milyen gyakran kell ellenőrizni, hogy a host online van-e (5 másodperc - 1 óra)",
"statusChecks": "Állapotellenőrzések",
"disableTcpPing": "TCP ping letiltása",
"disableTcpPingDescription": "Kapcsolja ki az állapotellenőrzéseket (online/offline észlelés) ennél a gazdagépnél",
"useGlobalStatusInterval": "Globális alapértelmezett érték használata",
"useGlobalMetricsInterval": "Globális alapértelmezett érték használata",
"usingGlobalDefault": "Globális alapértelmezett érték használata ({{value}}s)",
"metricsCollection": "Metrikagyűjtemény",
"metricsEnabled": "Metrikák monitorozásának engedélyezése",
"metricsEnabledDesc": "CPU-, RAM-, lemez- és egyéb rendszerstatisztikák gyűjtése",
"metricsInterval": "Metrikagyűjtési intervallum",
"metricsIntervalDesc": "Milyen gyakran gyűjtsünk szerverstatisztikákat (5 másodperc - 1 óra)",
"metricsNotAvailableForConnectionType": "A szervermetrikák csak SSH-hosztok esetén érhetők el. A TCP ping állapotellenőrzések továbbra is engedélyezhetők fent.",
"intervalSeconds": "másodperc",
"intervalMinutes": "jegyzőkönyv",
"intervalValidation": "A monitorozási időközöknek 5 másodperc és 1 óra (3600 másodperc) között kell lenniük.",
@@ -1133,6 +1203,10 @@
"noServerFound": "Nem található szerver",
"jumpHostsOrder": "A kapcsolatok a következő sorrendben jönnek létre: Jump Host 1 → Jump Host 2 → ... → Célkiszolgáló",
"socks5Proxy": "SOCKS5 proxy",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Send a sequence of connection attempts to specified ports before connecting. Used to open firewall rules dynamically.",
"addKnock": "Add Port",
"delayMs": "Delay",
"socks5Description": "SOCKS5 proxy konfigurálása SSH kapcsolathoz. Minden forgalom a megadott proxy szerveren keresztül fog haladni.",
"enableSocks5": "SOCKS5 proxy engedélyezése",
"enableSocks5Description": "SOCKS5 proxy használata ehhez az SSH kapcsolathoz",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "MOSH parancs automatikus futtatása csatlakozáskor",
"moshCommand": "MOSH parancs",
"moshCommandDesc": "A végrehajtandó MOSH parancs",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automatically attach to an existing (or start a new) tmux session for persistent sessions and cross-device continuity",
"environmentVariables": "Környezeti változók",
"environmentVariablesDesc": "Egyéni környezeti változók beállítása a terminál munkamenethez",
"variableName": "Változó neve",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Alagút URL-címének másolása",
"copyServerStatsUrl": "Szerverstatisztikák URL-címének másolása",
"copyDockerUrl": "Docker URL másolása",
"copyRemoteDesktopUrl": "Távoli asztal URL-címének másolása",
"fullScreenUrlTooltip": "URL másolása az alkalmazás teljes képernyős módban történő megnyitásához",
"notEnabled": "A Docker nincs engedélyezve ehhez a gazdagéphez. Engedélyezze a Gazdagép beállításaiban a Docker funkcióinak használatához.",
"validating": "Docker érvényesítése...",
@@ -1348,7 +1425,96 @@
"selectAll": "Összes kijelölése",
"deselectAll": "Összes kijelölés törlése",
"useGlobalStatusDefault": "Globális alapértelmezett érték használata (Állapot)",
"useGlobalMetricsDefault": "Globális alapértelmezett érték használata (metrikák)"
"useGlobalMetricsDefault": "Globális alapértelmezett érték használata (metrikák)",
"remoteDesktopSettings": "Távoli asztal beállításai",
"domain": "Domain",
"securityMode": "Biztonsági mód",
"ignoreCert": "Tanúsítvány figyelmen kívül hagyása",
"ignoreCertDesc": "TLS tanúsítvány-ellenőrzés kihagyása ennél a kapcsolatnál",
"displaySettings": "Kijelzőbeállítások",
"colorDepth": "Színmélység",
"width": "Szélesség",
"height": "Magasság",
"dpi": "DPI",
"resizeMethod": "Átméretezési módszer",
"forceLossless": "Veszteségmentes erő",
"audioSettings": "Hangbeállítások",
"disableAudio": "Hang letiltása",
"enableAudioInput": "Hangbemenet engedélyezése",
"rdpPerformance": "Teljesítmény",
"enableWallpaper": "Háttérkép engedélyezése",
"enableTheming": "Témák engedélyezése",
"enableFontSmoothing": "Betűsimítás engedélyezése",
"enableFullWindowDrag": "Teljes ablak húzásának engedélyezése",
"enableDesktopComposition": "Asztali kompozíció engedélyezése",
"enableMenuAnimations": "Menüanimációk engedélyezése",
"disableBitmapCaching": "Bitképes gyorsítótár letiltása",
"disableOffscreenCaching": "Képernyőn kívüli gyorsítótár letiltása",
"disableGlyphCaching": "Karakterjel-gyorsítótár letiltása",
"enableGfx": "GFX engedélyezése",
"deviceRedirection": "Eszközátirányítás",
"enablePrinting": "Nyomtatás engedélyezése",
"enableDrive": "Meghajtó átirányításának engedélyezése",
"driveName": "Meghajtó neve",
"drivePath": "Útvonal",
"createDrivePath": "Útvonal létrehozása",
"disableDownload": "Letöltés letiltása",
"disableUpload": "Feltöltés letiltása",
"enableTouch": "Érintés engedélyezése",
"rdpSession": "Ülés",
"clientName": "Ügyfél neve",
"consoleSession": "Konzol munkamenet",
"initialProgram": "Kezdeti program",
"serverLayout": "Szerver billentyűzetkiosztás",
"timezone": "Időzóna",
"gatewaySettings": "Átjáró",
"gatewayHostname": "Átjáró állomásneve",
"gatewayPort": "Átjáró kikötő",
"gatewayUsername": "Átjáró felhasználóneve",
"gatewayPassword": "Átjáró jelszava",
"gatewayDomain": "Átjáró domain",
"remoteApp": "RemoteApp",
"remoteAppProgram": "Távoli alkalmazás",
"remoteAppDir": "Távoli alkalmazáskönyvtár",
"remoteAppArgs": "Távoli alkalmazás argumentumai",
"clipboardSettings": "Vágólap",
"normalizeClipboard": "Vágólap normalizálása",
"disableCopy": "Másolás letiltása",
"disablePaste": "Beillesztés letiltása",
"vncSettings": "VNC-beállítások",
"cursorMode": "Kurzor mód",
"swapRedBlue": "Piros/Kék csere",
"readOnly": "Csak olvasható",
"recordingSettings": "Felvétel",
"recordingPath": "Felvételi útvonal",
"recordingName": "Felvétel neve",
"createRecordingPath": "Felvételi útvonal létrehozása",
"excludeOutput": "Kimenet kizárása",
"excludeMouse": "Egér kizárása",
"includeKeys": "Kulcsok beillesztése",
"sendWolPacket": "WoL csomag küldése",
"wolMacAddr": "MAC-cím",
"wolBroadcastAddr": "Sugárzási cím",
"wolUdpPort": "UDP port",
"wolWaitTime": "Várakozási idő (másodperc)",
"connectionSettings": "Kapcsolati beállítások",
"rdpOnly": "Csak RDP",
"vncOnly": "Csak VNC",
"telnetTerminalSettings": "Terminálbeállítások",
"terminalType": "Terminál típusa",
"guacFontName": "Betűtípus neve",
"guacFontSize": "Betűméret",
"guacColorScheme": "Színséma",
"guacBackspaceKey": "Visszatörlés billentyű"
},
"guacamole": {
"connecting": "Kapcsolódás a(z) {{type}} munkamenethez...",
"rdpConnecting": "Kapcsolódás az RDP-kiszolgálóhoz...",
"vncConnecting": "Kapcsolódás a VNC szerverhez...",
"telnetConnecting": "Kapcsolódás a Telnet szerverhez...",
"connectionError": "Csatlakozási hiba",
"connectionFailed": "Kapcsolódás sikertelen",
"failedToConnect": "Nem sikerült lekérni a kapcsolati tokent"
},
"terminal": {
"title": "Terminál",
@@ -1364,7 +1530,7 @@
"closePanel": "Panel bezárása",
"reconnect": "Újracsatlakozás",
"sessionEnded": "Munkamenet vége",
"connectionLost": "Kapcsolat megszakadt",
"connectionLost": "Connection lost",
"error": "HIBA: {{message}}",
"disconnected": "Szétkapcsolt",
"connectionClosed": "Kapcsolat lezárva",
@@ -1372,6 +1538,7 @@
"connected": "Csatlakoztatva",
"clipboardWriteFailed": "Nem sikerült a vágólapra másolni. Győződjön meg arról, hogy az oldal HTTPS vagy localhost protokollon keresztül jelenik meg.",
"clipboardReadFailed": "Nem sikerült beolvasni a vágólapról. Győződjön meg arról, hogy a vágólapra vonatkozó engedélyek meg vannak adva.",
"clipboardHttpWarning": "A beillesztéshez HTTPS szükséges. Használd a Ctrl+Shift+V billentyűkombinációt, vagy HTTPS-en keresztül szolgáltasd ki a Termixet.",
"sshConnected": "SSH-kapcsolat létrejött",
"authError": "Hitelesítés sikertelen: {{message}}",
"unknownError": "Ismeretlen hiba történt",
@@ -1380,7 +1547,25 @@
"connecting": "Kapcsolódás...",
"reconnecting": "Újracsatlakozás... ({{attempt}}/{{max}})",
"reconnected": "Sikeresen újrakapcsolódott",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.",
"tmuxWindows": "Windows",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Attached clients",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "Last activity",
"tmuxTimeJustNow": "just now",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Start new session",
"tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard",
"maxReconnectAttemptsReached": "Elérte az újracsatlakozási kísérletek maximális számát",
"closeTab": "Close",
"connectionTimeout": "Kapcsolat időtúllépése",
"terminalTitle": "Terminál - {{host}}",
"terminalWithPath": "Terminál - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "A hitelesítés időtúllépést okozott. Kérjük, próbálja újra.",
"opksshAuthFailed": "Sikertelen hitelesítés. Kérjük, ellenőrizze a hitelesítő adatait, és próbálja újra.",
"opksshConfigMissing": "Az OPKSSH konfiguráció nem található. Kérjük, hozza létre a ~/.opk/config.yml fájlt az OIDC-szolgáltató beállításaival. Lásd a dokumentációt: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Sign in with {{provider}}",
"sudoPasswordPopupTitle": "Jelszó beszúrása?",
"sudoPasswordPopupHint": "Nyomja meg az Enter billentyűt a beszúráshoz, az Esc billentyűt az elvetéshez",
"sudoPasswordPopupConfirm": "Beszúrás",
@@ -1428,7 +1614,8 @@
"connectionRejected": "A szerver elutasította a kapcsolatot. Kérjük, ellenőrizze a hitelesítést és a hálózati konfigurációt.",
"hostKeyRejected": "SSH host kulcs ellenőrzése elutasítva. Kapcsolat megszakítva.",
"sessionTakenOver": "A munkamenet egy másik lapon nyílt meg. Újrakapcsolódás...",
"sessionAttachTimeout": "A munkamenet csatolása időtúllépést szenvedett. Új kapcsolat létrehozása..."
"sessionAttachTimeout": "A munkamenet csatolása időtúllépést szenvedett. Új kapcsolat létrehozása...",
"openFileManagerHere": "Open File Manager Here"
},
"fileManager": {
"title": "Fájlkezelő",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Fájlok színkódolása típus szerint: mappák (piros), fájlok (kék), szimbolikus linkek (zöld)",
"commandAutocomplete": "Parancs automatikus kiegészítése",
"commandAutocompleteDesc": "Engedélyezze a Tab billentyű automatikus kiegészítési javaslatait a terminálparancsokhoz a parancselőzmények alapján",
"commandHistoryTracking": "Save Command History",
"commandHistoryTrackingDesc": "Store terminal commands in history for autocomplete and sidebar. Disabled by default for privacy.",
"defaultSnippetFoldersCollapsed": "Kódrészletmappák összecsukása alapértelmezés szerint",
"defaultSnippetFoldersCollapsedDesc": "Ha engedélyezve van, az összes kódrészlet mappa összecsukódik, amikor megnyitja a kódrészletek lapot.",
"terminalSyntaxHighlighting": "Terminál szintaxis kiemelése",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Parancsok, elérési utak, IP-címek és naplózási szintek automatikus kiemelése a terminál kimenetében",
"enableCommandPaletteShortcut": "Parancspaletta billentyűparancsának engedélyezése",
"enableCommandPaletteShortcutDesc": "Koppintson duplán a bal Shift billentyűre a Parancspaletta megnyitásához, amelyen gyorsan elérheti a gazdagépeket.",
"enableTerminalSessionPersistence": "Állandó terminál munkamenetek",
"enableTerminalSessionPersistence": "Állandó lapok/munkamenetek",
"enableTerminalSessionPersistenceDesc": "SSH-kapcsolatok fenntartása lapváltáskor vagy böngésző bezárásakor (instabil lehet)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Adatbázis",
"healthy": "Egészséges",
"error": "Hiba",
"totalServers": "Összes szerver",
"totalHosts": "Total Hosts",
"totalTunnels": "Összes alagutak",
"totalCredentials": "Összes hitelesítő adat",
"recentActivity": "Legutóbbi tevékenységek",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Salin cuplikan ke papan klip",
"editTooltip": "Edit cuplikan ini",
"deleteTooltip": "Hapus cuplikan ini",
"shareTooltip": "Share this snippet",
"sharedWithYou": "Shared",
"sharedBy": "Shared by {{username}}",
"shareSnippet": "Share Snippet",
"user": "User",
"role": "Role",
"selectTarget": "Select...",
"currentAccess": "Current Access",
"shareSuccess": "Snippet shared successfully",
"shareFailed": "Failed to share snippet",
"revokeSuccess": "Access revoked",
"revokeFailed": "Failed to revoke access",
"failedToLoadShareData": "Failed to load sharing data",
"newFolder": "Folder Baru",
"reorderSameFolder": "Hanya dapat mengubah urutan cuplikan dalam folder yang sama.",
"reorderSuccess": "Cuplikan berhasil diurutkan ulang",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Diperlukan autentikasi. Silakan segarkan halaman.",
"dataAccessLockedReauth": "Akses data terkunci. Silakan autentikasi ulang.",
"loading": "Memuat riwayat perintah...",
"error": "Kesalahan Saat Memuat Riwayat"
"error": "Kesalahan Saat Memuat Riwayat",
"disabledTitle": "Command History is Disabled",
"disabledDescription": "Enable Command History Tracking in your profile under Appearance settings."
},
"splitScreen": {
"title": "Layar Terpisah",
@@ -510,6 +525,8 @@
"checkingDatabase": "Memeriksa koneksi basis data...",
"checkingAuthentication": "Memeriksa autentikasi...",
"backendReconnected": "Koneksi server dipulihkan.",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "Tindakan",
"remove": "Menghapus",
"revoke": "Menarik kembali",
@@ -541,7 +558,8 @@
"copySudoPassword": "Salin Kata Sandi Sudo",
"passwordCopied": "Kata sandi disalin ke papan klip",
"sudoPasswordCopied": "Kata sandi Sudo disalin ke papan klip",
"noPasswordAvailable": "Tidak ada kata sandi yang tersedia"
"noPasswordAvailable": "Tidak ada kata sandi yang tersedia",
"failedToCopyPassword": "Failed to copy password"
},
"admin": {
"title": "Pengaturan Admin",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Pengaturan pemantauan global tersimpan",
"failedToSaveGlobalSettings": "Gagal menyimpan pengaturan pemantauan global.",
"failedToLoadGlobalSettings": "Gagal memuat pengaturan pemantauan global.",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Configure how long user sessions last before requiring re-authentication. 'Remember Me' sessions are unaffected (always 30 days).",
"sessionTimeoutHours": "Timeout Duration",
"hours": "hours",
"sessionTimeoutNote": "Valid range: 1720 hours. Changes apply to new sessions only.",
"sessionTimeoutSaved": "Session timeout saved",
"failedToSaveSessionTimeout": "Failed to save session timeout",
"guacamoleIntegration": "Integrasi Desktop Jarak Jauh (Guacamole)",
"guacamoleIntegrationDesc": "Aktifkan koneksi RDP, VNC, dan Telnet melalui guacd. Membutuhkan instance guacd yang sedang berjalan.",
"enableGuacamole": "Aktifkan dukungan RDP/VNC/Telnet",
"guacdUrl": "URL guacd (host:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Perubahan pada host/port guacd memerlukan restart server agar berlaku.",
"guacamoleSettingsSaved": "Pengaturan guacamole tersimpan.",
"failedToSaveGuacamoleSettings": "Gagal menyimpan pengaturan guacamole",
"failedToLoadGuacamoleSettings": "Gagal memuat pengaturan guacamole",
"logLevel": "Log Level",
"logLevelDesc": "Control the verbosity of server logs. Higher levels reduce log output. Can also be set via the LOG_LEVEL environment variable.",
"logVerbosity": "Verbosity",
"logLevelNote": "Changes take effect immediately. Debug level may impact performance.",
"logLevelSaved": "Log level saved",
"failedToSaveLogLevel": "Failed to save log level",
"clampedToValidRange": "disesuaikan ke rentang yang valid",
"loadingSessions": "Memuat sesi...",
"noActiveSessions": "Tidak ditemukan sesi aktif.",
"device": "Perangkat",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} tuan rumah",
"importJson": "Impor JSON",
"importing": "Pengimporan...",
"exportAllJson": "Export All",
"exporting": "Exporting...",
"exportedAllHosts": "Exported {{count}} hosts",
"failedToExportAllHosts": "Failed to export hosts. Please ensure you're logged in and have access to the host data.",
"exportAllSensitiveWarning": "The exported file will contain sensitive authentication data (passwords/SSH keys) in plaintext. Please keep the file secure and delete it after use.",
"importJsonTitle": "Impor Host SSH dari JSON",
"importJsonDesc": "Unggah file JSON untuk mengimpor beberapa host SSH secara massal (maksimal 100).",
"downloadSample": "Unduh Sampel",
@@ -866,11 +912,26 @@
"importError": "Kesalahan impor",
"failedToImportJson": "Gagal mengimpor file JSON",
"connectionDetails": "Detail Koneksi",
"connectionType": "Jenis Koneksi",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Desktop Jarak Jauh",
"guacamoleSettings": "Pengaturan Desktop Jarak Jauh",
"organization": "Organisasi",
"ipAddress": "Alamat IP atau Nama Host",
"macAddress": "MAC Address",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Wake-on-LAN packet sent",
"wolFailed": "Failed to send Wake-on-LAN packet",
"ipRequired": "Alamat IP diperlukan.",
"portRequired": "Port diperlukan",
"port": "Pelabuhan",
"name": "Nama",
"username": "Nama belakang",
"usernameRequired": "Nama pengguna wajib diisi (hanya untuk SSH/Telnet)",
"folder": "Map",
"tags": "Tag",
"pin": "Pin",
@@ -979,6 +1040,8 @@
"tunnel": "Terowongan",
"fileManager": "Pengelola File",
"serverStats": "Statistik Server",
"status": "Status",
"statistics": "Statistik",
"hostViewer": "Pemirsa Pembawa Acara",
"enableServerStats": "Aktifkan Statistik Server",
"enableServerStatsDesc": "Aktifkan/nonaktifkan pengumpulan statistik server untuk host ini.",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Seret untuk berpindah antar folder",
"exportedHostConfig": "Konfigurasi host yang diekspor untuk {{name}}",
"openTerminal": "Terminal Terbuka",
"openRdp": "Open RDP",
"openVnc": "Open VNC",
"openTelnet": "Open Telnet",
"openFileManager": "Buka Pengelola File",
"openTunnels": "Terowongan Terbuka",
"openServerDetails": "Buka Detail Server",
"statistics": "Statistik",
"enabledWidgets": "Widget yang Diaktifkan",
"openServerStats": "Statistik Server Terbuka",
"enabledWidgetsDesc": "Pilih widget statistik mana yang akan ditampilkan untuk host ini.",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Periksa apakah server sedang online atau offline.",
"statusCheckInterval": "Interval Pemeriksaan Status",
"statusCheckIntervalDesc": "Seberapa sering memeriksa apakah host sedang online (5 detik - 1 jam)",
"statusChecks": "Pemeriksaan Status",
"disableTcpPing": "Nonaktifkan TCP Ping",
"disableTcpPingDescription": "Nonaktifkan pemeriksaan status (deteksi online/offline) untuk host ini.",
"useGlobalStatusInterval": "Gunakan pengaturan default global.",
"useGlobalMetricsInterval": "Gunakan pengaturan default global.",
"usingGlobalDefault": "Menggunakan default global ({{value}}s)",
"metricsCollection": "Pengumpulan Metrik",
"metricsEnabled": "Aktifkan Pemantauan Metrik",
"metricsEnabledDesc": "Kumpulkan statistik CPU, RAM, disk, dan sistem lainnya.",
"metricsInterval": "Interval Pengumpulan Metrik",
"metricsIntervalDesc": "Seberapa sering mengumpulkan statistik server (5 detik - 1 jam)",
"metricsNotAvailableForConnectionType": "Metrik server hanya tersedia untuk host SSH. Pemeriksaan status ping TCP masih dapat diaktifkan di atas.",
"intervalSeconds": "detik",
"intervalMinutes": "menit",
"intervalValidation": "Interval pemantauan harus antara 5 detik dan 1 jam (3600 detik).",
@@ -1133,6 +1203,10 @@
"noServerFound": "Tidak ada server yang ditemukan.",
"jumpHostsOrder": "Koneksi akan dibuat secara berurutan: Jump Host 1 → Jump Host 2 → ... → Server Target",
"socks5Proxy": "Proksi SOCKS5",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Send a sequence of connection attempts to specified ports before connecting. Used to open firewall rules dynamically.",
"addKnock": "Add Port",
"delayMs": "Delay",
"socks5Description": "Konfigurasikan proxy SOCKS5 untuk koneksi SSH. Semua lalu lintas akan dialihkan melalui server proxy yang ditentukan.",
"enableSocks5": "Aktifkan Proksi SOCKS5",
"enableSocks5Description": "Gunakan proxy SOCKS5 untuk koneksi SSH ini.",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Jalankan perintah MOSH secara otomatis saat terhubung.",
"moshCommand": "Komando MOSH",
"moshCommandDesc": "Perintah MOSH untuk dieksekusi",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automatically attach to an existing (or start a new) tmux session for persistent sessions and cross-device continuity",
"environmentVariables": "Variabel Lingkungan",
"environmentVariablesDesc": "Tetapkan variabel lingkungan khusus untuk sesi terminal.",
"variableName": "Nama variabel",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Salin URL Terowongan",
"copyServerStatsUrl": "Salin URL Statistik Server",
"copyDockerUrl": "Salin URL Docker",
"copyRemoteDesktopUrl": "Salin URL Remote Desktop",
"fullScreenUrlTooltip": "Salin URL untuk membuka aplikasi ini dalam mode layar penuh.",
"notEnabled": "Docker belum diaktifkan untuk host ini. Aktifkan di Pengaturan Host untuk menggunakan fitur Docker.",
"validating": "Memvalidasi Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Pilih semua",
"deselectAll": "Batalkan pilihan semua",
"useGlobalStatusDefault": "Gunakan Pengaturan Default Global (Status)",
"useGlobalMetricsDefault": "Gunakan Pengaturan Default Global (Metrik)"
"useGlobalMetricsDefault": "Gunakan Pengaturan Default Global (Metrik)",
"remoteDesktopSettings": "Pengaturan Desktop Jarak Jauh",
"domain": "Domain",
"securityMode": "Mode Keamanan",
"ignoreCert": "Abaikan Sertifikat",
"ignoreCertDesc": "Lewati verifikasi sertifikat TLS untuk koneksi ini.",
"displaySettings": "Pengaturan Tampilan",
"colorDepth": "Kedalaman Warna",
"width": "Lebar",
"height": "Tinggi",
"dpi": "DPI",
"resizeMethod": "Metode Pengubahan Ukuran",
"forceLossless": "Paksa Tanpa Kehilangan",
"audioSettings": "Pengaturan Audio",
"disableAudio": "Nonaktifkan Audio",
"enableAudioInput": "Aktifkan Input Audio",
"rdpPerformance": "Pertunjukan",
"enableWallpaper": "Aktifkan Wallpaper",
"enableTheming": "Aktifkan Tema",
"enableFontSmoothing": "Aktifkan Perataan Font",
"enableFullWindowDrag": "Aktifkan fitur seret jendela penuh.",
"enableDesktopComposition": "Aktifkan Komposisi Desktop",
"enableMenuAnimations": "Aktifkan Animasi Menu",
"disableBitmapCaching": "Nonaktifkan Cache Bitmap",
"disableOffscreenCaching": "Nonaktifkan Cache di Luar Layar",
"disableGlyphCaching": "Nonaktifkan Cache Glif",
"enableGfx": "Aktifkan GFX",
"deviceRedirection": "Pengalihan Perangkat",
"enablePrinting": "Aktifkan Pencetakan",
"enableDrive": "Aktifkan Pengalihan Drive",
"driveName": "Nama Drive",
"drivePath": "Jalur Berkendara",
"createDrivePath": "Buat Jalur Mengemudi",
"disableDownload": "Nonaktifkan Unduhan",
"disableUpload": "Nonaktifkan Unggahan",
"enableTouch": "Aktifkan Sentuhan",
"rdpSession": "Sidang",
"clientName": "Nama Klien",
"consoleSession": "Sesi Konsol",
"initialProgram": "Program Awal",
"serverLayout": "Tata Letak Keyboard Server",
"timezone": "Zona waktu",
"gatewaySettings": "Gerbang",
"gatewayHostname": "Nama Host Gateway",
"gatewayPort": "Port Gerbang",
"gatewayUsername": "Nama Pengguna Gateway",
"gatewayPassword": "Kata Sandi Gerbang",
"gatewayDomain": "Domain Gerbang",
"remoteApp": "RemoteApp",
"remoteAppProgram": "Aplikasi Jarak Jauh",
"remoteAppDir": "Direktori Aplikasi Jarak Jauh",
"remoteAppArgs": "Argumen Aplikasi Jarak Jauh",
"clipboardSettings": "Papan klip",
"normalizeClipboard": "Normalisasi Papan Klip",
"disableCopy": "Nonaktifkan Salin",
"disablePaste": "Nonaktifkan Tempel",
"vncSettings": "Pengaturan VNC",
"cursorMode": "Mode Kursor",
"swapRedBlue": "Tukar Merah/Biru",
"readOnly": "Hanya Baca",
"recordingSettings": "Rekaman",
"recordingPath": "Jalur Perekaman",
"recordingName": "Nama Rekaman",
"createRecordingPath": "Buat Jalur Perekaman",
"excludeOutput": "Kecualikan Output",
"excludeMouse": "Kecualikan Tikus",
"includeKeys": "Sertakan Kunci",
"sendWolPacket": "Kirim Paket WoL",
"wolMacAddr": "Alamat MAC",
"wolBroadcastAddr": "Pidato Siaran",
"wolUdpPort": "Port UDP",
"wolWaitTime": "Waktu Tunggu (detik)",
"connectionSettings": "Pengaturan Koneksi",
"rdpOnly": "Hanya RDP",
"vncOnly": "Hanya VNC",
"telnetTerminalSettings": "Pengaturan Terminal",
"terminalType": "Jenis Terminal",
"guacFontName": "Nama Font",
"guacFontSize": "Ukuran Huruf",
"guacColorScheme": "Skema Warna",
"guacBackspaceKey": "Tombol Backspace"
},
"guacamole": {
"connecting": "Menghubungkan ke sesi {{type}}...",
"rdpConnecting": "Menghubungkan ke server RDP...",
"vncConnecting": "Menghubungkan ke server VNC...",
"telnetConnecting": "Menghubungkan ke server Telnet...",
"connectionError": "Kesalahan koneksi",
"connectionFailed": "Koneksi gagal",
"failedToConnect": "Gagal mendapatkan token koneksi."
},
"terminal": {
"title": "Terminal",
@@ -1364,7 +1530,7 @@
"closePanel": "Tutup Panel",
"reconnect": "Terhubung kembali",
"sessionEnded": "Sesi Berakhir",
"connectionLost": "Koneksi Terputus",
"connectionLost": "Connection lost",
"error": "KESALAHAN: {{message}}",
"disconnected": "Terputus",
"connectionClosed": "Koneksi terputus",
@@ -1372,6 +1538,7 @@
"connected": "Terhubung",
"clipboardWriteFailed": "Gagal menyalin ke papan klip. Pastikan halaman disajikan melalui HTTPS atau localhost.",
"clipboardReadFailed": "Gagal membaca dari clipboard. Pastikan izin clipboard telah diberikan.",
"clipboardHttpWarning": "Paste membutuhkan HTTPS. Gunakan Ctrl+Shift+V atau sajikan Termix melalui HTTPS.",
"sshConnected": "Koneksi SSH berhasil dibuat.",
"authError": "Autentikasi gagal: {{message}}",
"unknownError": "Terjadi kesalahan yang tidak diketahui.",
@@ -1380,7 +1547,25 @@
"connecting": "Menghubungkan...",
"reconnecting": "Menghubungkan kembali... ({{attempt}}/{{max}})",
"reconnected": "Berhasil terhubung kembali",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.",
"tmuxWindows": "Windows",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Attached clients",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "Last activity",
"tmuxTimeJustNow": "just now",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Start new session",
"tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard",
"maxReconnectAttemptsReached": "Upaya penyambungan kembali maksimum telah tercapai.",
"closeTab": "Close",
"connectionTimeout": "Waktu habis koneksi",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Autentikasi habis waktu. Silakan coba lagi.",
"opksshAuthFailed": "Autentikasi gagal. Silakan periksa kredensial Anda dan coba lagi.",
"opksshConfigMissing": "Konfigurasi OPKSSH tidak ditemukan. Silakan buat ~/.opk/config.yml dengan pengaturan penyedia OIDC Anda. Lihat dokumentasi: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Sign in with {{provider}}",
"sudoPasswordPopupTitle": "Masukkan kata sandi?",
"sudoPasswordPopupHint": "Tekan Enter untuk memasukkan, Esc untuk menutup.",
"sudoPasswordPopupConfirm": "Menyisipkan",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Koneksi ditolak oleh server. Harap periksa otentikasi dan konfigurasi jaringan Anda.",
"hostKeyRejected": "Verifikasi kunci host SSH ditolak. Koneksi dibatalkan.",
"sessionTakenOver": "Sesi dibuka di tab lain. Sedang menyambungkan kembali...",
"sessionAttachTimeout": "Waktu lampiran sesi habis. Membuat koneksi baru..."
"sessionAttachTimeout": "Waktu lampiran sesi habis. Membuat koneksi baru...",
"openFileManagerHere": "Open File Manager Here"
},
"fileManager": {
"title": "Pengelola File",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Beri kode warna pada file berdasarkan jenisnya: folder (merah), file (biru), symlink (hijau)",
"commandAutocomplete": "Pelengkapan Otomatis Perintah",
"commandAutocompleteDesc": "Aktifkan saran pelengkapan otomatis tombol Tab untuk perintah terminal berdasarkan riwayat perintah Anda.",
"commandHistoryTracking": "Save Command History",
"commandHistoryTrackingDesc": "Store terminal commands in history for autocomplete and sidebar. Disabled by default for privacy.",
"defaultSnippetFoldersCollapsed": "Secara default, folder cuplikan akan diciutkan.",
"defaultSnippetFoldersCollapsedDesc": "Saat diaktifkan, semua folder cuplikan akan dilipat saat Anda membuka tab cuplikan.",
"terminalSyntaxHighlighting": "Penyorotan Sintaks Terminal",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Secara otomatis menyorot perintah, jalur, IP, dan level log pada output terminal.",
"enableCommandPaletteShortcut": "Aktifkan Pintasan Palet Perintah",
"enableCommandPaletteShortcutDesc": "Tekan Shift kiri dua kali untuk membuka Palet Perintah guna akses cepat ke host.",
"enableTerminalSessionPersistence": "Sesi Terminal Permanen",
"enableTerminalSessionPersistence": "Tab/Sesi Permanen",
"enableTerminalSessionPersistenceDesc": "Pertahankan koneksi SSH saat beralih tab atau menutup browser (mungkin tidak stabil)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Basis data",
"healthy": "Sehat",
"error": "Kesalahan",
"totalServers": "Total Server",
"totalHosts": "Total Hosts",
"totalTunnels": "Total Terowongan",
"totalCredentials": "Kredensial Total",
"recentActivity": "Aktivitas Terkini",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Copia snippet negli appunti",
"editTooltip": "Modifica questa snippet",
"deleteTooltip": "Elimina questa snippet",
"shareTooltip": "Condividi questa snippet",
"sharedWithYou": "Condiviso",
"sharedBy": "Condiviso da {{username}}",
"shareSnippet": "Condividi Snippet",
"user": "Utente",
"role": "Ruolo",
"selectTarget": "Seleziona...",
"currentAccess": "Accesso Corrente",
"shareSuccess": "Snippet condiviso con successo",
"shareFailed": "Condivisione snippet non riuscita",
"revokeSuccess": "Accesso revocato",
"revokeFailed": "Impossibile revocare l'accesso",
"failedToLoadShareData": "Impossibile caricare la condivisione dei dati",
"newFolder": "Nuova Cartella",
"reorderSameFolder": "Può riordinare solo pezzetti di codice all'interno della stessa cartella",
"reorderSuccess": "Snippet riordinato con successo",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Autenticazione richiesta. Si prega di aggiornare la pagina.",
"dataAccessLockedReauth": "Accesso dati bloccato. Si prega di autenticarsi.",
"loading": "Caricamento cronologia comandi...",
"error": "Errore Nel Caricamento Della Cronologia"
"error": "Errore Nel Caricamento Della Cronologia",
"disabledTitle": "La cronologia dei comandi è disabilitata",
"disabledDescription": "Abilita il monitoraggio della cronologia dei comandi nel tuo profilo sotto le impostazioni di Aspetto."
},
"splitScreen": {
"title": "Schermo Diviso",
@@ -510,6 +525,8 @@
"checkingDatabase": "Controllo connessione database...",
"checkingAuthentication": "Controllo autenticazione...",
"backendReconnected": "Connessione al server ripristinata",
"connectionDegraded": "Connessione al server persa, recupero…",
"reload": "Reload",
"actions": "Azioni",
"remove": "Rimuovi",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Copia Password Sudo",
"passwordCopied": "Password copiata negli appunti",
"sudoPasswordCopied": "Password Sudo copiata negli appunti",
"noPasswordAvailable": "Nessuna password disponibile"
"noPasswordAvailable": "Nessuna password disponibile",
"failedToCopyPassword": "Impossibile copiare la password"
},
"admin": {
"title": "Impostazioni Amministratore",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Impostazioni di monitoraggio globali salvate",
"failedToSaveGlobalSettings": "Impossibile salvare le impostazioni di monitoraggio globale",
"failedToLoadGlobalSettings": "Impossibile caricare le impostazioni di monitoraggio globale",
"sessionTimeout": "Timeout Sessione",
"sessionTimeoutDesc": "Configura quanto durano le sessioni dell'utente prima di richiedere la ri-autenticazione. Le sessioni 'Remember Me' non sono influenzate (sempre 30 giorni).",
"sessionTimeoutHours": "Durata Timeout",
"hours": "ore",
"sessionTimeoutNote": "Intervallo valido: 1-720 ore. Le modifiche si applicano solo alle nuove sessioni.",
"sessionTimeoutSaved": "Timeout sessione salvato",
"failedToSaveSessionTimeout": "Impossibile salvare il timeout della sessione",
"guacamoleIntegration": "Integrazione Desktop Remoto (Guacamole)",
"guacamoleIntegrationDesc": "Abilita connessioni RDP, VNC e Telnet tramite guacd. Richiede un'istanza guacd per essere in esecuzione.",
"enableGuacamole": "Abilita supporto RDP/VNC/Telnet",
"guacdUrl": "guacd URL (host:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Le modifiche all'host/porta guacd richiedono che il riavvio del server abbia effetto.",
"guacamoleSettingsSaved": "Impostazioni di Guacamole salvate",
"failedToSaveGuacamoleSettings": "Impossibile salvare le impostazioni del guacamole",
"failedToLoadGuacamoleSettings": "Impossibile caricare le impostazioni di guacamole",
"logLevel": "Livello Registro",
"logLevelDesc": "Controlla la verbosità dei log del server. Livelli più alti riducono l'output del registro. Può anche essere impostato tramite la variabile d'ambiente LOG_LEVEL.",
"logVerbosity": "Verbalità",
"logLevelNote": "Le modifiche hanno effetto immediatamente. Il livello di debug potrebbe influire sulle prestazioni.",
"logLevelSaved": "Livello di registro salvato",
"failedToSaveLogLevel": "Impossibile salvare il livello di log",
"clampedToValidRange": "è stato adattato all'intervallo valido",
"loadingSessions": "Caricamento sessioni...",
"noActiveSessions": "Nessuna sessione attiva trovata.",
"device": "Dispositivo",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} host",
"importJson": "Importa JSON",
"importing": "Importazione...",
"exportAllJson": "Esporta Tutto",
"exporting": "Esportazione...",
"exportedAllHosts": "Host {{count}} esportati",
"failedToExportAllHosts": "Impossibile esportare gli host. Assicurati di aver effettuato l'accesso e di avere accesso ai dati dell'host.",
"exportAllSensitiveWarning": "Il file esportato conterrà dati di autenticazione sensibili (password / chiavi SSH) in testo semplice. Si prega di mantenere il file sicuro e cancellarlo dopo l'uso.",
"importJsonTitle": "Importa host SSH da JSON",
"importJsonDesc": "Carica un file JSON per importare in massa più host SSH (max 100).",
"downloadSample": "Scarica Esempio",
@@ -866,11 +912,26 @@
"importError": "Errore di importazione",
"failedToImportJson": "Importazione del file JSON non riuscita",
"connectionDetails": "Dettagli Della Connessione",
"connectionType": "Tipo Di Connessione",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Desktop Remoto",
"guacamoleSettings": "Impostazioni Desktop Remoto",
"organization": "Organizzazione",
"ipAddress": "Indirizzo IP o nome host",
"macAddress": "Indirizzo MAC",
"macAddressDesc": "Per Wake-on-LAN. Formato: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Pacchetto Wake-on-LAN inviato",
"wolFailed": "Impossibile inviare il pacchetto Wake-on-LAN",
"ipRequired": "Indirizzo IP è obbligatorio",
"portRequired": "La porta è obbligatoria",
"port": "Porta",
"name": "Nome",
"username": "Username",
"usernameRequired": "Il nome utente è obbligatorio (SSH/solo Telnet)",
"folder": "Cartella",
"tags": "Etichette",
"pin": "Pin",
@@ -979,6 +1040,8 @@
"tunnel": "Tunnel",
"fileManager": "Gestore File",
"serverStats": "Statistiche Server",
"status": "Stato",
"statistics": "Statistiche",
"hostViewer": "Visualizzatore Host",
"enableServerStats": "Abilita Statistiche Server",
"enableServerStatsDesc": "Abilita/disabilita la raccolta di statistiche server per questo host",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Trascina per spostarti tra le cartelle",
"exportedHostConfig": "Configurazione host esportata per {{name}}",
"openTerminal": "Apri Terminale",
"openRdp": "Apri RDP",
"openVnc": "Apri VNC",
"openTelnet": "Apri Telnet",
"openFileManager": "Apri File Manager",
"openTunnels": "Gallerie Aperte",
"openServerDetails": "Apri Dettagli Server",
"statistics": "Statistiche",
"enabledWidgets": "Widget Abilitati",
"openServerStats": "Apri Statistiche Server",
"enabledWidgetsDesc": "Seleziona quali widget statistiche mostrare per questo host",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Controlla se il server è online o offline",
"statusCheckInterval": "Intervallo Controllo Stato",
"statusCheckIntervalDesc": "Quanto spesso controllare se l'host è online (5s - 1h)",
"statusChecks": "Controlli Di Stato",
"disableTcpPing": "Disabilita TCP Ping",
"disableTcpPingDescription": "Disattiva i controlli di stato (rilevamento online/offline) per questo host",
"useGlobalStatusInterval": "Usa impostazione predefinita globale",
"useGlobalMetricsInterval": "Usa impostazione predefinita globale",
"usingGlobalDefault": "Utilizzo di default globale ({{value}}s)",
"metricsCollection": "Collezione Di Metriche",
"metricsEnabled": "Abilita Monitoraggio Metriche",
"metricsEnabledDesc": "Raccogliere le statistiche CPU, RAM, disco e altre statistiche di sistema",
"metricsInterval": "Intervallo Di Raccolta Metriche",
"metricsIntervalDesc": "Quanto spesso raccogliere statistiche del server (5s - 1h)",
"metricsNotAvailableForConnectionType": "Le metriche del server sono disponibili solo per gli host SSH. I controlli dello stato di ping TCP possono ancora essere abilitati sopra.",
"intervalSeconds": "secondi",
"intervalMinutes": "minuti",
"intervalValidation": "Gli intervalli di monitoraggio devono essere compresi tra 5 secondi e 1 ora (3600 secondi)",
@@ -1133,6 +1203,10 @@
"noServerFound": "Nessun server trovato",
"jumpHostsOrder": "Le connessioni saranno fatte in ordine: Salto Host 1 → Salto Host 2 → Target Server",
"socks5Proxy": "Proxy SOCKS5",
"portKnocking": "Colpo Porta",
"portKnockingDesc": "Invia una sequenza di tentativi di connessione alle porte specificate prima di connettersi. Usato per aprire dinamicamente le regole del firewall.",
"addKnock": "Aggiungi Porta",
"delayMs": "Ritardo",
"socks5Description": "Configura il proxy SOCKS5 per la connessione SSH. Tutto il traffico verrà instradato attraverso il server proxy specificato.",
"enableSocks5": "Abilita Proxy SOCKS5",
"enableSocks5Description": "Usa il proxy SOCKS5 per questa connessione SSH",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Esegui automaticamente il comando MOSH alla connessione",
"moshCommand": "Comando MOSH",
"moshCommandDesc": "Il comando MOSH da eseguire",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Collega automaticamente a una sessione tmux esistente (o avvia una nuova) per sessioni persistenti e continuità tra dispositivi",
"environmentVariables": "Variabili Di Ambiente",
"environmentVariablesDesc": "Imposta variabili di ambiente personalizzate per la sessione del terminale",
"variableName": "Nome della variabile",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Copia URL Tunnel",
"copyServerStatsUrl": "Copia Url Statistiche Server",
"copyDockerUrl": "Copia URL Docker",
"copyRemoteDesktopUrl": "Copia Url Del Desktop Remoto",
"fullScreenUrlTooltip": "Copia URL per aprire questa app in modalità a schermo intero",
"notEnabled": "Docker non è abilitato per questo host. Abilitarlo nelle impostazioni Host per utilizzare le funzioni Docker.",
"validating": "Convalida Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Seleziona tutto",
"deselectAll": "Deseleziona tutto",
"useGlobalStatusDefault": "Usa Global Default (Status)",
"useGlobalMetricsDefault": "Usa Globale Predefinito (Metrice)"
"useGlobalMetricsDefault": "Usa Globale Predefinito (Metrice)",
"remoteDesktopSettings": "Impostazioni Desktop Remoto",
"domain": "Dominio",
"securityMode": "Modalità Di Sicurezza",
"ignoreCert": "Ignora Certificato",
"ignoreCertDesc": "Verifica del certificato TLS per questa connessione",
"displaySettings": "Impostazioni Di Visualizzazione",
"colorDepth": "Profondità Colore",
"width": "Width",
"height": "Altezza",
"dpi": "DPI",
"resizeMethod": "Metodo Di Ridimensiona",
"forceLossless": "Forza Senza Perdita",
"audioSettings": "Impostazioni Audio",
"disableAudio": "Disabilita Audio",
"enableAudioInput": "Abilita Ingresso Audio",
"rdpPerformance": "Prestazioni",
"enableWallpaper": "Abilita Sfondo",
"enableTheming": "Abilita Temi",
"enableFontSmoothing": "Abilita Sfumatura Carattere",
"enableFullWindowDrag": "Abilita Trascinamento Finestra Completa",
"enableDesktopComposition": "Abilita Composizione Desktop",
"enableMenuAnimations": "Abilita Animazioni Menu",
"disableBitmapCaching": "Disabilita Bitmap Caching",
"disableOffscreenCaching": "Disabilita Cache Fuori Schermo",
"disableGlyphCaching": "Disabilita Glyph Caching",
"enableGfx": "Enable GFX",
"deviceRedirection": "Reindirizzamento Dispositivo",
"enablePrinting": "Abilita Stampa",
"enableDrive": "Abilita Reindirizzamento Unità",
"driveName": "Nome Unità",
"drivePath": "Percorso Unità",
"createDrivePath": "Crea Percorso Drive",
"disableDownload": "Disabilita Download",
"disableUpload": "Disabilita Caricamento",
"enableTouch": "Abilita Touch",
"rdpSession": "Sessione",
"clientName": "Nome Client",
"consoleSession": "Sessione Console",
"initialProgram": "Programma Iniziale",
"serverLayout": "Layout Tastiera Server",
"timezone": "Timezone",
"gatewaySettings": "Gateway",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Porta Del Gateway",
"gatewayUsername": "Nome Utente Gateway",
"gatewayPassword": "Password Del Gateway",
"gatewayDomain": "Dominio Gateway",
"remoteApp": "RemoteApp",
"remoteAppProgram": "Applicazione Remota",
"remoteAppDir": "Directory App Remota",
"remoteAppArgs": "Argomenti App Remote",
"clipboardSettings": "Appunti",
"normalizeClipboard": "Normalizza Appunti",
"disableCopy": "Disabilita Copia",
"disablePaste": "Disabilita Incolla",
"vncSettings": "Impostazioni VNC",
"cursorMode": "Modalità Cursore",
"swapRedBlue": "Scambia Rosso/Blu",
"readOnly": "Sola Lettura",
"recordingSettings": "Registrazione",
"recordingPath": "Percorso Di Registrazione",
"recordingName": "Nome Registrazione",
"createRecordingPath": "Crea Tracciato Di Registrazione",
"excludeOutput": "Escludi Output",
"excludeMouse": "Escludi Il Mouse",
"includeKeys": "Includi Tasti",
"sendWolPacket": "Invia Pacchetto WoL",
"wolMacAddr": "Indirizzo MAC",
"wolBroadcastAddr": "Indirizzo Di Trasmissione",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Tempo Di Attesa (Secondi)",
"connectionSettings": "Impostazioni Di Connessione",
"rdpOnly": "Solo PSR",
"vncOnly": "Solo VNC",
"telnetTerminalSettings": "Impostazioni Del Terminale",
"terminalType": "Tipo Di Terminale",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Schema Colore",
"guacBackspaceKey": "Tasto Backspace"
},
"guacamole": {
"connecting": "Connessione alla sessione {{type}}...",
"rdpConnecting": "Connessione al server RDP...",
"vncConnecting": "Connessione al server VNC...",
"telnetConnecting": "Connessione al server Telnet...",
"connectionError": "Errore di connessione",
"connectionFailed": "Connessione fallita",
"failedToConnect": "Recupero del token di connessione non riuscito"
},
"terminal": {
"title": "Terminale",
@@ -1364,7 +1530,7 @@
"closePanel": "Chiudi Pannello",
"reconnect": "Riconnetti",
"sessionEnded": "Sessione Terminata",
"connectionLost": "Connessione Persa",
"connectionLost": "Connessione persa",
"error": "ERRORE: {{message}}",
"disconnected": "Disconnesso",
"connectionClosed": "Connessione chiusa",
@@ -1372,6 +1538,7 @@
"connected": "Connesso",
"clipboardWriteFailed": "Impossibile copiare negli appunti. Assicurarsi che la pagina sia servita su HTTPS o localhost.",
"clipboardReadFailed": "Lettura dagli appunti non riuscita. Assicurarsi che i permessi degli appunti siano concessi.",
"clipboardHttpWarning": "Incolla richiede HTTPS. Usa Ctrl+Shift+V o serve Termix su HTTPS.",
"sshConnected": "Connessione SSH stabilita",
"authError": "Autenticazione non riuscita: {{message}}",
"unknownError": "Errore sconosciuto",
@@ -1380,7 +1547,25 @@
"connecting": "Connessione...",
"reconnecting": "Riconnessione... ({{attempt}}/{{max}})",
"reconnected": "Riconnesso con successo",
"tmuxSessionCreated": "sessione tmux creata: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmux non è installato sull'host remoto, tornando alla shell standard",
"tmuxSessionPickerTitle": "tmux Sessioni",
"tmuxSessionPickerDesc": "Sessioni tmux esistenti trovate su questo host. Selezionane una per ricollegare o creare una nuova sessione.",
"tmuxWindows": "Finestre",
"tmuxWindowCount": "Finestra {{count}}",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Client allegati",
"tmuxAttachedCount": "{{count}} allegato",
"tmuxLastActivity": "Ultima attività",
"tmuxTimeJustNow": "proprio ora",
"tmuxTimeMinutes": "{{count}}m fa",
"tmuxTimeHours": "{{count}}h fa",
"tmuxTimeDays": "{{count}}fa",
"tmuxCreateNew": "Avvia nuova sessione",
"tmuxCopyHint": "Regola la selezione e premi Invio per copiare negli appunti",
"maxReconnectAttemptsReached": "Massimi tentativi di riconnessione raggiunti",
"closeTab": "Chiudi",
"connectionTimeout": "Timeout connessione",
"terminalTitle": "Terminale - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Autenticazione scaduta. Riprova.",
"opksshAuthFailed": "Autenticazione non riuscita. Controlla le tue credenziali e riprova.",
"opksshConfigMissing": "Configurazione OPKSSH non trovata. Si prega di creare ~/.opk/config.yml con le impostazioni del provider OIDC. Vedi documentazione: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Accedi con {{provider}}",
"sudoPasswordPopupTitle": "Inserire Password?",
"sudoPasswordPopupHint": "Premere Invio per inserire, Esc per eliminare",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Connessione rifiutata dal server. Controlla la tua autenticazione e configurazione di rete.",
"hostKeyRejected": "Verifica della chiave host SSH rifiutata. Connessione annullata.",
"sessionTakenOver": "La sessione è stata aperta in un'altra scheda. Riconnessione...",
"sessionAttachTimeout": "Allegato sessione scaduto. Creazione nuova connessione..."
"sessionAttachTimeout": "Allegato sessione scaduto. Creazione nuova connessione...",
"openFileManagerHere": "Apri File Manager Qui"
},
"fileManager": {
"title": "Gestore File",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "File di codice colore per tipo: cartelle (rosse), file (blu), collegamenti simbolici (verde)",
"commandAutocomplete": "Completamento Automatico Dei Comandi",
"commandAutocompleteDesc": "Abilita i suggerimenti di completamento automatico dei tasti Tab per i comandi del terminale in base alla cronologia dei comandi",
"commandHistoryTracking": "Salva Cronologia Comandi",
"commandHistoryTrackingDesc": "Memorizza i comandi del terminale nella cronologia per completamento automatico e barra laterale. Disabilitato per impostazione predefinita per la privacy.",
"defaultSnippetFoldersCollapsed": "Comprimi cartelle snippet per impostazione predefinita",
"defaultSnippetFoldersCollapsedDesc": "Se abilitata, tutte le cartelle di snippet saranno collassate quando apri la scheda snippet",
"terminalSyntaxHighlighting": "Evidenziazione Sintassi Del Terminale",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Evidenzia automaticamente comandi, tracciati, IP e livelli di log nell'output del terminale",
"enableCommandPaletteShortcut": "Abilita Scorciatoia Tavolozza Comandi",
"enableCommandPaletteShortcutDesc": "Doppio tocco a sinistra Maiusc per aprire la Tavolozza dei comandi per un rapido accesso agli host",
"enableTerminalSessionPersistence": "Sessioni Terminali Persistenti",
"enableTerminalSessionPersistence": "Schede Persistenti/Sessioni",
"enableTerminalSessionPersistenceDesc": "Mantenere le connessioni SSH quando si cambiano schede o si chiude il browser (potrebbe essere instabile)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Database",
"healthy": "Sano",
"error": "Errore",
"totalServers": "Server Totali",
"totalHosts": "Totale Host",
"totalTunnels": "Totale Gallerie",
"totalCredentials": "Credenziali Totali",
"recentActivity": "Attività Recenti",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "スニペットをクリップボードにコピー",
"editTooltip": "このスニペットを編集",
"deleteTooltip": "このスニペットを削除",
"shareTooltip": "このスニペットを共有",
"sharedWithYou": "共有",
"sharedBy": "{{username}} で共有",
"shareSnippet": "スニペットを共有",
"user": "ユーザー",
"role": "ロール",
"selectTarget": "選択...",
"currentAccess": "現在のアクセス",
"shareSuccess": "スニペットの共有に成功しました",
"shareFailed": "スニペットを共有できませんでした",
"revokeSuccess": "アクセスが取り消されました",
"revokeFailed": "アクセスの取り消しに失敗しました",
"failedToLoadShareData": "共有データの読み込みに失敗しました",
"newFolder": "新規フォルダ",
"reorderSameFolder": "同じフォルダ内のスニペットの順序を変更することができます",
"reorderSuccess": "スニペットの並べ替えに成功しました",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "認証が必要です。ページを更新してください。",
"dataAccessLockedReauth": "データアクセスがロックされています。再認証してください。",
"loading": "コマンド履歴を読み込んでいます...",
"error": "履歴の読み込みエラー"
"error": "履歴の読み込みエラー",
"disabledTitle": "コマンド履歴は無効です",
"disabledDescription": "Appearance設定の下でプロフィールのコマンド履歴トラッキングを有効にします。"
},
"splitScreen": {
"title": "画面の分割",
@@ -510,6 +525,8 @@
"checkingDatabase": "データベース接続を確認しています...",
"checkingAuthentication": "認証中char@@0",
"backendReconnected": "サーバー接続が復元されました",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "アクション",
"remove": "削除",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Sudoパスワードをコピー",
"passwordCopied": "パスワードをクリップボードにコピーしました",
"sudoPasswordCopied": "Sudoパスワードをクリップボードにコピーしました",
"noPasswordAvailable": "パスワードがありません"
"noPasswordAvailable": "パスワードがありません",
"failedToCopyPassword": "パスワードのコピーに失敗しました"
},
"admin": {
"title": "管理者設定",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "グローバル監視設定を保存しました",
"failedToSaveGlobalSettings": "グローバル監視設定の保存に失敗しました",
"failedToLoadGlobalSettings": "グローバル監視設定の読み込みに失敗しました",
"sessionTimeout": "セッションタイムアウト",
"sessionTimeoutDesc": "再認証を必要とする前にユーザーセッションがどのくらい続くかを設定します。 'Remember Me' セッションは影響を受けません (常に 30 日間)。",
"sessionTimeoutHours": "タイムアウト時間",
"hours": "時間",
"sessionTimeoutNote": "有効範囲:1~720時間。変更内容は新しいセッションのみに適用されます。",
"sessionTimeoutSaved": "セッションタイムアウトを保存しました",
"failedToSaveSessionTimeout": "セッションのタイムアウトを保存できませんでした",
"guacamoleIntegration": "Remote Desktop Integration (Guacamole)",
"guacamoleIntegrationDesc": "Guacd 経由で RDP、VNC、および Telnet 接続を有効にします。実行するには guacd インスタンスが必要です。",
"enableGuacamole": "RDP/VNC/Telnet のサポートを有効にする",
"guacdUrl": "guacd URL (host:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "guacd ホスト/ポートへの変更を有効にするにはサーバーの再起動が必要です。",
"guacamoleSettingsSaved": "Guacamoleの設定を保存しました",
"failedToSaveGuacamoleSettings": "Guacamole設定の保存に失敗しました",
"failedToLoadGuacamoleSettings": "Guacamole設定の読み込みに失敗しました",
"logLevel": "ログレベル",
"logLevelDesc": "サーバーログの詳細度を制御します。レベルが高いほどログ出力が低下します。また、LOG_LEVEL 環境変数を使用して設定することもできます。",
"logVerbosity": "詳細度",
"logLevelNote": "変更はすぐに反映されます。デバッグレベルはパフォーマンスに影響する可能性があります。",
"logLevelSaved": "ログレベルを保存しました",
"failedToSaveLogLevel": "ログレベルの保存に失敗しました",
"clampedToValidRange": "有効な範囲に調整されました",
"loadingSessions": "セッションを読み込み中...",
"noActiveSessions": "アクティブなセッションが見つかりませんでした。",
"device": "デバイス",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} ホスト",
"importJson": "JSON をインポート",
"importing": "インポート中...",
"exportAllJson": "すべてエクスポート",
"exporting": "エクスポート中...",
"exportedAllHosts": "{{count}} ホストをエクスポートしました",
"failedToExportAllHosts": "ホストのエクスポートに失敗しました。ログインしてホストデータにアクセスできることを確認してください。",
"exportAllSensitiveWarning": "エクスポートされたファイルには、暗号化された認証データ (パスワード/SSH キー) がプレーンテキストに含まれます。ファイルを安全に保ち、使用後に削除してください。",
"importJsonTitle": "JSON から SSH ホストをインポート",
"importJsonDesc": "複数の SSH ホストを一括インポートする JSON ファイルをアップロードします (最大 100)。",
"downloadSample": "サンプルをダウンロード",
@@ -866,11 +912,26 @@
"importError": "インポートエラー",
"failedToImportJson": "JSON ファイルのインポートに失敗しました",
"connectionDetails": "接続詳細",
"connectionType": "接続タイプ",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "リモートデスクトップ",
"guacamoleSettings": "リモートデスクトップの設定",
"organization": "組織",
"ipAddress": "IPアドレスまたはホスト名",
"macAddress": "MACアドレス",
"macAddressDesc": "Wake-on-LANの場合 フォーマット: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "ウェイクオン-LANパケットを送信しました",
"wolFailed": "Wake-on-LANパケットの送信に失敗しました",
"ipRequired": "IPアドレスが必要です",
"portRequired": "ポートは必須です",
"port": "ポート",
"name": "名前",
"username": "ユーザー名",
"usernameRequired": "ユーザー名が必要です (SSH/Telnetのみ)",
"folder": "フォルダ",
"tags": "タグ",
"pin": "ピン留めする",
@@ -979,6 +1040,8 @@
"tunnel": "トンネル",
"fileManager": "ファイルマネージャー",
"serverStats": "サーバーの統計",
"status": "ステータス",
"statistics": "統計情報",
"hostViewer": "ホストビューアー",
"enableServerStats": "サーバー統計を有効にする",
"enableServerStatsDesc": "このホストのサーバ統計情報収集を有効/無効にする",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "ドラッグしてフォルダ間を移動します",
"exportedHostConfig": "{{name}} のホスト設定をエクスポートしました",
"openTerminal": "ターミナルを開く",
"openRdp": "RDP を開く",
"openVnc": "VNC を開く",
"openTelnet": "Telnet を開く",
"openFileManager": "ファイルマネージャを開く",
"openTunnels": "トンネルを開く",
"openServerDetails": "サーバーの詳細を開く",
"statistics": "統計情報",
"enabledWidgets": "有効なウィジェット",
"openServerStats": "サーバー統計を開く",
"enabledWidgetsDesc": "このホストに表示する統計ウィジェットを選択してください",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "サーバーがオンラインかオフラインかを確認する",
"statusCheckInterval": "ステータスチェックの間隔",
"statusCheckIntervalDesc": "ホストがオンラインかどうかを確認する頻度(5s - 1h)",
"statusChecks": "状態チェック",
"disableTcpPing": "TCP Ping を無効にする",
"disableTcpPingDescription": "このホストのステータスチェックをオフ(オンライン/オフライン検出)にする",
"useGlobalStatusInterval": "グローバルデフォルトを使用",
"useGlobalMetricsInterval": "グローバルデフォルトを使用",
"usingGlobalDefault": "グローバルデフォルト ({{value}}s)",
"metricsCollection": "メトリックコレクション",
"metricsEnabled": "メトリックモニタリングを有効にする",
"metricsEnabledDesc": "CPU、RAM、ディスク、その他のシステム統計を収集する",
"metricsInterval": "メトリックコレクションの間隔",
"metricsIntervalDesc": "サーバーの統計情報を収集する頻度(5秒~1時間)",
"metricsNotAvailableForConnectionType": "サーバーメトリックはSSHホストでのみ使用できます。TCPのpingステータスチェックは上記でも有効にできます。",
"intervalSeconds": "秒",
"intervalMinutes": "分",
"intervalValidation": "モニタリング間隔は5秒から1時間(3600秒)の間でなければなりません",
@@ -1133,6 +1203,10 @@
"noServerFound": "サーバーが見つかりません",
"jumpHostsOrder": "接続順序は以下の通りです: ジャンプホスト 1 → ジャンプホスト 2 → ... → ターゲットサーバー",
"socks5Proxy": "SOCKS5 プロキシ",
"portKnocking": "ポートノッキング(ポートノッキング)",
"portKnockingDesc": "接続する前に指定されたポートに一連の接続試行を送信します。ファイアウォールのルールを動的に開くために使用します。",
"addKnock": "ポートを追加",
"delayMs": "遅延",
"socks5Description": "SSH 接続用の SOCKS5 プロキシを設定します。すべてのトラフィックは指定したプロキシ サーバー経由でルーティングされます。",
"enableSocks5": "SOCKS5 プロキシを有効にする",
"enableSocks5Description": "このSSH接続にSOCKS5プロキシを使用する",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "接続時にMOSHコマンドを自動的に実行する",
"moshCommand": "MOSHコマンド",
"moshCommandDesc": "実行するMOSHコマンド",
"autoTmux": "自動tmux",
"autoTmuxDesc": "既存のtmuxセッションに自動的にアタッチする(または新しいセッションを開始)、クロスデバイス連続性",
"environmentVariables": "環境変数",
"environmentVariablesDesc": "端末セッションにカスタム環境変数を設定する",
"variableName": "変数名",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "トンネルURLをコピー",
"copyServerStatsUrl": "サーバー統計の URL をコピー",
"copyDockerUrl": "Docker URL をコピー",
"copyRemoteDesktopUrl": "リモートデスクトップの URL をコピー",
"fullScreenUrlTooltip": "URLをコピーしてフルスクリーンモードでこのアプリを開く",
"notEnabled": "Docker はこのホストでは有効になっていません。format@@0で有効にして、Docker 機能を使用します。",
"validating": "Dockerを検証中...",
@@ -1348,7 +1425,96 @@
"selectAll": "すべて選択",
"deselectAll": "すべての選択を解除",
"useGlobalStatusDefault": "Use Global Default (Status)",
"useGlobalMetricsDefault": "グローバルデフォルトを使用 (メトリック)"
"useGlobalMetricsDefault": "グローバルデフォルトを使用 (メトリック)",
"remoteDesktopSettings": "リモートデスクトップの設定",
"domain": "ドメイン",
"securityMode": "セキュリティモード",
"ignoreCert": "証明書を無視",
"ignoreCertDesc": "この接続の TLS 証明書の検証をスキップ",
"displaySettings": "表示設定",
"colorDepth": "色の深さ",
"width": "Width",
"height": "高さ",
"dpi": "DPI",
"resizeMethod": "リサイズ方法",
"forceLossless": "無損失力",
"audioSettings": "音声設定",
"disableAudio": "音声を無効にする",
"enableAudioInput": "音声入力を有効にする",
"rdpPerformance": "パフォーマンス",
"enableWallpaper": "壁紙を有効化",
"enableTheming": "テーマを有効にする",
"enableFontSmoothing": "フォントのスムージングを有効にする",
"enableFullWindowDrag": "ウィンドウ全体のドラッグを有効にする",
"enableDesktopComposition": "デスクトップの構成を有効にする",
"enableMenuAnimations": "メニューアニメーションを有効にする",
"disableBitmapCaching": "ビットマップキャッシュを無効にする",
"disableOffscreenCaching": "オフスクリーンキャッシュを無効にする",
"disableGlyphCaching": "グリフキャッシュを無効にする",
"enableGfx": "Enable GFX",
"deviceRedirection": "端末のリダイレクト",
"enablePrinting": "印刷を有効化",
"enableDrive": "ドライブリダイレクトを有効にする",
"driveName": "ドライブ名",
"drivePath": "ドライブパス",
"createDrivePath": "ドライブパスの作成",
"disableDownload": "ダウンロードを無効化",
"disableUpload": "アップロード無効",
"enableTouch": "タッチを有効化",
"rdpSession": "セッション",
"clientName": "クライアント名",
"consoleSession": "コンソールセッション",
"initialProgram": "初期プログラム",
"serverLayout": "サーバーキーボードのレイアウト",
"timezone": "Timezone",
"gatewaySettings": "ゲートウェイ",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "ゲートウェイポート",
"gatewayUsername": "ゲートウェイユーザー名",
"gatewayPassword": "ゲートウェイパスワード",
"gatewayDomain": "ゲートウェイドメイン",
"remoteApp": "RemoteApp",
"remoteAppProgram": "リモートアプリケーション",
"remoteAppDir": "リモートアプリディレクトリ",
"remoteAppArgs": "リモートアプリ引数",
"clipboardSettings": "Clipboard",
"normalizeClipboard": "クリップボードを正規化",
"disableCopy": "コピーを無効化",
"disablePaste": "貼り付けを無効にする",
"vncSettings": "VNC 設定",
"cursorMode": "カーソルモード",
"swapRedBlue": "赤/青の入れ替え",
"readOnly": "読み取り専用",
"recordingSettings": "録音中",
"recordingPath": "録画パス",
"recordingName": "録音名",
"createRecordingPath": "記録パスを作成",
"excludeOutput": "アウトプットを除外",
"excludeMouse": "マウスを除外",
"includeKeys": "キーを含める",
"sendWolPacket": "WoLパケットを送信",
"wolMacAddr": "MACアドレス",
"wolBroadcastAddr": "放送アドレス",
"wolUdpPort": "UDP Port",
"wolWaitTime": "待機時間 (秒)",
"connectionSettings": "接続設定",
"rdpOnly": "RDP のみ",
"vncOnly": "VNC のみ",
"telnetTerminalSettings": "端末の設定",
"terminalType": "端末の種類",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "配色設定",
"guacBackspaceKey": "バックスペースキー"
},
"guacamole": {
"connecting": "{{type}} セッションに接続中...",
"rdpConnecting": "RDPサーバーに接続中...",
"vncConnecting": "VNC サーバーに接続中...",
"telnetConnecting": "Telnet サーバーに接続中...",
"connectionError": "接続エラー",
"connectionFailed": "接続に失敗しました",
"failedToConnect": "コネクショントークンの取得に失敗しました"
},
"terminal": {
"title": "ターミナル",
@@ -1364,7 +1530,7 @@
"closePanel": "パネルを閉じる",
"reconnect": "再接続",
"sessionEnded": "セッション終了",
"connectionLost": "接続が失われました",
"connectionLost": "接続が切断されました",
"error": "エラー: {{message}}",
"disconnected": "切断されました",
"connectionClosed": "接続が切断されました",
@@ -1372,6 +1538,7 @@
"connected": "接続しました",
"clipboardWriteFailed": "クリップボードにコピーできませんでした。ページがHTTPSまたはlocalhostで配信されていることを確認してください。",
"clipboardReadFailed": "クリップボードからの読み取りに失敗しました。クリップボードのアクセス許可が付与されていることを確認してください。",
"clipboardHttpWarning": "貼り付けには HTTPS が必要です。Ctrl+Shift+V を使用するか、HTTPS 経由で Termix を提供します。",
"sshConnected": "SSH接続が確立されました",
"authError": "認証に失敗しました: {{message}}",
"unknownError": "不明なエラーが発生しました",
@@ -1380,7 +1547,25 @@
"connecting": "接続中...",
"reconnecting": "再接続中... ({{attempt}}/{{max}})",
"reconnected": "再接続に成功しました",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmuxはリモートホストにインストールされておらず、標準シェルに戻ります",
"tmuxSessionPickerTitle": "tmuxセッション",
"tmuxSessionPickerDesc": "このホスト上に既存のtmuxセッションがあります。再アタッチまたは新しいセッションを作成する場合は1つ選択してください。",
"tmuxWindows": "Windows",
"tmuxWindowCount": "{{count}} ウィンドウ",
"tmuxWindowCount_other": "{{count}} ウィンドウ",
"tmuxAttached": "添付されたクライアント",
"tmuxAttachedCount": "{{count}} が添付されました",
"tmuxLastActivity": "最後のアクティビティ",
"tmuxTimeJustNow": "ちょうど今",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "新しいセッションを開始",
"tmuxCopyHint": "選択範囲を調整してEnterキーを押してクリップボードにコピー",
"maxReconnectAttemptsReached": "最大再接続試行回数に達しました",
"closeTab": "閉じる",
"connectionTimeout": "接続タイムアウト",
"terminalTitle": "ターミナル - {{host}}",
"terminalWithPath": "ターミナル - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "認証がタイムアウトしました。もう一度やり直してください。",
"opksshAuthFailed": "認証に失敗しました。資格情報を確認して、もう一度やり直してください。",
"opksshConfigMissing": "OPKSSH設定が見つかりません。OIDCプロバイダ設定で~/.opk/config.ymlを作成してください。ドキュメントを参照してください: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "{{provider}} でサインイン",
"sudoPasswordPopupTitle": "パスワードを入力してください。",
"sudoPasswordPopupHint": "Enterキーを押して挿入、Escキーで閉じる",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "サーバーによって接続が拒否されました。認証とネットワーク設定を確認してください。",
"hostKeyRejected": "SSHホスト鍵の検証が拒否されました。接続がキャンセルされました。",
"sessionTakenOver": "セッションは別のタブで開かれました。再接続中...",
"sessionAttachTimeout": "セッションの添付ファイルがタイムアウトしました。新しい接続を作成しています..."
"sessionAttachTimeout": "セッションの添付ファイルがタイムアウトしました。新しい接続を作成しています...",
"openFileManagerHere": "ここでファイルマネージャーを開く"
},
"fileManager": {
"title": "ファイルマネージャー",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "種類別のカラーコードファイル: folder (red), files (blue), symlinks (green)",
"commandAutocomplete": "コマンドオートコンプリート",
"commandAutocompleteDesc": "コマンド履歴に基づいてターミナルコマンドの自動補完候補を有効にする",
"commandHistoryTracking": "コマンド履歴を保存",
"commandHistoryTrackingDesc": "オートコンプリートとサイドバーの端末コマンドを履歴に保存します。デフォルトではプライバシー保護は無効です。",
"defaultSnippetFoldersCollapsed": "デフォルトでスニペットフォルダを閉じる",
"defaultSnippetFoldersCollapsedDesc": "有効にすると、スニペットタブを開くと、すべてのスニペットフォルダが折りたたまれます",
"terminalSyntaxHighlighting": "端末の構文のハイライト",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "ターミナル出力でコマンド、パス、IP、ログレベルを自動的に強調表示する",
"enableCommandPaletteShortcut": "コマンドパレットショートカットを有効にする",
"enableCommandPaletteShortcutDesc": "左シフトをダブルタップしてコマンドパレットを開き、ホストへの迅速なアクセス",
"enableTerminalSessionPersistence": "永続的なターミナルセッション",
"enableTerminalSessionPersistence": "永続的なタブ/セッション",
"enableTerminalSessionPersistenceDesc": "タブの切り替えやブラウザの閉じ時にSSH接続を維持します (不安定な場合があります)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "データベース",
"healthy": "健康的",
"error": "エラー",
"totalServers": "合計サーバー",
"totalHosts": "ホストの合計",
"totalTunnels": "合計トンネル",
"totalCredentials": "合計資格情報",
"recentActivity": "最近のアクティビティ",
+244 -55
View File
@@ -29,7 +29,7 @@
"failedToFetchCredentials": "자격 증명을 가져오는 데 실패했습니다.",
"credentialDeletedSuccessfully": "자격 증명이 성공적으로 삭제되었습니다.",
"failedToDeleteCredential": "자격 증명 삭제에 실패했습니다.",
"confirmDeleteCredential": "Are you sure you want to delete credential \"{{name}}\"?",
"confirmDeleteCredential": "정말로 \"{{name}}\" 자격 증명을 삭제하시겠습니까?",
"credentialCreatedSuccessfully": "자격 증명이 성공적으로 생성되었습니다.",
"credentialUpdatedSuccessfully": "자격 증명이 성공적으로 업데이트되었습니다.",
"failedToSaveCredential": "자격 증명을 저장하는 데 실패했습니다.",
@@ -47,13 +47,13 @@
"credentialAddedSuccessfully": "자격 증명 \"{{name}}\"이 성공적으로 추가되었습니다.",
"savingCredential": "자격 증명 저장 중...",
"updatingCredential": "자격 증명을 업데이트하는 중...",
"general": "기본설정",
"general": "일반",
"description": "설명",
"folder": "폴더",
"tags": "태그",
"addTagsSpaceToAdd": "태그를 추가하세요 (스페이스 키를 눌러 추가)",
"password": "비밀번호",
"key": "열쇠",
"key": "",
"sshPrivateKey": "SSH 개인 키",
"upload": "업로드",
"updateKey": "업데이트 키",
@@ -162,7 +162,7 @@
"publicKeyGeneratedSuccessfully": "공개 키가 성공적으로 생성되었습니다.",
"detectedKeyType": "감지된 키 유형",
"detectingKeyType": "감지 중...",
"optional": "선택 과목",
"optional": "선택 사항",
"generateKeyPairNew": "새 키 쌍 생성",
"generateEd25519": "Ed25519 생성",
"generateECDSA": "ECDSA를 생성합니다",
@@ -203,7 +203,7 @@
"keyType": "키 유형",
"uploadFile": "파일 업로드",
"pasteKey": "붙여넣기 키",
"credential": "신임장",
"credential": "자격 증명",
"overrideUsername": "자격 증명 사용자 이름 재정의",
"overrideUsernameDesc": "자격 증명에 저장된 사용자 이름과 다른 사용자 이름을 사용하십시오.",
"connectTerminal": "터미널에 연결",
@@ -277,6 +277,19 @@
"copyTooltip": "스니펫을 클립보드에 복사합니다",
"editTooltip": "이 스니펫을 수정하세요",
"deleteTooltip": "이 부분을 삭제하세요",
"shareTooltip": "Share this snippet",
"sharedWithYou": "Shared",
"sharedBy": "Shared by {{username}}",
"shareSnippet": "Share Snippet",
"user": "User",
"role": "Role",
"selectTarget": "Select...",
"currentAccess": "Current Access",
"shareSuccess": "Snippet shared successfully",
"shareFailed": "Failed to share snippet",
"revokeSuccess": "Access revoked",
"revokeFailed": "Failed to revoke access",
"failedToLoadShareData": "Failed to load sharing data",
"newFolder": "새 폴더",
"reorderSameFolder": "같은 폴더 내에서만 스니펫 순서를 변경할 수 있습니다.",
"reorderSuccess": "스니펫 순서가 성공적으로 재정렬되었습니다.",
@@ -291,7 +304,7 @@
"selectTerminals": "터미널 선택 (선택 사항)",
"executeOnSelected": "{{count}} 선택된 터미널에서 실행",
"executeOnCurrent": "현재 터미널에서 실행 (여러 개를 선택하려면 클릭하세요)",
"folder": "접는 사람",
"folder": "폴더",
"selectFolder": "폴더를 선택하거나 비워 두세요.",
"noFolder": "폴더 없음 (미분류)",
"folderName": "폴더 이름",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "인증이 필요합니다. 페이지를 새로고침하세요.",
"dataAccessLockedReauth": "데이터 접근이 잠겼습니다. 다시 인증해 주세요.",
"loading": "명령 기록을 불러오는 중...",
"error": "기록 불러오기 오류"
"error": "기록 불러오기 오류",
"disabledTitle": "Command History is Disabled",
"disabledDescription": "Enable Command History Tracking in your profile under Appearance settings."
},
"splitScreen": {
"title": "분할 화면",
@@ -413,7 +428,7 @@
"success": "성공",
"loading": "로딩 중...",
"required": "필수의",
"optional": "선택 과목",
"optional": "선택 사항",
"connect": "연결하다",
"copied": "복사됨",
"connecting": "연결 중...",
@@ -446,7 +461,7 @@
"resetPassword": "비밀번호 재설정",
"resetCode": "재설정 코드",
"newPassword": "새 비밀번호",
"folder": "접는 사람",
"folder": "폴더",
"file": "파일",
"renamedSuccessfully": "성공적으로 이름이 변경되었습니다",
"deletedSuccessfully": "성공적으로 삭제되었습니다",
@@ -460,7 +475,7 @@
"name": "이름",
"login": "로그인",
"logout": "로그아웃",
"register": "등록하다",
"register": "등록",
"password": "비밀번호",
"version": "버전",
"confirmPassword": "비밀번호 확인",
@@ -471,7 +486,7 @@
"save": "구하다",
"saving": "저장 중...",
"delete": "삭제",
"edit": "편집하다",
"edit": "편집",
"add": "추가하다",
"search": "찾다",
"confirm": "확인하다",
@@ -504,12 +519,14 @@
"failedToInitiatePasswordReset": "비밀번호 재설정을 시작하는 데 실패했습니다.",
"failedToVerifyResetCode": "재설정 코드 확인에 실패했습니다.",
"failedToCompletePasswordReset": "비밀번호 재설정을 완료하지 못했습니다.",
"documentation": "선적 서류 비치",
"documentation": "문서",
"retry": "다시 해 보다",
"checking": "확인 중...",
"checkingDatabase": "데이터베이스 연결을 확인하는 중...",
"checkingAuthentication": "인증 상태를 확인하는 중...",
"backendReconnected": "서버 연결이 복구되었습니다.",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "행위",
"remove": "제거하다",
"revoke": "취소",
@@ -519,7 +536,7 @@
"nav": {
"home": "홈",
"hosts": "호스트",
"credentials": "신임장",
"credentials": "자격 증명",
"terminal": "터미널",
"docker": "도커",
"tunnels": "터널",
@@ -541,7 +558,8 @@
"copySudoPassword": "Sudo 암호 복사",
"passwordCopied": "비밀번호가 클립보드에 복사되었습니다.",
"sudoPasswordCopied": "Sudo 암호가 클립보드에 복사되었습니다.",
"noPasswordAvailable": "비밀번호를 사용할 수 없습니다."
"noPasswordAvailable": "비밀번호를 사용할 수 없습니다.",
"failedToCopyPassword": "Failed to copy password"
},
"admin": {
"title": "관리자 설정",
@@ -554,7 +572,7 @@
"allowRegistration": "등록을 허용합니다",
"oidcSettings": "OIDC 설정",
"clientId": "클라이언트 ID",
"clientSecret": "고객 비밀",
"clientSecret": "클라이언트 암호",
"issuerUrl": "발급자 URL",
"authorizationUrl": "인증 URL",
"tokenUrl": "토큰 URL",
@@ -569,7 +587,7 @@
"scopes": "스코프",
"saving": "저장 중...",
"saveConfiguration": "설정 저장",
"reset": "재설정",
"reset": "초기화",
"success": "성공",
"loading": "로딩 중...",
"refresh": "새로고침",
@@ -585,7 +603,7 @@
"currentAdmins": "현 관리자",
"adminBadge": "관리자",
"removeAdminButton": "관리자 제거",
"general": "일반적인",
"general": "일반",
"userRegistration": "사용자 등록",
"allowNewAccountRegistration": "새 계정 등록을 허용합니다",
"allowPasswordLogin": "사용자 이름/비밀번호 로그인 허용",
@@ -747,10 +765,10 @@
"encryptedBackupCreatedSuccessfully": "암호화된 백업이 성공적으로 생성되었습니다.",
"backupCreationFailed": "백업 생성 실패",
"databaseMigration": "데이터베이스 마이그레이션",
"exportForMigration": "이민을 위한 수출",
"exportForMigration": "마이그레이션을 위해 내보내기",
"exportDatabaseForHardwareMigration": "새 하드웨어로 마이그레이션하기 위해 암호화가 해제된 데이터가 포함된 SQLite 파일로 데이터베이스를 내보냅니다.",
"exportDatabase": "SQLite 데이터베이스 내보내기",
"exporting": "수출 중...",
"exporting": "내보내는 중...",
"exportCreated": "SQLite 내보내기가 생성되었습니다.",
"exportContainsDecryptedData": "SQLite 내보내기 파일에는 암호 해독된 데이터가 포함되어 있습니다. 안전하게 보관하세요!",
"databaseExportedSuccessfully": "SQLite 데이터베이스 내보내기가 성공적으로 완료되었습니다.",
@@ -784,9 +802,9 @@
"migrate": "이주하다",
"backup": "지원",
"createBackup": "백업 생성",
"exportImport": "수출/수입",
"export": "내보내",
"import": "수입",
"exportImport": "내보내기/가져오기",
"export": "내보내",
"import": "가져오기",
"passwordRequired": "비밀번호가 필요합니다",
"confirmExport": "내보내기 확인",
"exportDescription": "SSH 호스트 및 자격 증명을 SQLite 파일로 내보내기",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "글로벌 모니터링 설정이 저장되었습니다.",
"failedToSaveGlobalSettings": "전역 모니터링 설정을 저장하는 데 실패했습니다.",
"failedToLoadGlobalSettings": "전역 모니터링 설정을 불러오는 데 실패했습니다.",
"sessionTimeout": "Session Timeout",
"sessionTimeoutDesc": "Configure how long user sessions last before requiring re-authentication. 'Remember Me' sessions are unaffected (always 30 days).",
"sessionTimeoutHours": "Timeout Duration",
"hours": "hours",
"sessionTimeoutNote": "Valid range: 1720 hours. Changes apply to new sessions only.",
"sessionTimeoutSaved": "Session timeout saved",
"failedToSaveSessionTimeout": "Failed to save session timeout",
"guacamoleIntegration": "원격 데스크톱 통합(Guacamole)",
"guacamoleIntegrationDesc": "guacd를 통해 RDP, VNC 및 Telnet 연결을 활성화합니다. guacd 인스턴스가 실행 중이어야 합니다.",
"enableGuacamole": "RDP/VNC/Telnet 지원을 활성화합니다.",
"guacdUrl": "guacd URL (호스트:포트)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "guacd 호스트/포트 변경 사항을 적용하려면 서버를 다시 시작해야 합니다.",
"guacamoleSettingsSaved": "과카몰리 설정이 저장되었습니다.",
"failedToSaveGuacamoleSettings": "과카몰리 설정 저장에 실패했습니다.",
"failedToLoadGuacamoleSettings": "과카몰리 설정을 불러오는 데 실패했습니다.",
"logLevel": "Log Level",
"logLevelDesc": "Control the verbosity of server logs. Higher levels reduce log output. Can also be set via the LOG_LEVEL environment variable.",
"logVerbosity": "Verbosity",
"logLevelNote": "Changes take effect immediately. Debug level may impact performance.",
"logLevelSaved": "Log level saved",
"failedToSaveLogLevel": "Failed to save log level",
"clampedToValidRange": "유효 범위로 조정되었습니다.",
"loadingSessions": "세션 불러오는 중...",
"noActiveSessions": "활성화된 세션이 없습니다.",
"device": "장치",
@@ -839,10 +880,15 @@
"failedToLoadHosts": "호스트를 로드하는 데 실패했습니다.",
"retry": "다시 해 보다",
"refresh": "새로고침",
"optional": "선택 과목",
"optional": "선택 사항",
"hostsCount": "{{count}} 호스트",
"importJson": "JSON 가져오기",
"importing": "가져오는 중...",
"exportAllJson": "Export All",
"exporting": "Exporting...",
"exportedAllHosts": "Exported {{count}} hosts",
"failedToExportAllHosts": "Failed to export hosts. Please ensure you're logged in and have access to the host data.",
"exportAllSensitiveWarning": "The exported file will contain sensitive authentication data (passwords/SSH keys) in plaintext. Please keep the file secure and delete it after use.",
"importJsonTitle": "JSON에서 SSH 호스트 가져오기",
"importJsonDesc": "JSON 파일을 업로드하여 여러 SSH 호스트(최대 100개)를 일괄 가져올 수 있습니다.",
"downloadSample": "샘플 다운로드",
@@ -866,12 +912,27 @@
"importError": "가져오기 오류",
"failedToImportJson": "JSON 파일을 가져오는 데 실패했습니다.",
"connectionDetails": "연결 정보",
"connectionType": "연결 유형",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "텔넷",
"remoteDesktop": "원격 데스크톱",
"guacamoleSettings": "원격 데스크톱 설정",
"organization": "조직",
"ipAddress": "IP 주소 또는 호스트 이름",
"macAddress": "MAC Address",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Wake-on-LAN packet sent",
"wolFailed": "Failed to send Wake-on-LAN packet",
"ipRequired": "IP 주소가 필요합니다.",
"portRequired": "포트가 필요합니다",
"port": "포트",
"name": "이름",
"username": "사용자 이름",
"folder": "접는 사람",
"usernameRequired": "사용자 이름은 필수 입력 사항입니다 (SSH/Telnet에서만 해당).",
"folder": "폴더",
"tags": "태그",
"pin": "핀",
"notes": "메모",
@@ -944,9 +1005,9 @@
"authentication": "입증",
"password": "비밀번호",
"key": "열쇠",
"credential": "신임장",
"credential": "자격 증명",
"none": "없음",
"opkssh": "오프크시",
"opkssh": "OPKSSH",
"opksshAuthDescription": "접속할 때마다 OPKSSH 웹 인증을 요청합니다. 인증 토큰은 24시간 동안 유효하며, 갱신해야 합니다.",
"selectCredential": "자격증명을 선택하세요",
"selectCredentialPlaceholder": "자격증을 선택하세요...",
@@ -974,11 +1035,13 @@
"terminalBadge": "터미널",
"tunnelBadge": "터널",
"fileManagerBadge": "파일 관리자",
"general": "일반적인",
"general": "일반",
"terminal": "터미널",
"tunnel": "터널",
"fileManager": "파일 관리자",
"serverStats": "서버 통계",
"status": "상태",
"statistics": "통계",
"hostViewer": "호스트 뷰어",
"enableServerStats": "서버 통계 활성화",
"enableServerStatsDesc": "이 호스트에 대한 서버 통계 수집을 활성화/비활성화합니다.",
@@ -1025,12 +1088,14 @@
"dragToMoveBetweenFolders": "드래그하여 폴더 간 이동",
"exportedHostConfig": "{{name}}에 대한 호스트 구성을 내보냈습니다.",
"openTerminal": "터미널 열기",
"openRdp": "Open RDP",
"openVnc": "Open VNC",
"openTelnet": "Open Telnet",
"openFileManager": "파일 관리자 열기",
"openTunnels": "개방형 터널",
"openServerDetails": "오픈 서버 세부 정보",
"statistics": "통계",
"openServerDetails": "서버 세부 정보 열기",
"enabledWidgets": "활성화된 위젯",
"openServerStats": "오픈 서버 통계",
"openServerStats": "서버 통계 열기",
"enabledWidgetsDesc": "이 호스트에 대해 표시할 통계 위젯을 선택하세요.",
"monitoringConfiguration": "모니터링 구성",
"monitoringConfigurationDesc": "서버 통계 및 상태 확인 빈도를 구성합니다.",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "서버가 온라인 상태인지 오프라인 상태인지 확인하십시오.",
"statusCheckInterval": "상태 확인 간격",
"statusCheckIntervalDesc": "호스트가 온라인 상태인지 확인하는 빈도 (5초 ~ 1시간)",
"statusChecks": "상태 확인",
"disableTcpPing": "TCP Ping 비활성화",
"disableTcpPingDescription": "이 호스트에 대한 상태 확인(온라인/오프라인 감지)을 끄세요.",
"useGlobalStatusInterval": "전역 기본값을 사용합니다",
"useGlobalMetricsInterval": "전역 기본값을 사용합니다",
"usingGlobalDefault": "전역 기본값 사용({{value}}s)",
"metricsCollection": "측정항목 수집",
"metricsEnabled": "메트릭 모니터링 활성화",
"metricsEnabledDesc": "CPU, RAM, 디스크 및 기타 시스템 통계를 수집합니다.",
"metricsInterval": "측정항목 수집 간격",
"metricsIntervalDesc": "서버 통계 수집 빈도 (5초 ~ 1시간)",
"metricsNotAvailableForConnectionType": "서버 메트릭은 SSH 호스트에서만 사용할 수 있습니다. TCP 핑 상태 확인은 위에서 활성화할 수 있습니다.",
"intervalSeconds": "초",
"intervalMinutes": "분",
"intervalValidation": "모니터링 간격은 5초에서 1시간(3600초) 사이여야 합니다.",
@@ -1054,11 +1124,11 @@
"statusMonitoring": "상태",
"metricsMonitoring": "측정 기준",
"terminalCustomization": "터미널 맞춤 설정",
"appearance": "모습",
"appearance": "외관",
"behavior": "행동",
"advanced": "고급의",
"themePreview": "테마 미리보기",
"theme": "주제",
"theme": "테마",
"selectTheme": "테마를 선택하세요",
"chooseColorTheme": "터미널에 사용할 색상 테마를 선택하세요",
"fontFamily": "글꼴 패밀리",
@@ -1077,7 +1147,7 @@
"selectCursorStyle": "커서 스타일을 선택하세요",
"cursorStyleBlock": "차단하다",
"cursorStyleUnderline": "밑줄",
"cursorStyleBar": "술집",
"cursorStyleBar": "",
"chooseCursorAppearance": "커서 모양을 선택하세요",
"cursorBlink": "커서 깜빡임",
"enableCursorBlink": "커서 깜빡임 애니메이션 활성화",
@@ -1133,6 +1203,10 @@
"noServerFound": "서버를 찾을 수 없습니다.",
"jumpHostsOrder": "연결은 다음 순서로 이루어집니다: 점프 호스트 1 → 점프 호스트 2 → ... → 대상 서버",
"socks5Proxy": "SOCKS5 프록시",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Send a sequence of connection attempts to specified ports before connecting. Used to open firewall rules dynamically.",
"addKnock": "Add Port",
"delayMs": "Delay",
"socks5Description": "SSH 연결을 위해 SOCKS5 프록시를 구성합니다. 모든 트래픽은 지정된 프록시 서버를 통해 라우팅됩니다.",
"enableSocks5": "SOCKS5 프록시 활성화",
"enableSocks5Description": "이 SSH 연결에는 SOCKS5 프록시를 사용하십시오.",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "연결 시 MOSH 명령을 자동으로 실행합니다.",
"moshCommand": "MOSH 명령",
"moshCommandDesc": "실행할 MOSH 명령어",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automatically attach to an existing (or start a new) tmux session for persistent sessions and cross-device continuity",
"environmentVariables": "환경 변수",
"environmentVariablesDesc": "터미널 세션에 대한 사용자 지정 환경 변수를 설정합니다.",
"variableName": "변수 이름",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "터널 URL 복사",
"copyServerStatsUrl": "서버 통계 URL 복사",
"copyDockerUrl": "Docker URL 복사",
"copyRemoteDesktopUrl": "원격 데스크톱 URL 복사",
"fullScreenUrlTooltip": "이 앱을 전체 화면 모드로 열려면 URL을 복사하세요.",
"notEnabled": "이 호스트에서는 Docker가 활성화되어 있지 않습니다. Docker 기능을 사용하려면 호스트 설정에서 Docker를 활성화하십시오.",
"validating": "Docker 유효성 검사 중...",
@@ -1348,7 +1425,96 @@
"selectAll": "모두 선택하세요",
"deselectAll": "모두 선택 해제",
"useGlobalStatusDefault": "전역 기본값(상태) 사용",
"useGlobalMetricsDefault": "전역 기본값(메트릭) 사용"
"useGlobalMetricsDefault": "전역 기본값(메트릭) 사용",
"remoteDesktopSettings": "원격 데스크톱 설정",
"domain": "도메인",
"securityMode": "보안 모드",
"ignoreCert": "인증서 무시",
"ignoreCertDesc": "이 연결에 대한 TLS 인증서 확인을 건너뜁니다.",
"displaySettings": "디스플레이 설정",
"colorDepth": "색심도",
"width": "너비",
"height": "키",
"dpi": "DPI",
"resizeMethod": "크기 조정 방법",
"forceLossless": "힘 손실 없음",
"audioSettings": "오디오 설정",
"disableAudio": "오디오 비활성화",
"enableAudioInput": "오디오 입력 활성화",
"rdpPerformance": "성능",
"enableWallpaper": "배경화면 활성화",
"enableTheming": "테마 활성화",
"enableFontSmoothing": "글꼴 부드럽게 처리 활성화",
"enableFullWindowDrag": "전체 창 드래그 활성화",
"enableDesktopComposition": "데스크톱 구성 활성화",
"enableMenuAnimations": "메뉴 애니메이션 활성화",
"disableBitmapCaching": "비트맵 캐싱 비활성화",
"disableOffscreenCaching": "오프스크린 캐싱 비활성화",
"disableGlyphCaching": "글리프 캐싱 비활성화",
"enableGfx": "GFX 활성화",
"deviceRedirection": "기기 리디렉션",
"enablePrinting": "인쇄 활성화",
"enableDrive": "드라이브 리디렉션 활성화",
"driveName": "드라이브 이름",
"drivePath": "차량 통행로",
"createDrivePath": "드라이브 경로 생성",
"disableDownload": "다운로드 비활성화",
"disableUpload": "업로드 비활성화",
"enableTouch": "터치 활성화",
"rdpSession": "세션",
"clientName": "클라이언트 이름",
"consoleSession": "콘솔 세션",
"initialProgram": "초기 프로그램",
"serverLayout": "서버 키보드 레이아웃",
"timezone": "시간대",
"gatewaySettings": "게이트웨이",
"gatewayHostname": "게이트웨이 호스트 이름",
"gatewayPort": "게이트웨이 포트",
"gatewayUsername": "게이트웨이 사용자 이름",
"gatewayPassword": "게이트웨이 비밀번호",
"gatewayDomain": "게이트웨이 도메인",
"remoteApp": "원격 앱",
"remoteAppProgram": "원격 애플리케이션",
"remoteAppDir": "원격 앱 디렉터리",
"remoteAppArgs": "원격 앱 인수",
"clipboardSettings": "클립보드",
"normalizeClipboard": "클립보드 정규화",
"disableCopy": "복사 비활성화",
"disablePaste": "붙여넣기 비활성화",
"vncSettings": "VNC 설정",
"cursorMode": "커서 모드",
"swapRedBlue": "빨간색/파란색 바꾸기",
"readOnly": "읽기 전용",
"recordingSettings": "녹음",
"recordingPath": "기록 경로",
"recordingName": "녹음 이름",
"createRecordingPath": "녹화 경로 생성",
"excludeOutput": "출력 제외",
"excludeMouse": "마우스 제외",
"includeKeys": "키를 포함하세요",
"sendWolPacket": "WoL 패킷을 전송합니다",
"wolMacAddr": "MAC 주소",
"wolBroadcastAddr": "방송 주소",
"wolUdpPort": "UDP 포트",
"wolWaitTime": "대기 시간(초)",
"connectionSettings": "연결 설정",
"rdpOnly": "RDP 전용",
"vncOnly": "VNC 전용",
"telnetTerminalSettings": "터미널 설정",
"terminalType": "터미널 유형",
"guacFontName": "글꼴 이름",
"guacFontSize": "글꼴 크기",
"guacColorScheme": "색 구성표",
"guacBackspaceKey": "백스페이스 키"
},
"guacamole": {
"connecting": "{{type}} 세션에 연결 중...",
"rdpConnecting": "RDP 서버에 연결 중...",
"vncConnecting": "VNC 서버에 연결 중...",
"telnetConnecting": "텔넷 서버에 연결 중...",
"connectionError": "연결 오류",
"connectionFailed": "연결 실패",
"failedToConnect": "연결 토큰을 가져오는 데 실패했습니다."
},
"terminal": {
"title": "터미널",
@@ -1364,7 +1530,7 @@
"closePanel": "패널 닫기",
"reconnect": "다시 연결",
"sessionEnded": "세션이 종료되었습니다",
"connectionLost": "연결이 끊어졌습니다",
"connectionLost": "Connection lost",
"error": "오류: {{message}}",
"disconnected": "연결이 끊어졌습니다",
"connectionClosed": "연결이 종료되었습니다",
@@ -1372,6 +1538,7 @@
"connected": "연결됨",
"clipboardWriteFailed": "클립보드에 복사하는 데 실패했습니다. 페이지가 HTTPS 또는 localhost를 통해 제공되는지 확인하세요.",
"clipboardReadFailed": "클립보드에서 읽는 데 실패했습니다. 클립보드 권한이 부여되었는지 확인하십시오.",
"clipboardHttpWarning": "붙여넣기에는 HTTPS가 필요합니다. Ctrl+Shift+V를 사용하거나 Termix를 HTTPS를 통해 제공하십시오.",
"sshConnected": "SSH 연결이 설정되었습니다.",
"authError": "인증 실패: {{message}}",
"unknownError": "알 수 없는 오류가 발생했습니다.",
@@ -1380,7 +1547,25 @@
"connecting": "연결 중...",
"reconnecting": "다시 연결 중... ({{attempt}}/{{max}})",
"reconnected": "성공적으로 다시 연결되었습니다.",
"tmuxSessionCreated": "tmux session created: {{name}}",
"tmuxSessionAttached": "tmux session attached: {{name}}",
"tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.",
"tmuxWindows": "Windows",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "{{count}} windows",
"tmuxAttached": "Attached clients",
"tmuxAttachedCount": "{{count}} attached",
"tmuxLastActivity": "Last activity",
"tmuxTimeJustNow": "just now",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Start new session",
"tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard",
"maxReconnectAttemptsReached": "최대 재연결 시도 횟수에 도달했습니다.",
"closeTab": "Close",
"connectionTimeout": "연결 시간 초과",
"terminalTitle": "터미널 - {{host}}",
"terminalWithPath": "터미널 - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "인증 시간이 초과되었습니다. 다시 시도해 주세요.",
"opksshAuthFailed": "인증에 실패했습니다. 자격 증명을 확인하고 다시 시도해 주세요.",
"opksshConfigMissing": "OPKSSH 구성 파일을 찾을 수 없습니다. OIDC 공급자 설정이 포함된 ~/.opk/config.yml 파일을 생성하십시오. 자세한 내용은 다음 문서를 참조하십시오. https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Sign in with {{provider}}",
"sudoPasswordPopupTitle": "비밀번호를 입력하시겠습니까?",
"sudoPasswordPopupHint": "Enter 키를 눌러 삽입하고, Esc 키를 눌러 닫습니다.",
"sudoPasswordPopupConfirm": "끼워 넣다",
@@ -1428,12 +1614,13 @@
"connectionRejected": "서버에서 연결이 거부되었습니다. 인증 및 네트워크 구성을 확인하십시오.",
"hostKeyRejected": "SSH 호스트 키 인증이 거부되었습니다. 연결이 취소되었습니다.",
"sessionTakenOver": "세션이 다른 탭에서 열려 있습니다. 다시 연결 중...",
"sessionAttachTimeout": "세션 연결 시간이 초과되었습니다. 새 연결을 생성합니다..."
"sessionAttachTimeout": "세션 연결 시간이 초과되었습니다. 새 연결을 생성합니다...",
"openFileManagerHere": "Open File Manager Here"
},
"fileManager": {
"title": "파일 관리자",
"file": "파일",
"folder": "접는 사람",
"folder": "폴더",
"connectToSsh": "파일 작업을 사용하려면 SSH에 연결하십시오.",
"uploadFile": "파일 업로드",
"downloadFile": "다운로드",
@@ -1453,7 +1640,7 @@
"compressingFiles": "{{count}} 항목을 {{name}}로 압축 중...",
"filesCompressedSuccessfully": "{{name}} 이 성공적으로 생성되었습니다",
"compressFailed": "압축 실패",
"edit": "편집하다",
"edit": "편집",
"preview": "시사",
"previous": "이전의",
"next": "다음",
@@ -1930,7 +2117,7 @@
"loginTitle": "Termix에 로그인하세요",
"registerTitle": "계정 생성",
"loginButton": "로그인",
"registerButton": "등록하다",
"registerButton": "등록",
"forgotPassword": "비밀번호를 잊으셨나요?",
"rememberMe": "30일 동안 기기 정보를 기억해 두세요 (TOTP 포함).",
"noAccount": "계정이 없으신가요?",
@@ -2126,13 +2313,15 @@
"fileColorCodingDesc": "파일 유형별로 색상을 지정합니다. 폴더(빨간색), 파일(파란색), 심볼릭 링크(녹색)",
"commandAutocomplete": "명령어 자동 완성",
"commandAutocompleteDesc": "명령어 기록을 기반으로 터미널 명령어에 대한 Tab 키 자동 완성 제안을 활성화합니다.",
"commandHistoryTracking": "Save Command History",
"commandHistoryTrackingDesc": "Store terminal commands in history for autocomplete and sidebar. Disabled by default for privacy.",
"defaultSnippetFoldersCollapsed": "기본적으로 스니펫 폴더를 접으세요",
"defaultSnippetFoldersCollapsedDesc": "이 옵션을 활성화하면 스니펫 탭을 열 때 모든 스니펫 폴더가 접혀 표시됩니다.",
"terminalSyntaxHighlighting": "터미널 구문 강조 표시",
"showHostTags": "호스트 태그 표시",
"showHostTagsDesc": "사이드바의 각 호스트 아래에 태그를 표시합니다. 이 기능을 비활성화하면 모든 태그가 숨겨집니다.",
"account": "계정",
"appearance": "모습",
"appearance": "외관",
"languageLocalization": "언어 및 현지화",
"fileManagerSettings": "파일 관리자",
"terminalSettings": "터미널",
@@ -2146,15 +2335,15 @@
"currentPassword": "현재 비밀번호",
"passwordChangedSuccess": "비밀번호가 변경되었습니다! 다시 로그인해 주세요.",
"failedToChangePassword": "비밀번호 변경에 실패했습니다. 현재 비밀번호를 확인하고 다시 시도해 주세요.",
"theme": "주제",
"themeLight": "",
"themeDark": "어두운",
"themeSystem": "체계",
"theme": "테마",
"themeLight": "라이트",
"themeDark": "다크",
"themeSystem": "시스템",
"appearanceDesc": "애플리케이션의 색상 테마를 선택하세요",
"terminalSyntaxHighlightingDesc": "터미널 출력에서 명령어, 경로, IP 주소 및 로그 레벨을 자동으로 강조 표시합니다.",
"enableCommandPaletteShortcut": "명령 팔레트 바로가기 활성화",
"enableCommandPaletteShortcutDesc": "왼쪽 Shift 키를 두 번 누르면 호스트에 빠르게 접근할 수 있는 명령 팔레트가 열립니다.",
"enableTerminalSessionPersistence": "영구 터미널 세션",
"enableTerminalSessionPersistence": "고정 탭/세션",
"enableTerminalSessionPersistenceDesc": "탭을 전환하거나 브라우저를 닫을 때 SSH 연결을 유지합니다(불안정할 수 있음)."
},
"user": {
@@ -2169,7 +2358,7 @@
"language": "언어",
"username": "사용자 이름",
"hostname": "호스트 이름",
"folder": "접는 사람",
"folder": "폴더",
"password": "비밀번호",
"keyPassword": "키 비밀번호",
"sudoPassword": "sudo 비밀번호 (선택 사항)",
@@ -2183,7 +2372,7 @@
"sshConfig": "엔드포인트 SSH 구성",
"homePath": "/home",
"clientId": "클라이언트 ID",
"clientSecret": "고객 비밀",
"clientSecret": "클라이언트 암호",
"authUrl": "https://your-provider.com/application/o/authorize/",
"redirectUrl": "https://your-provider.com/application/o/termix/",
"tokenUrl": "https://your-provider.com/application/o/token/",
@@ -2259,7 +2448,7 @@
"sshHosts": "SSH 호스트",
"importSshHosts": "JSON에서 SSH 호스트 가져오기",
"clientId": "클라이언트 ID",
"clientSecret": "고객 비밀",
"clientSecret": "클라이언트 암호",
"error": "오류",
"warning": "경고",
"deleteAccount": "계정 삭제",
@@ -2364,11 +2553,11 @@
"database": "데이터 베이스",
"healthy": "건강한",
"error": "오류",
"totalServers": "총 서버 수",
"totalHosts": "Total Hosts",
"totalTunnels": "토탈 터널",
"totalCredentials": "총 자격 증명",
"recentActivity": "최근 활동",
"reset": "다시 놓기",
"reset": "초기화",
"loadingRecentActivity": "최근 활동 불러오는 중...",
"noRecentActivity": "최근 활동 없음",
"quickActions": "빠른 조치",
@@ -2421,7 +2610,7 @@
"openFileManager": "파일 관리자 열기",
"openTunnel": "개방형 터널",
"openDocker": "오픈 도커",
"openServerStats": "오픈 서버 세부 정보",
"openServerStats": "서버 세부 정보 열기",
"moveToGroupAction": "그룹으로 이동",
"removeFromGroup": "그룹에서 제거",
"addHostHere": "여기에 호스트를 추가하세요",
@@ -2636,9 +2825,9 @@
"userProfile": "사용자 프로필",
"updateLog": "업데이트 로그",
"hosts": "호스트",
"openServerDetails": "오픈 서버 세부 정보",
"openServerDetails": "서버 세부 정보 열기",
"openFileManager": "파일 관리자 열기",
"edit": "편집하다",
"edit": "편집",
"links": "링크",
"github": "깃허브",
"support": "문의하기",
@@ -2776,7 +2965,7 @@
}
},
"theme": {
"switchToLight": "밝은테마로 변경",
"switchToDark": "어두운테마로 변경"
"switchToLight": "라이트테마로 변경",
"switchToDark": "다크 테마로 변경"
}
}
+196 -7
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Kopieer snippet naar klembord",
"editTooltip": "Tekstfragment bewerken",
"deleteTooltip": "Tekstfragment verwijderen",
"shareTooltip": "Deel deze tekstbouwsteen",
"sharedWithYou": "Gedeeld",
"sharedBy": "Gedeeld door {{username}}",
"shareSnippet": "Deel snippet",
"user": "Gebruiker",
"role": "Functie",
"selectTarget": "Selecteer...",
"currentAccess": "Huidige Toegang",
"shareSuccess": "Snippet succesvol gedeeld",
"shareFailed": "Delen van snippet mislukt",
"revokeSuccess": "Toegang ingetrokken",
"revokeFailed": "Toegang intrekken mislukt",
"failedToLoadShareData": "Laden van delen van gegevens mislukt",
"newFolder": "Folder toevoegen",
"reorderSameFolder": "Kan alleen snippets binnen dezelfde map herordenen",
"reorderSuccess": "Tekstbouwstenen met succes gerangschikt",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Authenticatie vereist. Herlaad de pagina.",
"dataAccessLockedReauth": "Gegevenstoegang geblokkeerd. Gelieve opnieuw te verifiëren.",
"loading": "Opdrachtgeschiedenis wordt geladen...",
"error": "Fout bij laden geschiedenis"
"error": "Fout bij laden geschiedenis",
"disabledTitle": "Opdrachtgeschiedenis is uitgeschakeld",
"disabledDescription": "Activeer het bijhouden van opdrachten geschiedenis in je profiel onder instellingen van Uiterlijk"
},
"splitScreen": {
"title": "Scherm splitsen",
@@ -510,6 +525,8 @@
"checkingDatabase": "Database-verbinding controleren...",
"checkingAuthentication": "Authenticatie controleren...",
"backendReconnected": "Serververbinding hersteld",
"connectionDegraded": "Server connection lost, recovering…",
"reload": "Reload",
"actions": "acties",
"remove": "Verwijderen",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Sudo wachtwoord kopiëren",
"passwordCopied": "Wachtwoord gekopieerd naar klembord",
"sudoPasswordCopied": "Sudo wachtwoord gekopieerd naar klembord",
"noPasswordAvailable": "Geen wachtwoord beschikbaar"
"noPasswordAvailable": "Geen wachtwoord beschikbaar",
"failedToCopyPassword": "Wachtwoord kopiëren mislukt"
},
"admin": {
"title": "Beheerder Instellingen",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Algemene monitoring instellingen opgeslagen",
"failedToSaveGlobalSettings": "Fout bij het opslaan van de globale monitoring instellingen",
"failedToLoadGlobalSettings": "Fout bij het laden van de globale monitoring instellingen",
"sessionTimeout": "Sessie time-out",
"sessionTimeoutDesc": "Configureer hoe lang gebruikerssessies duren voordat opnieuw authenticatie vereist is. 'Onthoud mij' sessies worden niet beïnvloed (altijd 30 dagen).",
"sessionTimeoutHours": "Time-out duur",
"hours": "Uren",
"sessionTimeoutNote": "Geldige bereik: 1 tot 720 uur. Wijzigingen zijn alleen van toepassing op nieuwe sessies.",
"sessionTimeoutSaved": "Sessie timeout opgeslagen",
"failedToSaveSessionTimeout": "Opslaan van sessie timeout mislukt",
"guacamoleIntegration": "Externe Desktop integratie (Guacamole)",
"guacamoleIntegrationDesc": "Schakel RDP, VNC en Telnet verbindingen in via guacd. Vereist een guacd instantie om te worden uitgevoerd.",
"enableGuacamole": "RDP/VNC/Telnet ondersteuning inschakelen",
"guacdUrl": "guacd URL (host:poort)",
"guacdUrlPlaceholder": "adviseur:4822",
"guacdUrlNote": "Wijzigingen in de guacd host/poort vereisen een herstart van de server van kracht.",
"guacamoleSettingsSaved": "Guacamole instellingen opgeslagen",
"failedToSaveGuacamoleSettings": "Fout bij het opslaan van guacamole instellingen",
"failedToLoadGuacamoleSettings": "Fout bij het laden van guacamole instellingen",
"logLevel": "Log niveau",
"logLevelDesc": "Bestuur de uitgebreide server logs. Hogere niveaus verminderen loguitvoer. Kan ook worden ingesteld via de LOG_LEVEL omgeving variabele.",
"logVerbosity": "Verbossing",
"logLevelNote": "Wijzigingen worden onmiddellijk van kracht. Debug niveau kan de prestaties beïnvloeden.",
"logLevelSaved": "Logboek niveau opgeslagen",
"failedToSaveLogLevel": "Fout bij het opslaan van logboek niveau",
"clampedToValidRange": "is aangepast aan geldig bereik",
"loadingSessions": "Sessies laden...",
"noActiveSessions": "Geen actieve sessies gevonden.",
"device": "Apparaat",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} hosts",
"importJson": "JSON importeren",
"importing": "Importeren...",
"exportAllJson": "Alles exporteren",
"exporting": "Exporteren...",
"exportedAllHosts": "Geëxporteerde {{count}} host",
"failedToExportAllHosts": "Kan geen hosts exporteren. Zorg ervoor dat je ingelogd bent en toegang hebt tot de hostgegevens.",
"exportAllSensitiveWarning": "Het geëxporteerde bestand zal gevoelige verificatiegegevens (wachtwoorden/SSH sleutels) in platte tekst bevatten. Houd het bestand veilig en verwijder het na gebruik.",
"importJsonTitle": "SSH hosts importeren vanuit JSON",
"importJsonDesc": "Upload een JSON-bestand om meerdere SSH hosts te importeren (max 100).",
"downloadSample": "Download voorbeeld",
@@ -866,11 +912,26 @@
"importError": "Fout bij importeren",
"failedToImportJson": "Kan JSON bestand niet importeren",
"connectionDetails": "Connectie Details",
"connectionType": "Type verbinding",
"ssh": "ZZ",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Extern bureaublad",
"guacamoleSettings": "Externe bureaublad instellingen",
"organization": "Rekening",
"ipAddress": "IP-adres of hostnaam",
"macAddress": "MAC Adres",
"macAddressDesc": "Voor Wake-on-LAN. formaat: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Wake-on-LAN-pakket verzonden",
"wolFailed": "Wake-on-LAN-pakket verzenden mislukt",
"ipRequired": "IP-adres is vereist",
"portRequired": "Poort is vereist",
"port": "Poort",
"name": "naam",
"username": "Gebruikersnaam",
"usernameRequired": "Gebruikersnaam is vereist (SSH/Telnet alleen)",
"folder": "Map",
"tags": "Labels",
"pin": "Vastzetten",
@@ -979,6 +1040,8 @@
"tunnel": "Tunnel",
"fileManager": "Bestands Beheer",
"serverStats": "Server Statistieken",
"status": "status",
"statistics": "Statistieken",
"hostViewer": "Host Viewer",
"enableServerStats": "Server statistieken inschakelen",
"enableServerStatsDesc": "Schakel server statistieken verzameling voor deze host in/uit",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Sleep om tussen mappen te verplaatsen",
"exportedHostConfig": "Hostconfiguratie voor {{name}} geëxporteerd",
"openTerminal": "Open terminal",
"openRdp": "Open RDP",
"openVnc": "Open VNC",
"openTelnet": "Telnet openen",
"openFileManager": "Bestandsbeheer openen",
"openTunnels": "Open Tunnels",
"openServerDetails": "Server details openen",
"statistics": "Statistieken",
"enabledWidgets": "Ingeschakelde Widgets",
"openServerStats": "Open Server Statistieken",
"enabledWidgetsDesc": "Selecteer welke statistieken widgets moeten worden weergegeven voor deze host",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Controleer of de server online of offline is",
"statusCheckInterval": "Status controle interval",
"statusCheckIntervalDesc": "Hoe vaak controleren of host online is (5s - 1h)",
"statusChecks": "Status controles",
"disableTcpPing": "TCP Ping uitschakelen",
"disableTcpPingDescription": "Statuscontroles (online/offline detectie) voor deze host uitschakelen",
"useGlobalStatusInterval": "Globale standaard gebruiken",
"useGlobalMetricsInterval": "Globale standaard gebruiken",
"usingGlobalDefault": "Gebruik van globale standaard ({{value}}s)",
"metricsCollection": "Collectie Statistieken",
"metricsEnabled": "Metrics monitoring inschakelen",
"metricsEnabledDesc": "Verzamel CPU, RAM, schijf en andere systeemstatistieken",
"metricsInterval": "Metrics Collectie Interval",
"metricsIntervalDesc": "Hoe vaak serverstatistieken worden verzameld (5s - 1h)",
"metricsNotAvailableForConnectionType": "Serverstatistieken zijn alleen beschikbaar voor SSH hosts. TCP ping statuscontroles kunnen nog steeds worden ingeschakeld hierboven.",
"intervalSeconds": "seconden",
"intervalMinutes": "minuten",
"intervalValidation": "Controleintervallen moeten tussen de 5 en 1 uur zijn (3600 seconden)",
@@ -1133,6 +1203,10 @@
"noServerFound": "Geen server gevonden",
"jumpHostsOrder": "Verbindingen worden gemaakt in volgorde: Sprong Host 1 → Jump Host 2 → ... → Doel Server",
"socks5Proxy": "SOCKS5 Proxy",
"portKnocking": "Port Knocking",
"portKnockingDesc": "Stuur een reeks verbindingpogingen naar de opgegeven poorten voordat je verbindt. Wordt gebruikt om firewall regels dynamisch te openen.",
"addKnock": "Poort toevoegen",
"delayMs": "Vertraging",
"socks5Description": "Configureer SOCKS5 proxy voor SSH verbinding. Al het verkeer zal worden omgeleid via de opgegeven proxyserver.",
"enableSocks5": "SOCKS5 Proxy inschakelen",
"enableSocks5Description": "Gebruik SOCKS5 proxy voor deze SSH verbinding",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "MOSH automatisch uitvoeren bij verbinden",
"moshCommand": "MEER commando",
"moshCommandDesc": "De MOSH opdracht om uit te voeren",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Voeg automatisch een bestaande (of nieuwe sessie) tmux toe voor aanhoudende sessies en cross-device continuïteit",
"environmentVariables": "Omgeving variabelen",
"environmentVariablesDesc": "Aangepaste omgevingsvariabelen voor de terminale sessie instellen",
"variableName": "Naam variabele",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Kopieer Tunnel URL",
"copyServerStatsUrl": "Kopieer server statistieken URL",
"copyDockerUrl": "Docker URL kopiëren",
"copyRemoteDesktopUrl": "Kopieer URL van extern bureaublad",
"fullScreenUrlTooltip": "URL kopiëren om deze app te openen in volledig scherm modus",
"notEnabled": "Docker is niet ingeschakeld voor deze host. Zet het aan in Hostinstellingen om de Docker-functies te gebruiken.",
"validating": "Docker valideren...",
@@ -1348,7 +1425,96 @@
"selectAll": "Alles selecteren",
"deselectAll": "Deselecteer alles",
"useGlobalStatusDefault": "Gebruik Global Default (Status)",
"useGlobalMetricsDefault": "Gebruik Globale Standaard (Metrieken)"
"useGlobalMetricsDefault": "Gebruik Globale Standaard (Metrieken)",
"remoteDesktopSettings": "Externe bureaublad instellingen",
"domain": "Domein",
"securityMode": "Beveiliging modus",
"ignoreCert": "Certificaat negeren",
"ignoreCertDesc": "TLS-certificaat-verificatie voor deze verbinding overslaan",
"displaySettings": "Weergave instellingen",
"colorDepth": "Kleur diepte",
"width": "Width",
"height": "Højde",
"dpi": "DPI",
"resizeMethod": "Methode aanpassen",
"forceLossless": "Verlies afdwingen",
"audioSettings": "Audio instellingen",
"disableAudio": "Audio uitschakelen",
"enableAudioInput": "Audio-invoer inschakelen",
"rdpPerformance": "Prestaties",
"enableWallpaper": "Achtergrond inschakelen",
"enableTheming": "Thema inschakelen",
"enableFontSmoothing": "Font vloeiend maken inschakelen",
"enableFullWindowDrag": "Activeer volledig venster slepen",
"enableDesktopComposition": "Bureaublad compositie inschakelen",
"enableMenuAnimations": "Menu animaties inschakelen",
"disableBitmapCaching": "Bitmap caching uitschakelen",
"disableOffscreenCaching": "Offscreen caching uitschakelen",
"disableGlyphCaching": "Glief caching uitschakelen",
"enableGfx": "Enable GFX",
"deviceRedirection": "Apparaat omleiding",
"enablePrinting": "Afdrukken inschakelen",
"enableDrive": "Schijf omleiding inschakelen",
"driveName": "Drive Naam",
"drivePath": "Drive pad",
"createDrivePath": "Maak Drive Pad",
"disableDownload": "Download uitschakelen",
"disableUpload": "Uploaden uitschakelen",
"enableTouch": "Aanraking inschakelen",
"rdpSession": "Sessie",
"clientName": "Naam klant",
"consoleSession": "Console sessie",
"initialProgram": "Eerste programma",
"serverLayout": "Server toetsenbord lay-out",
"timezone": "Timezone",
"gatewaySettings": "Gateway",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Gateway poort",
"gatewayUsername": "Gateway Gebruikersnaam",
"gatewayPassword": "Gateway Wachtwoord",
"gatewayDomain": "Gateway Domein",
"remoteApp": "RemoteApp",
"remoteAppProgram": "Externe Applicatie",
"remoteAppDir": "Map voor externe app",
"remoteAppArgs": "Externe App Argumenten",
"clipboardSettings": "Klembord",
"normalizeClipboard": "Klembord normaliseren",
"disableCopy": "Kopiëren uitschakelen",
"disablePaste": "Plakken uitschakelen",
"vncSettings": "VNC instellingen",
"cursorMode": "Cursor Modus",
"swapRedBlue": "Rood/Blauw omwisselen",
"readOnly": "Alleen lezen",
"recordingSettings": "Opnemenchar@@0",
"recordingPath": "Opname pad",
"recordingName": "Naam van opname",
"createRecordingPath": "Maak Opname-pad",
"excludeOutput": "Uitvoer uitsluiten",
"excludeMouse": "Uitsluiten Muis",
"includeKeys": "Sleutels toevoegen",
"sendWolPacket": "WoL-pakket verzenden",
"wolMacAddr": "MAC Adres",
"wolBroadcastAddr": "Adres uitzenden",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Wachttijd (seconden)",
"connectionSettings": "Verbindingsinstellingen",
"rdpOnly": "Alleen RDP",
"vncOnly": "VNC alleen",
"telnetTerminalSettings": "Terminal Instellingen",
"terminalType": "Terminal type",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Kleurenschema",
"guacBackspaceKey": "Backspace Sleutel"
},
"guacamole": {
"connecting": "Verbinden met {{type}} sessie...",
"rdpConnecting": "Verbinden met RDP-server...",
"vncConnecting": "Verbinden met VNC server...",
"telnetConnecting": "Verbinden met Telnet server...",
"connectionError": "Verbindingsfout",
"connectionFailed": "Verbinding mislukt",
"failedToConnect": "Verbinding token ophalen mislukt"
},
"terminal": {
"title": "Terminal",
@@ -1372,6 +1538,7 @@
"connected": "Verbonden",
"clipboardWriteFailed": "Kopiëren naar klembord mislukt. Zorg ervoor dat de pagina via HTTPS of localhost wordt geserveerd.",
"clipboardReadFailed": "Lezen van klembord mislukt. Zorg ervoor dat machtigingen voor klembord zijn verleend.",
"clipboardHttpWarning": "Plakken vereist HTTPS. Gebruik Ctrl+Shift+V of gebruik Termix over HTTPS.",
"sshConnected": "SSH verbinding ingesteld",
"authError": "Authenticatie mislukt: {{message}}",
"unknownError": "Onbekende fout opgetreden",
@@ -1380,7 +1547,25 @@
"connecting": "Verbinden...",
"reconnecting": "Opnieuw verbinden... ({{attempt}}/{{max}})",
"reconnected": "Succesvol opnieuw verbonden",
"tmuxSessionCreated": "tmux sessie gemaakt: {{name}}",
"tmuxSessionAttached": "tmux sessie gekoppeld: {{name}}",
"tmuxUnavailable": "tmux is niet geïnstalleerd op de externe host, terug vallen op standaard shell",
"tmuxSessionPickerTitle": "tmux sessies",
"tmuxSessionPickerDesc": "Bestaande tmux-sessies gevonden in deze host. Selecteer er een om opnieuw toe te voegen of maak een nieuwe sessie aan.",
"tmuxWindows": "Vensters",
"tmuxWindowCount": "{{count}} venster",
"tmuxWindowCount_other": "{{count}} vensters",
"tmuxAttached": "Bijgevoegde klanten",
"tmuxAttachedCount": "{{count}} bijgevoegd",
"tmuxLastActivity": "Laatste activiteit",
"tmuxTimeJustNow": "op dit moment",
"tmuxTimeMinutes": "{{count}}m geleden",
"tmuxTimeHours": "{{count}}uur geleden",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Nieuwe sessie starten",
"tmuxCopyHint": "Pas de selectie aan en druk op Enter om naar het klembord te kopiëren",
"maxReconnectAttemptsReached": "Maximum aantal herverbindingpogingen bereikt",
"closeTab": "Afsluiten",
"connectionTimeout": "Connectie time-out",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Authenticatie is verlopen. Probeer het opnieuw.",
"opksshAuthFailed": "Authenticatie mislukt. Controleer uw inloggegevens en probeer het opnieuw.",
"opksshConfigMissing": "OPKSSH configuratie niet gevonden. Maak ~/.opk/config.yml aan met uw OIDC providerinstellingen. Zie documentatie: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Log in met {{provider}}",
"sudoPasswordPopupTitle": "Wachtwoord invoeren?",
"sudoPasswordPopupHint": "Druk op Enter om in te voegen, Esc om te sluiten",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Verbinding geweigerd door de server. Controleer uw authenticatie en netwerkconfiguratie.",
"hostKeyRejected": "SSH host sleutel verificatie afgewezen. Verbinding geannuleerd.",
"sessionTakenOver": "Sessie is geopend in een ander tabblad. Opnieuw verbinden...",
"sessionAttachTimeout": "Sessie bijlage is verlopen. Nieuwe verbinding maken..."
"sessionAttachTimeout": "Sessie bijlage is verlopen. Nieuwe verbinding maken...",
"openFileManagerHere": "Open bestandsbeheer hier"
},
"fileManager": {
"title": "Bestands Beheer",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Color-code bestanden per type: mappen (rood), bestanden (blauw), symlinks (groen)",
"commandAutocomplete": "Commando automatisch aanvullen",
"commandAutocompleteDesc": "Schakel Tab key automatisch complete suggesties in voor terminal commando's gebaseerd op je opdrachtgeschiedenis",
"commandHistoryTracking": "Opdrachtgeschiedenis opslaan",
"commandHistoryTrackingDesc": "Terminal-opdrachten in de geschiedenis opslaan voor autocomplete en sidebar. Standaard uitgeschakeld voor privacy.",
"defaultSnippetFoldersCollapsed": "Vouw Snippet mappen standaard in",
"defaultSnippetFoldersCollapsedDesc": "Wanneer ingeschakeld, worden alle snippetmappen samengevouwen wanneer u het tabblad snippets opent",
"terminalSyntaxHighlighting": "Terminal syntaxismarkering",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Automatisch commando's, pad en IP's markeren en levels loggen in de einduitvoer",
"enableCommandPaletteShortcut": "Schakel Command Palette Snelkoppeling in",
"enableCommandPaletteShortcutDesc": "Dubbeltikken op linker Shift om het Opdrachtpalet te openen voor snelle toegang tot hosts",
"enableTerminalSessionPersistence": "Persistente terminal sessies",
"enableTerminalSessionPersistence": "Aanhoudende Tabs/Sessies",
"enableTerminalSessionPersistenceDesc": "Behoud SSH verbindingen bij het wisselen van tabs of het sluiten van de browser (kan instabiel zijn)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Database",
"healthy": "Gezond",
"error": "Foutmelding",
"totalServers": "Totaal servers",
"totalHosts": "Totaal aantal hosts",
"totalTunnels": "Totaal aantal tunnels",
"totalCredentials": "Totaal Aanmeldgegevens",
"recentActivity": "Recente Activiteiten",
+196 -7
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Kopier snippet til utklippstavlen",
"editTooltip": "Rediger denne snippet",
"deleteTooltip": "Slett denne snippet",
"shareTooltip": "Del denne nebet",
"sharedWithYou": "Delt",
"sharedBy": "Delt av {{username}}",
"shareSnippet": "Del tekstutdrag",
"user": "Bruker",
"role": "Rolle",
"selectTarget": "Velg...",
"currentAccess": "Gjeldende tilgang",
"shareSuccess": "Slutt ble delt",
"shareFailed": "Kunne ikke dele rulling",
"revokeSuccess": "Tilgang tilbakekalt",
"revokeFailed": "Kunne ikke oppheve tilgangen",
"failedToLoadShareData": "Kunne ikke laste delingsdata",
"newFolder": "Ny mappe",
"reorderSameFolder": "Kan kun endre rekkefølgen av snippets i samme mappe",
"reorderSuccess": "Snippets ble resortert",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Autentisering kreves. Vennligst oppdater siden.",
"dataAccessLockedReauth": "Tilgang til data låst. Godkjennes på nytt.",
"loading": "Laster kommandoelogg...",
"error": "Feil ved lasting av historikk"
"error": "Feil ved lasting av historikk",
"disabledTitle": "Kommandohistorie er deaktivert",
"disabledDescription": "Aktiver kommandohistorie sporing i profilen din under Utseende innstillinger."
},
"splitScreen": {
"title": "Delt skjerm",
@@ -510,6 +525,8 @@
"checkingDatabase": "Kontrollerer databaseforbindelse...",
"checkingAuthentication": "Kontrollerer autentisering...",
"backendReconnected": "Servertilkobling gjenopprettet",
"connectionDegraded": "Servertilkoblingen ble avbrutt, gjenopprettes…",
"reload": "Reload",
"actions": "Handlinger",
"remove": "Fjern",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Kopier Sudo passord",
"passwordCopied": "Passordet er kopiert til utklippstavlen",
"sudoPasswordCopied": "Sudo-passordet ble kopiert til utklippstavlen",
"noPasswordAvailable": "Ingen passord tilgjengelig"
"noPasswordAvailable": "Ingen passord tilgjengelig",
"failedToCopyPassword": "Kopiering av passord feilet"
},
"admin": {
"title": "Administratorinnstillinger",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Globale overvåkingsinnstillinger lagret",
"failedToSaveGlobalSettings": "Kan ikke lagre globale overvåkingsinnstillinger",
"failedToLoadGlobalSettings": "Kan ikke laste inn globale overvåkingsinnstillinger",
"sessionTimeout": "Tidsavbrudd for økt",
"sessionTimeoutDesc": "Konfigurer hvor lenge brukerøkter varer før det krever ny autentisering. 'Husk meg' økter er upåvirket (alltid 30 dager).",
"sessionTimeoutHours": "Tidsavbrudd varighet",
"hours": "timer",
"sessionTimeoutNote": "Gyldig område: 1720 timer. Endringer gjelder bare for nye økter.",
"sessionTimeoutSaved": "Sesjonens tidsavbrudd lagret",
"failedToSaveSessionTimeout": "Kunne ikke lagre økttidsavbrudd",
"guacamoleIntegration": "Eksternt skrivebordsintegrering (Guacamole)",
"guacamoleIntegrationDesc": "Aktiver RDP, VNC, og Telnet forbindelser via guacd. Krever en guacd instans for å kjøre.",
"enableGuacamole": "Aktiver støtte for RDP/VNC/Telnet",
"guacdUrl": "guacd URL (host:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Endringer i guacd vert/port krever at serveren startes på nytt.",
"guacamoleSettingsSaved": "Innstillinger for Guacamol lagret",
"failedToSaveGuacamoleSettings": "Klarte ikke å lagre guacamol-innstillingene",
"failedToLoadGuacamoleSettings": "Kan ikke laste guacamol-innstillingene",
"logLevel": "Loggnivå (Automatic Translation)",
"logLevelDesc": "Kontroller håndteringen av serverlogger. Høyere nivåer reduserer loggresultatet. Kan også settes via miljøvariabelen LOG_LEVEL.",
"logVerbosity": "Årsak",
"logLevelNote": "Endringer trer i kraft umiddelbart. Feilsøkingsnivået kan påvirke ytelsen.",
"logLevelSaved": "Loggnivå lagret",
"failedToSaveLogLevel": "Kunne ikke lagre loggnivå",
"clampedToValidRange": "ble justert til gyldig område",
"loadingSessions": "Laster inn økter...",
"noActiveSessions": "Ingen aktive økter funnet.",
"device": "Enhet",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} verter",
"importJson": "Importer JSON",
"importing": "Importerer...",
"exportAllJson": "Eksporter alle",
"exporting": "Eksporterer...",
"exportedAllHosts": "Eksporterte {{count}} verter",
"failedToExportAllHosts": "Mislyktes i å eksportere verter. Kontroller at du er logget inn og har tilgang til vertsdata.",
"exportAllSensitiveWarning": "Den eksporterte filen inneholder sensitive godkjenningsdata (passorder/SSH-nøkler) i \"flyet\". Behold filen på en sikker og slett den etter bruk.",
"importJsonTitle": "Importer SSH-verter fra JSON",
"importJsonDesc": "Last opp en JSON-fil for å masseimportere flere SSH-verter (maks 100).",
"downloadSample": "Last ned eksempel",
@@ -866,11 +912,26 @@
"importError": "Importfeil",
"failedToImportJson": "Kunne ikke importere JSON-fil",
"connectionDetails": "Tilkoblingsdetaljer",
"connectionType": "Tilkobling type",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Eksternt skrivebord",
"guacamoleSettings": "Innstillinger for eksternt skrivebord",
"organization": "Organisasjon",
"ipAddress": "IP adresse eller vertsnavn",
"macAddress": "MAC Adresse",
"macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Vake-on-LAN pakke sendt",
"wolFailed": "Kunne ikke sende Wake-on-LAN pakke",
"ipRequired": "IP-adresse er påkrevd",
"portRequired": "Port er påkrevd",
"port": "Port",
"name": "Navn",
"username": "Brukernavn",
"usernameRequired": "Brukernavn er nødvendig (kun SH/Telnet)",
"folder": "Mappe",
"tags": "Tagger",
"pin": "Fest",
@@ -979,6 +1040,8 @@
"tunnel": "Tunnelen",
"fileManager": "Filbehandler",
"serverStats": "Serverstatistikk",
"status": "Status:",
"statistics": "Statistikk",
"hostViewer": "Vertvisning",
"enableServerStats": "Aktiver serverstatistikk",
"enableServerStatsDesc": "Aktiver/deaktiver innsamling av serverstatistikk for denne verten",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Dra for å flytte mellom mapper",
"exportedHostConfig": "Eksportert vertskonfigurasjon for {{name}}",
"openTerminal": "Åpne terminal",
"openRdp": "Åpne RDP",
"openVnc": "Åpne VNC",
"openTelnet": "Åpne telnet",
"openFileManager": "Åpne filbehandler",
"openTunnels": "Åpne tuneller",
"openServerDetails": "Åpne serverdetaljer",
"statistics": "Statistikk",
"enabledWidgets": "Aktiverte widgeter",
"openServerStats": "Åpne serverstatistikk",
"enabledWidgetsDesc": "Velg hvilke statistikk-widgeter som skal vises for denne verten",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Kontroller om serveren er online eller offline",
"statusCheckInterval": "Statussjekkintervall",
"statusCheckIntervalDesc": "Hvor ofte du skal kontrollere om verten er online (5s - 1t)",
"statusChecks": "Statussjekk",
"disableTcpPing": "Deaktiver TCP-Ping",
"disableTcpPingDescription": "Slå av statuskontroll (online/frakoblet deteksjon) for denne verten",
"useGlobalStatusInterval": "Bruk global standard",
"useGlobalMetricsInterval": "Bruk global standard",
"usingGlobalDefault": "Bruker global standard ({{value}}s)",
"metricsCollection": "Beregninger samling",
"metricsEnabled": "Aktiver statistikkovervåking",
"metricsEnabledDesc": "Samle inn CPU, RAM, disk og annen systemstatistikk",
"metricsInterval": "Intervall for statistikkinnsamling",
"metricsIntervalDesc": "Hvor ofte serverstatistikk skal innhentes (5s - 1t)",
"metricsNotAvailableForConnectionType": "Servermåltall er bare tilgjengelige for SSH-verter. TCP ping-statussjekker kan fortsatt være aktivert ovenfor.",
"intervalSeconds": "sekunder",
"intervalMinutes": "minutter",
"intervalValidation": "Overvåkingsintervaller må være mellom 5 sekunder og 1 time (3600 sekunder)",
@@ -1133,6 +1203,10 @@
"noServerFound": "Ingen server funnet",
"jumpHostsOrder": "Tilkoblinger opprettes i rekkefølge: Hoppvert 1 → Hoppvert 2 → … → Målserver",
"socks5Proxy": "SOCKS5 proxy",
"portKnocking": "Port knocking",
"portKnockingDesc": "Send en sekvens av tilkoblingsforsøk til angitte porter før tilkobling. Brukes til å åpne brannmurregler dynamisk.",
"addKnock": "Legge til port",
"delayMs": "Forsinkelse",
"socks5Description": "Konfigurer SOCKS5 proxy for SSH tilkobling. All trafikk vil bli rutet gjennom den angitte mellomtjeneren.",
"enableSocks5": "Aktiver SOCKS5 mellomtjener",
"enableSocks5Description": "Bruk SOCKS5-proxy for denne SSH-forbindelsen",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Kjør MOSH kommando ved tilkobling",
"moshCommand": "MOSH kommando",
"moshCommandDesc": "MOSH kommandoen å utføre",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Tilknytt automatisk en eksisterende (eller start en ny) tmux-økt for vedvarende økter og kontinuitet på tvers av enheter",
"environmentVariables": "Miljøvariabler",
"environmentVariablesDesc": "Angi egendefinerte miljøvariabler for terminaløkten",
"variableName": "Variabelens navn",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Kopier tunnel URL",
"copyServerStatsUrl": "Kopier Server Statistikk URL",
"copyDockerUrl": "Kopier Docker URL",
"copyRemoteDesktopUrl": "Kopier URL til eksternt skrivebord",
"fullScreenUrlTooltip": "Kopier URL for å åpne denne appen i fullskjermmodus",
"notEnabled": "Leger er ikke aktivert for denne verten. Aktiver dette i Vertsinnstillingene for å bruke Docker-funksjoner.",
"validating": "Validerer Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Velg alle",
"deselectAll": "Fjern all merking",
"useGlobalStatusDefault": "Bruk global standard (Status)",
"useGlobalMetricsDefault": "Bruk global standard (måltall)"
"useGlobalMetricsDefault": "Bruk global standard (måltall)",
"remoteDesktopSettings": "Innstillinger for eksternt skrivebord",
"domain": "Domene",
"securityMode": "Sikkerhet Modus",
"ignoreCert": "Ignorer Sertifikat",
"ignoreCertDesc": "Hopp over TLS-sertifikatverifisering for denne tilkoblingen",
"displaySettings": "Vis innstillinger",
"colorDepth": "Farge Dybde",
"width": "Width",
"height": "Høyde",
"dpi": "DPI",
"resizeMethod": "Endre størrelsen",
"forceLossless": "Tving tapsløs",
"audioSettings": "Lydinnstillinger for lyd",
"disableAudio": "Deaktivere lyd",
"enableAudioInput": "Aktiver lydinngang",
"rdpPerformance": "Ytelse",
"enableWallpaper": "Aktiver bakgrunnsbilde",
"enableTheming": "Aktiver mal",
"enableFontSmoothing": "Aktiver skriftutjevning",
"enableFullWindowDrag": "Aktiver dra for hele vinduet",
"enableDesktopComposition": "Slå på skrivebordskomposisjon",
"enableMenuAnimations": "Aktiver menyanimasjoner (Automatic Translation)",
"disableBitmapCaching": "Deaktivere Bitmapcaching",
"disableOffscreenCaching": "Deaktiver mellomlager i avslått skjerm",
"disableGlyphCaching": "Deaktiver Glyph Caching",
"enableGfx": "Enable GFX",
"deviceRedirection": "Omadressering av enhet",
"enablePrinting": "Aktiver utskrift",
"enableDrive": "Aktiver omdirigering av stasjon",
"driveName": "Stasjon navn",
"drivePath": "Sti disk",
"createDrivePath": "Opprett stasjon sti",
"disableDownload": "Deaktiver nedlasting",
"disableUpload": "Deaktiver opplasting",
"enableTouch": "Aktiver Touch",
"rdpSession": "Økt",
"clientName": "Klientens navn",
"consoleSession": "Konsoll Økten",
"initialProgram": "Første program",
"serverLayout": "Servertastaturoppsett",
"timezone": "Timezone",
"gatewaySettings": "Inngang",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Port for gateway",
"gatewayUsername": "Gateway brukernavn",
"gatewayPassword": "Gateway passord",
"gatewayDomain": "gateway Domene",
"remoteApp": "Fjernapp",
"remoteAppProgram": "Ekstern applikasjon",
"remoteAppDir": "Ekstern App mappe",
"remoteAppArgs": "Eksterne App Argumenter",
"clipboardSettings": "Utklippstavle",
"normalizeClipboard": "Normaliser utklippstavle",
"disableCopy": "Deaktiver kopi",
"disablePaste": "Deaktiver lim",
"vncSettings": "VNC Innstillinger",
"cursorMode": "Markør modus",
"swapRedBlue": "Bytte Rød/blå",
"readOnly": "Kun lese",
"recordingSettings": "Opptak",
"recordingPath": "Opptaks sti",
"recordingName": "Navn på opptak",
"createRecordingPath": "Lag opptakssti",
"excludeOutput": "Ekskluder utdata",
"excludeMouse": "Ekskluder musen",
"includeKeys": "Inkluder nøkler",
"sendWolPacket": "Send WoL Packet",
"wolMacAddr": "MAC Adresse",
"wolBroadcastAddr": "Kringkastings Adresse",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Ventetid (sekunder)",
"connectionSettings": "Tilkobling innstillinger",
"rdpOnly": "Kun prisP",
"vncOnly": "Kun VNC",
"telnetTerminalSettings": "Terminale innstillinger",
"terminalType": "Type terminal",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Farge Tema",
"guacBackspaceKey": "Bakovertast"
},
"guacamole": {
"connecting": "Kobler til {{type}} økt...",
"rdpConnecting": "Kobler til RDP-server...",
"vncConnecting": "Kobler til VNC server...",
"telnetConnecting": "Kobler til Telnet-serveren...",
"connectionError": "Feil ved tilkobling",
"connectionFailed": "Tilkobling mislyktes",
"failedToConnect": "Kan ikke hente tilkoblingstoken"
},
"terminal": {
"title": "Terminal",
@@ -1372,6 +1538,7 @@
"connected": "Tilkoblet",
"clipboardWriteFailed": "Kopiering til utklippstavlen. Sjekk at siden har tilgang til HTTPS eller localhost.",
"clipboardReadFailed": "Kan ikke lese fra utklippstavlen. Påse at utklippstavle tillatelser er gitt.",
"clipboardHttpWarning": "Lim inn krever HTTPS. Bruk Ctrl+Shift+V eller tjener termiks over HTTPS.",
"sshConnected": "SSH-tilkobling etablert",
"authError": "Autentisering mislyktes: {{message}}",
"unknownError": "Ukjent feil oppstod",
@@ -1380,7 +1547,25 @@
"connecting": "Kobler til...",
"reconnecting": "Kobler til på nytt... ({{attempt}}/{{max}})",
"reconnected": "Tilkoblet på nytt",
"tmuxSessionCreated": "opprettede tmux-økt: {{name}}",
"tmuxSessionAttached": "Tmux økt vedlagt: {{name}}",
"tmuxUnavailable": "tmux er ikke installert på den eksterne verten og faller tilbake til standard skall",
"tmuxSessionPickerTitle": "tmux økter",
"tmuxSessionPickerDesc": "Eksisterende tmux-økter funnet på denne verten. Velg en for å sende på nytt eller opprett en ny sesjon.",
"tmuxWindows": "Vinduer",
"tmuxWindowCount": "{{count}} vindu",
"tmuxWindowCount_other": "{{count}} vinduer",
"tmuxAttached": "Vedlagte klienter",
"tmuxAttachedCount": "{{count}} vedlagt",
"tmuxLastActivity": "Siste aktivitet",
"tmuxTimeJustNow": "akkurat nå",
"tmuxTimeMinutes": "{{count}}m siden",
"tmuxTimeHours": "{{count}}t siden",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Start ny økt",
"tmuxCopyHint": "Juster utvelgelse og trykk på Enter for å kopiere fra utklippstavlen",
"maxReconnectAttemptsReached": "Maks antall tilkoblingsforsøk nådd",
"closeTab": "Lukk",
"connectionTimeout": "Tilkoblingstidsavbrudd",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Autentisering ble tidsavbrutt. Vennligst prøv igjen.",
"opksshAuthFailed": "Godkjenning mislyktes. Kontroller påloggingsinformasjonen, og prøv på nytt.",
"opksshConfigMissing": "OPKSSH konfigurasjon ikke funnet. Lag ~/.opk/config.yml med dine OIDC leverandørinnstillinger. Se dokumentasjon: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Logg inn med {{provider}}",
"sudoPasswordPopupTitle": "Sette inn passord?",
"sudoPasswordPopupHint": "Trykk på Enter for å sette inn, Esc for å fjerne",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Tilkobling avvist av serveren. Kontroller godkjennings- og nettverkskonfigurasjonen.",
"hostKeyRejected": "SSH vertsnøkkelverifisering ble avvist. Tilkoblingen avbrutt.",
"sessionTakenOver": "Økten ble åpnet i en annen fane. Koble til...",
"sessionAttachTimeout": "Sesjonsvedlegget ble tidsavbrutt. Opprett ny tilkobling..."
"sessionAttachTimeout": "Sesjonsvedlegget ble tidsavbrutt. Opprett ny tilkobling...",
"openFileManagerHere": "Åpne filbehandler her"
},
"fileManager": {
"title": "Filbehandler",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Fargemerking per filtype: mapper (rød), filer (blå), symlenker (grønn)",
"commandAutocomplete": "Kommandoautofullføring",
"commandAutocompleteDesc": "Aktiver Tab-autofullføring for terminalkommandoer basert på kommandologgen",
"commandHistoryTracking": "Lagre kommandoen historie",
"commandHistoryTrackingDesc": "Lagre terminalkommandoer i loggen for autofullfør og sidepanel. Deaktivert som standard for privatliv.",
"defaultSnippetFoldersCollapsed": "Skjul støttemapper med standard",
"defaultSnippetFoldersCollapsedDesc": "Når aktivert, vil alle utdragsmapper bli brutt sammen når du åpner snippet-fanen",
"terminalSyntaxHighlighting": "Terminal syntaksutheving",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Automatisk markerer kommandoer, stier, IP-adresser og loggnivåer i terminalen",
"enableCommandPaletteShortcut": "Aktiver \"Kommando-palett\"-snarvei",
"enableCommandPaletteShortcutDesc": "Dobbelttrykk til venstre Skift for å åpne kommandopaletten for rask tilgang til verter",
"enableTerminalSessionPersistence": "Vedvarende terminaløkter",
"enableTerminalSessionPersistence": "Vedvarende faner/økter",
"enableTerminalSessionPersistenceDesc": "Oppretthold SSH-forbindelser når du bytter faner eller lukker nettleseren (kan være ustabil)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Databasen",
"healthy": "Frisk",
"error": "Feil",
"totalServers": "Totalt antall servere",
"totalHosts": "Totalt antall verter",
"totalTunnels": "Totalt antall tunneler",
"totalCredentials": "Totalt antall legitimasjoner",
"recentActivity": "Nylig aktivitet",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Skopiuj fragment do schowka",
"editTooltip": "Edytuj ten fragment",
"deleteTooltip": "Usuń ten fragment",
"shareTooltip": "Udostępnij ten fragment",
"sharedWithYou": "Udostępnione",
"sharedBy": "Udostępnione przez {{username}}",
"shareSnippet": "Udostępnij Snippet",
"user": "Użytkownik",
"role": "Rola",
"selectTarget": "Wybierz...",
"currentAccess": "Bieżący dostęp",
"shareSuccess": "Snippet udostępniony pomyślnie",
"shareFailed": "Nie udało się udostępnić snippet",
"revokeSuccess": "Dostęp odwołany",
"revokeFailed": "Nie udało się odwołać dostępu",
"failedToLoadShareData": "Nie udało się załadować danych udostępniania",
"newFolder": "Nowy folder",
"reorderSameFolder": "Może zmienić kolejność fragmentów w tym samym folderze",
"reorderSuccess": "Pomyślnie zmieniono kolejność fragmentów",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Wymagane uwierzytelnienie. Proszę odświeżyć stronę.",
"dataAccessLockedReauth": "Dostęp do danych zablokowany. Proszę ponownie uwierzytelnić.",
"loading": "Ładowanie historii poleceń...",
"error": "Błąd ładowania historii"
"error": "Błąd ładowania historii",
"disabledTitle": "Historia poleceń jest wyłączona",
"disabledDescription": "Włącz śledzenie historii poleceń w swoim profilu w ustawieniach wyglądu."
},
"splitScreen": {
"title": "Podziel ekran",
@@ -510,6 +525,8 @@
"checkingDatabase": "Sprawdzanie połączenia z bazą danych...",
"checkingAuthentication": "Sprawdzanie uwierzytelniania...",
"backendReconnected": "Połączenie z serwerem przywrócone",
"connectionDegraded": "Połączenie z serwerem zostało utracone, odzyskiwanie…",
"reload": "Reload",
"actions": "Akcje",
"remove": "Usuń",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Skopiuj hasło Sudo",
"passwordCopied": "Hasło skopiowane do schowka",
"sudoPasswordCopied": "Hasło Sudo skopiowane do schowka",
"noPasswordAvailable": "Hasło nie jest dostępne"
"noPasswordAvailable": "Hasło nie jest dostępne",
"failedToCopyPassword": "Nie udało się skopiować hasła"
},
"admin": {
"title": "Ustawienia administratora",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Globalne ustawienia monitorowania zapisane",
"failedToSaveGlobalSettings": "Nie udało się zapisać globalnych ustawień monitoringu",
"failedToLoadGlobalSettings": "Nie udało się załadować globalnych ustawień monitoringu",
"sessionTimeout": "Limit czasu sesji",
"sessionTimeoutDesc": "Skonfiguruj jak długo sesje użytkownika trwają przed ponownym uwierzytelnieniem. Sesje \"Zapamiętaj mnie\" nie mają wpływu na sesje (zawsze 30 dni).",
"sessionTimeoutHours": "Czas trwania",
"hours": "godziny",
"sessionTimeoutNote": "Prawidłowy zakres: 1720 godzin. Zmiany dotyczą tylko nowych sesji.",
"sessionTimeoutSaved": "Limit czasu sesji został zapisany",
"failedToSaveSessionTimeout": "Nie udało się zapisać limitu czasu sesji",
"guacamoleIntegration": "Integracja zdalnego pulpitu (Guacamole)",
"guacamoleIntegrationDesc": "Włącz połączenia RDP, VNC i Telnet przez guacd. Wymaga instancji guacd do uruchomienia.",
"enableGuacamole": "Włącz obsługę RDP/VNC/Telnet",
"guacdUrl": "adres URL guacd (host:port)",
"guacdUrlPlaceholder": "guakd:4822",
"guacdUrlNote": "Zmiany hosta/portu guacd wymagają ponownego uruchomienia serwera.",
"guacamoleSettingsSaved": "Ustawienia Guacamole zapisane",
"failedToSaveGuacamoleSettings": "Nie udało się zapisać ustawień guacamole",
"failedToLoadGuacamoleSettings": "Nie udało się załadować ustawień guacamole",
"logLevel": "Poziom logowania",
"logLevelDesc": "Kontroluj werbosity dzienników serwera. Wyższe poziomy zmniejszają wyjście dziennika. Można również ustawić poprzez zmienną środowiskową LOG_LEVEL.",
"logVerbosity": "Zjawisko",
"logLevelNote": "Zmiany stają się natychmiastowe. Poziom debugowania może mieć wpływ na wydajność.",
"logLevelSaved": "Poziom dziennika zapisany",
"failedToSaveLogLevel": "Nie udało się zapisać poziomu dziennika",
"clampedToValidRange": "został dostosowany do prawidłowego zakresu",
"loadingSessions": "Ładowanie sesji...",
"noActiveSessions": "Nie znaleziono aktywnych sesji.",
"device": "Urządzenie",
@@ -843,6 +884,11 @@
"hostsCount": "{{count}} hosty",
"importJson": "Importuj JSON",
"importing": "Importowanie...",
"exportAllJson": "Eksportuj wszystko",
"exporting": "Eksportowanie...",
"exportedAllHosts": "Eksportowano hosty {{count}}",
"failedToExportAllHosts": "Nie udało się wyeksportować hostów. Upewnij się, że jesteś zalogowany i masz dostęp do danych hosta.",
"exportAllSensitiveWarning": "Wyeksportowany plik będzie zawierał poufne dane uwierzytelniania (hasło/klucze SSH) w polu tekstowym. Proszę zachować plik bezpieczny i usunąć go po użyciu.",
"importJsonTitle": "Importuj hosty SSH z JSON",
"importJsonDesc": "Prześlij plik JSON aby masowo zaimportować wiele hostów SSH (maksymalnie 100).",
"downloadSample": "Pobierz próbkę",
@@ -866,11 +912,26 @@
"importError": "Błąd importu",
"failedToImportJson": "Nie udało się zaimportować pliku JSON",
"connectionDetails": "Szczegóły połączenia",
"connectionType": "Typ połączenia",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Zdalny pulpit",
"guacamoleSettings": "Ustawienia zdalnego pulpitu",
"organization": "Organizacja",
"ipAddress": "Adres IP lub nazwa hosta",
"macAddress": "Adres MAC",
"macAddressDesc": "Dla Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Pakiet Wake-on-LAN wysłany",
"wolFailed": "Nie udało się wysłać pakietu Wake-on-LAN",
"ipRequired": "Adres IP jest wymagany",
"portRequired": "Port jest wymagany",
"port": "Port",
"name": "Nazwisko",
"username": "Nazwa użytkownika",
"usernameRequired": "Nazwa użytkownika jest wymagana (tylko SSH/Telnet)",
"folder": "Folder",
"tags": "Tagi",
"pin": "Przypnij",
@@ -979,6 +1040,8 @@
"tunnel": "Tunel",
"fileManager": "Menedżer plików",
"serverStats": "Statystyki serwera",
"status": "Status",
"statistics": "Statystyki",
"hostViewer": "Przeglądarka hostów",
"enableServerStats": "Włącz statystyki serwera",
"enableServerStatsDesc": "Włącz/Wyłącz kolekcję statystyk serwera dla tego hosta",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Przeciągnij, aby przenieść się między folderami",
"exportedHostConfig": "Eksportowano konfigurację hosta dla {{name}}",
"openTerminal": "Otwórz terminal",
"openRdp": "Otwórz PROW",
"openVnc": "Otwórz VNC",
"openTelnet": "Otwórz Telnet",
"openFileManager": "Otwórz menedżera plików",
"openTunnels": "Otwórz tunele",
"openServerDetails": "Otwórz szczegóły serwera",
"statistics": "Statystyki",
"enabledWidgets": "Włączone widżety",
"openServerStats": "Otwórz statystyki serwera",
"enabledWidgetsDesc": "Wybierz, które widżety statystyczne mają być wyświetlane dla tego hosta",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Sprawdź, czy serwer jest online lub offline",
"statusCheckInterval": "Interwał sprawdzania statusu",
"statusCheckIntervalDesc": "Jak często sprawdzać, czy host jest online (5s - 1h)",
"statusChecks": "Sprawdzanie statusu",
"disableTcpPing": "Wyłącz Ping TCP",
"disableTcpPingDescription": "Wyłącz sprawdzanie statusu (wykrywanie online/offline) dla tego hosta",
"useGlobalStatusInterval": "Użyj globalnego domyślnego",
"useGlobalMetricsInterval": "Użyj globalnego domyślnego",
"usingGlobalDefault": "Używanie domyślnych globalnych ({{value}}s)",
"metricsCollection": "Kolekcja metryk",
"metricsEnabled": "Włącz monitorowanie liczników",
"metricsEnabledDesc": "Zbieraj CPU, RAM, dysk i inne statystyki systemowe",
"metricsInterval": "Interwał kolekcji liczników",
"metricsIntervalDesc": "Jak często zbierać statystyki serwera (5s - 1h)",
"metricsNotAvailableForConnectionType": "Metryki serwera są dostępne tylko dla hostów SSH. Sprawdzanie stanu ping TCP nadal może być włączone powyżej.",
"intervalSeconds": "sekundy",
"intervalMinutes": "minuty",
"intervalValidation": "Częstotliwość monitorowania musi wynosić od 5 sekund do 1 godziny (3600 sekund)",
@@ -1133,6 +1203,10 @@
"noServerFound": "Nie znaleziono serwera",
"jumpHostsOrder": "Połączenia zostaną wykonane w kolejności: Skok Host 1 → Skok Host 2 → ... → Serwer docelowy",
"socks5Proxy": "Proxy SOCKS5",
"portKnocking": "Odrzucenie portu",
"portKnockingDesc": "Wyślij sekwencję prób połączenia do określonych portów przed połączeniem. Używane do dynamicznego otwarcia reguł zapory.",
"addKnock": "Dodaj port",
"delayMs": "Opóźnienie",
"socks5Description": "Skonfiguruj proxy SOCKS5 dla połączenia SSH. Cały ruch zostanie przekierowany przez określony serwer proxy.",
"enableSocks5": "Włącz proxy SOCKS5",
"enableSocks5Description": "Użyj proxy SOCKS5 dla tego połączenia SSH",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Automatycznie uruchom polecenie MOSH przy połączeniu",
"moshCommand": "Polecenie MOSH",
"moshCommandDesc": "Polecenie MOSH do wykonania",
"autoTmux": "Auto-tmux",
"autoTmuxDesc": "Automatycznie dołącz do istniejącej (lub rozpocznij nową) sesji tmux dla sesji trwałych i ciągłości między urządzeniami",
"environmentVariables": "Zmienne środowiskowe",
"environmentVariablesDesc": "Ustaw niestandardowe zmienne środowiskowe dla sesji terminala",
"variableName": "Nazwa zmiennej",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Skopiuj adres URL tunelu",
"copyServerStatsUrl": "Skopiuj adres URL statystyk serwera",
"copyDockerUrl": "Skopiuj URL dokera",
"copyRemoteDesktopUrl": "Skopiuj adres URL zdalnego pulpitu",
"fullScreenUrlTooltip": "Skopiuj adres URL, aby otworzyć aplikację w trybie pełnoekranowym",
"notEnabled": "Docker nie jest włączony dla tego hosta. Włącz go w Ustawieniach hosta, aby używać funkcji Dockera.",
"validating": "Sprawdzanie poprawności Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Zaznacz wszystkie",
"deselectAll": "Odznacz wszystkie",
"useGlobalStatusDefault": "Użyj globalnego domyślnego (Status)",
"useGlobalMetricsDefault": "Użyj globalnego domyślnego (Metrics)"
"useGlobalMetricsDefault": "Użyj globalnego domyślnego (Metrics)",
"remoteDesktopSettings": "Ustawienia zdalnego pulpitu",
"domain": "Domena",
"securityMode": "Tryb bezpieczeństwa",
"ignoreCert": "Ignoruj certyfikat",
"ignoreCertDesc": "Pomiń weryfikację certyfikatu TLS dla tego połączenia",
"displaySettings": "Ustawienia wyświetlania",
"colorDepth": "Głębokość koloru",
"width": "Width",
"height": "Wysokość",
"dpi": "DPI",
"resizeMethod": "Metoda zmiany rozmiaru",
"forceLossless": "Wymuś bezstratne",
"audioSettings": "Ustawienia audio",
"disableAudio": "Wyłącz dźwięk",
"enableAudioInput": "Włącz wejście audio",
"rdpPerformance": "Efektywność",
"enableWallpaper": "Włącz tapetę",
"enableTheming": "Włącz motyw",
"enableFontSmoothing": "Włącz wygładzanie czcionki",
"enableFullWindowDrag": "Włącz przeciąganie pełnego okna",
"enableDesktopComposition": "Włącz kompozycję pulpitu",
"enableMenuAnimations": "Włącz animacje menu",
"disableBitmapCaching": "Wyłącz buforowanie bitmap",
"disableOffscreenCaching": "Wyłącz buforowanie",
"disableGlyphCaching": "Wyłącz buforowanie glifów",
"enableGfx": "Enable GFX",
"deviceRedirection": "Przekierowanie urządzenia",
"enablePrinting": "Włącz drukowanie",
"enableDrive": "Włącz przekierowanie na dysku",
"driveName": "Nazwa napędu",
"drivePath": "Ścieżka jazdy",
"createDrivePath": "Utwórz ścieżkę napędu",
"disableDownload": "Wyłącz pobieranie",
"disableUpload": "Wyłącz wysyłanie",
"enableTouch": "Włącz dotyk",
"rdpSession": "Sesja",
"clientName": "Nazwa klienta",
"consoleSession": "Sesja konsoli",
"initialProgram": "Program początkowy",
"serverLayout": "Układ klawiatury serwera",
"timezone": "Timezone",
"gatewaySettings": "Brama",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Port bramy",
"gatewayUsername": "Nazwa użytkownika bramy",
"gatewayPassword": "Hasło bramki",
"gatewayDomain": "Domena bramy",
"remoteApp": "Zdalna aplikacja",
"remoteAppProgram": "Zdalna aplikacja",
"remoteAppDir": "Zdalny katalog aplikacji",
"remoteAppArgs": "Argumenty aplikacji zdalnej",
"clipboardSettings": "Schowek",
"normalizeClipboard": "Normalizuj schowek",
"disableCopy": "Wyłącz kopiowanie",
"disablePaste": "Wyłącz wklej",
"vncSettings": "Ustawienia VNC",
"cursorMode": "Tryb kursora",
"swapRedBlue": "Zamień Czerwony/Niebieski",
"readOnly": "Tylko do odczytu",
"recordingSettings": "Nagrywanie",
"recordingPath": "Ścieżka nagrywania",
"recordingName": "Nazwa nagrania",
"createRecordingPath": "Utwórz ścieżkę nagrywania",
"excludeOutput": "Wyklucz dane wyjściowe",
"excludeMouse": "Wyklucz mysz",
"includeKeys": "Dołącz klucze",
"sendWolPacket": "Wyślij pakiet WoL",
"wolMacAddr": "Adres MAC",
"wolBroadcastAddr": "Adres nadawania",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Czas oczekiwania (w sekundach)",
"connectionSettings": "Ustawienia połączenia",
"rdpOnly": "Tylko RDP",
"vncOnly": "Tylko VNC",
"telnetTerminalSettings": "Ustawienia terminalu",
"terminalType": "Typ terminalu",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Schemat kolorów",
"guacBackspaceKey": "Klucz Backspace"
},
"guacamole": {
"connecting": "Łączenie z sesją {{type}}...",
"rdpConnecting": "Łączenie z serwerem RDP...",
"vncConnecting": "Łączenie z serwerem VNC...",
"telnetConnecting": "Łączenie z serwerem Telne...",
"connectionError": "Błąd połączenia",
"connectionFailed": "Połączenie nie powiodło się",
"failedToConnect": "Nie udało się uzyskać tokenu połączenia"
},
"terminal": {
"title": "Terminal",
@@ -1364,7 +1530,7 @@
"closePanel": "Zamknij panel",
"reconnect": "Połącz ponownie",
"sessionEnded": "Sesja zakończona",
"connectionLost": "Połączenie utracone",
"connectionLost": "Utracono połączenie",
"error": "BŁĄD: {{message}}",
"disconnected": "Rozłączony",
"connectionClosed": "Połączenie zamknięte",
@@ -1372,6 +1538,7 @@
"connected": "Połączono",
"clipboardWriteFailed": "Nie udało się skopiować do schowka. Upewnij się, że strona jest obsługiwana przez HTTPS lub localhost.",
"clipboardReadFailed": "Nie udało się odczytać ze schowka. Upewnij się, że uprawnienia do schowka są przyznane.",
"clipboardHttpWarning": "Wklej wymaga HTTPS. Użyj Ctrl+Shift+V lub podaj Termix przez HTTPS.",
"sshConnected": "Utworzono połączenie SSH",
"authError": "Uwierzytelnianie nie powiodło się: {{message}}",
"unknownError": "Wystąpił nieznany błąd",
@@ -1380,7 +1547,25 @@
"connecting": "Łączenie...",
"reconnecting": "Ponowne łączenie... ({{attempt}}/{{max}})",
"reconnected": "Ponowne połączenie zakończone pomyślnie",
"tmuxSessionCreated": "Sesja tmux utworzona: {{name}}",
"tmuxSessionAttached": "Dołączono sesję tmuxa: {{name}}",
"tmuxUnavailable": "tmux nie jest zainstalowany na zdalnym serwerze, powrót do standardowej powłoki",
"tmuxSessionPickerTitle": "Sesje tmux",
"tmuxSessionPickerDesc": "Znaleziono istniejące sesje tmux na tym serwerze. Wybierz jedną, aby ponowić lub utworzyć nową sesję.",
"tmuxWindows": "Okna",
"tmuxWindowCount": "Okno {{count}}",
"tmuxWindowCount_other": "{{count}} okien",
"tmuxAttached": "Dołączone klienci",
"tmuxAttachedCount": "{{count}} dołączony",
"tmuxLastActivity": "Ostatnia aktywność",
"tmuxTimeJustNow": "właśnie teraz",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h temu",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Rozpocznij nową sesję",
"tmuxCopyHint": "Dostosuj zaznaczenie i naciśnij Enter, aby skopiować do schowka",
"maxReconnectAttemptsReached": "Osiągnięto maksymalną liczbę prób ponownego połączenia",
"closeTab": "Zamknij",
"connectionTimeout": "Limit czasu połączenia",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Upłynął limit czasu uwierzytelniania. Spróbuj ponownie.",
"opksshAuthFailed": "Uwierzytelnianie nie powiodło się. Sprawdź swoje poświadczenia i spróbuj ponownie.",
"opksshConfigMissing": "Konfiguracja OPKSSH nie została znaleziona. Proszę utworzyć ~/.opk/config.yml z ustawieniami dostawcy OIDC. Zobacz dokumentacja: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Zaloguj się za pomocą {{provider}}",
"sudoPasswordPopupTitle": "Wprowadzić hasło?",
"sudoPasswordPopupHint": "Naciśnij Enter, aby wstawić, Esc, aby odrzucić",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Połączenie odrzucone przez serwer. Sprawdź swoje uwierzytelnianie i konfigurację sieci.",
"hostKeyRejected": "Weryfikacja klucza hosta SSH odrzucona. Połączenie anulowane.",
"sessionTakenOver": "Sesja została otwarta w innej karcie. Ponowne łączenie...",
"sessionAttachTimeout": "Upłynął limit czasu załącznika sesji. Tworzenie nowego połączenia..."
"sessionAttachTimeout": "Upłynął limit czasu załącznika sesji. Tworzenie nowego połączenia...",
"openFileManagerHere": "Otwórz Menedżera Plików tutaj"
},
"fileManager": {
"title": "Menedżer plików",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Pliki kodów kolorów według typu: foldery (czerwone), pliki (niebieskie), dowiązania symboliczne (zielone)",
"commandAutocomplete": "Autouzupełnianie polecenia",
"commandAutocompleteDesc": "Włącz automatyczne uzupełnianie sugestii kluczy zakładek dla komend terminali w oparciu o historię poleceń",
"commandHistoryTracking": "Zapisz historię poleceń",
"commandHistoryTrackingDesc": "Przechowuj komendy terminala w historii na pasku autokolateralizacji i bocznym. Domyślnie wyłączone dla prywatności.",
"defaultSnippetFoldersCollapsed": "Zwiń domyślnie foldery Snippet",
"defaultSnippetFoldersCollapsedDesc": "Po włączeniu wszystkie foldery snippet zostaną zwinięte po otwarciu zakładki snippety",
"terminalSyntaxHighlighting": "Podświetlanie składni terminalu",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Automatycznie podświetl polecenia, ścieżki, adresy IP i poziomy logów na wyjściu terminalu",
"enableCommandPaletteShortcut": "Włącz skrót palety poleceń",
"enableCommandPaletteShortcutDesc": "Dotknij dwukrotnie po lewej stronie, aby otworzyć paletę poleceń dla szybkiego dostępu do hostów",
"enableTerminalSessionPersistence": "Trwałe sesje terminali",
"enableTerminalSessionPersistence": "Trwałe zakładki/sesje",
"enableTerminalSessionPersistenceDesc": "Utrzymuj połączenia SSH podczas przełączania kart lub zamykania przeglądarki (może być niestabilne)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Baza danych",
"healthy": "Zdrowe",
"error": "Błąd",
"totalServers": "Wszystkie serwery",
"totalHosts": "Całkowita liczba hostów",
"totalTunnels": "Całkowita liczba tuneli",
"totalCredentials": "Łącznie dane logowania",
"recentActivity": "Ostatnia aktywność",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Copiar snippet para área de transferência",
"editTooltip": "Editar este trecho",
"deleteTooltip": "Excluir este snippet",
"shareTooltip": "Compartilhar este snippet",
"sharedWithYou": "Compartilhado",
"sharedBy": "Compartilhado por {{username}}",
"shareSnippet": "Compartilhar Snippet",
"user": "Usuário",
"role": "Funções",
"selectTarget": "Selecionar...",
"currentAccess": "Acesso atual",
"shareSuccess": "Snippet compartilhado com sucesso",
"shareFailed": "Falha ao compartilhar snippet",
"revokeSuccess": "Acesso revogado",
"revokeFailed": "Falha ao revogar acesso",
"failedToLoadShareData": "Falha ao carregar dados de compartilhamento",
"newFolder": "Adicionar uma pasta",
"reorderSameFolder": "Só é possível reordenar snippets dentro da mesma pasta",
"reorderSuccess": "Trechos reordenados com sucesso",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Autenticação necessária. Por favor, atualize a página.",
"dataAccessLockedReauth": "Acesso aos dados bloqueado. Por favor, autentique-se novamente.",
"loading": "Carregando histórico do comando...",
"error": "Erro ao Carregar Histórico"
"error": "Erro ao Carregar Histórico",
"disabledTitle": "Histórico de Comandos Desabilitado",
"disabledDescription": "Habilite o Rastreamento do Histórico do Comando no seu perfil nas configurações da aparência."
},
"splitScreen": {
"title": "Dividir a tela",
@@ -510,6 +525,8 @@
"checkingDatabase": "Verificando conexão com o banco de dados...",
"checkingAuthentication": "Verificando autenticação...",
"backendReconnected": "Conexão do servidor restaurada",
"connectionDegraded": "Conexão do servidor perdida, recuperando…",
"reload": "Reload",
"actions": "Ações.",
"remove": "Excluir",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Copiar Senha do Sudo",
"passwordCopied": "Senha copiada para área de transferência",
"sudoPasswordCopied": "Senha sudo copiada para área de transferência",
"noPasswordAvailable": "Nenhuma senha disponível"
"noPasswordAvailable": "Nenhuma senha disponível",
"failedToCopyPassword": "Falha ao copiar a senha"
},
"admin": {
"title": "Configurações de administrador",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Configurações globais de monitoramento salvas",
"failedToSaveGlobalSettings": "Falha ao salvar as configurações de monitoramento globais",
"failedToLoadGlobalSettings": "Falha ao carregar as configurações de monitoramento globais",
"sessionTimeout": "Sessão expirada",
"sessionTimeoutDesc": "Configurar quanto tempo as sessões de usuário duram antes de exigir re-autenticação. 'Lembrar de mim' as sessões não afetadas (sempre 30 dias).",
"sessionTimeoutHours": "Duração do Tempo Limite",
"hours": "horas",
"sessionTimeoutNote": "Intervalo válido: 1-720 horas. As alterações aplicam-se somente às novas sessões.",
"sessionTimeoutSaved": "Timeout da sessão salvo",
"failedToSaveSessionTimeout": "Falha ao salvar tempo limite de sessão",
"guacamoleIntegration": "Integração no Desktop Remoto (Guacamole)",
"guacamoleIntegrationDesc": "Habilitar conexões RDP, VNC e Telnet via guacd. Requer uma instância de guacd para estar rodando.",
"enableGuacamole": "Habilitar suporte a RDP/VNC/Telnet",
"guacdUrl": "URL guacd (host:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Alterações no host/porta do guacd requerem que o servidor reinicie para que tenha efeito.",
"guacamoleSettingsSaved": "Configurações de Guacamole salvas",
"failedToSaveGuacamoleSettings": "Falha ao salvar configurações do guacamole",
"failedToLoadGuacamoleSettings": "Falha ao carregar as configurações do guacamole",
"logLevel": "Nível do Registro",
"logLevelDesc": "Controle a verbosidade dos logs do servidor. Níveis maiores reduzem a saída de log. Também pode ser definido através da variável de ambiente LOG_LEVEL.",
"logVerbosity": "Verbossidade",
"logLevelNote": "As alterações terão efeito imediatamente. O nível de depuração pode afetar o desempenho.",
"logLevelSaved": "Nível de log salvo",
"failedToSaveLogLevel": "Falha ao salvar nível de log",
"clampedToValidRange": "foi ajustado para um intervalo válido",
"loadingSessions": "Carregando sessões...",
"noActiveSessions": "Não foram encontradas sessões ativas.",
"device": "Dispositivo",
@@ -843,6 +884,11 @@
"hostsCount": "Hosts {{count}}",
"importJson": "Importar JSON",
"importing": "Importando...",
"exportAllJson": "Exportar tudo",
"exporting": "Exportando...",
"exportedAllHosts": "Hosts {{count}} exportados",
"failedToExportAllHosts": "Falha ao exportar hosts. Verifique se você está logado e tenha acesso aos dados do host.",
"exportAllSensitiveWarning": "O arquivo exportado conterá dados confidenciais de autenticação (senhas/chaves SSH) em texto simples. Mantenha o arquivo seguro e apague-o após o uso.",
"importJsonTitle": "Importar o SSH Hosts do JSON",
"importJsonDesc": "Carregar um arquivo JSON para importar em massa vários hosts SSH (máx. 100).",
"downloadSample": "Baixar Exemplo",
@@ -866,11 +912,26 @@
"importError": "Erro ao importar",
"failedToImportJson": "Falha ao importar arquivo JSON",
"connectionDetails": "Detalhes da conexão",
"connectionType": "Tipo de conexão",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Computador Remoto",
"guacamoleSettings": "Configurações de Desktop Remoto",
"organization": "Cliente",
"ipAddress": "Endereço IP ou Nome de Host",
"macAddress": "Endereço MAC",
"macAddressDesc": "Para Wake-on-LAN. Formato: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Pacote Wake-on-LAN enviado",
"wolFailed": "Falha ao enviar pacote Wake-on-LAN",
"ipRequired": "O endereço IP é obrigatório",
"portRequired": "Porta é obrigatória",
"port": "Porta",
"name": "Nome:",
"username": "Usuário:",
"usernameRequired": "Nome de usuário obrigatório (SSH/Telnet)",
"folder": "pasta",
"tags": "Etiquetas",
"pin": "PIN",
@@ -979,6 +1040,8 @@
"tunnel": "Túnel",
"fileManager": "Gerenciador de Arquivos",
"serverStats": "Estatísticas do servidor",
"status": "SItuação",
"statistics": "estatísticas",
"hostViewer": "Visualizador do Host",
"enableServerStats": "Habilitar estatísticas do servidor",
"enableServerStatsDesc": "Ativar/desativar estatísticas do servidor para este host",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Arraste para mover entre as pastas",
"exportedHostConfig": "Configuração host exportada para {{name}}",
"openTerminal": "Abrir terminal",
"openRdp": "Abrir RDP",
"openVnc": "Abrir VNC",
"openTelnet": "Abrir Telnet",
"openFileManager": "Abrir Gerenciador de Arquivos",
"openTunnels": "Abrir túneis",
"openServerDetails": "Abrir Detalhes do Servidor",
"statistics": "estatísticas",
"enabledWidgets": "Widgets ativos",
"openServerStats": "Estatísticas do Servidor Aberto",
"enabledWidgetsDesc": "Selecione quais widgets de estatísticas a exibir para este host",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Verifique se o servidor está online ou offline",
"statusCheckInterval": "Intervalo de verificação de status",
"statusCheckIntervalDesc": "Frequência para verificar se o host está online (5s - 1h)",
"statusChecks": "Verificações de Status",
"disableTcpPing": "Desativar TCP Ping",
"disableTcpPingDescription": "Desativar verificações de status (detecção online/offline) para este host",
"useGlobalStatusInterval": "Usar padrão global",
"useGlobalMetricsInterval": "Usar padrão global",
"usingGlobalDefault": "Usando padrão global ({{value}}s)",
"metricsCollection": "Coleção de Métricas",
"metricsEnabled": "Habilitar monitoramento de métricas",
"metricsEnabledDesc": "Coletar estatísticas de CPU, RAM, disco e outros sistemas",
"metricsInterval": "Intervalo de Coleção de Métricas",
"metricsIntervalDesc": "Com que frequência coletar estatísticas do servidor (5s - 1h)",
"metricsNotAvailableForConnectionType": "Métricas do servidor só estão disponíveis para hosts SSH. Verificações de status do TCP ainda podem ser habilitadas acima.",
"intervalSeconds": "segundos",
"intervalMinutes": "Minutos",
"intervalValidation": "Intervalos de monitoramento devem ser entre 5 segundos e 1 hora (3600 segundos)",
@@ -1133,6 +1203,10 @@
"noServerFound": "Nenhum servidor encontrado",
"jumpHostsOrder": "Conexões serão feitas em ordem: Saltar Host 1 → Jump Host 2 → ... → Servidor de destino",
"socks5Proxy": "Proxy SOCKS5",
"portKnocking": "Knocking de Porta",
"portKnockingDesc": "Envia uma sequência de tentativas de conexão a portas especificadas antes de se conectar. Usado para abrir as regras do firewall dinamicamente.",
"addKnock": "Adicionar Porta",
"delayMs": "Atraso",
"socks5Description": "Configurar o proxy SOCKS5 para conexão SSH. Todo o tráfego será encaminhado através do servidor proxy especificado.",
"enableSocks5": "Habilitar SOCKS5 Proxy",
"enableSocks5Description": "Use o proxy SOCKS5 para esta conexão SSH",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Executar o comando MOSH automaticamente ao conectar",
"moshCommand": "Comando MOSH",
"moshCommandDesc": "O comando MOSH para executar",
"autoTmux": "Auto-mutar",
"autoTmuxDesc": "Anexar automaticamente a uma sessão de tmux existente (ou iniciar uma nova) para sessões persistentes e continuidade entre dispositivos",
"environmentVariables": "Variáveis de Ambiente",
"environmentVariablesDesc": "Definir variáveis de ambiente personalizadas para a sessão do terminal",
"variableName": "Nome da variável",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Copiar URL do túnel",
"copyServerStatsUrl": "Copiar URL das Estatísticas do Servidor",
"copyDockerUrl": "Copiar URL do Docker",
"copyRemoteDesktopUrl": "Copiar URL do Desktop Remoto",
"fullScreenUrlTooltip": "Copiar URL para abrir este app em tela cheia",
"notEnabled": "O Docker não está habilitado para este host. Habilite nas configurações de Host para usar os recursos Docker.",
"validating": "Validando o Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Selecionar todos",
"deselectAll": "Desmarcar todos",
"useGlobalStatusDefault": "Usar Padrão Global (Status)",
"useGlobalMetricsDefault": "Usar Padrão Global (Métricas)"
"useGlobalMetricsDefault": "Usar Padrão Global (Métricas)",
"remoteDesktopSettings": "Configurações de Desktop Remoto",
"domain": "Domínio",
"securityMode": "Modo de Segurança",
"ignoreCert": "Ignorar Certificado",
"ignoreCertDesc": "Pular verificação de certificado TLS para esta conexão",
"displaySettings": "Configurações de exibição",
"colorDepth": "Profundidade da Cor",
"width": "Width",
"height": "Altura",
"dpi": "DPI",
"resizeMethod": "Método de Redimensionar",
"forceLossless": "Forçar perda",
"audioSettings": "Configurações de Áudio",
"disableAudio": "Desativar Áudio",
"enableAudioInput": "Habilitar entrada de áudio",
"rdpPerformance": "Desempenho",
"enableWallpaper": "Habilitar papel de parede",
"enableTheming": "Habilitar Temas",
"enableFontSmoothing": "Ativar Suavização de Fonte",
"enableFullWindowDrag": "Habilitar Arrastar Janela Completa",
"enableDesktopComposition": "Ativar Composição no Desktop",
"enableMenuAnimations": "Ativar Animações do Menu",
"disableBitmapCaching": "Desabilitar Cache Bitmap",
"disableOffscreenCaching": "Desativar Cache Offscreen",
"disableGlyphCaching": "Desativar Cache Glyph",
"enableGfx": "Enable GFX",
"deviceRedirection": "Redirecionamento do dispositivo",
"enablePrinting": "Ativar impressão",
"enableDrive": "Habilitar redirecionamento de unidade",
"driveName": "Nome da unidade",
"drivePath": "Caminho do Drive",
"createDrivePath": "Criar caminho de unidade",
"disableDownload": "Desativar download",
"disableUpload": "Desabilitar Upload",
"enableTouch": "Ativar Touch",
"rdpSession": "Sessão",
"clientName": "Nome do Cliente",
"consoleSession": "Sessão de Console",
"initialProgram": "Programa inicial",
"serverLayout": "Layout do teclado do servidor",
"timezone": "Timezone",
"gatewaySettings": "Desvio",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Porta do Gateway",
"gatewayUsername": "Usuário do Gateway",
"gatewayPassword": "Senha do Gateway",
"gatewayDomain": "Domínio de Gateway",
"remoteApp": "RemoteApp",
"remoteAppProgram": "Aplicativo remoto",
"remoteAppDir": "Diretório da App Remota",
"remoteAppArgs": "Argumentos de App Remoto",
"clipboardSettings": "Área",
"normalizeClipboard": "Normalizar área de transferência",
"disableCopy": "Desativar Cópia",
"disablePaste": "Desativar Colar",
"vncSettings": "Configurações do VNC",
"cursorMode": "Modo Cursor",
"swapRedBlue": "Trocar Vermelho/Azul",
"readOnly": "Somente leitura",
"recordingSettings": "Gravação",
"recordingPath": "Caminho de gravação",
"recordingName": "Nome da gravação",
"createRecordingPath": "Criar Caminho de Gravação",
"excludeOutput": "Excluir saída",
"excludeMouse": "Excluir mouse",
"includeKeys": "Incluir Chaves",
"sendWolPacket": "Enviar Pacote WoL",
"wolMacAddr": "Endereço MAC",
"wolBroadcastAddr": "Endereço de Transmissão",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Tempo de espera (segundos)",
"connectionSettings": "Configurações de conexão",
"rdpOnly": "RDP apenas",
"vncOnly": "Somente VNC",
"telnetTerminalSettings": "Configurações do Terminal",
"terminalType": "Tipo de Terminal",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Esquema de Cor",
"guacBackspaceKey": "Chave de Backspace"
},
"guacamole": {
"connecting": "Conectando à sessão {{type}}",
"rdpConnecting": "Conectando ao servidor RDP...",
"vncConnecting": "Conectando-se ao servidor VNC...",
"telnetConnecting": "Conectando ao servidor Telnet...",
"connectionError": "Erro de conexão",
"connectionFailed": "Conexão falhou",
"failedToConnect": "Falha ao obter token de conexão"
},
"terminal": {
"title": "Terminal",
@@ -1364,7 +1530,7 @@
"closePanel": "Fechar Painel",
"reconnect": "Reconectar",
"sessionEnded": "Sessão Encerrada",
"connectionLost": "Conexão Perdida",
"connectionLost": "Conexão perdida",
"error": "ERRO: {{message}}",
"disconnected": "Desconectado",
"connectionClosed": "Conexão fechada",
@@ -1372,6 +1538,7 @@
"connected": "Conectado",
"clipboardWriteFailed": "Falha ao copiar para a área de transferência. Certifique-se de que a página seja servida por HTTPS ou localhost.",
"clipboardReadFailed": "Falha ao ler da área de transferência. Certifique-se de que as permissões da área de transferência foram concedidas.",
"clipboardHttpWarning": "Colar requer HTTPS. Use Ctrl+Shift+V ou sirva Termix em HTTPS.",
"sshConnected": "Conexão SSH estabelecida",
"authError": "Falha na autenticação: {{message}}",
"unknownError": "Ocorreu um erro desconhecido",
@@ -1380,7 +1547,25 @@
"connecting": "Conectandochar@@0",
"reconnecting": "Reconectando... ({{attempt}}/{{max}})",
"reconnected": "Reconectado com sucesso",
"tmuxSessionCreated": "sessão tmux criada: {{name}}",
"tmuxSessionAttached": "sessão tmux anexada: {{name}}",
"tmuxUnavailable": "tmux não está instalado no host remoto, voltando ao shell padrão",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Sessões tmux existentes encontradas neste host. Selecione uma para reanexar ou criar uma nova sessão.",
"tmuxWindows": "Janelas",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "Janelas {{count}}",
"tmuxAttached": "Clientes anexados",
"tmuxAttachedCount": "{{count}} anexou",
"tmuxLastActivity": "Última atividade",
"tmuxTimeJustNow": "neste momento",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Iniciar nova sessão",
"tmuxCopyHint": "Ajustar seleção e pressione Enter para copiar para área de transferência",
"maxReconnectAttemptsReached": "Máximo de tentativas de reconexão alcançadas",
"closeTab": "FECHAR",
"connectionTimeout": "Conexão expirada",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Tempo de autenticação esgotado. Por favor, tente novamente.",
"opksshAuthFailed": "Falha na autenticação. Por favor, verifique suas credenciais e tente novamente.",
"opksshConfigMissing": "Configuração OPKSSH não encontrada. Por favor, crie ~/.opk/config.yml com as configurações do provedor OIDC. Veja documentação: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Entrar com {{provider}}",
"sudoPasswordPopupTitle": "Inserir senha?",
"sudoPasswordPopupHint": "Pressione Enter para inserir, Esc para dispensar",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Conexão rejeitada pelo servidor. Verifique sua autenticação e configuração de rede.",
"hostKeyRejected": "Verificação de chave do host SSH rejeitada. Conexão cancelada.",
"sessionTakenOver": "A sessão foi aberta em outra aba. Reconectando...",
"sessionAttachTimeout": "O anexo da sessão expirou. Criando nova conexão..."
"sessionAttachTimeout": "O anexo da sessão expirou. Criando nova conexão...",
"openFileManagerHere": "Abrir Gerenciador de Arquivos Aqui"
},
"fileManager": {
"title": "Gerenciador de Arquivos",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Arquivos de código de cor por tipo: pastas (vermelho), arquivos (azul), links simbólicos (verde)",
"commandAutocomplete": "Auto-completar comando",
"commandAutocompleteDesc": "Ativar sugestões de autocompletar da tecla de aba para comandos de terminal com base no seu histórico de comandos",
"commandHistoryTracking": "Salvar Histórico de Comandos",
"commandHistoryTrackingDesc": "Armazenar comandos do terminal no histórico para preenchimento automático e barra lateral. Desativado por padrão para privacidade.",
"defaultSnippetFoldersCollapsed": "Recolher pastas de fragmentos por padrão",
"defaultSnippetFoldersCollapsedDesc": "Quando ativado, todas as pastas de snippet serão recolhidas quando você abrir a aba de snippets",
"terminalSyntaxHighlighting": "Realce de Sintaxe Terminal",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Destacar automaticamente comandos, caminhos, IPs e níveis de log na saída do terminal",
"enableCommandPaletteShortcut": "Ativar Atalho da Paleta de Comando",
"enableCommandPaletteShortcutDesc": "Toque duas vezes no botão esquerdo para abrir a Paleta de Comando para acesso rápido aos hosts",
"enableTerminalSessionPersistence": "Sessões Terminais Persistentes",
"enableTerminalSessionPersistence": "Guias persistentes/Sessões",
"enableTerminalSessionPersistenceDesc": "Manter conexões SSH ao alternar abas ou fechar o navegador (pode ser instável)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Banco",
"healthy": "Saudável",
"error": "ERRO",
"totalServers": "Total de Servidores",
"totalHosts": "Hosts totais",
"totalTunnels": "Total de túneis",
"totalCredentials": "Credenciais totais",
"recentActivity": "Atividade recente",
+197 -8
View File
@@ -277,6 +277,19 @@
"copyTooltip": "Copiar snippet para área de transferência",
"editTooltip": "Editar este trecho",
"deleteTooltip": "Excluir este snippet",
"shareTooltip": "Compartilhar este snippet",
"sharedWithYou": "Compartilhado",
"sharedBy": "Compartilhado por {{username}}",
"shareSnippet": "Compartilhar Snippet",
"user": "Usuário",
"role": "Funções",
"selectTarget": "Selecionar...",
"currentAccess": "Acesso atual",
"shareSuccess": "Snippet compartilhado com sucesso",
"shareFailed": "Falha ao compartilhar snippet",
"revokeSuccess": "Acesso revogado",
"revokeFailed": "Falha ao revogar acesso",
"failedToLoadShareData": "Falha ao carregar dados de compartilhamento",
"newFolder": "Adicionar uma pasta",
"reorderSameFolder": "Só é possível reordenar snippets dentro da mesma pasta",
"reorderSuccess": "Trechos reordenados com sucesso",
@@ -323,7 +336,9 @@
"authRequiredRefresh": "Autenticação necessária. Por favor, atualize a página.",
"dataAccessLockedReauth": "Acesso aos dados bloqueado. Por favor, autentique-se novamente.",
"loading": "Carregando histórico do comando...",
"error": "Erro ao Carregar Histórico"
"error": "Erro ao Carregar Histórico",
"disabledTitle": "Histórico de Comandos Desabilitado",
"disabledDescription": "Habilite o Rastreamento do Histórico do Comando no seu perfil nas configurações da aparência."
},
"splitScreen": {
"title": "Dividir a tela",
@@ -510,6 +525,8 @@
"checkingDatabase": "Verificando conexão com o banco de dados...",
"checkingAuthentication": "Verificando autenticação...",
"backendReconnected": "Conexão do servidor restaurada",
"connectionDegraded": "Conexão do servidor perdida, recuperando…",
"reload": "Reload",
"actions": "Ações.",
"remove": "Excluir",
"revoke": "Revoke",
@@ -541,7 +558,8 @@
"copySudoPassword": "Copiar Senha do Sudo",
"passwordCopied": "Senha copiada para área de transferência",
"sudoPasswordCopied": "Senha sudo copiada para área de transferência",
"noPasswordAvailable": "Nenhuma senha disponível"
"noPasswordAvailable": "Nenhuma senha disponível",
"failedToCopyPassword": "Falha ao copiar a senha"
},
"admin": {
"title": "Configurações de administrador",
@@ -808,6 +826,29 @@
"globalSettingsSaved": "Configurações globais de monitoramento salvas",
"failedToSaveGlobalSettings": "Falha ao salvar as configurações de monitoramento globais",
"failedToLoadGlobalSettings": "Falha ao carregar as configurações de monitoramento globais",
"sessionTimeout": "Sessão expirada",
"sessionTimeoutDesc": "Configurar quanto tempo as sessões de usuário duram antes de exigir re-autenticação. 'Lembrar de mim' as sessões não afetadas (sempre 30 dias).",
"sessionTimeoutHours": "Duração do Tempo Limite",
"hours": "horas",
"sessionTimeoutNote": "Intervalo válido: 1-720 horas. As alterações aplicam-se somente às novas sessões.",
"sessionTimeoutSaved": "Timeout da sessão salvo",
"failedToSaveSessionTimeout": "Falha ao salvar tempo limite de sessão",
"guacamoleIntegration": "Integração no Desktop Remoto (Guacamole)",
"guacamoleIntegrationDesc": "Habilitar conexões RDP, VNC e Telnet via guacd. Requer uma instância de guacd para estar rodando.",
"enableGuacamole": "Habilitar suporte a RDP/VNC/Telnet",
"guacdUrl": "URL guacd (host:port)",
"guacdUrlPlaceholder": "guacd:4822",
"guacdUrlNote": "Alterações no host/porta do guacd requerem que o servidor reinicie para que tenha efeito.",
"guacamoleSettingsSaved": "Configurações de Guacamole salvas",
"failedToSaveGuacamoleSettings": "Falha ao salvar configurações do guacamole",
"failedToLoadGuacamoleSettings": "Falha ao carregar as configurações do guacamole",
"logLevel": "Nível do Registro",
"logLevelDesc": "Controle a verbosidade dos logs do servidor. Níveis maiores reduzem a saída de log. Também pode ser definido através da variável de ambiente LOG_LEVEL.",
"logVerbosity": "Verbossidade",
"logLevelNote": "As alterações terão efeito imediatamente. O nível de depuração pode afetar o desempenho.",
"logLevelSaved": "Nível de log salvo",
"failedToSaveLogLevel": "Falha ao salvar nível de log",
"clampedToValidRange": "foi ajustado para um intervalo válido",
"loadingSessions": "Carregando sessões...",
"noActiveSessions": "Não foram encontradas sessões ativas.",
"device": "Dispositivo",
@@ -843,6 +884,11 @@
"hostsCount": "Hosts {{count}}",
"importJson": "Importar JSON",
"importing": "Importando...",
"exportAllJson": "Exportar tudo",
"exporting": "Exportando...",
"exportedAllHosts": "Hosts {{count}} exportados",
"failedToExportAllHosts": "Falha ao exportar hosts. Verifique se você está logado e tenha acesso aos dados do host.",
"exportAllSensitiveWarning": "O arquivo exportado conterá dados confidenciais de autenticação (senhas/chaves SSH) em texto simples. Mantenha o arquivo seguro e apague-o após o uso.",
"importJsonTitle": "Importar o SSH Hosts do JSON",
"importJsonDesc": "Carregar um arquivo JSON para importar em massa vários hosts SSH (máx. 100).",
"downloadSample": "Baixar Exemplo",
@@ -866,11 +912,26 @@
"importError": "Erro ao importar",
"failedToImportJson": "Falha ao importar arquivo JSON",
"connectionDetails": "Detalhes da conexão",
"connectionType": "Tipo de conexão",
"ssh": "SSH",
"rdp": "RDP",
"vnc": "VNC",
"telnet": "Telnet",
"remoteDesktop": "Computador Remoto",
"guacamoleSettings": "Configurações de Desktop Remoto",
"organization": "Cliente",
"ipAddress": "Endereço IP ou Nome de Host",
"macAddress": "Endereço MAC",
"macAddressDesc": "Para Wake-on-LAN. Formato: AA:BB:CC:DD:EE:FF",
"wakeOnLan": "Wake-on-LAN",
"wolSent": "Pacote Wake-on-LAN enviado",
"wolFailed": "Falha ao enviar pacote Wake-on-LAN",
"ipRequired": "O endereço IP é obrigatório",
"portRequired": "Porta é obrigatória",
"port": "Porta",
"name": "Nome:",
"username": "Usuário:",
"usernameRequired": "Nome de usuário obrigatório (SSH/Telnet)",
"folder": "pasta",
"tags": "Etiquetas",
"pin": "PIN",
@@ -979,6 +1040,8 @@
"tunnel": "Túnel",
"fileManager": "Gerenciador de Arquivos",
"serverStats": "Estatísticas do servidor",
"status": "SItuação",
"statistics": "estatísticas",
"hostViewer": "Visualizador do Host",
"enableServerStats": "Habilitar estatísticas do servidor",
"enableServerStatsDesc": "Ativar/desativar estatísticas do servidor para este host",
@@ -1025,10 +1088,12 @@
"dragToMoveBetweenFolders": "Arraste para mover entre as pastas",
"exportedHostConfig": "Configuração host exportada para {{name}}",
"openTerminal": "Abrir terminal",
"openRdp": "Abrir RDP",
"openVnc": "Abrir VNC",
"openTelnet": "Abrir Telnet",
"openFileManager": "Abrir Gerenciador de Arquivos",
"openTunnels": "Abrir túneis",
"openServerDetails": "Abrir Detalhes do Servidor",
"statistics": "estatísticas",
"enabledWidgets": "Widgets ativos",
"openServerStats": "Estatísticas do Servidor Aberto",
"enabledWidgetsDesc": "Selecione quais widgets de estatísticas a exibir para este host",
@@ -1038,13 +1103,18 @@
"statusCheckEnabledDesc": "Verifique se o servidor está online ou offline",
"statusCheckInterval": "Intervalo de verificação de status",
"statusCheckIntervalDesc": "Frequência para verificar se o host está online (5s - 1h)",
"statusChecks": "Verificações de Status",
"disableTcpPing": "Desativar TCP Ping",
"disableTcpPingDescription": "Desativar verificações de status (detecção online/offline) para este host",
"useGlobalStatusInterval": "Usar padrão global",
"useGlobalMetricsInterval": "Usar padrão global",
"usingGlobalDefault": "Usando padrão global ({{value}}s)",
"metricsCollection": "Coleção de Métricas",
"metricsEnabled": "Habilitar monitoramento de métricas",
"metricsEnabledDesc": "Coletar estatísticas de CPU, RAM, disco e outros sistemas",
"metricsInterval": "Intervalo de Coleção de Métricas",
"metricsIntervalDesc": "Com que frequência coletar estatísticas do servidor (5s - 1h)",
"metricsNotAvailableForConnectionType": "Métricas do servidor só estão disponíveis para hosts SSH. Verificações de status do TCP ainda podem ser habilitadas acima.",
"intervalSeconds": "segundos",
"intervalMinutes": "Minutos",
"intervalValidation": "Intervalos de monitoramento devem ser entre 5 segundos e 1 hora (3600 segundos)",
@@ -1133,6 +1203,10 @@
"noServerFound": "Nenhum servidor encontrado",
"jumpHostsOrder": "Conexões serão feitas em ordem: Saltar Host 1 → Jump Host 2 → ... → Servidor de destino",
"socks5Proxy": "Proxy SOCKS5",
"portKnocking": "Knocking de Porta",
"portKnockingDesc": "Envia uma sequência de tentativas de conexão a portas especificadas antes de se conectar. Usado para abrir as regras do firewall dinamicamente.",
"addKnock": "Adicionar Porta",
"delayMs": "Atraso",
"socks5Description": "Configurar o proxy SOCKS5 para conexão SSH. Todo o tráfego será encaminhado através do servidor proxy especificado.",
"enableSocks5": "Habilitar SOCKS5 Proxy",
"enableSocks5Description": "Use o proxy SOCKS5 para esta conexão SSH",
@@ -1217,6 +1291,8 @@
"autoMoshDesc": "Executar o comando MOSH automaticamente ao conectar",
"moshCommand": "Comando MOSH",
"moshCommandDesc": "O comando MOSH para executar",
"autoTmux": "Auto-mutar",
"autoTmuxDesc": "Anexar automaticamente a uma sessão de tmux existente (ou iniciar uma nova) para sessões persistentes e continuidade entre dispositivos",
"environmentVariables": "Variáveis de Ambiente",
"environmentVariablesDesc": "Definir variáveis de ambiente personalizadas para a sessão do terminal",
"variableName": "Nome da variável",
@@ -1232,6 +1308,7 @@
"copyTunnelUrl": "Copiar URL do túnel",
"copyServerStatsUrl": "Copiar URL das Estatísticas do Servidor",
"copyDockerUrl": "Copiar URL do Docker",
"copyRemoteDesktopUrl": "Copiar URL do Desktop Remoto",
"fullScreenUrlTooltip": "Copiar URL para abrir este app em tela cheia",
"notEnabled": "O Docker não está habilitado para este host. Habilite nas configurações de Host para usar os recursos Docker.",
"validating": "Validando o Docker...",
@@ -1348,7 +1425,96 @@
"selectAll": "Selecionar todos",
"deselectAll": "Desmarcar todos",
"useGlobalStatusDefault": "Usar Padrão Global (Status)",
"useGlobalMetricsDefault": "Usar Padrão Global (Métricas)"
"useGlobalMetricsDefault": "Usar Padrão Global (Métricas)",
"remoteDesktopSettings": "Configurações de Desktop Remoto",
"domain": "Domínio",
"securityMode": "Modo de Segurança",
"ignoreCert": "Ignorar Certificado",
"ignoreCertDesc": "Pular verificação de certificado TLS para esta conexão",
"displaySettings": "Configurações de exibição",
"colorDepth": "Profundidade da Cor",
"width": "Width",
"height": "Altura",
"dpi": "DPI",
"resizeMethod": "Método de Redimensionar",
"forceLossless": "Forçar perda",
"audioSettings": "Configurações de Áudio",
"disableAudio": "Desativar Áudio",
"enableAudioInput": "Habilitar entrada de áudio",
"rdpPerformance": "Desempenho",
"enableWallpaper": "Habilitar papel de parede",
"enableTheming": "Habilitar Temas",
"enableFontSmoothing": "Ativar Suavização de Fonte",
"enableFullWindowDrag": "Habilitar Arrastar Janela Completa",
"enableDesktopComposition": "Ativar Composição no Desktop",
"enableMenuAnimations": "Ativar Animações do Menu",
"disableBitmapCaching": "Desabilitar Cache Bitmap",
"disableOffscreenCaching": "Desativar Cache Offscreen",
"disableGlyphCaching": "Desativar Cache Glyph",
"enableGfx": "Enable GFX",
"deviceRedirection": "Redirecionamento do dispositivo",
"enablePrinting": "Ativar impressão",
"enableDrive": "Habilitar redirecionamento de unidade",
"driveName": "Nome da unidade",
"drivePath": "Caminho do Drive",
"createDrivePath": "Criar caminho de unidade",
"disableDownload": "Desativar download",
"disableUpload": "Desabilitar Upload",
"enableTouch": "Ativar Touch",
"rdpSession": "Sessão",
"clientName": "Nome do Cliente",
"consoleSession": "Sessão de Console",
"initialProgram": "Programa inicial",
"serverLayout": "Layout do teclado do servidor",
"timezone": "Timezone",
"gatewaySettings": "Desvio",
"gatewayHostname": "Gateway Hostname",
"gatewayPort": "Porta do Gateway",
"gatewayUsername": "Usuário do Gateway",
"gatewayPassword": "Senha do Gateway",
"gatewayDomain": "Domínio de Gateway",
"remoteApp": "RemoteApp",
"remoteAppProgram": "Aplicativo remoto",
"remoteAppDir": "Diretório da App Remota",
"remoteAppArgs": "Argumentos de App Remoto",
"clipboardSettings": "Área",
"normalizeClipboard": "Normalizar área de transferência",
"disableCopy": "Desativar Cópia",
"disablePaste": "Desativar Colar",
"vncSettings": "Configurações do VNC",
"cursorMode": "Modo Cursor",
"swapRedBlue": "Trocar Vermelho/Azul",
"readOnly": "Somente leitura",
"recordingSettings": "Gravação",
"recordingPath": "Caminho de gravação",
"recordingName": "Nome da gravação",
"createRecordingPath": "Criar Caminho de Gravação",
"excludeOutput": "Excluir saída",
"excludeMouse": "Excluir mouse",
"includeKeys": "Incluir Chaves",
"sendWolPacket": "Enviar Pacote WoL",
"wolMacAddr": "Endereço MAC",
"wolBroadcastAddr": "Endereço de Transmissão",
"wolUdpPort": "UDP Port",
"wolWaitTime": "Tempo de espera (segundos)",
"connectionSettings": "Configurações de conexão",
"rdpOnly": "RDP apenas",
"vncOnly": "Somente VNC",
"telnetTerminalSettings": "Configurações do Terminal",
"terminalType": "Tipo de Terminal",
"guacFontName": "Font Name",
"guacFontSize": "Font Size",
"guacColorScheme": "Esquema de Cor",
"guacBackspaceKey": "Chave de Backspace"
},
"guacamole": {
"connecting": "Conectando à sessão {{type}}",
"rdpConnecting": "Conectando ao servidor RDP...",
"vncConnecting": "Conectando-se ao servidor VNC...",
"telnetConnecting": "Conectando ao servidor Telnet...",
"connectionError": "Erro de conexão",
"connectionFailed": "Conexão falhou",
"failedToConnect": "Falha ao obter token de conexão"
},
"terminal": {
"title": "Terminal",
@@ -1364,7 +1530,7 @@
"closePanel": "Fechar Painel",
"reconnect": "Reconectar",
"sessionEnded": "Sessão Encerrada",
"connectionLost": "Conexão Perdida",
"connectionLost": "Conexão perdida",
"error": "ERRO: {{message}}",
"disconnected": "Desconectado",
"connectionClosed": "Conexão fechada",
@@ -1372,6 +1538,7 @@
"connected": "Conectado",
"clipboardWriteFailed": "Falha ao copiar para a área de transferência. Certifique-se de que a página seja servida por HTTPS ou localhost.",
"clipboardReadFailed": "Falha ao ler da área de transferência. Certifique-se de que as permissões da área de transferência foram concedidas.",
"clipboardHttpWarning": "Colar requer HTTPS. Use Ctrl+Shift+V ou sirva Termix em HTTPS.",
"sshConnected": "Conexão SSH estabelecida",
"authError": "Falha na autenticação: {{message}}",
"unknownError": "Ocorreu um erro desconhecido",
@@ -1380,7 +1547,25 @@
"connecting": "Conectandochar@@0",
"reconnecting": "Reconectando... ({{attempt}}/{{max}})",
"reconnected": "Reconectado com sucesso",
"tmuxSessionCreated": "sessão tmux criada: {{name}}",
"tmuxSessionAttached": "sessão tmux anexada: {{name}}",
"tmuxUnavailable": "tmux não está instalado no host remoto, voltando ao shell padrão",
"tmuxSessionPickerTitle": "tmux Sessions",
"tmuxSessionPickerDesc": "Sessões tmux existentes encontradas neste host. Selecione uma para reanexar ou criar uma nova sessão.",
"tmuxWindows": "Janelas",
"tmuxWindowCount": "{{count}} window",
"tmuxWindowCount_other": "Janelas {{count}}",
"tmuxAttached": "Clientes anexados",
"tmuxAttachedCount": "{{count}} anexou",
"tmuxLastActivity": "Última atividade",
"tmuxTimeJustNow": "neste momento",
"tmuxTimeMinutes": "{{count}}m ago",
"tmuxTimeHours": "{{count}}h ago",
"tmuxTimeDays": "{{count}}d ago",
"tmuxCreateNew": "Iniciar nova sessão",
"tmuxCopyHint": "Ajustar seleção e pressione Enter para copiar para área de transferência",
"maxReconnectAttemptsReached": "Máximo de tentativas de reconexão alcançadas",
"closeTab": "FECHAR",
"connectionTimeout": "Conexão expirada",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
@@ -1404,6 +1589,7 @@
"opksshTimeout": "Tempo de autenticação esgotado. Por favor, tente novamente.",
"opksshAuthFailed": "Falha na autenticação. Por favor, verifique suas credenciais e tente novamente.",
"opksshConfigMissing": "Configuração OPKSSH não encontrada. Por favor, crie ~/.opk/config.yml com as configurações do provedor OIDC. Veja documentação: https://github.com/openpubkey/opkssh#configuration",
"opksshSignInWith": "Entrar com {{provider}}",
"sudoPasswordPopupTitle": "Inserir senha?",
"sudoPasswordPopupHint": "Pressione Enter para inserir, Esc para dispensar",
"sudoPasswordPopupConfirm": "Insert",
@@ -1428,7 +1614,8 @@
"connectionRejected": "Conexão rejeitada pelo servidor. Verifique sua autenticação e configuração de rede.",
"hostKeyRejected": "Verificação de chave do host SSH rejeitada. Conexão cancelada.",
"sessionTakenOver": "A sessão foi aberta em outra aba. Reconectando...",
"sessionAttachTimeout": "O anexo da sessão expirou. Criando nova conexão..."
"sessionAttachTimeout": "O anexo da sessão expirou. Criando nova conexão...",
"openFileManagerHere": "Abrir Gerenciador de Arquivos Aqui"
},
"fileManager": {
"title": "Gerenciador de Arquivos",
@@ -2126,6 +2313,8 @@
"fileColorCodingDesc": "Arquivos de código de cor por tipo: pastas (vermelho), arquivos (azul), links simbólicos (verde)",
"commandAutocomplete": "Auto-completar comando",
"commandAutocompleteDesc": "Ativar sugestões de autocompletar da tecla de aba para comandos de terminal com base no seu histórico de comandos",
"commandHistoryTracking": "Salvar Histórico de Comandos",
"commandHistoryTrackingDesc": "Armazenar comandos do terminal no histórico para preenchimento automático e barra lateral. Desativado por padrão para privacidade.",
"defaultSnippetFoldersCollapsed": "Recolher pastas de fragmentos por padrão",
"defaultSnippetFoldersCollapsedDesc": "Quando ativado, todas as pastas de snippet serão recolhidas quando você abrir a aba de snippets",
"terminalSyntaxHighlighting": "Realce de Sintaxe Terminal",
@@ -2154,7 +2343,7 @@
"terminalSyntaxHighlightingDesc": "Destacar automaticamente comandos, caminhos, IPs e níveis de log na saída do terminal",
"enableCommandPaletteShortcut": "Ativar Atalho da Paleta de Comando",
"enableCommandPaletteShortcutDesc": "Toque duas vezes no botão esquerdo para abrir a Paleta de Comando para acesso rápido aos hosts",
"enableTerminalSessionPersistence": "Sessões Terminais Persistentes",
"enableTerminalSessionPersistence": "Guias persistentes/Sessões",
"enableTerminalSessionPersistenceDesc": "Manter conexões SSH ao alternar abas ou fechar o navegador (pode ser instável)"
},
"user": {
@@ -2364,7 +2553,7 @@
"database": "Banco",
"healthy": "Saudável",
"error": "ERRO",
"totalServers": "Total de Servidores",
"totalHosts": "Hosts totais",
"totalTunnels": "Total de túneis",
"totalCredentials": "Credenciais totais",
"recentActivity": "Atividade recente",

Some files were not shown because too many files have changed in this diff Show More