mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: convert tmux monitoring into a plugin (B10)
This commit is contained in:
1 parent
79d77678c9
commit
708eedb861
145 files changed
+30308
-4458
No files matched your search
@@ -1950,79 +1950,6 @@ const migrateSchema = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// --- tmux-monitor begin ---
|
||||
try {
|
||||
sqlite.prepare("SELECT id FROM tmux_session_tags LIMIT 1").get();
|
||||
} catch {
|
||||
try {
|
||||
sqlite.exec(`
|
||||
CREATE TABLE IF NOT EXISTS tmux_session_tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
host_id INTEGER NOT NULL,
|
||||
session_name TEXT NOT NULL,
|
||||
tag TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
} catch (createError) {
|
||||
databaseLogger.warn("Failed to create tmux_session_tags table", {
|
||||
operation: "schema_migration",
|
||||
error: createError,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild pre-release tables created without the host FK so deleting a
|
||||
// host cascades to its tags (SQLite cannot add a FK via ALTER TABLE).
|
||||
try {
|
||||
const tagFks = sqlite
|
||||
.prepare("PRAGMA foreign_key_list(tmux_session_tags)")
|
||||
.all() as Array<{ table: string; from: string }>;
|
||||
const hasHostFk = tagFks.some(
|
||||
(fk) => fk.from === "host_id" && fk.table === "ssh_data",
|
||||
);
|
||||
if (!hasHostFk) {
|
||||
sqlite.exec(`
|
||||
BEGIN;
|
||||
CREATE TABLE tmux_session_tags_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
host_id INTEGER NOT NULL,
|
||||
session_name TEXT NOT NULL,
|
||||
tag TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO tmux_session_tags_new (id, user_id, host_id, session_name, tag, created_at)
|
||||
SELECT id, user_id, host_id, session_name, tag, created_at
|
||||
FROM tmux_session_tags
|
||||
WHERE user_id IN (SELECT id FROM users)
|
||||
AND host_id IN (SELECT id FROM ssh_data);
|
||||
DROP TABLE tmux_session_tags;
|
||||
ALTER TABLE tmux_session_tags_new RENAME TO tmux_session_tags;
|
||||
COMMIT;
|
||||
`);
|
||||
databaseLogger.info("Rebuilt tmux_session_tags with host FK", {
|
||||
operation: "schema_migration",
|
||||
});
|
||||
}
|
||||
} catch (rebuildError) {
|
||||
try {
|
||||
sqlite.exec("ROLLBACK;");
|
||||
} catch {
|
||||
// no transaction open
|
||||
}
|
||||
databaseLogger.warn("Failed to add host FK to tmux_session_tags", {
|
||||
operation: "schema_migration",
|
||||
error: rebuildError,
|
||||
});
|
||||
}
|
||||
// --- tmux-monitor end ---
|
||||
|
||||
try {
|
||||
const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>;
|
||||
|
||||
|
||||
@@ -1204,23 +1204,6 @@ export const termixIdentityCa = mysqlTable("termix_identity_ca", {
|
||||
});
|
||||
// --- termix-id end ---
|
||||
|
||||
// --- tmux-monitor begin ---
|
||||
export const tmuxSessionTags = mysqlTable("tmux_session_tags", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
userId: varchar("user_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
hostId: int("host_id")
|
||||
.notNull()
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
sessionName: text("session_name").notNull(),
|
||||
tag: text("tag").notNull(),
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
});
|
||||
// --- tmux-monitor end ---
|
||||
|
||||
// --- metrics-history begin ---
|
||||
export const hostMetricsHistory = mysqlTable("host_metrics_history", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
|
||||
@@ -1205,23 +1205,6 @@ export const termixIdentityCa = pgTable("termix_identity_ca", {
|
||||
});
|
||||
// --- termix-id end ---
|
||||
|
||||
// --- tmux-monitor begin ---
|
||||
export const tmuxSessionTags = pgTable("tmux_session_tags", {
|
||||
id: serial("id").primaryKey(),
|
||||
userId: varchar("user_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
sessionName: text("session_name").notNull(),
|
||||
tag: text("tag").notNull(),
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
// --- tmux-monitor end ---
|
||||
|
||||
// --- metrics-history begin ---
|
||||
export const hostMetricsHistory = pgTable("host_metrics_history", {
|
||||
id: serial("id").primaryKey(),
|
||||
|
||||
@@ -1203,23 +1203,6 @@ export const termixIdentityCa = sqliteTable("termix_identity_ca", {
|
||||
});
|
||||
// --- termix-id end ---
|
||||
|
||||
// --- tmux-monitor begin ---
|
||||
export const tmuxSessionTags = sqliteTable("tmux_session_tags", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
sessionName: text("session_name").notNull(),
|
||||
tag: text("tag").notNull(),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
// --- tmux-monitor end ---
|
||||
|
||||
// --- metrics-history begin ---
|
||||
export const hostMetricsHistory = sqliteTable("host_metrics_history", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
|
||||
@@ -53,7 +53,6 @@ import { SyncTombstoneRepository } from "./sync-tombstone-repository.js";
|
||||
import { SsoProviderRepository } from "./sso-provider-repository.js";
|
||||
import { TermixIdentityCaRepository } from "./termix-identity-ca-repository.js";
|
||||
import { TermixIdentityRepository } from "./termix-identity-repository.js";
|
||||
import { TmuxSessionTagRepository } from "./tmux-session-tag-repository.js";
|
||||
import { TrustedDeviceRepository } from "./trusted-device-repository.js";
|
||||
import { UserDataExportRepository } from "./user-data-export-repository.js";
|
||||
import { UserPreferenceRepository } from "./user-preference-repository.js";
|
||||
@@ -482,14 +481,8 @@ export function createCurrentTermixIdentityRepository(): TermixIdentityRepositor
|
||||
);
|
||||
}
|
||||
|
||||
export function createCurrentTmuxSessionTagRepository(): TmuxSessionTagRepository {
|
||||
return new TmuxSessionTagRepository(
|
||||
createCurrentRepositoryContext(),
|
||||
createCurrentRepositoryWriteHook("tmux_session_tag_repository_write"),
|
||||
);
|
||||
}
|
||||
|
||||
// transfer_recent moved to the file-manager plugin.
|
||||
// tmux_session_tags moved to the tmux-monitor plugin.
|
||||
|
||||
export function createCurrentTrustedDeviceRepository(): TrustedDeviceRepository {
|
||||
return new TrustedDeviceRepository(
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { tmuxSessionTags } from "../db/schema.js";
|
||||
import type { DatabaseContext } from "./database-context.js";
|
||||
import { rowsAffected } from "./mutation-result.js";
|
||||
|
||||
export type TmuxSessionTagRecord = typeof tmuxSessionTags.$inferSelect;
|
||||
|
||||
export interface TmuxSessionTagInput {
|
||||
userId: string;
|
||||
hostId: number;
|
||||
sessionName: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export class TmuxSessionTagRepository {
|
||||
constructor(
|
||||
private readonly context: DatabaseContext,
|
||||
private readonly onWrite?: () => void | Promise<void>,
|
||||
) {}
|
||||
|
||||
async listByUserAndHost(
|
||||
userId: string,
|
||||
hostId: number,
|
||||
): Promise<Map<string, string[]>> {
|
||||
const rows = await this.context.drizzle
|
||||
.select()
|
||||
.from(tmuxSessionTags)
|
||||
.where(
|
||||
and(
|
||||
eq(tmuxSessionTags.userId, userId),
|
||||
eq(tmuxSessionTags.hostId, hostId),
|
||||
),
|
||||
);
|
||||
|
||||
const bySession = new Map<string, string[]>();
|
||||
for (const row of rows) {
|
||||
const tags = bySession.get(row.sessionName) ?? [];
|
||||
tags.push(row.tag);
|
||||
bySession.set(row.sessionName, tags);
|
||||
}
|
||||
return bySession;
|
||||
}
|
||||
|
||||
async renameSessionForHost(
|
||||
hostId: number,
|
||||
sessionName: string,
|
||||
newSessionName: string,
|
||||
): Promise<number> {
|
||||
const result = await this.context.drizzle
|
||||
.update(tmuxSessionTags)
|
||||
.set({ sessionName: newSessionName })
|
||||
.where(
|
||||
and(
|
||||
eq(tmuxSessionTags.hostId, hostId),
|
||||
eq(tmuxSessionTags.sessionName, sessionName),
|
||||
),
|
||||
);
|
||||
|
||||
if (rowsAffected(result) > 0) {
|
||||
await this.afterWrite();
|
||||
}
|
||||
|
||||
return rowsAffected(result);
|
||||
}
|
||||
|
||||
async deleteSessionForHost(
|
||||
hostId: number,
|
||||
sessionName: string,
|
||||
): Promise<number> {
|
||||
const result = await this.context.drizzle
|
||||
.delete(tmuxSessionTags)
|
||||
.where(
|
||||
and(
|
||||
eq(tmuxSessionTags.hostId, hostId),
|
||||
eq(tmuxSessionTags.sessionName, sessionName),
|
||||
),
|
||||
);
|
||||
|
||||
if (rowsAffected(result) > 0) {
|
||||
await this.afterWrite();
|
||||
}
|
||||
|
||||
return rowsAffected(result);
|
||||
}
|
||||
|
||||
async replaceForUserHostSession(
|
||||
userId: string,
|
||||
hostId: number,
|
||||
sessionName: string,
|
||||
tags: string[],
|
||||
): Promise<number> {
|
||||
const result = await this.context.drizzle
|
||||
.delete(tmuxSessionTags)
|
||||
.where(
|
||||
and(
|
||||
eq(tmuxSessionTags.userId, userId),
|
||||
eq(tmuxSessionTags.hostId, hostId),
|
||||
eq(tmuxSessionTags.sessionName, sessionName),
|
||||
),
|
||||
);
|
||||
|
||||
if (tags.length > 0) {
|
||||
await this.context.drizzle.insert(tmuxSessionTags).values(
|
||||
tags.map((tag) => ({
|
||||
userId,
|
||||
hostId,
|
||||
sessionName,
|
||||
tag,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const changedRows = rowsAffected(result) + tags.length;
|
||||
if (changedRows > 0) {
|
||||
await this.afterWrite();
|
||||
}
|
||||
|
||||
return changedRows;
|
||||
}
|
||||
|
||||
async deleteByUserId(userId: string): Promise<number> {
|
||||
const result = await this.context.drizzle
|
||||
.delete(tmuxSessionTags)
|
||||
.where(eq(tmuxSessionTags.userId, userId));
|
||||
|
||||
if (rowsAffected(result) > 0) {
|
||||
await this.afterWrite();
|
||||
}
|
||||
|
||||
return rowsAffected(result);
|
||||
}
|
||||
|
||||
private async afterWrite(): Promise<void> {
|
||||
await this.onWrite?.();
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
createCurrentSshCredentialUsageRepository,
|
||||
createCurrentTermixIdentityCaRepository,
|
||||
createCurrentTermixIdentityRepository,
|
||||
createCurrentTmuxSessionTagRepository,
|
||||
createCurrentTrustedDeviceRepository,
|
||||
createCurrentUserPreferenceRepository,
|
||||
createCurrentUserRepository,
|
||||
@@ -112,7 +111,6 @@ export async function deleteUserAndRelatedData(
|
||||
await createCurrentSecretSourceRepository().deleteByUserId(userId);
|
||||
await createCurrentTermixIdentityCaRepository().deleteByUserId(userId);
|
||||
await createCurrentTermixIdentityRepository().deleteByUserId(userId);
|
||||
await createCurrentTmuxSessionTagRepository().deleteByUserId(userId);
|
||||
await createCurrentOpenTabRepository().deleteByUserId(userId);
|
||||
await createCurrentUserPreferenceRepository().deleteByUserId(userId);
|
||||
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import type { Client, ClientChannel } from "ssh2";
|
||||
import { sshLogger } from "../../utils/logger.js";
|
||||
|
||||
export interface TmuxSessionInfo {
|
||||
name: string;
|
||||
created: number;
|
||||
lastActivity: number;
|
||||
windows: number;
|
||||
attachedClients: number;
|
||||
}
|
||||
|
||||
export interface TmuxDetectionResult {
|
||||
available: boolean;
|
||||
sessions: TmuxSessionInfo[];
|
||||
}
|
||||
|
||||
const TMUX_PATH_DIRS = [
|
||||
"/opt/homebrew/bin",
|
||||
"/usr/local/bin",
|
||||
"/opt/bin",
|
||||
"/usr/pkg/bin",
|
||||
];
|
||||
|
||||
export function withTmuxPath(command: string): string {
|
||||
const script = `PATH=${TMUX_PATH_DIRS.join(":")}:"$PATH"; export PATH; ${command}`;
|
||||
return `/bin/sh -c ${shellEscape(script)}`;
|
||||
}
|
||||
|
||||
export function tmuxCommand(args: string): string {
|
||||
return withTmuxPath(`tmux -u ${args}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command on the remote host via a separate exec channel.
|
||||
* Returns stdout as a string. Does not pollute the interactive shell.
|
||||
*/
|
||||
export function execCommand(conn: Client, command: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
conn.exec(command, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
stream.on("data", (data: Buffer) => {
|
||||
stdout += data.toString("utf-8");
|
||||
});
|
||||
stream.stderr.on("data", (data: Buffer) => {
|
||||
stderr += data.toString("utf-8");
|
||||
});
|
||||
stream.on("error", (err: Error) => {
|
||||
reject(err);
|
||||
});
|
||||
stream.on("close", (code: number) => {
|
||||
if (code !== 0 && stdout === "") {
|
||||
reject(
|
||||
new Error(stderr.trim() || `Command exited with code ${code}`),
|
||||
);
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether tmux is installed and list all existing sessions with details.
|
||||
*/
|
||||
export async function detectTmux(conn: Client): Promise<TmuxDetectionResult> {
|
||||
try {
|
||||
await execCommand(conn, tmuxCommand("-V"));
|
||||
} catch {
|
||||
return { available: false, sessions: [] };
|
||||
}
|
||||
|
||||
let sessions: TmuxSessionInfo[] = [];
|
||||
try {
|
||||
const output = await execCommand(
|
||||
conn,
|
||||
tmuxCommand(
|
||||
`list-sessions -F "#{session_name}|#{session_created}|#{session_activity}|#{session_windows}|#{session_attached}" 2>/dev/null`,
|
||||
),
|
||||
);
|
||||
if (output) {
|
||||
sessions = output
|
||||
.split("\n")
|
||||
.filter((line) => line.length > 0)
|
||||
.map((line) => {
|
||||
const [name, created, activity, windows, attached] = line.split("|");
|
||||
return {
|
||||
name,
|
||||
created: parseInt(created, 10) || 0,
|
||||
lastActivity: parseInt(activity, 10) || 0,
|
||||
windows: parseInt(windows, 10) || 0,
|
||||
attachedClients: parseInt(attached, 10) || 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// tmux server not running yet -- no sessions exist
|
||||
}
|
||||
|
||||
return { available: true, sessions };
|
||||
}
|
||||
|
||||
// tmux options applied on every attach/create:
|
||||
// - mouse on: enables mouse wheel / touch scrollback through tmux history
|
||||
// - history-limit: deep scrollback buffer on the remote host
|
||||
// - set-clipboard on: use OSC 52 to sync tmux selections to the client clipboard
|
||||
// - mode-keys vi: use vi-style keys in copy mode
|
||||
// - MouseDragEnd: stop the selection but keep it highlighted so the user can
|
||||
// adjust and press Enter to copy (or drag again)
|
||||
// - Enter: copy the (possibly adjusted) selection and exit copy mode
|
||||
// - pane-mode-changed hook: on copy-mode entry, show a brief hint so users
|
||||
// know to press Enter to copy the selection
|
||||
// Using -q on set/set-hook to suppress errors on older tmux versions that don't support
|
||||
// a particular option (e.g. set-clipboard on tmux < 2.5). Note: set-hook doesn't support -q.
|
||||
const TMUX_OPTS =
|
||||
`set -gq mouse on` +
|
||||
` \\; set -gq history-limit 50000` +
|
||||
` \\; set -gq set-clipboard on` +
|
||||
` \\; set -gq aggressive-resize on` +
|
||||
` \\; set -gq mode-keys vi` +
|
||||
` \\; bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X stop-selection` +
|
||||
` \\; bind-key -T copy-mode-vi Enter send-keys -X copy-selection-and-cancel` +
|
||||
` \\; set-hook -g pane-mode-changed` +
|
||||
` 'if -F "#{pane_in_mode}"` +
|
||||
` "display-message -d 2500 \\"Adjust selection and press Enter to copy\\""'`;
|
||||
|
||||
/**
|
||||
* Wait for a tmux session to appear by polling via exec channel.
|
||||
* Returns the session name once found, or null on timeout.
|
||||
*/
|
||||
export async function waitForTmuxSession(
|
||||
conn: Client,
|
||||
sessionName: string,
|
||||
timeoutMs = 5000,
|
||||
intervalMs = 100,
|
||||
): Promise<string | null> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
await execCommand(
|
||||
conn,
|
||||
tmuxCommand(`has-session -t ${shellEscape(sessionName)} 2>/dev/null`),
|
||||
);
|
||||
return sessionName;
|
||||
} catch {
|
||||
// session not ready yet
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write tmux attach or new-session command to the interactive shell stream.
|
||||
* Uses && exit so the shell only closes if tmux started successfully.
|
||||
*/
|
||||
export function attachOrCreateTmuxSession(
|
||||
stream: ClientChannel,
|
||||
existingSessionName?: string,
|
||||
newSessionName?: string,
|
||||
): void {
|
||||
let command: string;
|
||||
if (existingSessionName) {
|
||||
command = `${tmuxCommand(`${TMUX_OPTS} \\; attach-session -t ${shellEscape(existingSessionName)}`)} && exit\r`;
|
||||
} else {
|
||||
const nameFlag = newSessionName ? ` -s ${shellEscape(newSessionName)}` : "";
|
||||
command = `${tmuxCommand(`${TMUX_OPTS} \\; new-session${nameFlag}`)} && exit\r`;
|
||||
}
|
||||
|
||||
sshLogger.info("Writing tmux command to shell", {
|
||||
operation: "tmux_attach_or_create",
|
||||
sessionName: existingSessionName || "(auto)",
|
||||
isReattach: !!existingSessionName,
|
||||
});
|
||||
|
||||
stream.write(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the name of the most recently created tmux session via exec channel.
|
||||
*/
|
||||
|
||||
function shellEscape(s: string): string {
|
||||
return "'" + s.replace(/'/g, "'\\''") + "'";
|
||||
}
|
||||
@@ -1,767 +0,0 @@
|
||||
import { getErrorMessage } from "../../utils/error-message.js";
|
||||
import express from "express";
|
||||
import cookieParser from "cookie-parser";
|
||||
import type { Client } from "ssh2";
|
||||
import { connectHost, getConnectionPoolKey } from "../connect/connect-host.js";
|
||||
import type { SshConnectHost } from "../connect/types.js";
|
||||
import { createCorsMiddleware } from "../../utils/cors-config.js";
|
||||
import { createCompressionMiddleware } from "../../utils/compression-config.js";
|
||||
import { AuthManager } from "../../utils/auth-manager.js";
|
||||
import { DataCrypto } from "../../utils/data-crypto.js";
|
||||
import {
|
||||
createCurrentTmuxSessionTagRepository,
|
||||
createCurrentUserRepository,
|
||||
} from "../../database/repositories/factory.js";
|
||||
import { logAudit, getRequestMeta } from "../../utils/audit-logger.js";
|
||||
import { sshLogger } from "../../utils/logger.js";
|
||||
import { resolveHostById, checkHostAccess } from "../host-resolver.js";
|
||||
import type { HostAction } from "../../utils/permission-manager.js";
|
||||
import { withConnection } from "../ssh-connection-pool.js";
|
||||
import { execCommand, tmuxCommand } from "./helper.js";
|
||||
import {
|
||||
SEP,
|
||||
parseSessions,
|
||||
parseWindows,
|
||||
parsePanes,
|
||||
parsePsOutput,
|
||||
parseGpuOutput,
|
||||
buildPaneMetrics,
|
||||
attachPanesToWindows,
|
||||
shellEscape,
|
||||
type RawPane,
|
||||
type TmuxSessionSummary,
|
||||
type TmuxWindow,
|
||||
type PaneMetrics,
|
||||
} from "./monitor-helpers.js";
|
||||
import type { SSHHost, AuthenticatedRequest } from "../../../types/index.js";
|
||||
import { listenOnServicePort } from "../../utils/service-listen.js";
|
||||
|
||||
const PANE_ID_RE = /^%\d+$/;
|
||||
// tmux session names cannot contain ":" or "."; keep to a conservative
|
||||
// printable subset so the name is safe as a tmux target everywhere.
|
||||
const SESSION_NAME_RE = /^[A-Za-z0-9_@%+=-]{1,64}$/;
|
||||
const MAX_SEARCH_PANES = 100;
|
||||
const MAX_MATCHES_PER_PANE = 50;
|
||||
const SEARCH_HISTORY_LINES = 2000;
|
||||
const SEARCH_CONCURRENCY = 4;
|
||||
|
||||
interface TmuxSessionOverview extends TmuxSessionSummary {
|
||||
windows: TmuxWindow[];
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
// SSH connection
|
||||
|
||||
export function connectToHost(host: SSHHost): () => Promise<Client> {
|
||||
return async () => {
|
||||
const connection = await connectHost(host as unknown as SshConnectHost, {
|
||||
userId: host.userId || "",
|
||||
purpose: "tmux",
|
||||
});
|
||||
return connection.client;
|
||||
};
|
||||
}
|
||||
|
||||
function getPoolKey(host: SSHHost): string {
|
||||
return getConnectionPoolKey(
|
||||
"tmux-monitor",
|
||||
host as unknown as SshConnectHost,
|
||||
);
|
||||
}
|
||||
|
||||
async function withHostConnection<T>(
|
||||
host: SSHHost,
|
||||
fn: (client: Client) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return withConnection(getPoolKey(host), connectToHost(host), fn);
|
||||
}
|
||||
|
||||
// tmux queries
|
||||
|
||||
async function tmuxAvailable(conn: Client): Promise<boolean> {
|
||||
try {
|
||||
await execCommand(conn, tmuxCommand("-V"));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTmuxList(conn: Client, command: string): Promise<string> {
|
||||
try {
|
||||
return await execCommand(conn, command);
|
||||
} catch {
|
||||
return ""; // tmux server not running -- no sessions
|
||||
}
|
||||
}
|
||||
|
||||
function listSessionsCmd(): string {
|
||||
return tmuxCommand(
|
||||
`list-sessions -F "#{session_name}${SEP}#{session_created}${SEP}#{session_activity}${SEP}#{session_attached}" 2>/dev/null`,
|
||||
);
|
||||
}
|
||||
|
||||
function listWindowsCmd(): string {
|
||||
return tmuxCommand(
|
||||
`list-windows -a -F "#{session_name}${SEP}#{window_index}${SEP}#{window_active}${SEP}#{window_name}" 2>/dev/null`,
|
||||
);
|
||||
}
|
||||
|
||||
function listPanesCmd(): string {
|
||||
return tmuxCommand(
|
||||
`list-panes -a -F "#{session_name}${SEP}#{window_index}${SEP}#{pane_id}${SEP}#{pane_index}${SEP}#{pane_pid}${SEP}#{pane_active}${SEP}#{pane_width}${SEP}#{pane_height}${SEP}#{pane_current_command}${SEP}#{pane_current_path}${SEP}#{pane_title}" 2>/dev/null`,
|
||||
);
|
||||
}
|
||||
|
||||
async function listPanesRaw(conn: Client): Promise<RawPane[]> {
|
||||
return parsePanes(await runTmuxList(conn, listPanesCmd()));
|
||||
}
|
||||
|
||||
async function fetchSessionTags(
|
||||
userId: string,
|
||||
hostId: number,
|
||||
): Promise<Map<string, string[]>> {
|
||||
return createCurrentTmuxSessionTagRepository().listByUserAndHost(
|
||||
userId,
|
||||
hostId,
|
||||
);
|
||||
}
|
||||
|
||||
async function collectPaneMetrics(
|
||||
conn: Client,
|
||||
panes: RawPane[],
|
||||
): Promise<PaneMetrics[]> {
|
||||
let psOutput = "";
|
||||
try {
|
||||
psOutput = await execCommand(
|
||||
conn,
|
||||
"ps -eo pid=,ppid=,pcpu=,pmem=,rss=,comm= 2>/dev/null",
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
// GPU memory per pid (best effort; nvidia-smi may not exist)
|
||||
let gpuOutput = "";
|
||||
try {
|
||||
gpuOutput = await execCommand(
|
||||
conn,
|
||||
"command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi --query-compute-apps=pid,used_gpu_memory --format=csv,noheader,nounits 2>/dev/null || true",
|
||||
);
|
||||
} catch {
|
||||
// no GPU on host
|
||||
}
|
||||
|
||||
return buildPaneMetrics(
|
||||
panes,
|
||||
parsePsOutput(psOutput),
|
||||
parseGpuOutput(gpuOutput),
|
||||
);
|
||||
}
|
||||
|
||||
// Express app
|
||||
|
||||
const app = express();
|
||||
app.set("trust proxy", "loopback");
|
||||
const authManager = AuthManager.getInstance();
|
||||
|
||||
app.use(createCompressionMiddleware());
|
||||
app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"]));
|
||||
app.use(cookieParser());
|
||||
app.use(express.json({ limit: "1mb" }));
|
||||
app.use((_req, res, next) => {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
next();
|
||||
});
|
||||
app.use(authManager.createAuthMiddleware());
|
||||
|
||||
/**
|
||||
* Resolve the host for a request and verify the user can access it.
|
||||
* Sends the error response and returns null when access is denied.
|
||||
*/
|
||||
async function requireHost(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
permission: HostAction = "connect",
|
||||
): Promise<SSHHost | null> {
|
||||
const userId = (req as unknown as AuthenticatedRequest).userId;
|
||||
const hostId = parseInt(String(req.params.hostId), 10);
|
||||
if (isNaN(hostId)) {
|
||||
res.status(400).json({ error: "Invalid host ID" });
|
||||
return null;
|
||||
}
|
||||
if (DataCrypto.getUserDataKey(userId) === null) {
|
||||
res.status(401).json({ error: "User data is locked" });
|
||||
return null;
|
||||
}
|
||||
|
||||
let host: SSHHost | null = null;
|
||||
try {
|
||||
host = await resolveHostById(hostId, userId);
|
||||
} catch (err) {
|
||||
sshLogger.error(`Failed to resolve host ${hostId} for tmux monitor`, err);
|
||||
}
|
||||
if (!host) {
|
||||
res.status(404).json({ error: "Host not found" });
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasAccess = await checkHostAccess(
|
||||
hostId,
|
||||
userId,
|
||||
host.userId || userId,
|
||||
permission,
|
||||
);
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({ error: "Access denied" });
|
||||
return null;
|
||||
}
|
||||
|
||||
// The monitor is opt-in per host (same pattern as enableDocker in
|
||||
// docker.ts): hiding the UI is not enough, the API must refuse too.
|
||||
if (!host.enableTmuxMonitor) {
|
||||
res
|
||||
.status(403)
|
||||
.json({ error: "Tmux Monitor is not enabled for this host" });
|
||||
return null;
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
function toErrorMessage(err: unknown): string {
|
||||
return getErrorMessage(err);
|
||||
}
|
||||
|
||||
// Destructive tmux actions terminate processes on the remote host, so they
|
||||
// land in the audit log like other host-level mutations (see host.ts).
|
||||
async function auditTmuxAction(
|
||||
req: express.Request,
|
||||
host: SSHHost,
|
||||
action: string,
|
||||
resourceName: string,
|
||||
details?: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const userId = (req as unknown as AuthenticatedRequest).userId;
|
||||
const { ipAddress, userAgent } = getRequestMeta(req);
|
||||
let username = userId;
|
||||
try {
|
||||
const actor = await createCurrentUserRepository().findById(userId);
|
||||
username = actor?.username ?? userId;
|
||||
} catch {
|
||||
// fall back to the raw user id
|
||||
}
|
||||
await logAudit({
|
||||
userId,
|
||||
username,
|
||||
action,
|
||||
resourceType: "host",
|
||||
resourceId: String(host.id),
|
||||
resourceName,
|
||||
details: details ? JSON.stringify(details) : undefined,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Typed error codes so the frontend can render a helpful state instead of a
|
||||
// raw 500 (same pattern as SESSION_EXPIRED handling in main-axios).
|
||||
type TmuxErrorCode =
|
||||
"TMUX_NOT_INSTALLED" | "TMUX_NO_SERVER" | "HOST_UNREACHABLE" | "TMUX_ERROR";
|
||||
|
||||
function classifyTmuxError(err: unknown): TmuxErrorCode {
|
||||
const msg = getErrorMessage(err, "");
|
||||
if (/command not found|exited with code 127/i.test(msg))
|
||||
return "TMUX_NOT_INSTALLED";
|
||||
if (/no server running|lost server/i.test(msg)) return "TMUX_NO_SERVER";
|
||||
if (
|
||||
/timeout|timed out|econnrefused|ehostunreach|enotfound|enetunreach|econnreset|authentication|handshake|keepalive/i.test(
|
||||
msg,
|
||||
)
|
||||
)
|
||||
return "HOST_UNREACHABLE";
|
||||
return "TMUX_ERROR";
|
||||
}
|
||||
|
||||
function sendTmuxError(
|
||||
res: express.Response,
|
||||
err: unknown,
|
||||
context: string,
|
||||
hostId: number,
|
||||
): void {
|
||||
const code = classifyTmuxError(err);
|
||||
const status = code === "TMUX_ERROR" ? 500 : 503;
|
||||
const error =
|
||||
code === "TMUX_NOT_INSTALLED"
|
||||
? "tmux is not installed on this host"
|
||||
: code === "TMUX_NO_SERVER"
|
||||
? "No tmux server is running on this host"
|
||||
: code === "HOST_UNREACHABLE"
|
||||
? "Could not connect to the host"
|
||||
: toErrorMessage(err);
|
||||
sshLogger.error(`tmux ${context} failed for host ${hostId}`, err);
|
||||
res.status(status).json({ error, code });
|
||||
}
|
||||
|
||||
app.get("/tmux_monitor/:hostId/overview", async (req, res) => {
|
||||
const userId = (req as unknown as AuthenticatedRequest).userId;
|
||||
const host = await requireHost(req, res);
|
||||
if (!host) return;
|
||||
|
||||
try {
|
||||
const result = await withHostConnection(host, async (conn) => {
|
||||
if (!(await tmuxAvailable(conn))) {
|
||||
return { available: false, sessions: [] as TmuxSessionOverview[] };
|
||||
}
|
||||
const [sessionsOut, windowsOut, panesOut] = await Promise.all([
|
||||
runTmuxList(conn, listSessionsCmd()),
|
||||
runTmuxList(conn, listWindowsCmd()),
|
||||
runTmuxList(conn, listPanesCmd()),
|
||||
]);
|
||||
const sessions = parseSessions(sessionsOut);
|
||||
const windows = parseWindows(windowsOut);
|
||||
attachPanesToWindows(windows, parsePanes(panesOut));
|
||||
|
||||
const tags = await fetchSessionTags(userId, host.id);
|
||||
const full: TmuxSessionOverview[] = sessions.map((s) => ({
|
||||
...s,
|
||||
windows: windows.get(s.name) || [],
|
||||
tags: tags.get(s.name) || [],
|
||||
}));
|
||||
return { available: true, sessions: full };
|
||||
});
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
sendTmuxError(res, err, "overview", host.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Focus a pane: select its window and pane on the server so every attached
|
||||
// client (including the monitor's embedded terminal) switches to it.
|
||||
app.post("/tmux_monitor/:hostId/focus", async (req, res) => {
|
||||
const host = await requireHost(req, res, "connect");
|
||||
if (!host) return;
|
||||
|
||||
const paneId = String((req.body as { paneId?: string })?.paneId || "");
|
||||
if (!PANE_ID_RE.test(paneId)) {
|
||||
return res.status(400).json({ error: "Invalid pane ID" });
|
||||
}
|
||||
|
||||
try {
|
||||
await withHostConnection(host, (conn) =>
|
||||
// A pane id is a valid window target: tmux resolves it to the window
|
||||
// containing the pane.
|
||||
execCommand(
|
||||
conn,
|
||||
tmuxCommand(
|
||||
`select-window -t ${shellEscape(paneId)} \\; select-pane -t ${shellEscape(paneId)}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
sendTmuxError(res, err, "focus", host.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Create a detached session. Starts the tmux server if none is running.
|
||||
app.post("/tmux_monitor/:hostId/sessions", async (req, res) => {
|
||||
const host = await requireHost(req, res, "connect");
|
||||
if (!host) return;
|
||||
|
||||
const name = String((req.body as { name?: string })?.name || "").trim();
|
||||
if (!SESSION_NAME_RE.test(name)) {
|
||||
return res.status(400).json({ error: "Invalid session name" });
|
||||
}
|
||||
|
||||
try {
|
||||
await withHostConnection(host, (conn) =>
|
||||
execCommand(conn, tmuxCommand(`new-session -d -s ${shellEscape(name)}`)),
|
||||
);
|
||||
sshLogger.info("tmux session created", {
|
||||
operation: "tmux_session_create",
|
||||
hostId: host.id,
|
||||
sessionName: name,
|
||||
});
|
||||
res.json({ ok: true, name });
|
||||
} catch (err) {
|
||||
if (/duplicate session/i.test(toErrorMessage(err))) {
|
||||
return res
|
||||
.status(409)
|
||||
.json({ error: "A session with this name already exists" });
|
||||
}
|
||||
sendTmuxError(res, err, "create session", host.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Create a window in an existing session. The session name comes from tmux's
|
||||
// own listing, so it is only checked for characters that would change the
|
||||
// target's meaning (":" and "." are window/pane separators in tmux targets);
|
||||
// "=" prefixes the target for an exact-name match.
|
||||
app.post("/tmux_monitor/:hostId/windows", async (req, res) => {
|
||||
const host = await requireHost(req, res, "connect");
|
||||
if (!host) return;
|
||||
|
||||
const sessionName = String(
|
||||
(req.body as { sessionName?: string })?.sessionName || "",
|
||||
).trim();
|
||||
if (!sessionName || /[:.\n]/.test(sessionName)) {
|
||||
return res.status(400).json({ error: "Invalid session name" });
|
||||
}
|
||||
|
||||
try {
|
||||
await withHostConnection(host, (conn) =>
|
||||
execCommand(
|
||||
conn,
|
||||
tmuxCommand(`new-window -t ${shellEscape(`=${sessionName}`)}`),
|
||||
),
|
||||
);
|
||||
sshLogger.info("tmux window created", {
|
||||
operation: "tmux_window_create",
|
||||
hostId: host.id,
|
||||
sessionName,
|
||||
});
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
if (/can't find session|no such session/i.test(toErrorMessage(err))) {
|
||||
return res.status(404).json({ error: "Session not found" });
|
||||
}
|
||||
sendTmuxError(res, err, "create window", host.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Rename a session. Saved tags follow the session to its new name (for every
|
||||
// user — the session itself is shared on the host).
|
||||
app.post("/tmux_monitor/:hostId/rename", async (req, res) => {
|
||||
const host = await requireHost(req, res, "connect");
|
||||
if (!host) return;
|
||||
|
||||
const body = req.body as { sessionName?: string; newName?: string };
|
||||
const sessionName = String(body?.sessionName || "").trim();
|
||||
const newName = String(body?.newName || "").trim();
|
||||
if (!sessionName || /[:.\n]/.test(sessionName)) {
|
||||
return res.status(400).json({ error: "Invalid session name" });
|
||||
}
|
||||
if (!SESSION_NAME_RE.test(newName)) {
|
||||
return res.status(400).json({ error: "Invalid new session name" });
|
||||
}
|
||||
|
||||
try {
|
||||
await withHostConnection(host, (conn) =>
|
||||
execCommand(
|
||||
conn,
|
||||
tmuxCommand(
|
||||
`rename-session -t ${shellEscape(`=${sessionName}`)} ${shellEscape(newName)}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
await createCurrentTmuxSessionTagRepository().renameSessionForHost(
|
||||
host.id,
|
||||
sessionName,
|
||||
newName,
|
||||
);
|
||||
sshLogger.info("tmux session renamed", {
|
||||
operation: "tmux_session_rename",
|
||||
hostId: host.id,
|
||||
sessionName,
|
||||
newName,
|
||||
});
|
||||
await auditTmuxAction(req, host, "tmux_session_rename", sessionName, {
|
||||
newName,
|
||||
});
|
||||
res.json({ ok: true, name: newName });
|
||||
} catch (err) {
|
||||
if (/can't find session|no such session/i.test(toErrorMessage(err))) {
|
||||
return res.status(404).json({ error: "Session not found" });
|
||||
}
|
||||
if (/duplicate session/i.test(toErrorMessage(err))) {
|
||||
return res
|
||||
.status(409)
|
||||
.json({ error: "A session with this name already exists" });
|
||||
}
|
||||
sendTmuxError(res, err, "rename session", host.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Kill a session and drop its saved tags.
|
||||
app.post("/tmux_monitor/:hostId/kill", async (req, res) => {
|
||||
const host = await requireHost(req, res, "connect");
|
||||
if (!host) return;
|
||||
|
||||
const sessionName = String(
|
||||
(req.body as { sessionName?: string })?.sessionName || "",
|
||||
).trim();
|
||||
if (!sessionName || /[:.\n]/.test(sessionName)) {
|
||||
return res.status(400).json({ error: "Invalid session name" });
|
||||
}
|
||||
|
||||
try {
|
||||
await withHostConnection(host, (conn) =>
|
||||
execCommand(
|
||||
conn,
|
||||
tmuxCommand(`kill-session -t ${shellEscape(`=${sessionName}`)}`),
|
||||
),
|
||||
);
|
||||
await createCurrentTmuxSessionTagRepository().deleteSessionForHost(
|
||||
host.id,
|
||||
sessionName,
|
||||
);
|
||||
sshLogger.info("tmux session killed", {
|
||||
operation: "tmux_session_kill",
|
||||
hostId: host.id,
|
||||
sessionName,
|
||||
});
|
||||
await auditTmuxAction(req, host, "tmux_session_kill", sessionName);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
if (/can't find session|no such session/i.test(toErrorMessage(err))) {
|
||||
return res.status(404).json({ error: "Session not found" });
|
||||
}
|
||||
sendTmuxError(res, err, "kill session", host.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Kill a window (and every pane in it). Killing the last window of a session
|
||||
// ends the session — tmux semantics.
|
||||
app.post("/tmux_monitor/:hostId/kill-window", async (req, res) => {
|
||||
const host = await requireHost(req, res, "connect");
|
||||
if (!host) return;
|
||||
|
||||
const body = req.body as { sessionName?: string; windowIndex?: number };
|
||||
const sessionName = String(body?.sessionName || "").trim();
|
||||
const windowIndex = Number(body?.windowIndex);
|
||||
if (!sessionName || /[:.\n]/.test(sessionName)) {
|
||||
return res.status(400).json({ error: "Invalid session name" });
|
||||
}
|
||||
if (!Number.isInteger(windowIndex) || windowIndex < 0) {
|
||||
return res.status(400).json({ error: "Invalid window index" });
|
||||
}
|
||||
|
||||
try {
|
||||
await withHostConnection(host, (conn) =>
|
||||
execCommand(
|
||||
conn,
|
||||
tmuxCommand(
|
||||
`kill-window -t ${shellEscape(`=${sessionName}:${windowIndex}`)}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
sshLogger.info("tmux window killed", {
|
||||
operation: "tmux_window_kill",
|
||||
hostId: host.id,
|
||||
sessionName,
|
||||
windowIndex,
|
||||
});
|
||||
await auditTmuxAction(req, host, "tmux_window_kill", sessionName, {
|
||||
windowIndex,
|
||||
});
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
if (
|
||||
/can't find window|no such window|can't find session/i.test(
|
||||
toErrorMessage(err),
|
||||
)
|
||||
) {
|
||||
return res.status(404).json({ error: "Window not found" });
|
||||
}
|
||||
sendTmuxError(res, err, "kill window", host.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Kill a single pane. Killing the last pane of a window closes the window,
|
||||
// and the last window of a session ends the session — tmux semantics.
|
||||
app.post("/tmux_monitor/:hostId/kill-pane", async (req, res) => {
|
||||
const host = await requireHost(req, res, "connect");
|
||||
if (!host) return;
|
||||
|
||||
const paneId = String((req.body as { paneId?: string })?.paneId || "");
|
||||
if (!PANE_ID_RE.test(paneId)) {
|
||||
return res.status(400).json({ error: "Invalid pane ID" });
|
||||
}
|
||||
|
||||
try {
|
||||
await withHostConnection(host, (conn) =>
|
||||
execCommand(conn, tmuxCommand(`kill-pane -t ${shellEscape(paneId)}`)),
|
||||
);
|
||||
sshLogger.info("tmux pane killed", {
|
||||
operation: "tmux_pane_kill",
|
||||
hostId: host.id,
|
||||
paneId,
|
||||
});
|
||||
await auditTmuxAction(req, host, "tmux_pane_kill", paneId);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
if (/can't find pane|no such pane/i.test(toErrorMessage(err))) {
|
||||
return res.status(404).json({ error: "Pane not found" });
|
||||
}
|
||||
sendTmuxError(res, err, "kill pane", host.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Split the window containing a pane. "h" places the new pane to the right,
|
||||
// "v" below — matching tmux's own -h/-v semantics. The new pane starts in the
|
||||
// source pane's working directory.
|
||||
app.post("/tmux_monitor/:hostId/split", async (req, res) => {
|
||||
const host = await requireHost(req, res, "connect");
|
||||
if (!host) return;
|
||||
|
||||
const body = req.body as { paneId?: string; direction?: string };
|
||||
const paneId = String(body?.paneId || "");
|
||||
const direction = body?.direction === "v" ? "-v" : "-h";
|
||||
if (!PANE_ID_RE.test(paneId)) {
|
||||
return res.status(400).json({ error: "Invalid pane ID" });
|
||||
}
|
||||
if (body?.direction !== "h" && body?.direction !== "v") {
|
||||
return res.status(400).json({ error: "Invalid split direction" });
|
||||
}
|
||||
|
||||
try {
|
||||
await withHostConnection(host, (conn) =>
|
||||
execCommand(
|
||||
conn,
|
||||
tmuxCommand(
|
||||
`split-window ${direction} -t ${shellEscape(paneId)} -c ${shellEscape("#{pane_current_path}")}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
sshLogger.info("tmux pane split", {
|
||||
operation: "tmux_pane_split",
|
||||
hostId: host.id,
|
||||
paneId,
|
||||
direction: body.direction,
|
||||
});
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
sendTmuxError(res, err, "split", host.id);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/tmux_monitor/:hostId/search", async (req, res) => {
|
||||
const host = await requireHost(req, res);
|
||||
if (!host) return;
|
||||
|
||||
const query = String(req.query.q || "").trim();
|
||||
if (!query) {
|
||||
return res.status(400).json({ error: "Missing search query" });
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await withHostConnection(host, async (conn) => {
|
||||
const allPanes = await listPanesRaw(conn);
|
||||
const panes = allPanes.slice(0, MAX_SEARCH_PANES);
|
||||
// Flips to true whenever a limit was hit, so the UI can tell the user
|
||||
// the results are partial instead of silently truncating.
|
||||
let truncated = allPanes.length > MAX_SEARCH_PANES;
|
||||
const matches: Array<{
|
||||
paneId: string;
|
||||
sessionName: string;
|
||||
windowIndex: number;
|
||||
line: number;
|
||||
text: string;
|
||||
}> = [];
|
||||
|
||||
// Bounded concurrency; each search runs capture+grep remotely so only
|
||||
// matching lines travel back over the wire.
|
||||
for (let i = 0; i < panes.length; i += SEARCH_CONCURRENCY) {
|
||||
const batch = panes.slice(i, i + SEARCH_CONCURRENCY);
|
||||
await Promise.all(
|
||||
batch.map(async (pane) => {
|
||||
try {
|
||||
const output = await execCommand(
|
||||
conn,
|
||||
tmuxCommand(
|
||||
`capture-pane -p -J -t ${shellEscape(pane.id)} -S -${SEARCH_HISTORY_LINES} 2>/dev/null | grep -n -i -F -- ${shellEscape(query)} | head -${MAX_MATCHES_PER_PANE}`,
|
||||
),
|
||||
);
|
||||
const lines = output.split("\n").filter(Boolean);
|
||||
if (lines.length >= MAX_MATCHES_PER_PANE) truncated = true;
|
||||
for (const line of lines) {
|
||||
const sep = line.indexOf(":");
|
||||
if (sep === -1) continue;
|
||||
matches.push({
|
||||
paneId: pane.id,
|
||||
sessionName: pane.sessionName,
|
||||
windowIndex: pane.windowIndex,
|
||||
line: parseInt(line.slice(0, sep), 10) || 0,
|
||||
text: line.slice(sep + 1).slice(0, 500),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// grep exits non-zero when there are no matches -- not an error
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
return { matches, truncated };
|
||||
});
|
||||
res.json({
|
||||
query,
|
||||
matches: results.matches,
|
||||
truncated: results.truncated,
|
||||
searchedLines: SEARCH_HISTORY_LINES,
|
||||
maxPanes: MAX_SEARCH_PANES,
|
||||
});
|
||||
} catch (err) {
|
||||
sendTmuxError(res, err, "search", host.id);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/tmux_monitor/:hostId/metrics", async (req, res) => {
|
||||
const host = await requireHost(req, res);
|
||||
if (!host) return;
|
||||
|
||||
try {
|
||||
const metrics = await withHostConnection(host, async (conn) => {
|
||||
const panes = await listPanesRaw(conn);
|
||||
if (panes.length === 0) return [];
|
||||
return collectPaneMetrics(conn, panes);
|
||||
});
|
||||
res.json({ panes: metrics });
|
||||
} catch (err) {
|
||||
sendTmuxError(res, err, "metrics", host.id);
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/tmux_monitor/:hostId/tags", async (req, res) => {
|
||||
const userId = (req as unknown as AuthenticatedRequest).userId;
|
||||
const host = await requireHost(req, res);
|
||||
if (!host) return;
|
||||
|
||||
const { sessionName, tags } = req.body as {
|
||||
sessionName?: string;
|
||||
tags?: string[];
|
||||
};
|
||||
if (!sessionName || typeof sessionName !== "string") {
|
||||
return res.status(400).json({ error: "Missing session name" });
|
||||
}
|
||||
if (!Array.isArray(tags) || tags.some((t) => typeof t !== "string")) {
|
||||
return res.status(400).json({ error: "Tags must be an array of strings" });
|
||||
}
|
||||
const cleanTags = [
|
||||
...new Set(tags.map((t) => t.trim().slice(0, 64)).filter(Boolean)),
|
||||
].slice(0, 20);
|
||||
|
||||
try {
|
||||
await createCurrentTmuxSessionTagRepository().replaceForUserHostSession(
|
||||
userId,
|
||||
host.id,
|
||||
sessionName,
|
||||
cleanTags,
|
||||
);
|
||||
res.json({ sessionName, tags: cleanTags });
|
||||
} catch (err) {
|
||||
sshLogger.error(
|
||||
`Failed to save tmux session tags for host ${host.id}`,
|
||||
err,
|
||||
);
|
||||
res.status(500).json({ error: toErrorMessage(err) });
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = 30010;
|
||||
listenOnServicePort({
|
||||
app,
|
||||
port: PORT,
|
||||
logger: sshLogger,
|
||||
serviceName: "tmux",
|
||||
});
|
||||
@@ -1,250 +0,0 @@
|
||||
// Pure parsing/aggregation helpers for the tmux monitor module.
|
||||
// Kept free of SSH/Express dependencies so they can be unit-tested.
|
||||
|
||||
// Field separator used in tmux -F format strings. Session names, pane titles
|
||||
// and paths may contain "|", and tmux sanitizes control characters (and, under
|
||||
// non-UTF-8 locales, multibyte characters) in format output to "_", so a
|
||||
// printable ASCII token is the only separator that survives everywhere.
|
||||
export const SEP = "<<TMX>>";
|
||||
|
||||
export interface TmuxPane {
|
||||
id: string;
|
||||
index: number;
|
||||
pid: number;
|
||||
active: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
command: string;
|
||||
path: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface TmuxWindow {
|
||||
index: number;
|
||||
name: string;
|
||||
active: boolean;
|
||||
panes: TmuxPane[];
|
||||
}
|
||||
|
||||
export interface TmuxSessionSummary {
|
||||
name: string;
|
||||
created: number;
|
||||
lastActivity: number;
|
||||
attachedClients: number;
|
||||
}
|
||||
|
||||
export interface RawPane extends TmuxPane {
|
||||
sessionName: string;
|
||||
windowIndex: number;
|
||||
}
|
||||
|
||||
export interface ProcessInfo {
|
||||
pid: number;
|
||||
ppid: number;
|
||||
cpu: number;
|
||||
mem: number;
|
||||
rss: number;
|
||||
comm: string;
|
||||
}
|
||||
|
||||
export interface PaneMetrics {
|
||||
paneId: string;
|
||||
sessionName: string;
|
||||
pid: number;
|
||||
processCount: number;
|
||||
cpuPercent: number;
|
||||
memRssKb: number;
|
||||
gpuMemMb: number;
|
||||
topCommand: string | null;
|
||||
}
|
||||
|
||||
export function parseSessions(output: string): TmuxSessionSummary[] {
|
||||
return output
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [name, created, activity, attached] = line.split(SEP);
|
||||
return {
|
||||
name,
|
||||
created: parseInt(created, 10) || 0,
|
||||
lastActivity: parseInt(activity, 10) || 0,
|
||||
attachedClients: parseInt(attached, 10) || 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function parseWindows(output: string): Map<string, TmuxWindow[]> {
|
||||
const bySession = new Map<string, TmuxWindow[]>();
|
||||
for (const line of output.split("\n").filter(Boolean)) {
|
||||
const [session, index, active, name] = line.split(SEP);
|
||||
if (!bySession.has(session)) bySession.set(session, []);
|
||||
bySession.get(session)!.push({
|
||||
index: parseInt(index, 10) || 0,
|
||||
name: name || "",
|
||||
active: active === "1",
|
||||
panes: [],
|
||||
});
|
||||
}
|
||||
return bySession;
|
||||
}
|
||||
|
||||
export function parsePanes(output: string): RawPane[] {
|
||||
return output
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [
|
||||
sessionName,
|
||||
windowIndex,
|
||||
id,
|
||||
index,
|
||||
pid,
|
||||
active,
|
||||
width,
|
||||
height,
|
||||
command,
|
||||
path,
|
||||
title,
|
||||
] = line.split(SEP);
|
||||
return {
|
||||
sessionName,
|
||||
windowIndex: parseInt(windowIndex, 10) || 0,
|
||||
id,
|
||||
index: parseInt(index, 10) || 0,
|
||||
pid: parseInt(pid, 10) || 0,
|
||||
active: active === "1",
|
||||
width: parseInt(width, 10) || 0,
|
||||
height: parseInt(height, 10) || 0,
|
||||
command: command || "",
|
||||
path: path || "",
|
||||
title: title || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function parsePsOutput(output: string): ProcessInfo[] {
|
||||
const processes: ProcessInfo[] = [];
|
||||
for (const line of output.split("\n")) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length < 6) continue;
|
||||
const pid = parseInt(parts[0], 10);
|
||||
const ppid = parseInt(parts[1], 10);
|
||||
if (isNaN(pid) || isNaN(ppid)) continue;
|
||||
processes.push({
|
||||
pid,
|
||||
ppid,
|
||||
cpu: parseFloat(parts[2]) || 0,
|
||||
mem: parseFloat(parts[3]) || 0,
|
||||
rss: parseInt(parts[4], 10) || 0,
|
||||
comm: parts.slice(5).join(" "),
|
||||
});
|
||||
}
|
||||
return processes;
|
||||
}
|
||||
|
||||
export function parseGpuOutput(output: string): Map<number, number> {
|
||||
const gpuByPid = new Map<number, number>();
|
||||
for (const line of output.split("\n").filter(Boolean)) {
|
||||
const [pid, mem] = line.split(",").map((s) => s.trim());
|
||||
const pidNum = parseInt(pid, 10);
|
||||
const memNum = parseInt(mem, 10);
|
||||
if (!isNaN(pidNum) && !isNaN(memNum)) {
|
||||
gpuByPid.set(pidNum, (gpuByPid.get(pidNum) || 0) + memNum);
|
||||
}
|
||||
}
|
||||
return gpuByPid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map each pane's shell pid to its descendant process tree and aggregate
|
||||
* CPU/RAM/GPU usage per pane.
|
||||
*/
|
||||
export function buildPaneMetrics(
|
||||
panes: RawPane[],
|
||||
processes: ProcessInfo[],
|
||||
gpuByPid: Map<number, number>,
|
||||
): PaneMetrics[] {
|
||||
const byPid = new Map<number, ProcessInfo>();
|
||||
const childrenOf = new Map<number, number[]>();
|
||||
for (const p of processes) {
|
||||
byPid.set(p.pid, p);
|
||||
if (!childrenOf.has(p.ppid)) childrenOf.set(p.ppid, []);
|
||||
childrenOf.get(p.ppid)!.push(p.pid);
|
||||
}
|
||||
|
||||
return panes.map((pane) => {
|
||||
// Walk the descendant tree starting at (and including) the pane's shell
|
||||
const treePids: number[] = [];
|
||||
const queue = [pane.pid];
|
||||
const seen = new Set<number>();
|
||||
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);
|
||||
for (const child of childrenOf.get(pid) || []) queue.push(child);
|
||||
}
|
||||
|
||||
let cpuPercent = 0;
|
||||
let memRssKb = 0;
|
||||
let gpuMemMb = 0;
|
||||
let topCommand: string | null = null;
|
||||
let topCpu = -1;
|
||||
for (const pid of treePids) {
|
||||
const p = byPid.get(pid)!;
|
||||
cpuPercent += p.cpu;
|
||||
memRssKb += p.rss;
|
||||
gpuMemMb += gpuByPid.get(pid) || 0;
|
||||
// The pane shell itself is rarely the interesting process
|
||||
if (p.cpu > topCpu && pid !== pane.pid) {
|
||||
topCpu = p.cpu;
|
||||
topCommand = p.comm;
|
||||
}
|
||||
}
|
||||
if (topCommand === null && treePids.length > 0) {
|
||||
topCommand = byPid.get(treePids[0])!.comm;
|
||||
}
|
||||
|
||||
return {
|
||||
paneId: pane.id,
|
||||
sessionName: pane.sessionName,
|
||||
pid: pane.pid,
|
||||
processCount: treePids.length,
|
||||
cpuPercent: Math.round(cpuPercent * 10) / 10,
|
||||
memRssKb,
|
||||
gpuMemMb,
|
||||
topCommand,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Group panes into their windows (mutates the window objects' pane arrays).
|
||||
*/
|
||||
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 window = windowsBySessionAndIndex
|
||||
.get(pane.sessionName)
|
||||
?.get(pane.windowIndex);
|
||||
if (window) {
|
||||
const { sessionName: _s, windowIndex: _w, ...paneFields } = pane;
|
||||
window.panes.push(paneFields);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function shellEscape(s: string): string {
|
||||
return "'" + s.replace(/'/g, "'\\''") + "'";
|
||||
}
|
||||
@@ -60,6 +60,7 @@ const PLUGIN_PURPOSES = new Set<SshConnectPurpose>([
|
||||
"file-manager",
|
||||
"file-transfer",
|
||||
"terminal",
|
||||
"tmux",
|
||||
]);
|
||||
|
||||
function purposeOf(options?: PluginSshConnectOptions): SshConnectPurpose {
|
||||
|
||||
+10
-6
@@ -302,18 +302,18 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
|
||||
const { primeKnownPermissions } =
|
||||
await import("./utils/known-permissions.js");
|
||||
await primeKnownPermissions();
|
||||
// Terminal, docker, host-metrics and file-manager are deliberately
|
||||
// absent: the ssh-terminal, docker, host-metrics and file-manager plugins
|
||||
// start their own servers, so disabling any of them stops its WS/HTTP
|
||||
// server. See plugins/ssh-terminal, plugins/docker, plugins/host-metrics
|
||||
// and plugins/file-manager.
|
||||
// Terminal, docker, host-metrics, file-manager and tmux monitoring are
|
||||
// deliberately absent: the ssh-terminal, docker, host-metrics,
|
||||
// file-manager and tmux-monitor plugins start their own servers, so
|
||||
// disabling any of them stops its WS/HTTP server. See
|
||||
// plugins/ssh-terminal, plugins/docker, plugins/host-metrics,
|
||||
// plugins/file-manager and plugins/tmux-monitor.
|
||||
// Every other plugin (AI, Proxmox, Remote Desktop, Fleets, Automations,
|
||||
// Network Topology, Workspaces, Web Endpoint, Tunnels) is absent for the
|
||||
// same reason: each one serves its routes under /plugin-api/<id>/ through
|
||||
// ctx.http on activate, so disabling it answers 503 instead of leaving a
|
||||
// dead import here. Automations' scheduler and tunnel autostart also
|
||||
// start from their own activate() rather than here.
|
||||
await import("./hosts/tmux/index.js");
|
||||
await import("./hosts/serial.js");
|
||||
await import("./services/dashboard.js");
|
||||
await import("./services/homepage.js");
|
||||
@@ -365,6 +365,10 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
|
||||
const { runSshTerminalSettingsMigration } =
|
||||
await import("./utils/crypto-migration/ssh-terminal-settings-migration.js");
|
||||
await runSshTerminalSettingsMigration();
|
||||
|
||||
const { runTmuxMonitorSettingsMigration } =
|
||||
await import("./utils/crypto-migration/tmux-monitor-settings-migration.js");
|
||||
await runTmuxMonitorSettingsMigration();
|
||||
} catch (error) {
|
||||
systemLogger.warn("Plugin runtime failed to initialize", {
|
||||
operation: "plugin_init",
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { TestSqliteDatabase } from "./test-support.js";
|
||||
import { TmuxSessionTagRepository } from "../../../database/repositories/tmux-session-tag-repository.js";
|
||||
|
||||
describe("TmuxSessionTagRepository", () => {
|
||||
let adapter: TestSqliteDatabase | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
if (adapter) {
|
||||
await adapter.close();
|
||||
adapter = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function createRepository(
|
||||
onWrite?: () => void | Promise<void>,
|
||||
): Promise<TmuxSessionTagRepository> {
|
||||
adapter = new TestSqliteDatabase();
|
||||
const context = await adapter.connect();
|
||||
await adapter.exec(`
|
||||
INSERT INTO users (id, username, password_hash)
|
||||
VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash');
|
||||
INSERT INTO ssh_data (id, user_id, name, ip, port, username, auth_type)
|
||||
VALUES (1, 'user-1', 'one', '10.0.0.1', 22, 'root', 'password'), (2, 'user-2', 'two', '10.0.0.1', 22, 'root', 'password');
|
||||
INSERT INTO tmux_session_tags (user_id, host_id, session_name, tag)
|
||||
VALUES
|
||||
('user-1', 1, 'api', 'prod'),
|
||||
('user-1', 1, 'api', 'critical'),
|
||||
('user-1', 1, 'worker', 'batch'),
|
||||
('user-2', 2, 'api', 'other');
|
||||
`);
|
||||
|
||||
return new TmuxSessionTagRepository(context, onWrite);
|
||||
}
|
||||
|
||||
it("groups tags by session for a user and host", async () => {
|
||||
const repo = await createRepository();
|
||||
|
||||
const tags = await repo.listByUserAndHost("user-1", 1);
|
||||
|
||||
expect(tags.get("api")).toEqual(["prod", "critical"]);
|
||||
expect(tags.get("worker")).toEqual(["batch"]);
|
||||
expect(tags.has("missing")).toBe(false);
|
||||
});
|
||||
|
||||
it("renames and deletes session tags by host/session", async () => {
|
||||
let writeCount = 0;
|
||||
const repo = await createRepository(() => {
|
||||
writeCount += 1;
|
||||
});
|
||||
|
||||
expect(await repo.renameSessionForHost(1, "api", "api-renamed")).toBe(2);
|
||||
expect(await repo.renameSessionForHost(1, "missing", "noop")).toBe(0);
|
||||
|
||||
const renamed = await repo.listByUserAndHost("user-1", 1);
|
||||
expect(renamed.get("api-renamed")).toEqual(["prod", "critical"]);
|
||||
expect(renamed.has("api")).toBe(false);
|
||||
|
||||
expect(await repo.deleteSessionForHost(1, "api-renamed")).toBe(2);
|
||||
expect(await repo.deleteSessionForHost(1, "api-renamed")).toBe(0);
|
||||
expect(writeCount).toBe(2);
|
||||
});
|
||||
|
||||
it("replaces tags for one user/host/session", async () => {
|
||||
let writeCount = 0;
|
||||
const repo = await createRepository(() => {
|
||||
writeCount += 1;
|
||||
});
|
||||
|
||||
expect(
|
||||
await repo.replaceForUserHostSession("user-1", 1, "api", [
|
||||
"blue",
|
||||
"green",
|
||||
]),
|
||||
).toBe(4);
|
||||
|
||||
const tags = await repo.listByUserAndHost("user-1", 1);
|
||||
expect(tags.get("api")).toEqual(["blue", "green"]);
|
||||
expect(tags.get("worker")).toEqual(["batch"]);
|
||||
|
||||
expect(
|
||||
await repo.replaceForUserHostSession("user-1", 1, "worker", []),
|
||||
).toBe(1);
|
||||
expect(
|
||||
await repo.replaceForUserHostSession("user-1", 1, "missing", []),
|
||||
).toBe(0);
|
||||
expect(writeCount).toBe(2);
|
||||
});
|
||||
|
||||
it("deletes all tags for a user", async () => {
|
||||
let writeCount = 0;
|
||||
const repo = await createRepository(() => {
|
||||
writeCount += 1;
|
||||
});
|
||||
|
||||
await expect(repo.deleteByUserId("user-1")).resolves.toBe(3);
|
||||
await expect(repo.deleteByUserId("missing")).resolves.toBe(0);
|
||||
|
||||
expect(await repo.listByUserAndHost("user-1", 1)).toEqual(new Map());
|
||||
expect(await repo.listByUserAndHost("user-2", 2)).toEqual(
|
||||
new Map([["api", ["other"]]]),
|
||||
);
|
||||
expect(writeCount).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import type { Client } from "ssh2";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
detectTmux,
|
||||
tmuxCommand,
|
||||
withTmuxPath,
|
||||
} from "../../../hosts/tmux/helper.js";
|
||||
|
||||
describe("tmux command path handling", () => {
|
||||
it("prepends all non-login tmux paths while preserving inherited PATH", () => {
|
||||
expect(withTmuxPath("command -v tmux")).toBe(
|
||||
`/bin/sh -c 'PATH=/opt/homebrew/bin:/usr/local/bin:/opt/bin:/usr/pkg/bin:"$PATH"; export PATH; command -v tmux'`,
|
||||
);
|
||||
});
|
||||
|
||||
it("shell-escapes embedded single quotes in wrapped commands", () => {
|
||||
// Asserted as a string so the escaping rule is covered everywhere. The
|
||||
// round-trip below proves it against a real parser, but only where one
|
||||
// exists -- see the note there.
|
||||
expect(withTmuxPath(`printf '%s' "can't"`)).toBe(
|
||||
"/bin/sh -c 'PATH=/opt/homebrew/bin:/usr/local/bin:/opt/bin:/usr/pkg/bin:\"$PATH\"; export PATH; printf '\\''%s'\\'' \"can'\\''t\"'",
|
||||
);
|
||||
});
|
||||
|
||||
// /bin/sh is not on Windows, and Windows is a supported platform for the
|
||||
// desktop app -- contributors run `npm test` there. CI is ubuntu-only, so it
|
||||
// would never notice this failing.
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"produces a command a real shell parses back to the original",
|
||||
() => {
|
||||
const command = withTmuxPath(`printf '%s' "can't"`);
|
||||
|
||||
expect(
|
||||
execFileSync("/bin/sh", ["-c", command], { encoding: "utf8" }),
|
||||
).toBe("can't");
|
||||
},
|
||||
);
|
||||
|
||||
it("runs every tmux invocation in UTF-8 mode through the path wrapper", () => {
|
||||
expect(tmuxCommand("list-sessions")).toBe(
|
||||
`/bin/sh -c 'PATH=/opt/homebrew/bin:/usr/local/bin:/opt/bin:/usr/pkg/bin:"$PATH"; export PATH; tmux -u list-sessions'`,
|
||||
);
|
||||
});
|
||||
|
||||
it("detects tmux with the UTF-8 wrapper", async () => {
|
||||
const commands: string[] = [];
|
||||
const conn = {
|
||||
exec(command: string, callback: (error: null, stream: never) => void) {
|
||||
commands.push(command);
|
||||
const stream = new EventEmitter() as EventEmitter & {
|
||||
stderr: EventEmitter;
|
||||
};
|
||||
stream.stderr = new EventEmitter();
|
||||
callback(null, stream as never);
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (commands.length === 1) {
|
||||
stream.emit("data", Buffer.from("tmux 3.7b\n"));
|
||||
stream.emit("close", 0);
|
||||
return;
|
||||
}
|
||||
stream.emit("close", 1);
|
||||
});
|
||||
},
|
||||
} as unknown as Client;
|
||||
|
||||
await expect(detectTmux(conn)).resolves.toEqual({
|
||||
available: true,
|
||||
sessions: [],
|
||||
});
|
||||
expect(commands).toEqual([
|
||||
`/bin/sh -c 'PATH=/opt/homebrew/bin:/usr/local/bin:/opt/bin:/usr/pkg/bin:"$PATH"; export PATH; tmux -u -V'`,
|
||||
`/bin/sh -c 'PATH=/opt/homebrew/bin:/usr/local/bin:/opt/bin:/usr/pkg/bin:"$PATH"; export PATH; tmux -u list-sessions -F "#{session_name}|#{session_created}|#{session_activity}|#{session_windows}|#{session_attached}" 2>/dev/null'`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,272 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
SEP,
|
||||
parseSessions,
|
||||
parseWindows,
|
||||
parsePanes,
|
||||
parsePsOutput,
|
||||
parseGpuOutput,
|
||||
buildPaneMetrics,
|
||||
attachPanesToWindows,
|
||||
shellEscape,
|
||||
type ProcessInfo,
|
||||
type TmuxWindow,
|
||||
} from "../../../hosts/tmux/monitor-helpers.js";
|
||||
|
||||
function join(...fields: (string | number)[]): string {
|
||||
return fields.join(SEP);
|
||||
}
|
||||
|
||||
describe("parseSessions", () => {
|
||||
it("parses tmux list-sessions output", () => {
|
||||
const output = [
|
||||
join("training", 1760000000, 1760001000, 1),
|
||||
join("lab|with|pipes", 1760000500, 1760002000, 0),
|
||||
].join("\n");
|
||||
|
||||
const sessions = parseSessions(output);
|
||||
expect(sessions).toHaveLength(2);
|
||||
expect(sessions[0]).toEqual({
|
||||
name: "training",
|
||||
created: 1760000000,
|
||||
lastActivity: 1760001000,
|
||||
attachedClients: 1,
|
||||
});
|
||||
// Session names containing "|" survive because SEP is a multi-char token
|
||||
expect(sessions[1].name).toBe("lab|with|pipes");
|
||||
expect(sessions[1].attachedClients).toBe(0);
|
||||
});
|
||||
|
||||
it("returns empty array for empty output", () => {
|
||||
expect(parseSessions("")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseWindows", () => {
|
||||
it("groups windows by session", () => {
|
||||
const output = [
|
||||
join("training", 0, 1, "vim"),
|
||||
join("training", 1, 0, "logs"),
|
||||
join("api", 0, 1, "server"),
|
||||
].join("\n");
|
||||
|
||||
const windows = parseWindows(output);
|
||||
expect(windows.get("training")).toHaveLength(2);
|
||||
expect(windows.get("training")![0]).toMatchObject({
|
||||
index: 0,
|
||||
name: "vim",
|
||||
active: true,
|
||||
});
|
||||
expect(windows.get("api")![0].name).toBe("server");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parsePanes", () => {
|
||||
it("parses full pane lines including free-text fields", () => {
|
||||
const output = join(
|
||||
"training",
|
||||
0,
|
||||
"%3",
|
||||
1,
|
||||
12345,
|
||||
1,
|
||||
120,
|
||||
40,
|
||||
"python",
|
||||
"/home/user/my|dir",
|
||||
"gpu01: train.py",
|
||||
);
|
||||
|
||||
const panes = parsePanes(output);
|
||||
expect(panes).toHaveLength(1);
|
||||
expect(panes[0]).toEqual({
|
||||
sessionName: "training",
|
||||
windowIndex: 0,
|
||||
id: "%3",
|
||||
index: 1,
|
||||
pid: 12345,
|
||||
active: true,
|
||||
width: 120,
|
||||
height: 40,
|
||||
command: "python",
|
||||
path: "/home/user/my|dir",
|
||||
title: "gpu01: train.py",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parsePsOutput", () => {
|
||||
it("parses ps -eo pid,ppid,pcpu,pmem,rss,comm output", () => {
|
||||
const output = [
|
||||
" 1 0 0.0 0.1 1234 systemd",
|
||||
"12345 1 2.5 1.0 50000 bash",
|
||||
"12400 12345 95.3 12.5 800000 python3",
|
||||
"garbage line",
|
||||
].join("\n");
|
||||
|
||||
const procs = parsePsOutput(output);
|
||||
expect(procs).toHaveLength(3);
|
||||
expect(procs[2]).toEqual({
|
||||
pid: 12400,
|
||||
ppid: 12345,
|
||||
cpu: 95.3,
|
||||
mem: 12.5,
|
||||
rss: 800000,
|
||||
comm: "python3",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseGpuOutput", () => {
|
||||
it("parses nvidia-smi csv output and sums per pid", () => {
|
||||
const output = ["12400, 8000", "12400, 2000", "99999, 512"].join("\n");
|
||||
const gpu = parseGpuOutput(output);
|
||||
expect(gpu.get(12400)).toBe(10000);
|
||||
expect(gpu.get(99999)).toBe(512);
|
||||
});
|
||||
|
||||
it("handles empty output (no GPU)", () => {
|
||||
expect(parseGpuOutput("").size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPaneMetrics", () => {
|
||||
const panes = parsePanes(
|
||||
[
|
||||
join("training", 0, "%1", 0, 100, 1, 80, 24, "bash", "/", "t"),
|
||||
join("idle", 0, "%2", 0, 200, 1, 80, 24, "bash", "/", "t"),
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const processes = parsePsOutput(
|
||||
[
|
||||
// pane %1: bash(100) -> python3(110) -> worker(111)
|
||||
" 100 1 0.1 0.1 4000 bash",
|
||||
" 110 100 90.0 10.0 700000 python3",
|
||||
" 111 110 9.5 2.0 100000 dataloader",
|
||||
// pane %2: bash(200) only
|
||||
" 200 1 0.0 0.1 4000 bash",
|
||||
// unrelated process
|
||||
" 300 1 50.0 5.0 200000 chrome",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
it("aggregates descendant trees per pane", () => {
|
||||
const metrics = buildPaneMetrics(panes, processes, new Map());
|
||||
const m1 = metrics.find((m) => m.paneId === "%1")!;
|
||||
expect(m1.processCount).toBe(3);
|
||||
expect(m1.cpuPercent).toBeCloseTo(99.6, 1);
|
||||
expect(m1.memRssKb).toBe(804000);
|
||||
expect(m1.topCommand).toBe("python3");
|
||||
|
||||
const m2 = metrics.find((m) => m.paneId === "%2")!;
|
||||
expect(m2.processCount).toBe(1);
|
||||
expect(m2.cpuPercent).toBe(0);
|
||||
// Unrelated process is never attributed
|
||||
expect(m2.memRssKb).toBe(4000);
|
||||
});
|
||||
|
||||
it("attributes GPU memory through the process tree", () => {
|
||||
const gpu = new Map([
|
||||
[110, 8000],
|
||||
[300, 4000],
|
||||
]);
|
||||
const metrics = buildPaneMetrics(panes, processes, gpu);
|
||||
expect(metrics.find((m) => m.paneId === "%1")!.gpuMemMb).toBe(8000);
|
||||
expect(metrics.find((m) => m.paneId === "%2")!.gpuMemMb).toBe(0);
|
||||
});
|
||||
|
||||
it("handles a pane whose pid is missing from ps output", () => {
|
||||
const orphan = parsePanes(
|
||||
join("gone", 0, "%9", 0, 99999, 0, 80, 24, "bash", "/", "t"),
|
||||
);
|
||||
const metrics = buildPaneMetrics(orphan, processes, new Map());
|
||||
expect(metrics[0].processCount).toBe(0);
|
||||
expect(metrics[0].cpuPercent).toBe(0);
|
||||
expect(metrics[0].topCommand).toBeNull();
|
||||
});
|
||||
|
||||
it("does not loop on cyclic ppid data", () => {
|
||||
const cyclic = parsePsOutput(
|
||||
[" 100 101 1.0 0.1 1000 a", " 101 100 1.0 0.1 1000 b"].join("\n"),
|
||||
);
|
||||
const pane = parsePanes(
|
||||
join("s", 0, "%1", 0, 100, 1, 80, 24, "a", "/", "t"),
|
||||
);
|
||||
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", () => {
|
||||
it("places panes into their windows", () => {
|
||||
const windows = parseWindows(
|
||||
[join("s1", 0, 1, "main"), join("s1", 1, 0, "logs")].join("\n"),
|
||||
);
|
||||
const panes = parsePanes(
|
||||
[
|
||||
join("s1", 0, "%1", 0, 100, 1, 80, 24, "bash", "/", "t"),
|
||||
join("s1", 1, "%2", 0, 200, 0, 80, 24, "tail", "/", "t"),
|
||||
join("unknown", 5, "%3", 0, 300, 0, 80, 24, "bash", "/", "t"),
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
attachPanesToWindows(windows, panes);
|
||||
expect(windows.get("s1")![0].panes).toHaveLength(1);
|
||||
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", () => {
|
||||
it("wraps in single quotes and escapes embedded quotes", () => {
|
||||
expect(shellEscape("simple")).toBe("'simple'");
|
||||
expect(shellEscape("it's")).toBe("'it'\\''s'");
|
||||
expect(shellEscape("$(rm -rf /)")).toBe("'$(rm -rf /)'");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* The tmux monitor host-column migration.
|
||||
*
|
||||
* An upgrade must be lossless: a host with the monitor on must keep it on
|
||||
* once the ssh_data column is dropped. Running it twice must not duplicate
|
||||
* or clobber what it moved.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
interface HostRow {
|
||||
id: number;
|
||||
enable_tmux_monitor: boolean;
|
||||
}
|
||||
|
||||
interface SettingsRow {
|
||||
pluginId: string;
|
||||
scope: string;
|
||||
scopeId: string | null;
|
||||
key: string;
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
const hostRows: HostRow[] = [];
|
||||
const settingsRows: SettingsRow[] = [];
|
||||
|
||||
const find = (
|
||||
pluginId: string,
|
||||
scope: string,
|
||||
scopeId: string | null,
|
||||
key: string,
|
||||
) =>
|
||||
settingsRows.find(
|
||||
(row) =>
|
||||
row.pluginId === pluginId &&
|
||||
row.scope === scope &&
|
||||
row.scopeId === scopeId &&
|
||||
row.key === key,
|
||||
);
|
||||
|
||||
const pluginSettingsRepository = {
|
||||
get: async (
|
||||
pluginId: string,
|
||||
scope: string,
|
||||
scopeId: string | null,
|
||||
key: string,
|
||||
) => find(pluginId, scope, scopeId, key) ?? null,
|
||||
set: async (
|
||||
pluginId: string,
|
||||
scope: string,
|
||||
scopeId: string | null,
|
||||
key: string,
|
||||
value: string | null,
|
||||
) => {
|
||||
const existing = find(pluginId, scope, scopeId, key);
|
||||
if (existing) {
|
||||
existing.value = value;
|
||||
return;
|
||||
}
|
||||
settingsRows.push({ pluginId, scope, scopeId, key, value });
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
createCurrentPluginSettingsRepository: () => pluginSettingsRepository,
|
||||
}));
|
||||
|
||||
vi.mock("../../database/db/index.js", () => ({
|
||||
getDb: () => ({
|
||||
all: async () => hostRows.filter((row) => row.enable_tmux_monitor),
|
||||
}),
|
||||
}));
|
||||
|
||||
const { runTmuxMonitorSettingsMigration } =
|
||||
await import("../../utils/crypto-migration/tmux-monitor-settings-migration.js");
|
||||
|
||||
function storedValue(hostId: number, key: string): unknown {
|
||||
const row = settingsRows.find(
|
||||
(entry) => entry.scopeId === String(hostId) && entry.key === key,
|
||||
);
|
||||
return row?.value === null || row?.value === undefined
|
||||
? undefined
|
||||
: JSON.parse(row.value);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
hostRows.length = 0;
|
||||
settingsRows.length = 0;
|
||||
});
|
||||
|
||||
describe("runTmuxMonitorSettingsMigration", () => {
|
||||
it("moves a host's enabled switch into plugin settings", async () => {
|
||||
hostRows.push({ id: 1, enable_tmux_monitor: true });
|
||||
|
||||
const result = await runTmuxMonitorSettingsMigration();
|
||||
|
||||
expect(result.moved).toBe(1);
|
||||
expect(storedValue(1, "enableTmuxMonitor")).toBe(true);
|
||||
expect(settingsRows.every((row) => row.pluginId === "tmux-monitor")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(settingsRows.every((row) => row.scope === "host")).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves a disabled host untouched", async () => {
|
||||
hostRows.push({ id: 3, enable_tmux_monitor: false });
|
||||
|
||||
const result = await runTmuxMonitorSettingsMigration();
|
||||
|
||||
expect(result.moved).toBe(0);
|
||||
expect(storedValue(3, "enableTmuxMonitor")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does nothing on a fresh install", async () => {
|
||||
const result = await runTmuxMonitorSettingsMigration();
|
||||
|
||||
expect(result.moved).toBe(0);
|
||||
expect(settingsRows).toEqual([]);
|
||||
});
|
||||
|
||||
it("is idempotent: a second run changes nothing", async () => {
|
||||
hostRows.push({ id: 2, enable_tmux_monitor: true });
|
||||
|
||||
await runTmuxMonitorSettingsMigration();
|
||||
const afterFirst = JSON.parse(JSON.stringify(settingsRows));
|
||||
|
||||
const second = await runTmuxMonitorSettingsMigration();
|
||||
|
||||
expect(second.moved).toBe(0);
|
||||
expect(second.skipped).toBe(1);
|
||||
expect(settingsRows).toEqual(afterFirst);
|
||||
});
|
||||
|
||||
it("does not overwrite a value the plugin already saved", async () => {
|
||||
hostRows.push({ id: 5, enable_tmux_monitor: true });
|
||||
await pluginSettingsRepository.set(
|
||||
"tmux-monitor",
|
||||
"host",
|
||||
"5",
|
||||
"enableTmuxMonitor",
|
||||
"false",
|
||||
);
|
||||
|
||||
const result = await runTmuxMonitorSettingsMigration();
|
||||
|
||||
expect(result.skipped).toBe(1);
|
||||
expect(storedValue(5, "enableTmuxMonitor")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Moves the tmux monitor host column into the tmux-monitor plugin's host-scope
|
||||
* settings, before the column is dropped from ssh_data.
|
||||
*
|
||||
* Idempotent: skips a host that already has a tmux-monitor settings row, so it
|
||||
* is safe to run on every boot. Lossless: reads straight off the still-present
|
||||
* ssh_data column, so it must run and finish before that column is ever
|
||||
* dropped from schema.ts.
|
||||
*/
|
||||
|
||||
import { sql } from "drizzle-orm";
|
||||
import { databaseLogger } from "../logger.js";
|
||||
import { getDb } from "../../database/db/index.js";
|
||||
import { createCurrentPluginSettingsRepository } from "../../database/repositories/factory.js";
|
||||
|
||||
export interface TmuxMonitorSettingsMigrationResult {
|
||||
moved: number;
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
interface LegacyTmuxMonitorRow {
|
||||
id: number;
|
||||
enable_tmux_monitor: number | boolean | null;
|
||||
}
|
||||
|
||||
export async function runTmuxMonitorSettingsMigration(): Promise<TmuxMonitorSettingsMigrationResult> {
|
||||
const result: TmuxMonitorSettingsMigrationResult = { moved: 0, skipped: 0 };
|
||||
|
||||
try {
|
||||
const drizzleDb = getDb();
|
||||
// Raw SQL, not the typed schema, so this keeps working once schema.ts
|
||||
// stops declaring this column. The plugin's enable switch defaults to
|
||||
// off, matching the old column's default, so only hosts that turned it
|
||||
// on need a row.
|
||||
const rows = await drizzleDb.all<LegacyTmuxMonitorRow>(sql`
|
||||
SELECT id, enable_tmux_monitor
|
||||
FROM ssh_data
|
||||
WHERE enable_tmux_monitor = true
|
||||
`);
|
||||
|
||||
if (rows.length === 0) return result;
|
||||
|
||||
const pluginSettingsRepository = createCurrentPluginSettingsRepository();
|
||||
|
||||
for (const row of rows) {
|
||||
const scopeId = String(row.id);
|
||||
const existing = await pluginSettingsRepository.get(
|
||||
"tmux-monitor",
|
||||
"host",
|
||||
scopeId,
|
||||
"enableTmuxMonitor",
|
||||
);
|
||||
if (existing && existing.value !== null) {
|
||||
result.skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
await pluginSettingsRepository.set(
|
||||
"tmux-monitor",
|
||||
"host",
|
||||
scopeId,
|
||||
"enableTmuxMonitor",
|
||||
JSON.stringify(!!row.enable_tmux_monitor),
|
||||
);
|
||||
result.moved++;
|
||||
}
|
||||
|
||||
if (result.moved > 0) {
|
||||
databaseLogger.info(
|
||||
`Moved tmux monitor settings for ${result.moved} host(s) into plugin settings`,
|
||||
{ operation: "tmux_monitor_settings_migration", moved: result.moved },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// A failed migration must not stop the backend. Hosts simply keep the
|
||||
// monitor off, the plugin's default.
|
||||
databaseLogger.warn("Tmux monitor settings migration failed", {
|
||||
operation: "tmux_monitor_settings_migration",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -34,13 +34,6 @@ const AppShell = lazy(() =>
|
||||
|
||||
// Full-screen apps opened via query params (e.g. from external links or
|
||||
// Electron). Plugin views register a standalone component with their tab.
|
||||
// --- tmux-monitor ---
|
||||
const TmuxMonitorApp = lazy(() =>
|
||||
import("@/features/tmux-monitor/TmuxMonitorApp").then((m) => ({
|
||||
default: m.default,
|
||||
})),
|
||||
);
|
||||
|
||||
const HomepageApp = lazy(() =>
|
||||
import("@/features/homepage/HomepageApp").then((m) => ({
|
||||
default: m.default,
|
||||
@@ -74,9 +67,6 @@ function FullscreenApp() {
|
||||
const hostId = searchParams.get("hostId");
|
||||
|
||||
switch (view) {
|
||||
case "tmux-monitor": // --- tmux-monitor ---
|
||||
case "tmux_monitor": // tab type spelling, so copied links also resolve
|
||||
return <TmuxMonitorApp hostId={hostId || undefined} />;
|
||||
case "homepage":
|
||||
return <HomepageApp />;
|
||||
default: {
|
||||
|
||||
+3
-10
@@ -1699,11 +1699,6 @@ export function AppShell({
|
||||
) {
|
||||
const type = resolveHostTabType(host, preferredType);
|
||||
if (!type) return;
|
||||
// --- tmux-monitor --- singleton tab, not a per-host tab
|
||||
if (type === "tmux_monitor") {
|
||||
openSingletonTab(type, undefined, host);
|
||||
return;
|
||||
}
|
||||
openTab(host, type, undefined, options);
|
||||
}
|
||||
|
||||
@@ -1842,8 +1837,7 @@ export function AppShell({
|
||||
}, []);
|
||||
|
||||
const openSingletonTab = useCallback(
|
||||
// --- tmux-monitor --- (added optional `host` so tmux_monitor can open
|
||||
// with a preselected host; existing callers are unaffected)
|
||||
// `host` optionally preselects a host for a singleton plugin tab.
|
||||
function openSingletonTab(
|
||||
type: TabType,
|
||||
pendingEvent?: string,
|
||||
@@ -1890,7 +1884,6 @@ export function AppShell({
|
||||
const singletonLabels: Partial<Record<TabType, string>> = {
|
||||
"host-manager": t("nav.hostManager"),
|
||||
sftp: t("nav.sftp"),
|
||||
tmux_monitor: t("nav.tmuxMonitor"), // --- tmux-monitor ---
|
||||
homepage: t("nav.homepage"),
|
||||
};
|
||||
// A plugin tab names itself; promoted rail panels reuse the rail's own
|
||||
@@ -3157,8 +3150,8 @@ export function AppShell({
|
||||
openSingletonTab(type, pendingEvent);
|
||||
} else if (getTabType(type)?.multiInstance) {
|
||||
openMultiInstanceTab(type);
|
||||
} else if (type === "tmux_monitor") {
|
||||
// --- tmux-monitor --- singleton tab, optionally preselecting a host
|
||||
} else if (getTabType(type)?.singleton) {
|
||||
// A singleton plugin tab, optionally preselecting a host.
|
||||
openSingletonTab(
|
||||
type,
|
||||
undefined,
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import { tmuxMonitorApi } from "@/main-axios";
|
||||
|
||||
export interface TmuxPane {
|
||||
id: string;
|
||||
index: number;
|
||||
pid: number;
|
||||
active: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
command: string;
|
||||
path: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface TmuxWindow {
|
||||
index: number;
|
||||
name: string;
|
||||
active: boolean;
|
||||
panes: TmuxPane[];
|
||||
}
|
||||
|
||||
export interface TmuxSessionOverview {
|
||||
name: string;
|
||||
created: number;
|
||||
lastActivity: number;
|
||||
attachedClients: number;
|
||||
windows: TmuxWindow[];
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface TmuxOverview {
|
||||
available: boolean;
|
||||
sessions: TmuxSessionOverview[];
|
||||
}
|
||||
|
||||
export interface TmuxSearchMatch {
|
||||
paneId: string;
|
||||
sessionName: string;
|
||||
windowIndex: number;
|
||||
line: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface TmuxPaneMetrics {
|
||||
paneId: string;
|
||||
sessionName: string;
|
||||
pid: number;
|
||||
processCount: number;
|
||||
cpuPercent: number;
|
||||
memRssKb: number;
|
||||
gpuMemMb: number;
|
||||
topCommand: string | null;
|
||||
}
|
||||
|
||||
export async function getTmuxOverview(hostId: number): Promise<TmuxOverview> {
|
||||
const response = await tmuxMonitorApi.get(`/${hostId}/overview`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** Select a pane's window+pane on the server so the attached terminal
|
||||
* (and any other attached client) switches to it. */
|
||||
export async function focusTmuxPane(
|
||||
hostId: number,
|
||||
paneId: string,
|
||||
): Promise<void> {
|
||||
await tmuxMonitorApi.post(`/${hostId}/focus`, { paneId });
|
||||
}
|
||||
|
||||
/** Create a new detached session on the host. Starts the tmux server if
|
||||
* none is running yet. */
|
||||
export async function createTmuxSession(
|
||||
hostId: number,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
await tmuxMonitorApi.post(`/${hostId}/sessions`, { name });
|
||||
}
|
||||
|
||||
/** Create a new window in an existing session. */
|
||||
export async function createTmuxWindow(
|
||||
hostId: number,
|
||||
sessionName: string,
|
||||
): Promise<void> {
|
||||
await tmuxMonitorApi.post(`/${hostId}/windows`, { sessionName });
|
||||
}
|
||||
|
||||
/** Rename a session. Saved tags follow the session to its new name. */
|
||||
export async function renameTmuxSession(
|
||||
hostId: number,
|
||||
sessionName: string,
|
||||
newName: string,
|
||||
): Promise<void> {
|
||||
await tmuxMonitorApi.post(`/${hostId}/rename`, { sessionName, newName });
|
||||
}
|
||||
|
||||
/** Kill a session (terminates all of its windows and processes). */
|
||||
export async function killTmuxSession(
|
||||
hostId: number,
|
||||
sessionName: string,
|
||||
): Promise<void> {
|
||||
await tmuxMonitorApi.post(`/${hostId}/kill`, { sessionName });
|
||||
}
|
||||
|
||||
/** Kill a window and every pane in it. */
|
||||
export async function killTmuxWindow(
|
||||
hostId: number,
|
||||
sessionName: string,
|
||||
windowIndex: number,
|
||||
): Promise<void> {
|
||||
await tmuxMonitorApi.post(`/${hostId}/kill-window`, {
|
||||
sessionName,
|
||||
windowIndex,
|
||||
});
|
||||
}
|
||||
|
||||
/** Kill a single pane (last pane of a window closes the window). */
|
||||
export async function killTmuxPane(
|
||||
hostId: number,
|
||||
paneId: string,
|
||||
): Promise<void> {
|
||||
await tmuxMonitorApi.post(`/${hostId}/kill-pane`, { paneId });
|
||||
}
|
||||
|
||||
/** Split the window containing a pane: "h" adds a pane to the right,
|
||||
* "v" below (tmux -h/-v semantics). */
|
||||
export async function splitTmuxPane(
|
||||
hostId: number,
|
||||
paneId: string,
|
||||
direction: "h" | "v",
|
||||
): Promise<void> {
|
||||
await tmuxMonitorApi.post(`/${hostId}/split`, { paneId, direction });
|
||||
}
|
||||
|
||||
export interface TmuxSearchResult {
|
||||
matches: TmuxSearchMatch[];
|
||||
/** True when a search limit was hit and the results are partial. */
|
||||
truncated: boolean;
|
||||
searchedLines: number;
|
||||
maxPanes: number;
|
||||
}
|
||||
|
||||
export async function searchTmux(
|
||||
hostId: number,
|
||||
query: string,
|
||||
): Promise<TmuxSearchResult> {
|
||||
const response = await tmuxMonitorApi.get(`/${hostId}/search`, {
|
||||
params: { q: query },
|
||||
});
|
||||
return {
|
||||
matches: response.data.matches ?? [],
|
||||
truncated: response.data.truncated ?? false,
|
||||
searchedLines: response.data.searchedLines ?? 0,
|
||||
maxPanes: response.data.maxPanes ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getTmuxMetrics(
|
||||
hostId: number,
|
||||
): Promise<TmuxPaneMetrics[]> {
|
||||
const response = await tmuxMonitorApi.get(`/${hostId}/metrics`);
|
||||
return response.data.panes ?? [];
|
||||
}
|
||||
|
||||
export async function setTmuxSessionTags(
|
||||
hostId: number,
|
||||
sessionName: string,
|
||||
tags: string[],
|
||||
): Promise<string[]> {
|
||||
const response = await tmuxMonitorApi.put(`/${hostId}/tags`, {
|
||||
sessionName,
|
||||
tags,
|
||||
});
|
||||
return response.data.tags ?? [];
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
// Interactive view of a tmux pane. Embeds the real SSH terminal (the same
|
||||
// component as the Terminal feature) and attaches to the selected session via
|
||||
// the native tmux_attach flow, so the pane is fully usable — typing, mouse
|
||||
// scrolling and tmux copy-mode all behave exactly like a normal terminal tab.
|
||||
|
||||
import { PluginComponent } from "@/plugin-host/component-registry";
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Activity,
|
||||
Cpu,
|
||||
MemoryStick,
|
||||
RotateCw,
|
||||
SquareSplitHorizontal,
|
||||
SquareSplitVertical,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type { TabHandle as TerminalHandle } from "@termix/plugin-sdk/frontend";
|
||||
|
||||
type TerminalHostConfig = Record<string, unknown>;
|
||||
import type { SSHHost } from "@/types/index";
|
||||
import type { TmuxPaneMetrics } from "@/api/tmux-monitor-api";
|
||||
import { formatMem } from "./format";
|
||||
import type { SelectedPane } from "./types";
|
||||
|
||||
interface PanePreviewProps {
|
||||
host: SSHHost;
|
||||
pane: SelectedPane;
|
||||
metrics?: TmuxPaneMetrics;
|
||||
/** Imperative handle of the embedded terminal, used by the parent to nudge
|
||||
* a refit/redraw after layout-changing tmux actions. */
|
||||
terminalRef?: React.RefObject<TerminalHandle | null>;
|
||||
/** Split the window containing this pane ("h" = new pane to the right,
|
||||
* "v" = below — tmux -h/-v semantics). */
|
||||
onSplit: (direction: "h" | "v") => void;
|
||||
/** Ask for confirmation and kill this pane. */
|
||||
onKillPane: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function newInstanceId(): string {
|
||||
return typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
export function PanePreview({
|
||||
host,
|
||||
pane,
|
||||
metrics,
|
||||
terminalRef,
|
||||
onSplit,
|
||||
onKillPane,
|
||||
onClose,
|
||||
}: PanePreviewProps) {
|
||||
const { t } = useTranslation();
|
||||
// One PTY per mount; the parent remounts this component (via key) when the
|
||||
// host or session changes. Pane switches within a session go through the
|
||||
// focus endpoint instead, so the connection is reused.
|
||||
const instanceIdRef = useRef<string>(newInstanceId());
|
||||
// Bumping this remounts the embedded terminal with a fresh PTY — the rescue
|
||||
// hatch when the attached client's rendering gets out of sync.
|
||||
const [attachNonce, setAttachNonce] = useState(0);
|
||||
|
||||
function reattach() {
|
||||
instanceIdRef.current = newInstanceId();
|
||||
setAttachNonce((n) => n + 1);
|
||||
}
|
||||
|
||||
// The host's own authType travels in the spread, so a Tailscale host still
|
||||
// gets the Tailscale-aware PTY path. The SSH port may be non-standard.
|
||||
const resolvedPort = host.sshPort ?? host.port;
|
||||
const terminalHostConfig = {
|
||||
...host,
|
||||
port: resolvedPort,
|
||||
sshPort: resolvedPort,
|
||||
instanceId: instanceIdRef.current,
|
||||
} as TerminalHostConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-3 border-b border-border px-3 py-1.5 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">
|
||||
{pane.sessionName} · {pane.paneId}
|
||||
</span>
|
||||
{metrics && (
|
||||
<>
|
||||
<span className="flex items-center gap-1">
|
||||
<Cpu className="size-3" />
|
||||
{metrics.cpuPercent.toFixed(1)}%
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<MemoryStick className="size-3" />
|
||||
{formatMem(metrics.memRssKb)}
|
||||
</span>
|
||||
{metrics.gpuMemMb > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Activity className="size-3" />
|
||||
{metrics.gpuMemMb} MB GPU
|
||||
</span>
|
||||
)}
|
||||
{metrics.topCommand && (
|
||||
<span className="truncate">
|
||||
{metrics.topCommand} ({metrics.processCount})
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<button
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={t("tmuxMonitor.splitRight")}
|
||||
aria-label={t("tmuxMonitor.splitRight")}
|
||||
onClick={() => onSplit("h")}
|
||||
>
|
||||
<SquareSplitHorizontal className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={t("tmuxMonitor.splitDown")}
|
||||
aria-label={t("tmuxMonitor.splitDown")}
|
||||
onClick={() => onSplit("v")}
|
||||
>
|
||||
<SquareSplitVertical className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
title={t("tmuxMonitor.killPane")}
|
||||
aria-label={t("tmuxMonitor.killPane")}
|
||||
onClick={onKillPane}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</button>
|
||||
<div className="h-3.5 border-l border-border" />
|
||||
<button
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={t("tmuxMonitor.reattach")}
|
||||
aria-label={t("tmuxMonitor.reattach")}
|
||||
onClick={reattach}
|
||||
>
|
||||
<RotateCw className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={t("tmuxMonitor.closePreview")}
|
||||
aria-label={t("tmuxMonitor.closePreview")}
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<PluginComponent
|
||||
id="terminal.view"
|
||||
key={attachNonce}
|
||||
handleRef={terminalRef}
|
||||
hostConfig={terminalHostConfig}
|
||||
isVisible={true}
|
||||
title={pane.sessionName}
|
||||
showTitle={false}
|
||||
splitScreen={false}
|
||||
tmuxAttachSession={pane.sessionName}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { X } from "lucide-react";
|
||||
import type { TmuxSearchMatch, TmuxSearchResult } from "@/api/tmux-monitor-api";
|
||||
|
||||
interface SearchResultsProps {
|
||||
results: TmuxSearchMatch[];
|
||||
searching: boolean;
|
||||
/** The query that produced these results, used to highlight matches. */
|
||||
query: string;
|
||||
/** Search-limit info; renders a "partial results" note when truncated. */
|
||||
limits: Pick<
|
||||
TmuxSearchResult,
|
||||
"truncated" | "searchedLines" | "maxPanes"
|
||||
> | null;
|
||||
onSelect: (match: TmuxSearchMatch) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function highlightMatch(text: string, query: string): ReactNode {
|
||||
if (!query) return text;
|
||||
const index = text.toLowerCase().indexOf(query.toLowerCase());
|
||||
if (index === -1) return text;
|
||||
return (
|
||||
<>
|
||||
{text.slice(0, index)}
|
||||
<mark className="rounded-sm bg-accent-brand/30 px-0 text-foreground">
|
||||
{text.slice(index, index + query.length)}
|
||||
</mark>
|
||||
{text.slice(index + query.length)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchResults({
|
||||
results,
|
||||
searching,
|
||||
query,
|
||||
limits,
|
||||
onSelect,
|
||||
onClose,
|
||||
}: SearchResultsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="max-h-56 overflow-y-auto border-b border-border bg-card">
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-1.5">
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{searching
|
||||
? t("common.loading")
|
||||
: t("tmuxMonitor.searchResults", { count: results.length })}
|
||||
{!searching && limits?.truncated && (
|
||||
<span className="ml-2 text-muted-foreground/70">
|
||||
{t("tmuxMonitor.searchTruncated", {
|
||||
lines: limits.searchedLines,
|
||||
panes: limits.maxPanes,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={t("tmuxMonitor.closeSearchResults")}
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{results.map((match, i) => (
|
||||
<div
|
||||
key={`${match.paneId}-${match.line}-${i}`}
|
||||
className="flex cursor-pointer items-baseline gap-2 px-3 py-1 text-xs hover:bg-muted/40"
|
||||
onClick={() => onSelect(match)}
|
||||
>
|
||||
<span className="shrink-0 font-medium text-primary">
|
||||
{match.sessionName} · {match.paneId}
|
||||
</span>
|
||||
<span className="truncate font-mono text-muted-foreground">
|
||||
{highlightMatch(match.text, query)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
// Session/window/pane tree styled after the VSCode "tmux manager" view.
|
||||
// Each session is a single row: status dot, name, inline tag badges, and a
|
||||
// right-aligned meta label (relative time · cpu · mem) that is overlaid by
|
||||
// the hover actions (attach, new window, …-menu). The overlay uses a grid
|
||||
// stack with opacity so rows never shift and the actions stay reachable via
|
||||
// keyboard focus.
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AppWindow,
|
||||
BarChart2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Play,
|
||||
Plus,
|
||||
SquareSplitHorizontal,
|
||||
SquareSplitVertical,
|
||||
SquareTerminal,
|
||||
Tag,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/dropdown-menu";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/popover";
|
||||
import type {
|
||||
TmuxPaneMetrics,
|
||||
TmuxSessionOverview,
|
||||
} from "@/api/tmux-monitor-api";
|
||||
import { formatMem, formatRelativeTime } from "./format";
|
||||
import type { SelectedPane } from "./types";
|
||||
|
||||
export interface SessionMetricsAgg {
|
||||
cpu: number;
|
||||
memKb: number;
|
||||
gpuMb: number;
|
||||
}
|
||||
|
||||
const MAX_INLINE_TAGS = 2;
|
||||
|
||||
interface SessionTreeProps {
|
||||
sessions: TmuxSessionOverview[];
|
||||
expandedSessions: Set<string>;
|
||||
onToggleSession: (name: string) => void;
|
||||
selectedPaneId: string | null;
|
||||
onSelectPane: (pane: SelectedPane) => void;
|
||||
metricsByPane: Map<string, TmuxPaneMetrics>;
|
||||
metricsBySession: Map<string, SessionMetricsAgg>;
|
||||
/** Open the tag editor for a session (… menu). */
|
||||
onEditTags: (sessionName: string) => void;
|
||||
/** Open a terminal attached to the session (the ▷ hover action). */
|
||||
onAttachSession: (sessionName: string) => void;
|
||||
/** Create a new window in the session (the + hover action). */
|
||||
onNewWindow: (sessionName: string) => void;
|
||||
/** Open the rename dialog for a session (… menu). */
|
||||
onRenameSession: (sessionName: string) => void;
|
||||
/** Open the kill confirmation for a session (… menu). */
|
||||
onKillSession: (sessionName: string) => void;
|
||||
/** Open the kill confirmation for a single pane (hover ✕ on pane rows). */
|
||||
onKillPane: (paneId: string) => void;
|
||||
/** Split the window containing a pane (hover actions on pane rows). */
|
||||
onSplitPane: (paneId: string, direction: "h" | "v") => void;
|
||||
/** Open the kill confirmation for a window (hover ✕ on window rows). */
|
||||
onKillWindow: (sessionName: string, windowIndex: number) => void;
|
||||
/** Narrow tree panel: the inline time/cpu/mem label is hidden (the hover
|
||||
* tooltip still carries the full status). */
|
||||
compact: boolean;
|
||||
/** Timestamp used to render relative times; bumped periodically by the
|
||||
* parent so "Xm ago" labels do not go stale. */
|
||||
now: number;
|
||||
}
|
||||
|
||||
export function SessionTree({
|
||||
sessions,
|
||||
expandedSessions,
|
||||
onToggleSession,
|
||||
selectedPaneId,
|
||||
onSelectPane,
|
||||
metricsByPane,
|
||||
metricsBySession,
|
||||
onEditTags,
|
||||
onAttachSession,
|
||||
onNewWindow,
|
||||
onRenameSession,
|
||||
onKillSession,
|
||||
onKillPane,
|
||||
onSplitPane,
|
||||
onKillWindow,
|
||||
compact,
|
||||
now,
|
||||
}: SessionTreeProps) {
|
||||
const { t } = useTranslation();
|
||||
// Collapsed windows, keyed "session windowIndex". Windows default to
|
||||
// expanded (panes visible) like the VSCode tmux manager.
|
||||
const [collapsedWindows, setCollapsedWindows] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
function windowKey(sessionName: string, windowIndex: number): string {
|
||||
return `${sessionName} ${windowIndex}`;
|
||||
}
|
||||
|
||||
function toggleWindow(sessionName: string, windowIndex: number) {
|
||||
setCollapsedWindows((prev) => {
|
||||
const next = new Set(prev);
|
||||
const key = windowKey(sessionName, windowIndex);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function metaLabel(session: TmuxSessionOverview): string {
|
||||
const time = formatRelativeTime(session.lastActivity, now, t);
|
||||
const agg = metricsBySession.get(session.name);
|
||||
if (!agg) return time;
|
||||
return `${time} · ${agg.cpu.toFixed(0)}% · ${formatMem(agg.memKb)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{sessions.map((session) => {
|
||||
const expanded = expandedSessions.has(session.name);
|
||||
const agg = metricsBySession.get(session.name);
|
||||
const paneCount = session.windows.reduce(
|
||||
(sum, w) => sum + w.panes.length,
|
||||
0,
|
||||
);
|
||||
return (
|
||||
<div key={session.name} className="mb-0.5">
|
||||
<div
|
||||
className="group flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 hover:bg-muted/40"
|
||||
onClick={() => onToggleSession(session.name)}
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronDown className="size-3.5 shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="size-3.5 shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className={`size-2 shrink-0 rounded-full ${session.attachedClients > 0 ? "bg-accent-brand" : "bg-muted-foreground/40"}`}
|
||||
title={
|
||||
session.attachedClients > 0
|
||||
? t("tmuxMonitor.attached")
|
||||
: t("tmuxMonitor.detached")
|
||||
}
|
||||
/>
|
||||
<span className="truncate text-sm font-medium">
|
||||
{session.name}
|
||||
</span>
|
||||
{session.tags.slice(0, MAX_INLINE_TAGS).map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
className="h-4 shrink-0 px-1.5 text-[10px]"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{session.tags.length > MAX_INLINE_TAGS && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-4 shrink-0 px-1.5 text-[10px]"
|
||||
title={session.tags.join(", ")}
|
||||
>
|
||||
+{session.tags.length - MAX_INLINE_TAGS}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* Meta label and hover actions stacked in one grid cell */}
|
||||
<span className="ml-auto grid shrink-0">
|
||||
{!compact && (
|
||||
<span className="pointer-events-none col-start-1 row-start-1 justify-self-end self-center text-xs text-muted-foreground transition-opacity group-hover:opacity-0 group-focus-within:opacity-0">
|
||||
{metaLabel(session)}
|
||||
</span>
|
||||
)}
|
||||
<span className="pointer-events-none z-10 col-start-1 row-start-1 flex items-center gap-1.5 justify-self-end opacity-0 transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<button
|
||||
className="-m-1 rounded p-1 text-muted-foreground hover:text-foreground"
|
||||
title={t("tmuxMonitor.attachSessionTooltip", {
|
||||
session: session.name,
|
||||
})}
|
||||
aria-label={t("tmuxMonitor.attachSessionTooltip", {
|
||||
session: session.name,
|
||||
})}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAttachSession(session.name);
|
||||
}}
|
||||
>
|
||||
<Play className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="-m-1 rounded p-1 text-muted-foreground hover:text-foreground"
|
||||
title={t("tmuxMonitor.newWindow")}
|
||||
aria-label={t("tmuxMonitor.newWindow")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onNewWindow(session.name);
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="-m-1 rounded p-1 text-muted-foreground hover:text-foreground data-[state=open]:text-foreground"
|
||||
title={t("tmuxMonitor.sessionStats")}
|
||||
aria-label={t("tmuxMonitor.sessionStats")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<BarChart2 className="size-3.5" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className="w-56 rounded-none border border-border p-0 shadow-md"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="w-full px-3 py-2">
|
||||
<div className="mb-1 flex items-center gap-1.5">
|
||||
<span
|
||||
className={`size-2 rounded-full ${session.attachedClients > 0 ? "bg-accent-brand" : "bg-muted-foreground/40"}`}
|
||||
/>
|
||||
<span className="text-xs font-semibold">
|
||||
{session.name}
|
||||
</span>
|
||||
<span className="ml-auto text-[10px] text-muted-foreground">
|
||||
{session.attachedClients > 0
|
||||
? t("tmuxMonitor.attached")
|
||||
: t("tmuxMonitor.detached")}
|
||||
</span>
|
||||
</div>
|
||||
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-[11px]">
|
||||
<dt className="text-muted-foreground">
|
||||
{t("tmuxMonitor.statusActivity")}
|
||||
</dt>
|
||||
<dd className="text-right">
|
||||
{formatRelativeTime(session.lastActivity, now, t)}
|
||||
</dd>
|
||||
<dt className="text-muted-foreground">
|
||||
{t("tmuxMonitor.statusWindows")}
|
||||
</dt>
|
||||
<dd className="text-right">
|
||||
{session.windows.length} / {paneCount}{" "}
|
||||
{t("tmuxMonitor.statusPanes")}
|
||||
</dd>
|
||||
<dt className="text-muted-foreground">CPU</dt>
|
||||
<dd className="text-right">
|
||||
{agg ? `${agg.cpu.toFixed(1)}%` : "—"}
|
||||
</dd>
|
||||
<dt className="text-muted-foreground">RAM</dt>
|
||||
<dd className="text-right">
|
||||
{agg ? formatMem(agg.memKb) : "—"}
|
||||
</dd>
|
||||
{agg && agg.gpuMb > 0 && (
|
||||
<>
|
||||
<dt className="text-muted-foreground">GPU</dt>
|
||||
<dd className="text-right">{agg.gpuMb} MB</dd>
|
||||
</>
|
||||
)}
|
||||
{session.tags.length > 0 && (
|
||||
<>
|
||||
<dt className="text-muted-foreground">
|
||||
{t("tmuxMonitor.statusTags")}
|
||||
</dt>
|
||||
<dd className="truncate text-right">
|
||||
{session.tags.join(", ")}
|
||||
</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="-m-1 rounded p-1 text-muted-foreground hover:text-foreground data-[state=open]:text-foreground"
|
||||
title={t("tmuxMonitor.moreActions")}
|
||||
aria-label={t("tmuxMonitor.moreActions")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MoreHorizontal className="size-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="min-w-36"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="whitespace-nowrap"
|
||||
onSelect={() =>
|
||||
setTimeout(() => onRenameSession(session.name), 0)
|
||||
}
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
{t("tmuxMonitor.rename")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="whitespace-nowrap"
|
||||
onSelect={() =>
|
||||
setTimeout(() => onEditTags(session.name), 0)
|
||||
}
|
||||
>
|
||||
<Tag className="size-3.5" />
|
||||
{t("tmuxMonitor.editTags")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
className="whitespace-nowrap"
|
||||
onSelect={() =>
|
||||
setTimeout(() => onKillSession(session.name), 0)
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
{t("tmuxMonitor.kill")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{expanded &&
|
||||
session.windows.map((win) => {
|
||||
const winCollapsed = collapsedWindows.has(
|
||||
windowKey(session.name, win.index),
|
||||
);
|
||||
return (
|
||||
<div key={win.index} className="ml-4">
|
||||
<div
|
||||
className="group flex cursor-pointer items-center gap-1 rounded-md px-2 py-0.5 text-xs text-muted-foreground hover:bg-muted/40"
|
||||
onClick={() => toggleWindow(session.name, win.index)}
|
||||
>
|
||||
{winCollapsed ? (
|
||||
<ChevronRight className="size-3 shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="size-3 shrink-0" />
|
||||
)}
|
||||
<AppWindow className="size-3 shrink-0" />
|
||||
<span className="truncate">
|
||||
{win.index}: {win.name}
|
||||
</span>
|
||||
<button
|
||||
className="-m-1 ml-auto shrink-0 rounded p-1 text-muted-foreground opacity-0 transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100"
|
||||
title={t("tmuxMonitor.killWindow")}
|
||||
aria-label={t("tmuxMonitor.killWindow")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onKillWindow(session.name, win.index);
|
||||
}}
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
{!winCollapsed &&
|
||||
win.panes.map((pane) => {
|
||||
const m = metricsByPane.get(pane.id);
|
||||
const isSelected = selectedPaneId === pane.id;
|
||||
return (
|
||||
<div
|
||||
key={pane.id}
|
||||
className={`group ml-5 flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-xs hover:bg-muted/40 ${isSelected ? "bg-accent-brand/10" : ""}`}
|
||||
onClick={() =>
|
||||
onSelectPane({
|
||||
paneId: pane.id,
|
||||
sessionName: session.name,
|
||||
windowIndex: win.index,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SquareTerminal className="size-3 shrink-0" />
|
||||
<span className="truncate">
|
||||
{pane.index}: {pane.command}
|
||||
</span>
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate text-muted-foreground/70"
|
||||
title={pane.path}
|
||||
>
|
||||
{pane.path}
|
||||
</span>
|
||||
{/* Same overlay stack as the session rows: cpu
|
||||
label and hover actions share one grid cell so the
|
||||
buttons never widen the row (they would get
|
||||
clipped on a narrow panel). */}
|
||||
<span className="ml-auto grid shrink-0">
|
||||
{m && m.cpuPercent > 0 && (
|
||||
<span className="pointer-events-none col-start-1 row-start-1 justify-self-end self-center text-[10px] text-muted-foreground transition-opacity group-hover:opacity-0 group-focus-within:opacity-0">
|
||||
{m.cpuPercent.toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
<span className="pointer-events-none z-10 col-start-1 row-start-1 flex items-center gap-1 justify-self-end opacity-0 transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<button
|
||||
className="-m-1 rounded p-1 text-muted-foreground hover:text-foreground"
|
||||
title={t("tmuxMonitor.splitRight")}
|
||||
aria-label={t("tmuxMonitor.splitRight")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSplitPane(pane.id, "h");
|
||||
}}
|
||||
>
|
||||
<SquareSplitHorizontal className="size-3" />
|
||||
</button>
|
||||
<button
|
||||
className="-m-1 rounded p-1 text-muted-foreground hover:text-foreground"
|
||||
title={t("tmuxMonitor.splitDown")}
|
||||
aria-label={t("tmuxMonitor.splitDown")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSplitPane(pane.id, "v");
|
||||
}}
|
||||
>
|
||||
<SquareSplitVertical className="size-3" />
|
||||
</button>
|
||||
<button
|
||||
className="-m-1 rounded p-1 text-muted-foreground hover:text-destructive"
|
||||
title={t("tmuxMonitor.killPane")}
|
||||
aria-label={t("tmuxMonitor.killPane")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onKillPane(pane.id);
|
||||
}}
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,19 +0,0 @@
|
||||
import React from "react";
|
||||
import { TmuxMonitor } from "@/features/tmux-monitor/TmuxMonitor.tsx";
|
||||
|
||||
interface TmuxMonitorAppProps {
|
||||
hostId?: string;
|
||||
}
|
||||
|
||||
const TmuxMonitorApp: React.FC<TmuxMonitorAppProps> = ({ hostId }) => {
|
||||
const parsed = hostId ? parseInt(hostId, 10) : NaN;
|
||||
return (
|
||||
<div className="h-screen w-screen overflow-hidden">
|
||||
<TmuxMonitor
|
||||
initialHostId={Number.isFinite(parsed) ? parsed : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TmuxMonitorApp;
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
export function formatRelativeTime(
|
||||
unixSeconds: number,
|
||||
nowMs: number,
|
||||
t: TFunction,
|
||||
): string {
|
||||
if (!unixSeconds) return "";
|
||||
const diff = Math.max(0, nowMs / 1000 - unixSeconds);
|
||||
if (diff < 60) return String(t("tmuxMonitor.timeJustNow"));
|
||||
if (diff < 3600)
|
||||
return String(
|
||||
t("tmuxMonitor.timeMinutes", { count: Math.floor(diff / 60) }),
|
||||
);
|
||||
if (diff < 86400)
|
||||
return String(
|
||||
t("tmuxMonitor.timeHours", { count: Math.floor(diff / 3600) }),
|
||||
);
|
||||
return String(t("tmuxMonitor.timeDays", { count: Math.floor(diff / 86400) }));
|
||||
}
|
||||
|
||||
export function formatMem(kb: number): string {
|
||||
if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(1)} GB`;
|
||||
if (kb >= 1024) return `${(kb / 1024).toFixed(0)} MB`;
|
||||
return `${kb} KB`;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface SelectedPane {
|
||||
paneId: string;
|
||||
sessionName: string;
|
||||
windowIndex: number;
|
||||
}
|
||||
@@ -689,7 +689,6 @@
|
||||
"renameTab": "Rename tab",
|
||||
"openFileManager": "Open File Manager",
|
||||
"dashboard": "Dashboard",
|
||||
"tmuxMonitor": "Tmux Monitor",
|
||||
"homepage": "Homepage",
|
||||
"quickConnect": "Quick Connect",
|
||||
"sshTools": "SSH Tools",
|
||||
@@ -1063,8 +1062,6 @@
|
||||
"maxKeepaliveMisses": "Max Keepalive Misses",
|
||||
"sameHost": "This host (direct tunnel)",
|
||||
"bindHost": "Bind Host",
|
||||
"enableTmuxMonitor": "Enable Tmux Monitor",
|
||||
"enableTmuxMonitorDesc": "Show this host in the Tmux Monitor and add its tmux actions to the sidebar",
|
||||
"connectionToolbar": "Connection Toolbar",
|
||||
"showConnectionToolbar": "Show connection toolbar",
|
||||
"showConnectionToolbarDesc": "Show the toolbar in this host's SSH, RDP, VNC, and Telnet sessions. You can also hide it temporarily from the toolbar itself.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"terminalUrlCopied": "Terminal URL copied",
|
||||
"dockerUrlCopied": "Docker URL copied",
|
||||
"hostMetricsUrlCopied": "Host Metrics URL copied",
|
||||
"tmuxMonitorUrlCopied": "Tmux Monitor URL copied",
|
||||
"rdpUrlCopied": "RDP URL copied",
|
||||
"vncUrlCopied": "VNC URL copied",
|
||||
"telnetUrlCopied": "Telnet URL copied",
|
||||
@@ -1257,7 +1253,6 @@
|
||||
"copyTerminalUrlAction": "Copy Terminal URL",
|
||||
"copyDockerUrlAction": "Copy Docker URL",
|
||||
"copyHostMetricsUrlAction": "Copy Host Metrics URL",
|
||||
"copyTmuxMonitorUrlAction": "Copy Tmux Monitor URL",
|
||||
"copyRdpUrlAction": "Copy RDP URL",
|
||||
"copyVncUrlAction": "Copy VNC URL",
|
||||
"copyTelnetUrlAction": "Copy Telnet URL",
|
||||
@@ -1537,8 +1532,6 @@
|
||||
"userProfileDesc": "Manage your account and preferences",
|
||||
"addCredential": "Add Credential",
|
||||
"addCredentialDesc": "Store SSH keys or passwords",
|
||||
"tmuxMonitor": "Tmux Monitor",
|
||||
"tmuxMonitorDesc": "Monitor tmux sessions across your hosts",
|
||||
"snippets": "Snippets",
|
||||
"runSnippetDesc": "Run in the active terminal tab",
|
||||
"noSnippetsFound": "No snippets found matching \"{{search}}\"",
|
||||
@@ -3099,82 +3092,6 @@
|
||||
"openProxmoxStats": "Open Proxmox Stats",
|
||||
"openTmuxMonitor": "Open Tmux Monitor"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux Monitor",
|
||||
"failedToLoadHosts": "Failed to load hosts",
|
||||
"failedToLoad": "Failed to load tmux sessions",
|
||||
"tmuxUnavailable": "tmux is not installed on this host",
|
||||
"noSessions": "No tmux sessions on this host",
|
||||
"noHostSelected": "No host selected",
|
||||
"attached": "Attached",
|
||||
"detached": "Detached",
|
||||
"attach": "Attach",
|
||||
"editTags": "Edit tags",
|
||||
"tagsHint": "Comma-separated tags (e.g. YOLO, lab, training)",
|
||||
"tagsSaved": "Tags saved",
|
||||
"tagsSaveFailed": "Failed to save tags",
|
||||
"searchPlaceholder": "Search output across sessions...",
|
||||
"searchResults": "{{count}} matches",
|
||||
"searchFailed": "Search failed",
|
||||
"selectPaneHint": "Select a pane to preview its output",
|
||||
"closePreview": "Close preview",
|
||||
"newSession": "New session",
|
||||
"newSessionHint": "Session name (letters, digits, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "Create",
|
||||
"sessionCreated": "Session \"{{name}}\" created",
|
||||
"sessionCreateFailed": "Failed to create session",
|
||||
"splitRight": "Split right",
|
||||
"splitDown": "Split down",
|
||||
"splitFailed": "Failed to split pane",
|
||||
"newWindow": "New window",
|
||||
"windowCreateFailed": "Failed to create window",
|
||||
"attachSessionTooltip": "Attach to {{session}}",
|
||||
"refresh": "Refresh",
|
||||
"refreshFailed": "Failed to refresh sessions",
|
||||
"collapseAll": "Collapse all",
|
||||
"expandAll": "Expand all",
|
||||
"moreActions": "More actions",
|
||||
"sessionStats": "Session stats",
|
||||
"renameSessionTitle": "Rename session \"{{name}}\"",
|
||||
"rename": "Rename",
|
||||
"sessionRenamed": "Session renamed to \"{{name}}\"",
|
||||
"sessionRenameFailed": "Failed to rename session",
|
||||
"editTagsTitle": "Edit tags for \"{{name}}\"",
|
||||
"killPane": "Kill pane",
|
||||
"resizeTree": "Drag to resize — double-click to reset",
|
||||
"reattach": "Re-attach (fixes a garbled view)",
|
||||
"killWindow": "Kill window",
|
||||
"killWindowTitle": "Kill window {{index}} of \"{{session}}\"?",
|
||||
"killWindowBody": "Every pane and process in this window will be terminated. Killing the last window ends the session.",
|
||||
"windowKillFailed": "Failed to kill window",
|
||||
"statusActivity": "Activity",
|
||||
"statusWindows": "Windows",
|
||||
"statusPanes": "panes",
|
||||
"statusTags": "Tags",
|
||||
"killPaneTitle": "Kill pane {{id}}?",
|
||||
"killPaneBody": "The process running in this pane will be terminated. Killing the last pane closes its window.",
|
||||
"paneKillFailed": "Failed to kill pane",
|
||||
"killSessionTitle": "Kill session \"{{name}}\"?",
|
||||
"killSessionBody": "All windows and running processes in this session will be terminated. This cannot be undone.",
|
||||
"kill": "Kill",
|
||||
"sessionKilled": "Session \"{{name}}\" killed",
|
||||
"sessionKillFailed": "Failed to kill session",
|
||||
"hostUnreachable": "Could not connect to the host. Check that it is online and reachable.",
|
||||
"noServer": "No tmux server is running on this host.",
|
||||
"searchTruncated": "Partial results — search covers the last {{lines}} lines of each pane and at most {{panes}} panes.",
|
||||
"closeSearchResults": "Close search results",
|
||||
"retry": "Retry",
|
||||
"noHosts": "No SSH hosts available",
|
||||
"noHostsHint": "Enable the Tmux Monitor option on an SSH host (Host Manager → Terminal tab) to monitor its tmux sessions.",
|
||||
"tmuxInstallHint": "Install it on the host with:",
|
||||
"attachTooltip": "Open a terminal to {{host}}",
|
||||
"attachTooltipPane": "Open a terminal to {{host}} — tmux session {{session}}",
|
||||
"timeJustNow": "just now",
|
||||
"timeMinutes": "{{count}}m ago",
|
||||
"timeHours": "{{count}}h ago",
|
||||
"timeDays": "{{count}}d ago"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Port",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Hernoem oortjie",
|
||||
"openFileManager": "Maak Lêerbestuurder oop",
|
||||
"dashboard": "Dashboard",
|
||||
"tmuxMonitor": "Tmux-monitor",
|
||||
"homepage": "Tuisblad",
|
||||
"quickConnect": "Vinnige verbinding",
|
||||
"sshTools": "SSH-gereedskap",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Max Keepalive mis",
|
||||
"sameHost": "Hierdie gasheer (direkte tonnel)",
|
||||
"bindHost": "Bind gasheer",
|
||||
"enableTmuxMonitor": "Aktiveer Tmux-monitor",
|
||||
"enableTmuxMonitorDesc": "Wys hierdie gasheer in die Tmux Monitor en voeg sy tmux-aksies by die sybalk.",
|
||||
"connectionToolbar": "Verbindingsnutsbalk",
|
||||
"showConnectionToolbar": "Wys verbindingsbalk",
|
||||
"showConnectionToolbarDesc": "Wys die nutsbalk in hierdie gasheer se SSH-, RDP-, VNC- en Telnet-sessies. Jy kan dit ook tydelik van die nutsbalk self versteek.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Tonnel-URL gekopieer",
|
||||
"dockerUrlCopied": "Docker-URL gekopieer",
|
||||
"hostMetricsUrlCopied": "Gasheermetrieke-URL gekopieer",
|
||||
"tmuxMonitorUrlCopied": "Tmux Monitor URL gekopieer",
|
||||
"rdpUrlCopied": "RDP-URL gekopieer",
|
||||
"vncUrlCopied": "VNC URL gekopieer",
|
||||
"telnetUrlCopied": "Telnet-URL gekopieer",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Kopieer Tonnel URL",
|
||||
"copyDockerUrlAction": "Kopieer Docker URL",
|
||||
"copyHostMetricsUrlAction": "Kopieer gasheermetrieke-URL",
|
||||
"copyTmuxMonitorUrlAction": "Kopieer Tmux Monitor URL",
|
||||
"copyRdpUrlAction": "Kopieer RDP-URL",
|
||||
"copyVncUrlAction": "Kopieer VNC URL",
|
||||
"copyTelnetUrlAction": "Kopieer Telnet-URL",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Maak Proxmox-statistieke oop",
|
||||
"openTmuxMonitor": "Maak Tmux-monitor oop"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux-monitor",
|
||||
"failedToLoadHosts": "Kon nie gashere laai nie",
|
||||
"failedToLoad": "Kon nie tmux-sessies laai nie",
|
||||
"tmuxUnavailable": "tmux is nie op hierdie gasheer geïnstalleer nie",
|
||||
"noSessions": "Geen tmux-sessies op hierdie gasheer nie",
|
||||
"noHostSelected": "Geen gasheer gekies nie",
|
||||
"attached": "Aangeheg",
|
||||
"detached": "Losstaande",
|
||||
"attach": "Heg aan",
|
||||
"editTags": "Wysig etikette",
|
||||
"tagsHint": "Komma-geskeide etikette (bv. YOLO, laboratorium, opleiding)",
|
||||
"tagsSaved": "Etikette gestoor",
|
||||
"tagsSaveFailed": "Kon nie etikette stoor nie",
|
||||
"searchPlaceholder": "Soek uitset oor sessies heen...",
|
||||
"searchResults": "{{count}} ooreenkomste",
|
||||
"searchFailed": "Soektog het misluk",
|
||||
"selectPaneHint": "Kies 'n paneel om die uitvoer daarvan te voorskou",
|
||||
"closePreview": "Maak voorskou toe",
|
||||
"newSession": "Nuwe sessie",
|
||||
"newSessionHint": "Sessienaam (letters, syfers, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-sessie",
|
||||
"create": "Skep",
|
||||
"sessionCreated": "Sessie \"{{name}}\" geskep",
|
||||
"sessionCreateFailed": "Kon nie sessie skep nie",
|
||||
"splitRight": "Regs verdeel",
|
||||
"splitDown": "Verdeel af",
|
||||
"splitFailed": "Kon nie paneel verdeel nie",
|
||||
"newWindow": "Nuwe venster",
|
||||
"windowCreateFailed": "Kon nie venster skep nie",
|
||||
"attachSessionTooltip": "Heg aan {{session}}",
|
||||
"refresh": "Verfris",
|
||||
"refreshFailed": "Kon nie sessies verfris nie",
|
||||
"collapseAll": "Vou alles in",
|
||||
"expandAll": "Vou alles uit",
|
||||
"moreActions": "Meer aksies",
|
||||
"sessionStats": "Sessiestatistieke",
|
||||
"renameSessionTitle": "Hernoem sessie \"{{name}}\"",
|
||||
"rename": "Hernoem",
|
||||
"sessionRenamed": "Sessie hernoem na \"{{name}}\"",
|
||||
"sessionRenameFailed": "Kon nie sessie hernoem nie",
|
||||
"editTagsTitle": "Wysig etikette vir \"{{name}}\"",
|
||||
"killPane": "Maak paneel dood",
|
||||
"resizeTree": "Sleep om te verander — dubbelklik om terug te stel",
|
||||
"reattach": "Herbevestig (maak 'n deurmekaar aansig reg)",
|
||||
"killWindow": "Maak venster dood",
|
||||
"killWindowTitle": "Maak venster {{index}} van \"{{session}} \" dood?",
|
||||
"killWindowBody": "Elke paneel en proses in hierdie venster sal beëindig word. Deur die laaste venster dood te maak, word die sessie beëindig.",
|
||||
"windowKillFailed": "Kon nie venster doodmaak nie",
|
||||
"statusActivity": "Aktiwiteit",
|
||||
"statusWindows": "Vensters",
|
||||
"statusPanes": "ruite",
|
||||
"statusTags": "Etikette",
|
||||
"killPaneTitle": "Maak paneel dood {{id}}?",
|
||||
"killPaneBody": "Die proses wat in hierdie paneel loop, sal beëindig word. As die laaste paneel doodgemaak word, word die venster daarvan toegemaak.",
|
||||
"paneKillFailed": "Kon nie paneel doodmaak nie",
|
||||
"killSessionTitle": "Maak sessie \"{{name}} \" dood?",
|
||||
"killSessionBody": "Alle vensters en lopende prosesse in hierdie sessie sal beëindig word. Dit kan nie ongedaan gemaak word nie.",
|
||||
"kill": "Doodmaak",
|
||||
"sessionKilled": "Sessie \"{{name}}\" is doodgemaak",
|
||||
"sessionKillFailed": "Kon nie sessie doodmaak nie",
|
||||
"hostUnreachable": "Kon nie aan die gasheer koppel nie. Maak seker dat dit aanlyn en bereikbaar is.",
|
||||
"noServer": "Geen tmux-bediener loop op hierdie gasheer nie.",
|
||||
"searchTruncated": "Gedeeltelike resultate — soektog dek die laaste {{lines}} reëls van elke paneel en hoogstens {{panes}} panele.",
|
||||
"closeSearchResults": "Maak soekresultate toe",
|
||||
"retry": "Probeer weer",
|
||||
"noHosts": "Geen SSH-gashere beskikbaar nie",
|
||||
"noHostsHint": "Aktiveer die Tmux Monitor-opsie op 'n SSH-gasheer (Gasheerbestuurder → Terminal-oortjie) om sy tmux-sessies te monitor.",
|
||||
"tmuxInstallHint": "Installeer dit op die gasheer met:",
|
||||
"attachTooltip": "Maak 'n terminaal oop na {{host}}",
|
||||
"attachTooltipPane": "Maak 'n terminaal oop na {{host}} — tmux sessie {{session}}",
|
||||
"timeJustNow": "nou net",
|
||||
"timeMinutes": "{{count}}m gelede",
|
||||
"timeHours": "{{count}}uur gelede",
|
||||
"timeDays": "{{count}}dae gelede"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Reeks",
|
||||
"portLabel": "Hawe",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "إعادة تسمية علامة التبويب",
|
||||
"openFileManager": "فتح مدير الملفات",
|
||||
"dashboard": "لوحة المعلومات",
|
||||
"tmuxMonitor": "مراقب Tmux",
|
||||
"homepage": "الصفحة الرئيسية",
|
||||
"quickConnect": "اتصال سريع",
|
||||
"sshTools": "أدوات SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "الحد الأقصى لمرات فقدان إبقاء الاتصال",
|
||||
"sameHost": "هذا المضيف (نفق مباشر)",
|
||||
"bindHost": "مضيف الربط",
|
||||
"enableTmuxMonitor": "تمكين مراقب Tmux",
|
||||
"enableTmuxMonitorDesc": "إظهار هذا المضيف في مراقب Tmux وإضافة إجراءات tmux الخاصة به إلى الشريط الجانبي",
|
||||
"connectionToolbar": "شريط أدوات الاتصال",
|
||||
"showConnectionToolbar": "إظهار شريط أدوات الاتصال",
|
||||
"showConnectionToolbarDesc": "يمكنك إظهار شريط الأدوات في جلسات SSH وRDP وVNC وTelnet الخاصة بهذا المضيف. كما يمكنك إخفاؤه مؤقتًا من شريط الأدوات نفسه.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "تم نسخ رابط النفق",
|
||||
"dockerUrlCopied": "تم نسخ رابط Docker",
|
||||
"hostMetricsUrlCopied": "تم نسخ رابط مقاييس المضيف",
|
||||
"tmuxMonitorUrlCopied": "تم نسخ رابط Tmux Monitor",
|
||||
"rdpUrlCopied": "تم نسخ رابط RDP",
|
||||
"vncUrlCopied": "تم نسخ رابط VNC",
|
||||
"telnetUrlCopied": "تم نسخ رابط Telnet",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "نسخ رابط النفق",
|
||||
"copyDockerUrlAction": "نسخ رابط Docker",
|
||||
"copyHostMetricsUrlAction": "نسخ رابط مقاييس المضيف",
|
||||
"copyTmuxMonitorUrlAction": "نسخ رابط Tmux Monitor",
|
||||
"copyRdpUrlAction": "نسخ رابط RDP",
|
||||
"copyVncUrlAction": "نسخ رابط VNC",
|
||||
"copyTelnetUrlAction": "نسخ رابط Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "افتح إحصائيات Proxmox",
|
||||
"openTmuxMonitor": "افتح شاشة Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "مراقب Tmux",
|
||||
"failedToLoadHosts": "فشل تحميل الأجهزة المضيفة",
|
||||
"failedToLoad": "فشل تحميل جلسات tmux",
|
||||
"tmuxUnavailable": "tmux غير مثبت على هذا الجهاز المضيف",
|
||||
"noSessions": "لا توجد جلسات tmux على هذا الجهاز المضيف",
|
||||
"noHostSelected": "لم يتم تحديد أي جهاز مضيف",
|
||||
"attached": "متصلة",
|
||||
"detached": "منفصلة",
|
||||
"attach": "اتصال",
|
||||
"editTags": "تعديل العلامات",
|
||||
"tagsHint": "علامات مفصولة بفواصل (مثال: YOLO, lab, training)",
|
||||
"tagsSaved": "تم حفظ العلامات",
|
||||
"tagsSaveFailed": "فشل حفظ العلامات",
|
||||
"searchPlaceholder": "بحث في المخرجات عبر الجلسات...",
|
||||
"searchResults": "{{count}} مطابقة",
|
||||
"searchFailed": "فشل البحث",
|
||||
"selectPaneHint": "حدد إطارًا لمعاينة مخرجاته",
|
||||
"closePreview": "إغلاق المعاينة",
|
||||
"newSession": "جلسة جديدة",
|
||||
"newSessionHint": "اسم الجلسة (حروف، أرقام، _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "إنشاء",
|
||||
"sessionCreated": "تم إنشاء الجلسة \"{{name}}\"",
|
||||
"sessionCreateFailed": "فشل إنشاء الجلسة",
|
||||
"splitRight": "تقسيم لليمين",
|
||||
"splitDown": "تقسيم للأسفل",
|
||||
"splitFailed": "فشل تقسيم الإطار",
|
||||
"newWindow": "نافذة جديدة",
|
||||
"windowCreateFailed": "فشل إنشاء النافذة",
|
||||
"attachSessionTooltip": "اتصل بـ {{session}}",
|
||||
"refresh": "تحديث",
|
||||
"refreshFailed": "فشل تحديث الجلسات",
|
||||
"collapseAll": "طي الكل",
|
||||
"expandAll": "توسيع الكل",
|
||||
"moreActions": "إجراءات أخرى",
|
||||
"sessionStats": "إحصائيات الجلسة",
|
||||
"renameSessionTitle": "إعادة تسمية الجلسة \"{{name}}\"",
|
||||
"rename": "إعادة تسمية",
|
||||
"sessionRenamed": "تمت إعادة تسمية الجلسة إلى \"{{name}}\"",
|
||||
"sessionRenameFailed": "فشل إعادة تسمية الجلسة",
|
||||
"editTagsTitle": "تعديل العلامات لـ \"{{name}}\"",
|
||||
"killPane": "إنهاء الجزء",
|
||||
"resizeTree": "اسحب لتغيير الحجم — انقر نقرًا مزدوجًا لإعادة الضبط",
|
||||
"reattach": "إعادة الارتباط (لإصلاح العرض المشوش)",
|
||||
"killWindow": "إنهاء النافذة",
|
||||
"killWindowTitle": "هل تريد إنهاء النافذة {{index}} من الجلسة \"{{session}}\"؟",
|
||||
"killWindowBody": "سيتم إنهاء كل جزء وعملية في هذه النافذة. إنهاء آخر نافذة ينهي الجلسة.",
|
||||
"windowKillFailed": "فشل إنهاء النافذة",
|
||||
"statusActivity": "نشاط",
|
||||
"statusWindows": "نوافذ",
|
||||
"statusPanes": "أجزاء",
|
||||
"statusTags": "وسوم",
|
||||
"killPaneTitle": "هل تريد إنهاء الجزء {{id}}؟",
|
||||
"killPaneBody": "سيتم إنهاء العملية الجارية في هذا الجزء. إنهاء آخر جزء يغلق نافذته.",
|
||||
"paneKillFailed": "فشل إنهاء الجزء",
|
||||
"killSessionTitle": "هل تريد إنهاء الجلسة \"{{name}}\"؟",
|
||||
"killSessionBody": "سيتم إنهاء جميع النوافذ والعمليات الجارية في هذه الجلسة. لا يمكن التراجع عن ذلك.",
|
||||
"kill": "إنهاء",
|
||||
"sessionKilled": "تم إنهاء الجلسة \"{{name}}\"",
|
||||
"sessionKillFailed": "فشل إنهاء الجلسة",
|
||||
"hostUnreachable": "تعذر الاتصال بالمضيف. تأكد من أنه متصل وقابل للوصول.",
|
||||
"noServer": "لا يوجد خادم tmux يعمل على هذا المضيف.",
|
||||
"searchTruncated": "نتائج جزئية — يشمل البحث آخر {{lines}} سطرًا من كل جزء وبحد أقصى {{panes}} جزءًا.",
|
||||
"closeSearchResults": "إغلاق نتائج البحث",
|
||||
"retry": "إعادة المحاولة",
|
||||
"noHosts": "لا توجد مضيفات SSH متاحة",
|
||||
"noHostsHint": "قم بتمكين خيار Tmux Monitor على مضيف SSH (مدير المضيفات → تبويب الطرفية) لمراقبة جلسات tmux الخاصة به.",
|
||||
"tmuxInstallHint": "قم بتثبيته على المضيف باستخدام:",
|
||||
"attachTooltip": "افتح طرفية إلى {{host}}",
|
||||
"attachTooltipPane": "افتح طرفية إلى {{host}} — جلسة tmux {{session}}",
|
||||
"timeJustNow": "الآن",
|
||||
"timeMinutes": "قبل {{count}}m",
|
||||
"timeHours": "قبل {{count}}h",
|
||||
"timeDays": "قبل {{count}}d"
|
||||
},
|
||||
"serial": {
|
||||
"title": "منفذ تسلسلي",
|
||||
"portLabel": "المنفذ",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Преименуване на раздел",
|
||||
"openFileManager": "Отворете файловия мениджър",
|
||||
"dashboard": "Табло за управление",
|
||||
"tmuxMonitor": "Tmux монитор",
|
||||
"homepage": "Начална страница",
|
||||
"quickConnect": "Бързо свързване",
|
||||
"sshTools": "SSH инструменти",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Максимални пропуски на Keepalive",
|
||||
"sameHost": "Този хост (директен тунел)",
|
||||
"bindHost": "Свързване на хост",
|
||||
"enableTmuxMonitor": "Активиране на Tmux монитор",
|
||||
"enableTmuxMonitorDesc": "Показване на този хост в Tmux монитора и добавяне на неговите tmux действия към страничната лента",
|
||||
"connectionToolbar": "Инструменти за свързване",
|
||||
"showConnectionToolbar": "Показване на лентата с инструменти за връзки",
|
||||
"showConnectionToolbarDesc": "Показване на лентата с инструменти в SSH, RDP, VNC и Telnet сесиите на този хост. Можете също така временно да я скриете от самата лента с инструменти.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "URL адресът на тунела е копиран",
|
||||
"dockerUrlCopied": "URL адресът на Docker е копиран",
|
||||
"hostMetricsUrlCopied": "URL адресът на показателите за хоста е копиран",
|
||||
"tmuxMonitorUrlCopied": "URL адресът на Tmux Monitor е копиран",
|
||||
"rdpUrlCopied": "RDP URL адресът е копиран",
|
||||
"vncUrlCopied": "VNC URL адресът е копиран",
|
||||
"telnetUrlCopied": "URL адресът на Telnet е копиран",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Копиране на URL адреса на тунела",
|
||||
"copyDockerUrlAction": "Копиране на URL адреса на Docker",
|
||||
"copyHostMetricsUrlAction": "Копиране на URL адреса на показателите за хоста",
|
||||
"copyTmuxMonitorUrlAction": "Копиране на URL адреса на Tmux Monitor",
|
||||
"copyRdpUrlAction": "Копиране на RDP URL адрес",
|
||||
"copyVncUrlAction": "Копиране на VNC URL адрес",
|
||||
"copyTelnetUrlAction": "Копиране на Telnet URL адрес",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Отворете статистиката на Proxmox",
|
||||
"openTmuxMonitor": "Отворете Tmux монитора"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux монитор",
|
||||
"failedToLoadHosts": "Зареждането на хостовете не бе успешно",
|
||||
"failedToLoad": "Зареждането на tmux сесиите не бе успешно",
|
||||
"tmuxUnavailable": "tmux не е инсталиран на този хост",
|
||||
"noSessions": "Няма tmux сесии на този хост",
|
||||
"noHostSelected": "Няма избран хост",
|
||||
"attached": "Прикачен",
|
||||
"detached": "Отделен",
|
||||
"attach": "Прикачи",
|
||||
"editTags": "Редактиране на етикети",
|
||||
"tagsHint": "Етикети, разделени със запетаи (напр. YOLO, лаборатория, обучение)",
|
||||
"tagsSaved": "Етикетите са запазени",
|
||||
"tagsSaveFailed": "Запазването на етикетите не бе успешно",
|
||||
"searchPlaceholder": "Търсене на резултати в различни сесии...",
|
||||
"searchResults": "{{count}} съвпадения",
|
||||
"searchFailed": "Търсенето не беше успешно",
|
||||
"selectPaneHint": "Изберете панел, за да прегледате резултата му",
|
||||
"closePreview": "Затвори визуализацията",
|
||||
"newSession": "Нова сесия",
|
||||
"newSessionHint": "Име на сесията (букви, цифри, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "моята сесия",
|
||||
"create": "Създаване",
|
||||
"sessionCreated": "Сесия „{{name}}“ е създадена",
|
||||
"sessionCreateFailed": "Създаването на сесия не бе успешно",
|
||||
"splitRight": "Разделяне надясно",
|
||||
"splitDown": "Разделете",
|
||||
"splitFailed": "Разделянето на панела не бе успешно",
|
||||
"newWindow": "Нов прозорец",
|
||||
"windowCreateFailed": "Създаването на прозорец не бе успешно",
|
||||
"attachSessionTooltip": "Прикачи към {{session}}",
|
||||
"refresh": "Обновяване",
|
||||
"refreshFailed": "Неуспешно обновяване на сесиите",
|
||||
"collapseAll": "Свиване на всички",
|
||||
"expandAll": "Разгъване на всички",
|
||||
"moreActions": "Още действия",
|
||||
"sessionStats": "Статистика за сесиите",
|
||||
"renameSessionTitle": "Преименуване на сесия „{{name}}“",
|
||||
"rename": "Преименуване",
|
||||
"sessionRenamed": "Сесията е преименувана на „{{name}}“",
|
||||
"sessionRenameFailed": "Преименуването на сесията не бе успешно",
|
||||
"editTagsTitle": "Редактиране на етикети за „{{name}}“",
|
||||
"killPane": "Панелът за убиване",
|
||||
"resizeTree": "Плъзнете, за да промените размера — щракнете двукратно, за да нулирате",
|
||||
"reattach": "Повторно прикрепване (коригира неправилен изглед)",
|
||||
"killWindow": "Прозорец за затваряне",
|
||||
"killWindowTitle": "Да завършим ли прозореца {{index}} от \"{{session}}\"?",
|
||||
"killWindowBody": "Всеки панел и процес в този прозорец ще бъде прекратен. Затварянето на последния прозорец прекратява сесията.",
|
||||
"windowKillFailed": "Неуспешно затваряне на прозореца",
|
||||
"statusActivity": "Дейност",
|
||||
"statusWindows": "Прозорци",
|
||||
"statusPanes": "прозорци",
|
||||
"statusTags": "Етикети",
|
||||
"killPaneTitle": "Да се прекрати панелът {{id}}?",
|
||||
"killPaneBody": "Процесът, изпълняващ се в този панел, ще бъде прекратен. Затварянето на последния панел затваря неговия прозорец.",
|
||||
"paneKillFailed": "Неуспешно прекратяване на панела",
|
||||
"killSessionTitle": "Да прекратя сесията „{{name}}“?",
|
||||
"killSessionBody": "Всички прозорци и работещи процеси в тази сесия ще бъдат прекратени. Това не може да бъде отменено.",
|
||||
"kill": "Убий",
|
||||
"sessionKilled": "Сесия „{{name}}“ е прекратена",
|
||||
"sessionKillFailed": "Неуспешно прекратяване на сесията",
|
||||
"hostUnreachable": "Не можах да се свържа с хоста. Проверете дали е онлайн и достъпен.",
|
||||
"noServer": "На този хост не работи tmux сървър.",
|
||||
"searchTruncated": "Частични резултати — търсенето обхваща последните {{lines}} реда от всеки панел и най-много {{panes}} панела.",
|
||||
"closeSearchResults": "Затвори резултатите от търсенето",
|
||||
"retry": "Опитай отново",
|
||||
"noHosts": "Няма налични SSH хостове",
|
||||
"noHostsHint": "Активирайте опцията Tmux Monitor на SSH хост (Host Manager → раздел Terminal), за да наблюдавате неговите tmux сесии.",
|
||||
"tmuxInstallHint": "Инсталирайте го на хоста с:",
|
||||
"attachTooltip": "Отворете терминал до {{host}}",
|
||||
"attachTooltipPane": "Отворете терминал към {{host}} — tmux сесия {{session}}",
|
||||
"timeJustNow": "точно сега",
|
||||
"timeMinutes": "преди {{count}}мин.",
|
||||
"timeHours": "преди {{count}}ч",
|
||||
"timeDays": "преди {{count}}дни"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Сериен",
|
||||
"portLabel": "Порт",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "ট্যাবের নাম পরিবর্তন করুন",
|
||||
"openFileManager": "ফাইল ম্যানেজার খুলুন",
|
||||
"dashboard": "ড্যাশবোর্ড",
|
||||
"tmuxMonitor": "Tmux মনিটর",
|
||||
"homepage": "হোমপেজ",
|
||||
"quickConnect": "দ্রুত সংযোগ",
|
||||
"sshTools": "SSH টুলস",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "ম্যাক্স কিপঅ্যালাইভ মিস করে",
|
||||
"sameHost": "এই হোস্ট (সরাসরি টানেল)",
|
||||
"bindHost": "হোস্টকে বাঁধুন",
|
||||
"enableTmuxMonitor": "Tmux মনিটর সক্রিয় করুন",
|
||||
"enableTmuxMonitorDesc": "এই হোস্টটিকে Tmux মনিটরে দেখান এবং এর tmux অ্যাকশনগুলো সাইডবারে যোগ করুন।",
|
||||
"connectionToolbar": "সংযোগ টুলবার",
|
||||
"showConnectionToolbar": "সংযোগ টুলবার দেখান",
|
||||
"showConnectionToolbarDesc": "এই হোস্টের SSH, RDP, VNC, এবং Telnet সেশনগুলিতে টুলবারটি দেখান। আপনি টুলবারটি থেকেই এটিকে সাময়িকভাবে লুকাতেও পারেন।",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "টানেল ইউআরএল কপি করা হয়েছে",
|
||||
"dockerUrlCopied": "ডকার ইউআরএল কপি করা হয়েছে",
|
||||
"hostMetricsUrlCopied": "হোস্ট মেট্রিক্স ইউআরএল কপি করা হয়েছে",
|
||||
"tmuxMonitorUrlCopied": "Tmux মনিটর URL কপি করা হয়েছে",
|
||||
"rdpUrlCopied": "RDP URL কপি করা হয়েছে",
|
||||
"vncUrlCopied": "VNC URL কপি করা হয়েছে",
|
||||
"telnetUrlCopied": "টেলনেট ইউআরএল কপি করা হয়েছে",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "টানেল ইউআরএল কপি করুন",
|
||||
"copyDockerUrlAction": "ডকার ইউআরএল কপি করুন",
|
||||
"copyHostMetricsUrlAction": "হোস্ট মেট্রিক্স ইউআরএল কপি করুন",
|
||||
"copyTmuxMonitorUrlAction": "Tmux মনিটর URL কপি করুন",
|
||||
"copyRdpUrlAction": "RDP URL কপি করুন",
|
||||
"copyVncUrlAction": "VNC URL কপি করুন",
|
||||
"copyTelnetUrlAction": "টেলনেট ইউআরএল কপি করুন",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "ওপেন প্রক্সমক্স স্ট্যাটস",
|
||||
"openTmuxMonitor": "ওপেন Tmux মনিটর"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux মনিটর",
|
||||
"failedToLoadHosts": "হোস্ট লোড করতে ব্যর্থ হয়েছে",
|
||||
"failedToLoad": "tmux সেশন লোড করতে ব্যর্থ হয়েছে",
|
||||
"tmuxUnavailable": "এই হোস্টে tmux ইনস্টল করা নেই",
|
||||
"noSessions": "এই হোস্টে কোনো tmux সেশন নেই",
|
||||
"noHostSelected": "কোন হোস্ট নির্বাচন করা হয়নি",
|
||||
"attached": "সংযুক্ত",
|
||||
"detached": "বিচ্ছিন্ন",
|
||||
"attach": "সংযুক্ত করুন",
|
||||
"editTags": "ট্যাগ সম্পাদনা করুন",
|
||||
"tagsHint": "কমা দ্বারা পৃথক করা ট্যাগ (যেমন YOLO, lab, training)",
|
||||
"tagsSaved": "ট্যাগগুলি সংরক্ষিত হয়েছে",
|
||||
"tagsSaveFailed": "ট্যাগ সংরক্ষণ করতে ব্যর্থ হয়েছে",
|
||||
"searchPlaceholder": "বিভিন্ন সেশন জুড়ে অনুসন্ধানের ফলাফল...",
|
||||
"searchResults": "{{count}} মিল",
|
||||
"searchFailed": "অনুসন্ধান ব্যর্থ হয়েছে",
|
||||
"selectPaneHint": "এর আউটপুট প্রিভিউ করতে একটি প্যান নির্বাচন করুন।",
|
||||
"closePreview": "ক্লোজ প্রিভিউ",
|
||||
"newSession": "নতুন অধিবেশন",
|
||||
"newSessionHint": "সেশনের নাম (অক্ষর, সংখ্যা, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "আমার-সেশন",
|
||||
"create": "তৈরি করুন",
|
||||
"sessionCreated": "সেশন \"{{name}}\" তৈরি করা হয়েছে",
|
||||
"sessionCreateFailed": "সেশন তৈরি করতে ব্যর্থ হয়েছে",
|
||||
"splitRight": "ডানদিকে বিভক্ত",
|
||||
"splitDown": "বিভক্ত করুন",
|
||||
"splitFailed": "প্যান বিভক্ত করতে ব্যর্থ হয়েছে",
|
||||
"newWindow": "নতুন জানালা",
|
||||
"windowCreateFailed": "উইন্ডো তৈরি করতে ব্যর্থ হয়েছে",
|
||||
"attachSessionTooltip": "{{session}} এর সাথে সংযুক্ত করুন",
|
||||
"refresh": "রিফ্রেশ",
|
||||
"refreshFailed": "সেশনগুলি রিফ্রেশ করতে ব্যর্থ হয়েছে",
|
||||
"collapseAll": "সবগুলো সংকুচিত করুন",
|
||||
"expandAll": "সবগুলো প্রসারিত করুন",
|
||||
"moreActions": "আরও পদক্ষেপ",
|
||||
"sessionStats": "সেশন পরিসংখ্যান",
|
||||
"renameSessionTitle": "সেশনের নাম পরিবর্তন করুন \"{{name}}\"",
|
||||
"rename": "পুনঃনামকরণ",
|
||||
"sessionRenamed": "সেশনের নাম পরিবর্তন করে \"{{name}} \" রাখা হয়েছে",
|
||||
"sessionRenameFailed": "সেশনের নাম পরিবর্তন করতে ব্যর্থ হয়েছে",
|
||||
"editTagsTitle": "\"{{name}} \" এর জন্য ট্যাগ সম্পাদনা করুন",
|
||||
"killPane": "কিল প্যান",
|
||||
"resizeTree": "আকার পরিবর্তন করতে টেনে আনুন — রিসেট করতে ডাবল-ক্লিক করুন",
|
||||
"reattach": "পুনরায় সংযুক্ত করুন (বিকৃত দৃশ্য ঠিক করে)",
|
||||
"killWindow": "কিল উইন্ডো",
|
||||
"killWindowTitle": "\"{{session}} \" উইন্ডোটি {{index}} বন্ধ করুন?",
|
||||
"killWindowBody": "এই উইন্ডোর প্রতিটি প্যান এবং প্রসেস বন্ধ করে দেওয়া হবে। সর্বশেষ উইন্ডোটি বন্ধ করলে সেশনটি শেষ হয়ে যায়।",
|
||||
"windowKillFailed": "উইন্ডো বন্ধ করতে ব্যর্থ হয়েছে",
|
||||
"statusActivity": "কার্যকলাপ",
|
||||
"statusWindows": "উইন্ডোজ",
|
||||
"statusPanes": "প্যান",
|
||||
"statusTags": "ট্যাগ",
|
||||
"killPaneTitle": "প্যান বন্ধ করুন {{id}}?",
|
||||
"killPaneBody": "এই প্যানে চলমান প্রসেসটি বন্ধ করে দেওয়া হবে। সর্বশেষ প্যানটি কিল করলে তার উইন্ডোটি বন্ধ হয়ে যায়।",
|
||||
"paneKillFailed": "প্যানটি বন্ধ করতে ব্যর্থ হয়েছে",
|
||||
"killSessionTitle": "সেশন বন্ধ করুন \"{{name}}\"?",
|
||||
"killSessionBody": "এই সেশনের সমস্ত উইন্ডো এবং চলমান প্রসেস বন্ধ করে দেওয়া হবে। এটি পূর্বাবস্থায় ফেরানো যাবে না।",
|
||||
"kill": "হত্যা করুন",
|
||||
"sessionKilled": "সেশন \"{{name}}\" বাতিল করা হয়েছে",
|
||||
"sessionKillFailed": "সেশন বন্ধ করতে ব্যর্থ হয়েছে",
|
||||
"hostUnreachable": "হোস্টের সাথে সংযোগ স্থাপন করা যায়নি। এটি অনলাইন এবং অ্যাক্সেসযোগ্য আছে কিনা তা যাচাই করুন।",
|
||||
"noServer": "এই হোস্টে কোনো tmux সার্ভার চালু নেই।",
|
||||
"searchTruncated": "আংশিক ফলাফল — অনুসন্ধান প্রতিটি পেনের শেষ {{lines}} টি লাইন এবং সর্বাধিক {{panes}} টি পেন পর্যন্ত করা হয়।",
|
||||
"closeSearchResults": "অনুসন্ধানের ফলাফল বন্ধ করুন",
|
||||
"retry": "পুনরায় চেষ্টা করুন",
|
||||
"noHosts": "কোন SSH হোস্ট উপলব্ধ নেই",
|
||||
"noHostsHint": "একটি SSH হোস্টের tmux সেশনগুলো মনিটর করার জন্য, হোস্ট ম্যানেজার → টার্মিনাল ট্যাবে Tmux মনিটর অপশনটি সক্রিয় করুন।",
|
||||
"tmuxInstallHint": "হোস্টে এটি ইনস্টল করুন:",
|
||||
"attachTooltip": "{{host}} এ একটি টার্মিনাল খুলুন",
|
||||
"attachTooltipPane": "টার্মিনাল খুলুন {{host}} — tmux সেশন {{session}}",
|
||||
"timeJustNow": "এইমাত্র",
|
||||
"timeMinutes": "{{count}}মিনিট আগে",
|
||||
"timeHours": "{{count}}ঘণ্টা আগে",
|
||||
"timeDays": "{{count}}দিন আগে"
|
||||
},
|
||||
"serial": {
|
||||
"title": "সিরিয়াল",
|
||||
"portLabel": "বন্দর",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Canvia el nom de la pestanya",
|
||||
"openFileManager": "Obre el gestor de fitxers",
|
||||
"dashboard": "Tauler de control",
|
||||
"tmuxMonitor": "Monitor Tmux",
|
||||
"homepage": "Pàgina d'inici",
|
||||
"quickConnect": "Connexió ràpida",
|
||||
"sshTools": "Eines SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Màxims errors de manteniment en directe",
|
||||
"sameHost": "Aquest host (túnel directe)",
|
||||
"bindHost": "Vincula l'amfitrió",
|
||||
"enableTmuxMonitor": "Activa el monitor Tmux",
|
||||
"enableTmuxMonitorDesc": "Mostra aquest host al monitor Tmux i afegeix les seves accions tmux a la barra lateral",
|
||||
"connectionToolbar": "Barra d'eines de connexió",
|
||||
"showConnectionToolbar": "Mostra la barra d'eines de connexió",
|
||||
"showConnectionToolbarDesc": "Mostra la barra d'eines a les sessions SSH, RDP, VNC i Telnet d'aquest amfitrió. També la pots ocultar temporalment des de la barra d'eines.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "S'ha copiat l'URL del túnel",
|
||||
"dockerUrlCopied": "S'ha copiat l'URL de Docker",
|
||||
"hostMetricsUrlCopied": "URL de mètriques de l'amfitrió copiada",
|
||||
"tmuxMonitorUrlCopied": "S'ha copiat l'URL del monitor Tmux",
|
||||
"rdpUrlCopied": "URL RDP copiada",
|
||||
"vncUrlCopied": "URL de VNC copiada",
|
||||
"telnetUrlCopied": "S'ha copiat l'URL de Telnet",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Copia l'URL del túnel",
|
||||
"copyDockerUrlAction": "Copia l'URL de Docker",
|
||||
"copyHostMetricsUrlAction": "Copia l'URL de les mètriques de l'amfitrió",
|
||||
"copyTmuxMonitorUrlAction": "Copia l'URL del monitor Tmux",
|
||||
"copyRdpUrlAction": "Copia l'URL de l'RDP",
|
||||
"copyVncUrlAction": "Copia l'URL de VNC",
|
||||
"copyTelnetUrlAction": "Copia l'URL de Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Obre les estadístiques de Proxmox",
|
||||
"openTmuxMonitor": "Obre el monitor Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Monitor Tmux",
|
||||
"failedToLoadHosts": "No s'han pogut carregar els amfitrions",
|
||||
"failedToLoad": "No s'han pogut carregar les sessions tmux",
|
||||
"tmuxUnavailable": "tmux no està instal·lat en aquest host",
|
||||
"noSessions": "No hi ha sessions tmux en aquest host.",
|
||||
"noHostSelected": "No s'ha seleccionat cap amfitrió",
|
||||
"attached": "Adjuntat",
|
||||
"detached": "Desconnectat",
|
||||
"attach": "Adjunta",
|
||||
"editTags": "Edita les etiquetes",
|
||||
"tagsHint": "Etiquetes separades per comes (per exemple, YOLO, laboratori, formació)",
|
||||
"tagsSaved": "Etiquetes desades",
|
||||
"tagsSaveFailed": "No s'han pogut desar les etiquetes",
|
||||
"searchPlaceholder": "Cerca resultats entre sessions...",
|
||||
"searchResults": "coincidències {{count}}",
|
||||
"searchFailed": "La cerca ha fallat",
|
||||
"selectPaneHint": "Seleccioneu un panell per previsualitzar-ne el resultat",
|
||||
"closePreview": "Tanca la vista prèvia",
|
||||
"newSession": "Nova sessió",
|
||||
"newSessionHint": "Nom de la sessió (lletres, dígits, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "la meva sessió",
|
||||
"create": "Crea",
|
||||
"sessionCreated": "S'ha creat la sessió \"{{name}}\"",
|
||||
"sessionCreateFailed": "No s'ha pogut crear la sessió",
|
||||
"splitRight": "Dividir a la dreta",
|
||||
"splitDown": "Dividir",
|
||||
"splitFailed": "No s'ha pogut dividir el panell",
|
||||
"newWindow": "Nova finestra",
|
||||
"windowCreateFailed": "No s'ha pogut crear la finestra",
|
||||
"attachSessionTooltip": "Adjunta a {{session}}",
|
||||
"refresh": "Actualitza",
|
||||
"refreshFailed": "No s'han pogut actualitzar les sessions",
|
||||
"collapseAll": "Replega-ho tot",
|
||||
"expandAll": "Expandeix-ho tot",
|
||||
"moreActions": "Més accions",
|
||||
"sessionStats": "Estadístiques de sessió",
|
||||
"renameSessionTitle": "Canvia el nom de la sessió \"{{name}}\"",
|
||||
"rename": "Canvia el nom",
|
||||
"sessionRenamed": "La sessió s'ha rebatejat com a \"{{name}}\"",
|
||||
"sessionRenameFailed": "No s'ha pogut canviar el nom de la sessió",
|
||||
"editTagsTitle": "Edita les etiquetes per a \"{{name}}\"",
|
||||
"killPane": "Tauler de tancament",
|
||||
"resizeTree": "Arrossega per canviar la mida — fes doble clic per restablir",
|
||||
"reattach": "Torna a connectar (corregeix una vista distorsionada)",
|
||||
"killWindow": "Finestra de tancament",
|
||||
"killWindowTitle": "Finestra de tancament {{index}} de \"{{session}}\"?",
|
||||
"killWindowBody": "Tots els panells i processos d'aquesta finestra finalitzaran. Tancar l'última finestra finalitza la sessió.",
|
||||
"windowKillFailed": "No s'ha pogut tancar la finestra",
|
||||
"statusActivity": "Activitat",
|
||||
"statusWindows": "Finestres",
|
||||
"statusPanes": "panells",
|
||||
"statusTags": "Etiquetes",
|
||||
"killPaneTitle": "Tanca el panell {{id}}?",
|
||||
"killPaneBody": "El procés que s'executa en aquest panell finalitzarà. Si tanqueu l'últim panell, se'n tanca la finestra.",
|
||||
"paneKillFailed": "No s'ha pogut tancar el panell",
|
||||
"killSessionTitle": "Tancar sessió \"{{name}}\"?",
|
||||
"killSessionBody": "Totes les finestres i els processos en execució d'aquesta sessió es tancaran. Això no es pot desfer.",
|
||||
"kill": "Matar",
|
||||
"sessionKilled": "La sessió \"{{name}}\" ha estat cancel·lada",
|
||||
"sessionKillFailed": "No s'ha pogut tancar la sessió",
|
||||
"hostUnreachable": "No s'ha pogut connectar amb l'amfitrió. Comproveu que estigui en línia i accessible.",
|
||||
"noServer": "No hi ha cap servidor tmux en execució en aquest host.",
|
||||
"searchTruncated": "Resultats parcials: la cerca cobreix les últimes {{lines}} línies de cada panell i com a màxim {{panes}} panells.",
|
||||
"closeSearchResults": "Tanca els resultats de la cerca",
|
||||
"retry": "Torna-ho a intentar",
|
||||
"noHosts": "No hi ha hosts SSH disponibles",
|
||||
"noHostsHint": "Activeu l'opció Monitor de Tmux en un host SSH (Gestor d'amfitrions → pestanya Terminal) per monitoritzar les seves sessions tmux.",
|
||||
"tmuxInstallHint": "Instal·leu-lo a l'amfitrió amb:",
|
||||
"attachTooltip": "Obre un terminal a {{host}}",
|
||||
"attachTooltipPane": "Obre un terminal a {{host}} — sessió tmux {{session}}",
|
||||
"timeJustNow": "ara mateix",
|
||||
"timeMinutes": "fa {{count}}minuts",
|
||||
"timeHours": "fa {{count}}h",
|
||||
"timeDays": "fa {{count}}dies"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Sèrie",
|
||||
"portLabel": "Port",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Přejmenovat kartu",
|
||||
"openFileManager": "Otevřít Správce souborů",
|
||||
"dashboard": "Dashboard",
|
||||
"tmuxMonitor": "Monitor Tmux",
|
||||
"homepage": "Domovská stránka",
|
||||
"quickConnect": "Rychlé připojení",
|
||||
"sshTools": "SSH nástroje",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Max. chyb v Keepalive",
|
||||
"sameHost": "Tento hostitel (přímý tunel)",
|
||||
"bindHost": "Vázat hostitele",
|
||||
"enableTmuxMonitor": "Povolit monitorování Tmux",
|
||||
"enableTmuxMonitorDesc": "Zobrazit tohoto hostitele v Tmux Monitoru a přidat jeho tmux akce do postranního panelu",
|
||||
"connectionToolbar": "Panel nástrojů pro připojení",
|
||||
"showConnectionToolbar": "Zobrazit panel nástrojů pro připojení",
|
||||
"showConnectionToolbarDesc": "Zobrazit panel nástrojů v relacích SSH, RDP, VNC a Telnet tohoto hostitele. Můžete jej také dočasně skrýt přímo v panelu nástrojů.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "URL tunelu zkopírována",
|
||||
"dockerUrlCopied": "URL adresa Dockeru zkopírována",
|
||||
"hostMetricsUrlCopied": "URL adresa metrik hostitele zkopírována",
|
||||
"tmuxMonitorUrlCopied": "URL adresa monitoru Tmux zkopírována",
|
||||
"rdpUrlCopied": "URL adresa RDP zkopírována",
|
||||
"vncUrlCopied": "URL adresa VNC zkopírována",
|
||||
"telnetUrlCopied": "URL adresa Telnetu zkopírována",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Kopírovat URL tunelu",
|
||||
"copyDockerUrlAction": "Kopírovat URL Dockeru",
|
||||
"copyHostMetricsUrlAction": "Kopírovat URL metrik hostitele",
|
||||
"copyTmuxMonitorUrlAction": "Kopírovat URL adresu monitoru Tmux",
|
||||
"copyRdpUrlAction": "Kopírovat URL adresu RDP",
|
||||
"copyVncUrlAction": "Kopírovat URL VNC",
|
||||
"copyTelnetUrlAction": "Kopírovat URL adresu Telnetu",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Otevřít statistiky Proxmoxu",
|
||||
"openTmuxMonitor": "Otevřít monitor Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Monitor Tmux",
|
||||
"failedToLoadHosts": "Nepodařilo se načíst hostitele",
|
||||
"failedToLoad": "Nepodařilo se načíst relace tmux",
|
||||
"tmuxUnavailable": "tmux není na tomto hostiteli nainstalován",
|
||||
"noSessions": "Na tomto hostiteli nejsou žádné relace tmux.",
|
||||
"noHostSelected": "Nebyl vybrán žádný hostitel",
|
||||
"attached": "Připojeno",
|
||||
"detached": "Samostatně stojící",
|
||||
"attach": "Připojit",
|
||||
"editTags": "Upravit štítky",
|
||||
"tagsHint": "Štítky oddělené čárkami (např. YOLO, lab, training)",
|
||||
"tagsSaved": "Štítky uloženy",
|
||||
"tagsSaveFailed": "Uložení štítků se nezdařilo",
|
||||
"searchPlaceholder": "Vyhledávání výstupu napříč relacemi...",
|
||||
"searchResults": "{{count}} shod",
|
||||
"searchFailed": "Hledání se nezdařilo",
|
||||
"selectPaneHint": "Vyberte podokno pro náhled jeho výstupu",
|
||||
"closePreview": "Zavřít náhled",
|
||||
"newSession": "Nová relace",
|
||||
"newSessionHint": "Název relace (písmena, číslice, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "moje relace",
|
||||
"create": "Vytvořit",
|
||||
"sessionCreated": "Relace „{{name}}“ byla vytvořena",
|
||||
"sessionCreateFailed": "Nepodařilo se vytvořit relaci",
|
||||
"splitRight": "Rozdělit vpravo",
|
||||
"splitDown": "Rozdělit",
|
||||
"splitFailed": "Rozdělení panelu se nezdařilo",
|
||||
"newWindow": "Nové okno",
|
||||
"windowCreateFailed": "Nepodařilo se vytvořit okno",
|
||||
"attachSessionTooltip": "Připojit k {{session}}",
|
||||
"refresh": "Obnovit",
|
||||
"refreshFailed": "Nepodařilo se obnovit relace",
|
||||
"collapseAll": "Sbalit vše",
|
||||
"expandAll": "Rozbalit vše",
|
||||
"moreActions": "Další akce",
|
||||
"sessionStats": "Statistiky relací",
|
||||
"renameSessionTitle": "Přejmenovat relaci „{{name}}“",
|
||||
"rename": "Přejmenovat",
|
||||
"sessionRenamed": "Relace přejmenována na „{{name}}“",
|
||||
"sessionRenameFailed": "Přejmenování relace se nezdařilo",
|
||||
"editTagsTitle": "Upravit štítky pro „{{name}}“",
|
||||
"killPane": "Zavřete panel",
|
||||
"resizeTree": "Přetažením změníte velikost – dvojitým kliknutím resetujete",
|
||||
"reattach": "Znovu připevnit (opravuje zkreslený obraz)",
|
||||
"killWindow": "Okno pro ukončení",
|
||||
"killWindowTitle": "Zavřít okno {{index}} z \"{{session}}\"?",
|
||||
"killWindowBody": "Každé podokno a proces v tomto okně bude ukončeno. Zavřením posledního okna se relace ukončí.",
|
||||
"windowKillFailed": "Nepodařilo se ukončit okno",
|
||||
"statusActivity": "Aktivita",
|
||||
"statusWindows": "Okna",
|
||||
"statusPanes": "tabule",
|
||||
"statusTags": "Štítky",
|
||||
"killPaneTitle": "Zavřít panel {{id}}?",
|
||||
"killPaneBody": "Proces spuštěný v tomto panelu bude ukončen. Ukončením posledního panelu se zavře jeho okno.",
|
||||
"paneKillFailed": "Nepodařilo se ukončit podokno",
|
||||
"killSessionTitle": "Ukončit relaci „{{name}}“?",
|
||||
"killSessionBody": "Všechna okna a spuštěné procesy v této relaci budou ukončeny. Tuto akci nelze vrátit zpět.",
|
||||
"kill": "Zabít",
|
||||
"sessionKilled": "Relace „{{name}}“ byla ukončena",
|
||||
"sessionKillFailed": "Nepodařilo se ukončit relaci",
|
||||
"hostUnreachable": "Nepodařilo se připojit k hostiteli. Zkontrolujte, zda je online a dostupný.",
|
||||
"noServer": "Na tomto hostiteli neběží žádný tmux server.",
|
||||
"searchTruncated": "Částečné výsledky – vyhledávání pokrývá posledních {{lines}} řádků každého panelu a maximálně {{panes}} panelů.",
|
||||
"closeSearchResults": "Zavřít výsledky vyhledávání",
|
||||
"retry": "Zkusit znovu",
|
||||
"noHosts": "Žádní SSH hostitelé nejsou k dispozici",
|
||||
"noHostsHint": "Povolte možnost Monitor Tmux na hostiteli SSH (Správce hostitelů → záložka Terminál) pro sledování jeho relací tmux.",
|
||||
"tmuxInstallHint": "Nainstalujte jej na hostitele pomocí:",
|
||||
"attachTooltip": "Otevřete terminál pro {{host}}",
|
||||
"attachTooltipPane": "Otevřete terminál pro {{host}} — relace tmux {{session}}",
|
||||
"timeJustNow": "právě teď",
|
||||
"timeMinutes": "{{count}}před m",
|
||||
"timeHours": "{{count}}před hodinami",
|
||||
"timeDays": "Před {{count}}dny"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriál",
|
||||
"portLabel": "Přístav",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Omdøb faneblad",
|
||||
"openFileManager": "Åbn Filhåndtering",
|
||||
"dashboard": "Dashboard",
|
||||
"tmuxMonitor": "Tmux-skærm",
|
||||
"homepage": "Hjemmeside",
|
||||
"quickConnect": "Hurtig forbindelse",
|
||||
"sshTools": "SSH-værktøjer",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Max Keepalive misser",
|
||||
"sameHost": "Denne vært (direkte tunnel)",
|
||||
"bindHost": "Bind vært",
|
||||
"enableTmuxMonitor": "Aktivér Tmux-skærm",
|
||||
"enableTmuxMonitorDesc": "Vis denne vært i Tmux Monitor og tilføj dens tmux-handlinger til sidebjælken",
|
||||
"connectionToolbar": "Forbindelsesværktøjslinje",
|
||||
"showConnectionToolbar": "Vis forbindelsesværktøjslinjen",
|
||||
"showConnectionToolbarDesc": "Vis værktøjslinjen i denne værts SSH-, RDP-, VNC- og Telnet-sessioner. Du kan også skjule den midlertidigt fra selve værktøjslinjen.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Tunnel-URL kopieret",
|
||||
"dockerUrlCopied": "Docker-URL kopieret",
|
||||
"hostMetricsUrlCopied": "URL til værtsmålinger kopieret",
|
||||
"tmuxMonitorUrlCopied": "Tmux Monitor URL kopieret",
|
||||
"rdpUrlCopied": "RDP-URL kopieret",
|
||||
"vncUrlCopied": "VNC-URL kopieret",
|
||||
"telnetUrlCopied": "Telnet-URL kopieret",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Kopiér tunnel-URL",
|
||||
"copyDockerUrlAction": "Kopiér Docker-URL",
|
||||
"copyHostMetricsUrlAction": "Kopiér URL for værtsmålinger",
|
||||
"copyTmuxMonitorUrlAction": "Kopiér Tmux Monitor URL",
|
||||
"copyRdpUrlAction": "Kopiér RDP-URL",
|
||||
"copyVncUrlAction": "Kopiér VNC-URL",
|
||||
"copyTelnetUrlAction": "Kopiér Telnet-URL",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Åbn Proxmox-statistik",
|
||||
"openTmuxMonitor": "Åbn Tmux-skærmen"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux-skærm",
|
||||
"failedToLoadHosts": "Kunne ikke indlæse værter",
|
||||
"failedToLoad": "Kunne ikke indlæse tmux-sessioner",
|
||||
"tmuxUnavailable": "tmux er ikke installeret på denne vært",
|
||||
"noSessions": "Ingen tmux-sessioner på denne vært",
|
||||
"noHostSelected": "Ingen vært valgt",
|
||||
"attached": "Vedhæftet",
|
||||
"detached": "Fritliggende",
|
||||
"attach": "Vedhæft",
|
||||
"editTags": "Rediger tags",
|
||||
"tagsHint": "Kommaseparerede tags (f.eks. YOLO, lab, træning)",
|
||||
"tagsSaved": "Tags gemt",
|
||||
"tagsSaveFailed": "Kunne ikke gemme tags",
|
||||
"searchPlaceholder": "Søg i output på tværs af sessioner...",
|
||||
"searchResults": "{{count}} matcher",
|
||||
"searchFailed": "Søgningen mislykkedes",
|
||||
"selectPaneHint": "Vælg en rude for at få vist dens output",
|
||||
"closePreview": "Luk forhåndsvisning",
|
||||
"newSession": "Ny session",
|
||||
"newSessionHint": "Sessionsnavn (bogstaver, cifre, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "min-session",
|
||||
"create": "Skabe",
|
||||
"sessionCreated": "Sessionen \"{{name}}\" blev oprettet",
|
||||
"sessionCreateFailed": "Kunne ikke oprette sessionen",
|
||||
"splitRight": "Del højre",
|
||||
"splitDown": "Opdel ned",
|
||||
"splitFailed": "Kunne ikke opdele ruden",
|
||||
"newWindow": "Nyt vindue",
|
||||
"windowCreateFailed": "Kunne ikke oprette vindue",
|
||||
"attachSessionTooltip": "Vedhæft til {{session}}",
|
||||
"refresh": "Opfriske",
|
||||
"refreshFailed": "Kunne ikke opdatere sessioner",
|
||||
"collapseAll": "Skjul alle",
|
||||
"expandAll": "Udvid alle",
|
||||
"moreActions": "Flere handlinger",
|
||||
"sessionStats": "Sessionsstatistik",
|
||||
"renameSessionTitle": "Omdøb sessionen \"{{name}}\"",
|
||||
"rename": "Omdøb",
|
||||
"sessionRenamed": "Sessionen er omdøbt til \"{{name}}\"",
|
||||
"sessionRenameFailed": "Kunne ikke omdøbe sessionen",
|
||||
"editTagsTitle": "Rediger tags for \"{{name}}\"",
|
||||
"killPane": "Dræb ruden",
|
||||
"resizeTree": "Træk for at ændre størrelse — dobbeltklik for at nulstille",
|
||||
"reattach": "Genmonter (rettelser på en forvrænget visning)",
|
||||
"killWindow": "Dræb vinduet",
|
||||
"killWindowTitle": "Luk vindue {{index}} af \"{{session}}\"?",
|
||||
"killWindowBody": "Alle ruder og processer i dette vindue vil blive afsluttet. Hvis det sidste vindue lukkes, afsluttes sessionen.",
|
||||
"windowKillFailed": "Kunne ikke lukke vinduet",
|
||||
"statusActivity": "Aktivitet",
|
||||
"statusWindows": "Vinduer",
|
||||
"statusPanes": "ruder",
|
||||
"statusTags": "Tags",
|
||||
"killPaneTitle": "Afbryd rude {{id}}?",
|
||||
"killPaneBody": "Processen, der kører i denne rude, vil blive afsluttet. Hvis du afslutter den sidste rude, lukkes dens vindue.",
|
||||
"paneKillFailed": "Kunne ikke afslutte ruden",
|
||||
"killSessionTitle": "Afslut sessionen \"{{name}}\"?",
|
||||
"killSessionBody": "Alle vinduer og kørende processer i denne session vil blive afsluttet. Dette kan ikke fortrydes.",
|
||||
"kill": "Dræbe",
|
||||
"sessionKilled": "Sessionen \"{{name}}\" blev afbrudt",
|
||||
"sessionKillFailed": "Kunne ikke afslutte sessionen",
|
||||
"hostUnreachable": "Kunne ikke oprette forbindelse til værten. Kontroller, at den er online og tilgængelig.",
|
||||
"noServer": "Der kører ingen tmux-server på denne vært.",
|
||||
"searchTruncated": "Delvise resultater — søgningen dækker de sidste {{lines}} linjer i hver rude og højst {{panes}} ruder.",
|
||||
"closeSearchResults": "Luk søgeresultater",
|
||||
"retry": "Prøv igen",
|
||||
"noHosts": "Ingen SSH-værter tilgængelige",
|
||||
"noHostsHint": "Aktivér Tmux Monitor-indstillingen på en SSH-vært (Værtsadministrator → Terminal-fanen) for at overvåge dens tmux-sessioner.",
|
||||
"tmuxInstallHint": "Installer det på værten med:",
|
||||
"attachTooltip": "Åbn en terminal til {{host}}",
|
||||
"attachTooltipPane": "Åbn en terminal til {{host}} — tmux-session {{session}}",
|
||||
"timeJustNow": "lige nu",
|
||||
"timeMinutes": "For {{count}}minutter siden",
|
||||
"timeHours": "For {{count}}timer siden",
|
||||
"timeDays": "for {{count}}dage siden"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriel",
|
||||
"portLabel": "Havn",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Registerkarte umbenennen",
|
||||
"openFileManager": "Dateimanager öffnen",
|
||||
"dashboard": "Dashboard",
|
||||
"tmuxMonitor": "Tmux-Monitor",
|
||||
"homepage": "Startseite",
|
||||
"quickConnect": "Schnellverbindung",
|
||||
"sshTools": "SSH-Tools",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Max. Keepalive-Fehlschläge",
|
||||
"sameHost": "Dieser Host (direkter Tunnel)",
|
||||
"bindHost": "Bind-Host",
|
||||
"enableTmuxMonitor": "Tmux-Monitor aktivieren",
|
||||
"enableTmuxMonitorDesc": "Diesen Host im Tmux-Monitor anzeigen und seine tmux-Aktionen zur Seitenleiste hinzufügen",
|
||||
"connectionToolbar": "Verbindungssymbolleiste",
|
||||
"showConnectionToolbar": "Verbindungssymbolleiste anzeigen",
|
||||
"showConnectionToolbarDesc": "Die Symbolleiste wird in den SSH-, RDP-, VNC- und Telnet-Sitzungen dieses Hosts angezeigt. Sie kann auch vorübergehend in der Symbolleiste selbst ausgeblendet werden.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Tunnel-URL kopiert",
|
||||
"dockerUrlCopied": "Docker-URL kopiert",
|
||||
"hostMetricsUrlCopied": "Host-Metriken-URL kopiert",
|
||||
"tmuxMonitorUrlCopied": "Tmux-Monitor-URL kopiert",
|
||||
"rdpUrlCopied": "RDP-URL kopiert",
|
||||
"vncUrlCopied": "VNC-URL kopiert",
|
||||
"telnetUrlCopied": "Telnet-URL kopiert",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Tunnel-URL kopieren",
|
||||
"copyDockerUrlAction": "Docker-URL kopieren",
|
||||
"copyHostMetricsUrlAction": "Host-Metriken-URL kopieren",
|
||||
"copyTmuxMonitorUrlAction": "Tmux-Monitor-URL kopieren",
|
||||
"copyRdpUrlAction": "RDP-URL kopieren",
|
||||
"copyVncUrlAction": "VNC-URL kopieren",
|
||||
"copyTelnetUrlAction": "Telnet-URL kopieren",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Proxmox-Statistiken öffnen",
|
||||
"openTmuxMonitor": "Tmux-Monitor öffnen"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux-Monitor",
|
||||
"failedToLoadHosts": "Laden der Hosts fehlgeschlagen",
|
||||
"failedToLoad": "Laden der tmux-Sitzungen fehlgeschlagen",
|
||||
"tmuxUnavailable": "tmux ist auf diesem Host nicht installiert",
|
||||
"noSessions": "Keine tmux-Sitzungen auf diesem Host",
|
||||
"noHostSelected": "Kein Host ausgewählt",
|
||||
"attached": "Verbunden",
|
||||
"detached": "Getrennt",
|
||||
"attach": "Verbinden",
|
||||
"editTags": "Tags bearbeiten",
|
||||
"tagsHint": "Durch Kommas getrennte Tags (z. B. YOLO, lab, training)",
|
||||
"tagsSaved": "Tags gespeichert",
|
||||
"tagsSaveFailed": "Fehler beim Speichern der Tags",
|
||||
"searchPlaceholder": "Ausgabe in allen Sitzungen durchsuchen...",
|
||||
"searchResults": "{{count}} Treffer",
|
||||
"searchFailed": "Suche fehlgeschlagen",
|
||||
"selectPaneHint": "Wählen Sie einen Bereich aus, um dessen Ausgabe anzuzeigen",
|
||||
"closePreview": "Vorschau schließen",
|
||||
"newSession": "Neue Sitzung",
|
||||
"newSessionHint": "Sitzungsname (Buchstaben, Ziffern, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "Erstellen",
|
||||
"sessionCreated": "Sitzung \"{{name}}\" erstellt",
|
||||
"sessionCreateFailed": "Fehler beim Erstellen der Sitzung",
|
||||
"splitRight": "Rechts teilen",
|
||||
"splitDown": "Unten teilen",
|
||||
"splitFailed": "Fehler beim Teilen des Bereichs",
|
||||
"newWindow": "Neues Fenster",
|
||||
"windowCreateFailed": "Fehler beim Erstellen des Fensters",
|
||||
"attachSessionTooltip": "Mit {{session}} verbinden",
|
||||
"refresh": "Aktualisieren",
|
||||
"refreshFailed": "Fehler beim Aktualisieren der Sitzungen",
|
||||
"collapseAll": "Alle einklappen",
|
||||
"expandAll": "Alle ausklappen",
|
||||
"moreActions": "Weitere Aktionen",
|
||||
"sessionStats": "Sitzungsstatistiken",
|
||||
"renameSessionTitle": "Sitzung \"{{name}}\" umbenennen",
|
||||
"rename": "Umbenennen",
|
||||
"sessionRenamed": "Sitzung umbenannt in \"{{name}}\"",
|
||||
"sessionRenameFailed": "Fehler beim Umbenennen der Sitzung",
|
||||
"editTagsTitle": "Tags für \"{{name}}\" bearbeiten",
|
||||
"killPane": "Pane beenden",
|
||||
"resizeTree": "Ziehen zum Ändern der Größe — Doppelklick zum Zurücksetzen",
|
||||
"reattach": "Erneut anhängen (behebt eine verzerrte Darstellung)",
|
||||
"killWindow": "Fenster beenden",
|
||||
"killWindowTitle": "Fenster {{index}} von \"{{session}}\" beenden?",
|
||||
"killWindowBody": "Alle Panes und Prozesse in diesem Fenster werden beendet. Wird das letzte Fenster beendet, wird die Sitzung beendet.",
|
||||
"windowKillFailed": "Fenster konnte nicht beendet werden",
|
||||
"statusActivity": "Aktivität",
|
||||
"statusWindows": "Fenster",
|
||||
"statusPanes": "Panes",
|
||||
"statusTags": "Tags",
|
||||
"killPaneTitle": "Pane {{id}} beenden?",
|
||||
"killPaneBody": "Der in dieser Pane laufende Prozess wird beendet. Wird die letzte Pane beendet, wird das Fenster geschlossen.",
|
||||
"paneKillFailed": "Pane konnte nicht beendet werden",
|
||||
"killSessionTitle": "Sitzung \"{{name}}\" beenden?",
|
||||
"killSessionBody": "Alle Fenster und laufenden Prozesse in dieser Sitzung werden beendet. Dies kann nicht rückgängig gemacht werden.",
|
||||
"kill": "Beenden",
|
||||
"sessionKilled": "Sitzung \"{{name}}\" beendet",
|
||||
"sessionKillFailed": "Sitzung konnte nicht beendet werden",
|
||||
"hostUnreachable": "Verbindung zum Host konnte nicht hergestellt werden. Überprüfen Sie, ob er online und erreichbar ist.",
|
||||
"noServer": "Auf diesem Host läuft kein tmux-Server.",
|
||||
"searchTruncated": "Teilergebnisse — die Suche umfasst die letzten {{lines}} Zeilen jeder Pane und maximal {{panes}} Panes.",
|
||||
"closeSearchResults": "Suchergebnisse schließen",
|
||||
"retry": "Erneut versuchen",
|
||||
"noHosts": "Keine SSH-Hosts verfügbar",
|
||||
"noHostsHint": "Aktivieren Sie die Option Tmux Monitor für einen SSH-Host (Host-Manager → Terminal-Tab), um dessen tmux-Sitzungen zu überwachen.",
|
||||
"tmuxInstallHint": "Installieren Sie es auf dem Host mit:",
|
||||
"attachTooltip": "Terminal zu {{host}} öffnen",
|
||||
"attachTooltipPane": "Terminal zu {{host}} öffnen — tmux-Sitzung {{session}}",
|
||||
"timeJustNow": "gerade eben",
|
||||
"timeMinutes": "vor {{count}} Min.",
|
||||
"timeHours": "vor {{count}} Std.",
|
||||
"timeDays": "vor {{count}} T."
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriell",
|
||||
"portLabel": "Port",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Μετονομασία καρτέλας",
|
||||
"openFileManager": "Άνοιγμα Διαχείρισης Αρχείων",
|
||||
"dashboard": "Ταμπλό",
|
||||
"tmuxMonitor": "Παρακολούθηση Tmux",
|
||||
"homepage": "Αρχική σελίδα",
|
||||
"quickConnect": "Γρήγορη σύνδεση",
|
||||
"sshTools": "Εργαλεία SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Ο Max Keepalive αστοχεί",
|
||||
"sameHost": "Αυτός ο κεντρικός υπολογιστής (άμεση σήραγγα)",
|
||||
"bindHost": "Bind Host",
|
||||
"enableTmuxMonitor": "Ενεργοποίηση παρακολούθησης Tmux",
|
||||
"enableTmuxMonitorDesc": "Εμφάνιση αυτού του κεντρικού υπολογιστή στην οθόνη Tmux και προσθήκη των ενεργειών tmux του στην πλαϊνή μπάρα",
|
||||
"connectionToolbar": "Γραμμή εργαλείων σύνδεσης",
|
||||
"showConnectionToolbar": "Εμφάνιση γραμμής εργαλείων σύνδεσης",
|
||||
"showConnectionToolbarDesc": "Εμφάνιση της γραμμής εργαλείων στις συνεδρίες SSH, RDP, VNC και Telnet αυτού του κεντρικού υπολογιστή. Μπορείτε επίσης να την αποκρύψετε προσωρινά από την ίδια τη γραμμή εργαλείων.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Η διεύθυνση URL της σήραγγας αντιγράφηκε",
|
||||
"dockerUrlCopied": "Η διεύθυνση URL του Docker αντιγράφηκε",
|
||||
"hostMetricsUrlCopied": "Η διεύθυνση URL μετρήσεων κεντρικού υπολογιστή αντιγράφηκε",
|
||||
"tmuxMonitorUrlCopied": "Η διεύθυνση URL του Tmux Monitor αντιγράφηκε",
|
||||
"rdpUrlCopied": "Η διεύθυνση URL RDP αντιγράφηκε",
|
||||
"vncUrlCopied": "Η διεύθυνση URL VNC αντιγράφηκε",
|
||||
"telnetUrlCopied": "Η διεύθυνση URL Telnet αντιγράφηκε",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Αντιγραφή URL σήραγγας",
|
||||
"copyDockerUrlAction": "Αντιγραφή διεύθυνσης URL του Docker",
|
||||
"copyHostMetricsUrlAction": "Αντιγραφή URL μετρήσεων κεντρικού υπολογιστή",
|
||||
"copyTmuxMonitorUrlAction": "Αντιγραφή URL παρακολούθησης Tmux",
|
||||
"copyRdpUrlAction": "Αντιγραφή διεύθυνσης URL RDP",
|
||||
"copyVncUrlAction": "Αντιγραφή URL VNC",
|
||||
"copyTelnetUrlAction": "Αντιγραφή διεύθυνσης URL Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Άνοιγμα στατιστικών Proxmox",
|
||||
"openTmuxMonitor": "Άνοιγμα παρακολούθησης Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Παρακολούθηση Tmux",
|
||||
"failedToLoadHosts": "Αποτυχία φόρτωσης κεντρικών υπολογιστών",
|
||||
"failedToLoad": "Αποτυχία φόρτωσης συνεδριών tmux",
|
||||
"tmuxUnavailable": "Το tmux δεν είναι εγκατεστημένο σε αυτόν τον κεντρικό υπολογιστή",
|
||||
"noSessions": "Δεν υπάρχουν συνεδρίες tmux σε αυτόν τον κεντρικό υπολογιστή",
|
||||
"noHostSelected": "Δεν έχει επιλεγεί κεντρικός υπολογιστής",
|
||||
"attached": "Συνημμένο",
|
||||
"detached": "Απομονωμένος",
|
||||
"attach": "Συνδέω",
|
||||
"editTags": "Επεξεργασία ετικετών",
|
||||
"tagsHint": "Ετικέτες διαχωρισμένες με κόμμα (π.χ. YOLO, εργαστήριο, εκπαίδευση)",
|
||||
"tagsSaved": "Οι ετικέτες αποθηκεύτηκαν",
|
||||
"tagsSaveFailed": "Η αποθήκευση των ετικετών απέτυχε",
|
||||
"searchPlaceholder": "Αναζήτηση εξόδου σε όλες τις συνεδρίες...",
|
||||
"searchResults": "{{count}} αγώνες",
|
||||
"searchFailed": "Η αναζήτηση απέτυχε",
|
||||
"selectPaneHint": "Επιλέξτε ένα παράθυρο για να κάνετε προεπισκόπηση της εξόδου του",
|
||||
"closePreview": "Κλείσιμο προεπισκόπησης",
|
||||
"newSession": "Νέα συνεδρία",
|
||||
"newSessionHint": "Όνομα συνεδρίας (γράμματα, ψηφία, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "η-συνεδρία μου",
|
||||
"create": "Δημιουργώ",
|
||||
"sessionCreated": "Συνεδρία \"{{name}}\" δημιουργήθηκε",
|
||||
"sessionCreateFailed": "Αποτυχία δημιουργίας συνεδρίας",
|
||||
"splitRight": "Διαίρεση δεξιά",
|
||||
"splitDown": "Διαχωρισμός",
|
||||
"splitFailed": "Αποτυχία διαίρεσης παραθύρου",
|
||||
"newWindow": "Νέο παράθυρο",
|
||||
"windowCreateFailed": "Η δημιουργία παραθύρου απέτυχε",
|
||||
"attachSessionTooltip": "Επισύναψη σε {{session}}",
|
||||
"refresh": "Φρεσκάρω",
|
||||
"refreshFailed": "Η ανανέωση των περιόδων σύνδεσης απέτυχε",
|
||||
"collapseAll": "Σύμπτυξη όλων",
|
||||
"expandAll": "Ανάπτυξη όλων",
|
||||
"moreActions": "Περισσότερες ενέργειες",
|
||||
"sessionStats": "Στατιστικά περιόδου σύνδεσης",
|
||||
"renameSessionTitle": "Μετονομασία συνεδρίας \"{{name}}\"",
|
||||
"rename": "Μετονομάζω",
|
||||
"sessionRenamed": "Η συνεδρία μετονομάστηκε σε \"{{name}}\"",
|
||||
"sessionRenameFailed": "Η μετονομασία της συνεδρίας απέτυχε",
|
||||
"editTagsTitle": "Επεξεργασία ετικετών για \"{{name}}\"",
|
||||
"killPane": "Κλείσιμο παραθύρου",
|
||||
"resizeTree": "Σύρετε για αλλαγή μεγέθους — κάντε διπλό κλικ για επαναφορά",
|
||||
"reattach": "Επανασύνδεση (διορθώνει μια παραμορφωμένη προβολή)",
|
||||
"killWindow": "Σκότωμα παραθύρου",
|
||||
"killWindowTitle": "Να τερματιστεί το παράθυρο {{index}} του \"{{session}}\";",
|
||||
"killWindowBody": "Κάθε παράθυρο και διεργασία σε αυτό το παράθυρο θα τερματιστεί. Η διακοπή του τελευταίου παραθύρου τερματίζει την περίοδο λειτουργίας.",
|
||||
"windowKillFailed": "Αποτυχία τερματισμού του παραθύρου",
|
||||
"statusActivity": "Δραστηριότητα",
|
||||
"statusWindows": "Παράθυρα",
|
||||
"statusPanes": "τζάμια",
|
||||
"statusTags": "Ετικέτες",
|
||||
"killPaneTitle": "Κλείσιμο παραθύρου {{id}}?",
|
||||
"killPaneBody": "Η διεργασία που εκτελείται σε αυτό το παράθυρο θα τερματιστεί. Ο τερματισμός του τελευταίου παραθύρου κλείνει το παράθυρό του.",
|
||||
"paneKillFailed": "Αποτυχία τερματισμού του παραθύρου",
|
||||
"killSessionTitle": "Διακοπή συνεδρίας \"{{name}}\";",
|
||||
"killSessionBody": "Όλα τα παράθυρα και οι διεργασίες που εκτελούνται σε αυτήν την περίοδο λειτουργίας θα τερματιστούν. Αυτό δεν μπορεί να αναιρεθεί.",
|
||||
"kill": "Σκοτώνω",
|
||||
"sessionKilled": "Η συνεδρία \"{{name}}\" τερματίστηκε",
|
||||
"sessionKillFailed": "Αποτυχία τερματισμού της συνεδρίας",
|
||||
"hostUnreachable": "Δεν ήταν δυνατή η σύνδεση με τον κεντρικό υπολογιστή. Ελέγξτε ότι είναι συνδεδεμένος στο διαδίκτυο και προσβάσιμος.",
|
||||
"noServer": "Δεν εκτελείται διακομιστής tmux σε αυτόν τον κεντρικό υπολογιστή.",
|
||||
"searchTruncated": "Μερικά αποτελέσματα — η αναζήτηση καλύπτει τις τελευταίες {{lines}} γραμμές κάθε παραθύρου και το πολύ {{panes}} παραθύρους.",
|
||||
"closeSearchResults": "Κλείσιμο αποτελεσμάτων αναζήτησης",
|
||||
"retry": "Δοκιμάζω πάλι",
|
||||
"noHosts": "Δεν υπάρχουν διαθέσιμοι κεντρικοί υπολογιστές SSH",
|
||||
"noHostsHint": "Ενεργοποιήστε την επιλογή Tmux Monitor σε έναν κεντρικό υπολογιστή SSH (Καρτέλα Host Manager → Terminal) για να παρακολουθείτε τις συνεδρίες tmux του.",
|
||||
"tmuxInstallHint": "Εγκαταστήστε το στον κεντρικό υπολογιστή με:",
|
||||
"attachTooltip": "Ανοίξτε ένα τερματικό για να {{host}}",
|
||||
"attachTooltipPane": "Ανοίξτε ένα τερματικό για να {{host}} — συνεδρία tmux {{session}}",
|
||||
"timeJustNow": "μόλις τώρα",
|
||||
"timeMinutes": "{{count}}πριν από λεπτά",
|
||||
"timeHours": "{{count}}πριν από ώρες",
|
||||
"timeDays": "{{count}}πριν από ημέρες"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Σειράς",
|
||||
"portLabel": "Λιμάνι",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Renombrar pestaña",
|
||||
"openFileManager": "Abrir gestor de archivos",
|
||||
"dashboard": "Panel de control",
|
||||
"tmuxMonitor": "Monitor de Tmux",
|
||||
"homepage": "Inicio",
|
||||
"quickConnect": "Conexión rápida",
|
||||
"sshTools": "Herramientas SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Máximo de fallos de keepalive",
|
||||
"sameHost": "Este host (túnel directo)",
|
||||
"bindHost": "Host de enlace",
|
||||
"enableTmuxMonitor": "Activar Tmux Monitor",
|
||||
"enableTmuxMonitorDesc": "Mostrar este host en el Tmux Monitor y añadir sus acciones de tmux a la barra lateral.",
|
||||
"connectionToolbar": "Barra de herramientas de conexión",
|
||||
"showConnectionToolbar": "Mostrar barra de herramientas de conexión",
|
||||
"showConnectionToolbarDesc": "Muestra la barra de herramientas en las sesiones SSH, RDP, VNC y Telnet de este host. También puedes ocultarla temporalmente desde la propia barra de herramientas.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "URL del túnel copiada",
|
||||
"dockerUrlCopied": "URL de Docker copiada",
|
||||
"hostMetricsUrlCopied": "URL de métricas del host copiada",
|
||||
"tmuxMonitorUrlCopied": "URL de Tmux Monitor copiada",
|
||||
"rdpUrlCopied": "URL de RDP copiada",
|
||||
"vncUrlCopied": "URL de VNC copiada",
|
||||
"telnetUrlCopied": "URL de Telnet copiada",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Copiar URL del túnel",
|
||||
"copyDockerUrlAction": "Copiar URL de Docker",
|
||||
"copyHostMetricsUrlAction": "Copiar URL de métricas del host",
|
||||
"copyTmuxMonitorUrlAction": "Copiar URL de Tmux Monitor",
|
||||
"copyRdpUrlAction": "Copiar URL de RDP",
|
||||
"copyVncUrlAction": "Copiar URL de VNC",
|
||||
"copyTelnetUrlAction": "Copiar URL de Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Abrir estadísticas de Proxmox",
|
||||
"openTmuxMonitor": "Abrir el monitor de Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Monitor Tmux",
|
||||
"failedToLoadHosts": "Error al cargar los hosts",
|
||||
"failedToLoad": "Error al cargar las sesiones de tmux",
|
||||
"tmuxUnavailable": "tmux no está instalado en este host",
|
||||
"noSessions": "No hay sesiones de tmux en este host",
|
||||
"noHostSelected": "Ningún host seleccionado",
|
||||
"attached": "Conectado",
|
||||
"detached": "Desconectado",
|
||||
"attach": "Conectar",
|
||||
"editTags": "Editar etiquetas",
|
||||
"tagsHint": "Etiquetas separadas por comas (ej. YOLO, lab, training)",
|
||||
"tagsSaved": "Etiquetas guardadas",
|
||||
"tagsSaveFailed": "Error al guardar etiquetas",
|
||||
"searchPlaceholder": "Buscar en la salida de todas las sesiones...",
|
||||
"searchResults": "{{count}} coincidencias",
|
||||
"searchFailed": "Búsqueda fallida",
|
||||
"selectPaneHint": "Seleccione un panel para previsualizar su salida",
|
||||
"closePreview": "Cerrar previsualización",
|
||||
"newSession": "Nueva sesión",
|
||||
"newSessionHint": "Nombre de la sesión (letras, dígitos, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "Crear",
|
||||
"sessionCreated": "Sesión \"{{name}}\" creada",
|
||||
"sessionCreateFailed": "Error al crear la sesión",
|
||||
"splitRight": "Dividir a la derecha",
|
||||
"splitDown": "Dividir abajo",
|
||||
"splitFailed": "Error al dividir el panel",
|
||||
"newWindow": "Nueva ventana",
|
||||
"windowCreateFailed": "Error al crear la ventana",
|
||||
"attachSessionTooltip": "Conectar a {{session}}",
|
||||
"refresh": "Actualizar",
|
||||
"refreshFailed": "Error al actualizar las sesiones",
|
||||
"collapseAll": "Colapsar todo",
|
||||
"expandAll": "Expandir todo",
|
||||
"moreActions": "Más acciones",
|
||||
"sessionStats": "Estadísticas de la sesión",
|
||||
"renameSessionTitle": "Renombrar sesión \"{{name}}\"",
|
||||
"rename": "Renombrar",
|
||||
"sessionRenamed": "Sesión renombrada a \"{{name}}\"",
|
||||
"sessionRenameFailed": "Error al renombrar la sesión",
|
||||
"editTagsTitle": "Editar etiquetas para \"{{name}}\"",
|
||||
"killPane": "Finalizar panel",
|
||||
"resizeTree": "Arrastrar para redimensionar — doble clic para restablecer",
|
||||
"reattach": "Reconectar (soluciona una vista distorsionada)",
|
||||
"killWindow": "Finalizar ventana",
|
||||
"killWindowTitle": "¿Finalizar ventana {{index}} de \"{{session}}\"?",
|
||||
"killWindowBody": "Todos los paneles y procesos de esta ventana serán terminados. Finalizar la última ventana finaliza la sesión.",
|
||||
"windowKillFailed": "Error al finalizar la ventana",
|
||||
"statusActivity": "Actividad",
|
||||
"statusWindows": "Ventanas",
|
||||
"statusPanes": "paneles",
|
||||
"statusTags": "Etiquetas",
|
||||
"killPaneTitle": "¿Finalizar panel {{id}}?",
|
||||
"killPaneBody": "El proceso en ejecución en este panel será terminado. Finalizar el último panel cierra su ventana.",
|
||||
"paneKillFailed": "Error al finalizar el panel",
|
||||
"killSessionTitle": "¿Finalizar sesión \"{{name}}\"?",
|
||||
"killSessionBody": "Todas las ventanas y procesos en ejecución de esta sesión serán terminados. Esta acción no se puede deshacer.",
|
||||
"kill": "Finalizar",
|
||||
"sessionKilled": "Sesión \"{{name}}\" finalizada",
|
||||
"sessionKillFailed": "Error al finalizar la sesión",
|
||||
"hostUnreachable": "No se pudo conectar al host. Compruebe que esté en línea y accesible.",
|
||||
"noServer": "No hay ningún servidor tmux en ejecución en este host.",
|
||||
"searchTruncated": "Resultados parciales — la búsqueda cubre las últimas {{lines}} líneas de cada panel y un máximo de {{panes}} paneles.",
|
||||
"closeSearchResults": "Cerrar resultados de búsqueda",
|
||||
"retry": "Reintentar",
|
||||
"noHosts": "No hay hosts SSH disponibles",
|
||||
"noHostsHint": "Habilite la opción Tmux Monitor en un host SSH (Gestor de hosts → pestaña Terminal) para monitorizar sus sesiones tmux.",
|
||||
"tmuxInstallHint": "Instálelo en el host con:",
|
||||
"attachTooltip": "Abrir un terminal en {{host}}",
|
||||
"attachTooltipPane": "Abrir un terminal en {{host}} — sesión tmux {{session}}",
|
||||
"timeJustNow": "ahora mismo",
|
||||
"timeMinutes": "hace {{count}}m",
|
||||
"timeHours": "hace {{count}}h",
|
||||
"timeDays": "hace {{count}}d"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Puerto",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Nimeä välilehti uudelleen",
|
||||
"openFileManager": "Avaa tiedostonhallinta",
|
||||
"dashboard": "Kojelauta",
|
||||
"tmuxMonitor": "Tmux-näyttö",
|
||||
"homepage": "Etusivu",
|
||||
"quickConnect": "Pikayhteys",
|
||||
"sshTools": "SSH-työkalut",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Max Keepalive -heitot",
|
||||
"sameHost": "Tämä isäntä (suora tunneli)",
|
||||
"bindHost": "Sido isäntä",
|
||||
"enableTmuxMonitor": "Ota Tmux-monitori käyttöön",
|
||||
"enableTmuxMonitorDesc": "Näytä tämä isäntä Tmux-monitorissa ja lisää sen tmux-toiminnot sivupalkkiin",
|
||||
"connectionToolbar": "Yhteystyökalurivi",
|
||||
"showConnectionToolbar": "Näytä yhteystyökalurivi",
|
||||
"showConnectionToolbarDesc": "Näytä työkalupalkki tämän isännän SSH-, RDP-, VNC- ja Telnet-istunnoissa. Voit myös piilottaa sen tilapäisesti itse työkalupalkista.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Tunnelin URL-osoite kopioitu",
|
||||
"dockerUrlCopied": "Dockerin URL-osoite kopioitu",
|
||||
"hostMetricsUrlCopied": "Isäntämittarien URL-osoite kopioitu",
|
||||
"tmuxMonitorUrlCopied": "Tmux-monitorin URL-osoite kopioitu",
|
||||
"rdpUrlCopied": "RDP-URL-osoite kopioitu",
|
||||
"vncUrlCopied": "VNC-URL-osoite kopioitu",
|
||||
"telnetUrlCopied": "Telnet-URL-osoite kopioitu",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Kopioi tunnelin URL-osoite",
|
||||
"copyDockerUrlAction": "Kopioi Dockerin URL-osoite",
|
||||
"copyHostMetricsUrlAction": "Kopioi isäntätietojen URL-osoite",
|
||||
"copyTmuxMonitorUrlAction": "Kopioi Tmux-monitorin URL-osoite",
|
||||
"copyRdpUrlAction": "Kopioi RDP-URL-osoite",
|
||||
"copyVncUrlAction": "Kopioi VNC-URL-osoite",
|
||||
"copyTelnetUrlAction": "Kopioi Telnet-URL-osoite",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Avaa Proxmox-tilastot",
|
||||
"openTmuxMonitor": "Avaa Tmux-näyttö"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux-näyttö",
|
||||
"failedToLoadHosts": "Isäntien lataaminen epäonnistui",
|
||||
"failedToLoad": "tmux-istuntojen lataaminen epäonnistui",
|
||||
"tmuxUnavailable": "tmuxia ei ole asennettu tälle koneelle",
|
||||
"noSessions": "Ei tmux-istuntoja tällä isännöinnillä",
|
||||
"noHostSelected": "Ei valittua isäntää",
|
||||
"attached": "Liitteenä",
|
||||
"detached": "Erillinen",
|
||||
"attach": "Liittää",
|
||||
"editTags": "Muokkaa tunnisteita",
|
||||
"tagsHint": "Pilkuilla erotetut tunnisteet (esim. YOLO, lab, training)",
|
||||
"tagsSaved": "Tagit tallennettu",
|
||||
"tagsSaveFailed": "Tunnisteiden tallentaminen epäonnistui",
|
||||
"searchPlaceholder": "Hae tulostetta istuntojen välillä...",
|
||||
"searchResults": "{{count}} osumaa",
|
||||
"searchFailed": "Haku epäonnistui",
|
||||
"selectPaneHint": "Valitse ruutu esikatsellaksesi sen tulosta",
|
||||
"closePreview": "Sulje esikatselu",
|
||||
"newSession": "Uusi istunto",
|
||||
"newSessionHint": "Istunnon nimi (kirjaimet, numerot, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "Luoda",
|
||||
"sessionCreated": "Istunto \"{{name}}\" luotu",
|
||||
"sessionCreateFailed": "Istunnon luominen epäonnistui",
|
||||
"splitRight": "Jaa oikealle",
|
||||
"splitDown": "Jaa alas",
|
||||
"splitFailed": "Ruudun jakaminen epäonnistui",
|
||||
"newWindow": "Uusi ikkuna",
|
||||
"windowCreateFailed": "Ikkunan luominen epäonnistui",
|
||||
"attachSessionTooltip": "Liitä kohtaan {{session}}",
|
||||
"refresh": "Päivitä",
|
||||
"refreshFailed": "Istuntojen päivittäminen epäonnistui",
|
||||
"collapseAll": "Pienennä kaikki",
|
||||
"expandAll": "Laajenna kaikki",
|
||||
"moreActions": "Lisää toimintoja",
|
||||
"sessionStats": "Istuntotilastot",
|
||||
"renameSessionTitle": "Nimeä istunto uudelleen \"{{name}}\"",
|
||||
"rename": "Nimeä uudelleen",
|
||||
"sessionRenamed": "Istunto nimettiin uudelleen muotoon \"{{name}}\"",
|
||||
"sessionRenameFailed": "Istunnon uudelleennimeäminen epäonnistui",
|
||||
"editTagsTitle": "Muokkaa tunnisteita kohteelle \"{{name}}\"",
|
||||
"killPane": "Tappaa ruutu",
|
||||
"resizeTree": "Muuta kokoa vetämällä – kaksoisnapsauttamalla voit palauttaa sen",
|
||||
"reattach": "Kiinnitä uudelleen (korjaa vääristyneen näkymän)",
|
||||
"killWindow": "Tapa ikkuna",
|
||||
"killWindowTitle": "Tappaa ikkuna {{index}} kohteelle \"{{session}}\"?",
|
||||
"killWindowBody": "Jokainen tässä ikkunassa oleva ruutu ja prosessi lopetetaan. Viimeisen ikkunan sulkeminen lopettaa istunnon.",
|
||||
"windowKillFailed": "Ikkunan sulkeminen epäonnistui",
|
||||
"statusActivity": "Toiminta",
|
||||
"statusWindows": "Ikkunat",
|
||||
"statusPanes": "ruudut",
|
||||
"statusTags": "Tunnisteet",
|
||||
"killPaneTitle": "Tappaa ruutu {{id}}?",
|
||||
"killPaneBody": "Tässä ruudussa käynnissä oleva prosessi lopetetaan. Viimeisen ruudun sulkeminen sulkee sen ikkunan.",
|
||||
"paneKillFailed": "Ruudun sulkeminen epäonnistui",
|
||||
"killSessionTitle": "Lopeta istunto \"{{name}}\"?",
|
||||
"killSessionBody": "Kaikki tämän istunnon ikkunat ja käynnissä olevat prosessit lopetetaan. Tätä ei voi perua.",
|
||||
"kill": "Tappaa",
|
||||
"sessionKilled": "Istunto \"{{name}}\" lopetettu",
|
||||
"sessionKillFailed": "Istunnon lopettaminen epäonnistui",
|
||||
"hostUnreachable": "Yhteyden muodostaminen isäntään ei onnistu. Tarkista, että se on verkossa ja tavoitettavissa.",
|
||||
"noServer": "Tällä isännöinnillä ei ole käynnissä tmux-palvelinta.",
|
||||
"searchTruncated": "Osittaisia tuloksia — haku kattaa kunkin ruudun viimeiset {{lines}} rivit ja enintään {{panes}} ruutua.",
|
||||
"closeSearchResults": "Sulje hakutulokset",
|
||||
"retry": "Yritä uudelleen",
|
||||
"noHosts": "Ei SSH-isäntiä saatavilla",
|
||||
"noHostsHint": "Ota Tmux Monitor -asetus käyttöön SSH-isännässä (Isäntähallinta → Pääte-välilehti) sen tmux-istuntojen valvomiseksi.",
|
||||
"tmuxInstallHint": "Asenna se isäntäkoneelle komennolla:",
|
||||
"attachTooltip": "Avaa pääte ja kohdista {{host}}",
|
||||
"attachTooltipPane": "Avaa pääteohjelma ja kohdista {{host}} — tmux-istunto {{session}}",
|
||||
"timeJustNow": "juuri nyt",
|
||||
"timeMinutes": "{{count}}minuuttia sitten",
|
||||
"timeHours": "{{count}}tuntia sitten",
|
||||
"timeDays": "{{count}}d sitten"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Sarjanumero",
|
||||
"portLabel": "Portti",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Renommer l'onglet",
|
||||
"openFileManager": "Ouvrir le gestionnaire de fichiers",
|
||||
"dashboard": "Tableau de bord",
|
||||
"tmuxMonitor": "Moniteur Tmux",
|
||||
"homepage": "Accueil",
|
||||
"quickConnect": "Connexion rapide",
|
||||
"sshTools": "Outils SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Nombre maximal d'échecs de keepalive",
|
||||
"sameHost": "Cet hôte (tunnel direct)",
|
||||
"bindHost": "Hôte de liaison",
|
||||
"enableTmuxMonitor": "Activer le Moniteur Tmux",
|
||||
"enableTmuxMonitorDesc": "Afficher cet hôte dans le Moniteur Tmux et ajouter ses actions tmux à la barre latérale",
|
||||
"connectionToolbar": "Barre d'outils de connexion",
|
||||
"showConnectionToolbar": "Afficher la barre d'outils de connexion",
|
||||
"showConnectionToolbarDesc": "Afficher la barre d'outils dans les sessions SSH, RDP, VNC et Telnet de cet hôte. Vous pouvez également la masquer temporairement depuis la barre d'outils elle-même.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "URL du tunnel copiée",
|
||||
"dockerUrlCopied": "URL Docker copiée",
|
||||
"hostMetricsUrlCopied": "URL des métriques de l'hôte copiée",
|
||||
"tmuxMonitorUrlCopied": "URL du moniteur Tmux copiée",
|
||||
"rdpUrlCopied": "URL RDP copiée",
|
||||
"vncUrlCopied": "URL VNC copiée",
|
||||
"telnetUrlCopied": "URL Telnet copiée",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Copier l'URL du tunnel",
|
||||
"copyDockerUrlAction": "Copier l'URL Docker",
|
||||
"copyHostMetricsUrlAction": "Copier l'URL des métriques de l'hôte",
|
||||
"copyTmuxMonitorUrlAction": "Copier l'URL du moniteur Tmux",
|
||||
"copyRdpUrlAction": "Copier l'URL RDP",
|
||||
"copyVncUrlAction": "Copier l'URL VNC",
|
||||
"copyTelnetUrlAction": "Copier l'URL Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Ouvrir les statistiques de Proxmox",
|
||||
"openTmuxMonitor": "Moniteur Open Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Moniteur tmux",
|
||||
"failedToLoadHosts": "Échec du chargement des hôtes",
|
||||
"failedToLoad": "Échec du chargement des sessions tmux",
|
||||
"tmuxUnavailable": "tmux n'est pas installé sur cet hôte",
|
||||
"noSessions": "Aucune session tmux sur cet hôte",
|
||||
"noHostSelected": "Aucun hôte sélectionné",
|
||||
"attached": "Attaché",
|
||||
"detached": "Détaché",
|
||||
"attach": "Attacher",
|
||||
"editTags": "Modifier les tags",
|
||||
"tagsHint": "Tags séparés par des virgules (ex. : YOLO, lab, training)",
|
||||
"tagsSaved": "Tags enregistrés",
|
||||
"tagsSaveFailed": "Échec de l'enregistrement des tags",
|
||||
"searchPlaceholder": "Rechercher dans les sorties des sessions...",
|
||||
"searchResults": "{{count}} correspondances",
|
||||
"searchFailed": "Échec de la recherche",
|
||||
"selectPaneHint": "Sélectionnez un volet pour prévisualiser sa sortie",
|
||||
"closePreview": "Fermer l'aperçu",
|
||||
"newSession": "Nouvelle session",
|
||||
"newSessionHint": "Nom de session (lettres, chiffres, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "Créer",
|
||||
"sessionCreated": "Session « {{name}} » créée",
|
||||
"sessionCreateFailed": "Échec de la création de la session",
|
||||
"splitRight": "Diviser à droite",
|
||||
"splitDown": "Diviser en bas",
|
||||
"splitFailed": "Échec de la division du volet",
|
||||
"newWindow": "Nouvelle fenêtre",
|
||||
"windowCreateFailed": "Échec de la création de la fenêtre",
|
||||
"attachSessionTooltip": "Attacher à {{session}}",
|
||||
"refresh": "Actualiser",
|
||||
"refreshFailed": "Échec de l'actualisation des sessions",
|
||||
"collapseAll": "Réduire tout",
|
||||
"expandAll": "Développer tout",
|
||||
"moreActions": "Plus d'actions",
|
||||
"sessionStats": "Statistiques de session",
|
||||
"renameSessionTitle": "Renommer la session « {{name}} »",
|
||||
"rename": "Renommer",
|
||||
"sessionRenamed": "Session renommée en « {{name}} »",
|
||||
"sessionRenameFailed": "Échec du renommage de la session",
|
||||
"editTagsTitle": "Modifier les tags pour « {{name}} »",
|
||||
"killPane": "Fermer le volet",
|
||||
"resizeTree": "Faire glisser pour redimensionner — double-cliquer pour réinitialiser",
|
||||
"reattach": "Rattacher (corrige un affichage brouillé)",
|
||||
"killWindow": "Fermer la fenêtre",
|
||||
"killWindowTitle": "Fermer la fenêtre {{index}} de « {{session}} » ?",
|
||||
"killWindowBody": "Tous les volets et processus de cette fenêtre seront terminés. La fermeture de la dernière fenêtre met fin à la session.",
|
||||
"windowKillFailed": "Échec de la fermeture de la fenêtre",
|
||||
"statusActivity": "Activité",
|
||||
"statusWindows": "Fenêtres",
|
||||
"statusPanes": "Volets",
|
||||
"statusTags": "Étiquettes",
|
||||
"killPaneTitle": "Fermer le volet {{id}} ?",
|
||||
"killPaneBody": "Le processus s’exécutant dans ce volet sera terminé. La fermeture du dernier volet ferme sa fenêtre.",
|
||||
"paneKillFailed": "Échec de la fermeture du volet",
|
||||
"killSessionTitle": "Fermer la session « {{name}} » ?",
|
||||
"killSessionBody": "Toutes les fenêtres et processus en cours dans cette session seront terminés. Cette action est irréversible.",
|
||||
"kill": "Fermer",
|
||||
"sessionKilled": "Session « {{name}} » fermée",
|
||||
"sessionKillFailed": "Échec de la fermeture de la session",
|
||||
"hostUnreachable": "Impossible de se connecter à l’hôte. Vérifiez qu’il est en ligne et accessible.",
|
||||
"noServer": "Aucun serveur tmux n’est exécuté sur cet hôte.",
|
||||
"searchTruncated": "Résultats partiels — la recherche couvre les {{lines}} dernières lignes de chaque volet et jusqu’à {{panes}} volets.",
|
||||
"closeSearchResults": "Fermer les résultats de recherche",
|
||||
"retry": "Réessayer",
|
||||
"noHosts": "Aucun hôte SSH disponible",
|
||||
"noHostsHint": "Activez l’option Tmux Monitor sur un hôte SSH (Gestionnaire d’hôtes → onglet Terminal) pour surveiller ses sessions tmux.",
|
||||
"tmuxInstallHint": "Installez-le sur l’hôte avec :",
|
||||
"attachTooltip": "Ouvrir un terminal sur {{host}}",
|
||||
"attachTooltipPane": "Ouvrir un terminal sur {{host}} — session tmux {{session}}",
|
||||
"timeJustNow": "à l’instant",
|
||||
"timeMinutes": "il y a {{count}} min",
|
||||
"timeHours": "il y a {{count}} h",
|
||||
"timeDays": "il y a {{count}} j"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Série",
|
||||
"portLabel": "Port",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "שינוי שם הכרטיסייה",
|
||||
"openFileManager": "פתח את מנהל הקבצים",
|
||||
"dashboard": "לוּחַ מַחווָנִים",
|
||||
"tmuxMonitor": "צג Tmux",
|
||||
"homepage": "דף הבית",
|
||||
"quickConnect": "חיבור מהיר",
|
||||
"sshTools": "כלי SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "מקס קיפלייב מחמיץ",
|
||||
"sameHost": "מארח זה (מנהרה ישירה)",
|
||||
"bindHost": "מארח קשירה",
|
||||
"enableTmuxMonitor": "הפעל את צג Tmux",
|
||||
"enableTmuxMonitorDesc": "הצג מארח זה בצג Tmux והוסף את פעולות ה-tmux שלו לסרגל הצד",
|
||||
"connectionToolbar": "סרגל הכלים לחיבור",
|
||||
"showConnectionToolbar": "הצג סרגל הכלים של החיבור",
|
||||
"showConnectionToolbarDesc": "הצג את סרגל הכלים בהפעלות SSH, RDP, VNC ו-Telnet של מחשב מארח זה. ניתן גם להסתיר אותו באופן זמני מסרגל הכלים עצמו.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "כתובת ה-URL של המנהרה הועתקה",
|
||||
"dockerUrlCopied": "כתובת ה-URL של Docker הועתקה",
|
||||
"hostMetricsUrlCopied": "כתובת האתר של מדדי המארח הועתקה",
|
||||
"tmuxMonitorUrlCopied": "כתובת האתר של צג Tmux הועתקה",
|
||||
"rdpUrlCopied": "כתובת ה-URL של RDP הועתקה",
|
||||
"vncUrlCopied": "כתובת האתר של VNC הועתקה",
|
||||
"telnetUrlCopied": "כתובת ה-URL של Telnet הועתקה",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "העתקת כתובת URL של המנהרה",
|
||||
"copyDockerUrlAction": "העתקת כתובת URL של Docker",
|
||||
"copyHostMetricsUrlAction": "העתקת כתובת URL של מדדי מארח",
|
||||
"copyTmuxMonitorUrlAction": "העתקת כתובת URL של צג Tmux",
|
||||
"copyRdpUrlAction": "העתקת כתובת URL של RDP",
|
||||
"copyVncUrlAction": "העתקת כתובת URL של VNC",
|
||||
"copyTelnetUrlAction": "העתקת כתובת URL של Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "פתח את סטטיסטיקות Proxmox",
|
||||
"openTmuxMonitor": "פתח את צג Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "צג Tmux",
|
||||
"failedToLoadHosts": "טעינת המארחים נכשלה",
|
||||
"failedToLoad": "טעינת סשנים של tmux נכשלה",
|
||||
"tmuxUnavailable": "tmux אינו מותקן על מחשב זה",
|
||||
"noSessions": "אין הפעלות tmux על מארח זה",
|
||||
"noHostSelected": "לא נבחר מארח",
|
||||
"attached": "מְצוֹרָף",
|
||||
"detached": "מְנוּתָק",
|
||||
"attach": "לְצַרֵף",
|
||||
"editTags": "עריכת תגיות",
|
||||
"tagsHint": "תגיות מופרדות בפסיקים (למשל YOLO, מעבדה, הדרכה)",
|
||||
"tagsSaved": "תגיות נשמרו",
|
||||
"tagsSaveFailed": "שמירת התגים נכשלה",
|
||||
"searchPlaceholder": "חיפוש פלט בין סשנים...",
|
||||
"searchResults": "{{count}} התאמות",
|
||||
"searchFailed": "החיפוש נכשל",
|
||||
"selectPaneHint": "בחר חלונית כדי להציג תצוגה מקדימה של הפלט שלה",
|
||||
"closePreview": "סגור את התצוגה המקדימה",
|
||||
"newSession": "סשן חדש",
|
||||
"newSessionHint": "שם סשן (אותיות, ספרות, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "הסשן שלי",
|
||||
"create": "לִיצוֹר",
|
||||
"sessionCreated": "סשן \"{{name}}\" נוצר",
|
||||
"sessionCreateFailed": "יצירת הסשן נכשלה",
|
||||
"splitRight": "פיצול ימינה",
|
||||
"splitDown": "התפצלו למטה",
|
||||
"splitFailed": "נכשל פיצול החלונית",
|
||||
"newWindow": "חלון חדש",
|
||||
"windowCreateFailed": "נכשלה יצירת החלון",
|
||||
"attachSessionTooltip": "צרף אל {{session}}",
|
||||
"refresh": "לְרַעֲנֵן",
|
||||
"refreshFailed": "רענון ההפעלות נכשל",
|
||||
"collapseAll": "כווץ הכל",
|
||||
"expandAll": "הרחב הכל",
|
||||
"moreActions": "פעולות נוספות",
|
||||
"sessionStats": "סטטיסטיקות של סשן",
|
||||
"renameSessionTitle": "שינוי שם הסשן \"{{name}}\"",
|
||||
"rename": "שינוי שם",
|
||||
"sessionRenamed": "שם הסשן הוא \"{{name}}\"",
|
||||
"sessionRenameFailed": "שינוי שם ההפעלה נכשל",
|
||||
"editTagsTitle": "עריכת תגיות עבור \"{{name}}\"",
|
||||
"killPane": "חלונית הרג",
|
||||
"resizeTree": "גרור לשינוי גודל - לחץ פעמיים לאיפוס",
|
||||
"reattach": "חבר מחדש (מתקן תצוגה מעוותת)",
|
||||
"killWindow": "להרוג את החלון",
|
||||
"killWindowTitle": "להפסיק את החלון {{index}} של \"{{session}}\"?",
|
||||
"killWindowBody": "כל חלונית ותהליך בחלון זה יופסק. סגירת החלון האחרון תסיים את ההפעלה.",
|
||||
"windowKillFailed": "נכשלה הסגת החלון",
|
||||
"statusActivity": "פְּעִילוּת",
|
||||
"statusWindows": "חלונות",
|
||||
"statusPanes": "חלוניות",
|
||||
"statusTags": "תגיות",
|
||||
"killPaneTitle": "להרוג את החלונית {{id}}?",
|
||||
"killPaneBody": "התהליך הפועל בחלונית זו יופסק. סגירת החלונית האחרונה תסגור את החלון שלה.",
|
||||
"paneKillFailed": "נכשלה הסגת החלונית",
|
||||
"killSessionTitle": "להפסיק את הסשן \"{{name}}\"?",
|
||||
"killSessionBody": "כל החלונות והתהליכים הפועלים בהפעלה זו ייסגרו. לא ניתן לבטל פעולה זו.",
|
||||
"kill": "לַהֲרוֹג",
|
||||
"sessionKilled": "הסשן \"{{name}}\" נסגר",
|
||||
"sessionKillFailed": "נכשלה הסיום",
|
||||
"hostUnreachable": "לא ניתן היה להתחבר למארח. ודא שהוא מחובר ונגיש.",
|
||||
"noServer": "אף שרת tmux לא פועל על מחשב אחסון זה.",
|
||||
"searchTruncated": "תוצאות חלקיות - החיפוש מכסה את השורות האחרונות {{lines}} של כל חלונית ולכל היותר {{panes}} חלונות.",
|
||||
"closeSearchResults": "סגור את תוצאות החיפוש",
|
||||
"retry": "נסה שוב",
|
||||
"noHosts": "אין מארחי SSH זמינים",
|
||||
"noHostsHint": "הפעל את האפשרות Tmux Monitor במארח SSH (מנהל מארח → לשונית טרמינל) כדי לנטר את הפעלות ה-tmux שלו.",
|
||||
"tmuxInstallHint": "התקן אותו על המארח עם:",
|
||||
"attachTooltip": "פתח טרמינל אל {{host}}",
|
||||
"attachTooltipPane": "פתח טרמינל אל {{host}} — tmux session {{session}}",
|
||||
"timeJustNow": "זֶה עַתָה",
|
||||
"timeMinutes": "לפני {{count}}דקות",
|
||||
"timeHours": "{{count}}h ago",
|
||||
"timeDays": "{{count}}d ago"
|
||||
},
|
||||
"serial": {
|
||||
"title": "סִדרָתִי",
|
||||
"portLabel": "נָמָל",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "टैब का नाम बदलें",
|
||||
"openFileManager": "फ़ाइल प्रबंधक खोलें",
|
||||
"dashboard": "डैशबोर्ड",
|
||||
"tmuxMonitor": "Tmux मॉनिटर",
|
||||
"homepage": "होमपेज",
|
||||
"quickConnect": "त्वरित कनेक्ट",
|
||||
"sshTools": "SSH टूल्स",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "अधिकतम कीपअलाइव मिस",
|
||||
"sameHost": "यह होस्ट (डायरेक्ट टनल)",
|
||||
"bindHost": "बाइंड होस्ट",
|
||||
"enableTmuxMonitor": "Tmux मॉनिटर सक्षम करें",
|
||||
"enableTmuxMonitorDesc": "इस होस्ट को Tmux मॉनिटर में दिखाएँ और इसकी tmux क्रियाओं को साइडबार में जोड़ें",
|
||||
"connectionToolbar": "कनेक्शन टूलबार",
|
||||
"showConnectionToolbar": "कनेक्शन टूलबार दिखाएँ",
|
||||
"showConnectionToolbarDesc": "इस होस्ट के SSH, RDP, VNC और Telnet सेशन में टूलबार दिखाएँ। आप इसे टूलबार से अस्थायी रूप से छिपा भी सकते हैं।",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "टनल URL कॉपी किया गया",
|
||||
"dockerUrlCopied": "Docker URL कॉपी किया गया",
|
||||
"hostMetricsUrlCopied": "होस्ट मेट्रिक्स URL कॉपी किया गया",
|
||||
"tmuxMonitorUrlCopied": "Tmux मॉनिटर URL कॉपी किया गया",
|
||||
"rdpUrlCopied": "RDP URL कॉपी किया गया",
|
||||
"vncUrlCopied": "VNC URL कॉपी किया गया",
|
||||
"telnetUrlCopied": "Telnet URL कॉपी किया गया",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "टनल URL कॉपी करें",
|
||||
"copyDockerUrlAction": "Docker URL कॉपी करें",
|
||||
"copyHostMetricsUrlAction": "होस्ट मेट्रिक्स URL कॉपी करें",
|
||||
"copyTmuxMonitorUrlAction": "Tmux मॉनिटर URL कॉपी करें",
|
||||
"copyRdpUrlAction": "RDP URL कॉपी करें",
|
||||
"copyVncUrlAction": "VNC URL कॉपी करें",
|
||||
"copyTelnetUrlAction": "Telnet URL कॉपी करें",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "प्रोक्समॉक्स के आँकड़े खोलें",
|
||||
"openTmuxMonitor": "ओपन टीएमएक्स मॉनिटर"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux मॉनिटर",
|
||||
"failedToLoadHosts": "होस्ट लोड करने में विफल",
|
||||
"failedToLoad": "tmux सत्र लोड करने में विफल",
|
||||
"tmuxUnavailable": "इस होस्ट पर tmux इंस्टॉल नहीं है",
|
||||
"noSessions": "इस होस्ट पर कोई tmux सत्र नहीं है",
|
||||
"noHostSelected": "कोई होस्ट चयनित नहीं है",
|
||||
"attached": "अटैच्ड",
|
||||
"detached": "डिटैच्ड",
|
||||
"attach": "अटैच करें",
|
||||
"editTags": "टैग संपादित करें",
|
||||
"tagsHint": "अल्पविराम से अलग टैग (जैसे YOLO, lab, training)",
|
||||
"tagsSaved": "टैग सहेजे गए",
|
||||
"tagsSaveFailed": "टैग सहेजने में विफल",
|
||||
"searchPlaceholder": "सत्रों में आउटपुट खोजें...",
|
||||
"searchResults": "{{count}} मिलान",
|
||||
"searchFailed": "खोज विफल",
|
||||
"selectPaneHint": "इसके आउटपुट का पूर्वावलोकन करने के लिए एक पैन चुनें",
|
||||
"closePreview": "पूर्वावलोकन बंद करें",
|
||||
"newSession": "नया सत्र",
|
||||
"newSessionHint": "सत्र का नाम (अक्षर, अंक, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "बनाएं",
|
||||
"sessionCreated": "सत्र \"{{name}}\" बनाया गया",
|
||||
"sessionCreateFailed": "सत्र बनाने में विफल",
|
||||
"splitRight": "दाएं विभाजित करें",
|
||||
"splitDown": "नीचे विभाजित करें",
|
||||
"splitFailed": "पैन विभाजित करने में विफल",
|
||||
"newWindow": "नई विंडो",
|
||||
"windowCreateFailed": "विंडो बनाने में विफल",
|
||||
"attachSessionTooltip": "{{session}} में अटैच करें",
|
||||
"refresh": "ताज़ा करें",
|
||||
"refreshFailed": "सत्र ताज़ा करने में विफल",
|
||||
"collapseAll": "सभी संकुचित करें",
|
||||
"expandAll": "सभी विस्तृत करें",
|
||||
"moreActions": "अधिक कार्रवाइयां",
|
||||
"sessionStats": "सत्र आँकड़े",
|
||||
"renameSessionTitle": "सत्र \"{{name}}\" का नाम बदलें",
|
||||
"rename": "नाम बदलें",
|
||||
"sessionRenamed": "सत्र का नाम बदलकर \"{{name}}\" कर दिया गया",
|
||||
"sessionRenameFailed": "सत्र का नाम बदलने में विफल",
|
||||
"editTagsTitle": "\"{{name}}\" के लिए टैग संपादित करें",
|
||||
"killPane": "पेन नष्ट करें",
|
||||
"resizeTree": "आकार बदलने के लिए खींचें — रीसेट करने के लिए डबल-क्लिक करें",
|
||||
"reattach": "पुनः अटैच करें (विकृत दृश्य को ठीक करता है)",
|
||||
"killWindow": "विंडो नष्ट करें",
|
||||
"killWindowTitle": "क्या \"{{session}}\" की विंडो {{index}} नष्ट करें?",
|
||||
"killWindowBody": "इस विंडो के सभी पेन और प्रक्रियाएँ समाप्त हो जाएँगी। अंतिम विंडो नष्ट करने से सत्र समाप्त हो जाता है।",
|
||||
"windowKillFailed": "विंडो नष्ट करने में विफल",
|
||||
"statusActivity": "गतिविधि",
|
||||
"statusWindows": "विंडोज़",
|
||||
"statusPanes": "पेन",
|
||||
"statusTags": "टैग",
|
||||
"killPaneTitle": "क्या पेन {{id}} नष्ट करें?",
|
||||
"killPaneBody": "इस पेन में चल रही प्रक्रिया समाप्त हो जाएगी। अंतिम पेन नष्ट करने पर इसकी विंडो बंद हो जाती है।",
|
||||
"paneKillFailed": "पेन नष्ट करने में विफल",
|
||||
"killSessionTitle": "क्या सत्र \"{{name}}\" नष्ट करें?",
|
||||
"killSessionBody": "इस सत्र की सभी विंडोज़ और चल रही प्रक्रियाएँ समाप्त हो जाएँगी। इसे पूर्ववत नहीं किया जा सकता।",
|
||||
"kill": "नष्ट करें",
|
||||
"sessionKilled": "सत्र \"{{name}}\" नष्ट किया गया",
|
||||
"sessionKillFailed": "सत्र नष्ट करने में विफल",
|
||||
"hostUnreachable": "होस्ट से कनेक्ट नहीं हो सका। जाँचें कि यह ऑनलाइन और पहुँच योग्य है।",
|
||||
"noServer": "इस होस्ट पर कोई tmux सर्वर नहीं चल रहा है।",
|
||||
"searchTruncated": "आंशिक परिणाम — खोज प्रत्येक पेन की अंतिम {{lines}} पंक्तियों और अधिकतम {{panes}} पेनों में की गई है।",
|
||||
"closeSearchResults": "खोज परिणाम बंद करें",
|
||||
"retry": "पुनः प्रयास करें",
|
||||
"noHosts": "कोई SSH होस्ट उपलब्ध नहीं है",
|
||||
"noHostsHint": "किसी SSH होस्ट पर Tmux Monitor विकल्प सक्षम करें (होस्ट प्रबंधक → टर्मिनल टैब) ताकि उसके tmux सत्रों की निगरानी की जा सके।",
|
||||
"tmuxInstallHint": "इसे होस्ट पर इंस्टॉल करें:",
|
||||
"attachTooltip": "{{host}} पर टर्मिनल खोलें",
|
||||
"attachTooltipPane": "{{host}} पर टर्मिनल खोलें — tmux सत्र {{session}}",
|
||||
"timeJustNow": "अभी",
|
||||
"timeMinutes": "{{count}}m पहले",
|
||||
"timeHours": "{{count}}h पहले",
|
||||
"timeDays": "{{count}}d पहले"
|
||||
},
|
||||
"serial": {
|
||||
"title": "सीरियल",
|
||||
"portLabel": "पोर्ट",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Fül átnevezése",
|
||||
"openFileManager": "Fájlkezelő megnyitása",
|
||||
"dashboard": "Műszerfal",
|
||||
"tmuxMonitor": "Tmux monitor",
|
||||
"homepage": "Kezdőlap",
|
||||
"quickConnect": "Gyors csatlakozás",
|
||||
"sshTools": "SSH eszközök",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Max Keepalive kihagyások",
|
||||
"sameHost": "Ez a gazdagép (közvetlen alagút)",
|
||||
"bindHost": "Kötőállomás",
|
||||
"enableTmuxMonitor": "Tmux monitor engedélyezése",
|
||||
"enableTmuxMonitorDesc": "Mutassa meg ezt a gépet a Tmux Monitorban, és adja hozzá a tmux műveleteit az oldalsávhoz",
|
||||
"connectionToolbar": "Kapcsolat eszköztár",
|
||||
"showConnectionToolbar": "Kapcsolat eszköztár megjelenítése",
|
||||
"showConnectionToolbarDesc": "Eszköztár megjelenítése a gazdagép SSH, RDP, VNC és Telnet munkameneteiben. Ideiglenesen el is rejtheti magáról az eszköztárról.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Alagút URL-címe másolva",
|
||||
"dockerUrlCopied": "Docker URL másolva",
|
||||
"hostMetricsUrlCopied": "Gazdagép-metrikák URL-címe kimásolva",
|
||||
"tmuxMonitorUrlCopied": "Tmux Monitor URL-címe másolva",
|
||||
"rdpUrlCopied": "RDP URL másolva",
|
||||
"vncUrlCopied": "VNC URL másolva",
|
||||
"telnetUrlCopied": "Telnet URL másolva",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Alagút URL-címének másolása",
|
||||
"copyDockerUrlAction": "Docker URL másolása",
|
||||
"copyHostMetricsUrlAction": "Gazdagép-metrikák URL-címének másolása",
|
||||
"copyTmuxMonitorUrlAction": "Tmux Monitor URL másolása",
|
||||
"copyRdpUrlAction": "RDP URL másolása",
|
||||
"copyVncUrlAction": "VNC URL másolása",
|
||||
"copyTelnetUrlAction": "Telnet URL másolása",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Proxmox statisztikák megnyitása",
|
||||
"openTmuxMonitor": "Tmux monitor megnyitása"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux monitor",
|
||||
"failedToLoadHosts": "Nem sikerült betölteni a gazdagépeket",
|
||||
"failedToLoad": "Nem sikerült betölteni a tmux munkameneteket",
|
||||
"tmuxUnavailable": "A tmux nincs telepítve erre a gépre",
|
||||
"noSessions": "Nincsenek tmux munkamenetek ezen a gépen",
|
||||
"noHostSelected": "Nincs kiválasztva gazdagép",
|
||||
"attached": "Csatolt",
|
||||
"detached": "Különálló",
|
||||
"attach": "Csatolás",
|
||||
"editTags": "Címkék szerkesztése",
|
||||
"tagsHint": "Vesszővel elválasztott címkék (pl. YOLO, labor, képzés)",
|
||||
"tagsSaved": "Címkék mentve",
|
||||
"tagsSaveFailed": "Nem sikerült menteni a címkéket",
|
||||
"searchPlaceholder": "Keresési kimenet munkamenetek között...",
|
||||
"searchResults": "{{count}} találat",
|
||||
"searchFailed": "Keresés sikertelen",
|
||||
"selectPaneHint": "Válasszon ki egy panelt a kimenetének megtekintéséhez",
|
||||
"closePreview": "Előnézet bezárása",
|
||||
"newSession": "Új munkamenet",
|
||||
"newSessionHint": "Munkamenet neve (betűk, számjegyek, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "Teremt",
|
||||
"sessionCreated": "\"{{name}}\" munkamenet létrehozva",
|
||||
"sessionCreateFailed": "Nem sikerült létrehozni a munkamenetet",
|
||||
"splitRight": "Jobbra osztás",
|
||||
"splitDown": "Felosztás",
|
||||
"splitFailed": "Nem sikerült felosztani az ablaktáblát",
|
||||
"newWindow": "Új ablak",
|
||||
"windowCreateFailed": "Nem sikerült létrehozni az ablakot",
|
||||
"attachSessionTooltip": "Csatlakozás ehhez: {{session}}",
|
||||
"refresh": "Frissítés",
|
||||
"refreshFailed": "Nem sikerült frissíteni a munkameneteket",
|
||||
"collapseAll": "Összes összecsukása",
|
||||
"expandAll": "Összes kibontása",
|
||||
"moreActions": "További műveletek",
|
||||
"sessionStats": "Munkamenet-statisztikák",
|
||||
"renameSessionTitle": "Munkamenet átnevezése \"{{name}}\"",
|
||||
"rename": "Átnevezés",
|
||||
"sessionRenamed": "Munkamenet átnevezése erre: \"{{name}}\"",
|
||||
"sessionRenameFailed": "Nem sikerült átnevezni a munkamenetet",
|
||||
"editTagsTitle": "A(z) „{{name}} ” címkéinek szerkesztése",
|
||||
"killPane": "Ablak bezárása",
|
||||
"resizeTree": "Húzással átméretezhető – dupla kattintással visszaállítható",
|
||||
"reattach": "Újracsatolás (kijavítja a torz képet)",
|
||||
"killWindow": "Ablak ölése",
|
||||
"killWindowTitle": "A(z) \"{{session}} \" ablakának {{index}} leállítása?",
|
||||
"killWindowBody": "Az ablakban található összes panel és folyamat leáll. Az utolsó ablak bezárása a munkamenet végét jelenti.",
|
||||
"windowKillFailed": "Nem sikerült bezárni az ablakot",
|
||||
"statusActivity": "Tevékenység",
|
||||
"statusWindows": "Ablakok",
|
||||
"statusPanes": "ablaktáblák",
|
||||
"statusTags": "Címkék",
|
||||
"killPaneTitle": "Ablak bezárása {{id}}?",
|
||||
"killPaneBody": "Az ebben a panelben futó folyamat leáll. Az utolsó panel bezárása bezárja az ablakát.",
|
||||
"paneKillFailed": "Nem sikerült bezárni az ablaktáblát",
|
||||
"killSessionTitle": "Munkamenet leállítása: \"{{name}}\"?",
|
||||
"killSessionBody": "Ebben a munkamenetben az összes ablak és futó folyamat leáll. Ez a művelet nem vonható vissza.",
|
||||
"kill": "Megöl",
|
||||
"sessionKilled": "A(z) \"{{name}}\" munkamenet leállt",
|
||||
"sessionKillFailed": "Nem sikerült leállítani a munkamenetet",
|
||||
"hostUnreachable": "Nem sikerült csatlakozni a gazdagéphez. Ellenőrizze, hogy online van-e és elérhető-e.",
|
||||
"noServer": "Nem fut tmux szerver ezen a gépen.",
|
||||
"searchTruncated": "Részleges eredmények — a keresés az egyes panelek utolsó {{lines}} soraira, és legfeljebb {{panes}} panelre terjed ki.",
|
||||
"closeSearchResults": "Keresési eredmények bezárása",
|
||||
"retry": "Újrapróbálkozás",
|
||||
"noHosts": "Nincsenek elérhető SSH-hosztok",
|
||||
"noHostsHint": "Engedélyezze a Tmux Monitor opciót egy SSH hoszton (Host Manager → Terminal fül) a tmux munkamenetek monitorozásához.",
|
||||
"tmuxInstallHint": "Telepítse a gazdagépre a következő paranccsal:",
|
||||
"attachTooltip": "Nyisson meg egy terminált a következőhöz: {{host}}",
|
||||
"attachTooltipPane": "Nyiss meg egy terminált a következőhöz: {{host}} — tmux session {{session}}",
|
||||
"timeJustNow": "éppen most",
|
||||
"timeMinutes": "{{count}}perccel ezelőtt",
|
||||
"timeHours": "{{count}}órával ezelőtt",
|
||||
"timeDays": "{{count}}nappal ezelőtt"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Sorozatszám",
|
||||
"portLabel": "Kikötő",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Ganti nama tab",
|
||||
"openFileManager": "Buka Pengelola File",
|
||||
"dashboard": "Dasbor",
|
||||
"tmuxMonitor": "Monitor Tmux",
|
||||
"homepage": "Beranda",
|
||||
"quickConnect": "Koneksi Cepat",
|
||||
"sshTools": "Alat SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Max Keepalive Gagal",
|
||||
"sameHost": "Host ini (terowongan langsung)",
|
||||
"bindHost": "Host Terikat",
|
||||
"enableTmuxMonitor": "Aktifkan Monitor Tmux",
|
||||
"enableTmuxMonitorDesc": "Tampilkan host ini di Tmux Monitor dan tambahkan aksi tmux-nya ke sidebar.",
|
||||
"connectionToolbar": "Toolbar Koneksi",
|
||||
"showConnectionToolbar": "Tampilkan bilah alat koneksi",
|
||||
"showConnectionToolbarDesc": "Tampilkan toolbar di sesi SSH, RDP, VNC, dan Telnet host ini. Anda juga dapat menyembunyikannya sementara dari toolbar itu sendiri.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "URL terowongan disalin",
|
||||
"dockerUrlCopied": "URL Docker disalin",
|
||||
"hostMetricsUrlCopied": "URL Metrik Host disalin",
|
||||
"tmuxMonitorUrlCopied": "URL Monitor Tmux disalin",
|
||||
"rdpUrlCopied": "URL RDP disalin",
|
||||
"vncUrlCopied": "URL VNC disalin",
|
||||
"telnetUrlCopied": "URL Telnet disalin",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Salin URL Terowongan",
|
||||
"copyDockerUrlAction": "Salin URL Docker",
|
||||
"copyHostMetricsUrlAction": "Salin URL Metrik Host",
|
||||
"copyTmuxMonitorUrlAction": "Salin URL Monitor Tmux",
|
||||
"copyRdpUrlAction": "Salin URL RDP",
|
||||
"copyVncUrlAction": "Salin URL VNC",
|
||||
"copyTelnetUrlAction": "Salin URL Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Buka Statistik Proxmox",
|
||||
"openTmuxMonitor": "Buka Monitor Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Monitor Tmux",
|
||||
"failedToLoadHosts": "Gagal memuat host",
|
||||
"failedToLoad": "Gagal memuat sesi tmux",
|
||||
"tmuxUnavailable": "tmux tidak terinstal pada host ini",
|
||||
"noSessions": "Tidak ada sesi tmux di host ini.",
|
||||
"noHostSelected": "Tidak ada pembawa acara yang dipilih.",
|
||||
"attached": "Terlampir",
|
||||
"detached": "Terpisah",
|
||||
"attach": "Menempel",
|
||||
"editTags": "Edit tag",
|
||||
"tagsHint": "Tag yang dipisahkan koma (misalnya YOLO, lab, training)",
|
||||
"tagsSaved": "Tag tersimpan",
|
||||
"tagsSaveFailed": "Gagal menyimpan tag.",
|
||||
"searchPlaceholder": "Hasil pencarian di seluruh sesi...",
|
||||
"searchResults": "{{count}} cocok",
|
||||
"searchFailed": "Pencarian gagal",
|
||||
"selectPaneHint": "Pilih panel untuk melihat pratinjau hasilnya.",
|
||||
"closePreview": "Tutup pratinjau",
|
||||
"newSession": "Sesi baru",
|
||||
"newSessionHint": "Nama sesi (huruf, angka, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "sesi saya",
|
||||
"create": "Membuat",
|
||||
"sessionCreated": "Sesi \"{{name}}\" dibuat",
|
||||
"sessionCreateFailed": "Gagal membuat sesi",
|
||||
"splitRight": "Belah kanan",
|
||||
"splitDown": "Terbagi",
|
||||
"splitFailed": "Gagal membagi panel",
|
||||
"newWindow": "Jendela baru",
|
||||
"windowCreateFailed": "Gagal membuat jendela",
|
||||
"attachSessionTooltip": "Lampirkan ke {{session}}",
|
||||
"refresh": "Menyegarkan",
|
||||
"refreshFailed": "Gagal memperbarui sesi",
|
||||
"collapseAll": "Ciutkan semua",
|
||||
"expandAll": "Perluas semua",
|
||||
"moreActions": "Tindakan lainnya",
|
||||
"sessionStats": "Statistik sesi",
|
||||
"renameSessionTitle": "Ganti nama sesi \"{{name}}\"",
|
||||
"rename": "Ganti nama",
|
||||
"sessionRenamed": "Sesi diubah namanya menjadi \"{{name}}\"",
|
||||
"sessionRenameFailed": "Gagal mengganti nama sesi",
|
||||
"editTagsTitle": "Edit tag untuk \"{{name}}\"",
|
||||
"killPane": "Panel pembunuh",
|
||||
"resizeTree": "Seret untuk mengubah ukuran — klik dua kali untuk mengatur ulang",
|
||||
"reattach": "Pasangkan kembali (memperbaiki tampilan yang kacau)",
|
||||
"killWindow": "Jendela pembunuhan",
|
||||
"killWindowTitle": "Hapus jendela {{index}} dari \"{{session}}\"?",
|
||||
"killWindowBody": "Semua panel dan proses di jendela ini akan dihentikan. Menghentikan jendela terakhir akan mengakhiri sesi.",
|
||||
"windowKillFailed": "Gagal menutup jendela",
|
||||
"statusActivity": "Aktivitas",
|
||||
"statusWindows": "Windows",
|
||||
"statusPanes": "panel",
|
||||
"statusTags": "Tag",
|
||||
"killPaneTitle": "Hapus panel {{id}}?",
|
||||
"killPaneBody": "Proses yang berjalan di panel ini akan dihentikan. Menghentikan proses di panel terakhir akan menutup jendelanya.",
|
||||
"paneKillFailed": "Gagal mematikan panel",
|
||||
"killSessionTitle": "Hentikan sesi \"{{name}}\"?",
|
||||
"killSessionBody": "Semua jendela dan proses yang berjalan dalam sesi ini akan dihentikan. Tindakan ini tidak dapat dibatalkan.",
|
||||
"kill": "Membunuh",
|
||||
"sessionKilled": "Sesi \"{{name}}\" dihentikan",
|
||||
"sessionKillFailed": "Gagal mengakhiri sesi",
|
||||
"hostUnreachable": "Tidak dapat terhubung ke host. Periksa apakah host tersebut online dan dapat dijangkau.",
|
||||
"noServer": "Tidak ada server tmux yang berjalan di host ini.",
|
||||
"searchTruncated": "Hasil parsial — pencarian mencakup {{lines}} baris terakhir dari setiap panel dan paling banyak {{panes}} panel.",
|
||||
"closeSearchResults": "Tutup hasil pencarian",
|
||||
"retry": "Mencoba kembali",
|
||||
"noHosts": "Tidak ada host SSH yang tersedia.",
|
||||
"noHostsHint": "Aktifkan opsi Tmux Monitor pada host SSH (Host Manager → tab Terminal) untuk memantau sesi tmux-nya.",
|
||||
"tmuxInstallHint": "Instal di host dengan cara:",
|
||||
"attachTooltip": "Buka terminal ke {{host}}",
|
||||
"attachTooltipPane": "Buka terminal ke {{host}} — sesi tmux {{session}}",
|
||||
"timeJustNow": "baru saja",
|
||||
"timeMinutes": "{{count}}m lalu",
|
||||
"timeHours": "{{count}}h lalu",
|
||||
"timeDays": "{{count}}d lalu"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Pelabuhan",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Rinomina scheda",
|
||||
"openFileManager": "Apri Gestione file",
|
||||
"dashboard": "Dashboard",
|
||||
"tmuxMonitor": "Monitor Tmux",
|
||||
"homepage": "Homepage",
|
||||
"quickConnect": "Connessione rapida",
|
||||
"sshTools": "Strumenti SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Numero massimo di mancate keepalive",
|
||||
"sameHost": "Questo host (tunnel diretto)",
|
||||
"bindHost": "Host di bind",
|
||||
"enableTmuxMonitor": "Abilita Monitor Tmux",
|
||||
"enableTmuxMonitorDesc": "Mostra questo host nel Monitor Tmux e aggiungi le sue azioni tmux alla barra laterale",
|
||||
"connectionToolbar": "Barra degli strumenti di connessione",
|
||||
"showConnectionToolbar": "Mostra la barra degli strumenti di connessione",
|
||||
"showConnectionToolbarDesc": "Mostra la barra degli strumenti nelle sessioni SSH, RDP, VNC e Telnet di questo host. Puoi anche nasconderla temporaneamente dalla barra degli strumenti stessa.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "URL tunnel copiato",
|
||||
"dockerUrlCopied": "URL Docker copiato",
|
||||
"hostMetricsUrlCopied": "URL metriche host copiato",
|
||||
"tmuxMonitorUrlCopied": "URL Tmux Monitor copiato",
|
||||
"rdpUrlCopied": "URL RDP copiato",
|
||||
"vncUrlCopied": "URL VNC copiato",
|
||||
"telnetUrlCopied": "URL Telnet copiato",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Copia URL Tunnel",
|
||||
"copyDockerUrlAction": "Copia URL Docker",
|
||||
"copyHostMetricsUrlAction": "Copia URL metriche host",
|
||||
"copyTmuxMonitorUrlAction": "Copia URL Tmux Monitor",
|
||||
"copyRdpUrlAction": "Copia URL RDP",
|
||||
"copyVncUrlAction": "Copia URL VNC",
|
||||
"copyTelnetUrlAction": "Copia URL Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Statistiche di Proxmox aperte",
|
||||
"openTmuxMonitor": "Monitor Tmux aperto"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Monitor Tmux",
|
||||
"failedToLoadHosts": "Impossibile caricare gli host",
|
||||
"failedToLoad": "Impossibile caricare le sessioni tmux",
|
||||
"tmuxUnavailable": "tmux non è installato su questo host",
|
||||
"noSessions": "Nessuna sessione tmux su questo host",
|
||||
"noHostSelected": "Nessun host selezionato",
|
||||
"attached": "Connesso",
|
||||
"detached": "Scollegato",
|
||||
"attach": "Connetti",
|
||||
"editTags": "Modifica tag",
|
||||
"tagsHint": "Tag separati da virgole (es. YOLO, lab, training)",
|
||||
"tagsSaved": "Tag salvati",
|
||||
"tagsSaveFailed": "Salvataggio dei tag non riuscito",
|
||||
"searchPlaceholder": "Cerca output tra le sessioni...",
|
||||
"searchResults": "{{count}} corrispondenze",
|
||||
"searchFailed": "Ricerca non riuscita",
|
||||
"selectPaneHint": "Seleziona un pannello per visualizzare l'anteprima dell'output",
|
||||
"closePreview": "Chiudi anteprima",
|
||||
"newSession": "Nuova sessione",
|
||||
"newSessionHint": "Nome sessione (lettere, cifre, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "Crea",
|
||||
"sessionCreated": "Sessione \"{{name}}\" creata",
|
||||
"sessionCreateFailed": "Creazione della sessione non riuscita",
|
||||
"splitRight": "Dividi a destra",
|
||||
"splitDown": "Dividi in basso",
|
||||
"splitFailed": "Divisione del pannello non riuscita",
|
||||
"newWindow": "Nuova finestra",
|
||||
"windowCreateFailed": "Creazione della finestra non riuscita",
|
||||
"attachSessionTooltip": "Connetti a {{session}}",
|
||||
"refresh": "Aggiorna",
|
||||
"refreshFailed": "Aggiornamento delle sessioni non riuscito",
|
||||
"collapseAll": "Comprimi tutto",
|
||||
"expandAll": "Espandi tutto",
|
||||
"moreActions": "Altre azioni",
|
||||
"sessionStats": "Statistiche sessione",
|
||||
"renameSessionTitle": "Rinomina sessione \"{{name}}\"",
|
||||
"rename": "Rinomina",
|
||||
"sessionRenamed": "Sessione rinominata in \"{{name}}\"",
|
||||
"sessionRenameFailed": "Rinomina della sessione non riuscita",
|
||||
"editTagsTitle": "Modifica tag per \"{{name}}\"",
|
||||
"killPane": "Termina riquadro",
|
||||
"resizeTree": "Trascina per ridimensionare — doppio clic per ripristinare",
|
||||
"reattach": "Ricollega (risolve una visualizzazione alterata)",
|
||||
"killWindow": "Termina finestra",
|
||||
"killWindowTitle": "Terminare la finestra {{index}} di \"{{session}}\"?",
|
||||
"killWindowBody": "Tutti i riquadri e i processi in questa finestra verranno terminati. Terminare l'ultima finestra chiude la sessione.",
|
||||
"windowKillFailed": "Terminazione della finestra non riuscita",
|
||||
"statusActivity": "Attività",
|
||||
"statusWindows": "Finestre",
|
||||
"statusPanes": "riquadri",
|
||||
"statusTags": "Tag",
|
||||
"killPaneTitle": "Terminare il riquadro {{id}}?",
|
||||
"killPaneBody": "Il processo in esecuzione in questo riquadro verrà terminato. Terminare l'ultimo riquadro chiude la sua finestra.",
|
||||
"paneKillFailed": "Terminazione del riquadro non riuscita",
|
||||
"killSessionTitle": "Terminare la sessione \"{{name}}\"?",
|
||||
"killSessionBody": "Tutte le finestre e i processi in esecuzione in questa sessione verranno terminati. Questa operazione non può essere annullata.",
|
||||
"kill": "Termina",
|
||||
"sessionKilled": "Sessione \"{{name}}\" terminata",
|
||||
"sessionKillFailed": "Terminazione della sessione non riuscita",
|
||||
"hostUnreachable": "Impossibile connettersi all'host. Verifica che sia online e raggiungibile.",
|
||||
"noServer": "Nessun server tmux in esecuzione su questo host.",
|
||||
"searchTruncated": "Risultati parziali — la ricerca copre le ultime {{lines}} righe di ogni riquadro e al massimo {{panes}} riquadri.",
|
||||
"closeSearchResults": "Chiudi risultati ricerca",
|
||||
"retry": "Riprova",
|
||||
"noHosts": "Nessun host SSH disponibile",
|
||||
"noHostsHint": "Abilita l'opzione Monitor Tmux su un host SSH (Gestione host → scheda Terminale) per monitorare le sue sessioni tmux.",
|
||||
"tmuxInstallHint": "Installalo sull'host con:",
|
||||
"attachTooltip": "Apri un terminale su {{host}}",
|
||||
"attachTooltipPane": "Apri un terminale su {{host}} — sessione tmux {{session}}",
|
||||
"timeJustNow": "adesso",
|
||||
"timeMinutes": "{{count}}m fa",
|
||||
"timeHours": "{{count}}h fa",
|
||||
"timeDays": "{{count}}d fa"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriale",
|
||||
"portLabel": "Porta",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "タブ名を変更",
|
||||
"openFileManager": "ファイルマネージャーを開く",
|
||||
"dashboard": "ダッシュボード",
|
||||
"tmuxMonitor": "tmuxモニター",
|
||||
"homepage": "ホームページ",
|
||||
"quickConnect": "クイック接続",
|
||||
"sshTools": "SSHツール",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "最大キープアライブミス回数",
|
||||
"sameHost": "このホスト(直接トンネル)",
|
||||
"bindHost": "バインドホスト",
|
||||
"enableTmuxMonitor": "Tmuxモニターを有効にする",
|
||||
"enableTmuxMonitorDesc": "Tmuxモニターにこのホストを表示し、tmuxアクションをサイドバーに追加します。",
|
||||
"connectionToolbar": "接続ツールバー",
|
||||
"showConnectionToolbar": "接続ツールバーを表示する",
|
||||
"showConnectionToolbarDesc": "このホストのSSH、RDP、VNC、およびTelnetセッションでツールバーを表示します。ツールバー自体から一時的に非表示にすることもできます。",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "トンネルURLをコピーしました",
|
||||
"dockerUrlCopied": "Docker URLをコピーしました",
|
||||
"hostMetricsUrlCopied": "ホストメトリクスURLをコピーしました",
|
||||
"tmuxMonitorUrlCopied": "Tmux Monitor URL をコピーしました",
|
||||
"rdpUrlCopied": "RDP URL をコピーしました",
|
||||
"vncUrlCopied": "VNC URL をコピーしました",
|
||||
"telnetUrlCopied": "Telnet URL をコピーしました",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "トンネル URL をコピー",
|
||||
"copyDockerUrlAction": "Docker URL をコピー",
|
||||
"copyHostMetricsUrlAction": "ホストメトリクス URL をコピー",
|
||||
"copyTmuxMonitorUrlAction": "Tmux Monitor URL をコピー",
|
||||
"copyRdpUrlAction": "RDP URL をコピー",
|
||||
"copyVncUrlAction": "VNC URL をコピー",
|
||||
"copyTelnetUrlAction": "Telnet URL をコピー",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Proxmox統計を開く",
|
||||
"openTmuxMonitor": "Tmux Monitorを開く"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux モニター",
|
||||
"failedToLoadHosts": "ホストの読み込みに失敗しました",
|
||||
"failedToLoad": "Tmux セッションの読み込みに失敗しました",
|
||||
"tmuxUnavailable": "このホストに tmux がインストールされていません",
|
||||
"noSessions": "このホストに tmux セッションがありません",
|
||||
"noHostSelected": "ホストが選択されていません",
|
||||
"attached": "アタッチ済み",
|
||||
"detached": "デタッチ済み",
|
||||
"attach": "アタッチ",
|
||||
"editTags": "タグを編集",
|
||||
"tagsHint": "カンマ区切りのタグ (例: YOLO, lab, training)",
|
||||
"tagsSaved": "タグを保存しました",
|
||||
"tagsSaveFailed": "タグの保存に失敗しました",
|
||||
"searchPlaceholder": "セッション間で出力を検索...",
|
||||
"searchResults": "{{count}}件の一致",
|
||||
"searchFailed": "検索に失敗しました",
|
||||
"selectPaneHint": "出力をプレビューするには、ペインを選択してください",
|
||||
"closePreview": "プレビューを閉じる",
|
||||
"newSession": "新規セッション",
|
||||
"newSessionHint": "セッション名 (英数字, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "作成",
|
||||
"sessionCreated": "セッション \"{{name}}\" を作成しました",
|
||||
"sessionCreateFailed": "セッションの作成に失敗しました",
|
||||
"splitRight": "右に分割",
|
||||
"splitDown": "下に分割",
|
||||
"splitFailed": "ペインの分割に失敗しました",
|
||||
"newWindow": "新規ウィンドウ",
|
||||
"windowCreateFailed": "ウィンドウの作成に失敗しました",
|
||||
"attachSessionTooltip": "{{session}}にアタッチ",
|
||||
"refresh": "更新",
|
||||
"refreshFailed": "セッションの更新に失敗しました",
|
||||
"collapseAll": "すべて折りたたむ",
|
||||
"expandAll": "すべて展開",
|
||||
"moreActions": "その他の操作",
|
||||
"sessionStats": "セッション統計",
|
||||
"renameSessionTitle": "セッション \"{{name}}\" の名前変更",
|
||||
"rename": "名前変更",
|
||||
"sessionRenamed": "セッション名を \"{{name}}\" に変更しました",
|
||||
"sessionRenameFailed": "セッションの名前変更に失敗しました",
|
||||
"editTagsTitle": "\"{{name}}\" のタグを編集",
|
||||
"killPane": "ペインを終了",
|
||||
"resizeTree": "ドラッグでリサイズ — ダブルクリックでリセット",
|
||||
"reattach": "再接続 (表示の乱れを修正)",
|
||||
"killWindow": "ウィンドウを終了",
|
||||
"killWindowTitle": "ウィンドウ {{index}} (\"{{session}}\") を終了しますか?",
|
||||
"killWindowBody": "このウィンドウ内のすべてのペインとプロセスが終了されます。最後のウィンドウを終了するとセッションが終了します。",
|
||||
"windowKillFailed": "ウィンドウの終了に失敗しました",
|
||||
"statusActivity": "アクティビティ",
|
||||
"statusWindows": "ウィンドウ",
|
||||
"statusPanes": "ペイン",
|
||||
"statusTags": "タグ",
|
||||
"killPaneTitle": "ペイン {{id}} を終了しますか?",
|
||||
"killPaneBody": "このペインで実行中のプロセスが終了されます。最後のペインを終了するとウィンドウが閉じます。",
|
||||
"paneKillFailed": "ペインの終了に失敗しました",
|
||||
"killSessionTitle": "セッション \"{{name}}\" を終了しますか?",
|
||||
"killSessionBody": "このセッション内のすべてのウィンドウと実行中のプロセスが終了されます。この操作は取り消せません。",
|
||||
"kill": "終了",
|
||||
"sessionKilled": "セッション \"{{name}}\" を終了しました",
|
||||
"sessionKillFailed": "セッションの終了に失敗しました",
|
||||
"hostUnreachable": "ホストに接続できませんでした。オンラインで到達可能か確認してください。",
|
||||
"noServer": "このホストで tmux サーバーが実行されていません。",
|
||||
"searchTruncated": "一部の結果 — 各ペインの直近 {{lines}} 行と最大 {{panes}} ペインを検索対象としています。",
|
||||
"closeSearchResults": "検索結果を閉じる",
|
||||
"retry": "再試行",
|
||||
"noHosts": "利用可能な SSH ホストがありません",
|
||||
"noHostsHint": "SSH ホストで Tmux モニターオプションを有効にすると(ホストマネージャー → 端末タブ)、tmux セッションを監視できます。",
|
||||
"tmuxInstallHint": "ホストにインストールするには:",
|
||||
"attachTooltip": "{{host}} にターミナルを開く",
|
||||
"attachTooltipPane": "{{host}} にターミナルを開く — tmux セッション {{session}}",
|
||||
"timeJustNow": "たった今",
|
||||
"timeMinutes": "{{count}}分前",
|
||||
"timeHours": "{{count}}時間前",
|
||||
"timeDays": "{{count}}日前"
|
||||
},
|
||||
"serial": {
|
||||
"title": "シリアル",
|
||||
"portLabel": "ポート",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "탭 이름 변경",
|
||||
"openFileManager": "파일 관리자 열기",
|
||||
"dashboard": "대시보드",
|
||||
"tmuxMonitor": "Tmux 모니터",
|
||||
"homepage": "홈페이지",
|
||||
"quickConnect": "빠른 연결",
|
||||
"sshTools": "SSH 도구",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "최대 연결 유지 미스",
|
||||
"sameHost": "이 호스트 (직접 터널)",
|
||||
"bindHost": "바인드 호스트",
|
||||
"enableTmuxMonitor": "Tmux 모니터 활성화",
|
||||
"enableTmuxMonitorDesc": "Tmux 모니터에 이 호스트를 표시하고 tmux 작업을 사이드바에 추가합니다.",
|
||||
"connectionToolbar": "연결 도구 모음",
|
||||
"showConnectionToolbar": "연결 도구 모음 표시",
|
||||
"showConnectionToolbarDesc": "이 호스트의 SSH, RDP, VNC 및 Telnet 세션에 툴바를 표시합니다. 툴바 자체에서 툴바를 일시적으로 숨길 수도 있습니다.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "터널 URL 복사됨",
|
||||
"dockerUrlCopied": "Docker URL 복사됨",
|
||||
"hostMetricsUrlCopied": "호스트 메트릭 URL 복사됨",
|
||||
"tmuxMonitorUrlCopied": "Tmux 모니터 URL 복사됨",
|
||||
"rdpUrlCopied": "RDP URL 복사됨",
|
||||
"vncUrlCopied": "VNC URL 복사됨",
|
||||
"telnetUrlCopied": "Telnet URL 복사됨",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "터널 URL 복사",
|
||||
"copyDockerUrlAction": "Docker URL 복사",
|
||||
"copyHostMetricsUrlAction": "호스트 메트릭 URL 복사",
|
||||
"copyTmuxMonitorUrlAction": "Tmux 모니터 URL 복사",
|
||||
"copyRdpUrlAction": "RDP URL 복사",
|
||||
"copyVncUrlAction": "VNC URL 복사",
|
||||
"copyTelnetUrlAction": "Telnet URL 복사",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Proxmox 통계 열기",
|
||||
"openTmuxMonitor": "Tmux 모니터 열기"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux 모니터",
|
||||
"failedToLoadHosts": "호스트 목록을 불러오지 못했습니다",
|
||||
"failedToLoad": "tmux 세션을 불러오지 못했습니다",
|
||||
"tmuxUnavailable": "이 호스트에 tmux가 설치되어 있지 않습니다",
|
||||
"noSessions": "이 호스트에 tmux 세션이 없습니다",
|
||||
"noHostSelected": "선택된 호스트가 없습니다",
|
||||
"attached": "연결됨",
|
||||
"detached": "분리됨",
|
||||
"attach": "연결",
|
||||
"editTags": "태그 편집",
|
||||
"tagsHint": "쉼표로 구분된 태그 (예: YOLO, lab, training)",
|
||||
"tagsSaved": "태그 저장됨",
|
||||
"tagsSaveFailed": "태그 저장 실패",
|
||||
"searchPlaceholder": "세션 전체에서 출력 검색...",
|
||||
"searchResults": "{{count}}개 일치",
|
||||
"searchFailed": "검색 실패",
|
||||
"selectPaneHint": "출력을 미리 보려면 패널을 선택하세요",
|
||||
"closePreview": "미리보기 닫기",
|
||||
"newSession": "새 세션",
|
||||
"newSessionHint": "세션 이름 (영문자, 숫자, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "생성",
|
||||
"sessionCreated": "세션 \"{{name}}\" 생성됨",
|
||||
"sessionCreateFailed": "세션 생성 실패",
|
||||
"splitRight": "오른쪽으로 분할",
|
||||
"splitDown": "아래로 분할",
|
||||
"splitFailed": "패널 분할 실패",
|
||||
"newWindow": "새 창",
|
||||
"windowCreateFailed": "창 생성 실패",
|
||||
"attachSessionTooltip": "{{session}}에 연결",
|
||||
"refresh": "새로고침",
|
||||
"refreshFailed": "세션 새로고침 실패",
|
||||
"collapseAll": "모두 접기",
|
||||
"expandAll": "모두 펼치기",
|
||||
"moreActions": "더 보기",
|
||||
"sessionStats": "세션 통계",
|
||||
"renameSessionTitle": "세션 \"{{name}}\" 이름 바꾸기",
|
||||
"rename": "이름 바꾸기",
|
||||
"sessionRenamed": "세션 이름이 \"{{name}}\"(으)로 변경됨",
|
||||
"sessionRenameFailed": "세션 이름 변경 실패",
|
||||
"editTagsTitle": "\"{{name}}\"의 태그 편집",
|
||||
"killPane": "Pane 종료",
|
||||
"resizeTree": "드래그로 크기 조정 — 더블클릭하여 초기화",
|
||||
"reattach": "재연결 (화면 깨짐 복구)",
|
||||
"killWindow": "창 종료",
|
||||
"killWindowTitle": "{{session}}의 창 {{index}}을(를) 종료하시겠습니까?",
|
||||
"killWindowBody": "이 창의 모든 Pane과 프로세스가 종료됩니다. 마지막 창을 종료하면 세션이 끝납니다.",
|
||||
"windowKillFailed": "창 종료 실패",
|
||||
"statusActivity": "활동",
|
||||
"statusWindows": "Windows",
|
||||
"statusPanes": "Pane",
|
||||
"statusTags": "태그",
|
||||
"killPaneTitle": "Pane {{id}}을(를) 종료하시겠습니까?",
|
||||
"killPaneBody": "이 Pane에서 실행 중인 프로세스가 종료됩니다. 마지막 Pane을 종료하면 해당 창이 닫힙니다.",
|
||||
"paneKillFailed": "Pane 종료 실패",
|
||||
"killSessionTitle": "세션 \"{{name}}\"을(를) 종료하시겠습니까?",
|
||||
"killSessionBody": "이 세션의 모든 창과 실행 중인 프로세스가 종료됩니다. 이 작업은 취소할 수 없습니다.",
|
||||
"kill": "종료",
|
||||
"sessionKilled": "세션 \"{{name}}\"이(가) 종료되었습니다.",
|
||||
"sessionKillFailed": "세션 종료 실패",
|
||||
"hostUnreachable": "호스트에 연결할 수 없습니다. 온라인 상태이고 연결 가능한지 확인하세요.",
|
||||
"noServer": "이 호스트에서 tmux 서버가 실행 중이 아닙니다.",
|
||||
"searchTruncated": "일부 결과 — 각 Pane의 마지막 {{lines}}줄과 최대 {{panes}}개의 Pane을 검색합니다.",
|
||||
"closeSearchResults": "검색 결과 닫기",
|
||||
"retry": "재시도",
|
||||
"noHosts": "사용 가능한 SSH 호스트가 없습니다.",
|
||||
"noHostsHint": "SSH 호스트에서 Tmux Monitor 옵션을 활성화하여 (호스트 관리자 → 터미널 탭) tmux 세션을 모니터링하세요.",
|
||||
"tmuxInstallHint": "호스트에 설치하려면:",
|
||||
"attachTooltip": "{{host}}에 터미널 열기",
|
||||
"attachTooltipPane": "{{host}}에 터미널 열기 — tmux 세션 {{session}}",
|
||||
"timeJustNow": "방금",
|
||||
"timeMinutes": "{{count}}분 전",
|
||||
"timeHours": "{{count}}시간 전",
|
||||
"timeDays": "{{count}}일 전"
|
||||
},
|
||||
"serial": {
|
||||
"title": "시리얼",
|
||||
"portLabel": "포트",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Tabblad hernoemen",
|
||||
"openFileManager": "Open bestandsbeheerder",
|
||||
"dashboard": "Dashboard",
|
||||
"tmuxMonitor": "Tmux Monitor",
|
||||
"homepage": "Startpagina",
|
||||
"quickConnect": "Snel verbinden",
|
||||
"sshTools": "SSH-tools",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Max Keepalive mist",
|
||||
"sameHost": "Deze host (directe tunnel)",
|
||||
"bindHost": "Bind Host",
|
||||
"enableTmuxMonitor": "Tmux Monitor inschakelen",
|
||||
"enableTmuxMonitorDesc": "Toon deze host in de Tmux Monitor en voeg de bijbehorende Tmux-acties toe aan de zijbalk.",
|
||||
"connectionToolbar": "Verbindingswerkbalk",
|
||||
"showConnectionToolbar": "Verbindingswerkbalk weergeven",
|
||||
"showConnectionToolbarDesc": "Toon de werkbalk in de SSH-, RDP-, VNC- en Telnet-sessies van deze host. Je kunt de werkbalk ook tijdelijk verbergen via de werkbalk zelf.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Tunnel-URL gekopieerd",
|
||||
"dockerUrlCopied": "Docker-URL gekopieerd",
|
||||
"hostMetricsUrlCopied": "URL van hoststatistieken gekopieerd",
|
||||
"tmuxMonitorUrlCopied": "Tmux Monitor URL gekopieerd",
|
||||
"rdpUrlCopied": "RDP-URL gekopieerd",
|
||||
"vncUrlCopied": "VNC-URL gekopieerd",
|
||||
"telnetUrlCopied": "Telnet-URL gekopieerd",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Kopieer de tunnel-URL",
|
||||
"copyDockerUrlAction": "Kopieer de Docker-URL",
|
||||
"copyHostMetricsUrlAction": "Kopieer de URL voor hoststatistieken",
|
||||
"copyTmuxMonitorUrlAction": "Kopieer de URL van Tmux Monitor",
|
||||
"copyRdpUrlAction": "Kopieer de RDP-URL",
|
||||
"copyVncUrlAction": "Kopieer de VNC-URL",
|
||||
"copyTelnetUrlAction": "Telnet-URL kopiëren",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Open Proxmox-statistieken",
|
||||
"openTmuxMonitor": "Open Tmux Monitor"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux Monitor",
|
||||
"failedToLoadHosts": "Het laden van hosts is mislukt.",
|
||||
"failedToLoad": "Het laden van tmux-sessies is mislukt.",
|
||||
"tmuxUnavailable": "Tmux is niet geïnstalleerd op deze host.",
|
||||
"noSessions": "Er zijn geen tmux-sessies op deze host.",
|
||||
"noHostSelected": "Geen host geselecteerd",
|
||||
"attached": "Bijgevoegd",
|
||||
"detached": "Losgemaakt",
|
||||
"attach": "Bijvoegen",
|
||||
"editTags": "Labels bewerken",
|
||||
"tagsHint": "Tags gescheiden door komma's (bijv. YOLO, lab, training)",
|
||||
"tagsSaved": "Tags opgeslagen",
|
||||
"tagsSaveFailed": "Het opslaan van tags is mislukt.",
|
||||
"searchPlaceholder": "Zoekresultaten over meerdere sessies heen...",
|
||||
"searchResults": "{{count}} overeenkomsten",
|
||||
"searchFailed": "Zoeken mislukt",
|
||||
"selectPaneHint": "Selecteer een venster om de uitvoer ervan te bekijken.",
|
||||
"closePreview": "Voorbeeld sluiten",
|
||||
"newSession": "Nieuwe sessie",
|
||||
"newSessionHint": "Sessienaam (letters, cijfers, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "mijn sessie",
|
||||
"create": "Creëren",
|
||||
"sessionCreated": "Sessie \"{{name}}\" aangemaakt",
|
||||
"sessionCreateFailed": "Het aanmaken van de sessie is mislukt.",
|
||||
"splitRight": "Rechts gesplitst",
|
||||
"splitDown": "Splitsen",
|
||||
"splitFailed": "Het splitsen van het venster is mislukt.",
|
||||
"newWindow": "Nieuw venster",
|
||||
"windowCreateFailed": "Het is niet gelukt om het venster te maken.",
|
||||
"attachSessionTooltip": "Bevestigen aan {{session}}",
|
||||
"refresh": "Vernieuwen",
|
||||
"refreshFailed": "Sessies konden niet worden vernieuwd.",
|
||||
"collapseAll": "Alles inklappen",
|
||||
"expandAll": "Alles uitvouwen",
|
||||
"moreActions": "Meer acties",
|
||||
"sessionStats": "Sessiestatistieken",
|
||||
"renameSessionTitle": "Sessie hernoemen \"{{name}}\"",
|
||||
"rename": "Naam wijzigen",
|
||||
"sessionRenamed": "Sessie hernoemd naar \"{{name}}\"",
|
||||
"sessionRenameFailed": "Het hernoemen van de sessie is mislukt.",
|
||||
"editTagsTitle": "Bewerk tags voor \"{{name}}\"",
|
||||
"killPane": "Doodvenster",
|
||||
"resizeTree": "Sleep om het formaat aan te passen — dubbelklik om terug te zetten naar de beginpositie",
|
||||
"reattach": "Opnieuw koppelen (corrigeert een vervormde weergave)",
|
||||
"killWindow": "Dood venster",
|
||||
"killWindowTitle": "Venster {{index}} van \"{{session}} \" verwijderen?",
|
||||
"killWindowBody": "Alle vensters en processen in dit venster worden beëindigd. Het sluiten van het laatste venster beëindigt de sessie.",
|
||||
"windowKillFailed": "Het is niet gelukt om het venster te sluiten.",
|
||||
"statusActivity": "Activiteit",
|
||||
"statusWindows": "Windows",
|
||||
"statusPanes": "ruiten",
|
||||
"statusTags": "Tags",
|
||||
"killPaneTitle": "Kill pane {{id}}?",
|
||||
"killPaneBody": "Het proces dat in dit venster wordt uitgevoerd, wordt beëindigd. Door het laatste venster te sluiten, wordt het bijbehorende venster gesloten.",
|
||||
"paneKillFailed": "Het is niet gelukt om het venster te sluiten.",
|
||||
"killSessionTitle": "Sessie beëindigen \"{{name}}\"?",
|
||||
"killSessionBody": "Alle vensters en actieve processen in deze sessie worden beëindigd. Dit kan niet ongedaan worden gemaakt.",
|
||||
"kill": "Doden",
|
||||
"sessionKilled": "Sessie \"{{name}}\" is beëindigd",
|
||||
"sessionKillFailed": "Sessie kon niet worden beëindigd",
|
||||
"hostUnreachable": "Er kon geen verbinding met de host worden gemaakt. Controleer of deze online en bereikbaar is.",
|
||||
"noServer": "Er draait geen tmux-server op deze host.",
|
||||
"searchTruncated": "Gedeeltelijke resultaten — de zoekopdracht omvat de laatste {{lines}} regels van elk venster en maximaal {{panes}} vensters.",
|
||||
"closeSearchResults": "Zoekresultaten sluiten",
|
||||
"retry": "Opnieuw proberen",
|
||||
"noHosts": "Geen SSH-hosts beschikbaar",
|
||||
"noHostsHint": "Schakel de Tmux Monitor-optie in op een SSH-host (Host Manager → Terminal-tabblad) om de tmux-sessies te bewaken.",
|
||||
"tmuxInstallHint": "Installeer het op de host met:",
|
||||
"attachTooltip": "Open een terminal naar {{host}}",
|
||||
"attachTooltipPane": "Open een terminal naar {{host}} — tmux-sessie {{session}}",
|
||||
"timeJustNow": "zojuist",
|
||||
"timeMinutes": "{{count}}m geleden",
|
||||
"timeHours": "{{count}}uur geleden",
|
||||
"timeDays": "{{count}}dagen geleden"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serieel",
|
||||
"portLabel": "Haven",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Gi nytt navn til fanen",
|
||||
"openFileManager": "Åpne filbehandleren",
|
||||
"dashboard": "Dashbord",
|
||||
"tmuxMonitor": "Tmux-skjerm",
|
||||
"homepage": "Hjemmeside",
|
||||
"quickConnect": "Hurtigtilkobling",
|
||||
"sshTools": "SSH-verktøy",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Max Keepalive bommer",
|
||||
"sameHost": "Denne verten (direkte tunnel)",
|
||||
"bindHost": "Bind vert",
|
||||
"enableTmuxMonitor": "Aktiver Tmux-monitor",
|
||||
"enableTmuxMonitorDesc": "Vis denne verten i Tmux-monitoren og legg til tmux-handlingene i sidefeltet",
|
||||
"connectionToolbar": "Tilkoblingsverktøylinje",
|
||||
"showConnectionToolbar": "Vis tilkoblingsverktøylinjen",
|
||||
"showConnectionToolbarDesc": "Vis verktøylinjen i denne vertens SSH-, RDP-, VNC- og Telnet-økter. Du kan også skjule den midlertidig fra selve verktøylinjen.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Tunnel-URL kopiert",
|
||||
"dockerUrlCopied": "Docker-URL kopiert",
|
||||
"hostMetricsUrlCopied": "URL-en for vertsmålinger er kopiert",
|
||||
"tmuxMonitorUrlCopied": "URL-adressen til Tmux-monitoren er kopiert",
|
||||
"rdpUrlCopied": "RDP-URL kopiert",
|
||||
"vncUrlCopied": "VNC-URL kopiert",
|
||||
"telnetUrlCopied": "Telnet-URL-en er kopiert",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Kopier tunnel-URL",
|
||||
"copyDockerUrlAction": "Kopier Docker-URL",
|
||||
"copyHostMetricsUrlAction": "Kopier URL-adressen for vertsmålinger",
|
||||
"copyTmuxMonitorUrlAction": "Kopier Tmux Monitor-URL",
|
||||
"copyRdpUrlAction": "Kopier RDP-URL",
|
||||
"copyVncUrlAction": "Kopier VNC-URL",
|
||||
"copyTelnetUrlAction": "Kopier Telnet-URL",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Åpne Proxmox-statistikk",
|
||||
"openTmuxMonitor": "Åpne Tmux-skjermen"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux-skjerm",
|
||||
"failedToLoadHosts": "Kunne ikke laste inn verter",
|
||||
"failedToLoad": "Kunne ikke laste inn tmux-økter",
|
||||
"tmuxUnavailable": "tmux er ikke installert på denne verten",
|
||||
"noSessions": "Ingen tmux-økter på denne verten",
|
||||
"noHostSelected": "Ingen vert valgt",
|
||||
"attached": "Vedlagt",
|
||||
"detached": "Frittstående",
|
||||
"attach": "Feste",
|
||||
"editTags": "Rediger tagger",
|
||||
"tagsHint": "Kommaseparerte tagger (f.eks. YOLO, lab, trening)",
|
||||
"tagsSaved": "Tagger lagret",
|
||||
"tagsSaveFailed": "Kunne ikke lagre taggene",
|
||||
"searchPlaceholder": "Søk i utdata på tvers av økter...",
|
||||
"searchResults": "{{count}} treff",
|
||||
"searchFailed": "Søket mislyktes",
|
||||
"selectPaneHint": "Velg en rute for å forhåndsvise resultatet",
|
||||
"closePreview": "Lukk forhåndsvisning",
|
||||
"newSession": "Ny økt",
|
||||
"newSessionHint": "Øktnavn (bokstaver, sifre, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "min-økt",
|
||||
"create": "Skape",
|
||||
"sessionCreated": "Økt «{{name}}» opprettet",
|
||||
"sessionCreateFailed": "Kunne ikke opprette økten",
|
||||
"splitRight": "Del høyre",
|
||||
"splitDown": "Del ned",
|
||||
"splitFailed": "Kunne ikke dele ruten",
|
||||
"newWindow": "Nytt vindu",
|
||||
"windowCreateFailed": "Kunne ikke opprette vinduet",
|
||||
"attachSessionTooltip": "Fest til {{session}}",
|
||||
"refresh": "Forfriske",
|
||||
"refreshFailed": "Kunne ikke oppdatere øktene",
|
||||
"collapseAll": "Skjul alle",
|
||||
"expandAll": "Utvid alle",
|
||||
"moreActions": "Flere handlinger",
|
||||
"sessionStats": "Øktstatistikk",
|
||||
"renameSessionTitle": "Gi økten nytt navn «{{name}}»",
|
||||
"rename": "Gi nytt navn",
|
||||
"sessionRenamed": "Økten har fått nytt navn til «{{name}}»",
|
||||
"sessionRenameFailed": "Kunne ikke gi økten nytt navn",
|
||||
"editTagsTitle": "Rediger tagger for «{{name}}»",
|
||||
"killPane": "Drep ruten",
|
||||
"resizeTree": "Dra for å endre størrelse – dobbeltklikk for å tilbakestille",
|
||||
"reattach": "Fest på nytt (fikser en forvrengt visning)",
|
||||
"killWindow": "Drep vinduet",
|
||||
"killWindowTitle": "Drep vinduet {{index}} av «{{session}}»?",
|
||||
"killWindowBody": "Alle ruter og prosesser i dette vinduet vil bli avsluttet. Hvis du avslutter det siste vinduet, avsluttes økten.",
|
||||
"windowKillFailed": "Klarte ikke å avslutte vinduet",
|
||||
"statusActivity": "Aktivitet",
|
||||
"statusWindows": "Vinduer",
|
||||
"statusPanes": "ruter",
|
||||
"statusTags": "Tagger",
|
||||
"killPaneTitle": "Drep ruten {{id}}?",
|
||||
"killPaneBody": "Prosessen som kjører i denne ruten vil bli avsluttet. Hvis du avslutter den siste ruten, lukkes vinduet.",
|
||||
"paneKillFailed": "Kunne ikke avslutte ruten",
|
||||
"killSessionTitle": "Avslutt økten «{{name}}»?",
|
||||
"killSessionBody": "Alle vinduer og kjørende prosesser i denne økten vil bli avsluttet. Dette kan ikke angres.",
|
||||
"kill": "Drepe",
|
||||
"sessionKilled": "Økten «{{name}}» ble drept",
|
||||
"sessionKillFailed": "Klarte ikke å avslutte økten",
|
||||
"hostUnreachable": "Kunne ikke koble til verten. Sjekk at den er online og tilgjengelig.",
|
||||
"noServer": "Ingen tmux-server kjører på denne verten.",
|
||||
"searchTruncated": "Delvise resultater — søket dekker de siste {{lines}} linjene i hver rute og maksimalt {{panes}} ruter.",
|
||||
"closeSearchResults": "Lukk søkeresultatene",
|
||||
"retry": "Prøv på nytt",
|
||||
"noHosts": "Ingen SSH-verter tilgjengelig",
|
||||
"noHostsHint": "Aktiver Tmux Monitor-alternativet på en SSH-vert (Vertsadministrator → Terminal-fanen) for å overvåke tmux-øktene.",
|
||||
"tmuxInstallHint": "Installer den på verten med:",
|
||||
"attachTooltip": "Åpne en terminal til {{host}}",
|
||||
"attachTooltipPane": "Åpne en terminal til {{host}} — tmux-økt {{session}}",
|
||||
"timeJustNow": "akkurat nå",
|
||||
"timeMinutes": "For {{count}}minutter siden",
|
||||
"timeHours": "for {{count}}t siden",
|
||||
"timeDays": "for {{count}}dager siden"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriell",
|
||||
"portLabel": "Havn",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Zmień nazwę karty",
|
||||
"openFileManager": "Otwórz menedżer plików",
|
||||
"dashboard": "Pulpit",
|
||||
"tmuxMonitor": "Monitor Tmux",
|
||||
"homepage": "Strona główna",
|
||||
"quickConnect": "Szybkie połączenie",
|
||||
"sshTools": "Narzędzia SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Maks. pominiętych keepalive",
|
||||
"sameHost": "Ten host (tunel bezpośredni)",
|
||||
"bindHost": "Host nasłuchu",
|
||||
"enableTmuxMonitor": "Włącz monitor Tmux",
|
||||
"enableTmuxMonitorDesc": "Pokaż tego hosta w monitorze Tmux i dodaj jego akcje tmux do paska bocznego",
|
||||
"connectionToolbar": "Pasek narzędzi połączenia",
|
||||
"showConnectionToolbar": "Pokaż pasek narzędzi połączenia",
|
||||
"showConnectionToolbarDesc": "Pokaż pasek narzędzi w sesjach SSH, RDP, VNC i Telnet tego hosta. Możesz go również tymczasowo ukryć na samym pasku narzędzi.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Skopiowano adres URL tunelu",
|
||||
"dockerUrlCopied": "Skopiowano adres URL Dockera",
|
||||
"hostMetricsUrlCopied": "Skopiowano adres URL metryk hosta",
|
||||
"tmuxMonitorUrlCopied": "Skopiowano adres URL monitora Tmux",
|
||||
"rdpUrlCopied": "Skopiowano adres URL RDP",
|
||||
"vncUrlCopied": "Skopiowano adres URL VNC",
|
||||
"telnetUrlCopied": "Skopiowano adres URL Telnet",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Kopiuj adres URL tunelu",
|
||||
"copyDockerUrlAction": "Kopiuj adres URL Dockera",
|
||||
"copyHostMetricsUrlAction": "Kopiuj adres URL metryk hosta",
|
||||
"copyTmuxMonitorUrlAction": "Kopiuj adres URL monitora Tmux",
|
||||
"copyRdpUrlAction": "Kopiuj adres URL RDP",
|
||||
"copyVncUrlAction": "Kopiuj adres URL VNC",
|
||||
"copyTelnetUrlAction": "Kopiuj adres URL Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Otwórz statystyki Proxmox",
|
||||
"openTmuxMonitor": "Otwórz monitor Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Monitor Tmux",
|
||||
"failedToLoadHosts": "Nie udało się załadować hostów",
|
||||
"failedToLoad": "Nie udało się załadować sesji tmux",
|
||||
"tmuxUnavailable": "tmux nie jest zainstalowany na tym hoście",
|
||||
"noSessions": "Brak sesji tmux na tym hoście",
|
||||
"noHostSelected": "Nie wybrano hosta",
|
||||
"attached": "Dołączona",
|
||||
"detached": "Odłączona",
|
||||
"attach": "Dołącz",
|
||||
"editTags": "Edytuj tagi",
|
||||
"tagsHint": "Tagi rozdzielone przecinkami (np. YOLO, laboratorium, szkolenie)",
|
||||
"tagsSaved": "Zapisano tagi",
|
||||
"tagsSaveFailed": "Nie udało się zapisać tagów",
|
||||
"searchPlaceholder": "Przeszukaj wyjście we wszystkich sesjach...",
|
||||
"searchResults": "{{count}} dopasowań",
|
||||
"searchFailed": "Wyszukiwanie nie powiodło się",
|
||||
"selectPaneHint": "Wybierz panel, aby zobaczyć podgląd jego wyjścia",
|
||||
"closePreview": "Zamknij podgląd",
|
||||
"newSession": "Nowa sesja",
|
||||
"newSessionHint": "Nazwa sesji (litery, cyfry, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "moja-sesja",
|
||||
"create": "Utwórz",
|
||||
"sessionCreated": "Utworzono sesję „{{name}}”",
|
||||
"sessionCreateFailed": "Nie udało się utworzyć sesji",
|
||||
"splitRight": "Podziel w prawo",
|
||||
"splitDown": "Podziel w dół",
|
||||
"splitFailed": "Nie udało się podzielić panelu",
|
||||
"newWindow": "Nowe okno",
|
||||
"windowCreateFailed": "Nie udało się utworzyć okna",
|
||||
"attachSessionTooltip": "Dołącz do {{session}}",
|
||||
"refresh": "Odśwież",
|
||||
"refreshFailed": "Nie udało się odświeżyć sesji",
|
||||
"collapseAll": "Zwiń wszystko",
|
||||
"expandAll": "Rozwiń wszystko",
|
||||
"moreActions": "Więcej akcji",
|
||||
"sessionStats": "Statystyki sesji",
|
||||
"renameSessionTitle": "Zmień nazwę sesji „{{name}}”",
|
||||
"rename": "Zmień nazwę",
|
||||
"sessionRenamed": "Zmieniono nazwę sesji na „{{name}}”",
|
||||
"sessionRenameFailed": "Nie udało się zmienić nazwy sesji",
|
||||
"editTagsTitle": "Edytuj tagi dla „{{name}}”",
|
||||
"killPane": "Zakończ panel",
|
||||
"resizeTree": "Przeciągnij, aby zmienić rozmiar; kliknij dwukrotnie, aby zresetować",
|
||||
"reattach": "Dołącz ponownie (naprawia zniekształcony widok)",
|
||||
"killWindow": "Zakończ okno",
|
||||
"killWindowTitle": "Zakończyć okno {{index}} sesji „{{session}}”?",
|
||||
"killWindowBody": "Wszystkie panele i procesy w tym oknie zostaną zakończone. Zakończenie ostatniego okna kończy sesję.",
|
||||
"windowKillFailed": "Nie udało się zakończyć okna",
|
||||
"statusActivity": "Aktywność",
|
||||
"statusWindows": "Okna",
|
||||
"statusPanes": "panele",
|
||||
"statusTags": "Tagi",
|
||||
"killPaneTitle": "Zakończyć panel {{id}}?",
|
||||
"killPaneBody": "Proces działający w tym panelu zostanie zakończony. Zakończenie ostatniego panelu zamyka jego okno.",
|
||||
"paneKillFailed": "Nie udało się zakończyć panelu",
|
||||
"killSessionTitle": "Zakończyć sesję „{{name}}”?",
|
||||
"killSessionBody": "Wszystkie okna i działające procesy w tej sesji zostaną zakończone. Tej operacji nie można cofnąć.",
|
||||
"kill": "Zakończ",
|
||||
"sessionKilled": "Sesja „{{name}}” została zakończona",
|
||||
"sessionKillFailed": "Nie udało się zakończyć sesji",
|
||||
"hostUnreachable": "Nie udało się połączyć z hostem. Sprawdź, czy jest on online i osiągalny.",
|
||||
"noServer": "Na tym hoście nie działa żaden serwer tmux.",
|
||||
"searchTruncated": "Wyniki częściowe: wyszukiwanie obejmuje ostatnie {{lines}} wierszy każdego panelu i maksymalnie {{panes}} paneli.",
|
||||
"closeSearchResults": "Zamknij wyniki wyszukiwania",
|
||||
"retry": "Ponów",
|
||||
"noHosts": "Brak dostępnych hostów SSH",
|
||||
"noHostsHint": "Włącz opcję Monitor Tmux na hoście SSH (Menedżer hostów → karta Terminal), aby monitorować jego sesje tmux.",
|
||||
"tmuxInstallHint": "Zainstaluj go na hoście za pomocą:",
|
||||
"attachTooltip": "Otwórz terminal do {{host}}",
|
||||
"attachTooltipPane": "Otwórz terminal do {{host}}, sesja tmux {{session}}",
|
||||
"timeJustNow": "przed chwilą",
|
||||
"timeMinutes": "{{count}} min temu",
|
||||
"timeHours": "{{count}} godz. temu",
|
||||
"timeDays": "{{count}} dni temu"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Port szeregowy",
|
||||
"portLabel": "Port",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Renomear guia",
|
||||
"openFileManager": "Abra o Gerenciador de Arquivos",
|
||||
"dashboard": "Painel",
|
||||
"tmuxMonitor": "Monitor Tmux",
|
||||
"homepage": "Página inicial",
|
||||
"quickConnect": "Conexão rápida",
|
||||
"sshTools": "Ferramentas SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Max Keepalive falha",
|
||||
"sameHost": "Este host (túnel direto)",
|
||||
"bindHost": "Vincular host",
|
||||
"enableTmuxMonitor": "Ativar monitor Tmux",
|
||||
"enableTmuxMonitorDesc": "Exiba este host no Monitor Tmux e adicione suas ações tmux à barra lateral.",
|
||||
"connectionToolbar": "Barra de ferramentas de conexão",
|
||||
"showConnectionToolbar": "Mostrar barra de ferramentas de conexão",
|
||||
"showConnectionToolbarDesc": "Exiba a barra de ferramentas nas sessões SSH, RDP, VNC e Telnet deste host. Você também pode ocultá-la temporariamente na própria barra de ferramentas.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "URL do túnel copiada",
|
||||
"dockerUrlCopied": "URL do Docker copiada",
|
||||
"hostMetricsUrlCopied": "URL do Host Metrics copiada",
|
||||
"tmuxMonitorUrlCopied": "URL do monitor Tmux copiada",
|
||||
"rdpUrlCopied": "URL RDP copiada",
|
||||
"vncUrlCopied": "URL VNC copiada",
|
||||
"telnetUrlCopied": "URL do Telnet copiada",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Copiar URL do túnel",
|
||||
"copyDockerUrlAction": "Copiar URL do Docker",
|
||||
"copyHostMetricsUrlAction": "Copiar URL de métricas do host",
|
||||
"copyTmuxMonitorUrlAction": "Copiar URL do Tmux Monitor",
|
||||
"copyRdpUrlAction": "Copiar URL RDP",
|
||||
"copyVncUrlAction": "Copiar URL do VNC",
|
||||
"copyTelnetUrlAction": "Copiar URL do Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Estatísticas do Open Proxmox",
|
||||
"openTmuxMonitor": "Abra o monitor Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Monitor Tmux",
|
||||
"failedToLoadHosts": "Falha ao carregar os hosts",
|
||||
"failedToLoad": "Falha ao carregar as sessões do tmux",
|
||||
"tmuxUnavailable": "O tmux não está instalado neste host.",
|
||||
"noSessions": "Não há sessões tmux neste host.",
|
||||
"noHostSelected": "Nenhum host selecionado",
|
||||
"attached": "Apegado",
|
||||
"detached": "Desapegado",
|
||||
"attach": "Anexar",
|
||||
"editTags": "Editar etiquetas",
|
||||
"tagsHint": "Etiquetas separadas por vírgulas (ex: YOLO, laboratório, treinamento)",
|
||||
"tagsSaved": "Etiquetas salvas",
|
||||
"tagsSaveFailed": "Falha ao salvar as tags",
|
||||
"searchPlaceholder": "Resultados da pesquisa em todas as sessões...",
|
||||
"searchResults": "{{count}} correspondências",
|
||||
"searchFailed": "A busca falhou",
|
||||
"selectPaneHint": "Selecione um painel para visualizar sua saída.",
|
||||
"closePreview": "Fechar pré-visualização",
|
||||
"newSession": "Nova sessão",
|
||||
"newSessionHint": "Nome da sessão (letras, dígitos, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "minha-sessão",
|
||||
"create": "Criar",
|
||||
"sessionCreated": "Sessão \"{{name}}\" criada",
|
||||
"sessionCreateFailed": "Falha ao criar sessão",
|
||||
"splitRight": "Dividir à direita",
|
||||
"splitDown": "Dividir em partes",
|
||||
"splitFailed": "Falha ao dividir o painel",
|
||||
"newWindow": "Nova janela",
|
||||
"windowCreateFailed": "Falha ao criar a janela",
|
||||
"attachSessionTooltip": "Anexe a {{session}}",
|
||||
"refresh": "Atualizar",
|
||||
"refreshFailed": "Falha ao atualizar as sessões",
|
||||
"collapseAll": "Recolher tudo",
|
||||
"expandAll": "Expandir tudo",
|
||||
"moreActions": "Mais ações",
|
||||
"sessionStats": "Estatísticas da sessão",
|
||||
"renameSessionTitle": "Renomear sessão \"{{name}}\"",
|
||||
"rename": "Renomear",
|
||||
"sessionRenamed": "Sessão renomeada para \"{{name}}\"",
|
||||
"sessionRenameFailed": "Falha ao renomear a sessão",
|
||||
"editTagsTitle": "Editar tags para \"{{name}}\"",
|
||||
"killPane": "Painel de eliminação",
|
||||
"resizeTree": "Arraste para redimensionar — clique duas vezes para redefinir",
|
||||
"reattach": "Reconectar (corrige uma visualização distorcida)",
|
||||
"killWindow": "Janela de eliminação",
|
||||
"killWindowTitle": "Fechar janela {{index}} de \"{{session}}\"?",
|
||||
"killWindowBody": "Todos os painéis e processos nesta janela serão encerrados. Fechar a última janela finaliza a sessão.",
|
||||
"windowKillFailed": "Falha ao fechar a janela",
|
||||
"statusActivity": "Atividade",
|
||||
"statusWindows": "Windows",
|
||||
"statusPanes": "painéis",
|
||||
"statusTags": "Etiquetas",
|
||||
"killPaneTitle": "Painel de encerramento {{id}}?",
|
||||
"killPaneBody": "O processo em execução neste painel será encerrado. Fechar o último painel fecha a janela correspondente.",
|
||||
"paneKillFailed": "Falha ao desligar o painel",
|
||||
"killSessionTitle": "Sessão de encerramento \"{{name}}\"?",
|
||||
"killSessionBody": "Todas as janelas e processos em execução nesta sessão serão encerrados. Esta ação é irreversível.",
|
||||
"kill": "Matar",
|
||||
"sessionKilled": "Sessão \"{{name}}\" encerrada",
|
||||
"sessionKillFailed": "Falha ao encerrar a sessão",
|
||||
"hostUnreachable": "Não foi possível conectar ao host. Verifique se ele está online e acessível.",
|
||||
"noServer": "Não há nenhum servidor tmux em execução neste host.",
|
||||
"searchTruncated": "Resultados parciais — a pesquisa abrange as últimas {{lines}} linhas de cada painel e no máximo {{panes}} painéis.",
|
||||
"closeSearchResults": "Fechar resultados da pesquisa",
|
||||
"retry": "Tentar novamente",
|
||||
"noHosts": "Não há hosts SSH disponíveis.",
|
||||
"noHostsHint": "Habilite a opção Monitor do Tmux em um host SSH (Gerenciador de Hosts → guia Terminal) para monitorar suas sessões do tmux.",
|
||||
"tmuxInstallHint": "Instale-o no host com:",
|
||||
"attachTooltip": "Abra um terminal para {{host}}",
|
||||
"attachTooltipPane": "Abra um terminal para {{host}} — sessão tmux {{session}}",
|
||||
"timeJustNow": "agora mesmo",
|
||||
"timeMinutes": "{{count}}m atrás",
|
||||
"timeHours": "{{count}}h atrás",
|
||||
"timeDays": "{{count}}há alguns dias"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Porta",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Renomear separador",
|
||||
"openFileManager": "Abrir Gestor de Ficheiros",
|
||||
"dashboard": "Painel de controlo",
|
||||
"tmuxMonitor": "Monitor Tmux",
|
||||
"homepage": "Página inicial",
|
||||
"quickConnect": "Ligação Rápida",
|
||||
"sshTools": "Ferramentas SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Máximo de falhas de Keepalive",
|
||||
"sameHost": "Este host (túnel direto)",
|
||||
"bindHost": "Host de Ligação",
|
||||
"enableTmuxMonitor": "Ativar Monitor Tmux",
|
||||
"enableTmuxMonitorDesc": "Mostrar este host no Monitor Tmux e adicionar as suas ações tmux à barra lateral",
|
||||
"connectionToolbar": "Barra de ferramentas de ligação",
|
||||
"showConnectionToolbar": "Mostrar barra de ferramentas de ligação",
|
||||
"showConnectionToolbarDesc": "Apresente a barra de ferramentas nas sessões SSH, RDP, VNC e Telnet deste host. Também pode ocultá-la temporariamente na própria barra de ferramentas.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "URL do túnel copiada",
|
||||
"dockerUrlCopied": "URL do Docker copiada",
|
||||
"hostMetricsUrlCopied": "URL das métricas do host copiada",
|
||||
"tmuxMonitorUrlCopied": "URL do Monitor Tmux copiado",
|
||||
"rdpUrlCopied": "URL RDP copiado",
|
||||
"vncUrlCopied": "URL VNC copiado",
|
||||
"telnetUrlCopied": "URL Telnet copiado",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Copiar URL do Túnel",
|
||||
"copyDockerUrlAction": "Copiar URL do Docker",
|
||||
"copyHostMetricsUrlAction": "Copiar URL das Métricas do host",
|
||||
"copyTmuxMonitorUrlAction": "Copiar URL do Monitor Tmux",
|
||||
"copyRdpUrlAction": "Copiar URL RDP",
|
||||
"copyVncUrlAction": "Copiar URL VNC",
|
||||
"copyTelnetUrlAction": "Copiar URL Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Estatísticas do Open Proxmox",
|
||||
"openTmuxMonitor": "Abra o monitor Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Monitor Tmux",
|
||||
"failedToLoadHosts": "Falha ao carregar anfitriões",
|
||||
"failedToLoad": "Falha ao carregar sessões tmux",
|
||||
"tmuxUnavailable": "tmux não está instalado neste anfitrião",
|
||||
"noSessions": "Sem sessões tmux neste anfitrião",
|
||||
"noHostSelected": "Nenhum anfitrião selecionado",
|
||||
"attached": "Anexado",
|
||||
"detached": "Desanexado",
|
||||
"attach": "Anexar",
|
||||
"editTags": "Editar etiquetas",
|
||||
"tagsHint": "Etiquetas separadas por vírgulas (ex: YOLO, lab, training)",
|
||||
"tagsSaved": "Etiquetas guardadas",
|
||||
"tagsSaveFailed": "Falha ao guardar etiquetas",
|
||||
"searchPlaceholder": "Pesquisar saída em todas as sessões...",
|
||||
"searchResults": "{{count}} correspondências",
|
||||
"searchFailed": "Pesquisa falhou",
|
||||
"selectPaneHint": "Selecione um painel para pré-visualizar a sua saída",
|
||||
"closePreview": "Fechar pré-visualização",
|
||||
"newSession": "Nova sessão",
|
||||
"newSessionHint": "Nome da sessão (letras, dígitos, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "Criar",
|
||||
"sessionCreated": "Sessão \"{{name}}\" criada",
|
||||
"sessionCreateFailed": "Falha ao criar sessão",
|
||||
"splitRight": "Dividir à direita",
|
||||
"splitDown": "Dividir para baixo",
|
||||
"splitFailed": "Falha ao dividir painel",
|
||||
"newWindow": "Nova janela",
|
||||
"windowCreateFailed": "Falha ao criar janela",
|
||||
"attachSessionTooltip": "Anexar a {{session}}",
|
||||
"refresh": "Atualizar",
|
||||
"refreshFailed": "Falha ao atualizar sessões",
|
||||
"collapseAll": "Recolher tudo",
|
||||
"expandAll": "Expandir tudo",
|
||||
"moreActions": "Mais ações",
|
||||
"sessionStats": "Estatísticas da sessão",
|
||||
"renameSessionTitle": "Renomear sessão \"{{name}}\"",
|
||||
"rename": "Renomear",
|
||||
"sessionRenamed": "Sessão renomeada para \"{{name}}\"",
|
||||
"sessionRenameFailed": "Falha ao renomear sessão",
|
||||
"editTagsTitle": "Editar etiquetas para \"{{name}}\"",
|
||||
"killPane": "Terminar painel",
|
||||
"resizeTree": "Arraste para redimensionar — clique duplo para repor",
|
||||
"reattach": "Reanexar (corrige uma visualização distorcida)",
|
||||
"killWindow": "Terminar janela",
|
||||
"killWindowTitle": "Terminar janela {{index}} de \"{{session}}\"?",
|
||||
"killWindowBody": "Todos os painéis e processos nesta janela serão terminados. Terminar a última janela encerra a sessão.",
|
||||
"windowKillFailed": "Falha ao terminar janela",
|
||||
"statusActivity": "Atividade",
|
||||
"statusWindows": "Janelas",
|
||||
"statusPanes": "painéis",
|
||||
"statusTags": "Etiquetas",
|
||||
"killPaneTitle": "Terminar painel {{id}}?",
|
||||
"killPaneBody": "O processo em execução neste painel será terminado. Terminar o último painel fecha a sua janela.",
|
||||
"paneKillFailed": "Falha ao terminar painel",
|
||||
"killSessionTitle": "Terminar sessão \"{{name}}\"?",
|
||||
"killSessionBody": "Todas as janelas e processos em execução nesta sessão serão terminados. Isto não pode ser desfeito.",
|
||||
"kill": "Terminar",
|
||||
"sessionKilled": "Sessão \"{{name}}\" terminada",
|
||||
"sessionKillFailed": "Falha ao terminar sessão",
|
||||
"hostUnreachable": "Não foi possível ligar ao anfitrião. Verifique se está online e acessível.",
|
||||
"noServer": "Não há nenhum servidor tmux em execução neste anfitrião.",
|
||||
"searchTruncated": "Resultados parciais — a pesquisa abrange as últimas {{lines}} linhas de cada painel e, no máximo, {{panes}} painéis.",
|
||||
"closeSearchResults": "Fechar resultados da pesquisa",
|
||||
"retry": "Tentar novamente",
|
||||
"noHosts": "Nenhum anfitrião SSH disponível",
|
||||
"noHostsHint": "Ative a opção Monitor Tmux num anfitrião SSH (Gestor de Anfitriões → separador Terminal) para monitorizar as suas sessões tmux.",
|
||||
"tmuxInstallHint": "Instale-o no anfitrião com:",
|
||||
"attachTooltip": "Abrir um terminal para {{host}}",
|
||||
"attachTooltipPane": "Abrir um terminal para {{host}} — sessão tmux {{session}}",
|
||||
"timeJustNow": "agora mesmo",
|
||||
"timeMinutes": "há {{count}} min",
|
||||
"timeHours": "há {{count}} h",
|
||||
"timeDays": "há {{count}} d"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Porta",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Redenumiți fila",
|
||||
"openFileManager": "Deschideți Managerul de fișiere",
|
||||
"dashboard": "Bord",
|
||||
"tmuxMonitor": "Monitorul Tmux",
|
||||
"homepage": "Pagina principală",
|
||||
"quickConnect": "Conectare rapidă",
|
||||
"sshTools": "Instrumente SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Ratări maxime de menținere în viață",
|
||||
"sameHost": "Această gazdă (tunel direct)",
|
||||
"bindHost": "Legați gazda",
|
||||
"enableTmuxMonitor": "Activează monitorul Tmux",
|
||||
"enableTmuxMonitorDesc": "Afișează această gazdă în Monitorul Tmux și adaugă acțiunile sale tmux în bara laterală",
|
||||
"connectionToolbar": "Bara de instrumente Conexiune",
|
||||
"showConnectionToolbar": "Afișează bara de instrumente pentru conexiuni",
|
||||
"showConnectionToolbarDesc": "Afișează bara de instrumente în sesiunile SSH, RDP, VNC și Telnet ale acestei gazde. De asemenea, o poți ascunde temporar din bara de instrumente.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Adresa URL a tunelului a fost copiată",
|
||||
"dockerUrlCopied": "Adresa URL a Dockerului a fost copiată",
|
||||
"hostMetricsUrlCopied": "Adresa URL a metricilor gazdă a fost copiată",
|
||||
"tmuxMonitorUrlCopied": "Adresa URL a monitorului Tmux a fost copiată",
|
||||
"rdpUrlCopied": "Adresa URL RDP copiată",
|
||||
"vncUrlCopied": "Adresa URL VNC copiată",
|
||||
"telnetUrlCopied": "Adresa URL Telnet copiată",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Copiați URL-ul tunelului",
|
||||
"copyDockerUrlAction": "Copiați adresa URL a Dockerului",
|
||||
"copyHostMetricsUrlAction": "Copiați adresa URL a metricilor gazdă",
|
||||
"copyTmuxMonitorUrlAction": "Copiați URL-ul monitorului Tmux",
|
||||
"copyRdpUrlAction": "Copiați adresa URL RDP",
|
||||
"copyVncUrlAction": "Copiați adresa URL VNC",
|
||||
"copyTelnetUrlAction": "Copiați adresa URL Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Deschideți statisticile Proxmox",
|
||||
"openTmuxMonitor": "Deschideți monitorul Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Monitorul Tmux",
|
||||
"failedToLoadHosts": "Nu s-au putut încărca gazdele",
|
||||
"failedToLoad": "Nu s-au putut încărca sesiunile tmux",
|
||||
"tmuxUnavailable": "tmux nu este instalat pe această gazdă",
|
||||
"noSessions": "Nicio sesiune tmux pe această gazdă",
|
||||
"noHostSelected": "Nicio gazdă selectată",
|
||||
"attached": "Ataşat",
|
||||
"detached": "Detașat",
|
||||
"attach": "Atașați",
|
||||
"editTags": "Editați etichetele",
|
||||
"tagsHint": "Etichete separate prin virgulă (de exemplu, YOLO, laborator, antrenament)",
|
||||
"tagsSaved": "Etichete salvate",
|
||||
"tagsSaveFailed": "Nu s-au putut salva etichetele",
|
||||
"searchPlaceholder": "Rezultatul căutării în sesiuni...",
|
||||
"searchResults": "{{count}} potriviri",
|
||||
"searchFailed": "Căutarea a eșuat",
|
||||
"selectPaneHint": "Selectați un panou pentru a previzualiza rezultatul acestuia",
|
||||
"closePreview": "Închide previzualizarea",
|
||||
"newSession": "Sesiune nouă",
|
||||
"newSessionHint": "Numele sesiunii (litere, cifre, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "sesiunea mea",
|
||||
"create": "Crea",
|
||||
"sessionCreated": "Sesiunea „{{name}}” a fost creată",
|
||||
"sessionCreateFailed": "Nu s-a putut crea sesiunea",
|
||||
"splitRight": "Împărțire la dreapta",
|
||||
"splitDown": "Împărțiți",
|
||||
"splitFailed": "Nu s-a putut diviza panoul",
|
||||
"newWindow": "Fereastră nouă",
|
||||
"windowCreateFailed": "Nu s-a putut crea fereastra",
|
||||
"attachSessionTooltip": "Atașați la {{session}}",
|
||||
"refresh": "Reîmprospăta",
|
||||
"refreshFailed": "Nu s-au putut actualiza sesiunile",
|
||||
"collapseAll": "Restrângeți tot",
|
||||
"expandAll": "Extindeți tot",
|
||||
"moreActions": "Mai multe acțiuni",
|
||||
"sessionStats": "Statistici ale sesiunii",
|
||||
"renameSessionTitle": "Redenumiți sesiunea „{{name}}”",
|
||||
"rename": "Redenumire",
|
||||
"sessionRenamed": "Sesiunea a fost redenumită „{{name}}”",
|
||||
"sessionRenameFailed": "Nu s-a putut redenumi sesiunea",
|
||||
"editTagsTitle": "Editați etichetele pentru „{{name}}”",
|
||||
"killPane": "Panou de închidere",
|
||||
"resizeTree": "Trageți pentru redimensionare — faceți dublu clic pentru resetare",
|
||||
"reattach": "Reatașare (corectează o vizualizare deformată)",
|
||||
"killWindow": "Fereastră de închidere",
|
||||
"killWindowTitle": "Fereastră de închidere {{index}} a ferestrei „{{session}}”?",
|
||||
"killWindowBody": "Fiecare panou și proces din această fereastră va fi terminat. Închiderea ultimei ferestre încheie sesiunea.",
|
||||
"windowKillFailed": "Nu s-a putut închide fereastra",
|
||||
"statusActivity": "Activitate",
|
||||
"statusWindows": "Ferestre",
|
||||
"statusPanes": "geamuri",
|
||||
"statusTags": "Etichete",
|
||||
"killPaneTitle": "Panou de închidere {{id}}?",
|
||||
"killPaneBody": "Procesul care rulează în acest panou va fi terminat. Închiderea ultimului panou îi închide fereastra.",
|
||||
"paneKillFailed": "Nu s-a putut închide panoul",
|
||||
"killSessionTitle": "Închide sesiunea „{{name}}”?",
|
||||
"killSessionBody": "Toate ferestrele și procesele care rulează în această sesiune vor fi terminate. Această acțiune nu poate fi anulată.",
|
||||
"kill": "Ucide",
|
||||
"sessionKilled": "Sesiunea „{{name}}” a fost închisă",
|
||||
"sessionKillFailed": "Sesiunea nu a putut fi închisă",
|
||||
"hostUnreachable": "Nu s-a putut conecta la gazdă. Verificați dacă este online și accesibilă.",
|
||||
"noServer": "Niciun server tmux nu rulează pe această gazdă.",
|
||||
"searchTruncated": "Rezultate parțiale — căutarea acoperă ultimele {{lines}} linii ale fiecărui panou și cel mult {{panes}} panouri.",
|
||||
"closeSearchResults": "Închide rezultatele căutării",
|
||||
"retry": "Reîncercați",
|
||||
"noHosts": "Nu sunt disponibile gazde SSH",
|
||||
"noHostsHint": "Activați opțiunea Monitor Tmux pe o gazdă SSH (fila Manager gazdă → Terminal) pentru a monitoriza sesiunile tmux ale acesteia.",
|
||||
"tmuxInstallHint": "Instalați-l pe gazdă cu:",
|
||||
"attachTooltip": "Deschideți un terminal cu {{host}}",
|
||||
"attachTooltipPane": "Deschideți un terminal cu {{host}} — sesiune tmux {{session}}",
|
||||
"timeJustNow": "chiar acum",
|
||||
"timeMinutes": "{{count}}acum câteva minute",
|
||||
"timeHours": "{{count}}acum o oră",
|
||||
"timeDays": "{{count}}zile în urmă"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Port",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Переименовать вкладку",
|
||||
"openFileManager": "Открыть файловый менеджер",
|
||||
"dashboard": "Панель мониторинга",
|
||||
"tmuxMonitor": "Монитор Tmux",
|
||||
"homepage": "Главная",
|
||||
"quickConnect": "Быстрое подключение",
|
||||
"sshTools": "Инструменты SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Макс. пропусков keepalive",
|
||||
"sameHost": "Этот хост (прямой туннель)",
|
||||
"bindHost": "Хост для привязки",
|
||||
"enableTmuxMonitor": "Включить монитор Tmux",
|
||||
"enableTmuxMonitorDesc": "Показывать этот хост в Мониторе Tmux и добавлять действия tmux в боковую панель",
|
||||
"connectionToolbar": "Панель инструментов подключения",
|
||||
"showConnectionToolbar": "Показать панель инструментов подключения",
|
||||
"showConnectionToolbarDesc": "Отображать панель инструментов в сеансах SSH, RDP, VNC и Telnet этого хоста. Также можно временно скрыть её с самой панели инструментов.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "URL туннеля скопирован",
|
||||
"dockerUrlCopied": "URL Docker скопирован",
|
||||
"hostMetricsUrlCopied": "URL метрик хоста скопирован",
|
||||
"tmuxMonitorUrlCopied": "Скопирован URL Tmux Monitor",
|
||||
"rdpUrlCopied": "Скопирован URL RDP",
|
||||
"vncUrlCopied": "Скопирован URL VNC",
|
||||
"telnetUrlCopied": "Скопирован URL Telnet",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Копировать URL туннеля",
|
||||
"copyDockerUrlAction": "Копировать URL Docker",
|
||||
"copyHostMetricsUrlAction": "Копировать URL метрик хоста",
|
||||
"copyTmuxMonitorUrlAction": "Копировать URL Tmux Monitor",
|
||||
"copyRdpUrlAction": "Копировать URL RDP",
|
||||
"copyVncUrlAction": "Копировать URL VNC",
|
||||
"copyTelnetUrlAction": "Копировать URL Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Откройте статистику Proxmox",
|
||||
"openTmuxMonitor": "Открыть монитор Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Монитор Tmux",
|
||||
"failedToLoadHosts": "Ошибка загрузки хостов",
|
||||
"failedToLoad": "Ошибка загрузки сессий tmux",
|
||||
"tmuxUnavailable": "tmux не установлен на этом хосте",
|
||||
"noSessions": "Нет сессий tmux на этом хосте",
|
||||
"noHostSelected": "Хост не выбран",
|
||||
"attached": "Присоединён",
|
||||
"detached": "Отсоединён",
|
||||
"attach": "Присоединить",
|
||||
"editTags": "Изменить теги",
|
||||
"tagsHint": "Теги через запятую (например, YOLO, lab, training)",
|
||||
"tagsSaved": "Теги сохранены",
|
||||
"tagsSaveFailed": "Не удалось сохранить теги",
|
||||
"searchPlaceholder": "Поиск вывода по сессиям...",
|
||||
"searchResults": "{{count}} совпадений",
|
||||
"searchFailed": "Поиск не удался",
|
||||
"selectPaneHint": "Выберите панель для предпросмотра её вывода",
|
||||
"closePreview": "Закрыть предпросмотр",
|
||||
"newSession": "Новая сессия",
|
||||
"newSessionHint": "Имя сессии (буквы, цифры, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "Создать",
|
||||
"sessionCreated": "Сессия \"{{name}}\" создана",
|
||||
"sessionCreateFailed": "Не удалось создать сессию",
|
||||
"splitRight": "Разделить вправо",
|
||||
"splitDown": "Разделить вниз",
|
||||
"splitFailed": "Не удалось разделить панель",
|
||||
"newWindow": "Новое окно",
|
||||
"windowCreateFailed": "Не удалось создать окно",
|
||||
"attachSessionTooltip": "Присоединиться к {{session}}",
|
||||
"refresh": "Обновить",
|
||||
"refreshFailed": "Не удалось обновить сессии",
|
||||
"collapseAll": "Свернуть всё",
|
||||
"expandAll": "Развернуть всё",
|
||||
"moreActions": "Дополнительные действия",
|
||||
"sessionStats": "Статистика сессии",
|
||||
"renameSessionTitle": "Переименовать сессию \"{{name}}\"",
|
||||
"rename": "Переименовать",
|
||||
"sessionRenamed": "Сессия переименована в \"{{name}}\"",
|
||||
"sessionRenameFailed": "Не удалось переименовать сессию",
|
||||
"editTagsTitle": "Изменить теги для \"{{name}}\"",
|
||||
"killPane": "Завершить панель",
|
||||
"resizeTree": "Перетащите, чтобы изменить размер — двойной клик для сброса",
|
||||
"reattach": "Переподключиться (исправляет искажённое отображение)",
|
||||
"killWindow": "Завершить окно",
|
||||
"killWindowTitle": "Завершить окно {{index}} сессии \"{{session}}\"?",
|
||||
"killWindowBody": "Все панели и процессы в этом окне будут завершены. Завершение последнего окна завершит сессию.",
|
||||
"windowKillFailed": "Не удалось завершить окно",
|
||||
"statusActivity": "Активность",
|
||||
"statusWindows": "Окна",
|
||||
"statusPanes": "панели",
|
||||
"statusTags": "Теги",
|
||||
"killPaneTitle": "Завершить панель {{id}}?",
|
||||
"killPaneBody": "Процесс, запущенный в этой панели, будет завершён. Завершение последней панели закроет её окно.",
|
||||
"paneKillFailed": "Не удалось завершить панель",
|
||||
"killSessionTitle": "Завершить сессию \"{{name}}\"?",
|
||||
"killSessionBody": "Все окна и запущенные процессы в этой сессии будут завершены. Это действие нельзя отменить.",
|
||||
"kill": "Завершить",
|
||||
"sessionKilled": "Сессия \"{{name}}\" завершена",
|
||||
"sessionKillFailed": "Не удалось завершить сессию",
|
||||
"hostUnreachable": "Не удалось подключиться к хосту. Проверьте, что он онлайн и доступен.",
|
||||
"noServer": "На этом хосте не запущен сервер tmux.",
|
||||
"searchTruncated": "Частичные результаты — поиск охватывает последние {{lines}} строк каждой панели и не более {{panes}} панелей.",
|
||||
"closeSearchResults": "Закрыть результаты поиска",
|
||||
"retry": "Повторить",
|
||||
"noHosts": "Нет доступных SSH-хостов",
|
||||
"noHostsHint": "Включите опцию Tmux Monitor для SSH-хоста (Host Manager → вкладка Terminal), чтобы отслеживать его tmux-сессии.",
|
||||
"tmuxInstallHint": "Установите его на хост с помощью:",
|
||||
"attachTooltip": "Открыть терминал на {{host}}",
|
||||
"attachTooltipPane": "Открыть терминал на {{host}} — tmux-сессия {{session}}",
|
||||
"timeJustNow": "только что",
|
||||
"timeMinutes": "{{count}}м назад",
|
||||
"timeHours": "{{count}}ч назад",
|
||||
"timeDays": "{{count}}д назад"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Последовательный порт",
|
||||
"portLabel": "Порт",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Преименуј картицу",
|
||||
"openFileManager": "Отвори менаџер датотека",
|
||||
"dashboard": "Контролна табла",
|
||||
"tmuxMonitor": "Tmux монитор",
|
||||
"homepage": "Почетна страница",
|
||||
"quickConnect": "Брзо повезивање",
|
||||
"sshTools": "SSH алати",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Максимални промашаји у Keepalive-у",
|
||||
"sameHost": "Овај хост (директни тунел)",
|
||||
"bindHost": "Повежи хост",
|
||||
"enableTmuxMonitor": "Омогући Tmux монитор",
|
||||
"enableTmuxMonitorDesc": "Прикажи овај хост у Tmux монитору и додај његове tmux акције у бочну траку",
|
||||
"connectionToolbar": "Трака са алаткама за повезивање",
|
||||
"showConnectionToolbar": "Прикажи траку са алаткама за повезивање",
|
||||
"showConnectionToolbarDesc": "Прикажи траку са алаткама у SSH, RDP, VNC и Telnet сесијама овог хоста. Такође је можете привремено сакрити са саме траке са алаткама.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "URL тунела је копиран",
|
||||
"dockerUrlCopied": "URL адреса Докера је копирана",
|
||||
"hostMetricsUrlCopied": "URL метрика хоста је копиран",
|
||||
"tmuxMonitorUrlCopied": "URL адреса Tmux монитора је копирана",
|
||||
"rdpUrlCopied": "RDP URL је копиран",
|
||||
"vncUrlCopied": "VNC URL је копиран",
|
||||
"telnetUrlCopied": "УРЛ адреса Телнета је копирана",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Копирај URL тунела",
|
||||
"copyDockerUrlAction": "Копирај Докер УРЛ",
|
||||
"copyHostMetricsUrlAction": "Копирај URL метрика хоста",
|
||||
"copyTmuxMonitorUrlAction": "Копирај УРЛ адресу Tmux монитора",
|
||||
"copyRdpUrlAction": "Копирај RDP URL",
|
||||
"copyVncUrlAction": "Копирај VNC URL",
|
||||
"copyTelnetUrlAction": "Копирај Telnet URL",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Отвори Proxmox статистику",
|
||||
"openTmuxMonitor": "Отвори Tmux монитор"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux монитор",
|
||||
"failedToLoadHosts": "Учитавање хостова није успело",
|
||||
"failedToLoad": "Учитавање tmux сесија није успело",
|
||||
"tmuxUnavailable": "tmux није инсталиран на овом хосту",
|
||||
"noSessions": "Нема tmux сесија на овом хосту",
|
||||
"noHostSelected": "Није изабран домаћин",
|
||||
"attached": "Приложено",
|
||||
"detached": "Одвојено",
|
||||
"attach": "Приложи",
|
||||
"editTags": "Уреди ознаке",
|
||||
"tagsHint": "Ознаке одвојене зарезима (нпр. YOLO, лабораторија, обука)",
|
||||
"tagsSaved": "Ознаке сачуване",
|
||||
"tagsSaveFailed": "Чување ознака није успело",
|
||||
"searchPlaceholder": "Претрага резултата у свим сесијама...",
|
||||
"searchResults": "{{count}} подударања",
|
||||
"searchFailed": "Претрага није успела",
|
||||
"selectPaneHint": "Изаберите окно да бисте прегледали његов резултат",
|
||||
"closePreview": "Затвори преглед",
|
||||
"newSession": "Нова сесија",
|
||||
"newSessionHint": "Назив сесије (слова, цифре, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "моја-сесија",
|
||||
"create": "Креирај",
|
||||
"sessionCreated": "Сесија „{{name}}“ је креирана",
|
||||
"sessionCreateFailed": "Није успело креирање сесије",
|
||||
"splitRight": "Подели десно",
|
||||
"splitDown": "Подели",
|
||||
"splitFailed": "Подела окна није успела",
|
||||
"newWindow": "Нови прозор",
|
||||
"windowCreateFailed": "Није успело креирање прозора",
|
||||
"attachSessionTooltip": "Приложи уз {{session}}",
|
||||
"refresh": "Освежи",
|
||||
"refreshFailed": "Освежавање сесија није успело",
|
||||
"collapseAll": "Скупи све",
|
||||
"expandAll": "Прошири све",
|
||||
"moreActions": "Више акција",
|
||||
"sessionStats": "Статистика сесије",
|
||||
"renameSessionTitle": "Преименуј сесију „{{name}}“",
|
||||
"rename": "Преименуј",
|
||||
"sessionRenamed": "Сесија је преименована у „{{name}}“",
|
||||
"sessionRenameFailed": "Преименовање сесије није успело",
|
||||
"editTagsTitle": "Уреди ознаке за „{{name}}“",
|
||||
"killPane": "Окно за заустављање",
|
||||
"resizeTree": "Превуците да бисте променили величину — двапут кликните да бисте ресетовали",
|
||||
"reattach": "Поново причврстите (поправља искривљени приказ)",
|
||||
"killWindow": "Затвори прозор",
|
||||
"killWindowTitle": "Да ли да затворим прозор {{index}} од „{{session}}“?",
|
||||
"killWindowBody": "Сваки панел и процес у овом прозору биће завршени. Затварањем последњег прозора завршава се сесија.",
|
||||
"windowKillFailed": "Затварање прозора није успело",
|
||||
"statusActivity": "Активност",
|
||||
"statusWindows": "Прозори",
|
||||
"statusPanes": "окна",
|
||||
"statusTags": "Ознаке",
|
||||
"killPaneTitle": "Затвори окно {{id}}?",
|
||||
"killPaneBody": "Процес који се извршава у овом окну биће прекинут. Затварањем последњег окна затвара се његов прозор.",
|
||||
"paneKillFailed": "Није успело затварање окна",
|
||||
"killSessionTitle": "Укинути сесију „{{name}}“?",
|
||||
"killSessionBody": "Сви прозори и покренути процеси у овој сесији биће прекинути. Ова радња се не може поништити.",
|
||||
"kill": "Убити",
|
||||
"sessionKilled": "Сесија „{{name}}“ је завршена",
|
||||
"sessionKillFailed": "Завршавање сесије није успело",
|
||||
"hostUnreachable": "Није могуће повезати се са хостом. Проверите да ли је онлајн и доступан.",
|
||||
"noServer": "Ниједан tmux сервер не ради на овом хосту.",
|
||||
"searchTruncated": "Делимични резултати — претрага обухвата последњих {{lines}} редова сваког панела и највише {{panes}} панела.",
|
||||
"closeSearchResults": "Затвори резултате претраге",
|
||||
"retry": "Покушај поново",
|
||||
"noHosts": "Нема доступних SSH хостова",
|
||||
"noHostsHint": "Омогућите опцију Tmux Monitor на SSH хосту (Host Manager → картица Terminal) да бисте пратили његове tmux сесије.",
|
||||
"tmuxInstallHint": "Инсталирајте га на хост помоћу:",
|
||||
"attachTooltip": "Отворите терминал за {{host}}",
|
||||
"attachTooltipPane": "Отворите терминал за {{host}} — tmux сесију {{session}}",
|
||||
"timeJustNow": "управо сада",
|
||||
"timeMinutes": "{{count}}пре мин",
|
||||
"timeHours": "{{count}}пре сати",
|
||||
"timeDays": "{{count}}пре д"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Серијски",
|
||||
"portLabel": "Лука",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Byt namn på fliken",
|
||||
"openFileManager": "Öppna filhanteraren",
|
||||
"dashboard": "Instrumentpanel",
|
||||
"tmuxMonitor": "Tmux-monitor",
|
||||
"homepage": "Hemsida",
|
||||
"quickConnect": "Snabbanslutning",
|
||||
"sshTools": "SSH-verktyg",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Max Keepalive missar",
|
||||
"sameHost": "Denna värd (direkt tunnel)",
|
||||
"bindHost": "Bind värd",
|
||||
"enableTmuxMonitor": "Aktivera Tmux-monitorn",
|
||||
"enableTmuxMonitorDesc": "Visa denna värd i Tmux Monitor och lägg till dess tmux-åtgärder i sidofältet",
|
||||
"connectionToolbar": "Anslutningsverktygsfält",
|
||||
"showConnectionToolbar": "Visa anslutningsverktygsfältet",
|
||||
"showConnectionToolbarDesc": "Visa verktygsfältet i den här värdens SSH-, RDP-, VNC- och Telnet-sessioner. Du kan också dölja det tillfälligt från själva verktygsfältet.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Tunnel-URL kopierad",
|
||||
"dockerUrlCopied": "Docker-URL kopierad",
|
||||
"hostMetricsUrlCopied": "URL för värdstatistik kopierad",
|
||||
"tmuxMonitorUrlCopied": "Tmux Monitor-URL kopierad",
|
||||
"rdpUrlCopied": "RDP-URL kopierad",
|
||||
"vncUrlCopied": "VNC-URL kopierad",
|
||||
"telnetUrlCopied": "Telnet-URL kopierad",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Kopiera tunnel-URL",
|
||||
"copyDockerUrlAction": "Kopiera Docker-URL",
|
||||
"copyHostMetricsUrlAction": "Kopiera URL för värdstatistik",
|
||||
"copyTmuxMonitorUrlAction": "Kopiera Tmux Monitor-URL",
|
||||
"copyRdpUrlAction": "Kopiera RDP-URL",
|
||||
"copyVncUrlAction": "Kopiera VNC-URL",
|
||||
"copyTelnetUrlAction": "Kopiera Telnet-URL",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Öppna Proxmox-statistik",
|
||||
"openTmuxMonitor": "Öppna Tmux-monitorn"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux-monitor",
|
||||
"failedToLoadHosts": "Misslyckades med att ladda värdar",
|
||||
"failedToLoad": "Misslyckades med att ladda tmux-sessioner",
|
||||
"tmuxUnavailable": "tmux är inte installerat på den här värddatorn",
|
||||
"noSessions": "Inga tmux-sessioner på den här värden",
|
||||
"noHostSelected": "Ingen värd vald",
|
||||
"attached": "Bunden",
|
||||
"detached": "Fristående",
|
||||
"attach": "Bifoga",
|
||||
"editTags": "Redigera taggar",
|
||||
"tagsHint": "Kommaavgränsade taggar (t.ex. YOLO, lab, utbildning)",
|
||||
"tagsSaved": "Taggar sparade",
|
||||
"tagsSaveFailed": "Det gick inte att spara taggarna",
|
||||
"searchPlaceholder": "Sök utdata över flera sessioner...",
|
||||
"searchResults": "{{count}} matchningar",
|
||||
"searchFailed": "Sökningen misslyckades",
|
||||
"selectPaneHint": "Välj en ruta för att förhandsgranska dess utdata",
|
||||
"closePreview": "Stäng förhandsgranskningen",
|
||||
"newSession": "Ny session",
|
||||
"newSessionHint": "Sessionsnamn (bokstäver, siffror, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "min-session",
|
||||
"create": "Skapa",
|
||||
"sessionCreated": "Sessionen \"{{name}}\" skapades",
|
||||
"sessionCreateFailed": "Misslyckades med att skapa sessionen",
|
||||
"splitRight": "Dela höger",
|
||||
"splitDown": "Dela upp",
|
||||
"splitFailed": "Misslyckades med att dela rutan",
|
||||
"newWindow": "Nytt fönster",
|
||||
"windowCreateFailed": "Misslyckades med att skapa fönstret",
|
||||
"attachSessionTooltip": "Bifoga till {{session}}",
|
||||
"refresh": "Uppdatera",
|
||||
"refreshFailed": "Det gick inte att uppdatera sessionerna",
|
||||
"collapseAll": "Komprimera alla",
|
||||
"expandAll": "Expandera alla",
|
||||
"moreActions": "Fler åtgärder",
|
||||
"sessionStats": "Sessionsstatistik",
|
||||
"renameSessionTitle": "Byt namn på sessionen \"{{name}}\"",
|
||||
"rename": "Döpa om",
|
||||
"sessionRenamed": "Sessionen har bytt namn till \"{{name}}\"",
|
||||
"sessionRenameFailed": "Misslyckades med att byta namn på sessionen",
|
||||
"editTagsTitle": "Redigera taggar för \"{{name}}\"",
|
||||
"killPane": "Döda rutan",
|
||||
"resizeTree": "Dra för att ändra storlek — dubbelklicka för att återställa",
|
||||
"reattach": "Sätt tillbaka (åtgärdar en förvrängd bild)",
|
||||
"killWindow": "Döda fönstret",
|
||||
"killWindowTitle": "Avsluta fönstret {{index}} för \"{{session}}\"?",
|
||||
"killWindowBody": "Varje ruta och process i det här fönstret avslutas. Om du avslutar det sista fönstret avslutas sessionen.",
|
||||
"windowKillFailed": "Misslyckades med att avsluta fönstret",
|
||||
"statusActivity": "Aktivitet",
|
||||
"statusWindows": "Fönster",
|
||||
"statusPanes": "rutor",
|
||||
"statusTags": "Taggar",
|
||||
"killPaneTitle": "Avsluta rutan {{id}}?",
|
||||
"killPaneBody": "Processen som körs i den här rutan kommer att avslutas. Om du avslutar den sista rutan stängs dess fönster.",
|
||||
"paneKillFailed": "Misslyckades med att avsluta rutan",
|
||||
"killSessionTitle": "Avsluta sessionen \"{{name}}\"?",
|
||||
"killSessionBody": "Alla fönster och processer som körs i den här sessionen kommer att avslutas. Detta kan inte ångras.",
|
||||
"kill": "Döda",
|
||||
"sessionKilled": "Sessionen \"{{name}}\" avstängd",
|
||||
"sessionKillFailed": "Misslyckades med att avsluta sessionen",
|
||||
"hostUnreachable": "Kunde inte ansluta till värden. Kontrollera att den är online och nåbar.",
|
||||
"noServer": "Ingen tmux-server körs på den här värden.",
|
||||
"searchTruncated": "Delresultat — sökningen täcker de sista {{lines}} raderna i varje ruta och högst {{panes}} rutor.",
|
||||
"closeSearchResults": "Stäng sökresultaten",
|
||||
"retry": "Försöka igen",
|
||||
"noHosts": "Inga SSH-värdar tillgängliga",
|
||||
"noHostsHint": "Aktivera alternativet Tmux Monitor på en SSH-värd (Värdhanterare → Terminal-fliken) för att övervaka dess tmux-sessioner.",
|
||||
"tmuxInstallHint": "Installera det på värden med:",
|
||||
"attachTooltip": "Öppna en terminal till {{host}}",
|
||||
"attachTooltipPane": "Öppna en terminal till {{host}} — tmux-session {{session}}",
|
||||
"timeJustNow": "just nu",
|
||||
"timeMinutes": "för {{count}}minuter sedan",
|
||||
"timeHours": "för {{count}}timmar sedan",
|
||||
"timeDays": "för {{count}}dagar sedan"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serie",
|
||||
"portLabel": "Hamn",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "เปลี่ยนชื่อแท็บ",
|
||||
"openFileManager": "เปิดตัวจัดการไฟล์",
|
||||
"dashboard": "แดชบอร์ด",
|
||||
"tmuxMonitor": "มอนิเตอร์ Tmux",
|
||||
"homepage": "หน้าแรก",
|
||||
"quickConnect": "การเชื่อมต่อด่วน",
|
||||
"sshTools": "เครื่องมือ SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "แม็กซ์ คีทอะไลฟ์ พลาด",
|
||||
"sameHost": "โฮสต์นี้ (อุโมงค์ตรง)",
|
||||
"bindHost": "โฮสต์ผูก",
|
||||
"enableTmuxMonitor": "เปิดใช้งาน Tmux Monitor",
|
||||
"enableTmuxMonitorDesc": "แสดงโฮสต์นี้ใน Tmux Monitor และเพิ่มการดำเนินการ tmux ของโฮสต์นี้ลงในแถบด้านข้าง",
|
||||
"connectionToolbar": "แถบเครื่องมือการเชื่อมต่อ",
|
||||
"showConnectionToolbar": "แสดงแถบเครื่องมือการเชื่อมต่อ",
|
||||
"showConnectionToolbarDesc": "แสดงแถบเครื่องมือในเซสชัน SSH, RDP, VNC และ Telnet ของโฮสต์นี้ คุณยังสามารถซ่อนแถบเครื่องมือชั่วคราวได้จากแถบเครื่องมือโดยตรง",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "คัดลอก URL ของอุโมงค์แล้ว",
|
||||
"dockerUrlCopied": "คัดลอก URL ของ Docker แล้ว",
|
||||
"hostMetricsUrlCopied": "คัดลอก URL ของ Host Metrics แล้ว",
|
||||
"tmuxMonitorUrlCopied": "คัดลอก URL ของ Tmux Monitor แล้ว",
|
||||
"rdpUrlCopied": "คัดลอก URL ของ RDP แล้ว",
|
||||
"vncUrlCopied": "คัดลอก URL ของ VNC แล้ว",
|
||||
"telnetUrlCopied": "คัดลอก URL ของ Telnet แล้ว",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "คัดลอก URL อุโมงค์",
|
||||
"copyDockerUrlAction": "คัดลอก URL ของ Docker",
|
||||
"copyHostMetricsUrlAction": "คัดลอก URL ของ Host Metrics",
|
||||
"copyTmuxMonitorUrlAction": "คัดลอก URL ของ Tmux Monitor",
|
||||
"copyRdpUrlAction": "คัดลอก URL ของ RDP",
|
||||
"copyVncUrlAction": "คัดลอก URL ของ VNC",
|
||||
"copyTelnetUrlAction": "คัดลอก URL ของ Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "เปิดดูสถิติ Proxmox",
|
||||
"openTmuxMonitor": "เปิด Tmux Monitor"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "มอนิเตอร์ Tmux",
|
||||
"failedToLoadHosts": "ไม่สามารถโหลดโฮสต์ได้",
|
||||
"failedToLoad": "ไม่สามารถโหลดเซสชัน tmux ได้",
|
||||
"tmuxUnavailable": "tmux ไม่ได้ติดตั้งอยู่ในโฮสต์นี้",
|
||||
"noSessions": "ไม่มีเซสชัน tmux บนโฮสต์นี้",
|
||||
"noHostSelected": "ไม่ได้เลือกโฮสต์",
|
||||
"attached": "ที่แนบมา",
|
||||
"detached": "แยกออก",
|
||||
"attach": "แนบ",
|
||||
"editTags": "แก้ไขแท็ก",
|
||||
"tagsHint": "แท็กที่คั่นด้วยเครื่องหมายจุลภาค (เช่น YOLO, lab, training)",
|
||||
"tagsSaved": "แท็กถูกบันทึกแล้ว",
|
||||
"tagsSaveFailed": "ไม่สามารถบันทึกแท็กได้",
|
||||
"searchPlaceholder": "ผลการค้นหาข้ามเซสชัน...",
|
||||
"searchResults": "{{count}} ตรงกัน",
|
||||
"searchFailed": "การค้นหาล้มเหลว",
|
||||
"selectPaneHint": "เลือกบานหน้าต่างเพื่อดูตัวอย่างผลลัพธ์",
|
||||
"closePreview": "ปิดการแสดงตัวอย่าง",
|
||||
"newSession": "เซสชั่นใหม่",
|
||||
"newSessionHint": "ชื่อเซสชัน (ตัวอักษร ตัวเลข _ @ % + = -)",
|
||||
"newSessionPlaceholder": "เซสชั่นของฉัน",
|
||||
"create": "สร้าง",
|
||||
"sessionCreated": "เซสชัน \"{{name}}\" ถูกสร้างขึ้น",
|
||||
"sessionCreateFailed": "ไม่สามารถสร้างเซสชันได้",
|
||||
"splitRight": "แยกขวา",
|
||||
"splitDown": "แยกออก",
|
||||
"splitFailed": "ไม่สามารถแบ่งหน้าต่างได้",
|
||||
"newWindow": "หน้าต่างใหม่",
|
||||
"windowCreateFailed": "ไม่สามารถสร้างหน้าต่างได้",
|
||||
"attachSessionTooltip": "แนบกับ {{session}}",
|
||||
"refresh": "รีเฟรช",
|
||||
"refreshFailed": "ไม่สามารถรีเฟรชเซสชันได้",
|
||||
"collapseAll": "ยุบทั้งหมด",
|
||||
"expandAll": "ขยายทั้งหมด",
|
||||
"moreActions": "การดำเนินการเพิ่มเติม",
|
||||
"sessionStats": "สถิติเซสชั่น",
|
||||
"renameSessionTitle": "เปลี่ยนชื่อเซสชัน \"{{name}}\"",
|
||||
"rename": "เปลี่ยนชื่อ",
|
||||
"sessionRenamed": "เปลี่ยนชื่อเซสชันเป็น \"{{name}}\"",
|
||||
"sessionRenameFailed": "ไม่สามารถเปลี่ยนชื่อเซสชันได้",
|
||||
"editTagsTitle": "แก้ไขแท็กสำหรับ \"{{name}}\"",
|
||||
"killPane": "หน้าต่างปิดการทำงาน",
|
||||
"resizeTree": "ลากเพื่อปรับขนาด — ดับเบิ้ลคลิกเพื่อรีเซ็ต",
|
||||
"reattach": "เชื่อมต่อใหม่ (แก้ไขภาพที่แสดงผลผิดเพี้ยน)",
|
||||
"killWindow": "หน้าต่างสังหาร",
|
||||
"killWindowTitle": "ปิดหน้าต่าง {{index}} ของ \"{{session}}\"?",
|
||||
"killWindowBody": "หน้าต่างและกระบวนการทั้งหมดในหน้าต่างนี้จะถูกยุติ การปิดหน้าต่างสุดท้ายจะเป็นการสิ้นสุดเซสชัน",
|
||||
"windowKillFailed": "ไม่สามารถปิดหน้าต่างได้",
|
||||
"statusActivity": "กิจกรรม",
|
||||
"statusWindows": "วินโดวส์",
|
||||
"statusPanes": "บานหน้าต่าง",
|
||||
"statusTags": "แท็ก",
|
||||
"killPaneTitle": "แผงปิดการทำงาน {{id}}?",
|
||||
"killPaneBody": "กระบวนการที่กำลังทำงานอยู่ในบานหน้าต่างนี้จะถูกยุติ การยุติบานหน้าต่างสุดท้ายจะปิดหน้าต่างนั้นลง",
|
||||
"paneKillFailed": "ไม่สามารถปิดหน้าต่างได้",
|
||||
"killSessionTitle": "เซสชั่นสังหาร \"{{name}}\"?",
|
||||
"killSessionBody": "หน้าต่างและกระบวนการทำงานทั้งหมดในเซสชันนี้จะถูกปิดลง การดำเนินการนี้ไม่สามารถย้อนกลับได้",
|
||||
"kill": "ฆ่า",
|
||||
"sessionKilled": "เซสชั่น \"{{name}}\" ถูกฆ่า",
|
||||
"sessionKillFailed": "ไม่สามารถยุติเซสชันได้",
|
||||
"hostUnreachable": "ไม่สามารถเชื่อมต่อกับโฮสต์ได้ โปรดตรวจสอบว่าโฮสต์ออนไลน์และสามารถเข้าถึงได้",
|
||||
"noServer": "ไม่มีเซิร์ฟเวอร์ tmux ทำงานอยู่บนโฮสต์นี้",
|
||||
"searchTruncated": "ผลลัพธ์บางส่วน — การค้นหาครอบคลุมบรรทัดสุดท้าย {{lines}} บรรทัดของแต่ละบานหน้าต่าง และอย่างมากที่สุด {{panes}} บานหน้าต่าง",
|
||||
"closeSearchResults": "ปิดผลการค้นหา",
|
||||
"retry": "ลองใหม่อีกครั้ง",
|
||||
"noHosts": "ไม่มีโฮสต์ SSH ให้บริการ",
|
||||
"noHostsHint": "เปิดใช้งานตัวเลือก Tmux Monitor บนโฮสต์ SSH (Host Manager → แท็บ Terminal) เพื่อตรวจสอบเซสชัน tmux ของโฮสต์นั้น",
|
||||
"tmuxInstallHint": "ติดตั้งบนเครื่องโฮสต์ด้วยคำสั่ง:",
|
||||
"attachTooltip": "เปิดเทอร์มินัลไปที่ {{host}}",
|
||||
"attachTooltipPane": "เปิดเทอร์มินัลไปที่ {{host}} — เซสชัน tmux {{session}}",
|
||||
"timeJustNow": "เมื่อสักครู่นี้เอง",
|
||||
"timeMinutes": "{{count}}นาทีที่แล้ว",
|
||||
"timeHours": "{{count}}ชั่วโมงที่แล้ว",
|
||||
"timeDays": "{{count}}วันก่อน"
|
||||
},
|
||||
"serial": {
|
||||
"title": "ซีเรียล",
|
||||
"portLabel": "ท่าเรือ",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Sekmeyi Yeniden Adlandır",
|
||||
"openFileManager": "Dosya Yöneticisini Aç",
|
||||
"dashboard": "Dashboard",
|
||||
"tmuxMonitor": "Tmux İzleyicisi",
|
||||
"homepage": "Ana Sayfa",
|
||||
"quickConnect": "Hızlı Bağlantı",
|
||||
"sshTools": "SSH Araçları",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Maksimum Keepalive Kaçırma",
|
||||
"sameHost": "Bu ana bilgisayar (doğrudan tünel)",
|
||||
"bindHost": "Bağlanma Ana Bilgisayarı",
|
||||
"enableTmuxMonitor": "Tmux İzleyiciyi Etkinleştir",
|
||||
"enableTmuxMonitorDesc": "Bu ana bilgisayarı Tmux İzleyicide göster ve tmux eylemlerini kenar çubuğuna ekle",
|
||||
"connectionToolbar": "Bağlantı Araç Çubuğu",
|
||||
"showConnectionToolbar": "Bağlantı araç çubuğunu göster",
|
||||
"showConnectionToolbarDesc": "Bu sunucunun SSH, RDP, VNC ve Telnet oturumlarında araç çubuğunu gösterin. Ayrıca, araç çubuğundan geçici olarak gizleyebilirsiniz.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Tünel URL'si kopyalandı",
|
||||
"dockerUrlCopied": "Docker URL'si kopyalandı",
|
||||
"hostMetricsUrlCopied": "Ana makine metrikleri URL'si kopyalandı",
|
||||
"tmuxMonitorUrlCopied": "Tmux Monitor URL kopyalandı",
|
||||
"rdpUrlCopied": "RDP URL kopyalandı",
|
||||
"vncUrlCopied": "VNC URL kopyalandı",
|
||||
"telnetUrlCopied": "Telnet URL kopyalandı",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Tünel URL'sini Kopyala",
|
||||
"copyDockerUrlAction": "Docker URL'sini Kopyala",
|
||||
"copyHostMetricsUrlAction": "Sunucu Metrikleri URL'sini Kopyala",
|
||||
"copyTmuxMonitorUrlAction": "Tmux Monitor URL'sini Kopyala",
|
||||
"copyRdpUrlAction": "RDP URL'sini Kopyala",
|
||||
"copyVncUrlAction": "VNC URL'sini Kopyala",
|
||||
"copyTelnetUrlAction": "Telnet URL'sini Kopyala",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Proxmox İstatistiklerini Aç",
|
||||
"openTmuxMonitor": "Tmux Monitörünü Aç"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux İzleyici",
|
||||
"failedToLoadHosts": "Sunucular yüklenemedi",
|
||||
"failedToLoad": "Tmux oturumları yüklenemedi",
|
||||
"tmuxUnavailable": "Bu sunucuda tmux yüklü değil",
|
||||
"noSessions": "Bu sunucuda tmux oturumu yok",
|
||||
"noHostSelected": "Sunucu seçilmedi",
|
||||
"attached": "Bağlı",
|
||||
"detached": "Bağlantısız",
|
||||
"attach": "Bağlan",
|
||||
"editTags": "Etiketleri düzenle",
|
||||
"tagsHint": "Virgülle ayrılmış etiketler (örn. YOLO, lab, training)",
|
||||
"tagsSaved": "Etiketler kaydedildi",
|
||||
"tagsSaveFailed": "Etiketler kaydedilemedi",
|
||||
"searchPlaceholder": "Oturumlar arasında çıktı ara...",
|
||||
"searchResults": "{{count}} eşleşme",
|
||||
"searchFailed": "Arama başarısız",
|
||||
"selectPaneHint": "Çıktısını önizlemek için bir bölme seçin",
|
||||
"closePreview": "Önizlemeyi kapat",
|
||||
"newSession": "Yeni oturum",
|
||||
"newSessionHint": "Oturum adı (harfler, rakamlar, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "Oluştur",
|
||||
"sessionCreated": "Oturum \"{{name}}\" oluşturuldu",
|
||||
"sessionCreateFailed": "Oturum oluşturulamadı",
|
||||
"splitRight": "Sağa böl",
|
||||
"splitDown": "Aşağı böl",
|
||||
"splitFailed": "Bölme başarısız",
|
||||
"newWindow": "Yeni pencere",
|
||||
"windowCreateFailed": "Pencere oluşturulamadı",
|
||||
"attachSessionTooltip": "{{session}} oturumuna bağlan",
|
||||
"refresh": "Yenile",
|
||||
"refreshFailed": "Yenileme başarısız",
|
||||
"collapseAll": "Tümünü daralt",
|
||||
"expandAll": "Tümünü genişlet",
|
||||
"moreActions": "Diğer işlemler",
|
||||
"sessionStats": "Oturum istatistikleri",
|
||||
"renameSessionTitle": "Oturumu yeniden adlandır \"{{name}}\"",
|
||||
"rename": "Yeniden adlandır",
|
||||
"sessionRenamed": "Oturum \"{{name}}\" olarak yeniden adlandırıldı",
|
||||
"sessionRenameFailed": "Oturum yeniden adlandırılamadı",
|
||||
"editTagsTitle": "\"{{name}}\" için etiketleri düzenle",
|
||||
"killPane": "Paneyi sonlandır",
|
||||
"resizeTree": "Yeniden boyutlandırmak için sürükle — sıfırlamak için çift tıkla",
|
||||
"reattach": "Yeniden bağlan (bozuk görünümü düzeltir)",
|
||||
"killWindow": "Pencereyi sonlandır",
|
||||
"killWindowTitle": "\"{{session}}\" oturumundaki {{index}} numaralı pencere sonlandırılsın mı?",
|
||||
"killWindowBody": "Bu penceredeki tüm paneler ve işlemler sonlandırılacak. Son pencereyi sonlandırmak oturumu sona erdirir.",
|
||||
"windowKillFailed": "Pencere sonlandırılamadı",
|
||||
"statusActivity": "Etkinlik",
|
||||
"statusWindows": "Pencereler",
|
||||
"statusPanes": "Paneler",
|
||||
"statusTags": "Etiketler",
|
||||
"killPaneTitle": "{{id}} numaralı pane sonlandırılsın mı?",
|
||||
"killPaneBody": "Bu panede çalışan işlem sonlandırılacak. Son paneyi sonlandırmak penceresini kapatır.",
|
||||
"paneKillFailed": "Pane sonlandırılamadı",
|
||||
"killSessionTitle": "\"{{name}}\" oturumu sonlandırılsın mı?",
|
||||
"killSessionBody": "Bu oturumdaki tüm pencereler ve çalışan işlemler sonlandırılacak. Bu işlem geri alınamaz.",
|
||||
"kill": "Sonlandır",
|
||||
"sessionKilled": "\"{{name}}\" oturumu sonlandırıldı",
|
||||
"sessionKillFailed": "Oturum sonlandırılamadı",
|
||||
"hostUnreachable": "Ana bilgisayara bağlanılamadı. Çevrimiçi ve erişilebilir olduğunu kontrol edin.",
|
||||
"noServer": "Bu ana bilgisayarda tmux sunucusu çalışmıyor.",
|
||||
"searchTruncated": "Kısmi sonuçlar — arama her panenin son {{lines}} satırını ve en fazla {{panes}} paneyi kapsar.",
|
||||
"closeSearchResults": "Arama sonuçlarını kapat",
|
||||
"retry": "Yeniden dene",
|
||||
"noHosts": "Kullanılabilir SSH ana bilgisayarı yok",
|
||||
"noHostsHint": "Tmux oturumlarını izlemek için bir SSH ana bilgisayarında Tmux Monitor seçeneğini etkinleştirin (Ana Bilgisayar Yöneticisi → Terminal sekmesi).",
|
||||
"tmuxInstallHint": "Ana bilgisayara şu komutla yükleyin:",
|
||||
"attachTooltip": "{{host}} için bir terminal aç",
|
||||
"attachTooltipPane": "{{host}} için bir terminal aç — tmux oturumu {{session}}",
|
||||
"timeJustNow": "az önce",
|
||||
"timeMinutes": "{{count}} dakika önce",
|
||||
"timeHours": "{{count}} saat önce",
|
||||
"timeDays": "{{count}} gün önce"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seri",
|
||||
"portLabel": "Port",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Перейменувати вкладку",
|
||||
"openFileManager": "Відкрити файловий менеджер",
|
||||
"dashboard": "Інформаційна панель",
|
||||
"tmuxMonitor": "Монітор Tmux",
|
||||
"homepage": "Домашня сторінка",
|
||||
"quickConnect": "Швидке підключення",
|
||||
"sshTools": "SSH-інструменти",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Макс. промахи Keepalive",
|
||||
"sameHost": "Цей хост (прямий тунель)",
|
||||
"bindHost": "Прив’язати хост",
|
||||
"enableTmuxMonitor": "Увімкнути монітор Tmux",
|
||||
"enableTmuxMonitorDesc": "Показати цей хост у моніторі Tmux та додати його дії tmux до бічної панелі",
|
||||
"connectionToolbar": "Панель інструментів з’єднання",
|
||||
"showConnectionToolbar": "Показати панель інструментів з’єднань",
|
||||
"showConnectionToolbarDesc": "Показувати панель інструментів у сеансах SSH, RDP, VNC та Telnet цього хоста. Ви також можете тимчасово приховати її з самої панелі інструментів.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "URL-адресу тунелю скопійовано",
|
||||
"dockerUrlCopied": "URL-адресу Docker скопійовано",
|
||||
"hostMetricsUrlCopied": "URL-адресу показників хоста скопійовано",
|
||||
"tmuxMonitorUrlCopied": "URL-адресу монітора Tmux скопійовано",
|
||||
"rdpUrlCopied": "URL-адресу RDP скопійовано",
|
||||
"vncUrlCopied": "URL-адресу VNC скопійовано",
|
||||
"telnetUrlCopied": "URL-адресу Telnet скопійовано",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Копіювати URL-адресу тунелю",
|
||||
"copyDockerUrlAction": "Копіювати URL-адресу Docker",
|
||||
"copyHostMetricsUrlAction": "Копіювати URL-адресу показників хоста",
|
||||
"copyTmuxMonitorUrlAction": "Копіювати URL-адресу монітора Tmux",
|
||||
"copyRdpUrlAction": "Копіювати URL-адресу RDP",
|
||||
"copyVncUrlAction": "Копіювати URL-адресу VNC",
|
||||
"copyTelnetUrlAction": "Копіювати URL-адресу Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Відкрити статистику Proxmox",
|
||||
"openTmuxMonitor": "Відкрити монітор Tmux"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Монітор Tmux",
|
||||
"failedToLoadHosts": "Не вдалося завантажити хости",
|
||||
"failedToLoad": "Не вдалося завантажити сеанси tmux",
|
||||
"tmuxUnavailable": "tmux не встановлено на цьому хості",
|
||||
"noSessions": "На цьому хості немає сеансів tmux",
|
||||
"noHostSelected": "Хост не вибрано",
|
||||
"attached": "Додано",
|
||||
"detached": "Окремий",
|
||||
"attach": "Прикріпити",
|
||||
"editTags": "Редагувати теги",
|
||||
"tagsHint": "Теги, розділені комами (наприклад, YOLO, лабораторія, навчання)",
|
||||
"tagsSaved": "Теги збережено",
|
||||
"tagsSaveFailed": "Не вдалося зберегти теги",
|
||||
"searchPlaceholder": "Пошук результатів у сеансах...",
|
||||
"searchResults": "{{count}} збігів",
|
||||
"searchFailed": "Пошук не вдався",
|
||||
"selectPaneHint": "Виберіть панель для попереднього перегляду її виводу",
|
||||
"closePreview": "Закрити попередній перегляд",
|
||||
"newSession": "Новий сеанс",
|
||||
"newSessionHint": "Назва сеансу (літери, цифри, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "моя сесія",
|
||||
"create": "Створити",
|
||||
"sessionCreated": "Сеанс «{{name}}» створено",
|
||||
"sessionCreateFailed": "Не вдалося створити сеанс",
|
||||
"splitRight": "Розділити праворуч",
|
||||
"splitDown": "Розділити",
|
||||
"splitFailed": "Не вдалося розділити панель",
|
||||
"newWindow": "Нове вікно",
|
||||
"windowCreateFailed": "Не вдалося створити вікно",
|
||||
"attachSessionTooltip": "Прикріпити до {{session}}",
|
||||
"refresh": "Оновити",
|
||||
"refreshFailed": "Не вдалося оновити сеанси",
|
||||
"collapseAll": "Згорнути все",
|
||||
"expandAll": "Розгорнути все",
|
||||
"moreActions": "Більше дій",
|
||||
"sessionStats": "Статистика сеансу",
|
||||
"renameSessionTitle": "Перейменувати сеанс \"{{name}}\"",
|
||||
"rename": "Перейменувати",
|
||||
"sessionRenamed": "Сеанс перейменовано на «{{name}}»",
|
||||
"sessionRenameFailed": "Не вдалося перейменувати сеанс",
|
||||
"editTagsTitle": "Редагувати теги для \"{{name}}\"",
|
||||
"killPane": "Панель \"Вийти\"",
|
||||
"resizeTree": "Перетягніть, щоб змінити розмір — двічі клацніть, щоб скинути налаштування",
|
||||
"reattach": "Повторно прикріпити (виправляє спотворене зображення)",
|
||||
"killWindow": "Вікно для знищення",
|
||||
"killWindowTitle": "Закрити вікно {{index}} з \"{{session}}\"?",
|
||||
"killWindowBody": "Кожна панель і процес у цьому вікні будуть завершені. Закриття останнього вікна завершує сеанс.",
|
||||
"windowKillFailed": "Не вдалося завершити роботу вікна",
|
||||
"statusActivity": "Активність",
|
||||
"statusWindows": "Вікна",
|
||||
"statusPanes": "панелі",
|
||||
"statusTags": "Теги",
|
||||
"killPaneTitle": "Закрити панель {{id}}?",
|
||||
"killPaneBody": "Процес, що виконується в цій панелі, буде завершено. Закриття останньої панелі закриває її вікно.",
|
||||
"paneKillFailed": "Не вдалося завершити роботу панелі",
|
||||
"killSessionTitle": "Закінчити сеанс \"{{name}}\"?",
|
||||
"killSessionBody": "Усі вікна та запущені процеси в цьому сеансі будуть завершені. Цю дію неможливо скасувати.",
|
||||
"kill": "Вбити",
|
||||
"sessionKilled": "Сеанс «{{name}}» завершено",
|
||||
"sessionKillFailed": "Не вдалося завершити сеанс",
|
||||
"hostUnreachable": "Не вдалося підключитися до хоста. Перевірте, чи він онлайн і доступний.",
|
||||
"noServer": "На цьому хості не працює жоден сервер tmux.",
|
||||
"searchTruncated": "Часткові результати — пошук охоплює останні {{lines}} рядків кожної панелі та щонайбільше {{panes}} панелей.",
|
||||
"closeSearchResults": "Закрити результати пошуку",
|
||||
"retry": "Повторити спробу",
|
||||
"noHosts": "Немає доступних SSH-хостів",
|
||||
"noHostsHint": "Увімкніть опцію моніторингу Tmux на хості SSH (Менеджер хостів → вкладка Термінал), щоб контролювати його сеанси tmux.",
|
||||
"tmuxInstallHint": "Встановіть його на хост за допомогою:",
|
||||
"attachTooltip": "Відкрийте термінал для {{host}}",
|
||||
"attachTooltipPane": "Відкрийте термінал для {{host}} — сеанс tmux {{session}}",
|
||||
"timeJustNow": "щойно",
|
||||
"timeMinutes": "{{count}}хв тому",
|
||||
"timeHours": "{{count}}год тому",
|
||||
"timeDays": "{{count}}днів тому"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Серійний",
|
||||
"portLabel": "Порт",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "Đổi tên tab",
|
||||
"openFileManager": "Mở quản lý tệp",
|
||||
"dashboard": "Bảng điều khiển",
|
||||
"tmuxMonitor": "Giám sát Tmux",
|
||||
"homepage": "Trang chủ",
|
||||
"quickConnect": "Kết nối nhanh",
|
||||
"sshTools": "Công cụ SSH",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "Số lần bỏ lỡ Keepalive tối đa",
|
||||
"sameHost": "Máy chủ này (đường hầm trực tiếp)",
|
||||
"bindHost": "Máy chủ bind",
|
||||
"enableTmuxMonitor": "Bật Tmux Monitor",
|
||||
"enableTmuxMonitorDesc": "Hiển thị máy chủ này trong Tmux Monitor và thêm các hành động tmux của nó vào thanh bên",
|
||||
"connectionToolbar": "Thanh công cụ kết nối",
|
||||
"showConnectionToolbar": "Hiển thị thanh công cụ kết nối",
|
||||
"showConnectionToolbarDesc": "Hiển thị thanh công cụ trong các phiên SSH, RDP, VNC và Telnet của máy chủ này. Bạn cũng có thể tạm thời ẩn nó từ chính thanh công cụ.",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "Đã sao chép URL Đường hầm",
|
||||
"dockerUrlCopied": "Đã sao chép URL Docker",
|
||||
"hostMetricsUrlCopied": "Đã sao chép URL Chỉ số máy chủ",
|
||||
"tmuxMonitorUrlCopied": "Đã sao chép URL Tmux Monitor",
|
||||
"rdpUrlCopied": "Đã sao chép URL RDP",
|
||||
"vncUrlCopied": "Đã sao chép URL VNC",
|
||||
"telnetUrlCopied": "Đã sao chép URL Telnet",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "Sao chép URL Tunnel",
|
||||
"copyDockerUrlAction": "Sao chép URL Docker",
|
||||
"copyHostMetricsUrlAction": "Sao chép URL Host Metrics",
|
||||
"copyTmuxMonitorUrlAction": "Sao chép URL Tmux Monitor",
|
||||
"copyRdpUrlAction": "Sao chép URL RDP",
|
||||
"copyVncUrlAction": "Sao chép URL VNC",
|
||||
"copyTelnetUrlAction": "Sao chép URL Telnet",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "Mở thống kê Proxmox",
|
||||
"openTmuxMonitor": "Mở Tmux Monitor"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Giám sát Tmux",
|
||||
"failedToLoadHosts": "Tải danh sách máy chủ thất bại",
|
||||
"failedToLoad": "Tải phiên tmux thất bại",
|
||||
"tmuxUnavailable": "tmux chưa được cài đặt trên máy chủ này",
|
||||
"noSessions": "Không có phiên tmux nào trên máy chủ này",
|
||||
"noHostSelected": "Chưa chọn máy chủ",
|
||||
"attached": "Đã đính kèm",
|
||||
"detached": "Đã tách rời",
|
||||
"attach": "Đính kèm",
|
||||
"editTags": "Sửa nhãn",
|
||||
"tagsHint": "Nhãn phân cách bằng dấu phẩy (ví dụ: YOLO, lab, training)",
|
||||
"tagsSaved": "Nhãn đã lưu",
|
||||
"tagsSaveFailed": "Lưu nhãn thất bại",
|
||||
"searchPlaceholder": "Tìm kiếm đầu ra qua các phiên...",
|
||||
"searchResults": "{{count}} kết quả khớp",
|
||||
"searchFailed": "Tìm kiếm thất bại",
|
||||
"selectPaneHint": "Chọn một pane để xem trước đầu ra",
|
||||
"closePreview": "Đóng xem trước",
|
||||
"newSession": "Phiên mới",
|
||||
"newSessionHint": "Tên phiên (chữ cái, chữ số, _ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "Tạo",
|
||||
"sessionCreated": "Đã tạo phiên \"{{name}}\"",
|
||||
"sessionCreateFailed": "Tạo phiên thất bại",
|
||||
"splitRight": "Chia sang phải",
|
||||
"splitDown": "Chia xuống dưới",
|
||||
"splitFailed": "Chia pane thất bại",
|
||||
"newWindow": "Cửa sổ mới",
|
||||
"windowCreateFailed": "Tạo cửa sổ thất bại",
|
||||
"attachSessionTooltip": "Đính kèm vào {{session}}",
|
||||
"refresh": "Làm mới",
|
||||
"refreshFailed": "Làm mới phiên thất bại",
|
||||
"collapseAll": "Thu gọn tất cả",
|
||||
"expandAll": "Mở rộng tất cả",
|
||||
"moreActions": "Thao tác khác",
|
||||
"sessionStats": "Thống kê phiên",
|
||||
"renameSessionTitle": "Đổi tên phiên \"{{name}}\"",
|
||||
"rename": "Đổi tên",
|
||||
"sessionRenamed": "Đã đổi tên phiên thành \"{{name}}\"",
|
||||
"sessionRenameFailed": "Đổi tên phiên thất bại",
|
||||
"editTagsTitle": "Sửa nhãn cho \"{{name}}\"",
|
||||
"killPane": "Hủy pane",
|
||||
"resizeTree": "Kéo để thay đổi kích thước — nhấn đúp để đặt lại",
|
||||
"reattach": "Gắn lại (sửa hiển thị bị rối)",
|
||||
"killWindow": "Hủy cửa sổ",
|
||||
"killWindowTitle": "Hủy cửa sổ {{index}} của \"{{session}}\"?",
|
||||
"killWindowBody": "Mọi pane và tiến trình trong cửa sổ này sẽ bị kết thúc. Hủy cửa sổ cuối cùng sẽ kết thúc phiên.",
|
||||
"windowKillFailed": "Không thể hủy cửa sổ",
|
||||
"statusActivity": "Hoạt động",
|
||||
"statusWindows": "Cửa sổ",
|
||||
"statusPanes": "Pane",
|
||||
"statusTags": "Thẻ",
|
||||
"killPaneTitle": "Hủy pane {{id}}?",
|
||||
"killPaneBody": "Tiến trình đang chạy trong pane này sẽ bị kết thúc. Hủy pane cuối cùng sẽ đóng cửa sổ của nó.",
|
||||
"paneKillFailed": "Không thể hủy pane",
|
||||
"killSessionTitle": "Hủy phiên \"{{name}}\"?",
|
||||
"killSessionBody": "Tất cả cửa sổ và tiến trình đang chạy trong phiên này sẽ bị kết thúc. Hành động này không thể hoàn tác.",
|
||||
"kill": "Hủy",
|
||||
"sessionKilled": "Phiên \"{{name}}\" đã bị hủy",
|
||||
"sessionKillFailed": "Không thể hủy phiên",
|
||||
"hostUnreachable": "Không thể kết nối đến máy chủ. Kiểm tra xem máy chủ có trực tuyến và có thể truy cập được không.",
|
||||
"noServer": "Không có máy chủ tmux nào đang chạy trên máy chủ này.",
|
||||
"searchTruncated": "Kết quả bị cắt bớt — tìm kiếm bao gồm {{lines}} dòng cuối của mỗi pane và tối đa {{panes}} pane.",
|
||||
"closeSearchResults": "Đóng kết quả tìm kiếm",
|
||||
"retry": "Thử lại",
|
||||
"noHosts": "Không có máy chủ SSH nào",
|
||||
"noHostsHint": "Bật tùy chọn Tmux Monitor trên máy chủ SSH (Host Manager → tab Terminal) để giám sát phiên tmux của nó.",
|
||||
"tmuxInstallHint": "Cài đặt nó trên máy chủ bằng lệnh:",
|
||||
"attachTooltip": "Mở terminal đến {{host}}",
|
||||
"attachTooltipPane": "Mở terminal đến {{host}} — phiên tmux {{session}}",
|
||||
"timeJustNow": "vừa xong",
|
||||
"timeMinutes": "{{count}} phút trước",
|
||||
"timeHours": "{{count}} giờ trước",
|
||||
"timeDays": "{{count}} ngày trước"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Cổng nối tiếp",
|
||||
"portLabel": "Cổng",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "重命名标签页",
|
||||
"openFileManager": "打开文件管理器",
|
||||
"dashboard": "仪表盘",
|
||||
"tmuxMonitor": "Tmux 监视器",
|
||||
"homepage": "首页",
|
||||
"quickConnect": "快速连接",
|
||||
"sshTools": "SSH 工具",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "最大保活失败次数",
|
||||
"sameHost": "此主机(直接隧道)",
|
||||
"bindHost": "绑定主机",
|
||||
"enableTmuxMonitor": "启用 Tmux 监控",
|
||||
"enableTmuxMonitorDesc": "在 Tmux 监控中显示此主机,并将其 tmux 操作添加到侧边栏",
|
||||
"connectionToolbar": "连接工具栏",
|
||||
"showConnectionToolbar": "显示连接工具栏",
|
||||
"showConnectionToolbarDesc": "在主机的 SSH、RDP、VNC 和 Telnet 会话中显示工具栏。您也可以暂时将其从工具栏本身隐藏。",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "隧道 URL 已复制",
|
||||
"dockerUrlCopied": "Docker URL 已复制",
|
||||
"hostMetricsUrlCopied": "主机监控 URL 已复制",
|
||||
"tmuxMonitorUrlCopied": "Tmux 监视器 URL 已复制",
|
||||
"rdpUrlCopied": "RDP URL 已复制",
|
||||
"vncUrlCopied": "VNC URL 已复制",
|
||||
"telnetUrlCopied": "Telnet URL 已复制",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "复制隧道 URL",
|
||||
"copyDockerUrlAction": "复制 Docker URL",
|
||||
"copyHostMetricsUrlAction": "复制主机监控 URL",
|
||||
"copyTmuxMonitorUrlAction": "复制 Tmux 监视器 URL",
|
||||
"copyRdpUrlAction": "复制 RDP URL",
|
||||
"copyVncUrlAction": "复制 VNC URL",
|
||||
"copyTelnetUrlAction": "复制 Telnet URL",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "打开 Proxmox 统计数据",
|
||||
"openTmuxMonitor": "打开 Tmux 监视器"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux 监视器",
|
||||
"failedToLoadHosts": "加载主机失败",
|
||||
"failedToLoad": "加载tmux会话失败",
|
||||
"tmuxUnavailable": "此主机未安装tmux",
|
||||
"noSessions": "此主机上没有tmux会话",
|
||||
"noHostSelected": "未选择主机",
|
||||
"attached": "已连接",
|
||||
"detached": "已断开",
|
||||
"attach": "连接",
|
||||
"editTags": "编辑标签",
|
||||
"tagsHint": "逗号分隔的标签(例如:YOLO, lab, training)",
|
||||
"tagsSaved": "标签已保存",
|
||||
"tagsSaveFailed": "标签保存失败",
|
||||
"searchPlaceholder": "跨会话搜索输出...",
|
||||
"searchResults": "{{count}} 个匹配",
|
||||
"searchFailed": "搜索失败",
|
||||
"selectPaneHint": "选择一个窗格以预览其输出",
|
||||
"closePreview": "关闭预览",
|
||||
"newSession": "新建会话",
|
||||
"newSessionHint": "会话名称(字母、数字、_ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "创建",
|
||||
"sessionCreated": "会话 \"{{name}}\" 已创建",
|
||||
"sessionCreateFailed": "创建会话失败",
|
||||
"splitRight": "向右拆分",
|
||||
"splitDown": "向下拆分",
|
||||
"splitFailed": "拆分窗格失败",
|
||||
"newWindow": "新建窗口",
|
||||
"windowCreateFailed": "创建窗口失败",
|
||||
"attachSessionTooltip": "附加到 {{session}}",
|
||||
"refresh": "刷新",
|
||||
"refreshFailed": "刷新会话失败",
|
||||
"collapseAll": "全部折叠",
|
||||
"expandAll": "全部展开",
|
||||
"moreActions": "更多操作",
|
||||
"sessionStats": "会话统计",
|
||||
"renameSessionTitle": "重命名会话 \"{{name}}\"",
|
||||
"rename": "重命名",
|
||||
"sessionRenamed": "会话已重命名为 \"{{name}}\"",
|
||||
"sessionRenameFailed": "重命名会话失败",
|
||||
"editTagsTitle": "编辑 \"{{name}}\" 的标签",
|
||||
"killPane": "终止窗格",
|
||||
"resizeTree": "拖动调整大小 — 双击重置",
|
||||
"reattach": "重新附加(修复显示错乱)",
|
||||
"killWindow": "终止窗口",
|
||||
"killWindowTitle": "终止会话 \"{{session}}\" 的窗口 {{index}}?",
|
||||
"killWindowBody": "该窗口中的所有窗格和进程都将被终止。终止最后一个窗口将结束会话。",
|
||||
"windowKillFailed": "终止窗口失败",
|
||||
"statusActivity": "活动",
|
||||
"statusWindows": "窗口",
|
||||
"statusPanes": "窗格",
|
||||
"statusTags": "标签",
|
||||
"killPaneTitle": "终止窗格 {{id}}?",
|
||||
"killPaneBody": "该窗格中运行的进程将被终止。终止最后一个窗格将关闭其窗口。",
|
||||
"paneKillFailed": "终止窗格失败",
|
||||
"killSessionTitle": "终止会话 \"{{name}}\"?",
|
||||
"killSessionBody": "该会话中的所有窗口和运行中的进程都将被终止。此操作无法撤销。",
|
||||
"kill": "终止",
|
||||
"sessionKilled": "会话 \"{{name}}\" 已终止",
|
||||
"sessionKillFailed": "终止会话失败",
|
||||
"hostUnreachable": "无法连接到主机。请检查主机是否在线且可访问。",
|
||||
"noServer": "此主机上未运行 tmux 服务器。",
|
||||
"searchTruncated": "部分结果 — 搜索覆盖每个窗格的最近 {{lines}} 行,最多 {{panes}} 个窗格。",
|
||||
"closeSearchResults": "关闭搜索结果",
|
||||
"retry": "重试",
|
||||
"noHosts": "没有可用的 SSH 主机",
|
||||
"noHostsHint": "在 SSH 主机上启用 Tmux Monitor 选项(主机管理器 → 终端标签页)以监控其 tmux 会话。",
|
||||
"tmuxInstallHint": "使用以下命令在主机上安装:",
|
||||
"attachTooltip": "打开到 {{host}} 的终端",
|
||||
"attachTooltipPane": "打开到 {{host}} 的终端 — tmux 会话 {{session}}",
|
||||
"timeJustNow": "刚刚",
|
||||
"timeMinutes": "{{count}} 分钟前",
|
||||
"timeHours": "{{count}} 小时前",
|
||||
"timeDays": "{{count}} 天前"
|
||||
},
|
||||
"serial": {
|
||||
"title": "串口",
|
||||
"portLabel": "端口",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"renameTab": "重新命名分頁",
|
||||
"openFileManager": "開啟檔案管理員",
|
||||
"dashboard": "儀表板",
|
||||
"tmuxMonitor": "Tmux 監控器",
|
||||
"homepage": "首頁",
|
||||
"quickConnect": "快速連線",
|
||||
"sshTools": "SSH 工具",
|
||||
@@ -1067,8 +1066,6 @@
|
||||
"maxKeepaliveMisses": "最大保持連線遺失次數",
|
||||
"sameHost": "此主機(直接通道)",
|
||||
"bindHost": "綁定主機",
|
||||
"enableTmuxMonitor": "啟用 Tmux 監控器",
|
||||
"enableTmuxMonitorDesc": "在 Tmux 監控器中顯示此主機,並將其 tmux 操作新增至側邊欄",
|
||||
"connectionToolbar": "連接工具列",
|
||||
"showConnectionToolbar": "顯示連接工具列",
|
||||
"showConnectionToolbarDesc": "在主機的 SSH、RDP、VNC 和 Telnet 會話中顯示工具列。您也可以暫時將其從工具列本身隱藏。",
|
||||
@@ -1241,7 +1238,6 @@
|
||||
"tunnelUrlCopied": "通道 URL 已複製",
|
||||
"dockerUrlCopied": "Docker URL 已複製",
|
||||
"hostMetricsUrlCopied": "主機指標 URL 已複製",
|
||||
"tmuxMonitorUrlCopied": "Tmux 監控 URL 已複製",
|
||||
"rdpUrlCopied": "RDP URL 已複製",
|
||||
"vncUrlCopied": "VNC URL 已複製",
|
||||
"telnetUrlCopied": "Telnet URL 已複製",
|
||||
@@ -1258,7 +1254,6 @@
|
||||
"copyTunnelUrlAction": "複製通道 URL",
|
||||
"copyDockerUrlAction": "複製 Docker URL",
|
||||
"copyHostMetricsUrlAction": "複製主機指標 URL",
|
||||
"copyTmuxMonitorUrlAction": "複製 Tmux 監控 URL",
|
||||
"copyRdpUrlAction": "複製 RDP URL",
|
||||
"copyVncUrlAction": "複製 VNC URL",
|
||||
"copyTelnetUrlAction": "複製 Telnet URL",
|
||||
@@ -3060,82 +3055,6 @@
|
||||
"openProxmoxStats": "開啟 Proxmox 統計數據",
|
||||
"openTmuxMonitor": "打開 Tmux Monitor"
|
||||
},
|
||||
"tmuxMonitor": {
|
||||
"title": "Tmux 監視器",
|
||||
"failedToLoadHosts": "載入主機失敗",
|
||||
"failedToLoad": "載入 tmux 工作階段失敗",
|
||||
"tmuxUnavailable": "此主機未安裝 tmux",
|
||||
"noSessions": "此主機上沒有 tmux 工作階段",
|
||||
"noHostSelected": "未選取主機",
|
||||
"attached": "已附加",
|
||||
"detached": "已分離",
|
||||
"attach": "附加",
|
||||
"editTags": "編輯標籤",
|
||||
"tagsHint": "以逗號分隔的標籤(例如:YOLO, lab, training)",
|
||||
"tagsSaved": "標籤已儲存",
|
||||
"tagsSaveFailed": "標籤儲存失敗",
|
||||
"searchPlaceholder": "在所有會話中搜尋輸出...",
|
||||
"searchResults": "{{count}} 個符合項",
|
||||
"searchFailed": "搜尋失敗",
|
||||
"selectPaneHint": "選取一個窗格以預覽其輸出",
|
||||
"closePreview": "關閉預覽",
|
||||
"newSession": "新會話",
|
||||
"newSessionHint": "會話名稱(字母、數字、_ @ % + = -)",
|
||||
"newSessionPlaceholder": "my-session",
|
||||
"create": "建立",
|
||||
"sessionCreated": "會話「{{name}}」已建立",
|
||||
"sessionCreateFailed": "建立會話失敗",
|
||||
"splitRight": "向右分割",
|
||||
"splitDown": "向下分割",
|
||||
"splitFailed": "分割窗格失敗",
|
||||
"newWindow": "新視窗",
|
||||
"windowCreateFailed": "建立視窗失敗",
|
||||
"attachSessionTooltip": "附加至 {{session}}",
|
||||
"refresh": "重新整理",
|
||||
"refreshFailed": "重新整理會話失敗",
|
||||
"collapseAll": "全部收合",
|
||||
"expandAll": "全部展開",
|
||||
"moreActions": "更多動作",
|
||||
"sessionStats": "會話統計",
|
||||
"renameSessionTitle": "重新命名會話「{{name}}」",
|
||||
"rename": "重新命名",
|
||||
"sessionRenamed": "會話已重新命名為「{{name}}」",
|
||||
"sessionRenameFailed": "重新命名會話失敗",
|
||||
"editTagsTitle": "編輯「{{name}}」的標籤",
|
||||
"killPane": "終止窗格",
|
||||
"resizeTree": "拖曳調整大小 — 雙擊重設",
|
||||
"reattach": "重新附加(修復亂碼畫面)",
|
||||
"killWindow": "終止視窗",
|
||||
"killWindowTitle": "終止「{{session}}」的視窗 {{index}}?",
|
||||
"killWindowBody": "此視窗中的所有窗格和程序將被終止。終止最後一個視窗會結束該工作階段。",
|
||||
"windowKillFailed": "無法終止視窗",
|
||||
"statusActivity": "活動",
|
||||
"statusWindows": "視窗",
|
||||
"statusPanes": "窗格",
|
||||
"statusTags": "標籤",
|
||||
"killPaneTitle": "終止窗格 {{id}}?",
|
||||
"killPaneBody": "在此窗格中執行的程序將被終止。終止最後一個窗格會關閉其視窗。",
|
||||
"paneKillFailed": "無法終止窗格",
|
||||
"killSessionTitle": "終止工作階段「{{name}}」?",
|
||||
"killSessionBody": "此工作階段中的所有視窗和執行中的程序將被終止。此操作無法復原。",
|
||||
"kill": "終止",
|
||||
"sessionKilled": "工作階段「{{name}}」已終止",
|
||||
"sessionKillFailed": "無法終止工作階段",
|
||||
"hostUnreachable": "無法連線至主機。請檢查主機是否在線上且可連線。",
|
||||
"noServer": "此主機上沒有正在執行的 tmux 伺服器。",
|
||||
"searchTruncated": "部分結果 — 搜尋範圍涵蓋每個窗格的最後 {{lines}} 行,且最多 {{panes}} 個窗格。",
|
||||
"closeSearchResults": "關閉搜尋結果",
|
||||
"retry": "重試",
|
||||
"noHosts": "沒有可用的 SSH 主機",
|
||||
"noHostsHint": "在 SSH 主機上啟用 Tmux 監控選項(主機管理員 → 終端機頁籤),以監控其 tmux 工作階段。",
|
||||
"tmuxInstallHint": "在主機上使用以下指令安裝:",
|
||||
"attachTooltip": "開啟連至 {{host}} 的終端機",
|
||||
"attachTooltipPane": "開啟連至 {{host}} 的終端機 — tmux 工作階段 {{session}}",
|
||||
"timeJustNow": "剛剛",
|
||||
"timeMinutes": "{{count}} 分鐘前",
|
||||
"timeHours": "{{count}} 小時前",
|
||||
"timeDays": "{{count}} 天前"
|
||||
},
|
||||
"serial": {
|
||||
"title": "序列",
|
||||
"portLabel": "連接埠",
|
||||
|
||||
@@ -24,9 +24,12 @@ export * from "@/components/dropdown-menu";
|
||||
export * from "@/components/input";
|
||||
export * from "@/components/label";
|
||||
export * from "@/components/password-input";
|
||||
export * from "@/components/popover";
|
||||
export * from "@/components/scroll-area";
|
||||
export * from "@/components/select";
|
||||
export * from "@/components/select2";
|
||||
export * from "@/components/separator";
|
||||
export * from "@/components/skeleton";
|
||||
export * from "@/components/switch";
|
||||
export * from "@/components/textarea";
|
||||
export * from "@/components/tooltip";
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
Search,
|
||||
User,
|
||||
KeyRound,
|
||||
Layers, // --- tmux-monitor ---
|
||||
Clock,
|
||||
Folder,
|
||||
Pencil,
|
||||
@@ -57,26 +56,6 @@ interface CommandPaletteProps {
|
||||
onOpenPanel?: (view: string) => void;
|
||||
}
|
||||
|
||||
/** Core per-host tools. Plugins add theirs as host actions. */
|
||||
function getSshActions(host: Host): {
|
||||
type: TabType;
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
}[] {
|
||||
return [
|
||||
// --- tmux-monitor --- opt-in per host, off by default
|
||||
host.enableTmuxMonitor && {
|
||||
type: "tmux_monitor",
|
||||
icon: Layers,
|
||||
label: "Tmux Monitor",
|
||||
},
|
||||
].filter(Boolean) as {
|
||||
type: TabType;
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function CommandPalette({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
@@ -535,24 +514,6 @@ export function CommandPalette({
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{host.enableSsh &&
|
||||
getSshActions(host).map(
|
||||
({ type, icon: Icon, label }) => (
|
||||
<button
|
||||
key={type}
|
||||
title={label}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAction(() =>
|
||||
onOpenTab(type, host.name),
|
||||
);
|
||||
}}
|
||||
className="flex items-center justify-center size-7 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted-foreground/10 transition-colors"
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
{hostActionsFor(hostActions, host).map((action) => {
|
||||
const Icon = action.icon;
|
||||
const label =
|
||||
|
||||
@@ -146,9 +146,12 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
|
||||
const addTab = useCallback(
|
||||
(tabData: Omit<Tab, "id">): number => {
|
||||
// --- tmux-monitor --- (tmux_monitor is a singleton tab like ssh_manager:
|
||||
// re-opening focuses the existing tab instead of adding a duplicate)
|
||||
if (tabData.type === "ssh_manager" || tabData.type === "tmux_monitor") {
|
||||
// A singleton tab: re-opening focuses the existing tab instead of
|
||||
// adding a duplicate.
|
||||
if (
|
||||
tabData.type === "ssh_manager" ||
|
||||
getTabType(tabData.type)?.singleton
|
||||
) {
|
||||
const existingTab = tabs.find((t) => t.type === tabData.type);
|
||||
if (existingTab) {
|
||||
setTabs((prev) =>
|
||||
@@ -179,11 +182,12 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
const isCommandTarget = !!getTabType(tabData.type)?.commandTarget;
|
||||
const needsUniqueTitle =
|
||||
isCommandTarget || tabData.type === "file_manager";
|
||||
const singletonTitleKey = getTabType(tabData.type)?.singleton
|
||||
? getTabType(tabData.type)?.titleKey
|
||||
: undefined;
|
||||
const effectiveTitle = needsUniqueTitle
|
||||
? computeUniqueTitle(tabData.type, tabData.title)
|
||||
: tabData.type === "tmux_monitor" // --- tmux-monitor ---
|
||||
? tabData.title || t("nav.tmuxMonitor")
|
||||
: tabData.title || "";
|
||||
: tabData.title || (singletonTitleKey ? t(singletonTitleKey) : "");
|
||||
const newTab: Tab = {
|
||||
...tabData,
|
||||
id,
|
||||
@@ -204,7 +208,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
setAllSplitScreenTab((prev) => prev.filter((tid) => tid !== id));
|
||||
return id;
|
||||
},
|
||||
[computeUniqueTitle, tabs, t], // --- tmux-monitor --- (added t)
|
||||
[computeUniqueTitle, tabs, t],
|
||||
);
|
||||
|
||||
const pendingCurrentTabRef = useRef<number | null>(null);
|
||||
|
||||
@@ -19,7 +19,6 @@ import { getTabType } from "./tab-registry";
|
||||
* not a meaningful part of an arrangement.
|
||||
*/
|
||||
const CORE_CAPTURABLE = new Set([
|
||||
"tmux_monitor",
|
||||
"serial",
|
||||
"homepage",
|
||||
"termix-id",
|
||||
@@ -30,7 +29,6 @@ const CORE_CAPTURABLE = new Set([
|
||||
|
||||
/** Core types reopened as singletons, with an optional preselected host. */
|
||||
const CORE_SINGLETON = new Set([
|
||||
"tmux_monitor",
|
||||
"homepage",
|
||||
"termix-id",
|
||||
"session-logs",
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Settings,
|
||||
Usb,
|
||||
User,
|
||||
Layers, // --- tmux-monitor ---
|
||||
Fingerprint,
|
||||
Hammer,
|
||||
ScrollText,
|
||||
@@ -37,11 +36,6 @@ const CollabRoomTab = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const loadTmuxMonitor = () =>
|
||||
import("@/features/tmux-monitor/TmuxMonitor").then((m) => ({
|
||||
default: m.TmuxMonitor,
|
||||
}));
|
||||
const TmuxMonitor = lazy(loadTmuxMonitor);
|
||||
const DashboardTab = lazy(() =>
|
||||
import("@/dashboard/DashboardTab").then((m) => ({
|
||||
default: m.DashboardTab,
|
||||
@@ -73,13 +67,9 @@ const MacrosPanel = lazy(() =>
|
||||
const SshToolsPanel = lazy(() =>
|
||||
import("@/sidebar/SshToolsPanel").then((m) => ({ default: m.SshToolsPanel })),
|
||||
);
|
||||
const tabSurfaceLoaders: Partial<Record<TabType, () => Promise<unknown>>> = {
|
||||
tmux_monitor: loadTmuxMonitor,
|
||||
};
|
||||
|
||||
/** Download a likely next tab without starting a connection or mounting UI. */
|
||||
export function preloadTabSurface(type: TabType): void {
|
||||
const loader = tabSurfaceLoaders[type] ?? getTabType(type)?.preload;
|
||||
const loader = getTabType(type)?.preload;
|
||||
if (loader) runAdaptiveBackgroundTask("module", `tab:${type}`, loader);
|
||||
}
|
||||
|
||||
@@ -144,9 +134,6 @@ export function tabIcon(type: TabType) {
|
||||
return <User className="size-3.5" />;
|
||||
case "admin-settings":
|
||||
return <Settings className="size-3.5" />;
|
||||
// --- tmux-monitor ---
|
||||
case "tmux_monitor":
|
||||
return <Layers className="size-3.5" />;
|
||||
case "serial":
|
||||
return <Usb className="size-3.5" />;
|
||||
case "homepage":
|
||||
@@ -239,7 +226,6 @@ export interface TabRenderContext {
|
||||
|
||||
export function renderTabContent(tab: Tab, context: TabRenderContext) {
|
||||
const { shell, isVisible = true, isFocusedPane = true, panelProps } = context;
|
||||
const { host } = tab;
|
||||
|
||||
switch (tab.type) {
|
||||
case "dashboard":
|
||||
@@ -251,15 +237,6 @@ export function renderTabContent(tab: Tab, context: TabRenderContext) {
|
||||
/>,
|
||||
);
|
||||
|
||||
// --- tmux-monitor ---
|
||||
case "tmux_monitor":
|
||||
return withTabSuspense(
|
||||
<TmuxMonitor
|
||||
initialHostId={host ? parseInt(host.id, 10) : undefined}
|
||||
isVisible={isVisible}
|
||||
/>,
|
||||
);
|
||||
|
||||
case "serial":
|
||||
if (!tab.serialConfig)
|
||||
return <EmptyState icon={Usb} messageKey="serial.notSupportedTitle" />;
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
import {
|
||||
Globe,
|
||||
Info,
|
||||
Layers, // --- tmux-monitor ---
|
||||
Palette,
|
||||
Pencil,
|
||||
Plus,
|
||||
@@ -2062,35 +2061,6 @@ export function HostEditor({
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
{/* --- tmux-monitor --- */}
|
||||
<SectionCard
|
||||
title={t("tmuxMonitor.title")}
|
||||
icon={<Layers className="size-3.5" />}
|
||||
>
|
||||
<div className="flex flex-col gap-4 py-3">
|
||||
<SettingRow
|
||||
label={t("hosts.enableTmuxMonitor")}
|
||||
description={
|
||||
<>
|
||||
{t("hosts.enableTmuxMonitorDesc")}{" "}
|
||||
<a
|
||||
href="https://docs.termix.site/features/terminal/tmux"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-accent-brand hover:underline"
|
||||
>
|
||||
{t("hosts.docsLink")}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={form.enableTmuxMonitor}
|
||||
onChange={(v) => setField("enableTmuxMonitor", v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
</div>
|
||||
</SectionCard>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
GripVertical,
|
||||
Key,
|
||||
KeyRound,
|
||||
Layers, // --- tmux-monitor ---
|
||||
Link,
|
||||
MemoryStick,
|
||||
MoreHorizontal,
|
||||
@@ -113,33 +112,6 @@ export function buildStatusTooltip(
|
||||
return `${protocols.join(", ")}: ${statusLabel}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core's own per-host tools. Ways to connect and plugin tools (terminal,
|
||||
* remote desktop, Docker, metrics) are host actions plugins register.
|
||||
*/
|
||||
export function getSshActions(host: Host): {
|
||||
type: TabType;
|
||||
icon: typeof Terminal;
|
||||
label: string;
|
||||
order: number;
|
||||
}[] {
|
||||
return [
|
||||
// --- tmux-monitor --- opt-in per host, off by default
|
||||
host.enableSsh &&
|
||||
host.enableTmuxMonitor && {
|
||||
type: "tmux_monitor" as TabType,
|
||||
icon: Layers,
|
||||
label: "Tmux Monitor",
|
||||
order: 70,
|
||||
},
|
||||
].filter(Boolean) as {
|
||||
type: TabType;
|
||||
icon: typeof Terminal;
|
||||
label: string;
|
||||
order: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
/** Plugin actions sort by order; connect actions default to the end. */
|
||||
function actionOrder(action: HostActionDef): number {
|
||||
return action.order ?? (action.kind === "connect" ? 100 : 50);
|
||||
@@ -411,13 +383,9 @@ export function HostItem({
|
||||
const trayButtonClass =
|
||||
"flex items-center justify-center size-[22.75px] text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors";
|
||||
|
||||
const sshActions = getSshActions(host);
|
||||
const availableActions: TabType[] = [
|
||||
...sshActions.map(({ type }) => type),
|
||||
...pluginActions.flatMap((action) =>
|
||||
action.tabType ? [action.tabType] : [],
|
||||
),
|
||||
];
|
||||
const availableActions: TabType[] = pluginActions.flatMap((action) =>
|
||||
action.tabType ? [action.tabType] : [],
|
||||
);
|
||||
// Empty when no running plugin can connect to this host.
|
||||
const defaultAction: TabType =
|
||||
defaultConnectAction(allHostActions, host)?.tabType ?? "";
|
||||
@@ -450,19 +418,8 @@ export function HostItem({
|
||||
else if (action.tabType) openHostTab(action.tabType);
|
||||
};
|
||||
|
||||
/** Core tools and plugin actions in one row, in order. */
|
||||
/** Plugin actions in row order. */
|
||||
const rowEntries = [
|
||||
...sshActions.map((action) => ({
|
||||
key: action.type,
|
||||
order: action.order,
|
||||
icon: action.icon,
|
||||
label: action.label,
|
||||
tabType: action.type as string | undefined,
|
||||
tray: true,
|
||||
items: undefined as
|
||||
{ id: string; label: string; run: () => void }[] | undefined,
|
||||
run: () => openHostTab(action.type),
|
||||
})),
|
||||
...pluginActions.map((action) => {
|
||||
const items = action.items?.(host);
|
||||
return {
|
||||
@@ -799,20 +756,6 @@ export function HostItem({
|
||||
{t("hosts.copyLink")}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
{host.enableSsh && host.enableTmuxMonitor && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
writeClipboardText(
|
||||
`${window.location.origin}?view=tmux_monitor&hostId=${host.id}`,
|
||||
);
|
||||
toast.success(t("hosts.tmuxMonitorUrlCopied"));
|
||||
}}
|
||||
>
|
||||
<Layers className="size-3.5 mr-2" />
|
||||
{t("hosts.copyTmuxMonitorUrlAction")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{pluginActions
|
||||
.filter((action) => action.copyUrlView)
|
||||
.map((action) => {
|
||||
@@ -1015,13 +958,12 @@ export function HostItem({
|
||||
// reachable. If the host only exposes a single action, just launch it.
|
||||
if (isTouchOnly) {
|
||||
e.stopPropagation();
|
||||
const actionCount = getSshActions(host).length;
|
||||
const otherProtocols = [
|
||||
host.enableRdp,
|
||||
host.enableVnc,
|
||||
host.enableTelnet,
|
||||
].filter(Boolean).length;
|
||||
if (actionCount + otherProtocols <= 1) {
|
||||
if (otherProtocols <= 1) {
|
||||
openHostTab(defaultAction);
|
||||
} else {
|
||||
onTrayOpenChange?.(!isTrayOpen);
|
||||
|
||||
@@ -60,7 +60,6 @@ const baseHost: Host = {
|
||||
enableTerminal: true,
|
||||
enableCommandHistory: true,
|
||||
enableTunnel: true,
|
||||
enableTmuxMonitor: true,
|
||||
enableFileManager: true,
|
||||
enableDocker: true,
|
||||
enableRdp: true,
|
||||
@@ -219,18 +218,31 @@ describe("HostItem density parity", () => {
|
||||
});
|
||||
|
||||
it("learns repeated local actions and preloads the preferred host tool", () => {
|
||||
renderHostItem("comfortable");
|
||||
const tmuxButton = screen.getByTitle("Tmux Monitor");
|
||||
fireEvent.click(tmuxButton);
|
||||
fireEvent.click(tmuxButton);
|
||||
fireEvent.click(tmuxButton);
|
||||
const Icon = (() => null) as never;
|
||||
const dispose = registerHostAction({
|
||||
id: "tmux_monitor",
|
||||
titleKey: "Tmux Monitor",
|
||||
icon: Icon,
|
||||
kind: "open",
|
||||
tabType: "tmux_monitor",
|
||||
when: () => true,
|
||||
});
|
||||
try {
|
||||
renderHostItem("comfortable");
|
||||
const tmuxButton = screen.getByTitle("Tmux Monitor");
|
||||
fireEvent.click(tmuxButton);
|
||||
fireEvent.click(tmuxButton);
|
||||
fireEvent.click(tmuxButton);
|
||||
|
||||
const hostRow = screen.getByText("web-01").closest(".cursor-pointer");
|
||||
expect(hostRow).toBeTruthy();
|
||||
fireEvent.pointerEnter(hostRow!);
|
||||
const hostRow = screen.getByText("web-01").closest(".cursor-pointer");
|
||||
expect(hostRow).toBeTruthy();
|
||||
fireEvent.pointerEnter(hostRow!);
|
||||
|
||||
// No plugin registered a connect action, so only the learned tool preloads.
|
||||
expect(preloadTabSurfaceMock).not.toHaveBeenCalledWith("");
|
||||
expect(preloadTabSurfaceMock).toHaveBeenCalledWith("tmux_monitor");
|
||||
// No plugin registered a connect action, so only the learned tool preloads.
|
||||
expect(preloadTabSurfaceMock).not.toHaveBeenCalledWith("");
|
||||
expect(preloadTabSurfaceMock).toHaveBeenCalledWith("tmux_monitor");
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user