release-2.7.1 (#1296)

* Add Helm and GitOps deployment setup

* fix: build better-sqlite3 from source in Docker (#1267)

* fix: preserve runtime SSL settings (#1268)

* fix: support forwarding from the memory SSH agent (#1269)

* fix: support forwarding from the memory agent

* style: format memory agent test

* fix: prompt for encrypted SFTP key passphrases (#1270)

* fix: prompt for SFTP key passphrases

* style: format SSH key utility test

* fix: include host context in automation notifications (#1271)

* fix: include host context in automation notifications

* style: format automation notification changes

* fix: reserve sidebar height for host tags (#1272)

* fix: keep host action rows stable at large font sizes (#1273)

* fix: honor certificate setting during server probe (#1274)

* fix: package standard Linux icon sizes (#1275)

* fix: avoid duplicate Docker HTTPS listener (#1276)

* Fix host status without metrics collection (#1277)

* fix: allow eight-digit secure auth codes (#1263)

Allow TOTP prompts to accept secure auth codes longer than six digits without blocking valid authentication attempts.

Generated with Codebuff 🤖

Co-authored-by: Chetan <chetan.development@gmail.com>
Co-authored-by: Codebuff <noreply@codebuff.com>

* Harden Helm deployment defaults

* Update Helm workflow action

* Exclude Helm templates from Prettier

* Fix browser RDP file drops (#1279)

* Fix Proxmox guest credential usernames (#1280)

* Add WSL local terminal option (#1281)

* refactor: split the transfer engine into focused modules (#1282)

* refactor: extract SFTP promisify helpers into sftp-promisify module

* refactor: extract transfer timing and rate stats into transfer-stats module

* refactor: extract transfer error classes and recovery checks into transfer-errors module

* refactor: extract host/path utility helpers into transfer-host-utils module

* refactor: extract SFTP directory tree helpers into transfer-sftp-dir module

* refactor: extract segment copy job builder into transfer-segment-copy module

* refactor: extract file scan and sample helpers into transfer-scan module

* refactor: move throttled progress helper into transfer-stats module

* style: format transfer modules

* perf: optimize tmux monitor aggregation (#1283)

* fix: reserve credential tag row height (#1284)

* feat: edit AI provider model settings (#1285)

* fix: clarify click-to-expand host setting (#1286)

* fix: allow portable imports on remote databases (#1287)

* fix: allow HTTPS to share the configured port (#1288)

* fix: resolve synced jump hosts on the server (#1289)

* fix: make terminal clipboard shortcuts layout independent (#1290)

* fix: use compatible fetch dispatcher for Tailscale (#1291)

* fix: add OIDC environment recovery override (#1292)

* fix: coalesce rapid mobile terminal input (#1293)

* fix: coalesce rapid mobile terminal input

* fix: support clean xterm patch installs

* fix: resolve synced remote desktop host IDs (#1295)

* feat: make the SFTP file manager path bar editable (#1294)

Co-authored-by: Maxime Bonillo <257463937+dropafterfree@users.noreply.github.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>

* feat: add passkey sign in to the login screen

* fix: remove rounded corners from the host list search bar

* fix: stop image storage settings text wrapping to one word per line

* fix: prevent malformed websocket messages from crashing the server

* chore: increment version

* fix: remove gaps between host rows in the sidebar list

Keep sub-pixel row measurements and stop wiping the size cache on hover.

* fix: Failed to connect through jump hosts (#1180)

https://github.com/Termix-SSH/Support/issues/1180

* feat: Progress bar for file downloads in the file manager (#1158)

https://github.com/Termix-SSH/Support/issues/1158

* feat: Allow setting Silent OIDC Login via ENV var (#1174)

https://github.com/Termix-SSH/Support/issues/1174

* feat: `IdentityFile` to limit the number of attempts by agents (#1165)

https://github.com/Termix-SSH/Support/issues/1165

* feat: Credentials clone (#1159)

https://github.com/Termix-SSH/Support/issues/1159

* chore: update release notes

* docs: move helm setup guide to the docs site

* fix: type errors in FilteredAgent agent identity handling

* fix: remove stale better-sqlite3 prebuilds so the source build is used

* fix: actually build better-sqlite3 from source so arm64 docker images work

* fix: credential edit pencil in host editor and add clone action to credential list

* fix: clear editingHost so the credential pencil actually opens the editor

* chore: run format and lint

* fix: folder drag and drop upload failing in the file manager

* chore: sync Crowdin translations for 2.7.1

---------

Co-authored-by: alex-ctms <alex-ctms@users.noreply.github.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Chetan Kumar <74929596+ckloop@users.noreply.github.com>
Co-authored-by: Chetan <chetan.development@gmail.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: ZacharyZcR <payasonorahc@protonmail.com>
Co-authored-by: dropafterfree <maxime.bonillo@gmail.com>
Co-authored-by: Maxime Bonillo <257463937+dropafterfree@users.noreply.github.com>
This commit is contained in:
Luke Gustafson
2026-08-22 19:47:40 -05:00
committed by GitHub
co-authored by Chetan Codebuff Maxime Bonillo ZacharyZcR alex-ctms Chetan Kumar ZacharyZcR dropafterfree
parent 566b908daf
commit 76fd9eedbf
165 changed files with 6778 additions and 2090 deletions
+27 -1
View File
@@ -10,6 +10,7 @@ import {
} from "../../types/automations.js";
import { createCurrentAutomationRepository } from "../database/repositories/factory.js";
import { statsLogger } from "../utils/logger.js";
import { resolveHostById } from "../hosts/host-resolver.js";
import { executeStep } from "./actions/index.js";
import type { StepExecutionContext, StepResult } from "./actions/types.js";
import { compare } from "./conditions.js";
@@ -175,8 +176,33 @@ export class AutomationEngine {
if (!claimed) this.running.add(automation.id);
const trigger = { ...(request.triggerContext ?? {}) };
trigger.type ??= request.triggerType;
let host: TemplateContext["host"];
if (request.triggerHostId) {
try {
const resolved = await resolveHostById(
request.triggerHostId,
automation.userId,
);
if (resolved) {
host = {
id: request.triggerHostId,
name: resolved.name || resolved.ip,
ip: resolved.ip,
username: resolved.username,
port: resolved.port,
};
trigger.hostName ??= host.name;
}
} catch {
// A notification should still run with its numeric host id.
}
}
const template: TemplateContext = {
trigger: request.triggerContext ?? {},
host,
trigger,
steps: {},
vars: {},
run: {
+10
View File
@@ -88,6 +88,16 @@ async function sendWebhook(
headers,
body: JSON.stringify({
title: notification.title,
hostName:
notification.context?.host?.name ??
notification.context?.trigger?.hostName,
hostId:
notification.context?.host?.id ?? notification.context?.trigger?.hostId,
ruleName: notification.title,
ruleId: notification.context?.run?.automationId,
triggerType: notification.context?.trigger?.type,
value: notification.context?.trigger?.value,
threshold: notification.context?.trigger?.threshold,
message: notification.body,
severity: notification.severity,
timestamp: new Date().toISOString(),
+5
View File
@@ -2052,7 +2052,12 @@ if (sslConfig.enabled) {
ssl_port: sslConfig.port,
backend_http_port: HTTP_PORT,
});
}
if (
sslConfig.enabled &&
process.env.TERMIX_SSL_TERMINATED_BY_NGINX !== "true"
) {
try {
const httpsServer = https.createServer(
{
@@ -23,21 +23,17 @@ export async function withSqliteForeignKeysDisabled<T>(
* Backup restore writes tables in an order that is not dependency-safe, so the
* constraints have to stand down for the duration.
*
* **This has no equivalent on Postgres or MySQL here.** Postgres needs
* superuser to disable triggers, and MySQL's `SET FOREIGN_KEY_CHECKS = 0` is
* per-connection, which a pool does not guarantee. Rather than run the import
* with constraints enforced and have it fail partway through — leaving a
* half-restored database — it refuses with a message that says why.
* Postgres and MySQL keep their constraints enabled. The portable importer
* writes through repositories and handles individual row failures, so it must
* still be allowed to run there; only SQLite needs this connection-local
* relaxation for legacy backups whose rows are not dependency ordered.
*/
export async function withCurrentSqliteForeignKeysDisabled<T>(
operation: () => Promise<T>,
): Promise<T> {
const dialect = resolveDatabaseDialect();
if (!needsExplicitPersist(dialect)) {
throw new Error(
`Importing a backup is only supported on SQLite; this deployment uses ${dialect}. ` +
`Restore into the database directly with its own tooling instead.`,
);
return operation();
}
return withSqliteForeignKeysDisabled(getCurrentRepositorySqlite(), operation);
+159
View File
@@ -388,6 +388,165 @@ router.get(
},
);
/**
* @openapi
* /credentials/{id}/duplicate:
* post:
* summary: Duplicate a credential
* description: Creates a new credential from an existing one, optionally overriding fields (e.g. password), leaving the original credential untouched.
* tags:
* - Credentials
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* username:
* type: string
* password:
* type: string
* key:
* type: string
* keyPassword:
* type: string
* responses:
* 201:
* description: New credential created from the duplicate.
* 400:
* description: Invalid request.
* 404:
* description: Credential not found.
* 500:
* description: Failed to duplicate credential.
*/
router.post(
"/:id/duplicate",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
const { name, username, password, key, keyPassword, certPublicKey } =
req.body ?? {};
if (!isNonEmptyString(userId) || !id) {
authLogger.warn("Invalid request for credential duplicate");
return res.status(400).json({ error: "Invalid request" });
}
if (!isNonEmptyString(name)) {
return res.status(400).json({ error: "Name is required" });
}
const credentialId = parseInt(id);
try {
const credentialRepository = createCurrentCredentialRepository();
const source = await credentialRepository.findDecryptedByIdForUser(
userId,
credentialId,
);
if (!source) {
return res.status(404).json({ error: "Credential not found" });
}
const authType = source.authType;
const plainPassword =
password !== undefined ? password || null : source.password;
const plainKey = key !== undefined ? key || null : source.key;
const plainKeyPassword =
keyPassword !== undefined ? keyPassword || null : source.keyPassword;
let keyInfo = null;
if (authType === "key" && plainKey) {
keyInfo = parseSSHKey(plainKey, plainKeyPassword);
if (!keyInfo.success) {
return res.status(400).json({
error: keyInfo.error
? `Invalid SSH key: ${keyInfo.error}`
: "Unrecognized SSH key format. Use an OpenSSH, PEM, or PuTTY PPK v2 RSA/DSA private key.",
});
}
}
const credentialData = {
userId,
name: name.trim(),
description: source.description,
folder: source.folder,
tags: source.tags,
authType,
username:
username !== undefined ? username?.trim() || null : source.username,
password: authType === "password" ? plainPassword : null,
key: authType === "key" ? plainKey : null,
privateKey: authType === "key" ? keyInfo?.privateKey || plainKey : null,
publicKey: authType === "key" ? keyInfo?.publicKey || null : null,
keyPassword: authType === "key" ? plainKeyPassword : null,
keyType: source.keyType,
detectedKeyType: authType === "key" ? keyInfo?.keyType || null : null,
certPublicKey:
authType === "key"
? certPublicKey !== undefined
? certPublicKey?.trim() || null
: source.certPublicKey
: null,
usageCount: 0,
lastUsed: null,
};
const created = await credentialRepository.createEncryptedForUser(
userId,
credentialData,
);
const { ipAddress: dupIp, userAgent: dupUa } = getRequestMeta(req);
await logAudit({
userId,
username: await getAuditUsername(userId),
action: "duplicate_credential",
resourceType: "credential",
resourceId: String(created.id),
resourceName: name,
ipAddress: dupIp,
userAgent: dupUa,
success: true,
});
authLogger.success(
`SSH credential duplicated: ${name} (from ${credentialId}) by user ${userId}`,
{
operation: "credential_duplicate_success",
userId,
sourceCredentialId: credentialId,
credentialId: created.id,
},
);
res.status(201).json(formatCredentialOutput(created));
} catch (err) {
authLogger.error("Failed to duplicate credential", err, {
operation: "credential_duplicate",
userId,
credentialId,
});
res.status(500).json({
error: getErrorMessage(err, "Failed to duplicate credential"),
});
}
},
);
/**
* @openapi
* /credentials/{id}:
@@ -32,7 +32,7 @@ export function resolveProxmoxImportAuth(
return {
authType: "credential",
credentialId,
overrideCredentialUsername: 1,
overrideCredentialUsername: 0,
};
}
+17 -5
View File
@@ -689,6 +689,12 @@ async function syncProxmoxHost(
? existing.connectionType
: null;
const connectionType = existingConnectionType ?? guest.connectionType;
const usesImportCredential =
connectionType === "ssh" && importAuth.authType === "credential";
const existingUsesImportCredential =
usesImportCredential &&
existing?.authType === "credential" &&
existing?.credentialId === importAuth.credentialId;
const port =
typeof existing?.port === "number"
? existing.port
@@ -696,11 +702,13 @@ async function syncProxmoxHost(
? 3389
: 22;
const username =
typeof existing?.username === "string" && existing.username
? existing.username
: connectionType === "rdp"
? ""
: "root";
usesImportCredential && (!existing || existingUsesImportCredential)
? ""
: typeof existing?.username === "string" && existing.username
? existing.username
: connectionType === "rdp"
? ""
: "root";
const update: Record<string, unknown> = {
name: guest.name,
ip: guest.ip || existing?.ip || "0.0.0.0",
@@ -714,6 +722,10 @@ async function syncProxmoxHost(
};
if (existing) {
if (existingUsesImportCredential) {
update.credentialId = importAuth.credentialId;
update.overrideCredentialUsername = false;
}
await createCurrentHostRepository().updateEncryptedForUser(
userId,
existing.id as number,
@@ -7,7 +7,10 @@ import { authLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import type { SSOProviderType } from "../../../types/index.js";
import { createCurrentSsoProviderRepository } from "../repositories/factory.js";
import { getOIDCConfigFromEnv } from "./user-oidc-utils.js";
import {
getOIDCConfigFromEnv,
isOIDCEnvOverrideEnabled,
} from "./user-oidc-utils.js";
import {
decryptSsoConfigSecrets,
encryptSsoConfigSecrets,
@@ -18,6 +21,19 @@ function isOidcLike(type: SSOProviderType): boolean {
return type === "oidc" || type === "github" || type === "google";
}
export function isValidOidcIssuer(value: unknown): boolean {
if (typeof value !== "string") return false;
try {
const url = new URL(value);
return (
["http:", "https:"].includes(url.protocol) &&
!/\/userinfo\/?$/i.test(url.pathname)
);
} catch {
return false;
}
}
const authManager = AuthManager.getInstance();
/**
@@ -95,13 +111,19 @@ export function registerSSOProviderRoutes(router: Router): void {
*/
router.get("/sso-providers", async (_req, res) => {
try {
const envConfig = getOIDCConfigFromEnv();
if (envConfig && isOIDCEnvOverrideEnabled()) {
return res.json([
{ id: 0, name: "SSO", type: "oidc", displayOrder: 0 },
]);
}
const providers =
await createCurrentSsoProviderRepository().listEnabledPublic();
// If no DB providers exist, synthesize one from env vars so SSO login
// remains available when configured purely via environment variables.
if (providers.length === 0) {
const envConfig = getOIDCConfigFromEnv();
if (envConfig) {
providers.push({ id: 0, name: "SSO", type: "oidc", displayOrder: 0 });
}
@@ -222,6 +244,12 @@ export function registerSSOProviderRoutes(router: Router): void {
error: `Missing required OIDC fields: ${missing.join(", ")}`,
});
}
if (c.issuer_url && !isValidOidcIssuer(c.issuer_url)) {
return res.status(400).json({
error:
"Issuer URL must be an HTTP(S) issuer and not a userinfo endpoint",
});
}
if (
(type === "github" || type === "google") &&
(!c.client_id || !c.client_secret)
@@ -353,6 +381,16 @@ export function registerSSOProviderRoutes(router: Router): void {
),
...rawConfig,
};
if (
isOidcLike(effectiveType) &&
mergedConfig.issuer_url &&
!isValidOidcIssuer(mergedConfig.issuer_url)
) {
return res.status(400).json({
error:
"Issuer URL must be an HTTP(S) issuer and not a userinfo endpoint",
});
}
encryptedConfig = await encryptProviderConfig(
mergedConfig,
userId,
@@ -4,7 +4,7 @@ import {
type Router as ExpressRouter,
} from "express";
import { apiLogger } from "../../utils/logger.js";
import { getFetchDispatcher } from "../../utils/proxy-agent.js";
import { fetchWithProxy } from "../../utils/proxy-agent.js";
import { createCurrentSettingsRepository } from "../repositories/factory.js";
interface TailscaleDevice {
@@ -74,12 +74,11 @@ export function registerTailscaleRoutes(
);
const url = `${apiBase}/tailnet/-/devices?fields=all`;
const response = await fetch(url, {
const response = await fetchWithProxy(url, {
headers: {
Authorization: `Bearer ${apiKey}`,
"User-Agent": "Termix/1.0",
},
dispatcher: getFetchDispatcher(url),
});
if (!response.ok) {
+17 -2
View File
@@ -105,6 +105,10 @@ export function getOIDCConfigFromEnv(): OIDCConfig | null {
};
}
export function isOIDCEnvOverrideEnabled(): boolean {
return process.env.OIDC_ENV_OVERRIDE?.toLowerCase() === "true";
}
/**
* Normalizes a group name for comparison. Providers are inconsistent about
* whether they emit bare names (`devops-interns`) or full paths
@@ -438,6 +442,11 @@ export async function loadProviderConfig(
providerType: SSOProviderType;
providerDbId: number | null;
} | null> {
const envConfig = getOIDCConfigFromEnv();
if (envConfig && isOIDCEnvOverrideEnabled()) {
return { config: envConfig, providerType: "oidc", providerDbId: null };
}
if (providerId != null) {
try {
const row =
@@ -485,7 +494,6 @@ export async function loadProviderConfig(
}
// Fallback: env vars
const envConfig = getOIDCConfigFromEnv();
if (envConfig) {
return { config: envConfig, providerType: "oidc", providerDbId: null };
}
@@ -542,6 +550,14 @@ export async function resolveProviderByIssuer(issuer: string): Promise<{
providerDbId: number | null;
} | null> {
const target = normalizeIssuer(issuer);
const envConfig = getOIDCConfigFromEnv();
if (
envConfig?.issuer_url &&
isOIDCEnvOverrideEnabled() &&
normalizeIssuer(envConfig.issuer_url) === target
) {
return { config: envConfig, providerType: "oidc", providerDbId: null };
}
try {
const rows = await createCurrentSsoProviderRepository().listEnabled();
@@ -569,7 +585,6 @@ export async function resolveProviderByIssuer(issuer: string): Promise<{
});
}
const envConfig = getOIDCConfigFromEnv();
if (
envConfig?.issuer_url &&
normalizeIssuer(envConfig.issuer_url) === target
+21 -1
View File
@@ -137,6 +137,12 @@ function isPasswordResetAllowed(): boolean {
}
}
function getOidcSilentLoginDefaultFromEnv(): boolean | undefined {
const envVal = process.env.OIDC_SILENT_LOGIN_DEFAULT;
if (envVal === undefined) return undefined;
return envVal.trim().toLowerCase() === "true";
}
function isNativeAppRequest(req: Request): boolean {
return (
(req.get("User-Agent") || "").startsWith("Termix-Mobile/") ||
@@ -2460,7 +2466,7 @@ router.patch("/oidc-auto-provision", authenticateJWT, async (req, res) => {
* /users/oidc-silent-login-default:
* get:
* summary: Get OIDC silent login default setting
* description: Returns whether silent OIDC login is enabled as the default behavior.
* description: Returns whether silent OIDC login is enabled as the default behavior. Can be pinned via the OIDC_SILENT_LOGIN_DEFAULT env var.
* tags:
* - Users
* responses:
@@ -2471,11 +2477,17 @@ router.patch("/oidc-auto-provision", authenticateJWT, async (req, res) => {
*/
router.get("/oidc-silent-login-default", async (_req, res) => {
try {
const envVal = getOidcSilentLoginDefaultFromEnv();
if (envVal !== undefined) {
res.json({ enabled: envVal, locked: true });
return;
}
res.json({
enabled: await createCurrentSettingsRepository().getBoolean(
"oidc_silent_login_default",
false,
),
locked: false,
});
} catch (err) {
authLogger.error("Failed to get OIDC silent login default", err);
@@ -2507,6 +2519,8 @@ router.get("/oidc-silent-login-default", async (_req, res) => {
* description: Invalid value.
* 403:
* description: Not authorized.
* 409:
* description: Setting is pinned by the OIDC_SILENT_LOGIN_DEFAULT env var.
* 500:
* description: Failed to update setting.
*/
@@ -2520,6 +2534,12 @@ router.patch(
if (!user) {
return res.status(403).json({ error: "Not authorized" });
}
if (getOidcSilentLoginDefaultFromEnv() !== undefined) {
return res.status(409).json({
error:
"OIDC silent login default is set via the OIDC_SILENT_LOGIN_DEFAULT env var and cannot be changed here",
});
}
const { enabled } = req.body;
if (typeof enabled !== "boolean") {
return res.status(400).json({ error: "Invalid value for enabled" });
+22 -6
View File
@@ -9,7 +9,7 @@ import { createCorsMiddleware } from "../../utils/cors-config.js";
import { createCompressionMiddleware } from "../../utils/compression-config.js";
import cookieParser from "cookie-parser";
import axios from "axios";
import { Client as SSHClient } from "ssh2";
import ssh2Pkg, { Client as SSHClient } from "ssh2";
import { SSH_ALGORITHMS } from "../../utils/ssh-algorithms.js";
import { createCurrentHostResolutionRepository } from "../../database/repositories/factory.js";
import { fileLogger } from "../../utils/logger.js";
@@ -42,8 +42,11 @@ import {
} from "./transfer-engine.js";
import { registerFileContentRoutes } from "./content-routes.js";
import { createConnectionLog } from "../connection-log.js";
import { createJumpHostChain } from "../jump-host-chain.js";
import { preparePrivateKeyForSSH2 } from "../../utils/ssh-key-utils.js";
import { createJumpHostChain, JumpHostChainError } from "../jump-host-chain.js";
import {
isPrivateKeyPassphraseError,
preparePrivateKeyForSSH2,
} from "../../utils/ssh-key-utils.js";
import {
ChannelOpenSerializer,
execChannel,
@@ -840,7 +843,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
resolvedCredentials = {
password: resolvedHost.password,
sshKey: resolvedHost.key,
keyPassword: resolvedHost.keyPassword,
keyPassword: keyPassword || resolvedHost.keyPassword,
authType: resolvedHost.authType,
sudoPassword: resolvedHost.sudoPassword as string | undefined,
certPublicKey: (resolvedHost as { certPublicKey?: string })
@@ -909,7 +912,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
resolvedCredentials = {
password: resolvedHost.password,
sshKey: resolvedHost.key,
keyPassword: resolvedHost.keyPassword,
keyPassword: keyPassword || resolvedHost.keyPassword,
authType: resolvedHost.authType,
sudoPassword: resolvedHost.sudoPassword as string | undefined,
certPublicKey: (resolvedHost as { certPublicKey?: string })
@@ -1049,6 +1052,12 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
resolvedCredentials.keyPassword,
);
const parsedKey = ssh2Pkg.utils.parseKey(
config.privateKey as Buffer,
resolvedCredentials.keyPassword,
);
if (parsedKey instanceof Error) throw parsedKey;
if (resolvedCredentials.keyPassword)
config.passphrase = resolvedCredentials.keyPassword;
@@ -1071,6 +1080,10 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
),
);
} catch (keyError) {
if (isPrivateKeyPassphraseError(keyError)) {
return res.json({ status: "passphrase_required", connectionLogs });
}
fileLogger.error("SSH key format error for file manager", {
operation: "file_connect",
sessionId,
@@ -1832,7 +1845,10 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
),
);
return res.status(500).json({
error: "Failed to connect through jump hosts",
error:
error instanceof JumpHostChainError
? `Failed to connect through jump hosts: ${error.message}`
: "Failed to connect through jump hosts",
connectionLogs,
});
}
@@ -0,0 +1,120 @@
type SFTPWrapper = import("ssh2").SFTPWrapper;
export const SFTP_OPEN_READ = 0x00000001;
export const SFTP_OPEN_WRITE = 0x00000002 | 0x00000008 | 0x00000010;
export const SFTP_OPEN_WRITE_RESUME = 0x00000001 | 0x00000002 | 0x00000008;
export function promisifySftpStat(
sftp: SFTPWrapper,
path: string,
): Promise<import("ssh2").Stats> {
return new Promise((resolve, reject) => {
sftp.stat(path, (err, stats) => {
if (err) reject(err);
else resolve(stats);
});
});
}
export function promisifySftpUnlink(
sftp: SFTPWrapper,
path: string,
): Promise<void> {
return new Promise((resolve, reject) => {
sftp.unlink(path, (err) => {
if (err) reject(err);
else resolve();
});
});
}
export function promisifySftpRmdir(
sftp: SFTPWrapper,
path: string,
): Promise<void> {
return new Promise((resolve, reject) => {
sftp.rmdir(path, (err) => {
if (err) reject(err);
else resolve();
});
});
}
export function promisifySftpMkdir(
sftp: SFTPWrapper,
path: string,
mode: number,
): Promise<void> {
return new Promise((resolve, reject) => {
sftp.mkdir(path, { mode }, (err) => {
if (err && (err as NodeJS.ErrnoException).code !== "EEXIST") {
reject(err);
} else {
resolve();
}
});
});
}
export function promisifySftpChmod(
sftp: SFTPWrapper,
path: string,
mode: number,
): Promise<void> {
return new Promise((resolve, reject) => {
sftp.chmod(path, mode, (err) => {
if (err) reject(err);
else resolve();
});
});
}
export function promisifySftpReaddir(
sftp: SFTPWrapper,
path: string,
): Promise<Array<{ filename: string; attrs: import("ssh2").Stats }>> {
return new Promise((resolve, reject) => {
sftp.readdir(path, (err, list) => {
if (err) reject(err);
else resolve(list);
});
});
}
export function promisifySftpOpen(
sftp: SFTPWrapper,
path: string,
flags: number,
mode: number,
): Promise<Buffer> {
return new Promise((resolve, reject) => {
sftp.open(path, flags, mode, (err, handle) => {
if (err) reject(err);
else resolve(handle);
});
});
}
export function promisifySftpClose(
sftp: SFTPWrapper,
handle: Buffer,
): Promise<void> {
return new Promise((resolve, reject) => {
sftp.close(handle, (err) => {
if (err) reject(err);
else resolve();
});
});
}
export function promisifySftpFstat(
sftp: SFTPWrapper,
handle: Buffer,
): Promise<import("ssh2").Stats> {
return new Promise((resolve, reject) => {
sftp.fstat(handle, (err, stats) => {
if (err) reject(err);
else resolve(stats);
});
});
}
+59 -550
View File
@@ -1,12 +1,10 @@
import { getErrorMessage } from "../../utils/error-message.js";
import { randomUUID } from "crypto";
import { networkInterfaces } from "os";
import { performance } from "node:perf_hooks";
import type { ClientChannel } from "ssh2";
import { fileLogger } from "../../utils/logger.js";
import {
basename,
buildPathFromSegments,
dirname,
getWorkingDir,
inferPlatformFromPath,
@@ -14,7 +12,6 @@ import {
normalizeSftpPath,
pathsOverlap,
sftpPathToLocalPath,
splitPathSegments,
type TransferPlatform,
} from "../transfer-paths.js";
import {
@@ -26,6 +23,60 @@ import {
type TransferScanSummary,
} from "./transfer-routing.js";
import { verifySftpFileIntegrity } from "./transfer-integrity.js";
import {
promisifySftpChmod,
promisifySftpClose,
promisifySftpFstat,
promisifySftpOpen,
promisifySftpStat,
promisifySftpUnlink,
} from "./sftp-promisify.js";
import {
buildTransferHopTimings,
computeTransferMbPerSec,
createEmptyXferStats,
createHopWallClock,
createThrottledProgress,
elapsedMs,
hopSpanMs,
mergeXferStats,
noteHopEnd,
noteHopStart,
type PipelinedXferStats,
type TransferTimings,
} from "./transfer-stats.js";
import {
TransferCancelledError,
TransferStalledError,
isRecoverableTransferError,
} from "./transfer-errors.js";
import {
escapeShell,
isLocalSshEndpoint,
isPermissionError,
isRootOnlyPath,
} from "./transfer-host-utils.js";
import {
SFTP_OPEN_READ,
SFTP_OPEN_WRITE,
SFTP_OPEN_WRITE_RESUME,
} from "./sftp-promisify.js";
import {
collectFileWorkItems,
readSftpSample,
type FileWorkItem,
} from "./transfer-scan.js";
import {
deletePathSftp,
ensureDirectoryTreeSftp,
} from "./transfer-sftp-dir.js";
import {
DEFAULT_PARALLEL_SEGMENT_COUNT,
SFTP_XFER_SEGMENT_SIZE,
buildSegmentCopyJobs,
clampParallelSegmentCount,
type SegmentCopyJob,
} from "./transfer-segment-copy.js";
import {
buildDirectProbeCommand,
buildDirectRsyncCommand,
@@ -107,31 +158,11 @@ export type TransferStatus =
"running" | "success" | "partial" | "error" | "cancelled";
export type TransferMethod = "stream" | "tar" | "item_sftp" | "direct_rsync";
export type TransferHopId =
"source_read" | "dest_sftp_write" | "dest_local_write";
export interface TransferHopMetrics {
id: TransferHopId;
bytes: number;
/** Wall-clock span from first I/O on this hop to last I/O complete. */
spanMs: number;
mbPerSec: number;
}
export interface TransferTimings {
prepareDestMs?: number;
compressMs?: number;
transferMs?: number;
extractMs?: number;
verifyMs?: number;
directBenchmarkMs?: number;
relayBenchmarkMs?: number;
sourceDeleteMs?: number;
totalMs?: number;
transferBytes?: number;
endToEndMbPerSec?: number;
hops?: TransferHopMetrics[];
}
export type {
TransferHopId,
TransferHopMetrics,
TransferTimings,
} from "./transfer-stats.js";
export interface TransferProgress {
transferId: string;
@@ -204,34 +235,6 @@ interface ActiveXferControl {
const activeXferControls = new Map<string, ActiveXferControl>();
const cancelWatchdogs = new Map<string, ReturnType<typeof setTimeout>>();
class TransferCancelledError extends Error {
constructor() {
super("Transfer cancelled");
this.name = "TransferCancelledError";
}
}
class TransferStalledError extends Error {
readonly byteOffset?: number;
readonly segmentIndex?: number;
constructor(byteOffset?: number, segmentIndex?: number) {
const pos = byteOffset !== undefined ? ` at byte offset ${byteOffset}` : "";
const seg = segmentIndex !== undefined ? ` (segment ${segmentIndex})` : "";
super(`Transfer stalled — no data moved for 45 seconds${pos}${seg}`);
this.name = "TransferStalledError";
this.byteOffset = byteOffset;
this.segmentIndex = segmentIndex;
}
}
class TransferConnectionLostError extends Error {
constructor(message = "Transfer SSH connection lost") {
super(message);
this.name = "TransferConnectionLostError";
}
}
function throwIfCancelled(transferId: string): void {
if (cancelRequestedTransfers.has(transferId)) {
throw new TransferCancelledError();
@@ -315,8 +318,6 @@ const SMALL_FILE_SYNC_THRESHOLD = 10 * 1024 * 1024;
const SFTP_XFER_CHUNK_SIZE = 256 * 1024;
/** Pipelined in-flight READ requests per leg (ssh2 fastGet/fastPut default is 64). */
const SFTP_XFER_CONCURRENCY = 32;
/** Reset pipelined scheduler every segment to avoid long-run deadlocks at GiB boundaries. */
const SFTP_XFER_SEGMENT_SIZE = 256 * 1024 * 1024;
/** Files above this size use segmented copy; smaller files use a single scheduler run. */
const SFTP_XFER_SEGMENT_THRESHOLD = 32 * 1024 * 1024;
/** Per-segment attempts before giving up (sequential and parallel). */
@@ -328,17 +329,9 @@ const SFTP_SEQUENTIAL_COPY_MAX_ATTEMPTS = 2;
const SFTP_PARALLEL_COPY_MAX_ATTEMPTS = 2;
/** Short backoff before opening fresh dedicated SSH sessions. */
const TRANSFER_SESSION_RESET_DELAYS_MS = [1000, 2000, 3000];
const DEFAULT_PARALLEL_SEGMENT_COUNT = 2;
const MAX_PARALLEL_SEGMENT_COUNT = 8;
const TRANSFER_HANDLE_CLOSE_TIMEOUT_MS = 2500;
const HUNG_TRANSFER_MS = 90_000;
const HUNG_RECONNECTING_MS = 180_000;
const TRANSFER_PROGRESS_INTERVAL_MS = 200;
const SFTP_OPEN_READ = 0x00000001;
/** WRITE | CREATE | TRUNCATE — new file or overwrite from start. */
const SFTP_OPEN_WRITE = 0x00000002 | 0x00000008 | 0x00000010;
/** READ | WRITE | CREATE — resume into an existing partial file without truncating. */
const SFTP_OPEN_WRITE_RESUME = 0x00000001 | 0x00000002 | 0x00000008;
interface TransferReconnectContext {
deps: HostTransferDeps;
@@ -364,31 +357,6 @@ function buildTransferReconnectContext(
return { deps, transferId, ...meta };
}
function isRecoverableTransferConnectionError(err: unknown): boolean {
if (!(err instanceof Error)) return false;
const msg = err.message.toLowerCase();
return (
msg.includes("no response from server") ||
msg.includes("connection lost") ||
msg.includes("not connected") ||
msg.includes("econnreset") ||
msg.includes("econnrefused") ||
msg.includes("etimedout") ||
msg.includes("socket hang up") ||
msg.includes("protocol error") ||
msg.includes("connection closed") ||
msg.includes("channel open failure")
);
}
function isRecoverableTransferError(err: unknown): boolean {
return (
err instanceof TransferStalledError ||
err instanceof TransferConnectionLostError ||
isRecoverableTransferConnectionError(err)
);
}
async function probeDestResumeOffset(
destSftp: SFTPWrapper,
destPath: string,
@@ -560,69 +528,6 @@ async function resetDedicatedTransferSessions(
return { sourceSession, destSession, sourceSftp, destSftp };
}
let cachedLocalAddresses: Set<string> | null = null;
function normalizeHostAddress(host: string): string {
const trimmed = host.trim().toLowerCase();
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
return trimmed.slice(1, -1);
}
return trimmed.split(":")[0] ?? trimmed;
}
function getLocalAddresses(): Set<string> {
if (cachedLocalAddresses) return cachedLocalAddresses;
const addresses = new Set(["127.0.0.1", "::1", "localhost"]);
for (const ifaces of Object.values(networkInterfaces())) {
if (!ifaces) continue;
for (const iface of ifaces) {
if (!iface.internal && iface.family === "IPv4") {
addresses.add(iface.address.toLowerCase());
}
}
}
cachedLocalAddresses = addresses;
return addresses;
}
export function isLocalSshEndpoint(ip?: string): boolean {
if (!ip) return false;
const bare = normalizeHostAddress(ip);
if (bare === "localhost" || bare === "127.0.0.1" || bare === "::1") {
return true;
}
return getLocalAddresses().has(bare);
}
function createThrottledProgress(onProgress?: (bytes: number) => void) {
let pending = 0;
let lastFlush = 0;
const flush = () => {
if (pending > 0) {
onProgress?.(pending);
pending = 0;
lastFlush = Date.now();
}
};
return {
add(bytes: number) {
pending += bytes;
const now = Date.now();
if (now - lastFlush >= TRANSFER_PROGRESS_INTERVAL_MS) {
flush();
}
},
flush,
};
}
function escapeShell(s: string): string {
return s.replace(/'/g, "'\"'\"'");
}
async function detectTransferPlatform(
deps: HostTransferDeps,
session: SSHSessionLike,
@@ -680,150 +585,6 @@ async function detectTransferPlatform(
return "unix";
}
function isRootOnlyPath(path: string): boolean {
const normalized = normalizeSftpPath(path);
return (
normalized === "/" ||
/^\/[A-Za-z]:$/.test(normalized) ||
/^[A-Za-z]:$/.test(normalized)
);
}
function isPermissionError(err: Error): boolean {
const msg = err.message.toLowerCase();
return (
msg.includes("permission denied") ||
msg.includes("eacces") ||
msg.includes("access denied")
);
}
function promisifySftpStat(
sftp: SFTPWrapper,
path: string,
): Promise<import("ssh2").Stats> {
return new Promise((resolve, reject) => {
sftp.stat(path, (err, stats) => {
if (err) reject(err);
else resolve(stats);
});
});
}
function promisifySftpUnlink(sftp: SFTPWrapper, path: string): Promise<void> {
return new Promise((resolve, reject) => {
sftp.unlink(path, (err) => {
if (err) reject(err);
else resolve();
});
});
}
function promisifySftpRmdir(sftp: SFTPWrapper, path: string): Promise<void> {
return new Promise((resolve, reject) => {
sftp.rmdir(path, (err) => {
if (err) reject(err);
else resolve();
});
});
}
async function ensureDirectoryTreeSftp(
sftp: SFTPWrapper,
dirPath: string,
created: Set<string> = new Set(),
): Promise<void> {
const normalized = normalizeSftpPath(dirPath);
if (!normalized || isRootOnlyPath(normalized)) return;
const { root, segments } = splitPathSegments(normalized);
if (segments.length === 0) return;
for (let i = 0; i < segments.length; i++) {
const current = buildPathFromSegments(root, segments, i + 1);
if (created.has(current)) continue;
try {
await promisifySftpMkdir(sftp, current, 0o755);
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code !== "EEXIST") {
try {
const stats = await promisifySftpStat(sftp, current);
if (!stats.isDirectory()) throw err;
} catch {
throw err;
}
}
}
created.add(current);
}
}
async function deletePathSftp(sftp: SFTPWrapper, path: string): Promise<void> {
let stats: import("ssh2").Stats;
try {
stats = await promisifySftpStat(sftp, path);
} catch {
return;
}
if (stats.isDirectory()) {
const entries = await promisifySftpReaddir(sftp, path);
for (const entry of entries) {
if (entry.filename === "." || entry.filename === "..") continue;
await deletePathSftp(sftp, joinPath(path, entry.filename));
}
await promisifySftpRmdir(sftp, path);
return;
}
if (stats.isFile()) {
await promisifySftpUnlink(sftp, path);
}
}
function promisifySftpMkdir(
sftp: SFTPWrapper,
path: string,
mode: number,
): Promise<void> {
return new Promise((resolve, reject) => {
sftp.mkdir(path, { mode }, (err) => {
if (err && (err as NodeJS.ErrnoException).code !== "EEXIST") {
reject(err);
} else {
resolve();
}
});
});
}
function promisifySftpChmod(
sftp: SFTPWrapper,
path: string,
mode: number,
): Promise<void> {
return new Promise((resolve, reject) => {
sftp.chmod(path, mode, (err) => {
if (err) reject(err);
else resolve();
});
});
}
function promisifySftpReaddir(
sftp: SFTPWrapper,
path: string,
): Promise<Array<{ filename: string; attrs: import("ssh2").Stats }>> {
return new Promise((resolve, reject) => {
sftp.readdir(path, (err, list) => {
if (err) reject(err);
else resolve(list);
});
});
}
function execCommand(
deps: HostTransferDeps,
session: SSHSessionLike,
@@ -1157,10 +918,6 @@ function finalizeTransfer(
return result;
}
function elapsedMs(start: number): number {
return Date.now() - start;
}
async function verifyTransferredFile(
deps: HostTransferDeps,
transferId: string,
@@ -1208,103 +965,6 @@ async function verifyTransferredFile(
});
}
export function computeTransferMbPerSec(
bytes: number,
ms: number,
): number | undefined {
if (ms <= 0 || bytes <= 0) return undefined;
return ((bytes / ms) * 1000) / (1024 * 1024);
}
interface HopWallClock {
firstAt: number | null;
lastAt: number | null;
}
function createHopWallClock(): HopWallClock {
return { firstAt: null, lastAt: null };
}
function noteHopStart(
clock: HopWallClock,
t: number = performance.now(),
): void {
if (clock.firstAt === null) clock.firstAt = t;
}
function noteHopEnd(clock: HopWallClock, t: number = performance.now()): void {
clock.lastAt = t;
}
function hopSpanMs(clock: HopWallClock): number {
if (clock.firstAt === null || clock.lastAt === null) return 0;
return Math.max(0, clock.lastAt - clock.firstAt);
}
interface PipelinedXferStats {
bytes: number;
sourceReadSpanMs: number;
destWriteSpanMs: number;
destWriteKind: "sftp" | "local";
}
function createEmptyXferStats(): PipelinedXferStats {
return {
bytes: 0,
sourceReadSpanMs: 0,
destWriteSpanMs: 0,
destWriteKind: "sftp",
};
}
function mergeXferStats(
target: PipelinedXferStats,
source: PipelinedXferStats,
): void {
target.bytes += source.bytes;
target.sourceReadSpanMs += source.sourceReadSpanMs;
target.destWriteSpanMs += source.destWriteSpanMs;
target.destWriteKind = source.destWriteKind;
}
function buildTransferHopTimings(
stats: PipelinedXferStats,
transferMs: number,
): Pick<TransferTimings, "transferBytes" | "endToEndMbPerSec" | "hops"> {
const hops: TransferHopMetrics[] = [];
const sourceRate = computeTransferMbPerSec(
stats.bytes,
stats.sourceReadSpanMs,
);
if (sourceRate !== undefined) {
hops.push({
id: "source_read",
bytes: stats.bytes,
spanMs: stats.sourceReadSpanMs,
mbPerSec: sourceRate,
});
}
const destHopId: TransferHopId =
stats.destWriteKind === "local" ? "dest_local_write" : "dest_sftp_write";
const destRate = computeTransferMbPerSec(stats.bytes, stats.destWriteSpanMs);
if (destRate !== undefined) {
hops.push({
id: destHopId,
bytes: stats.bytes,
spanMs: stats.destWriteSpanMs,
mbPerSec: destRate,
});
}
return {
transferBytes: stats.bytes,
endToEndMbPerSec: computeTransferMbPerSec(stats.bytes, transferMs),
hops,
};
}
async function deleteSourcePathsAfterSuccess(
deps: HostTransferDeps,
transferId: string,
@@ -1334,63 +994,6 @@ async function ensureDestParentForFile(
await ensureDestDirectory(deps, destSession, parent);
}
interface FileWorkItem {
sourcePath: string;
destPath: string;
mode: number;
size: number;
}
async function collectFileWorkItems(
sftp: SFTPWrapper,
sourcePath: string,
destRoot: string,
destBaseName?: string,
): Promise<FileWorkItem[]> {
const stats = await promisifySftpStat(sftp, sourcePath);
const name = destBaseName ?? basename(sourcePath);
const destPath = joinPath(destRoot, name);
if (stats.isFile()) {
return [
{
sourcePath,
destPath,
mode: stats.mode & 0o7777,
size: stats.size,
},
];
}
if (!stats.isDirectory()) {
return [];
}
const items: FileWorkItem[] = [];
const walk = async (srcDir: string, dstDir: string) => {
const entries = await promisifySftpReaddir(sftp, srcDir);
for (const entry of entries) {
if (entry.filename === "." || entry.filename === "..") continue;
const srcChild = joinPath(srcDir, entry.filename);
const dstChild = joinPath(dstDir, entry.filename);
if (entry.attrs.isDirectory()) {
await walk(srcChild, dstChild);
} else if (entry.attrs.isFile()) {
items.push({
sourcePath: srcChild,
destPath: dstChild,
mode: entry.attrs.mode & 0o7777,
size: entry.attrs.size,
});
}
}
};
await walk(sourcePath, destPath);
return items;
}
async function scanSourcePathsForRouting(
sftp: SFTPWrapper,
sourcePaths: string[],
@@ -1430,63 +1033,6 @@ async function scanSourcePathsForRouting(
return summary;
}
async function readSftpSample(
sftp: SFTPWrapper,
path: string,
fileSize: number,
): Promise<Buffer> {
const sampleSize = Math.min(64 * 1024, fileSize);
const position = Math.max(0, Math.floor((fileSize - sampleSize) / 2));
const handle = await promisifySftpOpen(sftp, path, SFTP_OPEN_READ, 0o666);
try {
const buffer = Buffer.alloc(sampleSize);
const bytesRead = await new Promise<number>((resolve, reject) => {
sftp.read(handle, buffer, 0, sampleSize, position, (err, count) => {
if (err) reject(err);
else resolve(count);
});
});
return buffer.subarray(0, bytesRead);
} finally {
await promisifySftpClose(sftp, handle).catch(() => {});
}
}
function promisifySftpOpen(
sftp: SFTPWrapper,
path: string,
flags: number,
mode: number,
): Promise<Buffer> {
return new Promise((resolve, reject) => {
sftp.open(path, flags, mode, (err, handle) => {
if (err) reject(err);
else resolve(handle);
});
});
}
function promisifySftpClose(sftp: SFTPWrapper, handle: Buffer): Promise<void> {
return new Promise((resolve, reject) => {
sftp.close(handle, (err) => {
if (err) reject(err);
else resolve();
});
});
}
function promisifySftpFstat(
sftp: SFTPWrapper,
handle: Buffer,
): Promise<import("ssh2").Stats> {
return new Promise((resolve, reject) => {
sftp.fstat(handle, (err, stats) => {
if (err) reject(err);
else resolve(stats);
});
});
}
interface PipelinedXferOptions {
fileSize?: number;
initialOffset?: number;
@@ -1500,43 +1046,6 @@ interface PipelinedXferOptions {
onResumeOffset?: (offset: number) => void;
}
interface SegmentCopyJob {
offset: number;
length: number;
segmentIndex: number;
}
function clampParallelSegmentCount(value?: number): number {
const n = value ?? DEFAULT_PARALLEL_SEGMENT_COUNT;
return Math.max(1, Math.min(MAX_PARALLEL_SEGMENT_COUNT, Math.floor(n)));
}
function buildSegmentCopyJobs(
fileSize: number,
initialOffset: number,
destResumeSize: number,
): SegmentCopyJob[] {
const jobs: SegmentCopyJob[] = [];
for (
let offset = initialOffset;
offset < fileSize;
offset += SFTP_XFER_SEGMENT_SIZE
) {
const length = Math.min(SFTP_XFER_SEGMENT_SIZE, fileSize - offset);
const segmentIndex = Math.floor(offset / SFTP_XFER_SEGMENT_SIZE);
if (destResumeSize >= offset + length) {
continue;
}
const start = destResumeSize > offset ? destResumeSize : offset;
jobs.push({
offset: start,
length: offset + length - start,
segmentIndex,
});
}
return jobs;
}
function closeAllTransferSessions(
deps: HostTransferDeps,
ctx: TransferReconnectContext,
@@ -0,0 +1,52 @@
export class TransferCancelledError extends Error {
constructor() {
super("Transfer cancelled");
this.name = "TransferCancelledError";
}
}
export class TransferStalledError extends Error {
readonly byteOffset?: number;
readonly segmentIndex?: number;
constructor(byteOffset?: number, segmentIndex?: number) {
const pos = byteOffset !== undefined ? ` at byte offset ${byteOffset}` : "";
const seg = segmentIndex !== undefined ? ` (segment ${segmentIndex})` : "";
super(`Transfer stalled — no data moved for 45 seconds${pos}${seg}`);
this.name = "TransferStalledError";
this.byteOffset = byteOffset;
this.segmentIndex = segmentIndex;
}
}
export class TransferConnectionLostError extends Error {
constructor(message = "Transfer SSH connection lost") {
super(message);
this.name = "TransferConnectionLostError";
}
}
export function isRecoverableTransferConnectionError(err: unknown): boolean {
if (!(err instanceof Error)) return false;
const msg = err.message.toLowerCase();
return (
msg.includes("no response from server") ||
msg.includes("connection lost") ||
msg.includes("not connected") ||
msg.includes("econnreset") ||
msg.includes("econnrefused") ||
msg.includes("etimedout") ||
msg.includes("socket hang up") ||
msg.includes("protocol error") ||
msg.includes("connection closed") ||
msg.includes("channel open failure")
);
}
export function isRecoverableTransferError(err: unknown): boolean {
return (
err instanceof TransferStalledError ||
err instanceof TransferConnectionLostError ||
isRecoverableTransferConnectionError(err)
);
}
@@ -0,0 +1,59 @@
import { networkInterfaces } from "os";
import { normalizeSftpPath } from "../transfer-paths.js";
let cachedLocalAddresses: Set<string> | null = null;
export function normalizeHostAddress(host: string): string {
const trimmed = host.trim().toLowerCase();
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
return trimmed.slice(1, -1);
}
return trimmed.split(":")[0] ?? trimmed;
}
function getLocalAddresses(): Set<string> {
if (cachedLocalAddresses) return cachedLocalAddresses;
const addresses = new Set(["127.0.0.1", "::1", "localhost"]);
for (const ifaces of Object.values(networkInterfaces())) {
if (!ifaces) continue;
for (const iface of ifaces) {
if (!iface.internal && iface.family === "IPv4") {
addresses.add(iface.address.toLowerCase());
}
}
}
cachedLocalAddresses = addresses;
return addresses;
}
export function isLocalSshEndpoint(ip?: string): boolean {
if (!ip) return false;
const bare = normalizeHostAddress(ip);
if (bare === "localhost" || bare === "127.0.0.1" || bare === "::1") {
return true;
}
return getLocalAddresses().has(bare);
}
export function escapeShell(s: string): string {
return s.replace(/'/g, "'\"'\"'");
}
export function isRootOnlyPath(path: string): boolean {
const normalized = normalizeSftpPath(path);
return (
normalized === "/" ||
/^\/[A-Za-z]:$/.test(normalized) ||
/^[A-Za-z]:$/.test(normalized)
);
}
export function isPermissionError(err: Error): boolean {
const msg = err.message.toLowerCase();
return (
msg.includes("permission denied") ||
msg.includes("eacces") ||
msg.includes("access denied")
);
}
@@ -0,0 +1,89 @@
import { basename, joinPath } from "../transfer-paths.js";
import {
SFTP_OPEN_READ,
promisifySftpClose,
promisifySftpOpen,
promisifySftpReaddir,
promisifySftpStat,
} from "./sftp-promisify.js";
type SFTPWrapper = import("ssh2").SFTPWrapper;
export interface FileWorkItem {
sourcePath: string;
destPath: string;
mode: number;
size: number;
}
export async function collectFileWorkItems(
sftp: SFTPWrapper,
sourcePath: string,
destRoot: string,
destBaseName?: string,
): Promise<FileWorkItem[]> {
const stats = await promisifySftpStat(sftp, sourcePath);
const name = destBaseName ?? basename(sourcePath);
const destPath = joinPath(destRoot, name);
if (stats.isFile()) {
return [
{
sourcePath,
destPath,
mode: stats.mode & 0o7777,
size: stats.size,
},
];
}
if (!stats.isDirectory()) {
return [];
}
const items: FileWorkItem[] = [];
const walk = async (srcDir: string, dstDir: string) => {
const entries = await promisifySftpReaddir(sftp, srcDir);
for (const entry of entries) {
if (entry.filename === "." || entry.filename === "..") continue;
const srcChild = joinPath(srcDir, entry.filename);
const dstChild = joinPath(dstDir, entry.filename);
if (entry.attrs.isDirectory()) {
await walk(srcChild, dstChild);
} else if (entry.attrs.isFile()) {
items.push({
sourcePath: srcChild,
destPath: dstChild,
mode: entry.attrs.mode & 0o7777,
size: entry.attrs.size,
});
}
}
};
await walk(sourcePath, destPath);
return items;
}
export async function readSftpSample(
sftp: SFTPWrapper,
path: string,
fileSize: number,
): Promise<Buffer> {
const sampleSize = Math.min(64 * 1024, fileSize);
const position = Math.max(0, Math.floor((fileSize - sampleSize) / 2));
const handle = await promisifySftpOpen(sftp, path, SFTP_OPEN_READ, 0o666);
try {
const buffer = Buffer.alloc(sampleSize);
const bytesRead = await new Promise<number>((resolve, reject) => {
sftp.read(handle, buffer, 0, sampleSize, position, (err, count) => {
if (err) reject(err);
else resolve(count);
});
});
return buffer.subarray(0, bytesRead);
} finally {
await promisifySftpClose(sftp, handle).catch(() => {});
}
}
@@ -0,0 +1,40 @@
export const SFTP_XFER_SEGMENT_SIZE = 256 * 1024 * 1024;
export const DEFAULT_PARALLEL_SEGMENT_COUNT = 2;
export const MAX_PARALLEL_SEGMENT_COUNT = 8;
export interface SegmentCopyJob {
offset: number;
length: number;
segmentIndex: number;
}
export function clampParallelSegmentCount(value?: number): number {
const n = value ?? DEFAULT_PARALLEL_SEGMENT_COUNT;
return Math.max(1, Math.min(MAX_PARALLEL_SEGMENT_COUNT, Math.floor(n)));
}
export function buildSegmentCopyJobs(
fileSize: number,
initialOffset: number,
destResumeSize: number,
): SegmentCopyJob[] {
const jobs: SegmentCopyJob[] = [];
for (
let offset = initialOffset;
offset < fileSize;
offset += SFTP_XFER_SEGMENT_SIZE
) {
const length = Math.min(SFTP_XFER_SEGMENT_SIZE, fileSize - offset);
const segmentIndex = Math.floor(offset / SFTP_XFER_SEGMENT_SIZE);
if (destResumeSize >= offset + length) {
continue;
}
const start = destResumeSize > offset ? destResumeSize : offset;
jobs.push({
offset: start,
length: offset + length - start,
segmentIndex,
});
}
return jobs;
}
@@ -0,0 +1,74 @@
import {
buildPathFromSegments,
joinPath,
normalizeSftpPath,
splitPathSegments,
} from "../transfer-paths.js";
import { isRootOnlyPath } from "./transfer-host-utils.js";
import {
promisifySftpMkdir,
promisifySftpReaddir,
promisifySftpRmdir,
promisifySftpStat,
promisifySftpUnlink,
} from "./sftp-promisify.js";
type SFTPWrapper = import("ssh2").SFTPWrapper;
export async function ensureDirectoryTreeSftp(
sftp: SFTPWrapper,
dirPath: string,
created: Set<string> = new Set(),
): Promise<void> {
const normalized = normalizeSftpPath(dirPath);
if (!normalized || isRootOnlyPath(normalized)) return;
const { root, segments } = splitPathSegments(normalized);
if (segments.length === 0) return;
for (let i = 0; i < segments.length; i++) {
const current = buildPathFromSegments(root, segments, i + 1);
if (created.has(current)) continue;
try {
await promisifySftpMkdir(sftp, current, 0o755);
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code !== "EEXIST") {
try {
const stats = await promisifySftpStat(sftp, current);
if (!stats.isDirectory()) throw err;
} catch {
throw err;
}
}
}
created.add(current);
}
}
export async function deletePathSftp(
sftp: SFTPWrapper,
path: string,
): Promise<void> {
let stats: import("ssh2").Stats;
try {
stats = await promisifySftpStat(sftp, path);
} catch {
return;
}
if (stats.isDirectory()) {
const entries = await promisifySftpReaddir(sftp, path);
for (const entry of entries) {
if (entry.filename === "." || entry.filename === "..") continue;
await deletePathSftp(sftp, joinPath(path, entry.filename));
}
await promisifySftpRmdir(sftp, path);
return;
}
if (stats.isFile()) {
await promisifySftpUnlink(sftp, path);
}
}
@@ -0,0 +1,157 @@
import { performance } from "node:perf_hooks";
const TRANSFER_PROGRESS_INTERVAL_MS = 200;
export type TransferHopId =
"source_read" | "dest_sftp_write" | "dest_local_write";
export interface TransferHopMetrics {
id: TransferHopId;
bytes: number;
/** Wall-clock span from first I/O on this hop to last I/O complete. */
spanMs: number;
mbPerSec: number;
}
export interface TransferTimings {
prepareDestMs?: number;
compressMs?: number;
transferMs?: number;
extractMs?: number;
verifyMs?: number;
directBenchmarkMs?: number;
relayBenchmarkMs?: number;
sourceDeleteMs?: number;
totalMs?: number;
transferBytes?: number;
endToEndMbPerSec?: number;
hops?: TransferHopMetrics[];
}
export function elapsedMs(start: number): number {
return Date.now() - start;
}
export function computeTransferMbPerSec(
bytes: number,
ms: number,
): number | undefined {
if (ms <= 0 || bytes <= 0) return undefined;
return ((bytes / ms) * 1000) / (1024 * 1024);
}
export interface HopWallClock {
firstAt: number | null;
lastAt: number | null;
}
export function createHopWallClock(): HopWallClock {
return { firstAt: null, lastAt: null };
}
export function noteHopStart(
clock: HopWallClock,
t: number = performance.now(),
): void {
if (clock.firstAt === null) clock.firstAt = t;
}
export function noteHopEnd(
clock: HopWallClock,
t: number = performance.now(),
): void {
clock.lastAt = t;
}
export function hopSpanMs(clock: HopWallClock): number {
if (clock.firstAt === null || clock.lastAt === null) return 0;
return Math.max(0, clock.lastAt - clock.firstAt);
}
export function createThrottledProgress(onProgress?: (bytes: number) => void) {
let pending = 0;
let lastFlush = 0;
const flush = () => {
if (pending > 0) {
onProgress?.(pending);
pending = 0;
lastFlush = Date.now();
}
};
return {
add(bytes: number) {
pending += bytes;
const now = Date.now();
if (now - lastFlush >= TRANSFER_PROGRESS_INTERVAL_MS) {
flush();
}
},
flush,
};
}
export interface PipelinedXferStats {
bytes: number;
sourceReadSpanMs: number;
destWriteSpanMs: number;
destWriteKind: "sftp" | "local";
}
export function createEmptyXferStats(): PipelinedXferStats {
return {
bytes: 0,
sourceReadSpanMs: 0,
destWriteSpanMs: 0,
destWriteKind: "sftp",
};
}
export function mergeXferStats(
target: PipelinedXferStats,
source: PipelinedXferStats,
): void {
target.bytes += source.bytes;
target.sourceReadSpanMs += source.sourceReadSpanMs;
target.destWriteSpanMs += source.destWriteSpanMs;
target.destWriteKind = source.destWriteKind;
}
export function buildTransferHopTimings(
stats: PipelinedXferStats,
transferMs: number,
): Pick<TransferTimings, "transferBytes" | "endToEndMbPerSec" | "hops"> {
const hops: TransferHopMetrics[] = [];
const sourceRate = computeTransferMbPerSec(
stats.bytes,
stats.sourceReadSpanMs,
);
if (sourceRate !== undefined) {
hops.push({
id: "source_read",
bytes: stats.bytes,
spanMs: stats.sourceReadSpanMs,
mbPerSec: sourceRate,
});
}
const destHopId: TransferHopId =
stats.destWriteKind === "local" ? "dest_local_write" : "dest_sftp_write";
const destRate = computeTransferMbPerSec(stats.bytes, stats.destWriteSpanMs);
if (destRate !== undefined) {
hops.push({
id: destHopId,
bytes: stats.bytes,
spanMs: stats.destWriteSpanMs,
mbPerSec: destRate,
});
}
return {
transferBytes: stats.bytes,
endToEndMbPerSec: computeTransferMbPerSec(stats.bytes, transferMs),
hops,
};
}
+53 -9
View File
@@ -2,6 +2,8 @@ import { Client as SSHClient } from "ssh2";
import { fileLogger } from "../utils/logger.js";
import { createSocks5Connection } from "../utils/socks5-helper.js";
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
import { preparePrivateKeyForSSH2 } from "../utils/ssh-key-utils.js";
import { getErrorMessage } from "../utils/error-message.js";
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
import { getJumpHostSocks5Config } from "./jump-host-proxy.js";
import { applyAgentAuth } from "./terminal-auth-helpers.js";
@@ -46,6 +48,17 @@ async function resolveJumpHost(
}
}
export class JumpHostChainError extends Error {
constructor(
message: string,
readonly hopIndex: number,
readonly totalHops: number,
) {
super(message);
this.name = "JumpHostChainError";
}
}
export async function createJumpHostChain(
jumpHosts: Array<{ hostId: number }>,
userId: string,
@@ -76,7 +89,11 @@ export async function createJumpHostChain(
totalHops,
});
clients.forEach((c) => c.end());
return null;
throw new JumpHostChainError(
`Jump host ${i + 1} of ${totalHops} was not found`,
i,
totalHops,
);
}
}
@@ -106,11 +123,20 @@ export async function createJumpHostChain(
true,
);
let lastError: Error | null = null;
// eslint-disable-next-line no-async-promise-executor
const connected = await new Promise<boolean>(async (resolve) => {
const readyTimeoutMs = 60000;
const timeout = setTimeout(() => {
lastError = new Error(
`Timed out waiting for jump host ${i + 1}/${totalHops} to authenticate`,
);
resolve(false);
}, 30000);
// ssh2 has no explicit cancel; ending the client stops it from
// firing "ready"/"error" after we've already resolved.
jumpClient.end();
}, readyTimeoutMs + 5000);
jumpClient.on("ready", () => {
clearTimeout(timeout);
@@ -119,6 +145,7 @@ export async function createJumpHostChain(
jumpClient.on("error", (err) => {
clearTimeout(timeout);
lastError = err;
fileLogger.error(
`Jump host ${i + 1}/${totalHops} connection failed`,
err,
@@ -145,7 +172,7 @@ export async function createJumpHostChain(
port: jumpHostConfig.port || 22,
username: jumpHostConfig.username,
tryKeyboard: jumpHostConfig.authType !== "none",
readyTimeout: 60000,
readyTimeout: readyTimeoutMs,
hostVerifier: jumpHostVerifier,
algorithms: {
kex: [
@@ -190,11 +217,19 @@ export async function createJumpHostChain(
if (jumpHostConfig.authType === "password" && jumpHostConfig.password) {
connectConfig.password = jumpHostConfig.password;
} else if (jumpHostConfig.authType === "key" && jumpHostConfig.key) {
const cleanKey = jumpHostConfig.key
.trim()
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
connectConfig.privateKey = Buffer.from(cleanKey, "utf8");
try {
connectConfig.privateKey = preparePrivateKeyForSSH2(
jumpHostConfig.key,
jumpHostConfig.keyPassword,
);
} catch (keyError) {
clearTimeout(timeout);
lastError = new Error(
`Jump host ${i + 1}/${totalHops} key error: ${getErrorMessage(keyError, "Invalid private key format")}`,
);
resolve(false);
return;
}
if (jumpHostConfig.keyPassword) {
connectConfig.passphrase = jumpHostConfig.keyPassword;
}
@@ -237,6 +272,7 @@ export async function createJumpHostChain(
(err, stream) => {
if (err) {
clearTimeout(timeout);
lastError = err;
resolve(false);
return;
}
@@ -254,7 +290,14 @@ export async function createJumpHostChain(
if (!connected) {
clients.forEach((c) => c.end());
return null;
throw new JumpHostChainError(
getErrorMessage(
lastError,
`Jump host ${i + 1} of ${totalHops} failed to connect`,
),
i,
totalHops,
);
}
currentClient = jumpClient;
@@ -262,6 +305,7 @@ export async function createJumpHostChain(
return currentClient;
} catch (error) {
if (error instanceof JumpHostChainError) throw error;
fileLogger.error("Failed to create jump host chain", error, {
operation: "jump_host_chain",
});
+21 -5
View File
@@ -576,16 +576,32 @@ class PollingManager {
} else {
isOnline = await tcpPing(refreshedHost.ip, pingPort, 5000);
}
const config = this.pollingConfigs.get(refreshedHost.id);
let authenticated: boolean | undefined;
if (
isOnline &&
supportsMetrics(refreshedHost) &&
!config?.statsConfig.metricsEnabled
) {
try {
await withSshConnection(refreshedHost, async () => undefined);
authenticated = true;
} catch {
authenticated = false;
}
}
const statusEntry: StatusEntry = {
status: statusAfterReachabilityCheck(
isOnline,
this.statusStore.get(refreshedHost.id)?.status,
),
status:
authenticated === undefined
? statusAfterReachabilityCheck(
isOnline,
this.statusStore.get(refreshedHost.id)?.status,
)
: statusAfterAuthentication(authenticated),
lastChecked: new Date().toISOString(),
};
this.statusStore.set(refreshedHost.id, statusEntry);
if (isOnline && this.activeViewers.has(refreshedHost.id)) {
const config = this.pollingConfigs.get(refreshedHost.id);
if (config?.statsConfig.metricsEnabled) {
this.scheduleInitialMetricsPoll(config.host, config.viewerUserId);
}
+93 -89
View File
@@ -4,6 +4,7 @@ import { SerialPort } from "serialport";
import { AuthManager } from "../utils/auth-manager.js";
import { DataCrypto } from "../utils/data-crypto.js";
import { sshLogger } from "../utils/logger.js";
import { parseWsMessage } from "../utils/ws-message.js";
interface SerialConnectData {
path: string;
@@ -13,11 +14,6 @@ interface SerialConnectData {
parity?: "none" | "even" | "odd";
}
interface WebSocketMessage {
type: string;
data?: SerialConnectData | string | unknown;
}
const authManager = AuthManager.getInstance();
const wss = new WebSocketServer({ port: 30011 });
@@ -93,109 +89,117 @@ wss.on("connection", async (ws: WebSocket, req) => {
};
ws.on("message", async (raw: RawData) => {
let parsed: WebSocketMessage;
let type: string;
let data: unknown;
try {
parsed = JSON.parse(raw.toString()) as WebSocketMessage;
({ type, data } = parseWsMessage(raw));
} catch {
return;
}
const { type, data } = parsed;
switch (type) {
case "list_ports": {
try {
const ports = await SerialPort.list();
send({ type: "ports_list", data: ports });
} catch (err) {
send({
type: "error",
data: getErrorMessage(err, "Failed to list ports"),
});
}
break;
}
case "connect": {
if (port?.isOpen) {
port.close();
port = null;
}
const cfg = data as SerialConnectData;
if (!cfg?.path || !cfg?.baudRate) {
send({ type: "error", data: "Missing port path or baud rate" });
try {
switch (type) {
case "list_ports": {
try {
const ports = await SerialPort.list();
send({ type: "ports_list", data: ports });
} catch (err) {
send({
type: "error",
data: getErrorMessage(err, "Failed to list ports"),
});
}
break;
}
try {
port = new SerialPort({
path: cfg.path,
baudRate: cfg.baudRate,
dataBits: cfg.dataBits ?? 8,
stopBits: cfg.stopBits ?? 1,
parity: cfg.parity ?? "none",
autoOpen: false,
});
case "connect": {
if (port?.isOpen) {
port.close();
port = null;
}
port.open((err) => {
if (err) {
sshLogger.error("Serial port open failed", err, {
operation: "serial_open",
path: cfg.path,
userId,
});
send({ type: "error", data: err.message });
port = null;
return;
}
sshLogger.info("Serial port opened", {
operation: "serial_open",
const cfg = data as SerialConnectData;
if (!cfg?.path || !cfg?.baudRate) {
send({ type: "error", data: "Missing port path or baud rate" });
break;
}
try {
port = new SerialPort({
path: cfg.path,
baudRate: cfg.baudRate,
userId,
dataBits: cfg.dataBits ?? 8,
stopBits: cfg.stopBits ?? 1,
parity: cfg.parity ?? "none",
autoOpen: false,
});
send({ type: "connected" });
});
port.on("data", (chunk: Buffer) => {
send({ type: "data", data: chunk.toString("binary") });
});
port.open((err) => {
if (err) {
sshLogger.error("Serial port open failed", err, {
operation: "serial_open",
path: cfg.path,
userId,
});
send({ type: "error", data: err.message });
port = null;
return;
}
sshLogger.info("Serial port opened", {
operation: "serial_open",
path: cfg.path,
baudRate: cfg.baudRate,
userId,
});
send({ type: "connected" });
});
port.on("error", (err) => {
send({ type: "error", data: err.message });
});
port.on("data", (chunk: Buffer) => {
send({ type: "data", data: chunk.toString("binary") });
});
port.on("close", () => {
send({ type: "disconnected" });
port = null;
});
} catch (err) {
send({
type: "error",
data: getErrorMessage(err, "Failed to open serial port"),
});
}
break;
}
port.on("error", (err) => {
send({ type: "error", data: err.message });
});
case "input": {
if (!port?.isOpen) break;
const input = typeof data === "string" ? data : "";
if (!input) break;
port.write(Buffer.from(input, "binary"), (err) => {
if (err) {
send({ type: "error", data: err.message });
port.on("close", () => {
send({ type: "disconnected" });
port = null;
});
} catch (err) {
send({
type: "error",
data: getErrorMessage(err, "Failed to open serial port"),
});
}
});
break;
}
break;
}
case "disconnect": {
cleanup();
send({ type: "disconnected" });
break;
case "input": {
if (!port?.isOpen) break;
const input = typeof data === "string" ? data : "";
if (!input) break;
port.write(Buffer.from(input, "binary"), (err) => {
if (err) {
send({ type: "error", data: err.message });
}
});
break;
}
case "disconnect": {
cleanup();
send({ type: "disconnected" });
break;
}
}
} catch (err) {
sshLogger.error("Error handling serial WebSocket message", err, {
operation: "serial_message_handler_error",
userId,
messageType: type,
});
send({ type: "error", data: "Failed to process message" });
}
});
+116 -2
View File
@@ -1,13 +1,18 @@
import dgram from "dgram";
import net from "net";
import ssh2Pkg, {
type BaseAgent as BaseAgentType,
type GetStreamCallback,
type IdentityCallback,
type KnownPublicKeys,
type ParsedKey,
type SignCallback,
type SigningRequestOptions,
} from "ssh2";
const { BaseAgent } = ssh2Pkg;
type KnownPublicKey = KnownPublicKeys[number];
const { AgentProtocol, BaseAgent } = ssh2Pkg;
const DEFAULT_PORT_KNOCK_TIMEOUT_MS = 1000;
type Sleep = (ms: number) => Promise<void>;
@@ -34,6 +39,23 @@ export class MemoryAgent extends BaseAgent {
cb(null, [this.key]);
}
getStream(cb: GetStreamCallback): void {
const protocol = new AgentProtocol(false);
protocol.on("identities", (request) => {
protocol.getIdentitiesReply(request, [this.key]);
});
protocol.on("sign", (request, publicKey, data, options) => {
this.sign(publicKey, data, options, (error, signature) => {
if (error || !signature) return protocol.failureReply(request);
protocol.signReply(request, signature);
});
});
cb(null, protocol);
}
sign(
_pubKey: ParsedKey | Buffer | string,
data: Buffer,
@@ -85,6 +107,84 @@ export async function resolveAgentSocket(
return { socketPath: resolved };
}
/**
* Wraps an agent so only identities matching a specific public key are
* offered to the server, mirroring ssh_config's IdentityFile + IdentitiesOnly
* for agent auth. Prevents exhausting the server's MaxAuthTries when the
* agent holds many keys.
*/
export class FilteredAgent extends BaseAgent {
private inner: BaseAgentType;
private publicKeyBlob: Buffer;
constructor(inner: BaseAgentType, publicKeyBlob: Buffer) {
super();
this.inner = inner;
this.publicKeyBlob = publicKeyBlob;
}
private matches(key: KnownPublicKey): boolean {
try {
const blob =
typeof key === "string"
? Buffer.from(key)
: Buffer.isBuffer(key)
? key
: "getPublicSSH" in key
? key.getPublicSSH()
: null;
return (
Buffer.isBuffer(blob) && Buffer.compare(blob, this.publicKeyBlob) === 0
);
} catch {
return false;
}
}
getIdentities(cb: IdentityCallback): void {
this.inner.getIdentities((err, keys) => {
if (err || !keys) return cb(err, keys);
cb(
null,
keys.filter((key) => this.matches(key)),
);
});
}
getStream(cb: GetStreamCallback): void {
if (typeof this.inner.getStream !== "function") {
return cb(new Error("Agent does not support forwarding."));
}
this.inner.getStream(cb);
}
sign(
pubKey: ParsedKey | Buffer | string,
data: Buffer,
optionsOrCb: SigningRequestOptions | SignCallback,
cb?: SignCallback,
): void {
this.inner.sign(
pubKey,
data,
optionsOrCb as SigningRequestOptions,
cb as SignCallback,
);
}
}
function parseAgentIdentityBlob(agentIdentity: string): Buffer | null {
const { utils } = ssh2Pkg;
const parsed = utils.parseKey(agentIdentity.trim());
if (parsed instanceof Error || !parsed) return null;
const key = Array.isArray(parsed) ? parsed[0] : parsed;
try {
return key.getPublicSSH();
} catch {
return null;
}
}
export async function applyAgentAuth(
connectConfig: Record<string, unknown>,
terminalConfig: Record<string, unknown> | undefined,
@@ -93,7 +193,21 @@ export async function applyAgentAuth(
if ("error" in result) return result;
const { createAgent } = ssh2Pkg;
connectConfig.agent = createAgent(result.socketPath);
const agent = createAgent(result.socketPath);
const agentIdentity = (
terminalConfig?.agentIdentity as string | undefined
)?.trim();
if (agentIdentity) {
const publicKeyBlob = parseAgentIdentityBlob(agentIdentity);
if (!publicKeyBlob) {
return { error: "Invalid public key provided for agent identity." };
}
connectConfig.agent = new FilteredAgent(agent, publicKeyBlob);
} else {
connectConfig.agent = agent;
}
return result;
}
@@ -55,6 +55,15 @@ export const HOST_NOT_ON_THIS_SERVER_MESSAGE =
"This host does not exist on the sync server, so the connection was refused. " +
'Run a sync so the server knows about it, or set the connection origin to "This device" for this host.';
export function resolveServerJumpHosts(
clientJumpHosts: Array<{ hostId: number }> | undefined,
serverJumpHosts: Array<{ hostId: number }> | undefined,
hostSyncId?: string | null,
): Array<{ hostId: number }> | undefined {
if (hostSyncId) return serverJumpHosts ?? [];
return clientJumpHosts?.length ? clientJumpHosts : serverJumpHosts;
}
/**
* Thrown where a mismatch is reported by rejecting rather than by messaging
* the socket. Callers whose host-resolution is wrapped in a "failed to resolve
File diff suppressed because it is too large Load Diff
+14 -4
View File
@@ -177,8 +177,8 @@ export function buildPaneMetrics(
const treePids: number[] = [];
const queue = [pane.pid];
const seen = new Set<number>();
while (queue.length > 0) {
const pid = queue.shift()!;
for (let cursor = 0; cursor < queue.length; cursor++) {
const pid = queue[cursor];
if (seen.has(pid)) continue;
seen.add(pid);
if (byPid.has(pid)) treePids.push(pid);
@@ -225,9 +225,19 @@ export function attachPanesToWindows(
windows: Map<string, TmuxWindow[]>,
panes: RawPane[],
): void {
const windowsBySessionAndIndex = new Map<string, Map<number, TmuxWindow>>();
for (const [sessionName, sessionWindows] of windows) {
const byIndex = new Map<number, TmuxWindow>();
for (const window of sessionWindows) {
if (!byIndex.has(window.index)) byIndex.set(window.index, window);
}
windowsBySessionAndIndex.set(sessionName, byIndex);
}
for (const pane of panes) {
const sessionWindows = windows.get(pane.sessionName) || [];
const window = sessionWindows.find((w) => w.index === pane.windowIndex);
const window = windowsBySessionAndIndex
.get(pane.sessionName)
?.get(pane.windowIndex);
if (window) {
const { sessionName: _s, windowIndex: _w, ...paneFields } = pane;
window.panes.push(paneFields);
+20 -2
View File
@@ -384,18 +384,36 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
}
});
// A single bad request must not take the server down. Exit only on errors
// that leave the process genuinely unusable; log and keep serving
// otherwise, since these are almost always scoped to one connection.
const isFatalError = (error: unknown): boolean => {
const code = (error as NodeJS.ErrnoException)?.code;
if (code === "ERR_WORKER_OUT_OF_MEMORY") return true;
if (error instanceof RangeError) {
return /call stack|heap out of memory/i.test(error.message);
}
return false;
};
process.on("uncaughtException", (error) => {
systemLogger.error("Uncaught exception occurred", error, {
operation: "error_handling",
fatal: isFatalError(error),
});
process.exit(1);
if (isFatalError(error)) {
process.exit(1);
}
});
process.on("unhandledRejection", (reason) => {
systemLogger.error("Unhandled promise rejection", reason, {
operation: "error_handling",
fatal: isFatalError(reason),
});
process.exit(1);
if (isFatalError(reason)) {
process.exit(1);
}
});
} catch (error) {
systemLogger.error("Failed to initialize backend services", error, {
@@ -47,6 +47,12 @@ const repository = {
}),
};
const resolveHostById = vi.fn();
vi.mock("../../hosts/host-resolver.js", () => ({
resolveHostById: (...args: unknown[]) => resolveHostById(...args),
}));
vi.mock("../../database/repositories/factory.js", () => ({
createCurrentAutomationRepository: () => repository,
}));
@@ -93,12 +99,41 @@ beforeEach(() => {
nextRunId = 1;
nextStepRowId = 1;
vi.clearAllMocks();
resolveHostById.mockResolvedValue(null);
executeStep.mockResolvedValue({ success: true, output: "ok" });
// The singleton carries in-flight state between tests.
(AutomationEngine as unknown as { instance?: unknown }).instance = undefined;
});
describe("AutomationEngine.run", () => {
it("adds the trigger host name to the template context", async () => {
defineAutomation([step({ id: "notify", type: "notify" })]);
resolveHostById.mockResolvedValue({
name: "Proxmox Node",
ip: "10.0.0.11",
username: "root",
port: 22,
});
await AutomationEngine.getInstance().run({
automationId: 1,
triggerType: "metric_threshold",
triggerHostId: 11,
triggerContext: { hostId: 11, value: 97 },
});
const context = executeStep.mock.calls[0][1] as {
template: {
host: { id: number; name: string };
trigger: { hostName: string };
};
};
expect(context.template.host).toMatchObject({
id: 11,
name: "Proxmox Node",
});
expect(context.template.trigger.hostName).toBe("Proxmox Node");
});
it("runs steps in order and records each one", async () => {
defineAutomation([
step({ id: "a", type: "run_command" }),
@@ -0,0 +1,44 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const automationFetch = vi.fn();
vi.mock("../../automations/http.js", () => ({
automationFetch: (...args: unknown[]) => automationFetch(...args),
}));
const { sendAutomationNotification } =
await import("../../automations/notify.js");
beforeEach(() => {
automationFetch.mockReset();
automationFetch.mockResolvedValue({ ok: true });
});
describe("sendAutomationNotification", () => {
it("keeps alert-compatible host and rule fields in webhook payloads", async () => {
await sendAutomationNotification(
{ id: 1, type: "webhook", config: '{"url":"https://example.com"}' },
{
title: "CPU warning",
body: "cpu.percent is at 97",
severity: "warning",
context: {
host: { id: 11, name: "Proxmox Node" },
trigger: { value: 97, threshold: 90 },
run: { automationId: 42 },
},
},
);
const options = automationFetch.mock.calls[0][1] as RequestInit;
expect(JSON.parse(options.body as string)).toMatchObject({
hostName: "Proxmox Node",
hostId: 11,
ruleName: "CPU warning",
ruleId: 42,
value: 97,
threshold: 90,
message: "cpu.percent is at 97",
});
});
});
@@ -1,7 +1,8 @@
import { sql } from "drizzle-orm";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { HostRepository } from "../../../database/repositories/host-repository.js";
import { DataCrypto } from "../../../utils/data-crypto.js";
describe("HostRepository.reorderForUser", () => {
let adapter: TestSqliteDatabase | null = null;
@@ -72,3 +73,100 @@ describe("HostRepository.reorderForUser", () => {
await expect(repo.reorderForUser("user-1", [])).resolves.toBe(0);
});
});
describe("HostRepository Proxmox sync inserts", () => {
let adapter: TestSqliteDatabase | null = null;
afterEach(async () => {
vi.restoreAllMocks();
await adapter?.close();
adapter = null;
});
it("creates a discovered guest using the scheduled-sync payload", async () => {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
await adapter.exec(`
INSERT INTO users (id, username, password_hash)
VALUES ('user-1', 'alice', 'hash');
INSERT INTO ssh_credentials (id, user_id, name, auth_type, username)
VALUES (7, 'user-1', 'guest key', 'key', 'alice');
`);
const repository = new HostRepository(context);
const now = new Date().toISOString();
vi.spyOn(DataCrypto, "validateUserAccess").mockReturnValue(
Buffer.alloc(32, 1),
);
const created = await repository.createEncryptedForUser("user-1", {
userId: "user-1",
name: "guest",
ip: "10.0.0.8",
port: 22,
username: "",
connectionType: "ssh",
folder: "Proxmox",
tags: "proxmox,qemu,node-1,vm-100",
proxmoxConfig: JSON.stringify({
source: {
source: "proxmox",
sourceHostId: 1,
node: "node-1",
vmid: 100,
type: "qemu",
},
}),
updatedAt: now,
createdAt: now,
pin: false,
authType: "credential",
credentialId: 7,
overrideCredentialUsername: 0,
password: null,
key: null,
keyPassword: null,
keyType: null,
enableTerminal: true,
enableFileManager: true,
enableTunnel: true,
enableDocker: false,
enableSsh: true,
enableRdp: false,
rdpUser: null,
rdpPassword: null,
rdpDomain: null,
rdpSecurity: null,
rdpIgnoreCert: 0,
rdpPort: null,
vncUser: null,
vncPassword: null,
vncPort: null,
telnetUser: null,
telnetPassword: null,
telnetPort: null,
defaultPath: "/",
tunnelConnections: "[]",
jumpHosts: null,
quickActions: null,
statsConfig: null,
dockerConfig: null,
terminalConfig: null,
forceKeyboardInteractive: "false",
useSocks5: 0,
socks5Host: null,
socks5Port: null,
socks5Username: null,
socks5Password: null,
socks5ProxyChain: null,
portKnockSequence: null,
showTerminalInSidebar: 0,
showFileManagerInSidebar: 0,
showTunnelInSidebar: 0,
showDockerInSidebar: 0,
showServerStatsInSidebar: 0,
});
expect(created.username).toBe("");
expect(created.credentialId).toBe(7);
});
});
@@ -0,0 +1,51 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const factory = vi.hoisted(() => ({
getCurrentRepositorySqlite: vi.fn(),
}));
vi.mock("../../../database/repositories/factory.js", () => factory);
import {
withCurrentSqliteForeignKeysDisabled,
withSqliteForeignKeysDisabled,
} from "../../../database/repositories/sqlite-foreign-keys.js";
const previousDatabaseDialect = process.env.DATABASE_DIALECT;
afterEach(() => {
if (previousDatabaseDialect === undefined)
delete process.env.DATABASE_DIALECT;
else process.env.DATABASE_DIALECT = previousDatabaseDialect;
vi.clearAllMocks();
});
describe("withSqliteForeignKeysDisabled", () => {
it("restores foreign keys after an import", async () => {
const sqlite = { exec: vi.fn() };
await expect(
withSqliteForeignKeysDisabled(sqlite, async () => "imported"),
).resolves.toBe("imported");
expect(sqlite.exec.mock.calls).toEqual([
["PRAGMA foreign_keys = OFF"],
["PRAGMA foreign_keys = ON"],
]);
});
});
describe("withCurrentSqliteForeignKeysDisabled", () => {
it.each(["postgres", "mysql"])(
"runs portable imports with constraints enabled on %s",
async (dialect) => {
process.env.DATABASE_DIALECT = dialect;
const operation = vi.fn().mockResolvedValue("imported");
await expect(
withCurrentSqliteForeignKeysDisabled(operation),
).resolves.toBe("imported");
expect(operation).toHaveBeenCalledOnce();
expect(factory.getCurrentRepositorySqlite).not.toHaveBeenCalled();
},
);
});
@@ -9,7 +9,7 @@ describe("resolveProxmoxImportAuth", () => {
expect(resolveProxmoxImportAuth("key", 7)).toEqual({
authType: "credential",
credentialId: 7,
overrideCredentialUsername: 1,
overrideCredentialUsername: 0,
});
});
@@ -17,7 +17,7 @@ describe("resolveProxmoxImportAuth", () => {
expect(resolveProxmoxImportAuth("password", 7)).toEqual({
authType: "credential",
credentialId: 7,
overrideCredentialUsername: 1,
overrideCredentialUsername: 0,
});
});
@@ -35,7 +35,7 @@ describe("resolveProxmoxImportAuth", () => {
expect(resolveProxmoxImportAuth(undefined, 42)).toEqual({
authType: "credential",
credentialId: 42,
overrideCredentialUsername: 1,
overrideCredentialUsername: 0,
});
expect(resolveProxmoxImportAuth(undefined, null)).toEqual({
authType: "none",
@@ -0,0 +1,24 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("../../../utils/auth-manager.js", () => ({
AuthManager: {
getInstance: () => ({ createAdminMiddleware: vi.fn() }),
},
}));
const { isValidOidcIssuer } =
await import("../../../database/routes/sso-provider-routes.js");
describe("isValidOidcIssuer", () => {
it("rejects userinfo endpoints used as issuer URLs", () => {
expect(
isValidOidcIssuer("https://auth.example/application/o/userinfo/"),
).toBe(false);
});
it("accepts an Authentik application issuer", () => {
expect(isValidOidcIssuer("https://auth.example/application/o/termix")).toBe(
true,
);
});
});
@@ -21,6 +21,7 @@ const {
resolveOidcMappedRoles,
verifyOIDCToken,
describeFetchFailure,
isOIDCEnvOverrideEnabled,
} = await import("../../../database/routes/user-oidc-utils.js");
const BACKCHANNEL_LOGOUT_EVENT =
@@ -281,6 +282,7 @@ describe("getOIDCConfigFromEnv", () => {
"OIDC_SCOPES",
"OIDC_ALLOWED_USERS",
"OIDC_ADMIN_GROUP",
"OIDC_ENV_OVERRIDE",
];
const saved: Record<string, string | undefined> = {};
@@ -334,6 +336,12 @@ describe("getOIDCConfigFromEnv", () => {
expect(config?.identifier_path).toBe("email");
expect(config?.scopes).toBe("openid");
});
it("only enables database recovery override when explicitly requested", () => {
expect(isOIDCEnvOverrideEnabled()).toBe(false);
process.env.OIDC_ENV_OVERRIDE = "true";
expect(isOIDCEnvOverrideEnabled()).toBe(true);
});
});
describe("extractOidcGroups", () => {
@@ -0,0 +1,35 @@
import { createRequire } from "node:module";
import { describe, expect, it } from "vitest";
const require = createRequire(import.meta.url);
const { resolveLocalShell } =
require("../../../../electron/local-shell.cjs") as {
resolveLocalShell: (
platform: NodeJS.Platform,
requestedShell?: string,
env?: NodeJS.ProcessEnv,
) => { file: string; args: string[] };
};
describe("resolveLocalShell", () => {
it("starts the default WSL distribution without PowerShell arguments", () => {
expect(resolveLocalShell("win32", "wsl", {})).toEqual({
file: "wsl.exe",
args: [],
});
});
it("keeps PowerShell as the default Windows shell", () => {
expect(resolveLocalShell("win32", "default", {})).toEqual({
file: "powershell.exe",
args: ["-NoLogo"],
});
});
it("preserves the configured shell on non-Windows platforms", () => {
expect(resolveLocalShell("linux", "wsl", { SHELL: "/bin/fish" })).toEqual({
file: "/bin/fish",
args: ["-l"],
});
});
});
@@ -1,4 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { generateKeyPairSync } from "crypto";
import ssh2Pkg, { type ParsedKey } from "ssh2";
const mockAccess = vi.fn();
@@ -6,7 +8,57 @@ vi.mock("fs/promises", () => ({
access: mockAccess,
}));
import { resolveAgentSocket } from "../../hosts/terminal-auth-helpers.js";
import {
MemoryAgent,
FilteredAgent,
resolveAgentSocket,
} from "../../hosts/terminal-auth-helpers.js";
describe("MemoryAgent", () => {
it("serves identities and signatures over the agent protocol", async () => {
const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
const parsed = ssh2Pkg.utils.parseKey(
privateKey.export({ type: "pkcs1", format: "pem" }),
);
expect(parsed).not.toBeInstanceOf(Error);
const agent = new MemoryAgent(parsed as ParsedKey);
const stream = await new Promise<NodeJS.ReadWriteStream>(
(resolve, reject) => {
agent.getStream((error, result) => {
if (error || !result)
reject(error ?? new Error("Missing agent stream"));
else resolve(result);
});
},
);
const client = new ssh2Pkg.AgentProtocol(true);
client.pipe(stream).pipe(client);
const identities = await new Promise<ParsedKey[]>((resolve, reject) => {
client.getIdentities((error, keys) => {
if (error || !keys) reject(error ?? new Error("Missing identities"));
else resolve(keys);
});
});
expect(identities).toHaveLength(1);
expect(identities[0].getPublicSSH()).toEqual(
(parsed as ParsedKey).getPublicSSH(),
);
const data = Buffer.from("forwarded-agent-test");
const signature = await new Promise<Buffer>((resolve, reject) => {
client.sign(identities[0], data, (error, result) => {
if (error || !result) reject(error ?? new Error("Missing signature"));
else resolve(result);
});
});
expect((parsed as ParsedKey).verify(data, signature)).toBe(true);
client.destroy();
stream.destroy();
});
});
describe("resolveAgentSocket", () => {
const originalEnv = process.env.SSH_AUTH_SOCK;
@@ -101,3 +153,78 @@ describe("resolveAgentSocket", () => {
expect(mockAccess).not.toHaveBeenCalled();
});
});
describe("FilteredAgent", () => {
it("only returns identities matching the configured public key", async () => {
const { privateKey: keyA } = generateKeyPairSync("rsa", {
modulusLength: 2048,
});
const { privateKey: keyB } = generateKeyPairSync("rsa", {
modulusLength: 2048,
});
const parsedA = ssh2Pkg.utils.parseKey(
keyA.export({ type: "pkcs1", format: "pem" }),
) as ParsedKey;
const parsedB = ssh2Pkg.utils.parseKey(
keyB.export({ type: "pkcs1", format: "pem" }),
) as ParsedKey;
const inner = {
getIdentities: (cb: (err: Error | null, keys: ParsedKey[]) => void) =>
cb(null, [parsedA, parsedB]),
getStream: vi.fn(),
sign: vi.fn(),
};
const filtered = new FilteredAgent(
inner as unknown as ConstructorParameters<typeof FilteredAgent>[0],
parsedB.getPublicSSH(),
);
const identities = await new Promise<ParsedKey[]>((resolve, reject) => {
filtered.getIdentities((err, keys) => {
if (err || !keys) reject(err ?? new Error("Missing identities"));
else resolve(keys);
});
});
expect(identities).toHaveLength(1);
expect(identities[0].getPublicSSH()).toEqual(parsedB.getPublicSSH());
});
it("returns no identities when nothing matches", async () => {
const { privateKey: keyA } = generateKeyPairSync("rsa", {
modulusLength: 2048,
});
const { privateKey: keyB } = generateKeyPairSync("rsa", {
modulusLength: 2048,
});
const parsedA = ssh2Pkg.utils.parseKey(
keyA.export({ type: "pkcs1", format: "pem" }),
) as ParsedKey;
const parsedB = ssh2Pkg.utils.parseKey(
keyB.export({ type: "pkcs1", format: "pem" }),
) as ParsedKey;
const inner = {
getIdentities: (cb: (err: Error | null, keys: ParsedKey[]) => void) =>
cb(null, [parsedA]),
getStream: vi.fn(),
sign: vi.fn(),
};
const filtered = new FilteredAgent(
inner as unknown as ConstructorParameters<typeof FilteredAgent>[0],
parsedB.getPublicSSH(),
);
const identities = await new Promise<ParsedKey[]>((resolve, reject) => {
filtered.getIdentities((err, keys) => {
if (err || !keys) reject(err ?? new Error("Missing identities"));
else resolve(keys);
});
});
expect(identities).toHaveLength(0);
});
});
@@ -6,6 +6,7 @@ import {
HostAddressMismatchError,
HostNotOnThisServerError,
normalizeHostAddress,
resolveServerJumpHosts,
} from "../../../hosts/terminal/host-identity.js";
/**
@@ -59,6 +60,20 @@ describe("hostAddressMismatch", () => {
});
});
describe("resolveServerJumpHosts", () => {
it("uses server-side ids for a sync-delegated connection", () => {
expect(
resolveServerJumpHosts([{ hostId: 7 }], [{ hostId: 42 }], "host-sync-id"),
).toEqual([{ hostId: 42 }]);
});
it("keeps client ids for a local id-based connection", () => {
expect(resolveServerJumpHosts([{ hostId: 7 }], [{ hostId: 42 }])).toEqual([
{ hostId: 7 },
]);
});
});
describe("HostAddressMismatchError", () => {
it("survives the catch blocks that swallow resolution failures", () => {
// SFTP host resolution sits inside "failed to resolve credentials, carry
@@ -9,6 +9,8 @@ import {
buildPaneMetrics,
attachPanesToWindows,
shellEscape,
type ProcessInfo,
type TmuxWindow,
} from "../../../hosts/tmux/monitor-helpers.js";
function join(...fields: (string | number)[]): string {
@@ -194,6 +196,28 @@ describe("buildPaneMetrics", () => {
const metrics = buildPaneMetrics(pane, cyclic, new Map());
expect(metrics[0].processCount).toBe(2);
});
it("aggregates a wide process tree without dropping children", () => {
const childCount = 2_000;
const wideTree: ProcessInfo[] = [
{ pid: 1, ppid: 0, cpu: 0, mem: 0, rss: 1, comm: "bash" },
...Array.from({ length: childCount }, (_, index) => ({
pid: index + 2,
ppid: 1,
cpu: 0.1,
mem: 0,
rss: 1,
comm: `worker-${index}`,
})),
];
const pane = parsePanes(
join("wide", 0, "%1", 0, 1, 1, 80, 24, "bash", "/", "t"),
);
const [metrics] = buildPaneMetrics(pane, wideTree, new Map());
expect(metrics.processCount).toBe(childCount + 1);
expect(metrics.memRssKb).toBe(childCount + 1);
});
});
describe("attachPanesToWindows", () => {
@@ -214,6 +238,29 @@ describe("attachPanesToWindows", () => {
expect(windows.get("s1")![0].panes[0].id).toBe("%1");
expect(windows.get("s1")![1].panes[0].id).toBe("%2");
});
it("preserves first-match behavior for duplicate window indexes", () => {
const first: TmuxWindow = {
index: 0,
name: "first",
active: true,
panes: [],
};
const duplicate: TmuxWindow = {
index: 0,
name: "duplicate",
active: false,
panes: [],
};
const windows = new Map([["s1", [first, duplicate]]]);
const panes = parsePanes(
join("s1", 0, "%1", 0, 100, 1, 80, 24, "bash", "/", "t"),
);
attachPanesToWindows(windows, panes);
expect(first.panes).toHaveLength(1);
expect(duplicate.panes).toHaveLength(0);
});
});
describe("shellEscape", () => {
@@ -0,0 +1,41 @@
import { createServer } from "node:http";
import { afterEach, describe, expect, it } from "vitest";
import { fetchWithProxy } from "../../utils/proxy-agent.js";
describe("fetchWithProxy", () => {
const savedProxies = {
HTTP_PROXY: process.env.HTTP_PROXY,
HTTPS_PROXY: process.env.HTTPS_PROXY,
http_proxy: process.env.http_proxy,
https_proxy: process.env.https_proxy,
};
afterEach(() => {
for (const [name, value] of Object.entries(savedProxies)) {
if (value === undefined) delete process.env[name];
else process.env[name] = value;
}
});
it("uses a dispatcher compatible with the selected fetch implementation", async () => {
delete process.env.HTTP_PROXY;
delete process.env.HTTPS_PROXY;
delete process.env.http_proxy;
delete process.env.https_proxy;
const server = createServer((_request, response) => response.end("ok"));
await new Promise<void>((resolve) =>
server.listen(0, "127.0.0.1", resolve),
);
try {
const address = server.address();
if (!address || typeof address === "string") throw new Error("No port");
const response = await fetchWithProxy(`http://127.0.0.1:${address.port}`);
expect(await response.text()).toBe("ok");
} finally {
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
}
});
});
@@ -4,6 +4,7 @@ import {
parseSSHKey,
parsePublicKey,
preparePrivateKeyForSSH2,
isPrivateKeyPassphraseError,
getFriendlyKeyTypeName,
validateKeyPair,
} from "../../utils/ssh-key-utils.js";
@@ -97,6 +98,22 @@ describe("parseSSHKey", () => {
});
});
describe("isPrivateKeyPassphraseError", () => {
it("recognizes missing and incorrect passphrase errors", () => {
expect(
isPrivateKeyPassphraseError(
new Error(
"Encrypted OpenSSH private key detected, but no passphrase given",
),
),
).toBe(true);
expect(isPrivateKeyPassphraseError(new Error("Bad passphrase"))).toBe(true);
expect(
isPrivateKeyPassphraseError(new Error("Unsupported key format")),
).toBe(false);
});
});
describe("getFriendlyKeyTypeName", () => {
it("maps known key types to friendly names", () => {
expect(getFriendlyKeyTypeName("ssh-rsa")).toBe("RSA");
@@ -0,0 +1,99 @@
import { describe, it, expect } from "vitest";
import {
parseWsMessage,
asObject,
asString,
toTerminalDimension,
WsMessageError,
} from "../../utils/ws-message.js";
const frame = (s: string) => Buffer.from(s, "utf8");
describe("parseWsMessage", () => {
it("parses a well-formed message", () => {
expect(parseWsMessage(frame('{"type":"ping"}'))).toEqual({
type: "ping",
data: undefined,
});
expect(parseWsMessage(frame('{"type":"input","data":"ls"}'))).toEqual({
type: "input",
data: "ls",
});
});
it("rejects JSON that parses but cannot be destructured", () => {
// The original DoS: JSON.parse("null") succeeds, so it escaped the
// try/catch and threw a TypeError on destructure.
for (const payload of ["null", "123", '"str"', "[1,2]", "true"]) {
expect(() => parseWsMessage(frame(payload))).toThrow(WsMessageError);
}
});
it("rejects invalid JSON", () => {
expect(() => parseWsMessage(frame("{oops"))).toThrow(WsMessageError);
expect(() => parseWsMessage(frame(""))).toThrow(WsMessageError);
});
it("rejects a missing or non-string type", () => {
expect(() => parseWsMessage(frame("{}"))).toThrow(WsMessageError);
expect(() => parseWsMessage(frame('{"type":5}'))).toThrow(WsMessageError);
expect(() => parseWsMessage(frame('{"type":null}'))).toThrow(
WsMessageError,
);
});
it("rejects oversized frames", () => {
const huge = Buffer.alloc(1024 * 1024 + 1, 0x20);
expect(() => parseWsMessage(huge)).toThrow(WsMessageError);
});
it("never throws a TypeError for any malformed input", () => {
const payloads = [
"null",
"0",
"[]",
"{}",
'{"type":{}}',
'{"data":"x"}',
"undefined",
'{"type":"a","data":null}',
];
for (const p of payloads) {
try {
parseWsMessage(frame(p));
} catch (e) {
expect(e).toBeInstanceOf(WsMessageError);
}
}
});
});
describe("asObject / asString", () => {
it("narrows without throwing", () => {
expect(asObject({ a: 1 })).toEqual({ a: 1 });
expect(asObject(null)).toEqual({});
expect(asObject([1])).toEqual({});
expect(asObject("x")).toEqual({});
expect(asString("x")).toBe("x");
expect(asString(5)).toBe("");
expect(asString(undefined)).toBe("");
});
});
describe("toTerminalDimension", () => {
it("accepts sane values", () => {
expect(toTerminalDimension(80)).toBe(80);
expect(toTerminalDimension("120")).toBe(120);
expect(toTerminalDimension(24.7)).toBe(24);
});
it("rejects values that would poison setWindow", () => {
for (const bad of [0, -1, NaN, Infinity, null, undefined, "abc", {}]) {
expect(toTerminalDimension(bad)).toBe(0);
}
});
it("clamps absurdly large values", () => {
expect(toTerminalDimension(1e9)).toBe(10000);
});
});
+11 -1
View File
@@ -1,4 +1,4 @@
import { Agent, ProxyAgent } from "undici";
import { Agent, ProxyAgent, fetch as undiciFetch } from "undici";
import type { Dispatcher } from "undici-types";
const directAgent = new Agent({
@@ -39,3 +39,13 @@ export function getProxyAgent(targetUrl?: string): Dispatcher | undefined {
export function getFetchDispatcher(targetUrl: string): Dispatcher {
return getProxyAgent(targetUrl) ?? (directAgent as unknown as Dispatcher);
}
export function fetchWithProxy(
url: string,
init: RequestInit = {},
): Promise<Response> {
return undiciFetch(url, {
...init,
dispatcher: getFetchDispatcher(url),
});
}
+4
View File
@@ -256,6 +256,10 @@ export function preparePrivateKeyForSSH2(
return Buffer.from(cleanKey, "utf8");
}
export function isPrivateKeyPassphraseError(error: unknown): boolean {
return /passphrase/i.test(getErrorMessage(error, ""));
}
export function parseSSHKey(
privateKeyData: string,
passphrase?: string,
+76
View File
@@ -0,0 +1,76 @@
import type { RawData } from "ws";
// Cap on a single decoded text frame. Anything larger is almost certainly
// abuse - the legitimate control messages here are tiny, and terminal input is
// bounded by what a user can type or paste.
const MAX_MESSAGE_BYTES = 1024 * 1024;
export class WsMessageError extends Error {}
function rawByteLength(raw: RawData): number {
if (Buffer.isBuffer(raw)) return raw.length;
if (Array.isArray(raw))
return raw.reduce((sum, part) => sum + part.length, 0);
if (raw instanceof ArrayBuffer) return raw.byteLength;
return 0;
}
/**
* Parse a WebSocket frame into a plain message object.
*
* Throws WsMessageError - never a TypeError - for anything malformed, so
* callers can reject the frame instead of the parse blowing up the handler.
* `JSON.parse` happily returns null, numbers and arrays, none of which are
* safe to destructure, so the shape is checked here rather than at each site.
*/
export function parseWsMessage(raw: RawData): {
type: string;
data: unknown;
} {
if (rawByteLength(raw) > MAX_MESSAGE_BYTES) {
throw new WsMessageError("Message too large");
}
let parsed: unknown;
try {
parsed = JSON.parse(raw.toString());
} catch {
throw new WsMessageError("Invalid JSON");
}
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new WsMessageError("Message must be a JSON object");
}
const { type, data } = parsed as { type?: unknown; data?: unknown };
if (typeof type !== "string") {
throw new WsMessageError("Message type must be a string");
}
return { type, data };
}
/** Narrow unknown payload data to a plain object without throwing. */
export function asObject(data: unknown): Record<string, unknown> {
return data !== null && typeof data === "object" && !Array.isArray(data)
? (data as Record<string, unknown>)
: {};
}
/** Narrow unknown payload data to a string without throwing. */
export function asString(value: unknown): string {
return typeof value === "string" ? value : "";
}
/**
* Coerce a client-supplied terminal width/height to a sane integer.
* Returns 0 when the value is unusable, so callers can skip the resize
* rather than pass NaN or a negative into ssh2's setWindow.
*/
export function toTerminalDimension(value: unknown): number {
const n = typeof value === "number" ? value : Number(value);
if (!Number.isFinite(n)) return 0;
const rounded = Math.floor(n);
if (rounded < 1) return 0;
return Math.min(rounded, 10000);
}
+5 -1
View File
@@ -40,7 +40,10 @@ export interface ElectronAPI {
getServerConfig: () => Promise<ServerConfig>;
saveServerConfig: (config: ServerConfig) => Promise<{ success: boolean }>;
testServerConnection: (serverUrl: string) => Promise<ConnectionTestResult>;
testServerConnection: (
serverUrl: string,
allowInvalidCertificate?: boolean,
) => Promise<ConnectionTestResult>;
getC2STunnelConfig: () => Promise<unknown[]>;
saveC2STunnelConfig: (
config: unknown[],
@@ -171,6 +174,7 @@ export interface ElectronAPI {
startLocalTerminal(dimensions: {
cols: number;
rows: number;
shell?: "default" | "wsl";
}): Promise<{ sessionId: string; shell: string }>;
readyLocalTerminal(sessionId: string): Promise<boolean>;
writeLocalTerminal(sessionId: string, data: string): Promise<boolean>;
+1
View File
@@ -715,6 +715,7 @@ export interface TerminalConfig {
linkClickBehavior?: "confirm" | "direct";
useSSHTitle?: boolean;
agentSocketPath?: string;
agentIdentity?: string;
customThemeColors?: {
background: string;
foreground: string;
+15
View File
@@ -55,6 +55,21 @@ export async function updateCredential(
}
}
export async function duplicateCredential(
credentialId: number,
data: Record<string, unknown>,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.post(
`/credentials/${credentialId}/duplicate`,
data,
);
return response.data;
} catch (error) {
throw handleApiError(error, "duplicate credential");
}
}
export async function deleteCredential(
credentialId: number,
): Promise<Record<string, unknown>> {
+10 -1
View File
@@ -6,6 +6,7 @@ import {
} from "@/main-axios";
import type { AxiosInstance } from "axios";
import type { GuacamoleConfig } from "@/types/guacamole-config";
import { resolveRemoteHostId } from "@/lib/remote-server-api";
/**
* The embedded desktop backend does not bundle guacd, which is why
@@ -185,10 +186,18 @@ export async function getGuacamoleTokenFromHost(
password?: string;
domain?: string;
},
syncId?: string | null,
): Promise<GuacamoleTokenResponse> {
try {
const remoteHostId = isElectron()
? await resolveRemoteHostId(syncId)
: null;
if (isElectron() && syncId && remoteHostId === null) {
throw new Error("The synced host does not exist on the remote server");
}
const targetHostId = remoteHostId ?? hostId;
const response = await guacamoleApi().post(
`/guacamole/connect-host/${hostId}`,
`/guacamole/connect-host/${targetHostId}`,
{
...(protocol ? { protocol } : {}),
...(promptedCredentials?.username
+13 -1
View File
@@ -549,14 +549,26 @@ export async function downloadSSHFile(
}
}
export interface DownloadProgressEvent {
loaded: number;
total?: number;
}
export async function downloadSSHFileStream(
sessionId: string,
filePath: string,
onProgress?: (event: DownloadProgressEvent) => void,
): Promise<void> {
const response = await getFileManagerApiForSession(sessionId).post(
"/ssh/downloadFileStream",
{ sessionId, path: filePath },
{ responseType: "blob", timeout: 0 },
{
responseType: "blob",
timeout: 0,
onDownloadProgress: onProgress
? (event) => onProgress({ loaded: event.loaded, total: event.total })
: undefined,
},
);
const blob = response.data as Blob;
const fileName = filePath.split("/").pop() || "download";
+1
View File
@@ -257,6 +257,7 @@ export async function updateOidcAutoProvision(
export async function getOidcSilentLoginDefault(): Promise<{
enabled: boolean;
locked?: boolean;
}> {
try {
const response = await authApi.get("/users/oidc-silent-login-default");
+54 -1
View File
@@ -1,8 +1,14 @@
import type {
AuthenticationResponseJSON,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from "@simplewebauthn/browser";
import { startRegistration } from "@simplewebauthn/browser";
import {
browserSupportsWebAuthn,
startAuthentication,
startRegistration,
} from "@simplewebauthn/browser";
import { authApi, handleApiError } from "@/main-axios";
export type WebAuthnUserVerification = "discouraged" | "preferred" | "required";
@@ -23,6 +29,53 @@ type RegistrationOptionsResponse = {
challengeId: string;
};
type AuthenticationOptionsResponse = {
options: PublicKeyCredentialRequestOptionsJSON;
challengeId: string;
};
export type PasskeyLoginResult = {
success: boolean;
requires_totp?: boolean;
temp_token?: string;
is_admin?: boolean;
username?: string;
userId?: string;
is_oidc?: boolean;
totp_enabled?: boolean;
token?: string;
};
export function isPasskeySupported(): boolean {
return browserSupportsWebAuthn();
}
export async function loginWithPasskey(
username?: string,
rememberMe = false,
): Promise<PasskeyLoginResult> {
try {
const optionsResponse = await authApi.post<AuthenticationOptionsResponse>(
"/users/webauthn/authenticate/options",
username ? { username } : {},
);
const credential = await startAuthentication({
optionsJSON: optionsResponse.data.options,
});
const verifyResponse = await authApi.post<PasskeyLoginResult>(
"/users/webauthn/authenticate/verify",
{
challengeId: optionsResponse.data.challengeId,
response: credential as AuthenticationResponseJSON,
rememberMe,
},
);
return verifyResponse.data;
} catch (error) {
throw handleApiError(error, "sign in with passkey");
}
}
export async function listWebAuthnCredentials(): Promise<{
credentials: WebAuthnCredentialSummary[];
}> {
+103
View File
@@ -14,6 +14,7 @@ import {
CheckCircle2,
ChevronDown,
ChevronUp,
Fingerprint,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import {
@@ -36,6 +37,7 @@ import {
requestTrustedProxyLogin,
} from "@/main-axios";
import { getSSOProviders, ldapLogin } from "@/api/sso-provider-api";
import { isPasskeySupported, loginWithPasskey } from "@/api/webauthn-api";
import type { SSOProviderPublic } from "@/types/index";
import { Checkbox } from "@/components/checkbox";
import {
@@ -217,6 +219,13 @@ export function Auth({ onLogin }: AuthProps) {
const [providerLoading, setProviderLoading] = useState<
Record<number, boolean>
>({});
const [passkeySupported] = useState(() => {
try {
return isPasskeySupported();
} catch {
return false;
}
});
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
@@ -635,6 +644,72 @@ export function Auth({ onLogin }: AuthProps) {
}
}
async function handlePasskeyLogin() {
setLoading(true);
try {
const res = await loginWithPasskey(
username.trim() || undefined,
rememberMe,
);
if (res.requires_totp) {
setTotpTempToken(res.temp_token ?? "");
setView("totp");
return;
}
if (!res?.success) throw new Error(t("auth.passkeyLoginFailed"));
if (isInMobileWebView()) {
const token = res?.token ?? "";
(window as ExtendedWindow).ReactNativeWebView?.postMessage(
JSON.stringify({ type: "AUTH_SUCCESS", token }),
);
setWebviewAuthSuccess(true);
return;
}
if (isInElectronWebView()) {
// Same as handleLogin: the iframe never sends X-Electron-App, so read
// the JWT back from the cookie that was just set.
const token = res?.token ?? (await getCurrentToken());
window.parent.postMessage(
{
type: "AUTH_SUCCESS",
source: "passkey_auth_component",
platform: "desktop",
token: token ?? null,
timestamp: Date.now(),
},
"*",
);
setWebviewAuthSuccess(true);
return;
}
const meRes = await getUserInfo();
storeAuth(meRes.username || res.username || "");
toast.success(t("messages.loginSuccess"));
onLogin(
meRes.username || res.username || "",
meRes.userId || undefined,
!!meRes.is_admin,
);
} catch (err: unknown) {
const error = err as {
name?: string;
message?: string;
response?: { data?: { error?: string } };
};
// Closing or cancelling the browser prompt is not a failure worth a toast.
if (error?.name === "NotAllowedError" || error?.name === "AbortError") {
return;
}
toast.error(
error?.response?.data?.error ||
error?.message ||
t("auth.passkeyLoginFailed"),
);
} finally {
setLoading(false);
}
}
async function handleRegister(e: React.FormEvent) {
e.preventDefault();
if (!username.trim()) {
@@ -1469,6 +1544,20 @@ export function Auth({ onLogin }: AuthProps) {
</Button>
);
})}
{passkeySupported && (
<Button
type="button"
variant="outline"
onClick={handlePasskeyLogin}
disabled={loading}
className="w-full h-10 font-bold"
>
<span className="flex items-center gap-2">
<Fingerprint className="size-4" />
{t("auth.signInWithPasskey")}
</span>
</Button>
)}
</div>
</div>
)}
@@ -1536,6 +1625,20 @@ export function Auth({ onLogin }: AuthProps) {
</span>
)}
</Button>
{passkeySupported && (
<Button
type="button"
variant="outline"
onClick={handlePasskeyLogin}
disabled={loading}
className="w-full h-10 font-bold"
>
<span className="flex items-center gap-2">
<Fingerprint className="size-4" />
{t("auth.signInWithPasskey")}
</span>
</Button>
)}
</form>
)}
@@ -154,7 +154,10 @@ export function ProxmoxDiscoverDialog({
// the real IP. Re-sync keeps the manual value (guest.ip || existing.ip).
ip: g.ip || "0.0.0.0",
port: g.connectionType === "rdp" ? 3389 : 22,
username: defaultUsername ?? "root",
username:
importAuth.authType === "credential"
? ""
: (defaultUsername ?? "root"),
folder: importFolder,
// Inherit the jump-host chain from the scanned Proxmox host so the
// imported guests are reachable the same way; user can override.
@@ -30,7 +30,7 @@ export function resolveProxmoxImportAuth(
return {
authType: "credential",
credentialId,
overrideCredentialUsername: true,
overrideCredentialUsername: false,
};
}
+214 -23
View File
@@ -2,7 +2,7 @@ import { getErrorMessage } from "../../lib/error-message.js";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Loader2, Plus, RefreshCw, Trash2 } from "lucide-react";
import { Loader2, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
import { Button } from "@/components/button";
import { Input } from "@/components/input";
import { Label } from "@/components/label";
@@ -16,7 +16,9 @@ import {
import {
createAiProvider,
deleteAiProvider,
getAiProviderModels,
probeAiModels,
updateAiProvider,
type AiProvider,
type AiProviderType,
} from "@/api/ai-api";
@@ -68,6 +70,159 @@ interface AiProviderSettingsProps {
onAdded?: () => void;
}
function AiProviderEditForm({
provider,
onSaved,
onCancel,
}: {
provider: AiProvider;
onSaved: () => void;
onCancel: () => void;
}) {
const { t } = useTranslation();
const [label, setLabel] = useState(provider.label);
const [defaultModel, setDefaultModel] = useState(provider.defaultModel ?? "");
const [models, setModels] = useState<string[]>([]);
const [customModel, setCustomModel] = useState(false);
const [detecting, setDetecting] = useState(false);
const [saving, setSaving] = useState(false);
const detectModels = useCallback(async () => {
setDetecting(true);
try {
const detected = await getAiProviderModels(provider.id);
setModels(detected);
setCustomModel(!!defaultModel && !detected.includes(defaultModel));
} catch {
setModels([]);
setCustomModel(true);
} finally {
setDetecting(false);
}
}, [provider.id, defaultModel]);
useEffect(() => {
void detectModels();
// The initial model value belongs to this provider. Subsequent edits must
// not trigger a provider model-list request on every keystroke.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [provider.id]);
async function handleSave() {
if (!label.trim()) {
toast.error(t("ai.labelRequired"));
return;
}
setSaving(true);
try {
await updateAiProvider(provider.id, {
label: label.trim(),
defaultModel: defaultModel.trim() || null,
});
toast.success(t("ai.providerUpdated"));
onSaved();
} catch (error) {
toast.error(getErrorMessage(error, t("ai.providerSaveFailed")));
} finally {
setSaving(false);
}
}
return (
<div className="space-y-3 rounded-none border border-border p-3">
<div className="space-y-1.5">
<Label htmlFor={`ai-provider-label-${provider.id}`}>
{t("ai.providerLabel")}
</Label>
<Input
id={`ai-provider-label-${provider.id}`}
className="rounded-none"
value={label}
onChange={(event) => setLabel(event.target.value)}
autoFocus
/>
</div>
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<Label
htmlFor={`ai-provider-model-${provider.id}`}
className="min-w-0 flex-1"
>
{t("ai.defaultModel")}
</Label>
<button
type="button"
className="flex shrink-0 items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground disabled:opacity-50"
onClick={() => void detectModels()}
disabled={detecting}
>
{detecting ? (
<Loader2 size={11} className="animate-spin" />
) : (
<RefreshCw size={11} />
)}
{t("ai.modelRefresh")}
</button>
</div>
{models.length > 0 && !customModel ? (
<Select
value={defaultModel || undefined}
onValueChange={(value) => {
if (value === "__custom__") {
setCustomModel(true);
setDefaultModel("");
return;
}
setDefaultModel(value);
}}
>
<SelectTrigger
id={`ai-provider-model-${provider.id}`}
className="rounded-none"
>
<SelectValue placeholder={t("ai.modelPlaceholder")} />
</SelectTrigger>
<SelectContent>
{models.map((model) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
<SelectItem value="__custom__">{t("ai.modelCustom")}</SelectItem>
</SelectContent>
</Select>
) : (
<Input
id={`ai-provider-model-${provider.id}`}
className="rounded-none"
value={defaultModel}
onChange={(event) => setDefaultModel(event.target.value)}
placeholder={t("ai.defaultModelPlaceholder")}
/>
)}
</div>
<div className="flex gap-2">
<Button size="sm" disabled={saving} onClick={() => void handleSave()}>
{saving && <Loader2 size={14} className="animate-spin" />}
{t("ai.save")}
</Button>
<Button
size="sm"
variant="outline"
disabled={saving}
onClick={onCancel}
>
{t("ai.cancel")}
</Button>
</div>
</div>
);
}
export function AiProviderSettings({
providers,
onChanged,
@@ -75,6 +230,7 @@ export function AiProviderSettings({
}: AiProviderSettingsProps) {
const { t } = useTranslation();
const [adding, setAdding] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const [saving, setSaving] = useState(false);
const [providerType, setProviderType] = useState<AiProviderType>("ollama");
const [label, setLabel] = useState("");
@@ -173,32 +329,67 @@ export function AiProviderSettings({
return (
<div className="space-y-3">
{providers.map((provider) => (
<div
key={provider.id}
className="flex items-center justify-between gap-2 rounded-none border border-border bg-muted px-3 py-2"
>
<div className="min-w-0">
<div className="truncate text-sm font-medium">{provider.label}</div>
<div className="truncate text-xs text-muted-foreground">
{provider.providerType}
{provider.baseUrl ? ` · ${provider.baseUrl}` : ""}
{provider.apiKeyPrefix ? ` · ${provider.apiKeyPrefix}` : ""}
{providers.map((provider) =>
editingId === provider.id ? (
<AiProviderEditForm
key={provider.id}
provider={provider}
onSaved={() => {
setEditingId(null);
onChanged(provider.id);
}}
onCancel={() => setEditingId(null)}
/>
) : (
<div
key={provider.id}
className="flex items-center justify-between gap-2 rounded-none border border-border bg-muted px-3 py-2"
>
<div className="min-w-0">
<div className="truncate text-sm font-medium">
{provider.label}
</div>
<div className="truncate text-xs text-muted-foreground">
{provider.providerType}
{provider.defaultModel ? ` · ${provider.defaultModel}` : ""}
{provider.baseUrl ? ` · ${provider.baseUrl}` : ""}
{provider.apiKeyPrefix ? ` · ${provider.apiKeyPrefix}` : ""}
</div>
</div>
<div className="flex shrink-0 items-center">
<Button
size="sm"
variant="ghost"
onClick={() => {
setAdding(false);
setEditingId(provider.id);
}}
aria-label={t("ai.editProvider")}
>
<Pencil size={14} />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDelete(provider.id)}
aria-label={t("ai.removeProvider")}
>
<Trash2 size={14} />
</Button>
</div>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => handleDelete(provider.id)}
aria-label={t("ai.removeProvider")}
>
<Trash2 size={14} />
</Button>
</div>
))}
),
)}
{!adding && (
<Button size="sm" variant="outline" onClick={() => setAdding(true)}>
<Button
size="sm"
variant="outline"
onClick={() => {
setEditingId(null);
setAdding(true);
}}
>
<Plus size={14} />
{t("ai.addProvider")}
</Button>
+119 -45
View File
@@ -19,6 +19,7 @@ import {
useWindowManager,
} from "./components/WindowManager.tsx";
import { FileWindow } from "./components/FileWindow.tsx";
import { DownloadProgressToast } from "./components/DownloadProgressToast.tsx";
import { DiffWindow } from "./components/DiffWindow.tsx";
import { useDragToDesktop } from "@/features/file-manager/hooks/useDragToDesktop";
import { useDragToSystemDesktop } from "@/features/file-manager/hooks/useDragToSystemDesktop";
@@ -976,19 +977,14 @@ function FileManagerContent({
return () => document.removeEventListener("keydown", handleKeyDown);
}, [currentPath]);
async function handleItemsDropped(items: DataTransferItemList) {
async function handleItemsDropped(entries: FileSystemEntry[]) {
if (!sshSessionId) {
toast.error(t("fileManager.noSSHConnection"));
return;
}
const entries: FileSystemEntry[] = [];
for (let i = 0; i < items.length; i++) {
const entry = items[i].webkitGetAsEntry?.();
if (entry) entries.push(entry);
}
const files: { file: File; relativePath: string }[] = [];
const emptyDirs: string[] = [];
async function readEntry(
entry: FileSystemEntry,
@@ -999,51 +995,77 @@ function FileManagerContent({
(entry as FileSystemFileEntry).file(resolve, reject),
);
files.push({ file, relativePath: path });
} else if (entry.isDirectory) {
const reader = (entry as FileSystemDirectoryEntry).createReader();
let batch: FileSystemEntry[];
do {
batch = await new Promise<FileSystemEntry[]>((resolve, reject) =>
reader.readEntries(resolve, reject),
);
for (const child of batch) {
await readEntry(child, `${path}/${child.name}`);
}
} while (batch.length > 0);
return;
}
if (!entry.isDirectory) return;
// readEntries only hands back a page at a time and signals the end with an
// empty batch, so drain it fully before walking into the children.
const reader = (entry as FileSystemDirectoryEntry).createReader();
const children: FileSystemEntry[] = [];
for (;;) {
const batch = await new Promise<FileSystemEntry[]>((resolve, reject) =>
reader.readEntries(resolve, reject),
);
if (batch.length === 0) break;
children.push(...batch);
}
if (children.length === 0) {
emptyDirs.push(path);
return;
}
for (const child of children) {
await readEntry(child, `${path}/${child.name}`);
}
}
for (const entry of entries) {
await readEntry(entry, entry.name);
try {
for (const entry of entries) {
await readEntry(entry, entry.name);
}
} catch (error) {
toast.error(t("fileManager.failedToUploadFile"));
console.error("Failed to read dropped folder:", error);
return;
}
if (files.length === 0) return;
if (files.length === 0 && emptyDirs.length === 0) return;
const progressToast = toast.loading(
`Uploading ${files.length} file(s)...`,
t("fileManager.uploadingFolderFiles", { count: files.length }),
{ duration: Infinity },
);
const failed: string[] = [];
try {
await ensureSSHConnection();
const base = currentPath.endsWith("/") ? currentPath : currentPath + "/";
const dirs = new Set<string>();
for (const { relativePath } of files) {
for (const relativePath of [
...files.map((f) => f.relativePath),
...emptyDirs.map((d) => `${d}/`),
]) {
const parts = relativePath.split("/");
for (let i = 1; i < parts.length; i++) {
dirs.add(parts.slice(0, i).join("/"));
}
}
const sortedDirs = Array.from(dirs).sort();
// Shallowest first so each parent exists before its children.
const sortedDirs = Array.from(dirs).sort(
(a, b) =>
a.split("/").length - b.split("/").length || a.localeCompare(b),
);
for (const dir of sortedDirs) {
const parentPath = currentPath.endsWith("/")
? currentPath + dir.split("/").slice(0, -1).join("/")
: currentPath + "/" + dir.split("/").slice(0, -1).join("/");
const parentDir = dir.split("/").slice(0, -1).join("/");
const targetPath = parentDir ? `${base}${parentDir}/` : base;
const folderName = dir.split("/").pop()!;
const targetPath = parentPath.endsWith("/")
? parentPath
: parentPath + "/";
try {
await createSSHFolder(
sshSessionId,
@@ -1060,23 +1082,37 @@ function FileManagerContent({
const dirPart = relativePath.includes("/")
? relativePath.substring(0, relativePath.lastIndexOf("/"))
: "";
const uploadPath = dirPart
? (currentPath.endsWith("/") ? currentPath : currentPath + "/") +
dirPart +
"/"
: currentPath;
const uploadPath = dirPart ? `${base}${dirPart}/` : currentPath;
await uploadSSHFile(
sshSessionId,
uploadPath,
file.name,
file,
currentHost?.id,
);
try {
await uploadSSHFile(
sshSessionId,
uploadPath,
file.name,
file,
currentHost?.id,
);
} catch (error) {
failed.push(relativePath);
console.error(`Failed to upload ${relativePath}:`, error);
}
}
toast.dismiss(progressToast);
toast.success(`Uploaded ${files.length} file(s) successfully`);
if (failed.length === 0) {
toast.success(
t("fileManager.uploadedFolderFiles", { count: files.length }),
);
} else if (failed.length === files.length) {
toast.error(t("fileManager.failedToUploadFile"));
} else {
toast.warning(
t("fileManager.uploadedFolderPartial", {
uploaded: files.length - failed.length,
failed: failed.length,
}),
);
}
handleRefreshDirectory();
} catch (error) {
toast.dismiss(progressToast);
@@ -1166,14 +1202,51 @@ function FileManagerContent({
async function handleDownloadFile(file: FileItem) {
if (!sshSessionId) return;
const toastId = `download-${file.path}-${Date.now()}`;
let lastLoaded = 0;
let lastTime = Date.now();
let mbPerSec: number | undefined;
try {
await ensureSSHConnection();
const { downloadSSHFileStream } = await import("@/main-axios.ts");
await downloadSSHFileStream(sshSessionId, file.path);
toast.loading(<DownloadProgressToast fileName={file.name} loaded={0} />, {
id: toastId,
duration: Infinity,
});
await downloadSSHFileStream(
sshSessionId,
file.path,
({ loaded, total }) => {
const now = Date.now();
const deltaMs = now - lastTime;
if (deltaMs > 200) {
const deltaBytes = loaded - lastLoaded;
if (deltaBytes >= 0) {
mbPerSec = (deltaBytes / deltaMs / 1024 / 1024) * 1000;
}
lastLoaded = loaded;
lastTime = now;
}
toast.loading(
<DownloadProgressToast
fileName={file.name}
loaded={loaded}
total={total}
mbPerSec={mbPerSec}
/>,
{ id: toastId, duration: Infinity },
);
},
);
toast.success(
t("fileManager.fileDownloadedSuccessfully", { name: file.name }),
{ id: toastId },
);
} catch (error: unknown) {
const err = error instanceof Error ? error : null;
@@ -1187,9 +1260,10 @@ function FileManagerContent({
ip: currentHost?.ip,
port: currentHost?.port,
}),
{ id: toastId },
);
} else {
toast.error(t("fileManager.failedToDownloadFile"));
toast.error(t("fileManager.failedToDownloadFile"), { id: toastId });
}
console.error("Download failed:", error);
}
@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useRef, useState } from "react";
import {
ArrowUp,
ChevronLeft,
@@ -76,14 +76,20 @@ function Breadcrumb({
<React.Fragment key={i}>
{part === "" && i === 0 ? (
<button
onClick={() => navigateTo("/")}
onClick={(e) => {
e.stopPropagation();
navigateTo("/");
}}
className="hover:text-accent-brand transition-colors"
>
{t("fileManager.root")}
</button>
) : part !== "" ? (
<button
onClick={() => navigateTo(arr.slice(0, i + 1).join("/") || "/")}
onClick={(e) => {
e.stopPropagation();
navigateTo(arr.slice(0, i + 1).join("/") || "/");
}}
className="hover:text-accent-brand transition-colors"
>
{part}
@@ -102,6 +108,83 @@ function Breadcrumb({
);
}
function PathBar({
currentPath,
navigateTo,
t,
className,
}: Pick<FileManagerToolbarProps, "currentPath" | "navigateTo" | "t"> & {
className: string;
}) {
const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(currentPath);
const inputRef = useRef<HTMLInputElement>(null);
const doneRef = useRef(false);
useEffect(() => {
if (!isEditing) return;
doneRef.current = false;
const timer = setTimeout(() => {
inputRef.current?.focus();
inputRef.current?.select();
}, 0);
return () => clearTimeout(timer);
}, [isEditing]);
const commit = (path: string) => {
if (doneRef.current) return;
doneRef.current = true;
setIsEditing(false);
const trimmed = path.trim();
if (trimmed && trimmed !== currentPath) {
navigateTo(trimmed);
}
};
const cancel = () => {
if (doneRef.current) return;
doneRef.current = true;
setIsEditing(false);
};
if (isEditing) {
return (
<div className={className}>
<Folder className="size-3.5 text-accent-brand shrink-0" />
<input
ref={inputRef}
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
commit(value);
} else if (e.key === "Escape") {
e.preventDefault();
cancel();
}
}}
onBlur={() => commit(value)}
className="flex-1 min-w-0 bg-transparent text-xs font-semibold tracking-wide outline-none text-foreground"
/>
</div>
);
}
return (
<div
className={`${className} cursor-text`}
onClick={() => {
setValue(currentPath);
setIsEditing(true);
}}
>
<Breadcrumb currentPath={currentPath} navigateTo={navigateTo} t={t} />
</div>
);
}
export function FileManagerToolbar({
t,
currentPath,
@@ -182,9 +265,12 @@ export function FileManagerToolbar({
</Button>
</div>
<div className="hidden md:flex flex-1 items-center px-3 h-8 bg-muted/50 border border-border rounded-none gap-2 overflow-hidden">
<Breadcrumb currentPath={currentPath} navigateTo={navigateTo} t={t} />
</div>
<PathBar
currentPath={currentPath}
navigateTo={navigateTo}
t={t}
className="hidden md:flex flex-1 items-center px-3 h-8 bg-muted/50 border border-border rounded-none gap-2 overflow-hidden"
/>
<div className="flex items-center gap-2">
{selectedFiles.length > 0 && (
@@ -340,9 +426,12 @@ export function FileManagerToolbar({
</div>
<div className="md:hidden flex items-center px-3 pb-2 gap-2">
<div className="flex-1 flex items-center px-3 h-8 bg-muted/50 border border-border gap-2 overflow-hidden">
<Breadcrumb currentPath={currentPath} navigateTo={navigateTo} t={t} />
</div>
<PathBar
currentPath={currentPath}
navigateTo={navigateTo}
t={t}
className="flex-1 flex items-center px-3 h-8 bg-muted/50 border border-border gap-2 overflow-hidden"
/>
</div>
</div>
);
@@ -0,0 +1,87 @@
import { formatTransferMbPerSec } from "@/main-axios.ts";
import { useTranslation } from "react-i18next";
interface DownloadProgressToastProps {
fileName: string;
loaded: number;
total?: number;
mbPerSec?: number;
}
function formatBytes(bytes: number): string {
if (bytes <= 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
const formattedSize =
size < 10 && unitIndex > 0 ? size.toFixed(1) : Math.round(size).toString();
return `${formattedSize} ${units[unitIndex]}`;
}
function IndeterminateProgressBar() {
return (
<div className="bg-primary/20 relative h-2 w-full overflow-hidden rounded-full">
<div className="bg-primary/60 absolute inset-y-0 left-0 w-1/3 animate-pulse rounded-full" />
</div>
);
}
function DeterminateProgressBar({ value }: { value: number }) {
const clamped = Math.min(100, Math.max(0, value));
return (
<div className="bg-primary/20 relative h-2 w-full overflow-hidden rounded-full">
<div
className="bg-primary h-full rounded-full transition-[width]"
style={{ width: `${clamped}%` }}
/>
</div>
);
}
export function DownloadProgressToast({
fileName,
loaded,
total,
mbPerSec,
}: DownloadProgressToastProps) {
const { t } = useTranslation();
const percent =
total !== undefined && total > 0
? Math.min(100, Math.round((loaded / total) * 100))
: undefined;
const speed = formatTransferMbPerSec(mbPerSec);
return (
<div className="flex w-[min(calc(100vw-5rem),288px)] max-w-full flex-col gap-2 pr-2">
<p className="text-sm font-medium leading-tight truncate">
{t("fileManager.downloadingFile", { name: fileName })}
</p>
{percent === undefined ? (
<IndeterminateProgressBar />
) : (
<DeterminateProgressBar value={percent} />
)}
<div className="flex items-center justify-between gap-3 pr-1 text-xs text-muted-foreground">
<span className="min-w-0 truncate">
{total !== undefined
? t("fileManager.downloadProgressBytes", {
transferred: formatBytes(loaded),
total: formatBytes(total),
})
: formatBytes(loaded)}
</span>
<span
className={`shrink-0 tabular-nums ${speed ? "font-medium text-foreground" : "invisible"}`}
aria-hidden={!speed}
>
{speed || "0 MB/s"}
</span>
</div>
</div>
);
}
@@ -8,7 +8,7 @@ interface DragAndDropState {
interface UseDragAndDropProps {
onFilesDropped: (files: FileList) => void;
onItemsDropped?: (items: DataTransferItemList) => void;
onItemsDropped?: (entries: FileSystemEntry[]) => void;
onError?: (error: string) => void;
maxFileSize?: number;
allowedTypes?: string[];
@@ -119,24 +119,29 @@ export function useDragAndDrop({
e.preventDefault();
e.stopPropagation();
// Read the entries before touching state. Updating state flushes a render
// and the browser clears dataTransfer once the drop handler unwinds, so
// anything read later comes back empty (Firefox is strictest here).
const entries: FileSystemEntry[] = [];
if (onItemsDropped && e.dataTransfer.items?.length > 0) {
for (const item of Array.from(e.dataTransfer.items)) {
const entry = item.webkitGetAsEntry?.();
if (entry) entries.push(entry);
}
}
const files = e.dataTransfer.files;
setState({
isDragging: false,
dragCounter: 0,
draggedFiles: [],
});
if (onItemsDropped && e.dataTransfer.items?.length > 0) {
const hasDirectory = Array.from(e.dataTransfer.items).some(
(item) => item.webkitGetAsEntry?.()?.isDirectory,
);
if (hasDirectory) {
onItemsDropped(e.dataTransfer.items);
return;
}
if (onItemsDropped && entries.some((entry) => entry.isDirectory)) {
onItemsDropped(entries);
return;
}
const files = e.dataTransfer.files;
if (files.length === 0) {
return;
}
+15 -1
View File
@@ -7,6 +7,7 @@ import React, {
useImperativeHandle,
} from "react";
import type Guacamole from "guacamole-common-js";
import { toast } from "sonner";
import {
GuacamoleDisplay,
type GuacamoleDisplayHandle,
@@ -133,7 +134,7 @@ interface GuacamoleAppInnerProps {
hostId: number;
hostConfig: Pick<
SSHHost,
"connectionType" | "domain" | "guacamoleConfig" | "rdpAuthType"
"connectionType" | "domain" | "guacamoleConfig" | "rdpAuthType" | "syncId"
>;
hostName: string;
tabId?: string;
@@ -180,6 +181,16 @@ const GuacamoleAppInner = React.forwardRef<
setFileBrowserOpen(true);
}, []);
const handleDropUnavailable = useCallback(() => {
toast.error(
t(
allowUpload
? "guacamole.files.driveUnavailable"
: "guacamole.files.uploadDisabled",
),
);
}, [allowUpload, t]);
const resolvedProtocolForConnect = (protocol ??
hostConfig.connectionType ??
"rdp") as "rdp" | "vnc" | "telnet";
@@ -245,6 +256,7 @@ const GuacamoleAppInner = React.forwardRef<
hostId,
protocol,
promptedCredentials ?? undefined,
hostConfig.syncId,
);
if (result) {
setToken(result.token);
@@ -257,6 +269,7 @@ const GuacamoleAppInner = React.forwardRef<
protocol,
promptedCredentials,
resolvedProtocolForConnect,
hostConfig.syncId,
addLog,
t,
]);
@@ -461,6 +474,7 @@ const GuacamoleAppInner = React.forwardRef<
}
onFilesystem={setFilesystem}
onDropFiles={handleDropFiles}
onDropUnavailable={handleDropUnavailable}
/>
{filesystem && fileBrowserOpen && (
<GuacamoleFileBrowser
+20 -5
View File
@@ -25,6 +25,10 @@ import {
} from "./guacamole-clipboard.ts";
import { getGuacamoleDisplaySize } from "./guacamole-display-size.ts";
import { bindPointerInput } from "./guacamole-pointer.ts";
import {
getFileDropDisposition,
hasDraggedFiles,
} from "./guacamole-file-drop.ts";
import { guacStateToStage } from "@/components/connection/connection-status.ts";
import type { ConnectionStage } from "@/types/connection-log.ts";
@@ -66,6 +70,7 @@ interface GuacamoleDisplayProps {
onError?: (error: string) => void;
onFilesystem?: (filesystem: Guacamole.Object | null) => void;
onDropFiles?: (files: File[]) => void;
onDropUnavailable?: () => void;
onStageChange?: (stage: ConnectionStage) => void;
}
@@ -85,6 +90,7 @@ export const GuacamoleDisplay = forwardRef<
onError,
onFilesystem,
onDropFiles,
onDropUnavailable,
onStageChange,
},
ref,
@@ -776,8 +782,9 @@ export const GuacamoleDisplay = forwardRef<
const handleDragEnter = useCallback(
(event: React.DragEvent) => {
if (!canDropFiles || !event.dataTransfer.types.includes("Files")) return;
if (!hasDraggedFiles(event.dataTransfer.types)) return;
event.preventDefault();
if (!canDropFiles) return;
dragDepthRef.current += 1;
setIsDraggingFiles(true);
},
@@ -791,15 +798,21 @@ export const GuacamoleDisplay = forwardRef<
const handleDrop = useCallback(
(event: React.DragEvent) => {
if (!canDropFiles) return;
if (!hasDraggedFiles(event.dataTransfer.types)) return;
event.preventDefault();
dragDepthRef.current = 0;
setIsDraggingFiles(false);
const files = Array.from(event.dataTransfer.files);
if (files.length > 0) onDropFiles?.(files);
const disposition = getFileDropDisposition(
event.dataTransfer.types,
files.length,
canDropFiles,
);
if (disposition === "upload") onDropFiles?.(files);
if (disposition === "reject") onDropUnavailable?.();
},
[canDropFiles, onDropFiles],
[canDropFiles, onDropFiles, onDropUnavailable],
);
return (
@@ -808,7 +821,9 @@ export const GuacamoleDisplay = forwardRef<
className="absolute inset-0 overflow-hidden"
style={{ backgroundColor: "var(--bg-base)" }}
onDragEnter={handleDragEnter}
onDragOver={canDropFiles ? (e) => e.preventDefault() : undefined}
onDragOver={(event) => {
if (hasDraggedFiles(event.dataTransfer.types)) event.preventDefault();
}}
onDragLeave={canDropFiles ? handleDragLeave : undefined}
onDrop={handleDrop}
>
@@ -0,0 +1,14 @@
export type FileDropDisposition = "ignore" | "reject" | "upload";
export function hasDraggedFiles(types: readonly string[]): boolean {
return types.includes("Files");
}
export function getFileDropDisposition(
types: readonly string[],
fileCount: number,
canUpload: boolean,
): FileDropDisposition {
if (!hasDraggedFiles(types) || fileCount === 0) return "ignore";
return canUpload ? "upload" : "reject";
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { FitAddon } from "@xterm/addon-fit";
import { useXTerm } from "react-xtermjs";
import { useTheme } from "@/components/theme-provider";
@@ -17,6 +17,14 @@ export function LocalTerminal({
const { instance: terminal, ref: xtermRef } = useXTerm();
const fitAddonRef = useRef<FitAddon | null>(null);
const sessionIdRef = useRef<string | null>(null);
const [isWindows, setIsWindows] = useState(false);
const [shell, setShell] = useState<"default" | "wsl">("default");
useEffect(() => {
window.electronAPI?.getPlatform().then((platform) => {
setIsWindows(platform === "win32");
});
}, []);
const fit = useCallback(() => {
const fitAddon = fitAddonRef.current;
@@ -60,7 +68,7 @@ export function LocalTerminal({
});
window.electronAPI
.startLocalTerminal({ cols: terminal.cols, rows: terminal.rows })
.startLocalTerminal({ cols: terminal.cols, rows: terminal.rows, shell })
.then(({ sessionId }) => {
if (disposed) {
window.electronAPI.closeLocalTerminal(sessionId);
@@ -100,11 +108,30 @@ export function LocalTerminal({
fitAddonRef.current = null;
fitAddon.dispose();
};
}, [fit, instanceId, terminal, xtermRef]);
}, [fit, instanceId, shell, terminal, xtermRef]);
useEffect(() => {
if (isVisible) requestAnimationFrame(fit);
}, [fit, isVisible]);
return <div ref={xtermRef} className="h-full w-full bg-background p-2" />;
return (
<div className="flex h-full w-full flex-col bg-background">
{isWindows && (
<div className="flex justify-end border-b border-border px-2 py-1">
<select
aria-label="Local terminal shell"
className="rounded border border-border bg-background px-2 py-1 text-xs text-foreground"
value={shell}
onChange={(event) =>
setShell(event.target.value === "wsl" ? "wsl" : "default")
}
>
<option value="default">PowerShell</option>
<option value="wsl">WSL</option>
</select>
</div>
)}
<div ref={xtermRef} className="min-h-0 flex-1 p-2" />
</div>
);
}
+15 -12
View File
@@ -80,7 +80,7 @@ import {
getNextTerminalFontSize,
getTerminalFontZoomDirection,
} from "./terminal-font-zoom.ts";
import { isTabKeyEvent } from "./terminal-key-event.ts";
import { isPhysicalShortcutKey, isTabKeyEvent } from "./terminal-key-event.ts";
import { installTouchWheelCoordinator } from "./touch-wheel-coordinator.ts";
import { loadTouchInputSettings } from "./touch-input-settings-store.ts";
import { quoteTerminalImagePath } from "./terminal-image-path.ts";
@@ -2869,7 +2869,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
!e.shiftKey &&
!e.altKey &&
!e.metaKey &&
e.key.toLowerCase() === "c" &&
isPhysicalShortcutKey(e, "KeyC", "c") &&
terminal.hasSelection()
) {
const selection = terminal.getSelection();
@@ -2883,13 +2883,16 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
}
if (
((e.metaKey && !e.shiftKey && !e.ctrlKey && !e.altKey) ||
(e.ctrlKey &&
!e.shiftKey &&
!e.altKey &&
!e.metaKey &&
e.key === "Insert")) &&
(e.key.toLowerCase() === "c" || e.key === "Insert")
(e.metaKey &&
!e.shiftKey &&
!e.ctrlKey &&
!e.altKey &&
isPhysicalShortcutKey(e, "KeyC", "c")) ||
(e.ctrlKey &&
!e.shiftKey &&
!e.altKey &&
!e.metaKey &&
e.key === "Insert")
) {
const selection = terminal.getSelection();
if (selection) {
@@ -2905,7 +2908,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
e.shiftKey &&
!e.altKey &&
!e.metaKey &&
e.key.toLowerCase() === "c"
isPhysicalShortcutKey(e, "KeyC", "c")
) {
const selection = terminal.getSelection();
if (selection) {
@@ -2922,7 +2925,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
e.shiftKey &&
!e.altKey &&
!e.metaKey &&
e.key.toLowerCase() === "v"
isPhysicalShortcutKey(e, "KeyV", "v")
) {
e.preventDefault();
e.stopPropagation();
@@ -2937,7 +2940,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
!e.shiftKey &&
!e.altKey &&
!e.metaKey &&
e.key.toLowerCase() === "v"
isPhysicalShortcutKey(e, "KeyV", "v")
) {
// Let the browser handle Ctrl+V natively, the paste event
// listener will intercept the result without triggering the
@@ -1,3 +1,13 @@
export function isTabKeyEvent(event: KeyboardEvent): boolean {
return event.key === "Tab" || event.code === "Tab" || event.keyCode === 9;
}
export function isPhysicalShortcutKey(
event: Pick<KeyboardEvent, "code" | "key">,
code: string,
fallbackKey: string,
): boolean {
return event.code
? event.code === code
: event.key.toLowerCase() === fallbackKey;
}
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH Key",
"filterTagsGroup": "Tags",
"copyDeployCommand": "Copy deploy command",
"cloneCredentialAction": "Clone credential",
"clonedCredential": "Cloned {{name}}",
"clonedCredentialName": "{{name}} (copy)",
"deleteCredentialAction": "Delete credential",
"editCredentialAction": "Edit credential",
"failedToCloneCredential": "Failed to clone credential",
"failedToDeleteCredential": "Failed to delete credential",
"idBadge": "ID",
"keyBadge": "KEY",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Agent Socket Path",
"agentSocketPathPlaceholder": "Leave empty to use SSH_AUTH_SOCK",
"agentSocketPathHint": "Leave empty to auto-detect from the SSH_AUTH_SOCK environment variable, or enter a custom socket path (e.g. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Agent Identity (Public Key)",
"agentIdentityPlaceholder": "Leave empty to try all keys from the agent",
"agentIdentityHint": "Paste a public key (e.g. ssh-ed25519 AAAA...) to only offer that key from the agent. Recommended when the agent holds many keys, to avoid exceeding the server's MaxAuthTries.",
"shareSshAuthLabel": "Share SSH Authentication",
"shareSshAuthDesc": "Give recipients encrypted copies of this host's SSH authentication. A recipient's personal credential still takes precedence.",
"tailscaleDeviceSelect": "Select Tailscale device",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "No credentials found",
"addCredentialBtn": "Add Credential",
"updateCredentialBtn": "Update Credential",
"editCredentialAction": "Edit credential",
"backToHost": "Back to Host",
"saveCredentialAsNewBtn": "Save as New",
"saveCredentialAsNewDesc": "Creates a new credential with these changes and assigns it to this host. The original credential is left unchanged.",
"credentialSavedAsNew": "Saved as a new credential",
"failedToSaveCredentialAsNew": "Failed to save as a new credential",
"features": "Features",
"noFolder": "(No folder)",
"deleteSelected": "Delete",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Compact",
"displayShowTags": "Show tags",
"displayTrayOnClick": "Open actions on click (instead of hover)",
"actionsVisibility": "Host actions",
"actionsVisibilityDesc": "When the connection and management buttons on each host show up",
"actionsVisibility": "Host action display",
"actionsVisibilityDesc": "Choose when host actions expand. Select click to restore the former click-to-expand behavior.",
"actionsAlways": "Always visible",
"actionsOnly": "Connect only, rest on click",
"actionsHover": "Show on hover",
"actionsClick": "Show on click",
"actionsClick": "Click to expand/collapse",
"openOnDoubleClick": "Open on double click",
"openOnDoubleClickDesc": "Require a double click to open a host session instead of a single click",
"deletedHost": "Deleted {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Uploaded {{name}}",
"uploadFailed": "Failed to upload {{name}}",
"uploadDisabled": "Uploads are disabled for this connection",
"driveUnavailable": "Enable RDP drive redirection before uploading files",
"dropToUpload": "Drop files to upload"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Folder Name",
"fileUploadedSuccessfully": "File \"{{name}}\" uploaded successfully",
"failedToUploadFile": "Failed to upload file",
"uploadingFolderFiles": "Uploading {{count}} file(s)...",
"uploadedFolderFiles": "Uploaded {{count}} file(s) successfully",
"uploadedFolderPartial": "Uploaded {{uploaded}} file(s), {{failed}} failed",
"fileDownloadedSuccessfully": "File \"{{name}}\" downloaded successfully",
"failedToDownloadFile": "Failed to download file",
"fileCreatedSuccessfully": "File \"{{name}}\" created successfully",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} items",
"downloadFileSuccess": "File {{name}} downloaded successfully",
"downloadFileFailed": "Download failed",
"downloadingFile": "Downloading {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Move to {{name}}",
"diffCompareWith": "Diff compare with {{name}}",
"dragOutsideToDownload": "Drag outside window to download ({{count}} files)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Auto-create accounts for OIDC/SSO users on first login (independent of the registration toggle)",
"oidcSilentLoginDefault": "Silent OIDC Login by Default",
"oidcSilentLoginDefaultDesc": "Automatically redirect to OIDC login on every visit, skipping the login form entirely",
"oidcSilentLoginDefaultLockedDesc": "This setting is locked by the OIDC_SILENT_LOGIN_DEFAULT environment variable and cannot be changed here.",
"allowPasswordReset": "Allow Password Reset",
"allowPasswordResetDesc": "Reset code via Docker logs",
"commandHistoryEnabled": "Command History",
@@ -3317,11 +3337,11 @@
"createRole": "Create",
"creating": "Creating...",
"exportDatabase": "Export Database",
"exportDatabaseDesc": "Download a backup of all hosts, credentials, and settings",
"exportDatabaseDesc": "Download a portable SQLite backup of all hosts, credentials, and settings",
"export": "Export",
"exporting": "Exporting...",
"importDatabase": "Import Database",
"importDatabaseDesc": "Restore from a .sqlite backup file",
"importDatabaseDesc": "Restore from a portable .sqlite backup on any database backend",
"importDatabaseSelected": "Selected: {{name}}",
"selectFile": "Select File",
"changeFile": "Change",
@@ -4516,6 +4536,7 @@
"selectProvider": "Select a provider",
"providerSettings": "Provider settings",
"addProvider": "Add provider",
"editProvider": "Edit provider",
"removeProvider": "Remove provider",
"providerType": "Provider",
"providerLabel": "Name",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "For example: llama3.1 or claude-opus-5",
"labelRequired": "Give this provider a name",
"providerSaveFailed": "Could not save the provider",
"providerUpdated": "Provider updated",
"providerDeleteFailed": "Could not remove the provider",
"privateEndpointHint": "Addresses on your private network need an administrator to allow the host first.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH-sleutel",
"filterTagsGroup": "Etikette",
"copyDeployCommand": "Kopieer ontplooi-opdrag",
"cloneCredentialAction": "Kloonbewys",
"clonedCredential": "Gekloon {{name}}",
"clonedCredentialName": "{{name}} (kopieer)",
"deleteCredentialAction": "Vee geloofsbriewe uit",
"editCredentialAction": "Wysig geloofsbriewe",
"failedToCloneCredential": "Kon nie geloofsbriewe kloon nie",
"failedToDeleteCredential": "Kon nie geloofsbriewe verwyder nie",
"idBadge": "ID",
"keyBadge": "SLEUTEL",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Agent-sokpad",
"agentSocketPathPlaceholder": "Los leeg om SSH_AUTH_SOCK te gebruik",
"agentSocketPathHint": "Los leeg om outomaties op te spoor vanaf die SSH_AUTH_SOCK omgewing veranderlike, of voer 'n persoonlike sokpad in (bv. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Agentidentiteit (Publieke Sleutel)",
"agentIdentityPlaceholder": "Los leeg om alle sleutels van die agent te probeer",
"agentIdentityHint": "Plak 'n publieke sleutel (bv. ssh-ed25519 AAAA...) om slegs daardie sleutel van die agent aan te bied. Aanbeveel wanneer die agent baie sleutels besit, om te verhoed dat die bediener se MaxAuthTries oorskry word.",
"shareSshAuthLabel": "Deel SSH-verifikasie",
"shareSshAuthDesc": "Gee ontvangers geïnkripteerde kopieë van hierdie gasheer se SSH-verifikasie. 'n Ontvanger se persoonlike geloofsbriewe geniet steeds voorrang.",
"tailscaleDeviceSelect": "Kies Tailscale-toestel",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Geen geloofsbriewe gevind nie",
"addCredentialBtn": "Voeg geloofsbriewe by",
"updateCredentialBtn": "Opdatering van geloofsbriewe",
"editCredentialAction": "Wysig geloofsbriewe",
"backToHost": "Terug na gasheer",
"saveCredentialAsNewBtn": "Stoor as Nuut",
"saveCredentialAsNewDesc": "Skep 'n nuwe geloofsbrief met hierdie veranderinge en ken dit aan hierdie gasheer toe. Die oorspronklike geloofsbrief word onveranderd gelaat.",
"credentialSavedAsNew": "Gestoor as 'n nuwe geloofsbrief",
"failedToSaveCredentialAsNew": "Kon nie as 'n nuwe geloofsbrief stoor nie",
"features": "Kenmerke",
"noFolder": "(Geen vouer nie)",
"deleteSelected": "Vee uit",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Kompak",
"displayShowTags": "Wys etikette",
"displayTrayOnClick": "Maak aksies oop met klik (in plaas van om oor te beweeg)",
"actionsVisibility": "Gasheeraksies",
"actionsVisibilityDesc": "Wanneer die verbindings- en bestuursknoppies op elke gasheer verskyn",
"actionsVisibility": "Gasheeraksie-vertoning",
"actionsVisibilityDesc": "Kies wanneer gasheeraksies uitbrei. Kies klik om die vorige klik-om-uit te brei-gedrag te herstel.",
"actionsAlways": "Altyd sigbaar",
"actionsOnly": "Verbind slegs, rus op klik",
"actionsHover": "Wys wanneer jy beweeg",
"actionsClick": "Wys met klik",
"actionsClick": "Klik om uit te brei/in te vou",
"openOnDoubleClick": "Maak oop met dubbelklik",
"openOnDoubleClickDesc": "Vereis 'n dubbelklik om 'n gasheersessie oop te maak in plaas van 'n enkele klik",
"deletedHost": "Verwyder {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Opgelaai {{name}}",
"uploadFailed": "Kon nie {{name}} oplaai nie",
"uploadDisabled": "Oplaaie is gedeaktiveer vir hierdie verbinding",
"driveUnavailable": "Aktiveer RDP-skyfherleiding voordat lêers opgelaai word",
"dropToUpload": "Laat lêers los om op te laai"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Vouernaam",
"fileUploadedSuccessfully": "Lêer \"{{name}}\" is suksesvol opgelaai",
"failedToUploadFile": "Kon nie lêer oplaai nie",
"uploadingFolderFiles": "Laai tans {{count}} lêer(s) op...",
"uploadedFolderFiles": "{{count}} lêer(s) suksesvol opgelaai",
"uploadedFolderPartial": "Opgelaaide {{uploaded}} lêer(s), {{failed}} het misluk",
"fileDownloadedSuccessfully": "Lêer \"{{name}}\" is suksesvol afgelaai",
"failedToDownloadFile": "Kon nie lêer aflaai nie",
"fileCreatedSuccessfully": "Lêer \"{{name}}\" suksesvol geskep",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} items",
"downloadFileSuccess": "Lêer {{name}} suksesvol afgelaai",
"downloadFileFailed": "Aflaai het misluk",
"downloadingFile": "Laai tans af {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Skuif na {{name}}",
"diffCompareWith": "Verskil vergelyking met {{name}}",
"dragOutsideToDownload": "Sleep buite die venster om af te laai ({{count}} lêers)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Skep outomaties rekeninge vir OIDC/SSO-gebruikers met die eerste aanmelding (onafhanklik van die registrasie-skakelaar)",
"oidcSilentLoginDefault": "Stille OIDC-aanmelding by verstek",
"oidcSilentLoginDefaultDesc": "Herlei outomaties na OIDC-aanmelding met elke besoek, en slaan die aanmeldvorm heeltemal oor",
"oidcSilentLoginDefaultLockedDesc": "Hierdie instelling word gesluit deur die OIDC_SILENT_LOGIN_DEFAULT omgewingveranderlike en kan nie hier verander word nie.",
"allowPasswordReset": "Laat wagwoordherstel toe",
"allowPasswordResetDesc": "Herstel kode via Docker-logboeke",
"commandHistoryEnabled": "Opdraggeskiedenis",
@@ -3317,11 +3337,11 @@
"createRole": "Skep",
"creating": "Skep...",
"exportDatabase": "Uitvoer van databasis",
"exportDatabaseDesc": "Laai 'n rugsteun van alle gashere, geloofsbriewe en instellings af",
"exportDatabaseDesc": "Laai 'n draagbare SQLite-rugsteun van alle gashere, geloofsbriewe en instellings af",
"export": "Uitvoer",
"exporting": "Uitvoer tans...",
"importDatabase": "Voer databasis in",
"importDatabaseDesc": "Herstel vanaf 'n .sqlite rugsteunlêer",
"importDatabaseDesc": "Herstel vanaf 'n draagbare .sqlite-rugsteun op enige databasis-agtergrond",
"importDatabaseSelected": "Gekies: {{name}}",
"selectFile": "Kies Lêer",
"changeFile": "Verandering",
@@ -4516,6 +4536,7 @@
"selectProvider": "Kies 'n verskaffer",
"providerSettings": "Verskafferinstellings",
"addProvider": "Voeg verskaffer by",
"editProvider": "Wysig verskaffer",
"removeProvider": "Verwyder verskaffer",
"providerType": "Verskaffer",
"providerLabel": "Naam",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Byvoorbeeld: llama3.1 of claude-opus-5",
"labelRequired": "Gee hierdie verskaffer 'n naam",
"providerSaveFailed": "Kon nie die verskaffer stoor nie",
"providerUpdated": "Verskaffer opgedateer",
"providerDeleteFailed": "Kon nie die verskaffer verwyder nie",
"privateEndpointHint": "Adresse op jou private netwerk benodig eers 'n administrateur om die gasheer toe te laat.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "مفتاح SSH",
"filterTagsGroup": "وسوم",
"copyDeployCommand": "نسخ أمر النشر",
"cloneCredentialAction": "بيانات اعتماد الاستنساخ",
"clonedCredential": "مستنسخ {{name}}",
"clonedCredentialName": "{{name}} (نسخة)",
"deleteCredentialAction": "حذف بيانات الاعتماد",
"editCredentialAction": "تعديل بيانات الاعتماد",
"failedToCloneCredential": "فشل استنساخ بيانات الاعتماد",
"failedToDeleteCredential": "فشل حذف بيانات الاعتماد",
"idBadge": "بطاقة تعريف",
"keyBadge": "مفتاح",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "مسار مقبس الوكيل",
"agentSocketPathPlaceholder": "اتركه فارغًا لاستخدام SSH_AUTH_SOCK",
"agentSocketPathHint": "اتركه فارغًا للاكتشاف التلقائي من متغير البيئة SSH_AUTH_SOCK، أو أدخل مسار مقبس مخصص (مثال: /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "هوية الوكيل (المفتاح العام)",
"agentIdentityPlaceholder": "اترك هذا الحقل فارغًا لتجربة جميع المفاتيح من الوكيل.",
"agentIdentityHint": "ألصق مفتاحًا عامًا (مثل ssh-ed25519 AAAA...) لعرض هذا المفتاح فقط من جانب الوكيل. يُنصح بذلك عندما يمتلك الوكيل العديد من المفاتيح، لتجنب تجاوز الحد الأقصى لمحاولات المصادقة (MaxAuthTries) للخادم.",
"shareSshAuthLabel": "مشاركة مصادقة SSH",
"shareSshAuthDesc": "قم بتزويد المستلمين بنسخ مشفرة من بيانات مصادقة SSH الخاصة بهذا المضيف. مع ذلك، تظل بيانات اعتماد المستلم الشخصية هي المرجع الأساسي.",
"tailscaleDeviceSelect": "اختيار جهاز Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "لم يتم العثور على بيانات اعتماد",
"addCredentialBtn": "إضافة بيانات اعتماد",
"updateCredentialBtn": "تحديث بيانات اعتماد",
"editCredentialAction": "تعديل بيانات الاعتماد",
"backToHost": "العودة إلى المضيف",
"saveCredentialAsNewBtn": "حفظ كجديد",
"saveCredentialAsNewDesc": "يُنشئ بيانات اعتماد جديدة تتضمن هذه التغييرات ويُخصصها لهذا المضيف. وتبقى بيانات الاعتماد الأصلية دون تغيير.",
"credentialSavedAsNew": "تم حفظها كبيانات اعتماد جديدة",
"failedToSaveCredentialAsNew": "فشل الحفظ كبيانات اعتماد جديدة",
"features": "الميزات",
"noFolder": "(بلا مجلد)",
"deleteSelected": "حذف",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "صغير الحجم",
"displayShowTags": "عرض العلامات",
"displayTrayOnClick": "فتح الإجراءات عند النقر (بدلاً من التمرير)",
"actionsVisibility": "إجراءات المضيف",
"actionsVisibilityDesc": "عندما تظهر أزرار الاتصال والإدارة على كل مضيف",
"actionsVisibility": "عرض إجراءات المضيف",
"actionsVisibilityDesc": "اختر متى تتوسع إجراءات المضيف. حدد \"انقر\" لاستعادة سلوك النقر للتوسيع السابق.",
"actionsAlways": "مرئي دائمًا",
"actionsOnly": "الاتصال فقط، والباقي عند النقر",
"actionsHover": "عرض عند تمرير المؤشر",
"actionsClick": "يظهر عند النقر",
"actionsClick": "انقر للتوسيع/الطي",
"openOnDoubleClick": "افتح بالنقر المزدوج",
"openOnDoubleClickDesc": "يتطلب فتح جلسة المضيف نقرتين بدلاً من نقرة واحدة",
"deletedHost": "تم حذف {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "تم التحميل {{name}}",
"uploadFailed": "فشل تحميل {{name}}",
"uploadDisabled": "تم تعطيل التحميلات لهذا الاتصال",
"driveUnavailable": "قم بتمكين إعادة توجيه محرك الأقراص RDP قبل تحميل الملفات",
"dropToUpload": "قم بإسقاط الملفات لتحميلها"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "اسم المجلد",
"fileUploadedSuccessfully": "تم رفع الملف \"{{name}}\" بنجاح",
"failedToUploadFile": "فشل رفع الملف",
"uploadingFolderFiles": "جارٍ تحميل {{count}} ملف(ملفات)...",
"uploadedFolderFiles": "تم تحميل {{count}} ملف بنجاح",
"uploadedFolderPartial": "تم تحميل {{uploaded}} ملفًا، {{failed}} فشل",
"fileDownloadedSuccessfully": "تم تنزيل الملف \"{{name}}\" بنجاح",
"failedToDownloadFile": "فشل تنزيل الملف",
"fileCreatedSuccessfully": "تم إنشاء الملف \"{{name}}\" بنجاح",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} عناصر",
"downloadFileSuccess": "تم تنزيل الملف {{name}} بنجاح",
"downloadFileFailed": "فشل التنزيل",
"downloadingFile": "جارٍ التنزيل {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "نقل إلى {{name}}",
"diffCompareWith": "مقارنة الاختلافات مع {{name}}",
"dragOutsideToDownload": "اسحب خارج النافذة للتنزيل ({{count}} ملفات)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "إنشاء الحسابات تلقائياً لمستخدمي OIDC/SSO عند تسجيل الدخول الأول (بغض النظر عن مفتاح التسجيل)",
"oidcSilentLoginDefault": "تسجيل الدخول الصامت لـ OIDC بشكل افتراضي",
"oidcSilentLoginDefaultDesc": "إعادة التوجيه تلقائياً إلى تسجيل الدخول OIDC في كل زيارة، وتخطي نموذج تسجيل الدخول بالكامل",
"oidcSilentLoginDefaultLockedDesc": "هذا الإعداد مقفل بواسطة متغير البيئة OIDC_SILENT_LOGIN_DEFAULT ولا يمكن تغييره هنا.",
"allowPasswordReset": "السماح بإعادة تعيين كلمة المرور",
"allowPasswordResetDesc": "رمز إعادة التعيين عبر سجلات Docker",
"commandHistoryEnabled": "سجل الأوامر",
@@ -3317,11 +3337,11 @@
"createRole": "إنشاء",
"creating": "جارٍ الإنشاء...",
"exportDatabase": "تصدير قاعدة البيانات",
"exportDatabaseDesc": "تنزيل نسخة احتياطية لجميع المضيفين وبيانات الاعتماد والإعدادات",
"exportDatabaseDesc": "قم بتنزيل نسخة احتياطية محمولة من SQLite لجميع المضيفين وبيانات الاعتماد والإعدادات",
"export": "تصدير",
"exporting": "جارٍ التصدير...",
"importDatabase": "استيراد قاعدة البيانات",
"importDatabaseDesc": "استعادة من ملف نسخ احتياطي بصيغة .sqlite",
"importDatabaseDesc": "استعادة البيانات من نسخة احتياطية محمولة بصيغة .sqlite على أي نظام قاعدة بيانات.",
"importDatabaseSelected": "المحدد: {{name}}",
"selectFile": "اختيار ملف",
"changeFile": "تغيير",
@@ -4516,6 +4536,7 @@
"selectProvider": "اختر مزود الخدمة",
"providerSettings": "إعدادات المزوّد",
"addProvider": "إضافة موفر",
"editProvider": "موفر التحرير",
"removeProvider": "إزالة الموفر",
"providerType": "مزود",
"providerLabel": "اسم",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "على سبيل المثال: llama3.1 أو claude-opus-5",
"labelRequired": "أعطِ هذا المزوّد اسمًا",
"providerSaveFailed": "تعذر حفظ بيانات المزوّد",
"providerUpdated": "تم تحديث المزوّد",
"providerDeleteFailed": "تعذر إزالة الموفر",
"privateEndpointHint": "تحتاج العناوين الموجودة على شبكتك الخاصة إلى موافقة المسؤول أولاً للسماح للمضيف بالوصول إليها.",
"providerOllama": "أولاما",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH ключ",
"filterTagsGroup": "Етикети",
"copyDeployCommand": "Копиране на командата за разгръщане",
"cloneCredentialAction": "Клониране на идентификационни данни",
"clonedCredential": "Клонирано {{name}}",
"clonedCredentialName": "{{name}} (копие)",
"deleteCredentialAction": "Изтриване на идентификационни данни",
"editCredentialAction": "Редактиране на идентификационни данни",
"failedToCloneCredential": "Клонирането на идентификационните данни не бе успешно",
"failedToDeleteCredential": "Неуспешно изтриване на идентификационните данни",
"idBadge": "Идентификационен номер",
"keyBadge": "КЛЮЧ",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Път на сокета на агента",
"agentSocketPathPlaceholder": "Оставете празно, за да използвате SSH_AUTH_SOCK",
"agentSocketPathHint": "Оставете празно, за да се открие автоматично от променливата на средата SSH_AUTH_SOCK, или въведете персонализиран път до сокета (напр. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Идентичност на агента (публичен ключ)",
"agentIdentityPlaceholder": "Оставете празно, за да изпробвате всички ключове от агента",
"agentIdentityHint": "Поставете публичен ключ (напр. ssh-ed25519 AAAA...), за да предложите само този ключ от агента. Препоръчва се, когато агентът притежава много ключове, за да се избегне превишаване на MaxAuthTries на сървъра.",
"shareSshAuthLabel": "Споделяне на SSH удостоверяване",
"shareSshAuthDesc": "Дайте на получателите криптирани копия на SSH удостоверяването на този хост. Личните идентификационни данни на получателя все още имат приоритет.",
"tailscaleDeviceSelect": "Изберете устройство за Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Не са намерени идентификационни данни",
"addCredentialBtn": "Добавяне на идентификационни данни",
"updateCredentialBtn": "Актуализиране на идентификационните данни",
"editCredentialAction": "Редактиране на идентификационни данни",
"backToHost": "Обратно към хоста",
"saveCredentialAsNewBtn": "Запазване като ново",
"saveCredentialAsNewDesc": "Създава нови идентификационни данни с тези промени и ги присвоява на този хост. Оригиналните идентификационни данни се оставят непроменени.",
"credentialSavedAsNew": "Запазено като нови идентификационни данни",
"failedToSaveCredentialAsNew": "Запазването като нови идентификационни данни не бе успешно",
"features": "Характеристики",
"noFolder": "(Няма папка)",
"deleteSelected": "Изтриване",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Компактен",
"displayShowTags": "Показване на етикети",
"displayTrayOnClick": "Отваряне на действия при щракване (вместо при задържане на курсора на мишката)",
"actionsVisibility": "Действия на хоста",
"actionsVisibilityDesc": "Когато се появят бутоните за връзка и управление на всеки хост",
"actionsVisibility": "Показване на действието на хоста",
"actionsVisibilityDesc": "Изберете кога действията на хоста да се разгъват. Изберете „щракване“, за да възстановите предишното поведение „щракване за разгъване“.",
"actionsAlways": "Винаги видим",
"actionsOnly": "Само свързване, почивка при щракване",
"actionsHover": "Показване при задържане на курсора на мишката",
"actionsClick": "Показване при кликване",
"actionsClick": "Кликнете, за да разгънете/свиете",
"openOnDoubleClick": "Отваряне с двойно щракване",
"openOnDoubleClickDesc": "Изисква се двойно щракване за отваряне на хост сесия вместо едно щракване",
"deletedHost": "Изтрито {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Качено {{name}}",
"uploadFailed": "Качването не бе успешно {{name}}",
"uploadDisabled": "Качванията са деактивирани за тази връзка",
"driveUnavailable": "Активирайте пренасочването на RDP устройството, преди да качвате файлове",
"dropToUpload": "Пуснете файловете, за да ги качите"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Име на папката",
"fileUploadedSuccessfully": "Файлът „{{name}}“ е качен успешно",
"failedToUploadFile": "Качването на файла не бе успешно",
"uploadingFolderFiles": "Качване на {{count}} файл(а)...",
"uploadedFolderFiles": "Качени {{count}} файла(а) успешно",
"uploadedFolderPartial": "Качени {{uploaded}} файла(а), {{failed}} неуспешно",
"fileDownloadedSuccessfully": "Файлът „{{name}}“ е изтеглен успешно",
"failedToDownloadFile": "Изтеглянето на файла не бе успешно",
"fileCreatedSuccessfully": "Файлът „{{name}}“ е създаден успешно",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} артикули",
"downloadFileSuccess": "Файлът {{name}} е изтеглен успешно",
"downloadFileFailed": "Изтеглянето не бе успешно",
"downloadingFile": "Изтегляне {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Премести се в {{name}}",
"diffCompareWith": "Разлика в сравнение с {{name}}",
"dragOutsideToDownload": "Плъзнете извън прозореца, за да изтеглите ({{count}} файлове)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Автоматично създаване на акаунти за потребители на OIDC/SSO при първо влизане (независимо от превключвателя за регистрация)",
"oidcSilentLoginDefault": "Тихо влизане в OIDC по подразбиране",
"oidcSilentLoginDefaultDesc": "Автоматично пренасочване към вход в OIDC при всяко посещение, като се пропуска изцяло формуляра за вход",
"oidcSilentLoginDefaultLockedDesc": "Тази настройка е заключена от променливата на средата OIDC_SILENT_LOGIN_DEFAULT и не може да бъде променена тук.",
"allowPasswordReset": "Разрешаване на нулиране на паролата",
"allowPasswordResetDesc": "Нулиране на кода чрез Docker логове",
"commandHistoryEnabled": "История на командите",
@@ -3317,11 +3337,11 @@
"createRole": "Създаване",
"creating": "Създаване...",
"exportDatabase": "Експортиране на база данни",
"exportDatabaseDesc": "Изтеглете резервно копие на всички хостове, идентификационни данни и настройки",
"exportDatabaseDesc": "Изтеглете преносимо SQLite резервно копие на всички хостове, идентификационни данни и настройки",
"export": "Експорт",
"exporting": "Експортиране...",
"importDatabase": "Импортиране на база данни",
"importDatabaseDesc": "Възстановяване от .sqlite архивен файл",
"importDatabaseDesc": "Възстановяване от преносимо .sqlite архивиране на произволен backend за база данни",
"importDatabaseSelected": "Избрано: {{name}}",
"selectFile": "Изберете файл",
"changeFile": "Промяна",
@@ -4516,6 +4536,7 @@
"selectProvider": "Изберете доставчик",
"providerSettings": "Настройки на доставчика",
"addProvider": "Добавяне на доставчик",
"editProvider": "Редактиране на доставчика",
"removeProvider": "Премахване на доставчика",
"providerType": "Доставчик",
"providerLabel": "Име",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Например: llama3.1 или claude-opus-5",
"labelRequired": "Дайте име на този доставчик",
"providerSaveFailed": "Доставчикът не можа да бъде запазен",
"providerUpdated": "Доставчикът е актуализиран",
"providerDeleteFailed": "Доставчикът не можа да бъде премахнат",
"privateEndpointHint": "Адресите във вашата частна мрежа се нуждаят първо от администратор, който да разреши хоста.",
"providerOllama": "Олама",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH কী",
"filterTagsGroup": "ট্যাগ",
"copyDeployCommand": "ডিপ্লয় কমান্ড কপি করুন",
"cloneCredentialAction": "ক্লোন ক্রেডেনশিয়াল",
"clonedCredential": "ক্লোন করা {{name}}",
"clonedCredentialName": "{{name}} (অনুলিপি)",
"deleteCredentialAction": "পরিচয়পত্র মুছুন",
"editCredentialAction": "পরিচয়পত্র সম্পাদনা করুন",
"failedToCloneCredential": "ক্রেডেনশিয়াল ক্লোন করতে ব্যর্থ হয়েছে",
"failedToDeleteCredential": "ক্রেডেনশিয়াল মুছে ফেলতে ব্যর্থ হয়েছে",
"idBadge": "আইডি",
"keyBadge": "চাবি",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "এজেন্ট সকেট পাথ",
"agentSocketPathPlaceholder": "SSH_AUTH_SOCK ব্যবহার করতে খালি রাখুন",
"agentSocketPathHint": "SSH_AUTH_SOCK এনভায়রনমেন্ট ভেরিয়েবল থেকে স্বয়ংক্রিয়ভাবে শনাক্ত করার জন্য এটি খালি রাখুন, অথবা একটি কাস্টম সকেট পাথ লিখুন (যেমন /run/user/1000/gnupg/S.gpg-agent.ssh)।",
"agentIdentityLabel": "এজেন্ট পরিচয় (পাবলিক কী)",
"agentIdentityPlaceholder": "এজেন্ট থেকে সমস্ত কী চেষ্টা করার জন্য খালি রাখুন।",
"agentIdentityHint": "এজেন্ট থেকে শুধুমাত্র সেই নির্দিষ্ট পাবলিক কী (যেমন ssh-ed25519 AAAA...) অফার করার জন্য সেটি পেস্ট করুন। এজেন্টের কাছে একাধিক কী থাকলে সার্ভারের MaxAuthTries সীমা অতিক্রম করা এড়ানোর জন্য এটি করার পরামর্শ দেওয়া হয়।",
"shareSshAuthLabel": "SSH প্রমাণীকরণ শেয়ার করুন",
"shareSshAuthDesc": "প্রাপকদের এই হোস্টের SSH প্রমাণীকরণের এনক্রিপ্টেড কপি দিন। প্রাপকের ব্যক্তিগত পরিচয়পত্রই অগ্রাধিকার পাবে।",
"tailscaleDeviceSelect": "টেইলস্কেল ডিভাইস নির্বাচন করুন",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "কোনো পরিচয়পত্র খুঁজে পাওয়া যায়নি",
"addCredentialBtn": "পরিচয়পত্র যোগ করুন",
"updateCredentialBtn": "পরিচয়পত্র আপডেট করুন",
"editCredentialAction": "পরিচয়পত্র সম্পাদনা করুন",
"backToHost": "হোস্টে ফিরে যান",
"saveCredentialAsNewBtn": "নতুন হিসাবে সংরক্ষণ করুন",
"saveCredentialAsNewDesc": "এই পরিবর্তনগুলো সহ একটি নতুন ক্রেডেনশিয়াল তৈরি করে এবং এটিকে এই হোস্টে বরাদ্দ করে। মূল ক্রেডেনশিয়ালটি অপরিবর্তিত থাকে।",
"credentialSavedAsNew": "নতুন পরিচয়পত্র হিসেবে সংরক্ষিত।",
"failedToSaveCredentialAsNew": "নতুন পরিচয়পত্র হিসেবে সংরক্ষণ করা যায়নি",
"features": "বৈশিষ্ট্য",
"noFolder": "(কোন ফোল্ডার নেই)",
"deleteSelected": "মুছে ফেলুন",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "সংক্ষিপ্ত",
"displayShowTags": "ট্যাগ দেখান",
"displayTrayOnClick": "ক্লিক করলে অ্যাকশন খুলবে (হোভার করার পরিবর্তে)",
"actionsVisibility": "হোস্টের কার্যকলাপ",
"actionsVisibilityDesc": "যখন প্রতিটি হোস্টে সংযোগ এবং ব্যবস্থাপনা বোতামগুলি প্রদর্শিত হয়",
"actionsVisibility": "হোস্ট অ্যাকশন প্রদর্শন",
"actionsVisibilityDesc": "হোস্ট অ্যাকশন কখন প্রসারিত হবে তা বেছে নিন। পূর্ববর্তী ক্লিক-টু-এক্সপ্যান্ড আচরণ পুনরুদ্ধার করতে ক্লিক নির্বাচন করুন।",
"actionsAlways": "সর্বদা দৃশ্যমান",
"actionsOnly": "শুধু সংযোগ করুন, বাকিটা ক্লিকে।",
"actionsHover": "হোভার করলে দেখান",
"actionsClick": "ক্লিক করদেখান",
"actionsClick": "প্রসারিত/সংকুচিত করক্লিক করুন",
"openOnDoubleClick": "ডাবল ক্লিকে খুলুন",
"openOnDoubleClickDesc": "হোস্ট সেশন খুলতে একক ক্লিকের পরিবর্তে ডাবল ক্লিক প্রয়োজন।",
"deletedHost": "মুছে ফেলা হয়েছে {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "আপলোড করা হয়েছে {{name}}",
"uploadFailed": "{{name}} আপলোড করতে ব্যর্থ হয়েছে",
"uploadDisabled": "এই সংযোগের জন্য আপলোড নিষ্ক্রিয় করা হয়েছে।",
"driveUnavailable": "ফাইল আপলোড করার আগে RDP ড্রাইভ রিডাইরেকশন সক্রিয় করুন।",
"dropToUpload": "আপলোড করতে ফাইলগুলো ড্রপ করুন"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "ফোল্ডারের নাম",
"fileUploadedSuccessfully": "ফাইল \"{{name}}\" সফলভাবে আপলোড হয়েছে",
"failedToUploadFile": "ফাইল আপলোড করতে ব্যর্থ হয়েছে",
"uploadingFolderFiles": "{{count}} টি ফাইল আপলোড করা হচ্ছে...",
"uploadedFolderFiles": "{{count}} টি ফাইল সফলভাবে আপলোড করা হয়েছে",
"uploadedFolderPartial": "{{uploaded}} টি ফাইল আপলোড করা হয়েছে, {{failed}} টি ফাইল আপলোড ব্যর্থ হয়েছে",
"fileDownloadedSuccessfully": "ফাইল \"{{name}}\" সফলভাবে ডাউনলোড হয়েছে",
"failedToDownloadFile": "ফাইল ডাউনলোড করতে ব্যর্থ হয়েছে",
"fileCreatedSuccessfully": "ফাইল \"{{name}}\" সফলভাবে তৈরি হয়েছে",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} আইটেম",
"downloadFileSuccess": "ফাইল {{name}} সফলভাবে ডাউনলোড হয়েছে",
"downloadFileFailed": "ডাউনলোড ব্যর্থ হয়েছে",
"downloadingFile": "ডাউনলোড হচ্ছে {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "{{name}} এ যান",
"diffCompareWith": "{{name}} এর সাথে পার্থক্য তুলনা করুন",
"dragOutsideToDownload": "ডাউনলোড করতে উইন্ডোর বাইরে টেনে আনুন ({{count}} ফাইলগুলো)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "প্রথমবার লগইন করার সময় OIDC/SSO ব্যবহারকারীদের জন্য স্বয়ংক্রিয়ভাবে অ্যাকাউন্ট তৈরি করুন (রেজিস্ট্রেশন টগল থেকে স্বাধীনভাবে)",
"oidcSilentLoginDefault": "ডিফল্টরূপে নীরব OIDC লগইন",
"oidcSilentLoginDefaultDesc": "প্রতিবার পরিদর্শনে লগইন ফর্মটি সম্পূর্ণভাবে এড়িয়ে স্বয়ংক্রিয়ভাবে OIDC লগইনে পুনঃনির্দেশিত করুন।",
"oidcSilentLoginDefaultLockedDesc": "এই সেটিংটি OIDC_SILENT_LOGIN_DEFAULT এনভায়রনমেন্ট ভেরিয়েবল দ্বারা লক করা আছে এবং এখান থেকে এটি পরিবর্তন করা যাবে না।",
"allowPasswordReset": "পাসওয়ার্ড রিসেট করার অনুমতি দিন",
"allowPasswordResetDesc": "ডকার লগের মাধ্যমে কোড রিসেট করুন",
"commandHistoryEnabled": "কমান্ডের ইতিহাস",
@@ -3317,11 +3337,11 @@
"createRole": "তৈরি করুন",
"creating": "তৈরি করা হচ্ছে...",
"exportDatabase": "ডেটাবেস রপ্তানি করুন",
"exportDatabaseDesc": "সমস্ত হোস্ট, ক্রেডেনশিয়াল এবং সেটিংসের একটি ব্যাকআপ ডাউনলোড করুন।",
"exportDatabaseDesc": "সমস্ত হোস্ট, ক্রেডেনশিয়াল এবং সেটিংসের একটি পোর্টেবল SQLite ব্যাকআপ ডাউনলোড করুন।",
"export": "রপ্তানি",
"exporting": "রপ্তানি করা হচ্ছে...",
"importDatabase": "ডেটাবেস আমদানি করুন",
"importDatabaseDesc": "একটি .sqlite ব্যাকআপ ফাইল থেকে পুনরুদ্ধার করুন",
"importDatabaseDesc": "যেকোনো ডাটাবেস ব্যাকএন্ডে একটি পোর্টেবল .sqlite ব্যাকআপ থেকে পুনরুদ্ধার করুন",
"importDatabaseSelected": "নির্বাচিত: {{name}}",
"selectFile": "ফাইল নির্বাচন করুন",
"changeFile": "পরিবর্তন",
@@ -4516,6 +4536,7 @@
"selectProvider": "একজন সরবরাহকারী নির্বাচন করুন",
"providerSettings": "প্রদানকারীর সেটিংস",
"addProvider": "প্রদানকারী যোগ করুন",
"editProvider": "সম্পাদনা প্রদানকারী",
"removeProvider": "প্রদানকারীকে অপসারণ করুন",
"providerType": "সরবরাহকারী",
"providerLabel": "নাম",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "উদাহরণস্বরূপ: llama3.1 অথবা claude-opus-5",
"labelRequired": "এই সরবরাহকারীকে একটি নাম দিন",
"providerSaveFailed": "প্রদানকারীকে সংরক্ষণ করা যায়নি",
"providerUpdated": "প্রদানকারী আপডেট করেছেন",
"providerDeleteFailed": "প্রদানকারীকে সরানো যায়নি",
"privateEndpointHint": "আপনার ব্যক্তিগত নেটওয়ার্কের অ্যাড্রেসগুলো ব্যবহার করার জন্য প্রথমে একজন অ্যাডমিনিস্ট্রেটরের কাছ থেকে হোস্টটিকে অনুমতি নিতে হয়।",
"providerOllama": "ওল্লামা",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "Clau SSH",
"filterTagsGroup": "Etiquetes",
"copyDeployCommand": "Copia l'ordre de desplegament",
"cloneCredentialAction": "Credencial clonada",
"clonedCredential": "Clonat {{name}}",
"clonedCredentialName": "{{name}} (còpia)",
"deleteCredentialAction": "Suprimeix la credencial",
"editCredentialAction": "Edita la credencial",
"failedToCloneCredential": "No s'ha pogut clonar la credencial",
"failedToDeleteCredential": "No s'ha pogut suprimir la credencial",
"idBadge": "ID",
"keyBadge": "CLAU",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Ruta del sòcol de l'agent",
"agentSocketPathPlaceholder": "Deixeu-ho en blanc per utilitzar SSH_AUTH_SOCK",
"agentSocketPathHint": "Deixeu-ho en blanc per detectar-ho automàticament des de la variable d'entorn SSH_AUTH_SOCK o introduïu una ruta de sòcol personalitzada (per exemple, /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Identitat de l'agent (clau pública)",
"agentIdentityPlaceholder": "Deixeu-ho en blanc per provar totes les claus de l'agent",
"agentIdentityHint": "Enganxa una clau pública (per exemple, ssh-ed25519 AAAA...) per oferir només aquesta clau de l'agent. Es recomana quan l'agent té moltes claus, per evitar superar el MaxAuthTries del servidor.",
"shareSshAuthLabel": "Comparteix l'autenticació SSH",
"shareSshAuthDesc": "Doneu als destinataris còpies xifrades de l'autenticació SSH d'aquest amfitrió. Les credencials personals d'un destinatari encara tenen prioritat.",
"tailscaleDeviceSelect": "Selecciona el dispositiu Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "No s'han trobat credencials",
"addCredentialBtn": "Afegeix credencials",
"updateCredentialBtn": "Actualitza les credencials",
"editCredentialAction": "Edita la credencial",
"backToHost": "Torna a l'amfitrió",
"saveCredentialAsNewBtn": "Desa com a nou",
"saveCredentialAsNewDesc": "Crea una nova credencial amb aquests canvis i l'assigna a aquest amfitrió. La credencial original no es modifica.",
"credentialSavedAsNew": "Desada com a credencial nova",
"failedToSaveCredentialAsNew": "No s'ha pogut desar com a credencial nova",
"features": "Característiques",
"noFolder": "(Sense carpeta)",
"deleteSelected": "Suprimeix",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Compacte",
"displayShowTags": "Mostra les etiquetes",
"displayTrayOnClick": "Obreix accions en fer clic (en comptes de passar el cursor per sobre)",
"actionsVisibility": "Accions de l'amfitrió",
"actionsVisibilityDesc": "Quan apareixen els botons de connexió i administració de cada host",
"actionsVisibility": "Visualització de l'acció de l'amfitrió",
"actionsVisibilityDesc": "Trieu quan s'expandeixen les accions de l'amfitrió. Seleccioneu \"Clic\" per restaurar el comportament anterior de clic per expandir.",
"actionsAlways": "Sempre visible",
"actionsOnly": "Només connecta, descansa en fer clic",
"actionsHover": "Mostra en passar el cursor per sobre",
"actionsClick": "Mostra en fer clic",
"actionsClick": "Feu clic per expandir/replegar",
"openOnDoubleClick": "Obre amb doble clic",
"openOnDoubleClickDesc": "Cal fer doble clic per obrir una sessió d'amfitrió en comptes d'un sol clic",
"deletedHost": "Suprimit {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Pujat {{name}}",
"uploadFailed": "No s'ha pogut carregar {{name}}",
"uploadDisabled": "Les càrregues estan desactivades per a aquesta connexió",
"driveUnavailable": "Activa la redirecció de la unitat RDP abans de carregar fitxers",
"dropToUpload": "Deixa anar els fitxers per pujar-los"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Nom de la carpeta",
"fileUploadedSuccessfully": "El fitxer \"{{name}}\" s'ha carregat correctament",
"failedToUploadFile": "No s'ha pogut carregar el fitxer",
"uploadingFolderFiles": "S'estan pujant {{count}} fitxer(s)...",
"uploadedFolderFiles": "S'han carregat correctament els fitxers {{count}}",
"uploadedFolderPartial": "No s'ha pogut pujar el(s) fitxer(s) {{uploaded}} , {{failed}}",
"fileDownloadedSuccessfully": "Fitxer \"{{name}}\" descarregat correctament",
"failedToDownloadFile": "No s'ha pogut descarregar el fitxer",
"fileCreatedSuccessfully": "Fitxer \"{{name}}\" creat correctament",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} elements",
"downloadFileSuccess": "Fitxer {{name}} descarregat correctament",
"downloadFileFailed": "La descàrrega ha fallat",
"downloadingFile": "Descarregant {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Mou a {{name}}",
"diffCompareWith": "Comparació de diferències amb {{name}}",
"dragOutsideToDownload": "Arrossega fora de la finestra per descarregar (fitxers{{count}})",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Crea automàticament comptes per a usuaris OIDC/SSO en el primer inici de sessió (independentment de l'interruptor de registre)",
"oidcSilentLoginDefault": "Inici de sessió silenciós a l'OIDC per defecte",
"oidcSilentLoginDefaultDesc": "Redirecciona automàticament a l'inici de sessió d'OIDC en cada visita, ometent completament el formulari d'inici de sessió",
"oidcSilentLoginDefaultLockedDesc": "Aquesta configuració està bloquejada per la variable d'entorn OIDC_SILENT_LOGIN_DEFAULT i no es pot canviar aquí.",
"allowPasswordReset": "Permet el restabliment de la contrasenya",
"allowPasswordResetDesc": "Restableix el codi a través dels registres de Docker",
"commandHistoryEnabled": "Historial de comandaments",
@@ -3317,11 +3337,11 @@
"createRole": "Crea",
"creating": "Creant...",
"exportDatabase": "Exporta la base de dades",
"exportDatabaseDesc": "Baixeu una còpia de seguretat de tots els hosts, credencials i configuracions",
"exportDatabaseDesc": "Baixeu una còpia de seguretat SQLite portàtil de tots els hosts, credencials i configuracions",
"export": "Exporta",
"exporting": "Exportant...",
"importDatabase": "Importa la base de dades",
"importDatabaseDesc": "Restaurar des d'un fitxer de còpia de seguretat .sqlite",
"importDatabaseDesc": "Restaura des d'una còpia de seguretat .sqlite portàtil en qualsevol backend de base de dades",
"importDatabaseSelected": "Seleccionat: {{name}}",
"selectFile": "Selecciona un fitxer",
"changeFile": "Canvi",
@@ -4516,6 +4536,7 @@
"selectProvider": "Seleccioneu un proveïdor",
"providerSettings": "Configuració del proveïdor",
"addProvider": "Afegeix un proveïdor",
"editProvider": "Edita el proveïdor",
"removeProvider": "Elimina el proveïdor",
"providerType": "Proveïdor",
"providerLabel": "Nom",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Per exemple: llama3.1 o claude-opus-5",
"labelRequired": "Doneu un nom a aquest proveïdor",
"providerSaveFailed": "No s'ha pogut desar el proveïdor",
"providerUpdated": "Proveïdor actualitzat",
"providerDeleteFailed": "No s'ha pogut eliminar el proveïdor",
"privateEndpointHint": "Les adreces de la vostra xarxa privada necessiten primer que un administrador permeti l'accés a l'amfitrió.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH klíč",
"filterTagsGroup": "Štítky",
"copyDeployCommand": "Kopírovat příkaz nasazení",
"cloneCredentialAction": "Klonovat přihlašovací údaje",
"clonedCredential": "Klonováno {{name}}",
"clonedCredentialName": "{{name}} (kopie)",
"deleteCredentialAction": "Smazat přihlašovací údaje",
"editCredentialAction": "Upravit přihlašovací údaje",
"failedToCloneCredential": "Nepodařilo se klonovat přihlašovací údaje",
"failedToDeleteCredential": "Nepodařilo se smazat přihlašovací údaje",
"idBadge": "Průkaz totožnosti",
"keyBadge": "KLÍČ",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Cesta soketu agenta",
"agentSocketPathPlaceholder": "Pro použití SSH_AUTH_SOCK ponechte prázdné.",
"agentSocketPathHint": "Nechte prázdné pro automatickou detekci z proměnné prostředí SSH_AUTH_SOCK nebo zadejte vlastní cestu k socketu (např. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Identita agenta (veřejný klíč)",
"agentIdentityPlaceholder": "Nechte prázdné, chcete-li vyzkoušet všechny klíče od agenta.",
"agentIdentityHint": "Vložte veřejný klíč (např. ssh-ed25519 AAAA...), aby se od agenta nabídl pouze tento klíč. Doporučuje se, když agent drží mnoho klíčů, aby se zabránilo překročení maximálního počtu pokusů o ověření serveru.",
"shareSshAuthLabel": "Sdílení ověřování SSH",
"shareSshAuthDesc": "Poskytněte příjemcům šifrované kopie SSH ověření tohoto hostitele. Osobní přihlašovací údaje příjemce mají stále přednost.",
"tailscaleDeviceSelect": "Vyberte zařízení Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Nenalezeny žádné přihlašovací údaje",
"addCredentialBtn": "Přidat přihlašovací údaje",
"updateCredentialBtn": "Aktualizovat přihlašovací údaje",
"editCredentialAction": "Upravit přihlašovací údaje",
"backToHost": "Zpět k hostiteli",
"saveCredentialAsNewBtn": "Uložit jako nové",
"saveCredentialAsNewDesc": "Vytvoří nové přihlašovací údaje s těmito změnami a přiřadí je tomuto hostiteli. Původní přihlašovací údaje zůstanou nezměněny.",
"credentialSavedAsNew": "Uloženo jako nové pověření",
"failedToSaveCredentialAsNew": "Uložení jako nových přihlašovacích údajů se nezdařilo",
"features": "Funkce",
"noFolder": "(Žádná složka)",
"deleteSelected": "Vymazat",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Kompaktní",
"displayShowTags": "Zobrazit štítky",
"displayTrayOnClick": "Otevírat akce po kliknutí (místo najetí myší)",
"actionsVisibility": "Akce hostitele",
"actionsVisibilityDesc": "Když se na každém hostiteli zobrazí tlačítka pro připojení a správu",
"actionsVisibility": "Zobrazení akce hostitele",
"actionsVisibilityDesc": "Vyberte, kdy se akce hostitele rozbalí. Vyberte možnost Kliknout, chcete-li obnovit původní chování rozbalení kliknutím.",
"actionsAlways": "Vždy viditelné",
"actionsOnly": "Pouze připojení, odpočinek po kliknutí",
"actionsHover": "Zobrazit při najetí myší",
"actionsClick": "Zobrazit po kliknutí",
"actionsClick": "Kliknutím rozbalíte/sbalíte",
"openOnDoubleClick": "Otevřít dvojitým kliknutím",
"openOnDoubleClickDesc": "Vyžadovat dvojité kliknutí pro otevření hostitelské relace místo jednoho kliknutí",
"deletedHost": "Smazáno {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Nahráno {{name}}",
"uploadFailed": "Nahrávání se nezdařilo {{name}}",
"uploadDisabled": "Nahrávání je pro toto připojení zakázáno.",
"driveUnavailable": "Před nahráváním souborů povolte přesměrování disku RDP",
"dropToUpload": "Přetáhněte soubory k nahrání"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Název složky",
"fileUploadedSuccessfully": "Soubor „{{name}}“ byl úspěšně nahrán",
"failedToUploadFile": "Nahrávání souboru se nezdařilo",
"uploadingFolderFiles": "Nahrávání {{count}} souborů...",
"uploadedFolderFiles": "Úspěšně nahráno {{count}} souborů",
"uploadedFolderPartial": "Nahrání souboru (souborů) {{uploaded}} se nezdařilo. {{failed}}",
"fileDownloadedSuccessfully": "Soubor „{{name}}“ byl úspěšně stažen",
"failedToDownloadFile": "Stažení souboru se nezdařilo",
"fileCreatedSuccessfully": "Soubor „{{name}}“ byl úspěšně vytvořen",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} položek",
"downloadFileSuccess": "Soubor {{name}} byl úspěšně stažen",
"downloadFileFailed": "Stahování se nezdařilo",
"downloadingFile": "Stahování {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Přesunout na {{name}}",
"diffCompareWith": "Rozdíl ve srovnání s {{name}}",
"dragOutsideToDownload": "Přetáhněte mimo okno pro stažení ({{count}} souborů)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Automatické vytváření účtů pro uživatele OIDC/SSO při prvním přihlášení (nezávisle na přepínači registrace)",
"oidcSilentLoginDefault": "Tiché přihlášení OIDC ve výchozím nastavení",
"oidcSilentLoginDefaultDesc": "Automatické přesměrování na přihlášení k OIDC při každé návštěvě, úplné přeskakování přihlašovacího formuláře",
"oidcSilentLoginDefaultLockedDesc": "Toto nastavení je uzamčeno proměnnou prostředí OIDC_SILENT_LOGIN_DEFAULT a nelze jej zde změnit.",
"allowPasswordReset": "Povolit resetování hesla",
"allowPasswordResetDesc": "Reset kódu pomocí protokolů Dockeru",
"commandHistoryEnabled": "Historie příkazů",
@@ -3317,11 +3337,11 @@
"createRole": "Vytvořit",
"creating": "Vytváření...",
"exportDatabase": "Exportovat databázi",
"exportDatabaseDesc": "Stáhněte si zálohu všech hostitelů, přihlašovacích údajů a nastavení",
"exportDatabaseDesc": "Stáhněte si přenosnou zálohu SQLite všech hostitelů, přihlašovacích údajů a nastavení",
"export": "Vývozní",
"exporting": "Export...",
"importDatabase": "Importovat databázi",
"importDatabaseDesc": "Obnovení ze záložního souboru .sqlite",
"importDatabaseDesc": "Obnovení z přenosné zálohy .sqlite na libovolném backendu databáze",
"importDatabaseSelected": "Vybráno: {{name}}",
"selectFile": "Vybrat soubor",
"changeFile": "Přeměna",
@@ -4516,6 +4536,7 @@
"selectProvider": "Vyberte poskytovatele",
"providerSettings": "Nastavení poskytovatele",
"addProvider": "Přidat poskytovatele",
"editProvider": "Upravit poskytovatele",
"removeProvider": "Odebrat poskytovatele",
"providerType": "Poskytovatel",
"providerLabel": "Jméno",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Například: llama3.1 nebo claude-opus-5",
"labelRequired": "Zadejte tomuto poskytovateli název",
"providerSaveFailed": "Nepodařilo se uložit poskytovatele",
"providerUpdated": "Poskytovatel aktualizován",
"providerDeleteFailed": "Nepodařilo se odebrat poskytovatele",
"privateEndpointHint": "Adresy ve vaší privátní síti vyžadují, aby je správce nejprve povolil hostiteli.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH-nøgle",
"filterTagsGroup": "Tags",
"copyDeployCommand": "Kopiér deploy-kommandoen",
"cloneCredentialAction": "Klon legitimationsoplysninger",
"clonedCredential": "Klonet {{name}}",
"clonedCredentialName": "{{name}} (kopi)",
"deleteCredentialAction": "Slet legitimationsoplysninger",
"editCredentialAction": "Rediger legitimationsoplysninger",
"failedToCloneCredential": "Kunne ikke klone legitimationsoplysninger",
"failedToDeleteCredential": "Kunne ikke slette legitimationsoplysninger",
"idBadge": "ID",
"keyBadge": "NØGLE",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Agent Socket-sti",
"agentSocketPathPlaceholder": "Lad feltet stå tomt for at bruge SSH_AUTH_SOCK",
"agentSocketPathHint": "Lad feltet stå tomt for automatisk at detektere fra miljøvariablen SSH_AUTH_SOCK, eller indtast en brugerdefineret socket-sti (f.eks. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Agentidentitet (offentlig nøgle)",
"agentIdentityPlaceholder": "Lad feltet stå tomt for at prøve alle nøgler fra agenten",
"agentIdentityHint": "Indsæt en offentlig nøgle (f.eks. ssh-ed25519 AAAA...) for kun at tilbyde den nøgle fra agenten. Anbefales, når agenten har mange nøgler, for at undgå at overskride serverens MaxAuthTries.",
"shareSshAuthLabel": "Del SSH-godkendelse",
"shareSshAuthDesc": "Giv modtagerne krypterede kopier af denne værts SSH-godkendelse. En modtagers personlige legitimationsoplysninger har stadig forrang.",
"tailscaleDeviceSelect": "Vælg Tailscale-enhed",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Ingen legitimationsoplysninger fundet",
"addCredentialBtn": "Tilføj legitimationsoplysninger",
"updateCredentialBtn": "Opdater legitimationsoplysninger",
"editCredentialAction": "Rediger legitimationsoplysninger",
"backToHost": "Tilbage til værten",
"saveCredentialAsNewBtn": "Gem som ny",
"saveCredentialAsNewDesc": "Opretter en ny legitimationsoplysning med disse ændringer og tildeler den til denne vært. Den oprindelige legitimationsoplysning forbliver uændret.",
"credentialSavedAsNew": "Gemt som en ny legitimationsoplysninger",
"failedToSaveCredentialAsNew": "Kunne ikke gemme som en ny legitimationsoplysninger",
"features": "Funktioner",
"noFolder": "(Ingen mappe)",
"deleteSelected": "Slet",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Kompakt",
"displayShowTags": "Vis tags",
"displayTrayOnClick": "Åbn handlinger ved klik (i stedet for at holde musen over)",
"actionsVisibility": "Værtshandlinger",
"actionsVisibilityDesc": "Når knapperne for forbindelse og administration på hver vært vises",
"actionsVisibility": "Værtshandlingsvisning",
"actionsVisibilityDesc": "Vælg, hvornår værtshandlinger udvides. Vælg klik for at gendanne den tidligere klik-for-at-udvide-funktion.",
"actionsAlways": "Altid synlig",
"actionsOnly": "Kun forbindelse, hvile ved klik",
"actionsHover": "Vis ved at holde musen over",
"actionsClick": "Vis ved klik",
"actionsClick": "Klik for at udvide/skjule",
"openOnDoubleClick": "Åbn ved dobbeltklik",
"openOnDoubleClickDesc": "Kræv et dobbeltklik for at åbne en værtsession i stedet for et enkelt klik",
"deletedHost": "Slettet {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Uploadet {{name}}",
"uploadFailed": "Kunne ikke uploade {{name}}",
"uploadDisabled": "Uploads er deaktiveret for denne forbindelse",
"driveUnavailable": "Aktivér omdirigering af RDP-drev før upload af filer",
"dropToUpload": "Slip filer for at uploade"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Mappenavn",
"fileUploadedSuccessfully": "Filen \"{{name}}\" er blevet uploadet",
"failedToUploadFile": "Kunne ikke uploade filen",
"uploadingFolderFiles": "Uploader {{count}} fil(er)...",
"uploadedFolderFiles": "Uploadede {{count}} fil(er)",
"uploadedFolderPartial": "Upload af {{uploaded}} fil(er), {{failed}} mislykkedes",
"fileDownloadedSuccessfully": "Filen \"{{name}}\" er blevet downloadet",
"failedToDownloadFile": "Kunne ikke downloade filen",
"fileCreatedSuccessfully": "Filen \"{{name}}\" er blevet oprettet",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} elementer",
"downloadFileSuccess": "Fil {{name}} er blevet downloadet",
"downloadFileFailed": "Download mislykkedes",
"downloadingFile": "Downloader {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Flyt til {{name}}",
"diffCompareWith": "Differens sammenligning med {{name}}",
"dragOutsideToDownload": "Træk uden for vinduet for at downloade ({{count}} filer)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Automatisk oprettelse af konti til OIDC/SSO-brugere ved første login (uafhængigt af registreringsknappen)",
"oidcSilentLoginDefault": "Lydløs OIDC-login som standard",
"oidcSilentLoginDefaultDesc": "Omdiriger automatisk til OIDC-login ved hvert besøg, og spring loginformularen helt over",
"oidcSilentLoginDefaultLockedDesc": "Denne indstilling er låst af miljøvariablen OIDC_SILENT_LOGIN_DEFAULT og kan ikke ændres her.",
"allowPasswordReset": "Tillad nulstilling af adgangskode",
"allowPasswordResetDesc": "Nulstil kode via Docker-logfiler",
"commandHistoryEnabled": "Kommandohistorik",
@@ -3317,11 +3337,11 @@
"createRole": "Skabe",
"creating": "Opretter...",
"exportDatabase": "Eksportér database",
"exportDatabaseDesc": "Download en sikkerhedskopi af alle værter, legitimationsoplysninger og indstillinger",
"exportDatabaseDesc": "Download en bærbar SQLite-sikkerhedskopi af alle værter, legitimationsoplysninger og indstillinger",
"export": "Eksportere",
"exporting": "Eksporterer...",
"importDatabase": "Importér database",
"importDatabaseDesc": "Gendan fra en .sqlite-backupfil",
"importDatabaseDesc": "Gendan fra en bærbar .sqlite-backup på enhver database-backend",
"importDatabaseSelected": "Valgt: {{name}}",
"selectFile": "Vælg fil",
"changeFile": "Forandring",
@@ -4516,6 +4536,7 @@
"selectProvider": "Vælg en udbyder",
"providerSettings": "Udbyderindstillinger",
"addProvider": "Tilføj udbyder",
"editProvider": "Rediger udbyder",
"removeProvider": "Fjern udbyder",
"providerType": "Udbyder",
"providerLabel": "Navn",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "For eksempel: llama3.1 eller claude-opus-5",
"labelRequired": "Giv denne udbyder et navn",
"providerSaveFailed": "Udbyderen kunne ikke gemmes",
"providerUpdated": "Udbyder opdateret",
"providerDeleteFailed": "Udbyderen kunne ikke fjernes",
"privateEndpointHint": "Adresser på dit private netværk kræver først tilladelse fra en administrator til værten.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH-Schlüssel",
"filterTagsGroup": "Tags",
"copyDeployCommand": "Befehl zum Bereitstellen kopieren",
"cloneCredentialAction": "Anmeldeinformationen klonen",
"clonedCredential": "Geklont {{name}}",
"clonedCredentialName": "{{name}} (Kopie)",
"deleteCredentialAction": "Anmeldeinformationen löschen",
"editCredentialAction": "Anmeldeinformationen bearbeiten",
"failedToCloneCredential": "Fehler beim Klonen der Anmeldeinformationen",
"failedToDeleteCredential": "Anmeldeinformationen konnten nicht gelöscht werden",
"idBadge": "AUSWEIS",
"keyBadge": "SCHLÜSSEL",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Agent-Socket-Pfad",
"agentSocketPathPlaceholder": "Leer lassen, um SSH_AUTH_SOCK zu verwenden",
"agentSocketPathHint": "Leer lassen, um automatisch über die Umgebungsvariable SSH_AUTH_SOCK zu erkennen, oder geben Sie einen benutzerdefinierten Socket-Pfad ein (z. B. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Agentenidentität (öffentlicher Schlüssel)",
"agentIdentityPlaceholder": "Lassen Sie dieses Feld leer, um alle Schlüssel des Agenten auszuprobieren.",
"agentIdentityHint": "Fügen Sie einen öffentlichen Schlüssel ein (z. B. ssh-ed25519 AAAA...), damit der Agent nur diesen Schlüssel anbietet. Dies wird empfohlen, wenn der Agent viele Schlüssel verwaltet, um die maximale Anzahl an Authentifizierungsversuchen (MaxAuthTries) des Servers nicht zu überschreiten.",
"shareSshAuthLabel": "SSH-Authentifizierung teilen",
"shareSshAuthDesc": "Geben Sie den Empfängern verschlüsselte Kopien der SSH-Authentifizierungsinformationen dieses Hosts. Die persönlichen Anmeldeinformationen des Empfängers haben weiterhin Vorrang.",
"tailscaleDeviceSelect": "Tailscale-Gerät auswählen",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Keine Anmeldedaten gefunden",
"addCredentialBtn": "Anmeldedaten hinzufügen",
"updateCredentialBtn": "Anmeldedaten aktualisieren",
"editCredentialAction": "Anmeldeinformationen bearbeiten",
"backToHost": "Zurück zur Startseite",
"saveCredentialAsNewBtn": "Als neu speichern",
"saveCredentialAsNewDesc": "Erstellt mit diesen Änderungen eine neue Anmeldeinformation und weist sie diesem Host zu. Die ursprüngliche Anmeldeinformation bleibt unverändert.",
"credentialSavedAsNew": "Als neue Anmeldeinformation gespeichert",
"failedToSaveCredentialAsNew": "Speichern als neue Anmeldeinformationen fehlgeschlagen",
"features": "Funktionen",
"noFolder": "(Kein Ordner)",
"deleteSelected": "Löschen",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Kompakt",
"displayShowTags": "Tags anzeigen",
"displayTrayOnClick": "Aktionen per Klick öffnen (statt per Hover)",
"actionsVisibility": "Aktionen des Hosts",
"actionsVisibilityDesc": "Wenn die Verbindungs- und Verwaltungsschaltflächen auf jedem Host angezeigt werden",
"actionsVisibility": "Anzeige der Host-Aktion",
"actionsVisibilityDesc": "Wählen Sie aus, wann die Host-Aktionen erweitert werden. Wählen Sie „Klicken“, um das vorherige Verhalten (Klicken zum Erweitern) wiederherzustellen.",
"actionsAlways": "Immer sichtbar",
"actionsOnly": "Nur verbinden, Rest per Klick",
"actionsHover": "Beim Überfahren mit der Maus anzeigen",
"actionsClick": "Bei Klick anzeigen",
"actionsClick": "Zum Ein-/Ausklappen klicken",
"openOnDoubleClick": "Bei Doppelklick öffnen",
"openOnDoubleClickDesc": "Zum Öffnen einer Host-Sitzung ist ein Doppelklick anstelle eines einfachen Klicks erforderlich.",
"deletedHost": "{{name}} gelöscht",
@@ -1867,6 +1880,7 @@
"uploaded": "Hochgeladen {{name}}",
"uploadFailed": "Hochladen fehlgeschlagen {{name}}",
"uploadDisabled": "Uploads sind für diese Verbindung deaktiviert.",
"driveUnavailable": "Aktivieren Sie die RDP-Laufwerksumleitung, bevor Sie Dateien hochladen.",
"dropToUpload": "Dateien zum Hochladen per Drag & Drop ablegen"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Ordnername",
"fileUploadedSuccessfully": "Datei \"{{name}}\" erfolgreich hochgeladen",
"failedToUploadFile": "Hochladen der Datei fehlgeschlagen",
"uploadingFolderFiles": "Uploading {{count}} file(s)...",
"uploadedFolderFiles": "{{count}} Datei(en) erfolgreich hochgeladen",
"uploadedFolderPartial": "Hochladen von {{uploaded}} Datei(en), {{failed}} fehlgeschlagen",
"fileDownloadedSuccessfully": "Datei \"{{name}}\" erfolgreich heruntergeladen",
"failedToDownloadFile": "Herunterladen der Datei fehlgeschlagen",
"fileCreatedSuccessfully": "Datei \"{{name}}\" erfolgreich erstellt",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} Elemente",
"downloadFileSuccess": "Datei {{name}} erfolgreich heruntergeladen",
"downloadFileFailed": "Download fehlgeschlagen",
"downloadingFile": "Herunterladen {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Verschieben nach {{name}}",
"diffCompareWith": "Diff-Vergleich mit {{name}}",
"dragOutsideToDownload": "Zum Herunterladen aus dem Fenster ziehen ({{count}} Dateien)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Konten für OIDC/SSO-Benutzer bei der ersten Anmeldung automatisch erstellen (unabhängig vom Registrierungs-Schalter)",
"oidcSilentLoginDefault": "Stiller OIDC-Login standardmäßig",
"oidcSilentLoginDefaultDesc": "Bei jedem Besuch automatisch zum OIDC-Login weiterleiten, das Anmeldeformular vollständig überspringen",
"oidcSilentLoginDefaultLockedDesc": "Diese Einstellung ist durch die Umgebungsvariable OIDC_SILENT_LOGIN_DEFAULT gesperrt und kann hier nicht geändert werden.",
"allowPasswordReset": "Passwort-Zurücksetzen erlauben",
"allowPasswordResetDesc": "Reset-Code über Docker-Logs",
"commandHistoryEnabled": "Befehlsverlauf",
@@ -3317,11 +3337,11 @@
"createRole": "Erstellen",
"creating": "Wird erstellt...",
"exportDatabase": "Datenbank exportieren",
"exportDatabaseDesc": "Sicherung aller Hosts, Anmeldeinformationen und Einstellungen herunterladen",
"exportDatabaseDesc": "Laden Sie eine portable SQLite-Sicherung aller Hosts, Anmeldeinformationen und Einstellungen herunter.",
"export": "Exportieren",
"exporting": "Exportiere...",
"importDatabase": "Datenbank importieren",
"importDatabaseDesc": "Aus einer .sqlite-Sicherungsdatei wiederherstellen",
"importDatabaseDesc": "Wiederherstellung aus einem portablen .sqlite-Backup auf einem beliebigen Datenbank-Backend",
"importDatabaseSelected": "Ausgewählt: {{name}}",
"selectFile": "Datei auswählen",
"changeFile": "Ändern",
@@ -4516,6 +4536,7 @@
"selectProvider": "Wählen Sie einen Anbieter aus",
"providerSettings": "Anbietereinstellungen",
"addProvider": "Anbieter hinzufügen",
"editProvider": "Anbieter bearbeiten",
"removeProvider": "Anbieter entfernen",
"providerType": "Anbieter",
"providerLabel": "Name",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Zum Beispiel: llama3.1 oder claude-opus-5",
"labelRequired": "Geben Sie diesem Anbieter einen Namen",
"providerSaveFailed": "Der Anbieter konnte nicht gespeichert werden.",
"providerUpdated": "Anbieter aktualisiert",
"providerDeleteFailed": "Der Anbieter konnte nicht entfernt werden.",
"privateEndpointHint": "Adressen in Ihrem privaten Netzwerk benötigen eine vorherige Genehmigung durch einen Administrator.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "Κλειδί SSH",
"filterTagsGroup": "Ετικέτες",
"copyDeployCommand": "Αντιγραφή εντολής ανάπτυξης",
"cloneCredentialAction": "Κλωνοποίηση διαπιστευτηρίων",
"clonedCredential": "Κλωνοποιημένο {{name}}",
"clonedCredentialName": "{{name}} (αντίγραφο)",
"deleteCredentialAction": "Διαγραφή διαπιστευτηρίων",
"editCredentialAction": "Επεξεργασία διαπιστευτηρίων",
"failedToCloneCredential": "Αποτυχία κλωνοποίησης διαπιστευτηρίων",
"failedToDeleteCredential": "Η διαγραφή των διαπιστευτηρίων απέτυχε",
"idBadge": "ταυτότητα",
"keyBadge": "ΚΛΕΙΔΙ",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Διαδρομή υποδοχής παράγοντα",
"agentSocketPathPlaceholder": "Αφήστε κενό για να χρησιμοποιήσετε το SSH_AUTH_SOCK",
"agentSocketPathHint": "Αφήστε το κενό για αυτόματη ανίχνευση από τη μεταβλητή περιβάλλοντος SSH_AUTH_SOCK ή εισαγάγετε μια προσαρμοσμένη διαδρομή υποδοχής (π.χ. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Ταυτότητα Πράκτορα (Δημόσιο Κλειδί)",
"agentIdentityPlaceholder": "Αφήστε το κενό για να δοκιμάσετε όλα τα κλειδιά από τον πράκτορα",
"agentIdentityHint": "Επικολλήστε ένα δημόσιο κλειδί (π.χ. ssh-ed25519 AAAA...) για να προσφέρετε μόνο αυτό το κλειδί από τον παράγοντα. Συνιστάται όταν ο παράγοντας κατέχει πολλά κλειδιά, για να αποφευχθεί η υπέρβαση των MaxAuthTries του διακομιστή.",
"shareSshAuthLabel": "Κοινή χρήση ελέγχου ταυτότητας SSH",
"shareSshAuthDesc": "Δώστε στους παραλήπτες κρυπτογραφημένα αντίγραφα του ελέγχου ταυτότητας SSH αυτού του κεντρικού υπολογιστή. Τα προσωπικά διαπιστευτήρια ενός παραλήπτη εξακολουθούν να έχουν προτεραιότητα.",
"tailscaleDeviceSelect": "Επιλογή συσκευής κλίμακας ουράς",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Δεν βρέθηκαν διαπιστευτήρια",
"addCredentialBtn": "Προσθήκη διαπιστευτηρίων",
"updateCredentialBtn": "Ενημέρωση διαπιστευτηρίων",
"editCredentialAction": "Επεξεργασία διαπιστευτηρίων",
"backToHost": "Επιστροφή στον κεντρικό υπολογιστή",
"saveCredentialAsNewBtn": "Αποθήκευση ως νέα",
"saveCredentialAsNewDesc": "Δημιουργεί ένα νέο πιστοποιητικό με αυτές τις αλλαγές και το αντιστοιχίζει σε αυτόν τον κεντρικό υπολογιστή. Το αρχικό πιστοποιητικό παραμένει αμετάβλητο.",
"credentialSavedAsNew": "Αποθηκεύτηκε ως νέο πιστοποιητικό",
"failedToSaveCredentialAsNew": "Αποτυχία αποθήκευσης ως νέας πιστοποίησης",
"features": "Χαρακτηριστικά",
"noFolder": "(Δεν υπάρχει φάκελος)",
"deleteSelected": "Διαγράφω",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Συμπαγής",
"displayShowTags": "Εμφάνιση ετικετών",
"displayTrayOnClick": "Άνοιγμα ενεργειών με κλικ (αντί για τοποθέτηση του δείκτη του ποντικιού)",
"actionsVisibility": "Ενέργειες κεντρικού υπολογιστή",
"actionsVisibilityDesc": "Όταν εμφανίζονται τα κουμπιά σύνδεσης και διαχείρισης σε κάθε κεντρικό υπολογιστή",
"actionsVisibility": "Εμφάνιση ενέργειας οικοδεσπότη",
"actionsVisibilityDesc": "Επιλέξτε πότε θα αναπτύσσονται οι ενέργειες κεντρικού υπολογιστή. Επιλέξτε κλικ για να επαναφέρετε την προηγούμενη συμπεριφορά κλικ-για-ανάπτυξη.",
"actionsAlways": "Πάντα ορατό",
"actionsOnly": "Μόνο σύνδεση, ξεκούραση με κλικ",
"actionsHover": "Εμφάνιση κατά την τοποθέτηση του δείκτη του ποντικιού",
"actionsClick": "Εμφάνιση με κλικ",
"actionsClick": "Κάντε κλικ για ανάπτυξη/σύμπτυξη",
"openOnDoubleClick": "Άνοιγμα με διπλό κλικ",
"openOnDoubleClickDesc": "Απαιτείται διπλό κλικ για το άνοιγμα μιας συνεδρίας κεντρικού υπολογιστή αντί για ένα μόνο κλικ",
"deletedHost": "Διαγράφηκε {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Μεταφορτώθηκε {{name}}",
"uploadFailed": "Αποτυχία μεταφόρτωσης {{name}}",
"uploadDisabled": "Οι μεταφορτώσεις είναι απενεργοποιημένες για αυτήν τη σύνδεση",
"driveUnavailable": "Ενεργοποίηση ανακατεύθυνσης μονάδας δίσκου RDP πριν από την αποστολή αρχείων",
"dropToUpload": "Αποθέστε αρχεία για μεταφόρτωση"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Όνομα φακέλου",
"fileUploadedSuccessfully": "Το αρχείο \"{{name}}\" μεταφορτώθηκε με επιτυχία",
"failedToUploadFile": "Η μεταφόρτωση του αρχείου απέτυχε",
"uploadingFolderFiles": "Μεταφόρτωση {{count}} αρχείου(ων)...",
"uploadedFolderFiles": "Μεταφορτώθηκαν με επιτυχία τα {{count}} αρχεία",
"uploadedFolderPartial": "Η μεταφόρτωση {{uploaded}} αρχείου(ων), {{failed}} απέτυχε",
"fileDownloadedSuccessfully": "Το αρχείο \"{{name}}\" λήφθηκε με επιτυχία.",
"failedToDownloadFile": "Αποτυχία λήψης αρχείου",
"fileCreatedSuccessfully": "Το αρχείο \"{{name}}\" δημιουργήθηκε με επιτυχία",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} στοιχεία",
"downloadFileSuccess": "Το αρχείο {{name}} λήφθηκε με επιτυχία.",
"downloadFileFailed": "Η λήψη απέτυχε.",
"downloadingFile": "Λήψη {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Μετακίνηση σε {{name}}",
"diffCompareWith": "Διαφορά σε σύγκριση με {{name}}",
"dragOutsideToDownload": "Σύρετε έξω από το παράθυρο για λήψη ({{count}} αρχείων)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Αυτόματη δημιουργία λογαριασμών για χρήστες OIDC/SSO κατά την πρώτη σύνδεση (ανεξάρτητα από την εναλλαγή εγγραφής)",
"oidcSilentLoginDefault": "Σιωπηλή σύνδεση OIDC από προεπιλογή",
"oidcSilentLoginDefaultDesc": "Αυτόματη ανακατεύθυνση στα στοιχεία σύνδεσης OIDC σε κάθε επίσκεψη, παρακάμπτοντας εντελώς τη φόρμα σύνδεσης",
"oidcSilentLoginDefaultLockedDesc": "Αυτή η ρύθμιση είναι κλειδωμένη από τη μεταβλητή περιβάλλοντος OIDC_SILENT_LOGIN_DEFAULT και δεν μπορεί να αλλάξει εδώ.",
"allowPasswordReset": "Να επιτρέπεται η επαναφορά κωδικού πρόσβασης",
"allowPasswordResetDesc": "Επαναφορά κώδικα μέσω αρχείων καταγραφής Docker",
"commandHistoryEnabled": "Ιστορικό εντολών",
@@ -3317,11 +3337,11 @@
"createRole": "Δημιουργώ",
"creating": "Δημιουργία...",
"exportDatabase": "Εξαγωγή βάσης δεδομένων",
"exportDatabaseDesc": "Λήψη αντιγράφου ασφαλείας όλων των κεντρικών υπολογιστών, των διαπιστευτηρίων και των ρυθμίσεων",
"exportDatabaseDesc": "Λήψη ενός φορητού αντιγράφου ασφαλείας SQLite όλων των κεντρικών υπολογιστών, των διαπιστευτηρίων και των ρυθμίσεων",
"export": "Εξαγωγή",
"exporting": "Εξαγωγή...",
"importDatabase": "Εισαγωγή βάσης δεδομένων",
"importDatabaseDesc": "Επαναφορά από αρχείο αντιγράφου ασφαλείας .sqlite",
"importDatabaseDesc": "Επαναφορά από ένα φορητό αντίγραφο ασφαλείας .sqlite σε οποιοδήποτε backend βάσης δεδομένων",
"importDatabaseSelected": "Επιλεγμένα: {{name}}",
"selectFile": "Επιλογή Αρχείου",
"changeFile": "Αλλαγή",
@@ -4516,6 +4536,7 @@
"selectProvider": "Επιλέξτε έναν πάροχο",
"providerSettings": "Ρυθμίσεις παρόχου",
"addProvider": "Προσθήκη παρόχου",
"editProvider": "Επεξεργασία παρόχου",
"removeProvider": "Κατάργηση παρόχου",
"providerType": "Προμηθευτής",
"providerLabel": "Ονομα",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Για παράδειγμα: llama3.1 ή claude-opus-5",
"labelRequired": "Δώστε σε αυτόν τον πάροχο ένα όνομα",
"providerSaveFailed": "Δεν ήταν δυνατή η αποθήκευση του παρόχου",
"providerUpdated": "Ο πάροχος ενημερώθηκε",
"providerDeleteFailed": "Δεν ήταν δυνατή η κατάργηση του παρόχου",
"privateEndpointHint": "Οι διευθύνσεις στο ιδιωτικό σας δίκτυο χρειάζονται πρώτα έναν διαχειριστή για να επιτρέψει στον κεντρικό υπολογιστή.",
"providerOllama": "Ολάμα",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "Clave SSH",
"filterTagsGroup": "Etiquetas",
"copyDeployCommand": "Copiar comando de despliegue",
"cloneCredentialAction": "Credencial clonada",
"clonedCredential": "Clonado {{name}}",
"clonedCredentialName": "{{name}} (copiar)",
"deleteCredentialAction": "Eliminar credenciales",
"editCredentialAction": "Editar credenciales",
"failedToCloneCredential": "Error al clonar las credenciales",
"failedToDeleteCredential": "Error al eliminar las credenciales",
"idBadge": "IDENTIFICACIÓN",
"keyBadge": "LLAVE",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Ruta del socket del agente",
"agentSocketPathPlaceholder": "Dejar vacío para usar SSH_AUTH_SOCK",
"agentSocketPathHint": "Dejar vacío para detectar automáticamente desde la variable de entorno SSH_AUTH_SOCK, o introduzca una ruta de socket personalizada (p. ej. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Identidad del agente (clave pública)",
"agentIdentityPlaceholder": "Déjelo en blanco para probar todas las claves del agente.",
"agentIdentityHint": "Pegue una clave pública (por ejemplo, ssh-ed25519 AAAA...) para ofrecer únicamente esa clave desde el agente. Se recomienda cuando el agente maneja muchas claves, para evitar exceder el límite de intentos de autenticación (MaxAuthTries) del servidor.",
"shareSshAuthLabel": "Compartir autenticación SSH",
"shareSshAuthDesc": "Entregue a los destinatarios copias cifradas de la autenticación SSH de este host. La credencial personal del destinatario seguirá teniendo prioridad.",
"tailscaleDeviceSelect": "Seleccionar dispositivo Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "No se encontraron credenciales",
"addCredentialBtn": "Añadir credencial",
"updateCredentialBtn": "Actualizar credencial",
"editCredentialAction": "Editar credenciales",
"backToHost": "Volver al anfitrión",
"saveCredentialAsNewBtn": "Guardar como nuevo",
"saveCredentialAsNewDesc": "Crea una nueva credencial con estos cambios y la asigna a este host. La credencial original permanece sin cambios.",
"credentialSavedAsNew": "Guardado como una nueva credencial",
"failedToSaveCredentialAsNew": "No se pudo guardar como una nueva credencial.",
"features": "Características",
"noFolder": "(Sin carpeta)",
"deleteSelected": "Eliminar",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Compacto",
"displayShowTags": "Mostrar etiquetas",
"displayTrayOnClick": "Abrir acciones al hacer clic (en lugar de al pasar el ratón por encima)",
"actionsVisibility": "Acciones del anfitrión",
"actionsVisibilityDesc": "Cuando aparecen los botones de conexión y administración en cada host",
"actionsVisibility": "Visualización de acciones del host",
"actionsVisibilityDesc": "Elige cuándo se expanden las acciones del anfitrión. Selecciona \"Clic\" para restaurar el comportamiento anterior de \"clic para expandir\".",
"actionsAlways": "Siempre visible",
"actionsOnly": "Conéctate solamente, el resto al hacer clic.",
"actionsHover": "Mostrar al pasar el cursor",
"actionsClick": "Mostrar al hacer clic",
"actionsClick": "Haz clic para expandir/contraer",
"openOnDoubleClick": "Abrir con doble clic",
"openOnDoubleClickDesc": "Se requiere un doble clic para abrir una sesión de host en lugar de un solo clic.",
"deletedHost": "Eliminado {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Subido {{name}}",
"uploadFailed": "Error al cargar {{name}}",
"uploadDisabled": "Las cargas están deshabilitadas para esta conexión.",
"driveUnavailable": "Habilite la redirección de unidades RDP antes de cargar archivos.",
"dropToUpload": "Arrastra y suelta los archivos para subirlos"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Nombre de la carpeta",
"fileUploadedSuccessfully": "Archivo \"{{name}}\" subido correctamente",
"failedToUploadFile": "Error al subir el archivo",
"uploadingFolderFiles": "Subiendo {{count}} archivo(s)...",
"uploadedFolderFiles": "Se subieron correctamente los archivos {{count}}",
"uploadedFolderPartial": "Se subieron {{uploaded}} archivos, {{failed}} falló.",
"fileDownloadedSuccessfully": "Archivo \"{{name}}\" descargado correctamente",
"failedToDownloadFile": "Error al descargar el archivo",
"fileCreatedSuccessfully": "Archivo \"{{name}}\" creado correctamente",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} elementos",
"downloadFileSuccess": "Archivo {{name}} descargado correctamente",
"downloadFileFailed": "Error en la descarga",
"downloadingFile": "Descargando {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Mover a {{name}}",
"diffCompareWith": "Comparar diferencias con {{name}}",
"dragOutsideToDownload": "Arrastrar fuera de la ventana para descargar ({{count}} archivos)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Crear automáticamente cuentas para usuarios OIDC/SSO en el primer inicio de sesión (independiente de la opción de registro)",
"oidcSilentLoginDefault": "Inicio de sesión silencioso OIDC por defecto",
"oidcSilentLoginDefaultDesc": "Redirige automáticamente al inicio de sesión OIDC en cada visita, omitiendo por completo el formulario de inicio de sesión",
"oidcSilentLoginDefaultLockedDesc": "Esta configuración está bloqueada por la variable de entorno OIDC_SILENT_LOGIN_DEFAULT y no se puede cambiar aquí.",
"allowPasswordReset": "Permitir restablecimiento de contraseña",
"allowPasswordResetDesc": "Código de restablecimiento a través de los registros de Docker",
"commandHistoryEnabled": "Historial de comandos",
@@ -3317,11 +3337,11 @@
"createRole": "Crear",
"creating": "Creando...",
"exportDatabase": "Exportar base de datos",
"exportDatabaseDesc": "Descargar una copia de seguridad de todos los hosts, credenciales y configuraciones",
"exportDatabaseDesc": "Descarga una copia de seguridad portátil de SQLite de todos los hosts, credenciales y configuraciones.",
"export": "Exportar",
"exporting": "Exportando...",
"importDatabase": "Importar base de datos",
"importDatabaseDesc": "Restaurar desde un archivo de copia de seguridad .sqlite",
"importDatabaseDesc": "Restaurar desde una copia de seguridad .sqlite portátil en cualquier base de datos.",
"importDatabaseSelected": "Seleccionado: {{name}}",
"selectFile": "Seleccionar archivo",
"changeFile": "Cambiar",
@@ -4516,6 +4536,7 @@
"selectProvider": "Seleccione un proveedor",
"providerSettings": "Configuración del proveedor",
"addProvider": "Agregar proveedor",
"editProvider": "Editar proveedor",
"removeProvider": "Eliminar proveedor",
"providerType": "Proveedor",
"providerLabel": "Nombre",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Por ejemplo: llama3.1 o claude-opus-5",
"labelRequired": "Dale un nombre a este proveedor.",
"providerSaveFailed": "No se pudo guardar el proveedor",
"providerUpdated": "Proveedor actualizado",
"providerDeleteFailed": "No se pudo eliminar al proveedor.",
"privateEndpointHint": "Las direcciones de su red privada necesitan que un administrador autorice el acceso al host previamente.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH-avain",
"filterTagsGroup": "Tunnisteet",
"copyDeployCommand": "Kopioi käyttöönottokomento",
"cloneCredentialAction": "Kloonaa tunnistetiedot",
"clonedCredential": "Kloonattu {{name}}",
"clonedCredentialName": "{{name}} (kopio)",
"deleteCredentialAction": "Poista tunnistetiedot",
"editCredentialAction": "Muokkaa tunnistetietoja",
"failedToCloneCredential": "Tunnistetietojen kloonaaminen epäonnistui",
"failedToDeleteCredential": "Tunnuksen poistaminen epäonnistui",
"idBadge": "Henkilöllisyystodistus",
"keyBadge": "AVAIN",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Agentin soketin polku",
"agentSocketPathPlaceholder": "Jätä tyhjäksi käyttääksesi SSH_AUTH_SOCKia",
"agentSocketPathHint": "Jätä tyhjäksi, jos haluat automaattisen tunnistuksen SSH_AUTH_SOCK-ympäristömuuttujasta, tai anna mukautettu soketin polku (esim. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Agentin identiteetti (julkinen avain)",
"agentIdentityPlaceholder": "Jätä tyhjäksi kokeillaksesi kaikkia agentin avaimia",
"agentIdentityHint": "Liitä julkinen avain (esim. ssh-ed25519 AAAA...) tarjotaksesi agentilta vain kyseisen avaimen. Suositellaan, kun agentilla on useita avaimia, jotta palvelimen MaxAuthTries-arvo ei ylity.",
"shareSshAuthLabel": "Jaa SSH-todennus",
"shareSshAuthDesc": "Anna vastaanottajille salatut kopiot tämän isännän SSH-todennuksesta. Vastaanottajan henkilökohtaiset tunnistetiedot ovat edelleen etusijalla.",
"tailscaleDeviceSelect": "Valitse Tailscale-laite",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Tunnuksia ei löytynyt",
"addCredentialBtn": "Lisää tunnistetiedot",
"updateCredentialBtn": "Päivitä tunnistetiedot",
"editCredentialAction": "Muokkaa tunnistetietoja",
"backToHost": "Takaisin isäntään",
"saveCredentialAsNewBtn": "Tallenna uutena",
"saveCredentialAsNewDesc": "Luo uudet tunnistetiedot näillä muutoksilla ja määrittää ne tälle isännälle. Alkuperäiset tunnistetiedot jätetään muuttumattomiksi.",
"credentialSavedAsNew": "Tallennettu uutena tunnistetietona",
"failedToSaveCredentialAsNew": "Uutena tunnistetietona tallentaminen epäonnistui",
"features": "Ominaisuudet",
"noFolder": "(Ei kansiota)",
"deleteSelected": "Poistaa",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Kompakti",
"displayShowTags": "Näytä tunnisteet",
"displayTrayOnClick": "Avaa toiminnot napsauttamalla (hiirellä osoittimen sijaan)",
"actionsVisibility": "Isännän toiminnot",
"actionsVisibilityDesc": "Kun kunkin isännän yhteys- ja hallintapainikkeet tulevat näkyviin",
"actionsVisibility": "Isännän toiminnan näyttö",
"actionsVisibilityDesc": "Valitse, milloin isäntätoiminnot laajenevat. Valitse napsautus palauttaaksesi aiemman napsautuslaajennustoiminnon.",
"actionsAlways": "Aina näkyvissä",
"actionsOnly": "Vain yhdistä, lopeta napsauttamalla",
"actionsHover": "Näytä hiiren osoittimen päällä",
"actionsClick": "Näytä klikattaessa",
"actionsClick": "Napsauta laajentaaksesi/pienentääksesi",
"openOnDoubleClick": "Avaa kaksoisnapsauttamalla",
"openOnDoubleClickDesc": "Vaadi kaksoisnapsautus isäntäistunnon avaamiseen yhden napsautuksen sijaan",
"deletedHost": "Poistettu {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Ladattu {{name}}",
"uploadFailed": "Lataus epäonnistui {{name}}",
"uploadDisabled": "Lähetykset on poistettu käytöstä tällä yhteydellä",
"driveUnavailable": "Ota RDP-aseman uudelleenohjaus käyttöön ennen tiedostojen lataamista",
"dropToUpload": "Pudota tiedostot ladattavaksi"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Kansion nimi",
"fileUploadedSuccessfully": "Tiedosto \"{{name}}\" ladattu onnistuneesti",
"failedToUploadFile": "Tiedoston lataaminen epäonnistui",
"uploadingFolderFiles": "Ladataan {{count}} tiedostoa/tiedostoja...",
"uploadedFolderFiles": "{{count}} tiedostoa ladattu onnistuneesti",
"uploadedFolderPartial": "{{uploaded}} tiedoston/tiedostojen {{failed}} lataaminen epäonnistui",
"fileDownloadedSuccessfully": "Tiedosto \"{{name}}\" ladattu onnistuneesti",
"failedToDownloadFile": "Tiedoston lataaminen epäonnistui",
"fileCreatedSuccessfully": "Tiedosto \"{{name}}\" luotu onnistuneesti",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} kohdetta",
"downloadFileSuccess": "Tiedosto {{name}} ladattu onnistuneesti",
"downloadFileFailed": "Lataus epäonnistui",
"downloadingFile": "Ladataan {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Siirrä kohtaan {{name}}",
"diffCompareWith": "Vertaa eroa kohtaan {{name}}",
"dragOutsideToDownload": "Vedä ikkunan ulkopuolelle ladataksesi ({{count}} tiedostoa)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Luo OIDC/SSO-käyttäjille tilit automaattisesti ensimmäisen kirjautumisen yhteydessä (rekisteröintiasetuksista riippumatta)",
"oidcSilentLoginDefault": "Hiljainen OIDC-kirjautuminen oletuksena",
"oidcSilentLoginDefaultDesc": "Ohjaa automaattisesti OIDC-kirjautumiseen joka käynnillä, ohittaen kirjautumislomakkeen kokonaan",
"oidcSilentLoginDefaultLockedDesc": "Tämä asetus on lukittu OIDC_SILENT_LOGIN_DEFAULT-ympäristömuuttujalla, eikä sitä voi muuttaa täällä.",
"allowPasswordReset": "Salli salasanan palautus",
"allowPasswordResetDesc": "Palauta koodi Docker-lokien kautta",
"commandHistoryEnabled": "Komentohistoria",
@@ -3317,11 +3337,11 @@
"createRole": "Luoda",
"creating": "Luodaan...",
"exportDatabase": "Vie tietokanta",
"exportDatabaseDesc": "Lataa varmuuskopio kaikista isännistä, tunnistetiedoista ja asetuksista",
"exportDatabaseDesc": "Lataa kannettava SQLite-varmuuskopio kaikista isännistä, tunnistetiedoista ja asetuksista",
"export": "Viedä",
"exporting": "Vienti...",
"importDatabase": "Tuo tietokanta",
"importDatabaseDesc": "Palauttaminen .sqlite-varmuuskopiotiedostosta",
"importDatabaseDesc": "Palauta kannettavasta .sqlite-varmuuskopiosta millä tahansa tietokannan taustajärjestelmällä",
"importDatabaseSelected": "Valittu: {{name}}",
"selectFile": "Valitse tiedosto",
"changeFile": "Muuttaa",
@@ -4516,6 +4536,7 @@
"selectProvider": "Valitse palveluntarjoaja",
"providerSettings": "Palveluntarjoajan asetukset",
"addProvider": "Lisää palveluntarjoaja",
"editProvider": "Muokkaa palveluntarjoajaa",
"removeProvider": "Poista palveluntarjoaja",
"providerType": "Palveluntarjoaja",
"providerLabel": "Nimi",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Esimerkiksi: llama3.1 tai claude-opus-5",
"labelRequired": "Anna tälle palveluntarjoajalle nimi",
"providerSaveFailed": "Palveluntarjoajaa ei voitu tallentaa",
"providerUpdated": "Palveluntarjoaja päivitetty",
"providerDeleteFailed": "Palveluntarjoajaa ei voitu poistaa",
"privateEndpointHint": "Yksityisen verkon osoitteiden ylläpitäjän on ensin sallittava isäntä.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "Clé SSH",
"filterTagsGroup": "Étiquettes",
"copyDeployCommand": "Copier la commande de déploiement",
"cloneCredentialAction": "Cloner les informations d'identification",
"clonedCredential": "Cloné {{name}}",
"clonedCredentialName": "{{name}} (copie)",
"deleteCredentialAction": "Supprimer les identifiants",
"editCredentialAction": "Modifier les identifiants",
"failedToCloneCredential": "Échec du clonage des identifiants",
"failedToDeleteCredential": "Échec de la suppression des identifiants",
"idBadge": "IDENTIFIANT",
"keyBadge": "CLÉ",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Chemin du socket de l'agent",
"agentSocketPathPlaceholder": "Laisser vide pour utiliser SSH_AUTH_SOCK",
"agentSocketPathHint": "Laisser vide pour détecter automatiquement à partir de la variable d'environnement SSH_AUTH_SOCK, ou saisir un chemin de socket personnalisé (ex. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Identité de l'agent (clé publique)",
"agentIdentityPlaceholder": "Laisser vide pour essayer toutes les clés de l'agent",
"agentIdentityHint": "Collez une clé publique (par exemple, ssh-ed25519 AAAA...) pour que seule cette clé soit autorisée par l'agent. Cette opération est recommandée lorsque l'agent détient de nombreuses clés, afin d'éviter de dépasser le nombre maximal de tentatives d'authentification (MaxAuthTries) du serveur.",
"shareSshAuthLabel": "Authentification SSH partagée",
"shareSshAuthDesc": "Fournissez aux destinataires des copies chiffrées de l'authentification SSH de cet hôte. Les informations d'identification personnelles du destinataire restent prioritaires.",
"tailscaleDeviceSelect": "Sélectionner un appareil Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Aucun identifiant trouvé",
"addCredentialBtn": "Ajouter un identifiant",
"updateCredentialBtn": "Mettre à jour l'identifiant",
"editCredentialAction": "Modifier les identifiants",
"backToHost": "Retour à l'hôte",
"saveCredentialAsNewBtn": "Enregistrer comme nouveau",
"saveCredentialAsNewDesc": "Crée une nouvelle authentification avec ces modifications et l'attribue à cet hôte. L'authentification d'origine reste inchangée.",
"credentialSavedAsNew": "Enregistré comme nouvelle authentification",
"failedToSaveCredentialAsNew": "Impossible d'enregistrer en tant que nouvelle information d'identification",
"features": "Fonctionnalités",
"noFolder": "(Aucun dossier)",
"deleteSelected": "Supprimer",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Compact",
"displayShowTags": "Afficher les étiquettes",
"displayTrayOnClick": "Ouvrir les actions au clic (au lieu du survol)",
"actionsVisibility": "Actions de l'hôte",
"actionsVisibilityDesc": "Lorsque les boutons de connexion et de gestion de chaque hôte s'affichent",
"actionsVisibility": "Affichage des actions de l'hôte",
"actionsVisibilityDesc": "Choisissez le moment où les actions de l'hôte s'affichent. Sélectionnez « cliquer » pour rétablir le comportement précédent (cliquer pour développer).",
"actionsAlways": "Toujours visible",
"actionsOnly": "Connexion uniquement, le reste se fait en cliquant",
"actionsHover": "Afficher au survol",
"actionsClick": "Afficher au clic",
"actionsClick": "Cliquez pour développer/réduire",
"openOnDoubleClick": "Ouvrir en double-cliquant",
"openOnDoubleClickDesc": "Exiger un double clic pour ouvrir une session hôte au lieu d'un simple clic",
"deletedHost": "{{name}} supprimé",
@@ -1867,6 +1880,7 @@
"uploaded": "Téléchargé {{name}}",
"uploadFailed": "Échec du chargement {{name}}",
"uploadDisabled": "Les chargements sont désactivés pour cette connexion.",
"driveUnavailable": "Activez la redirection du lecteur RDP avant de télécharger les fichiers",
"dropToUpload": "Déposez les fichiers à télécharger"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Nom du dossier",
"fileUploadedSuccessfully": "Fichier \"{{name}}\" téléversé avec succès",
"failedToUploadFile": "Échec du téléversement du fichier",
"uploadingFolderFiles": "Téléchargement du ou des fichiers {{count}}...",
"uploadedFolderFiles": "Fichier(s) {{count}} téléchargé(s) avec succès",
"uploadedFolderPartial": "Le(s) fichier(s) {{uploaded}} téléchargé(s), {{failed}} a échoué",
"fileDownloadedSuccessfully": "Fichier \"{{name}}\" téléchargé avec succès",
"failedToDownloadFile": "Échec du téléchargement du fichier",
"fileCreatedSuccessfully": "Fichier \"{{name}}\" créé avec succès",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} éléments",
"downloadFileSuccess": "Fichier {{name}} téléchargé avec succès",
"downloadFileFailed": "Échec du téléchargement",
"downloadingFile": "Téléchargement de {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Déplacer vers {{name}}",
"diffCompareWith": "Diff avec {{name}}",
"dragOutsideToDownload": "Faites glisser hors de la fenêtre pour télécharger ({{count}} fichiers)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Créer automatiquement des comptes pour les utilisateurs OIDC/SSO lors de la première connexion (indépendant de l'option d'enregistrement)",
"oidcSilentLoginDefault": "Connexion silencieuse OIDC par défaut",
"oidcSilentLoginDefaultDesc": "Rediriger automatiquement vers la connexion OIDC à chaque visite, sans afficher le formulaire de connexion",
"oidcSilentLoginDefaultLockedDesc": "Ce paramètre est verrouillé par la variable d'environnement OIDC_SILENT_LOGIN_DEFAULT et ne peut pas être modifié ici.",
"allowPasswordReset": "Autoriser la réinitialisation du mot de passe",
"allowPasswordResetDesc": "Code de réinitialisation via les journaux Docker",
"commandHistoryEnabled": "Historique des commandes",
@@ -3317,11 +3337,11 @@
"createRole": "Créer",
"creating": "Création...",
"exportDatabase": "Exporter la base de données",
"exportDatabaseDesc": "Télécharger une sauvegarde de tous les hôtes, identifiants et paramètres",
"exportDatabaseDesc": "Téléchargez une sauvegarde SQLite portable de tous les hôtes, identifiants et paramètres",
"export": "Exporter",
"exporting": "Exportation en cours...",
"importDatabase": "Importer la base de données",
"importDatabaseDesc": "Restaurer à partir d'un fichier de sauvegarde .sqlite",
"importDatabaseDesc": "Restauration à partir d'une sauvegarde .sqlite portable sur n'importe quel système de gestion de base de données",
"importDatabaseSelected": "Sélectionné : {{name}}",
"selectFile": "Sélectionner un fichier",
"changeFile": "Modifier",
@@ -4516,6 +4536,7 @@
"selectProvider": "Sélectionnez un fournisseur",
"providerSettings": "Paramètres du fournisseur",
"addProvider": "Ajouter un fournisseur",
"editProvider": "Modifier le fournisseur",
"removeProvider": "Supprimer le fournisseur",
"providerType": "Fournisseur",
"providerLabel": "Nom",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Par exemple : llama3.1 ou claude-opus-5",
"labelRequired": "Donnez un nom à ce fournisseur",
"providerSaveFailed": "Impossible de sauver le fournisseur",
"providerUpdated": "Mise à jour du fournisseur",
"providerDeleteFailed": "Impossible de supprimer le fournisseur",
"privateEndpointHint": "Les adresses de votre réseau privé nécessitent l'autorisation préalable d'un administrateur pour accéder à l'hôte.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "מפתח SSH",
"filterTagsGroup": "תגיות",
"copyDeployCommand": "העתקת פקודת פריסה",
"cloneCredentialAction": "אישור שכפול",
"clonedCredential": "משוכפל {{name}}",
"clonedCredentialName": "{{name}} (העתק)",
"deleteCredentialAction": "מחיקת אישורים",
"editCredentialAction": "עריכת אישורים",
"failedToCloneCredential": "שכפול האישורים נכשל",
"failedToDeleteCredential": "מחיקת האישורים נכשלה",
"idBadge": "תְעוּדַת זֶהוּת",
"keyBadge": "מַפְתֵחַ",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "נתיב שקע הסוכן",
"agentSocketPathPlaceholder": "השאר ריק כדי להשתמש ב-SSH_AUTH_SOCK",
"agentSocketPathHint": "השאר ריק כדי לזהות אוטומטית ממשתנה הסביבה SSH_AUTH_SOCK, או הזן נתיב socket מותאם אישית (לדוגמה /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "זהות סוכן (מפתח ציבורי)",
"agentIdentityPlaceholder": "השאר ריק כדי לנסות את כל המפתחות מהסוכן",
"agentIdentityHint": "הדבק מפתח ציבורי (למשל ssh-ed25519 AAAA...) כדי להציע רק את המפתח הזה מהסוכן. מומלץ כאשר הסוכן מחזיק במספר מפתחות, כדי להימנע מחריגה מ-MaxAuthTries של השרת.",
"shareSshAuthLabel": "שתף אימות SSH",
"shareSshAuthDesc": "תן לנמענים עותקים מוצפנים של אימות SSH של מארח זה. אישורים אישיים של הנמען עדיין מקבלים עדיפות.",
"tailscaleDeviceSelect": "בחר מכשיר Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "לא נמצאו אישורים",
"addCredentialBtn": "הוסף אישור",
"updateCredentialBtn": "עדכון אישורים",
"editCredentialAction": "עריכת אישורים",
"backToHost": "חזרה למארח",
"saveCredentialAsNewBtn": "שמור כחדש",
"saveCredentialAsNewDesc": "יוצר אישור חדש עם שינויים אלה ומקצה אותו למארח זה. האישור המקורי נשאר ללא שינוי.",
"credentialSavedAsNew": "נשמר כאישור חדש",
"failedToSaveCredentialAsNew": "נכשלה השמירה כאישור חדש",
"features": "תכונות",
"noFolder": "(אין תיקייה)",
"deleteSelected": "לִמְחוֹק",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "קוֹמפָּקטִי",
"displayShowTags": "הצג תגיות",
"displayTrayOnClick": "פעולות פתיחה בלחיצה (במקום ריחוף)",
"actionsVisibility": "פעולות מארח",
"actionsVisibilityDesc": "כאשר כפתורי החיבור והניהול בכל מארח מופיעים",
"actionsVisibility": "תצוגת פעולה מארח",
"actionsVisibilityDesc": "בחר מתי פעולות מארח מתרחבות. בחר לחץ כדי לשחזר את התנהגות הלחץ להרחבה הקודמת.",
"actionsAlways": "תמיד גלוי",
"actionsOnly": "חיבור בלבד, מנוחה בלחיצה",
"actionsHover": "הצג בעת ריחוף",
"actionsClick": "הצג בלחיצה",
"actionsClick": "לחץ כדי להרחיב/לכווץ",
"openOnDoubleClick": "פתיחה בלחיצה כפולה",
"openOnDoubleClickDesc": "נדרש לחיצה כפולה כדי לפתוח סשן מארח במקום לחיצה בודדת",
"deletedHost": "נמחק {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "הועלה {{name}}",
"uploadFailed": "נכשלה העלאת {{name}}",
"uploadDisabled": "העלאות מושבתות עבור חיבור זה",
"driveUnavailable": "הפעל ניתוב מחדש של כונן RDP לפני העלאת קבצים",
"dropToUpload": "שחררו קבצים להעלאה"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "שם התיקייה",
"fileUploadedSuccessfully": "הקובץ \"{{name}}\" הועלה בהצלחה",
"failedToUploadFile": "העלאת הקובץ נכשלה",
"uploadingFolderFiles": "מעלה קובץ/ים {{count}}...",
"uploadedFolderFiles": "הקבצים {{count}} הועלו בהצלחה",
"uploadedFolderPartial": "העלאת קובץ/קבצים {{uploaded}} , {{failed}} נכשלה",
"fileDownloadedSuccessfully": "הקובץ \"{{name}}\" הורד בהצלחה",
"failedToDownloadFile": "הורדת הקובץ נכשלה",
"fileCreatedSuccessfully": "הקובץ \"{{name}}\" נוצר בהצלחה",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} פריטים",
"downloadFileSuccess": "הקובץ {{name}} הורד בהצלחה",
"downloadFileFailed": "ההורדה נכשלה",
"downloadingFile": "מוריד {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "מעבר אל {{name}}",
"diffCompareWith": "השוואת הבדלים עם {{name}}",
"dragOutsideToDownload": "גרור את הקבצים אל מחוץ לחלון כדי להוריד אותם ({{count}})",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "יצירה אוטומטית של חשבונות עבור משתמשי OIDC/SSO בכניסה הראשונה (ללא תלות בלחצן הרישום)",
"oidcSilentLoginDefault": "כניסה שקטה ל-OIDC כברירת מחדל",
"oidcSilentLoginDefaultDesc": "הפניה אוטומטית לכניסה ל-OIDC בכל ביקור, דילוג מוחלט על טופס הכניסה",
"oidcSilentLoginDefaultLockedDesc": "הגדרה זו נעולה על ידי משתנה הסביבה OIDC_SILENT_LOGIN_DEFAULT ולא ניתן לשנות אותה כאן.",
"allowPasswordReset": "אפשר איפוס סיסמה",
"allowPasswordResetDesc": "איפוס קוד דרך יומני Docker",
"commandHistoryEnabled": "היסטוריית פקודות",
@@ -3317,11 +3337,11 @@
"createRole": "לִיצוֹר",
"creating": "יוצר...",
"exportDatabase": "ייצוא מסד נתונים",
"exportDatabaseDesc": "הורד גיבוי של כל המארחים, האישורים וההגדרות",
"exportDatabaseDesc": "הורד גיבוי SQLite נייד של כל המארחים, האישורים וההגדרות",
"export": "יְצוּא",
"exporting": "ייצוא...",
"importDatabase": "ייבוא מסד נתונים",
"importDatabaseDesc": "שחזור מקובץ גיבוי .sqlite",
"importDatabaseDesc": "שחזור מגיבוי .sqlite נייד על כל backend של מסד נתונים",
"importDatabaseSelected": "נבחר: {{name}}",
"selectFile": "בחר קובץ",
"changeFile": "לְשַׁנוֹת",
@@ -4516,6 +4536,7 @@
"selectProvider": "בחר ספק",
"providerSettings": "הגדרות ספק",
"addProvider": "הוסף ספק",
"editProvider": "עריכת ספק",
"removeProvider": "הסר ספק",
"providerType": "ספק",
"providerLabel": "שֵׁם",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "לדוגמה: llama3.1 או claude-opus-5",
"labelRequired": "תן שם לספק הזה",
"providerSaveFailed": "לא ניתן היה לשמור את הספק",
"providerUpdated": "הספק עודכן",
"providerDeleteFailed": "לא ניתן היה להסיר את הספק",
"privateEndpointHint": "כתובות ברשת הפרטית שלך דורשות תחילה אישור ממנהל מערכת למארח.",
"providerOllama": "אולמה",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH कुंजी",
"filterTagsGroup": "टैग",
"copyDeployCommand": "डिप्लॉय कमांड कॉपी करें",
"cloneCredentialAction": "क्लोन क्रेडेंशियल",
"clonedCredential": "क्लोन किया गया {{name}}",
"clonedCredentialName": "{{name}} (कॉपी)",
"deleteCredentialAction": "क्रेडेंशियल हटाएं",
"editCredentialAction": "क्रेडेंशियल संपादित करें",
"failedToCloneCredential": "क्रेडेंशियल क्लोन करने में विफल",
"failedToDeleteCredential": "क्रेडेंशियल हटाने में विफल",
"idBadge": "पहचान",
"keyBadge": "चाबी",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "एजेंट सॉकेट पथ",
"agentSocketPathPlaceholder": "SSH_AUTH_SOCK का उपयोग करने के लिए खाली छोड़ें",
"agentSocketPathHint": "SSH_AUTH_SOCK पर्यावरण चर से स्वतः पता लगाने के लिए खाली छोड़ें, या एक कस्टम सॉकेट पथ दर्ज करें (जैसे /run/user/1000/gnupg/S.gpg-agent.ssh)।",
"agentIdentityLabel": "एजेंट पहचान (सार्वजनिक कुंजी)",
"agentIdentityPlaceholder": "एजेंट से सभी कुंजियों को आज़माने के लिए इसे खाली छोड़ दें",
"agentIdentityHint": "एजेंट से केवल वही कुंजी उपलब्ध कराने के लिए एक सार्वजनिक कुंजी (जैसे ssh-ed25519 AAAA...) पेस्ट करें। यह तब अनुशंसित है जब एजेंट के पास कई कुंजियाँ हों, ताकि सर्वर की अधिकतम प्रमाणीकरण सीमा से अधिक न हो।",
"shareSshAuthLabel": "SSH प्रमाणीकरण साझा करें",
"shareSshAuthDesc": "प्राप्तकर्ताओं को इस होस्ट के SSH प्रमाणीकरण की एन्क्रिप्टेड प्रतियां प्रदान करें। प्राप्तकर्ता के व्यक्तिगत क्रेडेंशियल को ही प्राथमिकता दी जाएगी।",
"tailscaleDeviceSelect": "Tailscale डिवाइस चुनें",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "कोई क्रेडेंशियल नहीं मिला",
"addCredentialBtn": "क्रेडेंशियल जोड़ें",
"updateCredentialBtn": "क्रेडेंशियल अपडेट करें",
"editCredentialAction": "क्रेडेंशियल संपादित करें",
"backToHost": "होस्ट पर वापस जाएँ",
"saveCredentialAsNewBtn": "नए के रूप में सहेजें",
"saveCredentialAsNewDesc": "इन परिवर्तनों के साथ एक नया क्रेडेंशियल बनाता है और इसे इस होस्ट को असाइन करता है। मूल क्रेडेंशियल अपरिवर्तित रहता है।",
"credentialSavedAsNew": "नए क्रेडेंशियल के रूप में सहेजा गया",
"failedToSaveCredentialAsNew": "नए क्रेडेंशियल के रूप में सहेजने में विफल",
"features": "विशेषताएं",
"noFolder": "(कोई फ़ोल्डर नहीं)",
"deleteSelected": "हटाएं",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "सघन",
"displayShowTags": "टैग्स दिखाएं",
"displayTrayOnClick": "क्लिक करने पर क्रियाएं खोलें (होवर करने के बजाय)",
"actionsVisibility": "मेज़बान कार्रवाइयाँ",
"actionsVisibilityDesc": "जब प्रत्येक होस्ट पर कनेक्शन और प्रबंधन बटन दिखाई देते हैं",
"actionsVisibility": "होस्ट क्रिया प्रदर्शन",
"actionsVisibilityDesc": "होस्ट क्रियाओं के विस्तार का समय चुनें। पहले की क्लिक-टू-एक्सपैंड प्रक्रिया को बहाल करने के लिए क्लिक का चयन करें।",
"actionsAlways": "सदैव दृश्य",
"actionsOnly": "केवल कनेक्ट करें, क्लिक पर आराम करें",
"actionsHover": "होवर करने पर दिखाएँ",
"actionsClick": "क्लिक करने पर दिखाएँ",
"actionsClick": "विस्तार/संक्षिप्त करने के लिए क्लिक करें",
"openOnDoubleClick": "डबल क्लिक करके खोलें",
"openOnDoubleClickDesc": "होस्ट सेशन खोलने के लिए सिंगल क्लिक के बजाय डबल क्लिक की आवश्यकता होगी।",
"deletedHost": "{{name}} हटा दिया गया",
@@ -1867,6 +1880,7 @@
"uploaded": "अपलोड किया गया {{name}}",
"uploadFailed": "अपलोड करने में विफल {{name}}",
"uploadDisabled": "इस कनेक्शन के लिए अपलोड अक्षम हैं।",
"driveUnavailable": "फ़ाइलें अपलोड करने से पहले RDP ड्राइव रीडायरेक्शन को सक्षम करें",
"dropToUpload": "अपलोड करने के लिए फ़ाइलें ड्रॉप करें"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "फ़ोल्डर का नाम",
"fileUploadedSuccessfully": "फ़ाइल \"{{name}}\" सफलतापूर्वक अपलोड हो गई",
"failedToUploadFile": "फ़ाइल अपलोड करने में विफल",
"uploadingFolderFiles": "{{count}} फ़ाइलें अपलोड की जा रही हैं...",
"uploadedFolderFiles": "{{count}} फ़ाइलें सफलतापूर्वक अपलोड हो गईं",
"uploadedFolderPartial": "{{uploaded}} फ़ाइलें अपलोड की गईं, {{failed}} विफल रहीं",
"fileDownloadedSuccessfully": "फ़ाइल \"{{name}}\" सफलतापूर्वक डाउनलोड हो गई",
"failedToDownloadFile": "फ़ाइल डाउनलोड करने में विफल",
"fileCreatedSuccessfully": "फ़ाइल \"{{name}}\" सफलतापूर्वक बनाई गई",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} आइटम",
"downloadFileSuccess": "फ़ाइल {{name}} सफलतापूर्वक डाउनलोड हुई",
"downloadFileFailed": "डाउनलोड विफल",
"downloadingFile": "डाउनलोड हो रहा है {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "{{name}} में ले जाएँ",
"diffCompareWith": "{{name}} के साथ डिफ तुलना करें",
"dragOutsideToDownload": "डाउनलोड करने के लिए विंडो से बाहर खींचें ({{count}} फ़ाइलें)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "OIDC/SSO उपयोगकर्ताओं के लिए पहली लॉगिन पर स्वचालित रूप से खाते बनाएं (पंजीकरण टॉगल से स्वतंत्र)",
"oidcSilentLoginDefault": "डिफ़ॉल्ट रूप से साइलेंट OIDC लॉगिन",
"oidcSilentLoginDefaultDesc": "हर विज़िट पर स्वचालित रूप से OIDC लॉगिन पर रीडायरेक्ट करें, लॉगिन फ़ॉर्म को पूरी तरह छोड़ दें",
"oidcSilentLoginDefaultLockedDesc": "यह सेटिंग OIDC_SILENT_LOGIN_DEFAULT पर्यावरण चर द्वारा लॉक की गई है और इसे यहां बदला नहीं जा सकता है।",
"allowPasswordReset": "पासवर्ड रीसेट की अनुमति दें",
"allowPasswordResetDesc": "Docker लॉग के माध्यम से रीसेट कोड",
"commandHistoryEnabled": "कमांड इतिहास",
@@ -3317,11 +3337,11 @@
"createRole": "बनाएं",
"creating": "बन रहा है...",
"exportDatabase": "डेटाबेस निर्यात करें",
"exportDatabaseDesc": "सभी होस्टों, क्रेडेंशियलों और सेटिंग्स का बैकअप डाउनलोड करें",
"exportDatabaseDesc": "सभी होस्ट, क्रेडेंशियल और सेटिंग्स का पोर्टेबल SQLite बैकअप डाउनलोड करें",
"export": "निर्यात करें",
"exporting": "निर्यात हो रहा है...",
"importDatabase": "डेटाबेस आयात करें",
"importDatabaseDesc": ".sqlite बैकअप फ़ाइल से पुनर्स्थापित करें",
"importDatabaseDesc": "किसी भी डेटाबेस बैकएंड पर पोर्टेबल .sqlite बैकअप से पुनर्स्थापित करें",
"importDatabaseSelected": "चयनित: {{name}}",
"selectFile": "फ़ाइल चुनें",
"changeFile": "बदलें",
@@ -4516,6 +4536,7 @@
"selectProvider": "एक प्रदाता का चयन करें",
"providerSettings": "प्रदाता सेटिंग्स",
"addProvider": "प्रदाता जोड़ें",
"editProvider": "प्रदाता संपादित करें",
"removeProvider": "प्रदाता को हटाएँ",
"providerType": "प्रदाता",
"providerLabel": "नाम",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "उदाहरण के लिए: llama3.1 या claude-opus-5",
"labelRequired": "इस प्रदाता को एक नाम दें",
"providerSaveFailed": "प्रदाता को बचाया नहीं जा सका",
"providerUpdated": "प्रदाता ने अपडेट किया",
"providerDeleteFailed": "प्रदाता को हटाया नहीं जा सका",
"privateEndpointHint": "आपके निजी नेटवर्क पर मौजूद पतों के लिए पहले एक प्रशासक द्वारा होस्ट को अनुमति देना आवश्यक है।",
"providerOllama": "ओलामा",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH-kulcs",
"filterTagsGroup": "Címkék",
"copyDeployCommand": "Telepítési parancs másolása",
"cloneCredentialAction": "Klónozott hitelesítő adatok",
"clonedCredential": "Klónozott {{name}}",
"clonedCredentialName": "{{name}} (másolat)",
"deleteCredentialAction": "Hitelesítő adatok törlése",
"editCredentialAction": "Hitelesítő adatok szerkesztése",
"failedToCloneCredential": "Nem sikerült klónozni a hitelesítő adatokat",
"failedToDeleteCredential": "Nem sikerült törölni a hitelesítő adatokat",
"idBadge": "azonosító",
"keyBadge": "KULCSFONTOSSÁGÚ",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Ügynök socket útvonala",
"agentSocketPathPlaceholder": "Hagyja üresen az SSH_AUTH_SOCK használatához",
"agentSocketPathHint": "Hagyja üresen az SSH_AUTH_SOCK környezeti változóból történő automatikus felismeréshez, vagy adjon meg egyéni socket elérési utat (pl. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Ügynök azonosítója (nyilvános kulcs)",
"agentIdentityPlaceholder": "Hagyja üresen, ha az ügynöktől származó összes kulcsot ki szeretné próbálni",
"agentIdentityHint": "Illesszen be egy nyilvános kulcsot (pl. ssh-ed25519 AAAA...), hogy az ügynök csak ezt a kulcsot kínálja fel. Ajánlott, ha az ügynök sok kulccsal rendelkezik, hogy elkerülje a szerver MaxAuthTries értékének túllépését.",
"shareSshAuthLabel": "SSH-hitelesítés megosztása",
"shareSshAuthDesc": "Küldje el a címzetteknek a gazdagép SSH-hitelesítésének titkosított másolatait. A címzett személyes hitelesítő adatai továbbra is elsőbbséget élveznek.",
"tailscaleDeviceSelect": "Tailscale eszköz kiválasztása",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Nem találhatók hitelesítő adatok",
"addCredentialBtn": "Hitelesítő adat hozzáadása",
"updateCredentialBtn": "Hitelesítő adatok frissítése",
"editCredentialAction": "Hitelesítő adatok szerkesztése",
"backToHost": "Vissza a Hosthoz",
"saveCredentialAsNewBtn": "Mentés újként",
"saveCredentialAsNewDesc": "Létrehoz egy új hitelesítő adatot ezekkel a módosításokkal, és hozzárendeli azt ehhez a gazdagéphez. Az eredeti hitelesítő adat változatlan marad.",
"credentialSavedAsNew": "Új hitelesítő adatként mentve",
"failedToSaveCredentialAsNew": "Nem sikerült menteni új hitelesítő adatként",
"features": "Jellemzők",
"noFolder": "(Nincs mappa)",
"deleteSelected": "Töröl",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Kompakt",
"displayShowTags": "Címkék megjelenítése",
"displayTrayOnClick": "Műveletek megnyitása kattintással (az egérrel való rámutatás helyett)",
"actionsVisibility": "Gazdagép-műveletek",
"actionsVisibilityDesc": "Amikor megjelennek a csatlakozási és kezelési gombok az egyes gazdagépeken",
"actionsVisibility": "Gazdagép művelet megjelenítése",
"actionsVisibilityDesc": "Válassza ki, hogy mikor bontódnak ki a gazdaműveletek. Válassza a „kattintás” lehetőséget a korábbi „kattintásos kibontás” viselkedés visszaállításához.",
"actionsAlways": "Mindig látható",
"actionsOnly": "Csak csatlakozás, kattintásra maradjon",
"actionsHover": "Mutatás az egérrel való lebegtetéskor",
"actionsClick": "Megjelenítés kattintásra",
"actionsClick": "Kattintson a kibontáshoz/összecsukáshoz",
"openOnDoubleClick": "Megnyitás dupla kattintással",
"openOnDoubleClickDesc": "Dupla kattintás szükséges a gazdamunkamenet megnyitásához egyetlen kattintás helyett",
"deletedHost": "Törölt {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Feltöltve {{name}}",
"uploadFailed": "Nem sikerült feltölteni {{name}}",
"uploadDisabled": "A feltöltések le vannak tiltva ennél a kapcsolatnál.",
"driveUnavailable": "RDP meghajtó átirányításának engedélyezése fájlok feltöltése előtt",
"dropToUpload": "Fájlok húzása a feltöltéshez"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Mappa neve",
"fileUploadedSuccessfully": "A(z) \"{{name}}\" fájl feltöltése sikeresen megtörtént.",
"failedToUploadFile": "Nem sikerült feltölteni a fájlt",
"uploadingFolderFiles": "{{count}} fájl(ok) feltöltése...",
"uploadedFolderFiles": "{{count}} fájl(ok) feltöltve sikeresen",
"uploadedFolderPartial": "{{uploaded}} fájl(ok) feltöltése, {{failed}} sikertelen",
"fileDownloadedSuccessfully": "A(z) \"{{name}}\" fájl sikeresen letöltve",
"failedToDownloadFile": "Nem sikerült letölteni a fájlt",
"fileCreatedSuccessfully": "A(z) \"{{name}}\" fájl sikeresen létrehozva",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} elem",
"downloadFileSuccess": "A {{name}} fájl sikeresen letöltve",
"downloadFileFailed": "Letöltés sikertelen",
"downloadingFile": "{{name}}… letöltése",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Áthelyezés ide: {{name}}",
"diffCompareWith": "Különbség összehasonlítása {{name}}-val",
"dragOutsideToDownload": "Húzd az ablakon kívülre a letöltéshez ({{count}} fájl)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Automatikus fiókok létrehozása OIDC/SSO felhasználók számára az első bejelentkezéskor (a regisztrációs kapcsolótól függetlenül)",
"oidcSilentLoginDefault": "Csendes OIDC bejelentkezés alapértelmezés szerint",
"oidcSilentLoginDefaultDesc": "Automatikus átirányítás az OIDC bejelentkezésre minden látogatáskor, a bejelentkezési űrlap teljes kihagyásával",
"oidcSilentLoginDefaultLockedDesc": "Ezt a beállítást az OIDC_SILENT_LOGIN_DEFAULT környezeti változó zárolja, és itt nem módosítható.",
"allowPasswordReset": "Jelszó-visszaállítás engedélyezése",
"allowPasswordResetDesc": "Kód visszaállítása Docker naplókon keresztül",
"commandHistoryEnabled": "Parancsnoki előzmények",
@@ -3317,11 +3337,11 @@
"createRole": "Teremt",
"creating": "Létrehozás...",
"exportDatabase": "Adatbázis exportálása",
"exportDatabaseDesc": "Töltsön le egy biztonsági másolatot az összes gazdagépről, hitelesítő adatokról és beállításokról",
"exportDatabaseDesc": "Töltsön le egy hordozható SQLite biztonsági mentést az összes gazdagépről, hitelesítő adatokról és beállításokról",
"export": "Export",
"exporting": "Exportálás...",
"importDatabase": "Adatbázis importálása",
"importDatabaseDesc": "Visszaállítás .sqlite biztonsági mentésfájlból",
"importDatabaseDesc": "Visszaállítás hordozható .sqlite biztonsági mentésből bármely adatbázis-háttérrendszeren",
"importDatabaseSelected": "Kiválasztva: {{name}}",
"selectFile": "Fájl kiválasztása",
"changeFile": "Változás",
@@ -4516,6 +4536,7 @@
"selectProvider": "Válasszon szolgáltatót",
"providerSettings": "Szolgáltatói beállítások",
"addProvider": "Szolgáltató hozzáadása",
"editProvider": "Szolgáltató szerkesztése",
"removeProvider": "Szolgáltató eltávolítása",
"providerType": "Szolgáltató",
"providerLabel": "Név",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Például: llama3.1 vagy claude-opus-5",
"labelRequired": "Adjon nevet ennek a szolgáltatónak",
"providerSaveFailed": "Nem sikerült menteni a szolgáltatót",
"providerUpdated": "Szolgáltató frissítve",
"providerDeleteFailed": "Nem sikerült eltávolítani a szolgáltatót",
"privateEndpointHint": "A privát hálózaton lévő címekhez először egy rendszergazdának kell engedélyeznie a gazdagépet.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "Kunci SSH",
"filterTagsGroup": "Tag",
"copyDeployCommand": "Salin perintah penyebaran",
"cloneCredentialAction": "Kloning kredensial",
"clonedCredential": "Dikloning {{name}}",
"clonedCredentialName": "{{name}} (salinan)",
"deleteCredentialAction": "Hapus kredensial",
"editCredentialAction": "Edit kredensial",
"failedToCloneCredential": "Gagal mengkloning kredensial",
"failedToDeleteCredential": "Gagal menghapus kredensial",
"idBadge": "PENGENAL",
"keyBadge": "KUNCI",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Jalur Soket Agen",
"agentSocketPathPlaceholder": "Biarkan kosong untuk menggunakan SSH_AUTH_SOCK",
"agentSocketPathHint": "Biarkan kosong untuk mendeteksi secara otomatis dari variabel lingkungan SSH_AUTH_SOCK, atau masukkan jalur soket khusus (misalnya /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Identitas Agen (Kunci Publik)",
"agentIdentityPlaceholder": "Biarkan kosong untuk mencoba semua tombol dari agen.",
"agentIdentityHint": "Tempelkan kunci publik (misalnya ssh-ed25519 AAAA...) untuk hanya menawarkan kunci tersebut dari agen. Disarankan jika agen memegang banyak kunci, untuk menghindari melebihi MaxAuthTries server.",
"shareSshAuthLabel": "Bagikan Otentikasi SSH",
"shareSshAuthDesc": "Berikan kepada penerima salinan terenkripsi dari otentikasi SSH host ini. Kredensial pribadi penerima tetap diutamakan.",
"tailscaleDeviceSelect": "Pilih perangkat Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Tidak ditemukan kredensial.",
"addCredentialBtn": "Tambahkan Kredensial",
"updateCredentialBtn": "Perbarui Kredensial",
"editCredentialAction": "Edit kredensial",
"backToHost": "Kembali ke Host",
"saveCredentialAsNewBtn": "Simpan sebagai Baru",
"saveCredentialAsNewDesc": "Membuat kredensial baru dengan perubahan ini dan menetapkannya ke host ini. Kredensial asli dibiarkan tidak berubah.",
"credentialSavedAsNew": "Disimpan sebagai kredensial baru",
"failedToSaveCredentialAsNew": "Gagal menyimpan sebagai kredensial baru.",
"features": "Fitur",
"noFolder": "(Tidak ada folder)",
"deleteSelected": "Menghapus",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Kompak",
"displayShowTags": "Tampilkan tag",
"displayTrayOnClick": "Aksi terbuka saat diklik (bukan saat diarahkan kursor)",
"actionsVisibility": "Tindakan tuan rumah",
"actionsVisibilityDesc": "Saat tombol koneksi dan manajemen pada setiap host muncul",
"actionsVisibility": "Tampilan tindakan host",
"actionsVisibilityDesc": "Pilih kapan tindakan host diperluas. Pilih klik untuk mengembalikan perilaku klik-untuk-memperluas sebelumnya.",
"actionsAlways": "Selalu terlihat",
"actionsOnly": "Hanya perlu terhubung, sisanya dengan sekali klik.",
"actionsHover": "Tampilkan saat kursor diarahkan ke atasnya.",
"actionsClick": "Tampilkan saat diklik",
"actionsClick": "Klik untuk memperbesar/memperkecil",
"openOnDoubleClick": "Buka dengan klik dua kali",
"openOnDoubleClickDesc": "Membutuhkan klik ganda untuk membuka sesi host, bukan klik tunggal.",
"deletedHost": "Dihapus {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Diunggah {{name}}",
"uploadFailed": "Gagal mengunggah {{name}}",
"uploadDisabled": "Unggahan dinonaktifkan untuk koneksi ini.",
"driveUnavailable": "Aktifkan pengalihan drive RDP sebelum mengunggah file.",
"dropToUpload": "Seret file untuk diunggah"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Nama Folder",
"fileUploadedSuccessfully": "Berkas \"{{name}}\" berhasil diunggah",
"failedToUploadFile": "Gagal mengunggah file",
"uploadingFolderFiles": "Mengunggah {{count}} berkas...",
"uploadedFolderFiles": "Berhasil mengunggah {{count}} berkas",
"uploadedFolderPartial": "Pengunggahan {{uploaded}} berkas, {{failed}} gagal",
"fileDownloadedSuccessfully": "File \"{{name}}\" berhasil diunduh",
"failedToDownloadFile": "Gagal mengunduh file",
"fileCreatedSuccessfully": "File \"{{name}}\" berhasil dibuat",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} item",
"downloadFileSuccess": "File {{name}} berhasil diunduh",
"downloadFileFailed": "Pengunduhan gagal",
"downloadingFile": "Mengunduh {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Pindah ke {{name}}",
"diffCompareWith": "Bandingkan perbedaan dengan {{name}}",
"dragOutsideToDownload": "Seret ke luar jendela untuk mengunduh ({{count}} file)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Buat akun secara otomatis untuk pengguna OIDC/SSO pada saat login pertama (terlepas dari tombol pendaftaran)",
"oidcSilentLoginDefault": "Login OIDC Senyap Secara Default",
"oidcSilentLoginDefaultDesc": "Secara otomatis mengarahkan ke halaman login OIDC pada setiap kunjungan, melewati formulir login sepenuhnya.",
"oidcSilentLoginDefaultLockedDesc": "Pengaturan ini dikunci oleh variabel lingkungan OIDC_SILENT_LOGIN_DEFAULT dan tidak dapat diubah di sini.",
"allowPasswordReset": "Izinkan Reset Kata Sandi",
"allowPasswordResetDesc": "Reset kode melalui log Docker",
"commandHistoryEnabled": "Riwayat Perintah",
@@ -3317,11 +3337,11 @@
"createRole": "Membuat",
"creating": "Membuat...",
"exportDatabase": "Ekspor Basis Data",
"exportDatabaseDesc": "Unduh cadangan semua host, kredensial, dan pengaturan.",
"exportDatabaseDesc": "Unduh cadangan SQLite portabel dari semua host, kredensial, dan pengaturan.",
"export": "Ekspor",
"exporting": "Mengekspor...",
"importDatabase": "Impor Basis Data",
"importDatabaseDesc": "Pulihkan dari file cadangan .sqlite",
"importDatabaseDesc": "Pulihkan dari cadangan .sqlite portabel pada backend basis data apa pun.",
"importDatabaseSelected": "Dipilih: {{name}}",
"selectFile": "Pilih File",
"changeFile": "Mengubah",
@@ -4516,6 +4536,7 @@
"selectProvider": "Pilih penyedia",
"providerSettings": "Pengaturan penyedia",
"addProvider": "Tambahkan penyedia",
"editProvider": "Penyedia pengeditan",
"removeProvider": "Hapus penyedia",
"providerType": "Penyedia",
"providerLabel": "Nama",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Misalnya: llama3.1 atau claude-opus-5",
"labelRequired": "Beri nama penyedia ini",
"providerSaveFailed": "Penyedia layanan tidak dapat disimpan.",
"providerUpdated": "Penyedia diperbarui",
"providerDeleteFailed": "Penyedia layanan tidak dapat dihapus.",
"privateEndpointHint": "Alamat-alamat di jaringan pribadi Anda memerlukan izin administrator terlebih dahulu untuk host tersebut.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "Chiave SSH",
"filterTagsGroup": "Tag",
"copyDeployCommand": "Copia il comando di distribuzione",
"cloneCredentialAction": "Credenziali di clonazione",
"clonedCredential": "Clonato {{name}}",
"clonedCredentialName": "{{name}} (copia)",
"deleteCredentialAction": "Elimina le credenziali",
"editCredentialAction": "Modifica credenziali",
"failedToCloneCredential": "Impossibile clonare le credenziali",
"failedToDeleteCredential": "Impossibile eliminare le credenziali",
"idBadge": "ID",
"keyBadge": "CHIAVE",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Percorso socket agente",
"agentSocketPathPlaceholder": "Lascia vuoto per usare SSH_AUTH_SOCK",
"agentSocketPathHint": "Lascia vuoto per il rilevamento automatico dalla variabile d'ambiente SSH_AUTH_SOCK o inserisci un percorso socket personalizzato (es. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Identità dell'agente (chiave pubblica)",
"agentIdentityPlaceholder": "Lascia vuoto per provare tutte le chiavi dell'agente",
"agentIdentityHint": "Incolla una chiave pubblica (ad esempio ssh-ed25519 AAAA...) per offrire solo quella chiave dall'agente. Consigliato quando l'agente possiede molte chiavi, per evitare di superare il limite di MaxAuthTries del server.",
"shareSshAuthLabel": "Autenticazione SSH condivisa",
"shareSshAuthDesc": "Fornite ai destinatari copie crittografate delle credenziali di autenticazione SSH di questo host. Le credenziali personali del destinatario hanno comunque la precedenza.",
"tailscaleDeviceSelect": "Seleziona dispositivo Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Nessuna credenziale trovata",
"addCredentialBtn": "Aggiungi credenziale",
"updateCredentialBtn": "Aggiorna credenziale",
"editCredentialAction": "Modifica credenziali",
"backToHost": "Torna all'host",
"saveCredentialAsNewBtn": "Salva come nuovo",
"saveCredentialAsNewDesc": "Crea una nuova credenziale con queste modifiche e la assegna a questo host. La credenziale originale rimane invariata.",
"credentialSavedAsNew": "Salvato come nuova credenziale",
"failedToSaveCredentialAsNew": "Impossibile salvare le credenziali come nuove.",
"features": "Funzionalità",
"noFolder": "(Nessuna cartella)",
"deleteSelected": "Elimina",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Compatto",
"displayShowTags": "Mostra tag",
"displayTrayOnClick": "Apri le azioni al clic (anziché al passaggio del mouse)",
"actionsVisibility": "Azioni dell'host",
"actionsVisibilityDesc": "Quando vengono visualizzati i pulsanti di connessione e gestione su ciascun host",
"actionsVisibility": "Visualizzazione delle azioni host",
"actionsVisibilityDesc": "Scegli quando le azioni dell'host si espandono. Seleziona \"clicca\" per ripristinare il precedente comportamento di espansione tramite clic.",
"actionsAlways": "Sempre visibile",
"actionsOnly": "Connetti solo, il resto al clic",
"actionsHover": "Mostra al passaggio del mouse",
"actionsClick": "Mostra al clic",
"actionsClick": "Fai clic per espandere/comprimere",
"openOnDoubleClick": "Apri con un doppio clic",
"openOnDoubleClickDesc": "Richiedere un doppio clic per aprire una sessione host anziché un singolo clic",
"deletedHost": "{{name}} eliminato",
@@ -1867,6 +1880,7 @@
"uploaded": "Caricato {{name}}",
"uploadFailed": "Impossibile caricare {{name}}",
"uploadDisabled": "Il caricamento è disabilitato per questa connessione.",
"driveUnavailable": "Abilita il reindirizzamento delle unità RDP prima di caricare i file",
"dropToUpload": "Trascina i file da caricare"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Nome cartella",
"fileUploadedSuccessfully": "File \"{{name}}\" caricato con successo",
"failedToUploadFile": "Caricamento file non riuscito",
"uploadingFolderFiles": "Caricamento del/dei file {{count}} in corso...",
"uploadedFolderFiles": "File caricati con successo {{count}}",
"uploadedFolderPartial": "Uploaded {{uploaded}} file(s), {{failed}} failed",
"fileDownloadedSuccessfully": "File \"{{name}}\" scaricato con successo",
"failedToDownloadFile": "Scaricamento file non riuscito",
"fileCreatedSuccessfully": "File \"{{name}}\" creato con successo",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} elementi",
"downloadFileSuccess": "File {{name}} scaricato con successo",
"downloadFileFailed": "Download non riuscito",
"downloadingFile": "Download in corso {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Sposta in {{name}}",
"diffCompareWith": "Diff con {{name}}",
"dragOutsideToDownload": "Trascina fuori dalla finestra per scaricare ({{count}} file)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Crea automaticamente account per utenti OIDC/SSO al primo accesso (indipendentemente dall'opzione di registrazione)",
"oidcSilentLoginDefault": "Accesso OIDC silenzioso predefinito",
"oidcSilentLoginDefaultDesc": "Reindirizza automaticamente all'accesso OIDC ad ogni visita, saltando completamente il modulo di accesso",
"oidcSilentLoginDefaultLockedDesc": "Questa impostazione è bloccata dalla variabile d'ambiente OIDC_SILENT_LOGIN_DEFAULT e non può essere modificata qui.",
"allowPasswordReset": "Consenti reset password",
"allowPasswordResetDesc": "Codice di reset tramite log Docker",
"commandHistoryEnabled": "Cronologia comandi",
@@ -3317,11 +3337,11 @@
"createRole": "Crea",
"creating": "Creazione...",
"exportDatabase": "Esporta database",
"exportDatabaseDesc": "Scarica un backup di tutti gli host, credenziali e impostazioni",
"exportDatabaseDesc": "Scarica un backup SQLite portatile di tutti gli host, le credenziali e le impostazioni",
"export": "Esporta",
"exporting": "Esportazione...",
"importDatabase": "Importa database",
"importDatabaseDesc": "Ripristina da un file di backup .sqlite",
"importDatabaseDesc": "Ripristina da un backup .sqlite portatile su qualsiasi database backend",
"importDatabaseSelected": "Selezionato: {{name}}",
"selectFile": "Seleziona file",
"changeFile": "Cambia",
@@ -4516,6 +4536,7 @@
"selectProvider": "Seleziona un fornitore",
"providerSettings": "Impostazioni del provider",
"addProvider": "Aggiungi fornitore",
"editProvider": "Fornitore di modifica",
"removeProvider": "Rimuovi il fornitore",
"providerType": "Fornitore",
"providerLabel": "Nome",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Ad esempio: llama3.1 o claude-opus-5",
"labelRequired": "Assegna un nome a questo fornitore",
"providerSaveFailed": "Impossibile salvare il provider",
"providerUpdated": "Fornitore aggiornato",
"providerDeleteFailed": "Impossibile rimuovere il provider",
"privateEndpointHint": "Gli indirizzi sulla tua rete privata richiedono prima l'autorizzazione di un amministratore per l'host.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH 鍵",
"filterTagsGroup": "タグ",
"copyDeployCommand": "デプロイコマンドをコピーする",
"cloneCredentialAction": "クローン認証情報",
"clonedCredential": "クローン {{name}}",
"clonedCredentialName": "{{name}} (コピー)",
"deleteCredentialAction": "認証情報を削除します",
"editCredentialAction": "認証情報を編集する",
"failedToCloneCredential": "認証情報の複製に失敗しました",
"failedToDeleteCredential": "認証情報の削除に失敗しました",
"idBadge": "ID",
"keyBadge": "鍵",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "エージェントソケットパス",
"agentSocketPathPlaceholder": "SSH_AUTH_SOCKを使用するには空のままにしてください",
"agentSocketPathHint": "SSH_AUTH_SOCK環境変数から自動検出するには空のままにするか、カスタムソケットパスを入力してください(例: /run/user/1000/gnupg/S.gpg-agent.ssh)。",
"agentIdentityLabel": "エージェントID(公開鍵)",
"agentIdentityPlaceholder": "エージェントからのすべてのキーを試す場合は空欄のままにしてください。",
"agentIdentityHint": "エージェントから提供される鍵のみを指定するには、公開鍵(例:ssh-ed25519 AAAA...)を貼り付けてください。エージェントが多数の鍵を保持している場合に、サーバーの MaxAuthTries を超えないようにするために推奨されます。",
"shareSshAuthLabel": "SSH認証を共有する",
"shareSshAuthDesc": "受信者には、このホストのSSH認証情報を暗号化して送信します。受信者の個人認証情報が優先されます。",
"tailscaleDeviceSelect": "Tailscaleデバイスを選択",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "認証情報が見つかりません",
"addCredentialBtn": "認証情報を追加",
"updateCredentialBtn": "認証情報を更新",
"editCredentialAction": "認証情報を編集する",
"backToHost": "ホストに戻る",
"saveCredentialAsNewBtn": "新規として保存",
"saveCredentialAsNewDesc": "これらの変更を加えた新しい認証情報を作成し、このホストに割り当てます。元の認証情報は変更されません。",
"credentialSavedAsNew": "新しい認証情報として保存されました",
"failedToSaveCredentialAsNew": "新しい認証情報として保存できませんでした",
"features": "機能",
"noFolder": "(フォルダなし)",
"deleteSelected": "削除",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "コンパクト",
"displayShowTags": "タグを表示",
"displayTrayOnClick": "クリック時にアクションを開く(ホバー時ではなく)",
"actionsVisibility": "ホストアクション",
"actionsVisibilityDesc": "ホスト上の接続ボタンと管理ボタンが表示されたら",
"actionsVisibility": "ホストアクションの表示",
"actionsVisibilityDesc": "ホストアクションを展開するタイミングを選択します。クリックを選択すると、以前のクリックして展開する動作に戻ります。",
"actionsAlways": "常に見える",
"actionsOnly": "接続のみ、クリックで終了",
"actionsHover": "マウスオーバー時に表示",
"actionsClick": "クリックで表示",
"actionsClick": "クリックして展開/折りたたみ",
"openOnDoubleClick": "ダブルクリックで開く",
"openOnDoubleClickDesc": "ホストセッションを開く際に、シングルクリックではなくダブルクリックを要求する。",
"deletedHost": "{{name}} を削除しました",
@@ -1867,6 +1880,7 @@
"uploaded": "アップロード済み {{name}}",
"uploadFailed": "アップロードに失敗しました {{name}}",
"uploadDisabled": "この接続ではアップロードが無効になっています。",
"driveUnavailable": "ファイルをアップロードする前に、RDPドライブリダイレクトを有効にしてください。",
"dropToUpload": "アップロードするファイルをドロップしてください"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "フォルダ名",
"fileUploadedSuccessfully": "ファイル \"{{name}}\" のアップロードが完了しました",
"failedToUploadFile": "ファイルのアップロードに失敗しました",
"uploadingFolderFiles": "{{count}} 個のファイルをアップロードしています...",
"uploadedFolderFiles": "{{count}} 個のファイルが正常にアップロードされました",
"uploadedFolderPartial": "Uploaded {{uploaded}} file(s), {{failed}} failed",
"fileDownloadedSuccessfully": "ファイル \"{{name}}\" のダウンロードが完了しました",
"failedToDownloadFile": "ファイルのダウンロードに失敗しました",
"fileCreatedSuccessfully": "ファイル \"{{name}}\" が正常に作成されました",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{count}} アイテムを{{operation}}しました",
"downloadFileSuccess": "ファイル {{name}} のダウンロードが完了しました",
"downloadFileFailed": "ダウンロードに失敗しました",
"downloadingFile": "ダウンロード中 {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "{{name}} に移動",
"diffCompareWith": "{{name}} と差分比較",
"dragOutsideToDownload": "ウィンドウ外にドラッグしてダウンロード ({{count}} ファイル)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "初回ログイン時にOIDC/SSOユーザーのアカウントを自動作成します(登録のトグルとは独立)",
"oidcSilentLoginDefault": "デフォルトでサイレントOIDCログイン",
"oidcSilentLoginDefaultDesc": "アクセス時に自動でOIDCログインにリダイレクトし、ログインフォームを完全にスキップします",
"oidcSilentLoginDefaultLockedDesc": "この設定は環境変数 OIDC_SILENT_LOGIN_DEFAULT によってロックされているため、ここで変更することはできません。",
"allowPasswordReset": "パスワードリセットを許可",
"allowPasswordResetDesc": "リセットコードをDockerログから取得",
"commandHistoryEnabled": "コマンド履歴",
@@ -3317,11 +3337,11 @@
"createRole": "作成",
"creating": "作成中...",
"exportDatabase": "データベースをエクスポート",
"exportDatabaseDesc": "すべてのホスト、認証情報、設定のバックアップをダウンロードします",
"exportDatabaseDesc": "すべてのホスト、認証情報、および設定のポータブルな SQLite バックアップをダウンロードします",
"export": "エクスポート",
"exporting": "エクスポート中...",
"importDatabase": "データベースをインポート",
"importDatabaseDesc": ".sqlite バックアップファイルから復元します。",
"importDatabaseDesc": "任意のデータベースバックエンド上のポータブルな.sqliteバックアップから復元します。",
"importDatabaseSelected": "選択済み: {{name}}",
"selectFile": "ファイルを選択",
"changeFile": "変更",
@@ -4516,6 +4536,7 @@
"selectProvider": "プロバイダーを選択してください",
"providerSettings": "プロバイダー設定",
"addProvider": "プロバイダーを追加",
"editProvider": "プロバイダーを編集",
"removeProvider": "プロバイダーを削除",
"providerType": "プロバイダー",
"providerLabel": "名前",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "例えば、llama3.1 や claude-opus-5 など。",
"labelRequired": "このプロバイダーに名前を付けてください",
"providerSaveFailed": "プロバイダーを保存できませんでした",
"providerUpdated": "プロバイダーが更新されました",
"providerDeleteFailed": "プロバイダーを削除できませんでした",
"privateEndpointHint": "プライベートネットワーク上のアドレスを使用するには、まず管理者がホストを許可する必要があります。",
"providerOllama": "オラマ",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH 키",
"filterTagsGroup": "태그",
"copyDeployCommand": "복사 배포 명령",
"cloneCredentialAction": "자격 증명 복제",
"clonedCredential": "복제됨 {{name}}",
"clonedCredentialName": "{{name}} (복사)",
"deleteCredentialAction": "자격 증명 삭제",
"editCredentialAction": "자격 증명 수정",
"failedToCloneCredential": "자격 증명 복제에 실패했습니다.",
"failedToDeleteCredential": "자격 증명 삭제에 실패했습니다.",
"idBadge": "ID",
"keyBadge": "열쇠",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "에이전트 소켓 경로",
"agentSocketPathPlaceholder": "SSH_AUTH_SOCK을 사용하려면 비워 두세요.",
"agentSocketPathHint": "비워 두면 SSH_AUTH_SOCK 환경 변수에서 자동 감지합니다. 또는 사용자 지정 소켓 경로를 입력하세요(예: /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "에이전트 ID(공개 키)",
"agentIdentityPlaceholder": "에이전트에서 제공하는 모든 키를 시도하려면 비워 두십시오.",
"agentIdentityHint": "에이전트에서 해당 키만 제공하려면 공개 키(예: ssh-ed25519 AAAA...)를 붙여넣으세요. 에이전트에 키가 많은 경우 서버의 MaxAuthTries 제한을 초과하지 않도록 하는 데 권장됩니다.",
"shareSshAuthLabel": "SSH 인증 공유",
"shareSshAuthDesc": "수신자에게 이 호스트의 SSH 인증 정보를 암호화한 사본을 제공하십시오. 수신자의 개인 자격 증명이 여전히 우선합니다.",
"tailscaleDeviceSelect": "Tailscale 기기 선택",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "자격증명을 찾을 수 없습니다",
"addCredentialBtn": "자격증명 추가",
"updateCredentialBtn": "자격증명 업데이트",
"editCredentialAction": "자격 증명 수정",
"backToHost": "호스트 페이지로 돌아가기",
"saveCredentialAsNewBtn": "새 상품으로 저장",
"saveCredentialAsNewDesc": "이러한 변경 사항을 반영하여 새 자격 증명을 생성하고 해당 호스트에 할당합니다. 기존 자격 증명은 변경되지 않습니다.",
"credentialSavedAsNew": "새 자격 증명으로 저장되었습니다",
"failedToSaveCredentialAsNew": "새 자격 증명으로 저장하는 데 실패했습니다.",
"features": "기능",
"noFolder": "(폴더 없음)",
"deleteSelected": "삭제",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "콤팩트",
"displayShowTags": "태그 표시",
"displayTrayOnClick": "마우스 오버 대신 클릭 시 실행 항목을 엽니다.",
"actionsVisibility": "호스트 작업",
"actionsVisibilityDesc": "호스트에 연결 및 관리 버튼이 표시될 때",
"actionsVisibility": "호스트 액션 표시",
"actionsVisibilityDesc": "호스트 작업이 확장되는 시점을 선택하세요. '클릭'을 선택하면 이전의 클릭 확장 동작으로 복원됩니다.",
"actionsAlways": "항상 보이는",
"actionsOnly": "연결만 하시고, 나머지는 클릭 한 번으로!",
"actionsHover": "마우스 오버 시 표시",
"actionsClick": "클릭하면 표시됩니다",
"actionsClick": "클릭하여 펼치기/접기",
"openOnDoubleClick": "두 번 클릭하면 열립니다",
"openOnDoubleClickDesc": "호스트 세션을 열 때 한 번 클릭하는 대신 두 번 클릭하도록 변경합니다.",
"deletedHost": "{{name}} 삭제됨",
@@ -1867,6 +1880,7 @@
"uploaded": "업로드됨 {{name}}",
"uploadFailed": "{{name}} 업로드에 실패했습니다.",
"uploadDisabled": "이 연결에서는 업로드가 비활성화되어 있습니다.",
"driveUnavailable": "파일을 업로드하기 전에 RDP 드라이브 리디렉션을 활성화하십시오.",
"dropToUpload": "업로드할 파일을 드롭하세요"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "폴더 이름",
"fileUploadedSuccessfully": "파일 \"{{name}}\" 업로드 완료",
"failedToUploadFile": "파일 업로드 실패",
"uploadingFolderFiles": "{{count}} 파일을 업로드하는 중...",
"uploadedFolderFiles": "{{count}} 파일을 성공적으로 업로드했습니다.",
"uploadedFolderPartial": "업로드된 파일 {{uploaded}} 개 중 {{failed}} 개에 실패했습니다.",
"fileDownloadedSuccessfully": "파일 \"{{name}}\" 다운로드 완료",
"failedToDownloadFile": "파일 다운로드 실패",
"fileCreatedSuccessfully": "파일 \"{{name}}\" 생성 완료",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{count}}개 항목 {{operation}}",
"downloadFileSuccess": "파일 {{name}} 다운로드 성공",
"downloadFileFailed": "다운로드 실패",
"downloadingFile": "{{name}}… 다운로드 중",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "{{name}}로 이동",
"diffCompareWith": "{{name}}와/과 Diff 비교",
"dragOutsideToDownload": "창 밖으로 드래그하여 다운로드 ({{count}}개 파일)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "최초 로그인 시 OIDC/SSO 사용자 계정 자동 생성 (등록 토글과 무관)",
"oidcSilentLoginDefault": "기본 자동 OIDC 로그인",
"oidcSilentLoginDefaultDesc": "방문 시마다 OIDC 로그인으로 자동 리디렉션, 로그인 양식 완전히 건너뜀",
"oidcSilentLoginDefaultLockedDesc": "이 설정은 OIDC_SILENT_LOGIN_DEFAULT 환경 변수에 의해 잠겨 있으므로 여기에서 변경할 수 없습니다.",
"allowPasswordReset": "비밀번호 재설정 허용",
"allowPasswordResetDesc": "Docker 로그를 통해 재설정 코드 확인",
"commandHistoryEnabled": "명령어 히스토리",
@@ -3317,11 +3337,11 @@
"createRole": "생성",
"creating": "생성 중...",
"exportDatabase": "데이터베이스 내보내기",
"exportDatabaseDesc": "모든 호스트, 자격 증명 및 설정의 백업을 다운로드합니다.",
"exportDatabaseDesc": "모든 호스트, 자격 증명 및 설정이 포함된 휴대용 SQLite 백업 파일을 다운로드하세요.",
"export": "내보내기",
"exporting": "내보내는 중...",
"importDatabase": "데이터베이스 가져오기",
"importDatabaseDesc": ".sqlite 백업 파일에서 복원합니다.",
"importDatabaseDesc": "휴대용 .sqlite 백업에서 모든 데이터베이스 백엔드로 복원할 수 있습니다.",
"importDatabaseSelected": "선택됨: {{name}}",
"selectFile": "파일 선택",
"changeFile": "변경",
@@ -4516,6 +4536,7 @@
"selectProvider": "제공업체를 선택하세요",
"providerSettings": "공급자 설정",
"addProvider": "공급자 추가",
"editProvider": "제공자 수정",
"removeProvider": "제공업체를 제거하세요",
"providerType": "공급자",
"providerLabel": "이름",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "예를 들어: llama3.1 또는 claude-opus-5",
"labelRequired": "이 제공업체에 이름을 지정하세요",
"providerSaveFailed": "공급자를 저장할 수 없습니다.",
"providerUpdated": "제공업체 업데이트됨",
"providerDeleteFailed": "공급자를 제거할 수 없습니다.",
"privateEndpointHint": "개인 네트워크의 주소에 접근하려면 관리자가 먼저 호스트를 허용해야 합니다.",
"providerOllama": "올라마",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH-sleutel",
"filterTagsGroup": "Tags",
"copyDeployCommand": "Kopieer de implementatieopdracht",
"cloneCredentialAction": "Kopieer inloggegevens",
"clonedCredential": "Gekloond {{name}}",
"clonedCredentialName": "{{name}} (kopiëren)",
"deleteCredentialAction": "Verwijder inloggegevens",
"editCredentialAction": "Inloggegevens bewerken",
"failedToCloneCredential": "Het klonen van de inloggegevens is mislukt.",
"failedToDeleteCredential": "Het verwijderen van de inloggegevens is mislukt.",
"idBadge": "ID",
"keyBadge": "SLEUTEL",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Agent Socket Path",
"agentSocketPathPlaceholder": "Laat dit veld leeg om SSH_AUTH_SOCK te gebruiken.",
"agentSocketPathHint": "Laat dit veld leeg om automatisch te detecteren via de omgevingsvariabele SSH_AUTH_SOCK, of voer een aangepast socketpad in (bijv. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Agentidentiteit (openbare sleutel)",
"agentIdentityPlaceholder": "Laat dit veld leeg om alle sleutels van de agent te proberen.",
"agentIdentityHint": "Plak hier een openbare sleutel (bijv. ssh-ed25519 AAAA...) zodat alleen die sleutel door de agent wordt aangeboden. Dit wordt aanbevolen wanneer de agent veel sleutels beheert, om te voorkomen dat het maximale aantal autorisaties (MaxAuthTries) van de server wordt overschreden.",
"shareSshAuthLabel": "Deel SSH-authenticatie",
"shareSshAuthDesc": "Geef ontvangers versleutelde kopieën van de SSH-authenticatiegegevens van deze host. De persoonlijke inloggegevens van de ontvanger hebben echter nog steeds voorrang.",
"tailscaleDeviceSelect": "Selecteer het Tailscale-apparaat",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Geen inloggegevens gevonden",
"addCredentialBtn": "Voeg inloggegevens toe",
"updateCredentialBtn": "Inloggegevens bijwerken",
"editCredentialAction": "Inloggegevens bewerken",
"backToHost": "Terug naar de host",
"saveCredentialAsNewBtn": "Opslaan als nieuw",
"saveCredentialAsNewDesc": "Hiermee wordt een nieuwe referentie aangemaakt met deze wijzigingen en toegewezen aan deze host. De oorspronkelijke referentie blijft ongewijzigd.",
"credentialSavedAsNew": "Opgeslagen als een nieuwe referentie",
"failedToSaveCredentialAsNew": "Opslaan als nieuwe inloggegevens is mislukt.",
"features": "Functies",
"noFolder": "(Geen map)",
"deleteSelected": "Verwijderen",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Compact",
"displayShowTags": "Toon tags",
"displayTrayOnClick": "Acties openen bij klikken (in plaats van bij hoveren)",
"actionsVisibility": "Hostacties",
"actionsVisibilityDesc": "Wanneer de verbindings- en beheerknoppen op elke host verschijnen",
"actionsVisibility": "Weergave van hostacties",
"actionsVisibilityDesc": "Kies wanneer hostacties worden uitgevouwen. Selecteer 'klikken' om het eerdere 'klikken om uit te vouwen'-gedrag te herstellen.",
"actionsAlways": "Altijd zichtbaar",
"actionsOnly": "Alleen verbinden, de rest is klikken.",
"actionsHover": "Weergeven bij hoveren",
"actionsClick": "Weergeven bij klikken",
"actionsClick": "Klik om uit te vouwen/in te klappen",
"openOnDoubleClick": "Openen bij dubbelklikken",
"openOnDoubleClickDesc": "Vereis een dubbelklik in plaats van een enkele klik om een hostsessie te openen.",
"deletedHost": "Verwijderd {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Geüpload {{name}}",
"uploadFailed": "Uploaden mislukt {{name}}",
"uploadDisabled": "Uploaden is uitgeschakeld voor deze verbinding.",
"driveUnavailable": "Schakel RDP-schijfomleiding in voordat u bestanden uploadt.",
"dropToUpload": "Sleep de bestanden hierheen om ze te uploaden."
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Mapnaam",
"fileUploadedSuccessfully": "Bestand \"{{name}}\" succesvol geüpload",
"failedToUploadFile": "Het uploaden van het bestand is mislukt.",
"uploadingFolderFiles": "Bestand(en) {{count}} uploaden...",
"uploadedFolderFiles": "Bestand(en) {{count}} succesvol geüpload",
"uploadedFolderPartial": "Geüploade {{uploaded}} bestand(en), {{failed}} mislukt",
"fileDownloadedSuccessfully": "Bestand \"{{name}}\" succesvol gedownload",
"failedToDownloadFile": "Het downloaden van het bestand is mislukt.",
"fileCreatedSuccessfully": "Bestand \"{{name}}\" is succesvol aangemaakt.",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} items",
"downloadFileSuccess": "Bestand {{name}} succesvol gedownload",
"downloadFileFailed": "Download mislukt",
"downloadingFile": "Downloaden {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Ga naar {{name}}",
"diffCompareWith": "Vergelijk het verschil met {{name}}",
"dragOutsideToDownload": "Sleep buiten het venster om bestanden te downloaden ({{count}})",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Automatisch accounts aanmaken voor OIDC/SSO-gebruikers bij de eerste aanmelding (onafhankelijk van de registratie-optie).",
"oidcSilentLoginDefault": "Standaard stille OIDC-aanmelding",
"oidcSilentLoginDefaultDesc": "Automatisch doorverwijzen naar de OIDC-inlogpagina bij elk bezoek, waarbij het inlogformulier volledig wordt overgeslagen.",
"oidcSilentLoginDefaultLockedDesc": "Deze instelling is vergrendeld door de omgevingsvariabele OIDC_SILENT_LOGIN_DEFAULT en kan hier niet worden gewijzigd.",
"allowPasswordReset": "Wachtwoordherstel toestaan",
"allowPasswordResetDesc": "Resetcode via Docker-logs",
"commandHistoryEnabled": "Commandogeschiedenis",
@@ -3317,11 +3337,11 @@
"createRole": "Creëren",
"creating": "Aan het creëren...",
"exportDatabase": "Exporteer database",
"exportDatabaseDesc": "Download een back-up van alle hosts, inloggegevens en instellingen.",
"exportDatabaseDesc": "Download een draagbare SQLite-back-up van alle hosts, inloggegevens en instellingen.",
"export": "Exporteren",
"exporting": "Exporteren...",
"importDatabase": "Importeer database",
"importDatabaseDesc": "Herstellen vanuit een .sqlite-back-upbestand",
"importDatabaseDesc": "Herstel vanuit een draagbare .sqlite-back-up op elke databasebackend.",
"importDatabaseSelected": "Geselecteerd: {{name}}",
"selectFile": "Selecteer bestand",
"changeFile": "Wijziging",
@@ -4516,6 +4536,7 @@
"selectProvider": "Selecteer een aanbieder",
"providerSettings": "Aanbiederinstellingen",
"addProvider": "Aanbieder toevoegen",
"editProvider": "Bewerk provider",
"removeProvider": "Verwijder provider",
"providerType": "Aanbieder",
"providerLabel": "Naam",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Bijvoorbeeld: llama3.1 of claude-opus-5",
"labelRequired": "Geef deze aanbieder een naam",
"providerSaveFailed": "De provider kon niet worden opgeslagen.",
"providerUpdated": "Aanbieder bijgewerkt",
"providerDeleteFailed": "De provider kon niet worden verwijderd.",
"privateEndpointHint": "IP-adressen op uw privénetwerk moeten eerst door een beheerder worden toegestaan.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH-nøkkel",
"filterTagsGroup": "Tagger",
"copyDeployCommand": "Kopier distribuer-kommandoen",
"cloneCredentialAction": "Klon legitimasjon",
"clonedCredential": "Klonet {{name}}",
"clonedCredentialName": "{{name}} (kopi)",
"deleteCredentialAction": "Slett legitimasjon",
"editCredentialAction": "Rediger legitimasjon",
"failedToCloneCredential": "Kunne ikke klone legitimasjon",
"failedToDeleteCredential": "Kunne ikke slette legitimasjonen",
"idBadge": "ID",
"keyBadge": "NØKKEL",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Agent Socket-bane",
"agentSocketPathPlaceholder": "La stå tomt for å bruke SSH_AUTH_SOCK",
"agentSocketPathHint": "La feltet stå tomt for automatisk deteksjon fra miljøvariabelen SSH_AUTH_SOCK, eller skriv inn en egendefinert socket-sti (f.eks. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Agentidentitet (offentlig nøkkel)",
"agentIdentityPlaceholder": "La feltet stå tomt for å prøve alle nøkler fra agenten",
"agentIdentityHint": "Lim inn en offentlig nøkkel (f.eks. ssh-ed25519 AAAA...) for å kun tilby den nøkkelen fra agenten. Anbefales når agenten har mange nøkler, for å unngå å overskride serverens MaxAuthTries.",
"shareSshAuthLabel": "Del SSH-autentisering",
"shareSshAuthDesc": "Gi mottakerne krypterte kopier av denne vertens SSH-autentisering. Mottakerens personlige legitimasjon har fortsatt forrang.",
"tailscaleDeviceSelect": "Velg Tailscale-enhet",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Ingen legitimasjon funnet",
"addCredentialBtn": "Legg til legitimasjon",
"updateCredentialBtn": "Oppdater legitimasjon",
"editCredentialAction": "Rediger legitimasjon",
"backToHost": "Tilbake til verten",
"saveCredentialAsNewBtn": "Lagre som ny",
"saveCredentialAsNewDesc": "Oppretter en ny legitimasjon med disse endringene og tilordner den til denne verten. Den opprinnelige legitimasjonen forblir uendret.",
"credentialSavedAsNew": "Lagret som ny legitimasjon",
"failedToSaveCredentialAsNew": "Kunne ikke lagre som ny legitimasjon",
"features": "Funksjoner",
"noFolder": "(Ingen mappe)",
"deleteSelected": "Slett",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Kompakt",
"displayShowTags": "Vis tagger",
"displayTrayOnClick": "Åpne handlinger ved klikk (i stedet for å holde musepekeren over)",
"actionsVisibility": "Vertshandlinger",
"actionsVisibilityDesc": "Når tilkoblings- og administrasjonsknappene på hver vert vises",
"actionsVisibility": "Vertshandlingsvisning",
"actionsVisibilityDesc": "Velg når vertshandlinger skal utvides. Velg klikk for å gjenopprette den tidligere klikk-for-å-utvide-virkemåten.",
"actionsAlways": "Alltid synlig",
"actionsOnly": "Bare tilkobling, hvile på klikk",
"actionsHover": "Vis ved å holde musepekeren over",
"actionsClick": "Vis ved klikk",
"actionsClick": "Klikk for å utvide/skjule",
"openOnDoubleClick": "Åpne ved dobbeltklikk",
"openOnDoubleClickDesc": "Krev et dobbeltklikk for å åpne en vertsøkt i stedet for et enkeltklikk",
"deletedHost": "Slettet {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Lastet opp {{name}}",
"uploadFailed": "Kunne ikke laste opp {{name}}",
"uploadDisabled": "Opplastinger er deaktivert for denne tilkoblingen",
"driveUnavailable": "Aktiver omdirigering av RDP-stasjoner før du laster opp filer",
"dropToUpload": "Slipp filer for å laste opp"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Mappenavn",
"fileUploadedSuccessfully": "Filen «{{name}}» ble lastet opp",
"failedToUploadFile": "Kunne ikke laste opp filen",
"uploadingFolderFiles": "Laster opp {{count}} fil(er)...",
"uploadedFolderFiles": "Lastet opp {{count}} fil(er)",
"uploadedFolderPartial": "Opplasting av {{uploaded}} fil(er), {{failed}} mislyktes",
"fileDownloadedSuccessfully": "Filen «{{name}}» er lastet ned",
"failedToDownloadFile": "Kunne ikke laste ned filen",
"fileCreatedSuccessfully": "Filen «{{name}}» er opprettet",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} elementer",
"downloadFileSuccess": "Fil {{name}} er lastet ned",
"downloadFileFailed": "Nedlastingen mislyktes",
"downloadingFile": "Laster ned {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Flytt til {{name}}",
"diffCompareWith": "Differansesammenligning med {{name}}",
"dragOutsideToDownload": "Dra utenfor vinduet for å laste ned ({{count}} filer)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Opprett kontoer automatisk for OIDC/SSO-brukere ved første pålogging (uavhengig av registreringsbryteren)",
"oidcSilentLoginDefault": "Stille OIDC-pålogging som standard",
"oidcSilentLoginDefaultDesc": "Omdiriger automatisk til OIDC-innlogging ved hvert besøk, og hopper over innloggingsskjemaet helt",
"oidcSilentLoginDefaultLockedDesc": "Denne innstillingen er låst av miljøvariabelen OIDC_SILENT_LOGIN_DEFAULT og kan ikke endres her.",
"allowPasswordReset": "Tillat tilbakestilling av passord",
"allowPasswordResetDesc": "Tilbakestill kode via Docker-logger",
"commandHistoryEnabled": "Kommandohistorikk",
@@ -3317,11 +3337,11 @@
"createRole": "Skape",
"creating": "Oppretter...",
"exportDatabase": "Eksporter database",
"exportDatabaseDesc": "Last ned en sikkerhetskopi av alle verter, påloggingsinformasjon og innstillinger",
"exportDatabaseDesc": "Last ned en bærbar SQLite-sikkerhetskopi av alle verter, påloggingsinformasjon og innstillinger",
"export": "Eksport",
"exporting": "Eksporterer...",
"importDatabase": "Importer database",
"importDatabaseDesc": "Gjenopprett fra en .sqlite-sikkerhetskopifil",
"importDatabaseDesc": "Gjenopprett fra en bærbar .sqlite-sikkerhetskopi på en hvilken som helst database-backend",
"importDatabaseSelected": "Valgt: {{name}}",
"selectFile": "Velg fil",
"changeFile": "Endre",
@@ -4516,6 +4536,7 @@
"selectProvider": "Velg en leverandør",
"providerSettings": "Leverandørinnstillinger",
"addProvider": "Legg til leverandør",
"editProvider": "Rediger leverandør",
"removeProvider": "Fjern leverandør",
"providerType": "Leverandør",
"providerLabel": "Navn",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "For eksempel: llama3.1 eller claude-opus-5",
"labelRequired": "Gi denne leverandøren et navn",
"providerSaveFailed": "Kunne ikke lagre leverandøren",
"providerUpdated": "Leverandøren er oppdatert",
"providerDeleteFailed": "Kunne ikke fjerne leverandøren",
"privateEndpointHint": "Adresser på ditt private nettverk krever at en administrator tillater verten først.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "Klucz SSH",
"filterTagsGroup": "Tagi",
"copyDeployCommand": "Kopiuj polecenie wdrożenia",
"cloneCredentialAction": "Klonuj dane uwierzytelniające",
"clonedCredential": "Sklonowano {{name}}",
"clonedCredentialName": "{{name}} (kopiuj)",
"deleteCredentialAction": "Usuń poświadczenia",
"editCredentialAction": "Edytuj poświadczenia",
"failedToCloneCredential": "Nie udało się sklonować danych uwierzytelniających",
"failedToDeleteCredential": "Nie udało się usunąć poświadczeń",
"idBadge": "ID",
"keyBadge": "KLAWISZ",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Ścieżka gniazda agenta",
"agentSocketPathPlaceholder": "Pozostaw puste, aby użyć SSH_AUTH_SOCK",
"agentSocketPathHint": "Pozostaw puste, aby automatycznie wykryć na podstawie zmiennej środowiskowej SSH_AUTH_SOCK, lub wprowadź niestandardową ścieżkę gniazda (np. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Tożsamość agenta (klucz publiczny)",
"agentIdentityPlaceholder": "Pozostaw puste, aby wypróbować wszystkie klucze od agenta",
"agentIdentityHint": "Wklej klucz publiczny (np. ssh-ed25519 AAAA...), aby udostępnić tylko ten klucz agentowi. Zalecane, gdy agent posiada wiele kluczy, aby uniknąć przekroczenia limitu MaxAuthTries serwera.",
"shareSshAuthLabel": "Udostępnij uwierzytelnianie SSH",
"shareSshAuthDesc": "Przekaż odbiorcom zaszyfrowane kopie uwierzytelnienia SSH tego hosta. Dane osobowe odbiorcy nadal mają pierwszeństwo.",
"tailscaleDeviceSelect": "Wybierz urządzenie Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Nie znaleziono danych uwierzytelniających",
"addCredentialBtn": "Dodaj poświadczenia",
"updateCredentialBtn": "Aktualizacja poświadczeń",
"editCredentialAction": "Edytuj poświadczenia",
"backToHost": "Powrót do hosta",
"saveCredentialAsNewBtn": "Zapisz jako nowy",
"saveCredentialAsNewDesc": "Tworzy nowe dane uwierzytelniające z tymi zmianami i przypisuje je do tego hosta. Oryginalne dane uwierzytelniające pozostają niezmienione.",
"credentialSavedAsNew": "Zapisano jako nowe dane uwierzytelniające",
"failedToSaveCredentialAsNew": "Nie udało się zapisać jako nowych danych uwierzytelniających",
"features": "Cechy",
"noFolder": "(Brak folderu)",
"deleteSelected": "Usuwać",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Kompaktowy",
"displayShowTags": "Pokaż tagi",
"displayTrayOnClick": "Otwórz akcje po kliknięciu (zamiast po najechaniu kursorem)",
"actionsVisibility": "Działania gospodarza",
"actionsVisibilityDesc": "Gdy na każdym hoście pojawią się przyciski połączenia i zarządzania",
"actionsVisibility": "Wyświetlanie akcji hosta",
"actionsVisibilityDesc": "Wybierz, kiedy akcje hosta mają się rozwijać. Wybierz „kliknij”, aby przywrócić poprzednie działanie „kliknij, aby rozwinąć”.",
"actionsAlways": "Zawsze widoczny",
"actionsOnly": "Tylko połączenie, reszta po kliknięciu",
"actionsHover": "Pokaż po najechaniu kursorem",
"actionsClick": "Pokaż po kliknięciu",
"actionsClick": "Kliknij, aby rozwinąć/zwinąć",
"openOnDoubleClick": "Otwórz po dwukrotnym kliknięciu",
"openOnDoubleClickDesc": "Wymagaj dwukrotnego kliknięcia, aby otworzyć sesję hosta zamiast pojedynczego kliknięcia",
"deletedHost": "Usunięto {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Przesłano {{name}}",
"uploadFailed": "Nie udało się przesłać {{name}}",
"uploadDisabled": "Przesyłanie danych jest wyłączone dla tego połączenia",
"driveUnavailable": "Włącz przekierowanie dysku RDP przed przesłaniem plików",
"dropToUpload": "Upuść pliki, aby je przesłać"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Nazwa folderu",
"fileUploadedSuccessfully": "Plik „{{name}}” został pomyślnie przesłany",
"failedToUploadFile": "Nie udało się przesłać pliku",
"uploadingFolderFiles": "Przesyłanie {{count}} plików...",
"uploadedFolderFiles": "Pomyślnie przesłano {{count}} plików",
"uploadedFolderPartial": "Przesłano {{uploaded}} plików, {{failed}} nie powiodło się",
"fileDownloadedSuccessfully": "Plik „{{name}}” został pomyślnie pobrany",
"failedToDownloadFile": "Nie udało się pobrać pliku",
"fileCreatedSuccessfully": "Plik „{{name}}” został pomyślnie utworzony",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} elementów",
"downloadFileSuccess": "Plik {{name}} został pomyślnie pobrany",
"downloadFileFailed": "Pobieranie nie powiodło się",
"downloadingFile": "Pobieranie {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Przejdź do {{name}}",
"diffCompareWith": "Porównaj z {{name}}",
"dragOutsideToDownload": "Przeciągnij poza okno, aby pobrać ({{count}} plików)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Automatyczne tworzenie kont dla użytkowników OIDC/SSO przy pierwszym logowaniu (niezależnie od przełącznika rejestracji)",
"oidcSilentLoginDefault": "Domyślne ciche logowanie do OIDC",
"oidcSilentLoginDefaultDesc": "Automatyczne przekierowywanie do logowania OIDC przy każdej wizycie, całkowicie pomijając formularz logowania",
"oidcSilentLoginDefaultLockedDesc": "To ustawienie jest zablokowane przez zmienną środowiskową OIDC_SILENT_LOGIN_DEFAULT i nie można go tutaj zmienić.",
"allowPasswordReset": "Zezwól na resetowanie hasła",
"allowPasswordResetDesc": "Zresetuj kod za pomocą dzienników Dockera",
"commandHistoryEnabled": "Historia dowodzenia",
@@ -3317,11 +3337,11 @@
"createRole": "Tworzyć",
"creating": "Tworzenie...",
"exportDatabase": "Eksportuj bazę danych",
"exportDatabaseDesc": "Pobierz kopię zapasową wszystkich hostów, poświadczeń i ustawień",
"exportDatabaseDesc": "Pobierz przenośną kopię zapasową SQLite wszystkich hostów, poświadczeń i ustawień",
"export": "Eksport",
"exporting": "Eksportowanie...",
"importDatabase": "Importuj bazę danych",
"importDatabaseDesc": "Przywracanie z pliku kopii zapasowej .sqlite",
"importDatabaseDesc": "Przywracanie z przenośnej kopii zapasowej .sqlite na dowolnym zapleczu bazy danych",
"importDatabaseSelected": "Wybrano: {{name}}",
"selectFile": "Wybierz plik",
"changeFile": "Zmiana",
@@ -4516,6 +4536,7 @@
"selectProvider": "Wybierz dostawcę",
"providerSettings": "Ustawienia dostawcy",
"addProvider": "Dodaj dostawcę",
"editProvider": "Edytuj dostawcę",
"removeProvider": "Usuń dostawcę",
"providerType": "Dostawca",
"providerLabel": "Nazwa",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Na przykład: llama3.1 lub claude-opus-5",
"labelRequired": "Podaj nazwę tego dostawcy",
"providerSaveFailed": "Nie udało się zapisać dostawcy",
"providerUpdated": "Dostawca zaktualizowany",
"providerDeleteFailed": "Nie można usunąć dostawcy",
"privateEndpointHint": "Adresy w Twojej sieci prywatnej wymagają uprzedniej zgody administratora na dostęp do hosta.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "Chave SSH",
"filterTagsGroup": "Etiquetas",
"copyDeployCommand": "Copiar comando de implantação",
"cloneCredentialAction": "Credencial de clonagem",
"clonedCredential": "Clonado {{name}}",
"clonedCredentialName": "{{name}} (copiar)",
"deleteCredentialAction": "Excluir credencial",
"editCredentialAction": "Editar credencial",
"failedToCloneCredential": "Falha ao clonar a credencial",
"failedToDeleteCredential": "Falha ao excluir credencial",
"idBadge": "EU IA",
"keyBadge": "CHAVE",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Caminho do soquete do agente",
"agentSocketPathPlaceholder": "Deixe em branco para usar SSH_AUTH_SOCK",
"agentSocketPathHint": "Deixe em branco para detecção automática a partir da variável de ambiente SSH_AUTH_SOCK ou insira um caminho de socket personalizado (por exemplo, /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Identidade do agente (chave pública)",
"agentIdentityPlaceholder": "Deixe em branco para tentar todas as chaves do agente.",
"agentIdentityHint": "Cole uma chave pública (ex.: ssh-ed25519 AAAA...) para que somente essa chave seja oferecida pelo agente. Recomendado quando o agente possui muitas chaves, para evitar exceder o limite de tentativas de autenticação (MaxAuthTries) do servidor.",
"shareSshAuthLabel": "Compartilhar autenticação SSH",
"shareSshAuthDesc": "Forneça aos destinatários cópias criptografadas da autenticação SSH deste host. As credenciais pessoais do destinatário ainda têm precedência.",
"tailscaleDeviceSelect": "Selecione o dispositivo Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Nenhuma credencial encontrada",
"addCredentialBtn": "Adicionar credencial",
"updateCredentialBtn": "Atualizar credenciais",
"editCredentialAction": "Editar credencial",
"backToHost": "Voltar ao anfitrião",
"saveCredentialAsNewBtn": "Salvar como novo",
"saveCredentialAsNewDesc": "Cria uma nova credencial com essas alterações e a atribui a este host. A credencial original permanece inalterada.",
"credentialSavedAsNew": "Salvo como uma nova credencial",
"failedToSaveCredentialAsNew": "Falha ao salvar como uma nova credencial.",
"features": "Características",
"noFolder": "(Sem pasta)",
"deleteSelected": "Excluir",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Compactar",
"displayShowTags": "Mostrar etiquetas",
"displayTrayOnClick": "Abrir ações ao clicar (em vez de passar o cursor sobre elas)",
"actionsVisibility": "Ações do anfitrião",
"actionsVisibilityDesc": "Quando os botões de conexão e gerenciamento em cada host aparecerem",
"actionsVisibility": "Exibição de ação do host",
"actionsVisibilityDesc": "Escolha quando as ões do host se expandirão. Selecione \"clicar\" para restaurar o comportamento anterior de \"clicar para expandir\".",
"actionsAlways": "Sempre visível",
"actionsOnly": "Conecte apenas, descanse com um clique.",
"actionsHover": "Mostrar ao passar o cursor",
"actionsClick": "Mostrar ao clicar",
"actionsClick": "Clique para expandir/recolher",
"openOnDoubleClick": "Abrir com um clique duplo",
"openOnDoubleClickDesc": "Exigir um clique duplo para abrir uma sessão de host em vez de um único clique.",
"deletedHost": "Excluído {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Enviado {{name}}",
"uploadFailed": "Falha ao carregar {{name}}",
"uploadDisabled": "Os uploads estão desativados para esta conexão.",
"driveUnavailable": "Ative o redirecionamento de unidade RDP antes de enviar arquivos.",
"dropToUpload": "Arraste e solte os arquivos para fazer o upload."
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Nome da pasta",
"fileUploadedSuccessfully": "Arquivo \"{{name}}\" enviado com sucesso",
"failedToUploadFile": "Falha ao carregar o arquivo",
"uploadingFolderFiles": "Enviando {{count}} arquivo(s)...",
"uploadedFolderFiles": "Arquivo(s) carregado(s) com sucesso {{count}}",
"uploadedFolderPartial": "Uploaded {{uploaded}} file(s), {{failed}} failed",
"fileDownloadedSuccessfully": "Arquivo \"{{name}}\" baixado com sucesso",
"failedToDownloadFile": "Falha ao baixar o arquivo",
"fileCreatedSuccessfully": "Arquivo \"{{name}}\" criado com sucesso",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} itens",
"downloadFileSuccess": "Arquivo {{name}} baixado com sucesso",
"downloadFileFailed": "Falha no download",
"downloadingFile": "Baixando {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Mover para {{name}}",
"diffCompareWith": "Diff compare com {{name}}",
"dragOutsideToDownload": "Arraste para fora da janela para baixar ({{count}} arquivos)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Criar automaticamente contas para usuários OIDC/SSO no primeiro login (independentemente da ativação/desativação do registro).",
"oidcSilentLoginDefault": "Login OIDC silencioso por padrão",
"oidcSilentLoginDefaultDesc": "Redirecionamento automático para o login OIDC em todas as visitas, ignorando completamente o formulário de login.",
"oidcSilentLoginDefaultLockedDesc": "Essa configuração está bloqueada pela variável de ambiente OIDC_SILENT_LOGIN_DEFAULT e não pode ser alterada aqui.",
"allowPasswordReset": "Permitir redefinição de senha",
"allowPasswordResetDesc": "Redefinir código através dos logs do Docker",
"commandHistoryEnabled": "Histórico de comandos",
@@ -3317,11 +3337,11 @@
"createRole": "Criar",
"creating": "Criando...",
"exportDatabase": "Exportar banco de dados",
"exportDatabaseDesc": "Faça o download de um backup de todos os hosts, credenciais e configurações.",
"exportDatabaseDesc": "Baixe um backup portátil em SQLite de todos os hosts, credenciais e configurações.",
"export": "Exportar",
"exporting": "Exportador...",
"importDatabase": "Importar banco de dados",
"importDatabaseDesc": "Restaurar a partir de um arquivo de backup .sqlite",
"importDatabaseDesc": "Restaure a partir de um backup .sqlite portátil em qualquer banco de dados.",
"importDatabaseSelected": "Selecionado: {{name}}",
"selectFile": "Selecionar arquivo",
"changeFile": "Mudar",
@@ -4516,6 +4536,7 @@
"selectProvider": "Selecione um fornecedor",
"providerSettings": "Configurações do provedor",
"addProvider": "Adicionar provedor",
"editProvider": "Editar provedor",
"removeProvider": "Remover provedor",
"providerType": "Fornecedor",
"providerLabel": "Nome",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Por exemplo: llama3.1 ou claude-opus-5",
"labelRequired": "Dê um nome a este fornecedor.",
"providerSaveFailed": "Não foi possível salvar o provedor.",
"providerUpdated": "Fornecedor atualizado",
"providerDeleteFailed": "Não foi possível remover o provedor.",
"privateEndpointHint": "Os endereços na sua rede privada precisam que um administrador autorize o host primeiro.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "Chave SSH",
"filterTagsGroup": "Etiquetas",
"copyDeployCommand": "Copiar comando de implantação",
"cloneCredentialAction": "Credencial de clonagem",
"clonedCredential": "Clonado {{name}}",
"clonedCredentialName": "{{name}} (copiar)",
"deleteCredentialAction": "Apagar credencial",
"editCredentialAction": "Editar credencial",
"failedToCloneCredential": "Falha ao clonar a credencial",
"failedToDeleteCredential": "Falha ao eliminar credencial",
"idBadge": "EU IA",
"keyBadge": "CHAVE",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Caminho do socket do agente",
"agentSocketPathPlaceholder": "Deixar vazio para usar SSH_AUTH_SOCK",
"agentSocketPathHint": "Deixar vazio para detetar automaticamente a partir da variável de ambiente SSH_AUTH_SOCK, ou introduza um caminho de socket personalizado (ex.: /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Identidade do agente (chave pública)",
"agentIdentityPlaceholder": "Deixe em branco para tentar todas as chaves do agente.",
"agentIdentityHint": "Cole uma chave pública (ex.: ssh-ed25519 AAAA...) para que apenas essa chave seja oferecida pelo agente. Recomendado quando o agente tem muitas chaves, para evitar exceder o limite de tentativas de autenticação (MaxAuthTries) do servidor.",
"shareSshAuthLabel": "Partilhar autenticação SSH",
"shareSshAuthDesc": "Forneça aos destinatários cópias encriptadas da autenticação SSH deste host. As credenciais pessoais do destinatário ainda têm precedência.",
"tailscaleDeviceSelect": "Selecionar dispositivo Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Nenhuma credencial encontrada",
"addCredentialBtn": "Adicionar Credencial",
"updateCredentialBtn": "Atualizar Credencial",
"editCredentialAction": "Editar credencial",
"backToHost": "Voltar ao anfitrião",
"saveCredentialAsNewBtn": "Guardar como novo",
"saveCredentialAsNewDesc": "Cria uma nova credencial com estas alterações e atribui-a a este host. A credencial original permanece inalterada.",
"credentialSavedAsNew": "Guardado como uma nova credencial",
"failedToSaveCredentialAsNew": "Falha ao guardar como uma nova credencial.",
"features": "Funcionalidades",
"noFolder": "(Nenhuma pasta)",
"deleteSelected": "Eliminar",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Compactar",
"displayShowTags": "Mostrar etiquetas",
"displayTrayOnClick": "Abrir ações ao clicar (em vez de passar o cursor sobre as mesmas)",
"actionsVisibility": "Ações do anfitrião",
"actionsVisibilityDesc": "Quando os botões de ligação e gestão em cada host aparecerem",
"actionsVisibility": "Exibição de ação do host",
"actionsVisibilityDesc": "Escolha quando as ões do host se irão expandir. Selecione \"clicar\" para restaurar o comportamento anterior de \"clicar para expandir\".",
"actionsAlways": "Sempre visível",
"actionsOnly": "Ligue apenas, descanse com um clique.",
"actionsHover": "Mostrar ao passar o cursor",
"actionsClick": "Mostrar ao clicar",
"actionsClick": "Clique para expandir/recolher",
"openOnDoubleClick": "Abrir com um duplo clique",
"openOnDoubleClickDesc": "Exigir um duplo clique para abrir uma sessão de host em vez de um único clique.",
"deletedHost": "{{name}} eliminado",
@@ -1867,6 +1880,7 @@
"uploaded": "Enviado {{name}}",
"uploadFailed": "Falha ao carregar {{name}}",
"uploadDisabled": "Os uploads estão desativados para esta ligação.",
"driveUnavailable": "Ative o redireccionamento da unidade RDP antes de enviar ficheiros.",
"dropToUpload": "Arraste e largue os ficheiros para fazer o upload."
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Nome da pasta",
"fileUploadedSuccessfully": "Ficheiro \"{{name}}\" enviado com sucesso",
"failedToUploadFile": "Falha ao enviar ficheiro",
"uploadingFolderFiles": "A enviar {{count}} ficheiro(s)...",
"uploadedFolderFiles": "Ficheiro(s) carregado(s) com sucesso {{count}}",
"uploadedFolderPartial": "Uploaded {{uploaded}} file(s), {{failed}} failed",
"fileDownloadedSuccessfully": "Ficheiro \"{{name}}\" descarregado com sucesso",
"failedToDownloadFile": "Falha ao descarregar ficheiro",
"fileCreatedSuccessfully": "Ficheiro \"{{name}}\" criado com sucesso",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} itens",
"downloadFileSuccess": "Ficheiro {{name}} descarregado com sucesso",
"downloadFileFailed": "Falha ao descarregar",
"downloadingFile": "A descarregar {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Mover para {{name}}",
"diffCompareWith": "Comparar diferenças com {{name}}",
"dragOutsideToDownload": "Arraste para fora da janela para descarregar ({{count}} ficheiros)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Criar automaticamente contas para utilizadores OIDC/SSO no primeiro início de sessão (independente da opção de registo)",
"oidcSilentLoginDefault": "Início de Sessão Silencioso OIDC por Defeito",
"oidcSilentLoginDefaultDesc": "Redirecionar automaticamente para o início de sessão OIDC em cada visita, ignorando completamente o formulário de início de sessão",
"oidcSilentLoginDefaultLockedDesc": "Esta configuração está bloqueada pela variável de ambiente OIDC_SILENT_LOGIN_DEFAULT e não pode ser alterada aqui.",
"allowPasswordReset": "Permitir Reposição de Palavra-passe",
"allowPasswordResetDesc": "Código de reposição via registos do Docker",
"commandHistoryEnabled": "Histórico de Comandos",
@@ -3317,11 +3337,11 @@
"createRole": "Criar",
"creating": "A criar...",
"exportDatabase": "Exportar Base de Dados",
"exportDatabaseDesc": "Transferir uma cópia de segurança de todos os hosts, credenciais e definições",
"exportDatabaseDesc": "Transfira um backup portátil em SQLite de todos os hosts, credenciais e definições.",
"export": "Exportar",
"exporting": "A exportar...",
"importDatabase": "Importar Base de Dados",
"importDatabaseDesc": "Restaurar a partir de um ficheiro de cópia de segurança .sqlite",
"importDatabaseDesc": "Restaure a partir de um backup . sqlite portátil em qualquer base de dados.",
"importDatabaseSelected": "Selecionado: {{name}}",
"selectFile": "Selecionar Ficheiro",
"changeFile": "Alterar",
@@ -4516,6 +4536,7 @@
"selectProvider": "Selecione um fornecedor",
"providerSettings": "Configurações do fornecedor",
"addProvider": "Adicionar provedor",
"editProvider": "Editar provedor",
"removeProvider": "Remover fornecedor",
"providerType": "Fornecedor",
"providerLabel": "Nome",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Por exemplo: llama3.1 ou claude-opus-5",
"labelRequired": "Dê um nome a este fornecedor.",
"providerSaveFailed": "Não foi possível guardar o fornecedor.",
"providerUpdated": "Fornecedor atualizado",
"providerDeleteFailed": "Não foi possível remover o fornecedor.",
"privateEndpointHint": "Os endereços na sua rede privada necessitam que um administrador autorize primeiro o host.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "Cheie SSH",
"filterTagsGroup": "Etichete",
"copyDeployCommand": "Copiați comanda de implementare",
"cloneCredentialAction": "Acreditare clonată",
"clonedCredential": "Clonat {{name}}",
"clonedCredentialName": "{{name}} (copie)",
"deleteCredentialAction": "Ștergeți acreditările",
"editCredentialAction": "Editați acreditările",
"failedToCloneCredential": "Nu s-a putut clona acreditările",
"failedToDeleteCredential": "Ștergerea acreditării nu a reușit",
"idBadge": "ID-ul",
"keyBadge": "CHEIE",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Calea socketului agentului",
"agentSocketPathPlaceholder": "Lăsați gol pentru a utiliza SSH_AUTH_SOCK",
"agentSocketPathHint": "Lăsați câmpul gol pentru detectarea automată din variabila de mediu SSH_AUTH_SOCK sau introduceți o cale de socket personalizată (de exemplu, /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Identitatea agentului (cheie publică)",
"agentIdentityPlaceholder": "Lăsați câmpul gol pentru a încerca toate cheile de la agent",
"agentIdentityHint": "Lipiți o cheie publică (de exemplu, ssh-ed25519 AAAA...) pentru a oferi doar acea cheie de la agent. Recomandat atunci când agentul deține mai multe chei, pentru a evita depășirea numărului MaxAuthTries al serverului.",
"shareSshAuthLabel": "Partajați autentificarea SSH",
"shareSshAuthDesc": "Oferiți destinatarilor copii criptate ale autentificării SSH a acestei gazde. Acreditările personale ale destinatarului au în continuare prioritate.",
"tailscaleDeviceSelect": "Selectați dispozitivul Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Nu au fost găsite acreditări",
"addCredentialBtn": "Adăugați acreditări",
"updateCredentialBtn": "Actualizați acreditările",
"editCredentialAction": "Editați acreditările",
"backToHost": "Înapoi la gazdă",
"saveCredentialAsNewBtn": "Salvare ca nou",
"saveCredentialAsNewDesc": "Creează o nouă acreditare cu aceste modificări și o atribuie acestei gazde. Acreditarea originală rămâne neschimbată.",
"credentialSavedAsNew": "Salvat ca o nouă acreditare",
"failedToSaveCredentialAsNew": "Nu s-a putut salva ca o nouă acreditare",
"features": "Caracteristici",
"noFolder": "(Fără dosar)",
"deleteSelected": "Şterge",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Compact",
"displayShowTags": "Afișați etichetele",
"displayTrayOnClick": "Deschideți acțiunile la clic (în loc să treceți cu mouse-ul peste ele)",
"actionsVisibility": "Acțiunile gazdei",
"actionsVisibilityDesc": "Când apar butoanele de conectare și administrare de pe fiecare gazdă",
"actionsVisibility": "Afișajul acțiunilor gazdei",
"actionsVisibilityDesc": "Alegeți când se extind acțiunile gazdei. Selectați clic pentru a restabili comportamentul anterior de tip clic pentru extindere.",
"actionsAlways": "Întotdeauna vizibil",
"actionsOnly": "Doar conectare, odihnă la clic",
"actionsHover": "Afișare la trecerea cu mouse-ul",
"actionsClick": "Afișare la clic",
"actionsClick": "Faceți clic pentru a extinde/restrânge",
"openOnDoubleClick": "Deschideți cu dublu clic",
"openOnDoubleClickDesc": "Necesită un dublu clic pentru a deschide o sesiune gazdă în loc de un singur clic",
"deletedHost": "Șters {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Încărcat {{name}}",
"uploadFailed": "Nu s-a putut încărca {{name}}",
"uploadDisabled": "Încărcările sunt dezactivate pentru această conexiune",
"driveUnavailable": "Activați redirecționarea unității RDP înainte de încărcarea fișierelor",
"dropToUpload": "Plasați fișierele pentru încărcare"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Nume folder",
"fileUploadedSuccessfully": "Fișierul „{{name}}” a fost încărcat cu succes",
"failedToUploadFile": "Nu s-a putut încărca fișierul",
"uploadingFolderFiles": "Se încarcă {{count}} fișier(e)...",
"uploadedFolderFiles": "Fișier(e) {{count}} încărcat(e) cu succes",
"uploadedFolderPartial": "Încărcarea fișierelor {{uploaded}} , {{failed}} a eșuat.",
"fileDownloadedSuccessfully": "Fișierul „{{name}}” a fost descărcat cu succes",
"failedToDownloadFile": "Nu s-a putut descărca fișierul",
"fileCreatedSuccessfully": "Fișierul „{{name}}” a fost creat cu succes",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} articole",
"downloadFileSuccess": "Fișierul {{name}} a fost descărcat cu succes",
"downloadFileFailed": "Descărcarea a eșuat",
"downloadingFile": "Se descarcă {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Mută la {{name}}",
"diffCompareWith": "Comparație diferențială cu {{name}}",
"dragOutsideToDownload": "Trageți în afara ferestrei pentru a descărca (fișiere{{count}})",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Creare automată conturi pentru utilizatorii OIDC/SSO la prima autentificare (independent de comutarea înregistrării)",
"oidcSilentLoginDefault": "Conectare OIDC silențioasă în mod implicit",
"oidcSilentLoginDefaultDesc": "Redirecționare automată către autentificarea OIDC la fiecare vizită, omițând complet formularul de autentificare",
"oidcSilentLoginDefaultLockedDesc": "Această setare este blocată de variabila de mediu OIDC_SILENT_LOGIN_DEFAULT și nu poate fi modificată aici.",
"allowPasswordReset": "Permite resetarea parolei",
"allowPasswordResetDesc": "Resetează codul prin jurnalele Docker",
"commandHistoryEnabled": "Istoricul comenzilor",
@@ -3317,11 +3337,11 @@
"createRole": "Crea",
"creating": "Se creează...",
"exportDatabase": "Exportați baza de date",
"exportDatabaseDesc": "Descărcați o copie de rezervă a tuturor gazdelor, acreditărilor și setărilor",
"exportDatabaseDesc": "Descărcați o copie de rezervă SQLite portabilă a tuturor gazdelor, acreditărilor și setărilor",
"export": "Export",
"exporting": "Exportator...",
"importDatabase": "Importați baza de date",
"importDatabaseDesc": "Restaurare dintr-un fișier de rezervă .sqlite",
"importDatabaseDesc": "Restaurare dintr-o copie de rezervă .sqlite portabilă pe orice backend de bază de date",
"importDatabaseSelected": "Selectat: {{name}}",
"selectFile": "Selectați fișierul",
"changeFile": "Schimba",
@@ -4516,6 +4536,7 @@
"selectProvider": "Selectați un furnizor",
"providerSettings": "Setări furnizor",
"addProvider": "Adăugați furnizor",
"editProvider": "Editați furnizorul",
"removeProvider": "Eliminați furnizorul",
"providerType": "Furnizor",
"providerLabel": "Nume",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "De exemplu: llama3.1 sau claude-opus-5",
"labelRequired": "Dați un nume acestui furnizor",
"providerSaveFailed": "Nu s-a putut salva furnizorul",
"providerUpdated": "Furnizor actualizat",
"providerDeleteFailed": "Nu s-a putut elimina furnizorul",
"privateEndpointHint": "Adresele din rețeaua privată necesită mai întâi permisiunea unui administrator pentru gazdă.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH-ключ",
"filterTagsGroup": "Теги",
"copyDeployCommand": "Скопировать команду развертывания",
"cloneCredentialAction": "Клонировать учетные данные",
"clonedCredential": "Клонировано {{name}}",
"clonedCredentialName": "{{name}} (копия)",
"deleteCredentialAction": "Удалить учетные данные",
"editCredentialAction": "Изменить учетные данные",
"failedToCloneCredential": "Не удалось клонировать учетные данные.",
"failedToDeleteCredential": "Не удалось удалить учетные данные.",
"idBadge": "ИДЕНТИФИКАТОР",
"keyBadge": "КЛЮЧ",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Путь к сокету агента",
"agentSocketPathPlaceholder": "Оставьте пустым для использования SSH_AUTH_SOCK",
"agentSocketPathHint": "Оставьте пустым для автоопределения из переменной окружения SSH_AUTH_SOCK или укажите свой путь к сокету (например, /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Идентификатор агента (открытый ключ)",
"agentIdentityPlaceholder": "Оставьте поле пустым, чтобы попробовать все клавиши от агента.",
"agentIdentityHint": "Вставьте открытый ключ (например, ssh-ed25519 AAAA...), чтобы агент предлагал только этот ключ. Рекомендуется, если у агента много ключей, чтобы избежать превышения лимита MaxAuthTries сервера.",
"shareSshAuthLabel": "Совместное использование SSH-аутентификации",
"shareSshAuthDesc": "Передайте получателям зашифрованные копии данных SSH-аутентификации этого хоста. При этом персональные учётные данные получателя имеют приоритет.",
"tailscaleDeviceSelect": "Выберите устройство Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Учётные данные не найдены",
"addCredentialBtn": "Добавить учётные данные",
"updateCredentialBtn": "Обновить учётные данные",
"editCredentialAction": "Изменить учетные данные",
"backToHost": "Вернуться к хосту",
"saveCredentialAsNewBtn": "Сохранить как новый",
"saveCredentialAsNewDesc": "Создает новые учетные данные с учетом внесенных изменений и назначает их данному хосту. Исходные учетные данные остаются без изменений.",
"credentialSavedAsNew": "Сохранено как новые учетные данные.",
"failedToSaveCredentialAsNew": "Не удалось сохранить новые учетные данные.",
"features": "Функции",
"noFolder": "(Нет папки)",
"deleteSelected": "Удалить",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Компактный",
"displayShowTags": "Показать теги",
"displayTrayOnClick": "Действия открываются по клику (а не при наведении курсора).",
"actionsVisibility": "Действия хоста",
"actionsVisibilityDesc": "Когда на каждом хосте появятся кнопки подключения и управления",
"actionsVisibility": "Отображение действий хоста",
"actionsVisibilityDesc": "Выберите момент, когда действия хоста разворачиваются. Выберите «кликнуть», чтобы восстановить прежнее поведение «кликнуть для разворачивания».",
"actionsAlways": "Всегда видимый",
"actionsOnly": "Подключайтесь только, остальное — щелчком мыши.",
"actionsHover": "Отображать при наведении курсора",
"actionsClick": "Показать при клике",
"actionsClick": "Нажмите, чтобы развернуть/свернуть",
"openOnDoubleClick": "Открывается двойным щелчком",
"openOnDoubleClickDesc": "Для открытия сессии хоста теперь требуется двойной щелчок вместо одного.",
"deletedHost": "Удалён {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Загружено {{name}}",
"uploadFailed": "Не удалось загрузить {{name}}",
"uploadDisabled": "Загрузка файлов для этого соединения отключена.",
"driveUnavailable": "Включите перенаправление RDP-дисков перед загрузкой файлов.",
"dropToUpload": "Перетащите файлы для загрузки"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Имя папки",
"fileUploadedSuccessfully": "Файл «{{name}}» успешно загружен",
"failedToUploadFile": "Не удалось загрузить файл",
"uploadingFolderFiles": "Загрузка {{count}} файла(ов)...",
"uploadedFolderFiles": "Файл(ы) {{count}} успешно загружен(ы).",
"uploadedFolderPartial": "Загрузка {{uploaded}} файла(ов), {{failed}} не удалась.",
"fileDownloadedSuccessfully": "Файл «{{name}}» успешно скачан",
"failedToDownloadFile": "Не удалось скачать файл",
"fileCreatedSuccessfully": "Файл «{{name}}» успешно создан",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} элементов",
"downloadFileSuccess": "Файл {{name}} успешно загружен",
"downloadFileFailed": "Не удалось загрузить",
"downloadingFile": "Загрузка {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Переместить в {{name}}",
"diffCompareWith": "Сравнить с {{name}}",
"dragOutsideToDownload": "Перетащите за пределы окна для загрузки ({{count}} файлов)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Автоматически создавать учётные записи для пользователей OIDC/SSO при первом входе (независимо от переключателя регистрации)",
"oidcSilentLoginDefault": "Автоматический вход OIDC по умолчанию",
"oidcSilentLoginDefaultDesc": "Автоматически перенаправлять на страницу входа OIDC при каждом посещении, полностью пропуская форму входа",
"oidcSilentLoginDefaultLockedDesc": "Этот параметр заблокирован переменной среды OIDC_SILENT_LOGIN_DEFAULT и не может быть изменен здесь.",
"allowPasswordReset": "Разрешить сброс пароля",
"allowPasswordResetDesc": "Код сброса в логах Docker",
"commandHistoryEnabled": "История команд",
@@ -3317,11 +3337,11 @@
"createRole": "Создать",
"creating": "Создание...",
"exportDatabase": "Экспорт базы данных",
"exportDatabaseDesc": "Скачать резервную копию всех хостов, учётных данных и настроек",
"exportDatabaseDesc": "Загрузите портативную резервную копию SQLite, содержащую все хосты, учетные данные и настройки.",
"export": "Экспортировать",
"exporting": "Экспортирование...",
"importDatabase": "Импорт базы данных",
"importDatabaseDesc": "Восстановить из файла резервной копии .sqlite",
"importDatabaseDesc": "Восстановление из портативной резервной копии .sqlite на любой базе данных.",
"importDatabaseSelected": "Выбрано: {{name}}",
"selectFile": "Выбрать файл",
"changeFile": "Изменить",
@@ -4516,6 +4536,7 @@
"selectProvider": "Выберите поставщика",
"providerSettings": "Настройки поставщика",
"addProvider": "Добавить поставщика",
"editProvider": "Поставщик редактирования",
"removeProvider": "Удалить поставщика",
"providerType": "Поставщик",
"providerLabel": "Имя",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Например: llama3.1 или claude-opus-5",
"labelRequired": "Укажите имя этого поставщика услуг.",
"providerSaveFailed": "Не удалось сохранить поставщика услуг.",
"providerUpdated": "Поставщик обновлен",
"providerDeleteFailed": "Не удалось удалить поставщика услуг.",
"privateEndpointHint": "Для доступа к адресам в вашей частной сети необходимо предварительное разрешение администратора.",
"providerOllama": "оллама",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH кључ",
"filterTagsGroup": "Ознаке",
"copyDeployCommand": "Копирај команду за распоређивање",
"cloneCredentialAction": "Клонирај акредитив",
"clonedCredential": "Клонирано {{name}}",
"clonedCredentialName": "{{name}} (копија)",
"deleteCredentialAction": "Обриши акредитив",
"editCredentialAction": "Измени акредитив",
"failedToCloneCredential": "Клонирање акредитива није успело",
"failedToDeleteCredential": "Брисање акредитива није успело",
"idBadge": "ИД",
"keyBadge": "КЉУЧ",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Путања агентског сокета",
"agentSocketPathPlaceholder": "Оставите празно да бисте користили SSH_AUTH_SOCK",
"agentSocketPathHint": "Оставите празно да би се аутоматски детектовало из променљиве окружења SSH_AUTH_SOCK или унесите прилагођену путању до сокета (нпр. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Идентитет агента (јавни кључ)",
"agentIdentityPlaceholder": "Оставите празно да бисте испробали све кључеве од агента",
"agentIdentityHint": "Налепите јавни кључ (нпр. ssh-ed25519 AAAA...) да бисте понудили само тај кључ од агента. Препоручује се када агент поседује много кључева, како би се избегло прекорачење максималног броја покушаја овлашћења сервера.",
"shareSshAuthLabel": "Дељење SSH аутентификације",
"shareSshAuthDesc": "Доставите примаоцима шифроване копије SSH аутентификације овог хоста. Лични акредитив примаоца и даље има предност.",
"tailscaleDeviceSelect": "Изаберите уређај Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Нису пронађени акредитиви",
"addCredentialBtn": "Додај акредитив",
"updateCredentialBtn": "Ажурирај акредитив",
"editCredentialAction": "Измени акредитив",
"backToHost": "Назад на домаћина",
"saveCredentialAsNewBtn": "Сачувај као ново",
"saveCredentialAsNewDesc": "Креира нови акредитив са овим променама и додељује га овом хосту. Оригинални акредитив остаје непромењен.",
"credentialSavedAsNew": "Сачувано као нови акредитив",
"failedToSaveCredentialAsNew": "Чување као новог акредитива није успело",
"features": "Карактеристике",
"noFolder": "(Нема фасцикле)",
"deleteSelected": "Обриши",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Компактно",
"displayShowTags": "Прикажи ознаке",
"displayTrayOnClick": "Отварање акција кликом (уместо преласка мишем)",
"actionsVisibility": "Акције домаћина",
"actionsVisibilityDesc": "Када се појаве дугмад за повезивање и управљање на сваком хосту",
"actionsVisibility": "Приказ акције домаћина",
"actionsVisibilityDesc": "Изаберите када се радње хоста проширују. Изаберите клик да бисте вратили претходно понашање клика за проширивање.",
"actionsAlways": "Увек видљиво",
"actionsOnly": "Само повезивање, остатак на клик",
"actionsHover": "Прикажи превлачењем миша",
"actionsClick": "Прикажи кликом",
"actionsClick": "Кликните да бисте проширили/склопили",
"openOnDoubleClick": "Отвори двоструким кликом",
"openOnDoubleClickDesc": "За отварање сесије домаћина потребан је двоструки клик уместо једног клика",
"deletedHost": "Обрисано {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Отпремљено {{name}}",
"uploadFailed": "Није успело отпремање {{name}}",
"uploadDisabled": "Отпремања су онемогућена за ову везу",
"driveUnavailable": "Омогућите преусмеравање RDP диска пре отпремања датотека",
"dropToUpload": "Отпустите датотеке да бисте их отпремили"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Назив фасцикле",
"fileUploadedSuccessfully": "Датотека „{{name}}“ је успешно отпремљена",
"failedToUploadFile": "Отпремање датотеке није успело",
"uploadingFolderFiles": "Отпремање {{count}} датотека...",
"uploadedFolderFiles": "Успешно отпремљено {{count}} датотека",
"uploadedFolderPartial": "Отпремљено {{uploaded}} датотека, {{failed}} није успело",
"fileDownloadedSuccessfully": "Датотека „{{name}}“ је успешно преузета",
"failedToDownloadFile": "Преузимање датотеке није успело",
"fileCreatedSuccessfully": "Датотека „{{name}}“ је успешно креирана",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} ставке",
"downloadFileSuccess": "Датотека {{name}} је успешно преузета",
"downloadFileFailed": "Преузимање није успело",
"downloadingFile": "Преузимање {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Премести на {{name}}",
"diffCompareWith": "Разлика у поређењу са {{name}}",
"dragOutsideToDownload": "Превуците спољашњост прозора да бисте преузели ({{count}} датотека)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Аутоматски креирај налоге за OIDC/SSO кориснике при првом пријављивању (независно од прекидача за регистрацију)",
"oidcSilentLoginDefault": "Тихо OIDC пријављивање по подразумеваним подешавањима",
"oidcSilentLoginDefaultDesc": "Аутоматско преусмеравање на OIDC пријаву при свакој посети, потпуно прескачући образац за пријаву",
"oidcSilentLoginDefaultLockedDesc": "Ово подешавање је закључано променљивом окружења OIDC_SILENT_LOGIN_DEFAULT и не може се овде променити.",
"allowPasswordReset": "Дозволи ресетовање лозинке",
"allowPasswordResetDesc": "Ресетујте код путем Докер логова",
"commandHistoryEnabled": "Историја команди",
@@ -3317,11 +3337,11 @@
"createRole": "Креирај",
"creating": "Креирање...",
"exportDatabase": "Извоз базе података",
"exportDatabaseDesc": "Преузмите резервну копију свих хостова, акредитива и подешавања",
"exportDatabaseDesc": "Преузмите преносиву SQLite резервну копију свих хостова, акредитива и подешавања",
"export": "Извоз",
"exporting": "Извоз...",
"importDatabase": "Увоз базе података",
"importDatabaseDesc": "Враћање из .sqlite датотеке резервне копије",
"importDatabaseDesc": "Враћање из преносиве .sqlite резервне копије на било којој бази података",
"importDatabaseSelected": "Изабрано: {{name}}",
"selectFile": "Изаберите датотеку",
"changeFile": "Промена",
@@ -4516,6 +4536,7 @@
"selectProvider": "Изаберите добављача",
"providerSettings": "Подешавања добављача",
"addProvider": "Додај добављача",
"editProvider": "Измени добављача",
"removeProvider": "Уклони добављача",
"providerType": "Провајдер",
"providerLabel": "Име",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "На пример: llama3.1 или claude-opus-5",
"labelRequired": "Дајте име овом добављачу",
"providerSaveFailed": "Није могуће сачувати добављача",
"providerUpdated": "Провајдер је ажуриран",
"providerDeleteFailed": "Није могуће уклонити добављача",
"privateEndpointHint": "Адресе на вашој приватној мрежи прво треба да дозволе администратору хоста.",
"providerOllama": "Олама",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH-nyckel",
"filterTagsGroup": "Taggar",
"copyDeployCommand": "Kopiera deploy-kommandot",
"cloneCredentialAction": "Klona inloggningsuppgifter",
"clonedCredential": "Klonad {{name}}",
"clonedCredentialName": "{{name}} (kopiera)",
"deleteCredentialAction": "Ta bort inloggningsuppgifter",
"editCredentialAction": "Redigera inloggningsuppgifter",
"failedToCloneCredential": "Misslyckades med att klona inloggningsuppgifterna",
"failedToDeleteCredential": "Misslyckades med att ta bort inloggningsuppgifter",
"idBadge": "ID",
"keyBadge": "NYCKEL",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Agent Socket-sökväg",
"agentSocketPathPlaceholder": "Lämna tomt för att använda SSH_AUTH_SOCK",
"agentSocketPathHint": "Lämna tomt för att automatiskt identifiera från miljövariabeln SSH_AUTH_SOCK, eller ange en anpassad socket-sökväg (t.ex. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Agentidentitet (publik nyckel)",
"agentIdentityPlaceholder": "Lämna tomt för att prova alla nycklar från agenten",
"agentIdentityHint": "Klistra in en offentlig nyckel (t.ex. ssh-ed25519 AAAA...) för att endast erbjuda den nyckeln från agenten. Rekommenderas när agenten har många nycklar, för att undvika att överskrida serverns MaxAuthTries.",
"shareSshAuthLabel": "Dela SSH-autentisering",
"shareSshAuthDesc": "Ge mottagarna krypterade kopior av denna värds SSH-autentisering. Mottagarens personliga inloggningsuppgifter har fortfarande företräde.",
"tailscaleDeviceSelect": "Välj Tailscale-enhet",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Inga inloggningsuppgifter hittades",
"addCredentialBtn": "Lägg till autentiseringsuppgifter",
"updateCredentialBtn": "Uppdatera autentiseringsuppgifter",
"editCredentialAction": "Redigera inloggningsuppgifter",
"backToHost": "Tillbaka till värden",
"saveCredentialAsNewBtn": "Spara som ny",
"saveCredentialAsNewDesc": "Skapar en ny autentiseringsuppgift med dessa ändringar och tilldelar den till denna värd. Den ursprungliga autentiseringsuppgiften lämnas oförändrad.",
"credentialSavedAsNew": "Sparad som en ny autentiseringsuppgift",
"failedToSaveCredentialAsNew": "Misslyckades med att spara som en ny autentiseringsuppgift",
"features": "Drag",
"noFolder": "(Ingen mapp)",
"deleteSelected": "Radera",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Kompakt",
"displayShowTags": "Visa taggar",
"displayTrayOnClick": "Öppna åtgärder vid klick (istället för att hålla muspekaren över)",
"actionsVisibility": "Värdåtgärder",
"actionsVisibilityDesc": "När anslutnings- och hanteringsknapparna på varje värd visas",
"actionsVisibility": "Värdåtgärdsvisning",
"actionsVisibilityDesc": "Välj när värdåtgärder expanderas. Välj klicka för att återställa det tidigare klicka-för-att-expandera-beteendet.",
"actionsAlways": "Alltid synlig",
"actionsOnly": "Endast anslutning, vila på klick",
"actionsHover": "Visa vid muspekning",
"actionsClick": "Visa vid klick",
"actionsClick": "Klicka för att expandera/minimera",
"openOnDoubleClick": "Öppna vid dubbelklick",
"openOnDoubleClickDesc": "Kräv ett dubbelklick för att öppna en värdsession istället för ett enda klick",
"deletedHost": "Borttagen {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Uppladdad {{name}}",
"uploadFailed": "Misslyckades med att ladda upp {{name}}",
"uploadDisabled": "Uppladdningar är inaktiverade för den här anslutningen",
"driveUnavailable": "Aktivera omdirigering av RDP-enheter innan du laddar upp filer",
"dropToUpload": "Släpp filer för att ladda upp"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Mappnamn",
"fileUploadedSuccessfully": "Filen \"{{name}}\" har laddats upp",
"failedToUploadFile": "Misslyckades med att ladda upp filen",
"uploadingFolderFiles": "Laddar upp {{count}} fil(er)...",
"uploadedFolderFiles": "Uppladdade {{count}} fil(er) framgångsrikt",
"uploadedFolderPartial": "Uppladdade {{uploaded}} fil(er), {{failed}} misslyckades",
"fileDownloadedSuccessfully": "Filen \"{{name}}\" har laddats ner",
"failedToDownloadFile": "Misslyckades med att ladda ner filen",
"fileCreatedSuccessfully": "Filen \"{{name}}\" skapades",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} objekt",
"downloadFileSuccess": "Filen {{name}} har laddats ner",
"downloadFileFailed": "Nedladdningen misslyckades",
"downloadingFile": "Laddar ner {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Flytta till {{name}}",
"diffCompareWith": "Differensjämförelse med {{name}}",
"dragOutsideToDownload": "Dra utanför fönstret för att ladda ner ({{count}} filer)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Skapa konton automatiskt för OIDC/SSO-användare vid första inloggningen (oberoende av registreringsknappen)",
"oidcSilentLoginDefault": "Tyst OIDC-inloggning som standard",
"oidcSilentLoginDefaultDesc": "Omdirigera automatiskt till OIDC-inloggning vid varje besök, hoppar över inloggningsformuläret helt",
"oidcSilentLoginDefaultLockedDesc": "Den här inställningen är låst av miljövariabeln OIDC_SILENT_LOGIN_DEFAULT och kan inte ändras här.",
"allowPasswordReset": "Tillåt lösenordsåterställning",
"allowPasswordResetDesc": "Återställ kod via Docker-loggar",
"commandHistoryEnabled": "Kommandohistorik",
@@ -3317,11 +3337,11 @@
"createRole": "Skapa",
"creating": "Skapande...",
"exportDatabase": "Exportera databas",
"exportDatabaseDesc": "Ladda ner en säkerhetskopia av alla värdar, inloggningsuppgifter och inställningar",
"exportDatabaseDesc": "Ladda ner en portabel SQLite-säkerhetskopia av alla värdar, inloggningsuppgifter och inställningar",
"export": "Exportera",
"exporting": "Exporterar...",
"importDatabase": "Importera databas",
"importDatabaseDesc": "Återställ från en .sqlite-säkerhetskopia",
"importDatabaseDesc": "Återställ från en portabel .sqlite-säkerhetskopia på valfri databas-backend",
"importDatabaseSelected": "Vald: {{name}}",
"selectFile": "Välj fil",
"changeFile": "Ändra",
@@ -4516,6 +4536,7 @@
"selectProvider": "Välj en leverantör",
"providerSettings": "Leverantörsinställningar",
"addProvider": "Lägg till leverantör",
"editProvider": "Redigera leverantör",
"removeProvider": "Ta bort leverantör",
"providerType": "Leverantör",
"providerLabel": "Namn",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Till exempel: llama3.1 eller claude-opus-5",
"labelRequired": "Ge den här leverantören ett namn",
"providerSaveFailed": "Kunde inte spara leverantören",
"providerUpdated": "Leverantören uppdaterad",
"providerDeleteFailed": "Kunde inte ta bort leverantören",
"privateEndpointHint": "Adresser i ditt privata nätverk behöver en administratör som först tillåter värden.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "คีย์ SSH",
"filterTagsGroup": "แท็ก",
"copyDeployCommand": "คัดลอกคำสั่งปรับใช้",
"cloneCredentialAction": "คัดลอกข้อมูลประจำตัว",
"clonedCredential": "โคลน {{name}}",
"clonedCredentialName": "{{name}} (คัดลอก)",
"deleteCredentialAction": "ลบข้อมูลรับรอง",
"editCredentialAction": "แก้ไขข้อมูลประจำตัว",
"failedToCloneCredential": "ไม่สามารถคัดลอกข้อมูลประจำตัวได้",
"failedToDeleteCredential": "ไม่สามารถลบข้อมูลประจำตัวได้",
"idBadge": "รหัสประจำตัว",
"keyBadge": "สำคัญ",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "เส้นทางซ็อกเก็ตเอเจนต์",
"agentSocketPathPlaceholder": "เว้นว่างไว้เพื่อใช้ SSH_AUTH_SOCK",
"agentSocketPathHint": "เว้นว่างไว้เพื่อให้ระบบตรวจจับอัตโนมัติจากตัวแปรสภาพแวดล้อม SSH_AUTH_SOCK หรือป้อนเส้นทางซ็อกเก็ตแบบกำหนดเอง (เช่น /run/user/1000/gnupg/S.gpg-agent.ssh)",
"agentIdentityLabel": "รหัสประจำตัวของเอเจนต์ (คีย์สาธารณะ)",
"agentIdentityPlaceholder": "ปล่อยว่างไว้เพื่อลองใช้คีย์ทั้งหมดจากเอเจนต์",
"agentIdentityHint": "วางคีย์สาธารณะ (เช่น ssh-ed25519 AAAA...) เพื่อให้ตัวแทนเสนอคีย์นั้นเพียงคีย์เดียว แนะนำให้ใช้เมื่อตัวแทนมีคีย์จำนวนมาก เพื่อหลีกเลี่ยงการเกินจำนวน MaxAuthTries ของเซิร์ฟเวอร์",
"shareSshAuthLabel": "แชร์การตรวจสอบสิทธิ์ SSH",
"shareSshAuthDesc": "ส่งสำเนาการเข้ารหัสของข้อมูลรับรอง SSH ของโฮสต์นี้ให้แก่ผู้รับ อย่างไรก็ตาม ข้อมูลรับรองส่วนบุคคลของผู้รับยังคงมีความสำคัญเหนือกว่า",
"tailscaleDeviceSelect": "เลือกอุปกรณ์ Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "ไม่พบข้อมูลประจำตัว",
"addCredentialBtn": "เพิ่มข้อมูลรับรอง",
"updateCredentialBtn": "อัปเดตข้อมูลรับรอง",
"editCredentialAction": "แก้ไขข้อมูลประจำตัว",
"backToHost": "กลับสู่หน้าหลัก",
"saveCredentialAsNewBtn": "บันทึกเป็นรายการใหม่",
"saveCredentialAsNewDesc": "สร้างข้อมูลรับรองใหม่โดยใช้การเปลี่ยนแปลงเหล่านี้และกำหนดให้กับโฮสต์นี้ ข้อมูลรับรองเดิมยังคงไม่เปลี่ยนแปลง",
"credentialSavedAsNew": "บันทึกเป็นข้อมูลรับรองใหม่แล้ว",
"failedToSaveCredentialAsNew": "ไม่สามารถบันทึกเป็นข้อมูลรับรองใหม่ได้",
"features": "คุณสมบัติ",
"noFolder": "(ไม่มีโฟลเดอร์)",
"deleteSelected": "ลบ",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "กะทัดรัด",
"displayShowTags": "แสดงแท็ก",
"displayTrayOnClick": "เปิดใช้งานการทำงานเมื่อคลิก (แทนการวางเมาส์เหนือ)",
"actionsVisibility": "การกระทำของโฮสต์",
"actionsVisibilityDesc": "เมื่อปุ่มเชื่อมต่อและการจัดการบนโฮสต์แต่ละเครื่องปรากฏขึ้น",
"actionsVisibility": "การแสดงผลการกระทำของโฮสต์",
"actionsVisibilityDesc": "เลือกเวลาที่การกระทำของโฮสต์จะขยายออก เลือก \"คลิก\" เพื่อคืนค่าพฤติกรรมการคลิกเพื่อขยายแบบเดิม",
"actionsAlways": "มองเห็นได้เสมอ",
"actionsOnly": "เชื่อมต่อเท่านั้น ที่เหลือก็คลิกได้เลย",
"actionsHover": "แสดงเมื่อวางเมาส์เหนือภาพ",
"actionsClick": "แสดงเมื่อคลิก",
"actionsClick": "คลิกเพื่อขยาย/ยุบ",
"openOnDoubleClick": "เปิดเมื่อดับเบิ้ลคลิก",
"openOnDoubleClickDesc": "ต้องดับเบิ้ลคลิกเพื่อเปิดเซสชันโฮสต์ แทนที่จะคลิกเพียงครั้งเดียว",
"deletedHost": "ลบแล้ว {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "อัปโหลดแล้ว {{name}}",
"uploadFailed": "การอัปโหลดล้มเหลว {{name}}",
"uploadDisabled": "การอัปโหลดถูกปิดใช้งานสำหรับการเชื่อมต่อนี้",
"driveUnavailable": "เปิดใช้งานการส่งต่อไดรฟ์ RDP ก่อนอัปโหลดไฟล์",
"dropToUpload": "ลากไฟล์เพื่ออัปโหลด"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "ชื่อโฟลเดอร์",
"fileUploadedSuccessfully": "ไฟล์ \"{{name}}\" อัปโหลดสำเร็จแล้ว",
"failedToUploadFile": "ไม่สามารถอัปโหลดไฟล์ได้",
"uploadingFolderFiles": "กำลังอัปโหลดไฟล์ {{count}} ไฟล์...",
"uploadedFolderFiles": "อัปโหลดไฟล์ {{count}} ไฟล์สำเร็จแล้ว",
"uploadedFolderPartial": "อัปโหลดไฟล์ {{uploaded}} ไฟล์, {{failed}} ล้มเหลว",
"fileDownloadedSuccessfully": "ดาวน์โหลดไฟล์ \"{{name}}\" สำเร็จแล้ว",
"failedToDownloadFile": "ไม่สามารถดาวน์โหลดไฟล์ได้",
"fileCreatedSuccessfully": "สร้างไฟล์ \"{{name}}\" สำเร็จแล้ว",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} รายการ",
"downloadFileSuccess": "ดาวน์โหลดไฟล์ {{name}} สำเร็จแล้ว",
"downloadFileFailed": "การดาวน์โหลดล้มเหลว",
"downloadingFile": "กำลังดาวน์โหลด {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "ย้ายไปที่ {{name}}",
"diffCompareWith": "เปรียบเทียบความแตกต่างกับ {{name}}",
"dragOutsideToDownload": "ลากออกนอกหน้าต่างเพื่อดาวน์โหลดไฟล์ ({{count}} ไฟล์)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "สร้างบัญชีอัตโนมัติสำหรับผู้ใช้ OIDC/SSO เมื่อเข้าสู่ระบบครั้งแรก (โดยไม่ขึ้นอยู่กับการตั้งค่าการลงทะเบียน)",
"oidcSilentLoginDefault": "การเข้าสู่ระบบ OIDC แบบเงียบโดยค่าเริ่มต้น",
"oidcSilentLoginDefaultDesc": "ระบบจะเปลี่ยนเส้นทางไปยังหน้าเข้าสู่ระบบ OIDC โดยอัตโนมัติทุกครั้งที่เข้าชม โดยไม่ต้องกรอกแบบฟอร์มเข้าสู่ระบบอีกต่อไป",
"oidcSilentLoginDefaultLockedDesc": "การตั้งค่านี้ถูกล็อกโดยตัวแปรสภาพแวดล้อม OIDC_SILENT_LOGIN_DEFAULT และไม่สามารถเปลี่ยนแปลงได้ที่นี่",
"allowPasswordReset": "อนุญาตให้รีเซ็ตรหัสผ่าน",
"allowPasswordResetDesc": "รีเซ็ตโค้ดผ่านบันทึก Docker",
"commandHistoryEnabled": "ประวัติคำสั่ง",
@@ -3317,11 +3337,11 @@
"createRole": "สร้าง",
"creating": "กำลังสร้าง...",
"exportDatabase": "ส่งออกฐานข้อมูล",
"exportDatabaseDesc": "ดาวน์โหลดไฟล์สำรองข้อมูลของโฮสต์ ข้อมูลประจำตัว และการตั้งค่าทั้งหมด",
"exportDatabaseDesc": "ดาวน์โหลดไฟล์สำรองข้อมูล SQLite แบบพกพาที่มีข้อมูลโฮสต์ ข้อมูลประจำตัว และการตั้งค่าทั้งหมด",
"export": "ส่งออก",
"exporting": "กำลังส่งออก...",
"importDatabase": "นำเข้าฐานข้อมูล",
"importDatabaseDesc": "กู้คืนจากไฟล์สำรองข้อมูล .sqlite",
"importDatabaseDesc": "กู้คืนจากไฟล์สำรองข้อมูล .sqlite แบบพกพาบนระบบฐานข้อมูลใดก็ได้",
"importDatabaseSelected": "เลือกแล้ว: {{name}}",
"selectFile": "เลือกไฟล์",
"changeFile": "เปลี่ยน",
@@ -4516,6 +4536,7 @@
"selectProvider": "เลือกผู้ให้บริการ",
"providerSettings": "การตั้งค่าผู้ให้บริการ",
"addProvider": "เพิ่มผู้ให้บริการ",
"editProvider": "แก้ไขผู้ให้บริการ",
"removeProvider": "ลบผู้ให้บริการ",
"providerType": "ผู้ให้บริการ",
"providerLabel": "ชื่อ",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "ตัวอย่างเช่น: llama3.1 หรือ claude-opus-5",
"labelRequired": "ตั้งชื่อให้ผู้ให้บริการรายนี้",
"providerSaveFailed": "ไม่สามารถบันทึกผู้ให้บริการได้",
"providerUpdated": "ผู้ให้บริการได้รับการอัปเดตแล้ว",
"providerDeleteFailed": "ไม่สามารถลบผู้ให้บริการได้",
"privateEndpointHint": "การเข้าถึงที่อยู่บนเครือข่ายส่วนตัวของคุณนั้น จำเป็นต้องให้ผู้ดูแลระบบอนุญาตก่อน",
"providerOllama": "โอลาม่า",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH Anahtarı",
"filterTagsGroup": "Etiketler",
"copyDeployCommand": "Dağıtım komutunu kopyala",
"cloneCredentialAction": "Klon kimlik bilgisi",
"clonedCredential": "Klonlandı {{name}}",
"clonedCredentialName": "{{name}} (kopyala)",
"deleteCredentialAction": "Kimlik bilgilerini sil",
"editCredentialAction": "Kimlik bilgilerini düzenle",
"failedToCloneCredential": "Kimlik bilgilerini kopyalama başarısız oldu.",
"failedToDeleteCredential": "Kimlik bilgilerini silme işlemi başarısız oldu.",
"idBadge": "İD",
"keyBadge": "ANAHTAR",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Ajan Soket Yolu",
"agentSocketPathPlaceholder": "SSH_AUTH_SOCK kullanmak için boş bırakın",
"agentSocketPathHint": "SSH_AUTH_SOCK ortam değişkeninden otomatik algılamak için boş bırakın veya özel bir soket yolu girin (örn. /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Temsilci Kimliği (Herkese Açık Anahtar)",
"agentIdentityPlaceholder": "Aracının tüm tuşlarını denemek için boş bırakın.",
"agentIdentityHint": "Aracıdan yalnızca o anahtarı sunmak için bir genel anahtar yapıştırın (örneğin ssh-ed25519 AAAA...). Aracının birçok anahtar tuttuğu durumlarda, sunucunun MaxAuthTries sınırını aşmamak için önerilir.",
"shareSshAuthLabel": "SSH Kimlik Doğrulamasını Paylaş",
"shareSshAuthDesc": "Alıcılara bu sunucunun SSH kimlik doğrulama bilgilerinin şifrelenmiş kopyalarını verin. Alıcının kişisel kimlik bilgileri öncelikli olmaya devam eder.",
"tailscaleDeviceSelect": "Tailscale cihazı seçin",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Hiç kimlik bilgisi bulunamadı",
"addCredentialBtn": "Kimlik Bilgisi Ekle",
"updateCredentialBtn": "Kimlik Bilgisini Güncelle",
"editCredentialAction": "Kimlik bilgilerini düzenle",
"backToHost": "Ana bilgisayara geri dön",
"saveCredentialAsNewBtn": "Yeni olarak kaydet",
"saveCredentialAsNewDesc": "Bu değişikliklerle yeni bir kimlik bilgisi oluşturulur ve bu sunucuya atanır. Orijinal kimlik bilgisi değiştirilmeden kalır.",
"credentialSavedAsNew": "Yeni bir kimlik bilgisi olarak kaydedildi.",
"failedToSaveCredentialAsNew": "Yeni bir kimlik bilgisi olarak kaydedilemedi.",
"features": "Özellikler",
"noFolder": "(Klasör yok)",
"deleteSelected": "Sil",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Kompakt",
"displayShowTags": "Etiketleri göster",
"displayTrayOnClick": "Fareyle üzerine gelmek yerine tıklama ile eylemleri aç",
"actionsVisibility": "Sunucu lemleri",
"actionsVisibilityDesc": "Her sunucudaki bağlantı ve yönetim düğmeleri göründüğünde",
"actionsVisibility": "Sunucu eylemi gösterimi",
"actionsVisibilityDesc": "Sunucu eylemlerinin ne zaman genişleyeceğini seçin. Önceki tıklayarak genişletme davranışını geri yüklemek için \"tıkla\"yı seçin.",
"actionsAlways": "Her zaman görünür",
"actionsOnly": "Sadece bağlantı kurun, gerisi tıklamaya bağlı.",
"actionsHover": "Üzerine gelindiğinde göster",
"actionsClick": "Tıklayınca göster",
"actionsClick": "Genişletmek/Daraltmak için tıklayın",
"openOnDoubleClick": "Çift tıklayarak açın",
"openOnDoubleClickDesc": "Ana bilgisayar oturumunu açmak için tek tıklama yerine çift tıklama gerektir.",
"deletedHost": "{{name}} silindi",
@@ -1867,6 +1880,7 @@
"uploaded": "Yüklenen {{name}}",
"uploadFailed": "{{name}} yüklenemedi.",
"uploadDisabled": "Bu bağlantı için yüklemeler devre dışı bırakıldı.",
"driveUnavailable": "Dosyaları yüklemeden önce RDP sürücü yönlendirmesini etkinleştirin.",
"dropToUpload": "Yüklemek için dosyaları sürükleyip bırakın."
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Klasör Adı",
"fileUploadedSuccessfully": "\"{{name}}\" dosyası başarıyla yüklendi.",
"failedToUploadFile": "Dosya yüklenemedi.",
"uploadingFolderFiles": "{{count}} dosya yükleniyor...",
"uploadedFolderFiles": "{{count}} dosya başarıyla yüklendi",
"uploadedFolderPartial": "{{uploaded}} dosya yüklendi, {{failed}} başarısız oldu",
"fileDownloadedSuccessfully": "\"{{name}}\" dosyası başarıyla indirildi.",
"failedToDownloadFile": "Dosya indirilemedi.",
"fileCreatedSuccessfully": "\"{{name}}\" dosyası başarıyla oluşturuldu.",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{count}} öğe {{operation}}",
"downloadFileSuccess": "{{name}} dosyası başarıyla indirildi",
"downloadFileFailed": "İndirme başarısız oldu",
"downloadingFile": "{{name}}… indiriliyor",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "{{name}} konumuna taşı",
"diffCompareWith": "{{name}} ile diff karşılaştır",
"dragOutsideToDownload": "İndirmek için pencere dışına sürükleyin ({{count}} dosya)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "OIDC/SSO kullanıcıları için ilk girişte otomatik hesap oluştur (kayıt ayarından bağımsız)",
"oidcSilentLoginDefault": "Varsayılan Olarak Sessiz OIDC Girişi",
"oidcSilentLoginDefaultDesc": "Her ziyarette otomatik olarak OIDC girişine yönlendir, giriş formunu tamamen atla",
"oidcSilentLoginDefaultLockedDesc": "Bu ayar, OIDC_SILENT_LOGIN_DEFAULT ortam değişkeni tarafından kilitlenmiştir ve burada değiştirilemez.",
"allowPasswordReset": "Parola Sıfırlamaya İzin Ver",
"allowPasswordResetDesc": "Docker günlükleri aracılığıyla sıfırlama kodu",
"commandHistoryEnabled": "Komut Geçmişi",
@@ -3317,11 +3337,11 @@
"createRole": "Oluştur",
"creating": "Oluşturuluyor...",
"exportDatabase": "Veritabanını Dışa Aktar",
"exportDatabaseDesc": "Tüm sunucuların, kimlik bilgilerinin ve ayarların yedeğini indir",
"exportDatabaseDesc": "Tüm sunucuların, kimlik bilgilerinin ve ayarların taşınabilir bir SQLite yedeğini indirin.",
"export": "Dışa Aktar",
"exporting": "Dışa aktarılıyor...",
"importDatabase": "Veritabanını İçe Aktar",
"importDatabaseDesc": "Bir .sqlite yedek dosyasından geri yükle",
"importDatabaseDesc": "Herhangi bir veritabanı arka ucunda taşınabilir bir .sqlite yedeklemesinden geri yükleme",
"importDatabaseSelected": "Seçilen: {{name}}",
"selectFile": "Dosya Seç",
"changeFile": "Değiştir",
@@ -4516,6 +4536,7 @@
"selectProvider": "Bir sağlayıcı seçin",
"providerSettings": "Sağlayıcı ayarları",
"addProvider": "Sağlayıcı ekle",
"editProvider": "Sağlayıcıyı düzenle",
"removeProvider": "Sağlayıcıyı kaldır",
"providerType": "Sağlayıcı",
"providerLabel": "İsim",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Örneğin: llama3.1 veya claude-opus-5",
"labelRequired": "Bu sağlayıcıya bir isim verin.",
"providerSaveFailed": "Sağlayıcı kaydedilemedi.",
"providerUpdated": "Sağlayıcı güncellendi.",
"providerDeleteFailed": "Sağlayıcı kaldırılamadı.",
"privateEndpointHint": "Özel ağınızdaki adreslerin kullanılabilmesi için öncelikle yöneticinin sunucuya izin vermesi gerekmektedir.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH-ключ",
"filterTagsGroup": "Теги",
"copyDeployCommand": "Скопіювати команду розгортання",
"cloneCredentialAction": "Клонувати облікові дані",
"clonedCredential": "Клоновано {{name}}",
"clonedCredentialName": "{{name}} (копія)",
"deleteCredentialAction": "Видалити облікові дані",
"editCredentialAction": "Редагувати облікові дані",
"failedToCloneCredential": "Не вдалося клонувати облікові дані",
"failedToDeleteCredential": "Не вдалося видалити облікові дані",
"idBadge": "Ідентифікатор",
"keyBadge": "КЛЮЧ",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Шлях сокета агента",
"agentSocketPathPlaceholder": "Залиште поле порожнім, щоб використовувати SSH_AUTH_SOCK",
"agentSocketPathHint": "Залиште поле порожнім для автоматичного визначення зі змінної середовища SSH_AUTH_SOCK або введіть власний шлях до сокета (наприклад, /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Ідентифікація агента (відкритий ключ)",
"agentIdentityPlaceholder": "Залиште поле порожнім, щоб спробувати всі ключі від агента",
"agentIdentityHint": "Вставте відкритий ключ (наприклад, ssh-ed25519 AAAA...), щоб пропонувати лише цей ключ від агента. Рекомендовано, коли агент має багато ключів, щоб уникнути перевищення максимальної кількості спроб автентифікації сервера.",
"shareSshAuthLabel": "Спільний доступ до SSH-автентифікації",
"shareSshAuthDesc": "Надати одержувачам зашифровані копії SSH-автентифікації цього хоста. Особисті облікові дані одержувача все ще мають пріоритет.",
"tailscaleDeviceSelect": "Виберіть пристрій Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Облікові дані не знайдено",
"addCredentialBtn": "Додати облікові дані",
"updateCredentialBtn": "Оновити облікові дані",
"editCredentialAction": "Редагувати облікові дані",
"backToHost": "Назад до хоста",
"saveCredentialAsNewBtn": "Зберегти як нове",
"saveCredentialAsNewDesc": "Створює нові облікові дані з цими змінами та призначає їх цьому хосту. Початкові облікові дані залишаються без змін.",
"credentialSavedAsNew": "Збережено як нові облікові дані",
"failedToSaveCredentialAsNew": "Не вдалося зберегти як нові облікові дані",
"features": "Особливості",
"noFolder": "(Без папки)",
"deleteSelected": "Видалити",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Компактний",
"displayShowTags": "Показати теги",
"displayTrayOnClick": "Відкривати дії при кліці (замість наведення курсора)",
"actionsVisibility": "Дії хоста",
"actionsVisibilityDesc": "Коли на кожному хості відображаються кнопки підключення та керування",
"actionsVisibility": "Відображення дій хоста",
"actionsVisibilityDesc": "Виберіть, коли розгортатимуться дії хоста. Виберіть «Клацнути», щоб відновити попередню поведінку «розгортання клацанням».",
"actionsAlways": "Завжди видно",
"actionsOnly": "Тільки підключення, відпочинок після клацання",
"actionsHover": "Показувати при наведенні курсора",
"actionsClick": "Показувати після кліку",
"actionsClick": "Натисніть, щоб розгорнути/згорнути",
"openOnDoubleClick": "Відкрити подвійним клацанням",
"openOnDoubleClickDesc": "Для відкриття сеансу хоста потрібно подвійне клацання замість одного клацання",
"deletedHost": "Видалено {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Завантажено {{name}}",
"uploadFailed": "Не вдалося завантажити {{name}}",
"uploadDisabled": "Завантаження для цього з’єднання вимкнено",
"driveUnavailable": "Увімкніть перенаправлення диска RDP перед завантаженням файлів",
"dropToUpload": "Перетягніть файли для завантаження"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Назва папки",
"fileUploadedSuccessfully": "Файл «{{name}}» успішно завантажено",
"failedToUploadFile": "Не вдалося завантажити файл",
"uploadingFolderFiles": "Завантаження {{count}} файлів...",
"uploadedFolderFiles": "Успішно завантажено {{count}} файл(ів)",
"uploadedFolderPartial": "Завантажено {{uploaded}} файл(ів), {{failed}} не вдалося",
"fileDownloadedSuccessfully": "Файл «{{name}}» успішно завантажено",
"failedToDownloadFile": "Не вдалося завантажити файл",
"fileCreatedSuccessfully": "Файл «{{name}}» успішно створено",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}} {{count}} елементів",
"downloadFileSuccess": "Файл {{name}} успішно завантажено",
"downloadFileFailed": "Завантаження не вдалося",
"downloadingFile": "Завантаження {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Перейти до {{name}}",
"diffCompareWith": "Різниця у порівнянні з {{name}}",
"dragOutsideToDownload": "Перетягніть за межі вікна, щоб завантажити ({{count}} файлів)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Автоматичне створення облікових записів для користувачів OIDC/SSO під час першого входу (незалежно від перемикача реєстрації)",
"oidcSilentLoginDefault": "Тихий вхід OIDC за замовчуванням",
"oidcSilentLoginDefaultDesc": "Автоматично перенаправляти на сторінку входу в OIDC під час кожного відвідування, повністю пропускаючи форму входу",
"oidcSilentLoginDefaultLockedDesc": "Цей параметр заблоковано змінною середовища OIDC_SILENT_LOGIN_DEFAULT і його не можна змінити тут.",
"allowPasswordReset": "Дозволити скидання пароля",
"allowPasswordResetDesc": "Скидання коду через журнали Docker",
"commandHistoryEnabled": "Історія команд",
@@ -3317,11 +3337,11 @@
"createRole": "Створити",
"creating": "Створення...",
"exportDatabase": "Експорт бази даних",
"exportDatabaseDesc": "Завантажте резервну копію всіх хостів, облікових даних та налаштувань",
"exportDatabaseDesc": "Завантажте портативну резервну копію SQLite всіх хостів, облікових даних та налаштувань",
"export": "Експорт",
"exporting": "Експорт...",
"importDatabase": "Імпорт бази даних",
"importDatabaseDesc": "Відновлення з резервної копії файлу .sqlite",
"importDatabaseDesc": "Відновлення з портативної резервної копії .sqlite на будь-якому сервері бази даних",
"importDatabaseSelected": "Вибрано: {{name}}",
"selectFile": "Виберіть файл",
"changeFile": "Зміна",
@@ -4516,6 +4536,7 @@
"selectProvider": "Виберіть постачальника",
"providerSettings": "Налаштування постачальника",
"addProvider": "Додати постачальника",
"editProvider": "Редагувати постачальника",
"removeProvider": "Видалити постачальника",
"providerType": "Постачальник",
"providerLabel": "Ім'я",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Наприклад: llama3.1 або claude-opus-5",
"labelRequired": "Введіть назву цього постачальника",
"providerSaveFailed": "Не вдалося зберегти постачальника",
"providerUpdated": "Постачальника оновлено",
"providerDeleteFailed": "Не вдалося видалити постачальника",
"privateEndpointHint": "Адреси у вашій приватній мережі спочатку потребують дозволу адміністратора хоста.",
"providerOllama": "Оллама",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "Khóa SSH",
"filterTagsGroup": "Thẻ",
"copyDeployCommand": "Sao chép lệnh triển khai",
"cloneCredentialAction": "Sao chép thông tin đăng nhập",
"clonedCredential": "Đã sao chép {{name}}",
"clonedCredentialName": "{{name}} (copy)",
"deleteCredentialAction": "Xóa thông tin đăng nhập",
"editCredentialAction": "Chỉnh sửa thông tin đăng nhập",
"failedToCloneCredential": "Không thể sao chép thông tin xác thực.",
"failedToDeleteCredential": "Không thể xóa thông tin đăng nhập",
"idBadge": "NHẬN DẠNG",
"keyBadge": "CHÌA KHÓA",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "Đường dẫn socket của Agent",
"agentSocketPathPlaceholder": "Để trống để sử dụng SSH_AUTH_SOCK",
"agentSocketPathHint": "Để trống để tự động phát hiện từ biến môi trường SSH_AUTH_SOCK, hoặc nhập đường dẫn socket tùy chỉnh (ví dụ: /run/user/1000/gnupg/S.gpg-agent.ssh).",
"agentIdentityLabel": "Thông tin nhận dạng của tác nhân (Khóa công khai)",
"agentIdentityPlaceholder": "Để trống để thử tất cả các phím từ trình quản lý.",
"agentIdentityHint": "Dán khóa công khai (ví dụ: ssh-ed25519 AAAA...) để chỉ cho phép sử dụng khóa đó từ tác nhân. Cách này được khuyến nghị khi tác nhân giữ nhiều khóa, để tránh vượt quá giới hạn MaxAuthTries của máy chủ.",
"shareSshAuthLabel": "Chia sẻ xác thực SSH",
"shareSshAuthDesc": "Cung cấp cho người nhận bản sao đã mã hóa thông tin xác thực SSH của máy chủ này. Thông tin xác thực cá nhân của người nhận vẫn được ưu tiên.",
"tailscaleDeviceSelect": "Chọn thiết bị Tailscale",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "Không tìm thấy thông tin xác thực",
"addCredentialBtn": "Thêm Chứng thực",
"updateCredentialBtn": "Cập nhật Chứng thực",
"editCredentialAction": "Chỉnh sửa thông tin đăng nhập",
"backToHost": "Trở lại trang chủ",
"saveCredentialAsNewBtn": "Lưu dưới dạng mới",
"saveCredentialAsNewDesc": "Tạo một thông tin xác thực mới với những thay đổi này và gán nó cho máy chủ này. Thông tin xác thực ban đầu vẫn được giữ nguyên.",
"credentialSavedAsNew": "Đã lưu dưới dạng thông tin đăng nhập mới",
"failedToSaveCredentialAsNew": "Không thể lưu thông tin đăng nhập mới.",
"features": "Tính năng",
"noFolder": "(Không có thư mục)",
"deleteSelected": "Xóa",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "Nhỏ gọn",
"displayShowTags": "Hiển thị thẻ",
"displayTrayOnClick": "Mở các thao tác khi nhấp chuột (thay vì khi di chuột)",
"actionsVisibility": "Hành động của máy chủ",
"actionsVisibilityDesc": "Khi các nút kết nối và quản lý trên mỗi máy chủ hiển thị",
"actionsVisibility": "Hiển thị hành động của máy chủ",
"actionsVisibilityDesc": "Chọn thời điểm các thao tác trên máy chủ mở rộng. Chọn nhấp chuột để khôi phục hành vi nhấp chuột để mở rộng trước đó.",
"actionsAlways": "Luôn hiển thị",
"actionsOnly": "Chỉ cần kết nối, phần còn lại chỉ cần nhấp chuột.",
"actionsHover": "Hiển thị khi di chuột",
"actionsClick": "Hiển thị khi nhấp chuột",
"actionsClick": "Nhấp để mở rộng/thu gọn",
"openOnDoubleClick": "Mở bằng cách nhấp đúp chuột",
"openOnDoubleClickDesc": "Yêu cầu nhấp đúp chuột để mở phiên máy chủ thay vì chỉ nhấp chuột một lần.",
"deletedHost": "Đã xóa {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "Đã tải lên {{name}}",
"uploadFailed": "Tải lên không thành công {{name}}",
"uploadDisabled": "Chức năng tải lên đã bị vô hiệu hóa cho kết nối này.",
"driveUnavailable": "Hãy bật tính năng chuyển hướng ổ đĩa RDP trước khi tải lên tệp.",
"dropToUpload": "Kéo thả tệp để tải lên"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "Tên thư mục",
"fileUploadedSuccessfully": "Tập tin \"{{name}}\" đã được tải lên thành công",
"failedToUploadFile": "Tải tập tin lên thất bại",
"uploadingFolderFiles": "Đang tải lên {{count}} tập tin...",
"uploadedFolderFiles": "Đã tải lên {{count}} tập tin thành công",
"uploadedFolderPartial": "Đã tải lên {{uploaded}} tập tin, {{failed}} thất bại",
"fileDownloadedSuccessfully": "Tập tin \"{{name}}\" đã được tải xuống thành công",
"failedToDownloadFile": "Tải tập tin xuống thất bại",
"fileCreatedSuccessfully": "Tập tin \"{{name}}\" đã được tạo thành công",
@@ -2208,6 +2225,8 @@
"operationCompleted": "Đã {{operation}} {{count}} mục",
"downloadFileSuccess": "Đã tải xuống tệp {{name}} thành công",
"downloadFileFailed": "Tải xuống thất bại",
"downloadingFile": "Đang tải xuống {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "Di chuyển tới {{name}}",
"diffCompareWith": "So sánh khác biệt với {{name}}",
"dragOutsideToDownload": "Kéo ra ngoài cửa sổ để tải xuống ({{count}} tệp)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "Tự động tạo tài khoản cho người dùng OIDC/SSO khi đăng nhập lần đầu (độc lập với nút bật đăng ký)",
"oidcSilentLoginDefault": "Đăng nhập OIDC tự động mặc định",
"oidcSilentLoginDefaultDesc": "Tự động chuyển hướng đến đăng nhập OIDC mỗi lần truy cập, bỏ qua hoàn toàn biểu mẫu đăng nhập",
"oidcSilentLoginDefaultLockedDesc": "Cài đặt này bị khóa bởi biến môi trường OIDC_SILENT_LOGIN_DEFAULT và không thể thay đổi ở đây.",
"allowPasswordReset": "Cho phép đặt lại mật khẩu",
"allowPasswordResetDesc": "Mã đặt lại qua nhật ký Docker",
"commandHistoryEnabled": "Lịch sử lệnh",
@@ -3317,11 +3337,11 @@
"createRole": "Tạo",
"creating": "Đang tạo...",
"exportDatabase": "Xuất cơ sở dữ liệu",
"exportDatabaseDesc": "Tải về bản sao lưu tất cả máy chủ, thông tin đăng nhập và cài đặt",
"exportDatabaseDesc": "Tải xuống bản sao lưu SQLite di động chứa tất cả máy chủ, thông tin đăng nhập và cài đặt.",
"export": "Xuất",
"exporting": "Đang xuất...",
"importDatabase": "Nhập cơ sở dữ liệu",
"importDatabaseDesc": "Khôi phục từ tệp sao lưu .sqlite",
"importDatabaseDesc": "Khôi phục từ bản sao lưu .sqlite di động trên bất kỳ hệ thống cơ sở dữ liệu nào.",
"importDatabaseSelected": "Đã chọn: {{name}}",
"selectFile": "Chọn tệp",
"changeFile": "Đổi tệp",
@@ -4516,6 +4536,7 @@
"selectProvider": "Chọn nhà cung cấp",
"providerSettings": "Cài đặt nhà cung cấp",
"addProvider": "Thêm nhà cung cấp",
"editProvider": "Chỉnh sửa nhà cung cấp",
"removeProvider": "Xóa nhà cung cấp",
"providerType": "Nhà cung cấp",
"providerLabel": "Tên",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "Ví dụ: llama3.1 hoặc claude-opus-5",
"labelRequired": "Đặt tên cho nhà cung cấp này",
"providerSaveFailed": "Không thể lưu nhà cung cấp",
"providerUpdated": "Nhà cung cấp đã cập nhật",
"providerDeleteFailed": "Không thể xóa nhà cung cấp",
"privateEndpointHint": "Các địa chỉ trên mạng riêng của bạn cần được quản trị viên cho phép truy cập trước.",
"providerOllama": "Ollama",
+27 -5
View File
@@ -111,8 +111,12 @@
"filterTypeKey": "SSH 密钥",
"filterTagsGroup": "标签",
"copyDeployCommand": "复制部署命令",
"cloneCredentialAction": "克隆凭证",
"clonedCredential": "克隆 {{name}}",
"clonedCredentialName": "{{name}} (复制)",
"deleteCredentialAction": "删除凭据",
"editCredentialAction": "编辑凭证",
"failedToCloneCredential": "克隆凭证失败",
"failedToDeleteCredential": "删除凭据失败",
"idBadge": "ID",
"keyBadge": "钥匙",
@@ -925,6 +929,9 @@
"agentSocketPathLabel": "代理套接字路径",
"agentSocketPathPlaceholder": "留空以使用 SSH_AUTH_SOCK",
"agentSocketPathHint": "留空以从 SSH_AUTH_SOCK 环境变量自动检测,或输入自定义套接字路径(例如 /run/user/1000/gnupg/S.gpg-agent.ssh)。",
"agentIdentityLabel": "代理身份(公钥)",
"agentIdentityPlaceholder": "留空以尝试代理提供的所有密钥",
"agentIdentityHint": "粘贴公钥(例如 ssh-ed25519 AAAA...)以仅允许代理提供该密钥。当代理持有大量密钥时,建议这样做,以避免超出服务器的最大身份验证尝试次数 (MaxAuthTries)。",
"shareSshAuthLabel": "共享 SSH 身份验证",
"shareSshAuthDesc": "向收件人提供此主机 SSH 身份验证信息的加密副本。收件人的个人凭证仍然优先。",
"tailscaleDeviceSelect": "选择 Tailscale 设备",
@@ -1234,6 +1241,12 @@
"noCredentialsFound": "未找到凭据",
"addCredentialBtn": "添加凭据",
"updateCredentialBtn": "更新凭据",
"editCredentialAction": "编辑凭证",
"backToHost": "返回主机",
"saveCredentialAsNewBtn": "另存为新文件",
"saveCredentialAsNewDesc": "使用这些更改创建一个新的凭据,并将其分配给此主机。原始凭据保持不变。",
"credentialSavedAsNew": "已保存为新凭证",
"failedToSaveCredentialAsNew": "保存为新凭据失败",
"features": "功能",
"noFolder": "(无文件夹)",
"deleteSelected": "删除",
@@ -1318,12 +1331,12 @@
"displayDensityCompact": "袖珍的",
"displayShowTags": "显示标签",
"displayTrayOnClick": "点击打开操作(而不是悬停打开)",
"actionsVisibility": "主机操作",
"actionsVisibilityDesc": "当每台主机上的连接和管理按钮都显示出来时",
"actionsVisibility": "主机动作显示",
"actionsVisibilityDesc": "选择主机操作何时展开。选择“单击”以恢复以前的单击展开行为。",
"actionsAlways": "始终可见",
"actionsOnly": "仅连接,其余操作请点击",
"actionsHover": "鼠标悬停时显示",
"actionsClick": "点击显示",
"actionsClick": "点击展开/收起",
"openOnDoubleClick": "双击打开",
"openOnDoubleClickDesc": "打开主机会话需要双击,而不是单击。",
"deletedHost": "已删除 {{name}}",
@@ -1867,6 +1880,7 @@
"uploaded": "已上传 {{name}}",
"uploadFailed": "上传失败 {{name}}",
"uploadDisabled": "此连接已禁用上传功能",
"driveUnavailable": "上传文件前启用 RDP 驱动器重定向",
"dropToUpload": "拖放文件即可上传"
},
"toolbar": {
@@ -2037,6 +2051,9 @@
"folderName": "文件夹名称",
"fileUploadedSuccessfully": "文件 \"{{name}}\" 上传成功",
"failedToUploadFile": "上传文件失败",
"uploadingFolderFiles": "正在上传 {{count}} 文件...",
"uploadedFolderFiles": "已成功上传 {{count}} 个文件",
"uploadedFolderPartial": "上传了 {{uploaded}} 个文件, {{failed}} 个文件失败",
"fileDownloadedSuccessfully": "文件 \"{{name}}\" 下载成功",
"failedToDownloadFile": "下载文件失败",
"fileCreatedSuccessfully": "文件 \"{{name}}\" 创建成功",
@@ -2208,6 +2225,8 @@
"operationCompleted": "{{operation}}了{{count}}个项目",
"downloadFileSuccess": "文件 {{name}} 下载成功",
"downloadFileFailed": "下载失败",
"downloadingFile": "正在下载 {{name}}…",
"downloadProgressBytes": "{{transferred}} / {{total}}",
"moveTo": "移动到 {{name}}",
"diffCompareWith": "与 {{name}} 对比差异",
"dragOutsideToDownload": "拖到窗口外以下载 ({{count}} 个文件)",
@@ -3254,6 +3273,7 @@
"oidcAutoProvisionDesc": "首次登录时自动为 OIDC/SSO 用户创建账户(与注册开关无关)",
"oidcSilentLoginDefault": "默认静默 OIDC 登录",
"oidcSilentLoginDefaultDesc": "每次访问时自动重定向到 OIDC 登录,完全跳过登录表单",
"oidcSilentLoginDefaultLockedDesc": "此设置受 OIDC_SILENT_LOGIN_DEFAULT 环境变量锁定,无法在此处更改。",
"allowPasswordReset": "允许密码重置",
"allowPasswordResetDesc": "通过 Docker 日志获取重置码",
"commandHistoryEnabled": "命令历史",
@@ -3317,11 +3337,11 @@
"createRole": "创建",
"creating": "正在创建...",
"exportDatabase": "导出数据库",
"exportDatabaseDesc": "下载包含所有主机、凭据和设置的备份",
"exportDatabaseDesc": "下载包含所有主机、凭据和设置的便携式 SQLite 备份",
"export": "导出",
"exporting": "正在导出...",
"importDatabase": "导入数据库",
"importDatabaseDesc": "从 .sqlite 备份文件恢复",
"importDatabaseDesc": "从任何数据库后端上的便携式 .sqlite 备份进行恢复",
"importDatabaseSelected": "已选择:{{name}}",
"selectFile": "选择文件",
"changeFile": "更改",
@@ -4516,6 +4536,7 @@
"selectProvider": "选择服务提供商",
"providerSettings": "提供商设置",
"addProvider": "添加提供商",
"editProvider": "编辑提供者",
"removeProvider": "移除提供者",
"providerType": "提供者",
"providerLabel": "姓名",
@@ -4526,6 +4547,7 @@
"defaultModelPlaceholder": "例如:llama3.1 或 claude-opus-5",
"labelRequired": "给这家服务提供商起个名字",
"providerSaveFailed": "无法保存提供商",
"providerUpdated": "提供商已更新",
"providerDeleteFailed": "无法移除提供商",
"privateEndpointHint": "您私有网络上的地址需要管理员先允许主机访问。",
"providerOllama": "奥拉玛",

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